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
+
+
+
+
+
+