From a60f4133b5abf0cd8cfeebf9068bee0288d6327f Mon Sep 17 00:00:00 2001 From: Sven Date: Thu, 9 Jul 2026 15:30:56 +0200 Subject: [PATCH] Implementing switching of beamline path with a case structure --- beamline_editor/editor_widget.py | 53 +++++++++++++++++++++++++++++-- beamline_editor/nodes.py | 54 +++++++++++++++++++++++++++++--- beamline_editor/scene.py | 17 +++++----- containers.py | 3 +- main.py | 14 ++++++++- protoliste.py | 1 - 6 files changed, 123 insertions(+), 19 deletions(-) diff --git a/beamline_editor/editor_widget.py b/beamline_editor/editor_widget.py index 6785ff3..6ca6ac0 100644 --- a/beamline_editor/editor_widget.py +++ b/beamline_editor/editor_widget.py @@ -378,13 +378,26 @@ class BeamlineEditorWidget(QWidget): nodes_data = [] for item in self.scene.items(): if isinstance(item, BeamlineNode): - nodes_data.append({ + nd = { "id": item.node_id, "element_type": item.element_type, "display_name": item.display_name(), "x": item.pos().x(), "y": item.pos().y(), - }) + } + # Persist collapse state for Start markers + if item.element_type == "Start": + nd["collapsed"] = item.node_id in self.scene._collapsed_starts + # Save relative offsets so the layout is restored correctly + # even if the file is loaded with the Start node in a new position. + # Offsets are stored as {downstream_node_id: [dx, dy]}. + offsets = self.scene._collapse_offsets.get(item.node_id, {}) + if offsets: + nd["collapse_offsets"] = { + nid: [pt.x(), pt.y()] + for nid, pt in offsets.items() + } + nodes_data.append(nd) conns_data = [] for item in self.scene.items(): @@ -565,4 +578,40 @@ class BeamlineEditorWidget(QWidget): conn = Connection(src_port, dst_port) self.scene.addItem(conn) + # ── Pass 3: restore collapse state for Start markers ────────────────── + # Must run after connections are built so _downstream_items() works. + for nd in payload.get("nodes", []): + if not nd.get("collapsed", False): + continue + canvas_id = remap.get(nd["id"], nd["id"]) + start_node = node_lookup.get(canvas_id) + if start_node is None: + continue + + # Restore the saved relative offsets (remapping downstream IDs too) + raw_offsets = nd.get("collapse_offsets", {}) + offsets: dict[str, QPointF] = {} + for file_nid, (dx, dy) in raw_offsets.items(): + canvas_nid = remap.get(file_nid, file_nid) + offsets[canvas_nid] = QPointF(dx, dy) + + # If no offsets were saved, compute them now from current positions + if not offsets: + start_pos = start_node.pos() + down_nodes, _ = self.scene._downstream_items(start_node) + for dn in down_nodes: + offsets[dn.node_id] = dn.pos() - start_pos + + self.scene._collapse_offsets[canvas_id] = offsets + + # Apply the collapse (hide nodes/wires, update label) + self.scene._collapsed_starts.add(canvas_id) + down_nodes, down_conns = self.scene._downstream_items(start_node) + for n in down_nodes: + n.setVisible(False) + for c in down_conns: + c.setVisible(False) + if hasattr(start_node, "set_collapsed_label_visible"): + start_node.set_collapsed_label_visible(True) + return remap diff --git a/beamline_editor/nodes.py b/beamline_editor/nodes.py index bfe1a8d..d9de588 100644 --- a/beamline_editor/nodes.py +++ b/beamline_editor/nodes.py @@ -447,16 +447,60 @@ class AlignmentNode(BeamlineNode): p.drawLine(QPointF(cx, cy), QPointF(cx, cy + dy)) class StartNode(BeamlineNode): - """Start marker: no in-port, 1 out.""" - def __init__(self, pos): super().__init__("Start", pos) + """ + Start marker: no in-port, 1 out. + Arrow-head sits flush against the right edge (near the out-port). + A second label row shows "(collapsed)" when collapsed. + """ + def __init__(self, pos): + super().__init__("Start", pos) + # Second-row label for collapsed state — hidden by default + self._collapsed_label = QGraphicsTextItem("(collapsed)", self) + self._collapsed_label.setDefaultTextColor(QColor("#A5D6A7")) + self._collapsed_label.setFont(QFont("Segoe UI", 6)) + self._collapsed_label.setZValue(6) + self._collapsed_label.setVisible(False) + self._layout_labels() + + def _layout_labels(self): + """Stack name and (collapsed) label vertically, centred in the node.""" + br1 = self._label.boundingRect() + br2 = self._collapsed_label.boundingRect() + if self._collapsed_label.isVisible(): + gap = 1 + total = br1.height() + gap + br2.height() + y1 = (self.H - total) / 2 + y2 = y1 + br1.height() + gap + else: + y1 = (self.H - br1.height()) / 2 + y2 = y1 + self._label.setPos((self.W - br1.width()) / 2, y1) + self._collapsed_label.setPos((self.W - br2.width()) / 2, y2) + + def set_display_name(self, name: str): + """Override to re-run the two-row layout after the name changes.""" + super().set_display_name(name) + self._layout_labels() + + def set_collapsed_label_visible(self, visible: bool): + self._collapsed_label.setVisible(visible) + self._layout_labels() + self.update() + def _draw_body(self, p): p.setBrush(QBrush(self._gradient())) p.setPen(QPen(QColor("#00897B"), 1.5)) p.drawRoundedRect(QRectF(0, 0, self.W, self.H), 6, 6) - cx, cy = self.W / 2, self.H / 2 + # Arrow-head at the right edge, pointing right toward the out-port + cy = self.H / 2 + tip_x = self.W - 4 # flush with right edge + base_x = tip_x - 12 + half_h = 8 pts = QPolygonF([ - QPointF(cx - 10, cy - 10), QPointF(cx + 4, cy), - QPointF(cx - 10, cy + 10), QPointF(cx - 6, cy), + QPointF(base_x, cy - half_h), + QPointF(tip_x, cy), + QPointF(base_x, cy + half_h), + QPointF(base_x + 4, cy), ]) p.setBrush(QBrush(QColor("#A5D6A7"))) p.setPen(Qt.NoPen) diff --git a/beamline_editor/scene.py b/beamline_editor/scene.py index 218c071..7e50ae1 100644 --- a/beamline_editor/scene.py +++ b/beamline_editor/scene.py @@ -332,10 +332,9 @@ class BeamlineScene(QGraphicsScene): for c in conns: c.setVisible(True) c.update_path() - # revert label (strip suffix added at collapse time) - start_node.set_display_name( - start_node.display_name().replace(" (collapsed)", "") - ) + # hide second-row collapsed label + if hasattr(start_node, "set_collapsed_label_visible"): + start_node.set_collapsed_label_visible(False) self.status_changed.emit( f"Uncollapsed downstream of {start_node.display_name()}" ) @@ -350,10 +349,12 @@ class BeamlineScene(QGraphicsScene): self._collapse_offsets[start_node.node_id] = offsets for c in conns: c.setVisible(False) - name = start_node.display_name() - if "(collapsed)" not in name: - start_node.set_display_name(name + " (collapsed)") - self.status_changed.emit(f"Collapsed downstream of {name}") + # show second-row "(collapsed)" label + if hasattr(start_node, "set_collapsed_label_visible"): + start_node.set_collapsed_label_visible(True) + self.status_changed.emit( + f"Collapsed downstream of {start_node.display_name()}" + ) def _downstream_items( self, diff --git a/containers.py b/containers.py index 6ca426b..beb5426 100644 --- a/containers.py +++ b/containers.py @@ -171,7 +171,6 @@ class LineContainer: seq += ele.writeLattice(app) elif isinstance(ele, VariableContainer): dL = ele.getResLength() - print('Adding Drift:',dL) app.writeDrift(dL) Last +=dL else: @@ -183,7 +182,7 @@ class LineContainer: # if name<>0: # not valid in python3.x if name != 0: seq.append(ele) - print('Extend Cell',self.getResLength(), Last) + #print('Extend Cell',self.getResLength(), Last) app.writeDrift(self.getResLength() - Last) app.writeLine(self,seq) # indicate the app that a sequence is done diff --git a/main.py b/main.py index a616e64..44b0a9d 100644 --- a/main.py +++ b/main.py @@ -46,6 +46,7 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): self.elementDB={} self.savefile='' self.lines={} + self.branchingID=[] self.PL=ProtoListe(0) self.loadFile('Layouts/SFTest2.json') @@ -205,8 +206,19 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): return + def findBranchingDipoles(self): + self.branchingID.clear() + for key in self.elementDB.keys(): + ele = self.elementDB[key] + if ele.branching: + self.branchingID.append(key) + print('Branching elements') + print(self.branchingID) + def flatten(self, node_id, case_num=1 ): - print('Flatten case:', case_num) + print('Flatten beamline for case:', case_num) + self.findBranchingDipoles() + self.lines.clear() type = self.editor.getElementType(node_id) name='XXX' diff --git a/protoliste.py b/protoliste.py index 0e95187..89a0e10 100644 --- a/protoliste.py +++ b/protoliste.py @@ -223,7 +223,6 @@ class ProtoListe(ApplicationTemplate): def writeLine(self,ele,seq): if not ele.Name in self.info.keys() and len(ele.Name) > 1: - print('Empty Cell encountered:', ele.Name) domain = self.getDomain(ele.Name) align = 'Entrance' angh = np.arctan2(self.ev[0], self.ev[2]) * 180 / np.pi