From 0b23aedbd7cb2cf3f58cf64711e7299bab916ca5 Mon Sep 17 00:00:00 2001 From: Sven Date: Thu, 16 Jul 2026 17:21:38 +0200 Subject: [PATCH] Building up the lattice --- beamline_editor/scene.py | 125 +++++++++++++++++++++++++++++++++++++++ main.py | 66 +++++++++++++++------ protoliste.py | 34 ++++++++++- ui/BeamlineGUI.ui | 98 +++++++++++++++++++++++++++++- 4 files changed, 303 insertions(+), 20 deletions(-) diff --git a/beamline_editor/scene.py b/beamline_editor/scene.py index edd3382..ad6f488 100644 --- a/beamline_editor/scene.py +++ b/beamline_editor/scene.py @@ -359,6 +359,23 @@ class BeamlineScene(QGraphicsScene): none_act.setEnabled(False) tmpl_menu.addAction(none_act) + # Align / Distribute — shown when 2+ nodes are selected + selected_nodes = [i for i in self.selectedItems() + if isinstance(i, BeamlineNode)] + if len(selected_nodes) >= 2: + menu.addSeparator() + align_menu = menu.addMenu("Align && Distribute") + align_menu.setStyleSheet(menu.styleSheet()) + for label, slot in ( + ("Align Horizontally", lambda: self._align(True)), + ("Align Vertically", lambda: self._align(False)), + ("Distribute Horizontally", lambda: self._distribute(True)), + ("Distribute Vertically", lambda: self._distribute(False)), + ): + act = QAction(label, align_menu) + act.triggered.connect(slot) + align_menu.addAction(act) + menu.exec_(screen_pos) def _ctx_copy(self, node: BeamlineNode): @@ -503,8 +520,116 @@ class BeamlineScene(QGraphicsScene): act_paste.setEnabled(bool(self._clipboard)) act_paste.triggered.connect(self.paste) menu.addAction(act_paste) + + # Align / Distribute — only when 2+ nodes are selected + selected_nodes = [i for i in self.selectedItems() + if isinstance(i, BeamlineNode)] + if len(selected_nodes) >= 2: + menu.addSeparator() + align_menu = menu.addMenu("Align && Distribute") + align_menu.setStyleSheet(menu.styleSheet()) + + for label, slot in ( + ("Align Horizontally", lambda: self._align(True)), + ("Align Vertically", lambda: self._align(False)), + ("Distribute Horizontally", lambda: self._distribute(True)), + ("Distribute Vertically", lambda: self._distribute(False)), + ): + act = QAction(label, align_menu) + act.triggered.connect(slot) + align_menu.addAction(act) + menu.exec_(screen_pos) + # ── Align / Distribute helpers ──────────────────────────────────────────── + + def _selected_nodes(self) -> list[BeamlineNode]: + return [i for i in self.selectedItems() if isinstance(i, BeamlineNode)] + + def _node_center(self, node: BeamlineNode) -> QPointF: + """Scene-coordinate centre of a node.""" + return node.pos() + QPointF(node.W / 2, node.H / 2) + + def _align(self, horizontal: bool): + """ + Align all selected nodes so their centres share a common axis. + + horizontal=True → align to the mean Y (same horizontal line) + horizontal=False → align to the mean X (same vertical line) + """ + nodes = self._selected_nodes() + if len(nodes) < 2: + return + + centers = [self._node_center(n) for n in nodes] + if horizontal: + mean_y = sum(c.y() for c in centers) / len(centers) + for node in nodes: + cx = self._node_center(node).x() + node.setPos(cx - node.W / 2, mean_y - node.H / 2) + self.status_changed.emit( + f"Aligned {len(nodes)} elements horizontally." + ) + else: + mean_x = sum(c.x() for c in centers) / len(centers) + for node in nodes: + cy = self._node_center(node).y() + node.setPos(mean_x - node.W / 2, cy - node.H / 2) + self.status_changed.emit( + f"Aligned {len(nodes)} elements vertically." + ) + + # Update all connections attached to moved nodes + for node in nodes: + for port in node._all_ports(): + for conn in port.connections: + conn.update_path() + + def _distribute(self, horizontal: bool): + """ + Space all selected nodes evenly along one axis so their centres + are equidistant. + + horizontal=True → distribute along X axis + horizontal=False → distribute along Y axis + """ + nodes = self._selected_nodes() + if len(nodes) < 3: + self.status_changed.emit( + "Select at least 3 elements to distribute." + ) + return + + if horizontal: + nodes_sorted = sorted(nodes, key=lambda n: self._node_center(n).x()) + x_min = self._node_center(nodes_sorted[0]).x() + x_max = self._node_center(nodes_sorted[-1]).x() + step = (x_max - x_min) / (len(nodes_sorted) - 1) + for i, node in enumerate(nodes_sorted): + cx = x_min + i * step + cy = self._node_center(node).y() + node.setPos(cx - node.W / 2, cy - node.H / 2) + self.status_changed.emit( + f"Distributed {len(nodes)} elements horizontally." + ) + else: + nodes_sorted = sorted(nodes, key=lambda n: self._node_center(n).y()) + y_min = self._node_center(nodes_sorted[0]).y() + y_max = self._node_center(nodes_sorted[-1]).y() + step = (y_max - y_min) / (len(nodes_sorted) - 1) + for i, node in enumerate(nodes_sorted): + cx = self._node_center(node).x() + cy = y_min + i * step + node.setPos(cx - node.W / 2, cy - node.H / 2) + self.status_changed.emit( + f"Distributed {len(nodes)} elements vertically." + ) + + for node in nodes: + for port in node._all_ports(): + for conn in port.connections: + conn.update_path() + def _ctx_save_template(self, node: BeamlineNode): props = None if self._get_properties: diff --git a/main.py b/main.py index ae66192..6f031a1 100644 --- a/main.py +++ b/main.py @@ -52,8 +52,11 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): self.elementDB={} self.savefile='' self.lines={} - self.branchingID={} + self.branchingID=[] + self.branching={} self.branchingTemp={} + self.branchnames={} + self.targetReached=False self.PL=ProtoListe(0) self.loadFile('Layouts/SFTest2.json') @@ -235,7 +238,7 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): retdict['Position']['Reserved Length'] = float(retdict['Position']['Reserved Length']) return retdict - def updateProperties(self,node_id: str, data: dict): + def updateProperties(self,node_id: str, data: dict): # make sure that the ID is not changed data['Name']['ID']=node_id type = self.editor.getElementType(node_id) @@ -275,17 +278,23 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): self.branchingID.clear() for key in self.elementDB.keys(): ele = self.elementDB[key] - if isinstance(ele,Dipole) and ele.branching: - self.branchingID[key] = False + self.branchingID.append(key) print('Branching elements:') - for key in self.branchingID.keys(): - print('ID:',key) + for key in self.branchingID: + print(' ID:',key) def flattenSingleLine(self,node_id,branch=0,case_num=1): - for key in self.branchingID.keys(): - self.branchingID[key]=(branch == 1) + self.targetReached=False + self.currentBranch='' + self.branching.clear() + for ib, id in enumerate(self.branchingID): + ibit = 2**ib + if branch & ibit > 0: + self.branching[id]=True + else: + self.branching[id]=False type = self.editor.getElementType(node_id) name = 'XXX' if type == 'Line': @@ -295,7 +304,8 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): name = self.elementDB[node_id].Name line = LineContainer(namein=name, Lin=0) line.clear() - print('\nUnwrapping beamline...\n') + if self.verbose: + print('\nUnwrapping beamline...\n') self.branchingTemp.clear() self.unwrap(name, node_id, line, case_num) for key in self.branchingTemp.keys(): @@ -305,15 +315,22 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): def flatten(self, node_id, case_num=1 ): print('Flatten beamline for case:', case_num) self.findBranchingDipoles() - nbranch = len(self.branchingID.keys()) + nbranch = len(self.branchingID) print('Possible Number of Branches:', 2**nbranch) self.lines.clear() - # first write the baseline (all branching off) - - self.lines['1'] = copy.deepcopy(self.flattenSingleLine(node_id,branch=0,case_num=case_num)) - #self.lines['2'] = copy.deepcopy(self.flattenSingleLine(node_id,branch=1,case_num=case_num)) - +# for ib in range(2**nbranch): + for ib in range(0,4): + line=self.flattenSingleLine(node_id,branch=ib,case_num=case_num) + if self.currentBranch=='': + self.currentBranch='%d' % ib + if not self.currentBranch in self.lines.keys(): + self.lines[self.currentBranch]=copy.deepcopy(line) + else: + print('Skipping redundant line:',self.currentBranch) + print('Writing Layout for the lines:') + for key in self.lines.keys(): + print(' ',key) self.PL.generateLayout(self.lines) self.plrecs = [self.PL.info[ele] for ele in self.PL.order if len(self.PL.info[ele]['Prefix'])> 5] populate_table(self.UIProtoList,self.plrecs) @@ -328,6 +345,8 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): tree=self.editor.flatten(node_id,case_num) secondBranch = False for ele in tree.path: + if self.targetReached: + return self.report(ele, name) type = self.editor.getElementType(ele) if type == 'Line': @@ -340,6 +359,8 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): ref = subline.firstElementID if ref in self.elementDB.keys(): secondBranch = self.unwrap(name + name1, ref, subline,case_num) + if secondBranch: + break # do not continue with any other elements else: if self.verbose: print('Undefined reference in Line Container:', name + name1) @@ -352,6 +373,12 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): line.append(self.elementDB[ele],sRef=0,Ref='relative') elif type == 'Case' or type == 'Merge': continue + elif type == 'Target': + target=self.elementDB[ele].Name + if not target in self.lines.keys(): + print('New Target reached:',self.elementDB[ele].Name) + self.targetReached=True + self.currentBranch=self.elementDB[ele].Name else: sRef=self.elementDB[ele].OffsetS Ref='absolute' @@ -359,8 +386,9 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): Ref='relative' line.append(self.elementDB[ele],sRef=sRef,Ref=Ref) self.elementDB[ele].Prefix=name - if ele in self.branchingID.keys(): - if self.branchingID[ele] is True: + if ele in self.branching.keys(): + if self.branching[ele] is True: + print('Branching at element:', name+'-'+self.elementDB[ele].Name,'with angle:',self.elementDB[ele].design_angle) self.elementDB[ele].angle=self.elementDB[ele].design_angle line.hasBranchPoint=True return True @@ -370,10 +398,12 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI): if secondBranch: if tree.secondary: self.unwrap(name, tree.secondary.node_id, line, case_num) + else: + return secondBranch else: if tree.primary: self.unwrap(name,tree.primary.node_id,line,case_num) - return False + return secondBranch diff --git a/protoliste.py b/protoliste.py index cc7ee6e..6abd3dc 100644 --- a/protoliste.py +++ b/protoliste.py @@ -24,7 +24,6 @@ class ProtoListe(ApplicationTemplate): self.dipole.clear() self.setRef(-0.1, -0.1) #self.iterateLine(line) - keys=['1'] for key in lines.keys(): print('Writing Line for:',key,lines[key].Name) self.setRef(-0.1, -0.1) @@ -258,6 +257,37 @@ class ProtoListe(ApplicationTemplate): def writeMarker(self,ele): + if not ele.Name in self.info.keys() and len(ele.Name) > 4: + name = ele.Prefix + '-' + ele.Name + domain = self.getDomain(name) + align = 'Entrance' + angh = np.arctan2(self.ev[0], self.ev[2]) * 180 / np.pi + rh = np.sqrt(self.ev[2] * self.ev[2] + self.ev[0] * self.ev[0]) + angv = np.arctan2(self.ev[1], rh) * 180 / np.pi + if ele.Group == 'Start': + group = 'Section' + bg ='Marker' + prefix= ele.Prefix + suffix='' + index='' + elif ele.Group == 'Marker' and 'MREF' in ele.Name: + group = 'Marker' + bg = 'Reference Point' + prefix=ele.Prefix + suffix = 'MREF' + index=ele.index + else: + return + info = {'Domain': domain, 'Prefix': prefix, 'Suffix': suffix, 'Index': index, + 's (m)': self.s, 'z (m)': self.z, 'x (m)': self.x, 'y (m)': self.y, 'L (m)': 0, + 'Reference': 'Start', 'Group': group, 'Baugruppe': bg, 'Variante': 'A', 'Roll': 0, + 'PS-Ch.': 0, 'P1': self.p1, 'PV': self.p1, 'P2': self.p1, 'EV1': self.ev1, + 'EV2': self.ev1, 'Yaw': angh, 'Pitch': angv, 'Alignment': align} + self.info[ele.Name] = info + self.order.append(ele.Name) + return + + if ele.Group == 'Marker': if 'MREF' in ele.Name: self.write((ele.Name, ele.LengthRes, 'Start', ele.Group, ele.Tag, ele.Baugruppe, 0, 0)) @@ -302,6 +332,8 @@ class ProtoListe(ApplicationTemplate): channels=0 if 'PSChannels' in ele.__dict__: channels=ele['PSChannels'] + if ele.Baugruppe=='U15': + channels=channels+1 if ele.Baugruppe=='QFU': channels=channels-1 if ele.Baugruppe=='QFUE': diff --git a/ui/BeamlineGUI.ui b/ui/BeamlineGUI.ui index 793d71c..01e055b 100644 --- a/ui/BeamlineGUI.ui +++ b/ui/BeamlineGUI.ui @@ -24,7 +24,7 @@ background-color: rgb(13, 15, 26); - 0 + 2 @@ -46,6 +46,102 @@ background-color: rgb(13, 15, 26); + + + Settings + + + + + + + + + + + 0 + 0 + + + + Offset for Flatten + + + true + + + + + + + + 0 + 0 + + + + -0.1 + + + + + + + + 0 + 0 + + + + Tolerance + + + + + + + + 0 + 0 + + + + 1e-5 + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Qt::Horizontal + + + + 1944 + 20 + + + + + +