SOme iteration with CLaude to improve the program
This commit is contained in:
1 parent
9cf907d978
commit
2e782e481c
5 files changed
+367
-13
No files matched your search
@@ -311,3 +311,219 @@ class BeamlineEditorWidget(QWidget):
|
||||
def connections(self):
|
||||
from .connections import Connection
|
||||
return [i for i in self.scene.items() if isinstance(i, Connection)]
|
||||
|
||||
# ── layout persistence ────────────────────────────────────────────────────
|
||||
|
||||
def saveLayout(self, path: str) -> None:
|
||||
"""
|
||||
Save the current canvas to a JSON file.
|
||||
|
||||
Saved data
|
||||
----------
|
||||
version : int — file-format version (1)
|
||||
nodes : list — one dict per node:
|
||||
id, element_type, display_name, x, y
|
||||
connections : list — one dict per wire:
|
||||
src_id, src_port_index,
|
||||
dst_id, dst_port_index
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : str
|
||||
File path to write (created or overwritten).
|
||||
"""
|
||||
import json
|
||||
from .nodes import BeamlineNode
|
||||
from .connections import Connection
|
||||
|
||||
nodes_data = []
|
||||
for item in self.scene.items():
|
||||
if isinstance(item, BeamlineNode):
|
||||
nodes_data.append({
|
||||
"id": item.node_id,
|
||||
"element_type": item.element_type,
|
||||
"display_name": item.display_name(),
|
||||
"x": item.pos().x(),
|
||||
"y": item.pos().y(),
|
||||
})
|
||||
|
||||
conns_data = []
|
||||
for item in self.scene.items():
|
||||
if isinstance(item, Connection):
|
||||
conns_data.append({
|
||||
"src_id": item.src_port.node.node_id,
|
||||
"src_port_index": item.src_port.index,
|
||||
"dst_id": item.dst_port.node.node_id,
|
||||
"dst_port_index": item.dst_port.index,
|
||||
})
|
||||
|
||||
payload = {
|
||||
"version": 1,
|
||||
"nodes": nodes_data,
|
||||
"connections": conns_data,
|
||||
}
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
json.dump(payload, fh, indent=2)
|
||||
self.scene.status_changed.emit(f"Layout saved → {path}")
|
||||
|
||||
def loadLayout(self, path: str) -> None:
|
||||
"""
|
||||
Clear the canvas and restore a layout previously saved with
|
||||
saveLayout().
|
||||
|
||||
on_element_added is fired for every restored node so the host
|
||||
program can rebuild its own data structures.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : str
|
||||
Path to a JSON file written by saveLayout().
|
||||
"""
|
||||
self.clear()
|
||||
self._load_layout_file(path, id_map=None, offset_y=0.0)
|
||||
self.scene.status_changed.emit(f"Layout loaded ← {path}")
|
||||
|
||||
def importLayout(self, path: str) -> dict[str, str]:
|
||||
"""
|
||||
Import a saved layout onto the canvas *without* clearing existing
|
||||
elements. The imported nodes are placed below all current nodes
|
||||
so they do not overlap.
|
||||
|
||||
ID conflicts
|
||||
------------
|
||||
If any node_id in the file already exists on the canvas, a new
|
||||
UUID is assigned automatically. The mapping is returned so the
|
||||
host program can update its own records.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : str
|
||||
Path to a JSON file written by saveLayout().
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict[str, str]
|
||||
{ old_id: new_id } for every node whose ID was remapped.
|
||||
Empty dict if there were no conflicts.
|
||||
"""
|
||||
existing_ids = {
|
||||
item.node_id
|
||||
for item in self.scene.items()
|
||||
if hasattr(item, "node_id")
|
||||
}
|
||||
|
||||
# Compute vertical offset: place imported layout below current content
|
||||
from .nodes import BeamlineNode
|
||||
current_nodes = [i for i in self.scene.items() if isinstance(i, BeamlineNode)]
|
||||
if current_nodes:
|
||||
max_y = max(n.pos().y() + n.H for n in current_nodes)
|
||||
offset_y = max_y + 80 # 80 px gap between layouts
|
||||
else:
|
||||
offset_y = 0.0
|
||||
|
||||
id_map = self._load_layout_file(
|
||||
path,
|
||||
id_map=existing_ids,
|
||||
offset_y=offset_y,
|
||||
)
|
||||
conflicts = {old: new for old, new in id_map.items() if old != new}
|
||||
msg = (
|
||||
f"Imported layout ← {path}"
|
||||
+ (f" ({len(conflicts)} ID(s) remapped)" if conflicts else "")
|
||||
)
|
||||
self.scene.status_changed.emit(msg)
|
||||
return conflicts
|
||||
|
||||
# ── internal loader ───────────────────────────────────────────────────────
|
||||
|
||||
def _load_layout_file(
|
||||
self,
|
||||
path: str,
|
||||
id_map, # set[str] of existing IDs to check, or None
|
||||
offset_y: float,
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Parse the JSON file and instantiate nodes + connections.
|
||||
|
||||
Returns a dict mapping every node_id in the file to the id actually
|
||||
used on the canvas (identical unless a conflict forced a remap).
|
||||
"""
|
||||
import json, uuid as _uuid
|
||||
from PyQt5.QtCore import QPointF
|
||||
from .nodes import create_node, BeamlineNode
|
||||
from .connections import Connection
|
||||
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
payload = json.load(fh)
|
||||
|
||||
if payload.get("version", 1) != 1:
|
||||
raise ValueError(
|
||||
f"Unsupported layout file version: {payload.get('version')}"
|
||||
)
|
||||
|
||||
# ── Pass 1: create nodes, remap conflicting IDs ───────────────────────
|
||||
existing_ids: set[str] = set(id_map) if id_map is not None else set()
|
||||
remap: dict[str, str] = {} # file_id → canvas_id
|
||||
|
||||
for nd in payload.get("nodes", []):
|
||||
file_id = nd["id"]
|
||||
element_type = nd["element_type"]
|
||||
display_name = nd.get("display_name", element_type)
|
||||
x = float(nd["x"])
|
||||
y = float(nd["y"]) + offset_y
|
||||
|
||||
node = create_node(element_type, QPointF(x, y))
|
||||
|
||||
if file_id in existing_ids:
|
||||
# Conflict: keep the freshly generated UUID the node already has
|
||||
canvas_id = node.node_id
|
||||
else:
|
||||
# No conflict: reuse the saved ID so external records still match
|
||||
node.node_id = file_id
|
||||
canvas_id = file_id
|
||||
|
||||
remap[file_id] = canvas_id
|
||||
existing_ids.add(canvas_id) # prevent later nodes conflicting too
|
||||
|
||||
if display_name != element_type:
|
||||
node.set_display_name(display_name)
|
||||
|
||||
self.scene.addItem(node)
|
||||
|
||||
# Notify host so it can register the element
|
||||
if self.scene.on_element_added:
|
||||
self.scene.on_element_added(element_type, canvas_id)
|
||||
|
||||
# ── Pass 2: rebuild connections using remapped IDs ────────────────────
|
||||
# Build a fast lookup: canvas_id → node
|
||||
node_lookup: dict[str, BeamlineNode] = {}
|
||||
for item in self.scene.items():
|
||||
if isinstance(item, BeamlineNode):
|
||||
node_lookup[item.node_id] = item
|
||||
|
||||
def _get_port(node: BeamlineNode, port_type: str, index: int):
|
||||
"""Return the Port matching port_type + index, or None."""
|
||||
candidates = (
|
||||
node._all_out_ports() if port_type == "out"
|
||||
else node._all_in_ports()
|
||||
)
|
||||
for p in candidates:
|
||||
if p.index == index:
|
||||
return p
|
||||
return None
|
||||
|
||||
for cd in payload.get("connections", []):
|
||||
src_id = remap.get(cd["src_id"], cd["src_id"])
|
||||
dst_id = remap.get(cd["dst_id"], cd["dst_id"])
|
||||
src_node = node_lookup.get(src_id)
|
||||
dst_node = node_lookup.get(dst_id)
|
||||
if src_node is None or dst_node is None:
|
||||
continue
|
||||
src_port = _get_port(src_node, "out", cd.get("src_port_index", 0))
|
||||
dst_port = _get_port(dst_node, "in", cd.get("dst_port_index", 0))
|
||||
if src_port is None or dst_port is None:
|
||||
continue
|
||||
conn = Connection(src_port, dst_port)
|
||||
self.scene.addItem(conn)
|
||||
|
||||
return remap
|
||||
+12
-12
@@ -131,7 +131,7 @@ class Quadrupole(Magnet):
|
||||
def __init__(self, prop):
|
||||
Magnet.__init__(self, prop)
|
||||
if not ('k1' in prop): # prop.has_key('k1')):
|
||||
self.__dict__.update({'k1': 0})
|
||||
self.__dict__.update({'k1': 0.})
|
||||
if not ('Tag' in prop): # prop.has_key('Tag')):
|
||||
self.__dict__.update({'Tag': 'MQUA'})
|
||||
|
||||
@@ -146,7 +146,7 @@ class Sextupole(Magnet):
|
||||
def __init__(self, prop):
|
||||
Magnet.__init__(self, prop)
|
||||
if not ('k2' in prop): # prop.has_key('k2')):
|
||||
self.__dict__.update({'k2': 0})
|
||||
self.__dict__.update({'k2': 0.})
|
||||
if not ('Tag' in prop): # prop.has_key('Tag')):
|
||||
self.__dict__.update({'Tag': 'MSEX'})
|
||||
|
||||
@@ -162,7 +162,7 @@ class Solenoid(Magnet):
|
||||
def __init__(self, prop):
|
||||
Magnet.__init__(self, prop)
|
||||
if not ('ks' in prop): # prop.has_key('ks')):
|
||||
self.__dict__.update({'ks': 0})
|
||||
self.__dict__.update({'ks': 0.})
|
||||
if not ('Tag' in prop): # prop.has_key('Tag')):
|
||||
self.__dict__.update({'Tag': 'MSOL'})
|
||||
|
||||
@@ -205,19 +205,19 @@ class Undulator(SimpleContainer):
|
||||
if not ('Group' in prop): # prop.has_key('Group')):
|
||||
self.__dict__.update({'Group': 'ID'})
|
||||
if not ('K' in prop): # prop.has_key('K')):
|
||||
self.__dict__.update({'K': 0})
|
||||
self.__dict__.update({'K': 0.})
|
||||
if not ('ku' in prop): # prop.has_key('ku')):
|
||||
self.__dict__.update({'ku': 0})
|
||||
self.__dict__.update({'ku': 0.})
|
||||
if not ('kx' in prop): # prop.has_key('kx')):
|
||||
self.__dict__.update({'kx': 0})
|
||||
self.__dict__.update({'kx': 0.})
|
||||
if not ('ky' in prop): # prop.has_key('ky')):
|
||||
self.__dict__.update({'ky': 1})
|
||||
self.__dict__.update({'ky': 1.})
|
||||
if not ('Tag' in prop): # prop.has_key('Tag')):
|
||||
self.__dict__.update({'Tag': 'UIND'})
|
||||
if not ('Power' in prop): # prop.has_key('Power')):
|
||||
self.__dict__.update({'Power': 0})
|
||||
self.__dict__.update({'Power': 0.})
|
||||
if not ('Waist' in prop): # prop.has_key('Waist')):
|
||||
self.__dict__.update({'Waist': 1})
|
||||
self.__dict__.update({'Waist': 1.})
|
||||
|
||||
|
||||
|
||||
@@ -232,9 +232,9 @@ class RF(SimpleContainer):
|
||||
def __init__(self, prop):
|
||||
SimpleContainer.__init__(self, prop)
|
||||
if not ('Gradient' in prop): # prop.has_key('Gradient')):
|
||||
self.__dict__.update({'Gradient': 0})
|
||||
self.__dict__.update({'Gradient': 0.})
|
||||
if not ('Phase' in prop): # prop.has_key('Phase')):
|
||||
self.__dict__.update({'Phase': 0})
|
||||
self.__dict__.update({'Phase': 0.})
|
||||
if not ('Band' in prop): # prop.has_key('Band')):
|
||||
self.__dict__.update({'Band': 'S'})
|
||||
if not ('Frequency' in prop): # prop.has_key('Frequency')):
|
||||
@@ -251,7 +251,7 @@ class RF(SimpleContainer):
|
||||
|
||||
# Enable flag for TDS as well as in the diagnostics
|
||||
if 'TDS' in prop['Baugruppe']:
|
||||
self.__dict__.update({'enable': 0})
|
||||
self.__dict__.update({'enable': 0.})
|
||||
|
||||
def writeElement(self, app):
|
||||
app.writeRF(self)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import sys
|
||||
import json
|
||||
from PyQt5 import QtWidgets, QtCore, QtGui
|
||||
|
||||
from ui.BeamlineGUI import Ui_BeamlineGUI
|
||||
@@ -19,9 +20,32 @@ class BeamlineEditor(QtWidgets.QMainWindow, Ui_BeamlineGUI):
|
||||
|
||||
QtWidgets.QVBoxLayout(self.UIEditorBox).addWidget(self.editor)
|
||||
|
||||
self.actionSave_Templates.triggered.connect(self.saveTemplates)
|
||||
self.actionLoad_Templates.triggered.connect(self.loadTemplates)
|
||||
|
||||
self.actionSave.triggered.connect(self.save)
|
||||
self.actionLoad.triggered.connect(self.load)
|
||||
|
||||
# elements, lines and branch points
|
||||
self.elementDB={}
|
||||
|
||||
def save(self):
|
||||
self.editor.saveLayout("Layouts/lattice.json")
|
||||
|
||||
def load(self):
|
||||
self.editor.loadLayout("Layouts/lattice.json")
|
||||
|
||||
def saveTemplates(self):
|
||||
template=self.editor.get_templates()
|
||||
with (open('Settings/templates.json','w', encoding = "utf-8") as file):
|
||||
json.dump(template, file, indent=2)
|
||||
|
||||
def loadTemplates(self):
|
||||
with (open('Settings/templates.json','r', encoding = "utf-8") as file):
|
||||
template=json.load(file)
|
||||
self.editor.set_templates(template)
|
||||
|
||||
|
||||
#editor = BeamlineEditorWidget(
|
||||
# on_element_added = fn(element_type: str, node_id: str),
|
||||
# on_elements_deleted = fn(node_ids: list[str]),
|
||||
|
||||
@@ -26,14 +26,52 @@ class Ui_BeamlineGUI(object):
|
||||
self.menubar = QtWidgets.QMenuBar(BeamlineGUI)
|
||||
self.menubar.setGeometry(QtCore.QRect(0, 0, 1491, 22))
|
||||
self.menubar.setObjectName("menubar")
|
||||
self.menuFile = QtWidgets.QMenu(self.menubar)
|
||||
self.menuFile.setObjectName("menuFile")
|
||||
BeamlineGUI.setMenuBar(self.menubar)
|
||||
self.statusbar = QtWidgets.QStatusBar(BeamlineGUI)
|
||||
self.statusbar.setObjectName("statusbar")
|
||||
BeamlineGUI.setStatusBar(self.statusbar)
|
||||
self.actionNew_Layout = QtWidgets.QAction(BeamlineGUI)
|
||||
self.actionNew_Layout.setObjectName("actionNew_Layout")
|
||||
self.actionSave = QtWidgets.QAction(BeamlineGUI)
|
||||
self.actionSave.setObjectName("actionSave")
|
||||
self.actionSave_as = QtWidgets.QAction(BeamlineGUI)
|
||||
self.actionSave_as.setObjectName("actionSave_as")
|
||||
self.actionSave_Templates = QtWidgets.QAction(BeamlineGUI)
|
||||
self.actionSave_Templates.setObjectName("actionSave_Templates")
|
||||
self.actionLoad_Templates = QtWidgets.QAction(BeamlineGUI)
|
||||
self.actionLoad_Templates.setObjectName("actionLoad_Templates")
|
||||
self.actionLoad = QtWidgets.QAction(BeamlineGUI)
|
||||
self.actionLoad.setObjectName("actionLoad")
|
||||
self.actionQuit = QtWidgets.QAction(BeamlineGUI)
|
||||
self.actionQuit.setObjectName("actionQuit")
|
||||
self.menuFile.addAction(self.actionNew_Layout)
|
||||
self.menuFile.addAction(self.actionLoad)
|
||||
self.menuFile.addAction(self.actionSave)
|
||||
self.menuFile.addAction(self.actionSave_as)
|
||||
self.menuFile.addSeparator()
|
||||
self.menuFile.addAction(self.actionLoad_Templates)
|
||||
self.menuFile.addAction(self.actionSave_Templates)
|
||||
self.menuFile.addSeparator()
|
||||
self.menuFile.addAction(self.actionQuit)
|
||||
self.menubar.addAction(self.menuFile.menuAction())
|
||||
|
||||
self.retranslateUi(BeamlineGUI)
|
||||
self.actionQuit.triggered.connect(self.centralwidget.close) # type: ignore
|
||||
QtCore.QMetaObject.connectSlotsByName(BeamlineGUI)
|
||||
|
||||
def retranslateUi(self, BeamlineGUI):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
BeamlineGUI.setWindowTitle(_translate("BeamlineGUI", "MainWindow"))
|
||||
self.menuFile.setTitle(_translate("BeamlineGUI", "File"))
|
||||
self.actionNew_Layout.setText(_translate("BeamlineGUI", "New Layout"))
|
||||
self.actionSave.setText(_translate("BeamlineGUI", "Save"))
|
||||
self.actionSave.setShortcut(_translate("BeamlineGUI", "Ctrl+S"))
|
||||
self.actionSave_as.setText(_translate("BeamlineGUI", "Save as.."))
|
||||
self.actionSave_Templates.setText(_translate("BeamlineGUI", "Save Templates"))
|
||||
self.actionLoad_Templates.setText(_translate("BeamlineGUI", "Load Templates"))
|
||||
self.actionLoad.setText(_translate("BeamlineGUI", "Load"))
|
||||
self.actionLoad.setShortcut(_translate("BeamlineGUI", "Ctrl+L"))
|
||||
self.actionQuit.setText(_translate("BeamlineGUI", "Quit"))
|
||||
self.actionQuit.setShortcut(_translate("BeamlineGUI", "Ctrl+Q"))
|
||||
+77
-1
@@ -29,9 +29,85 @@
|
||||
<height>22</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuFile">
|
||||
<property name="title">
|
||||
<string>File</string>
|
||||
</property>
|
||||
<addaction name="actionNew_Layout"/>
|
||||
<addaction name="actionLoad"/>
|
||||
<addaction name="actionSave"/>
|
||||
<addaction name="actionSave_as"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionLoad_Templates"/>
|
||||
<addaction name="actionSave_Templates"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionQuit"/>
|
||||
</widget>
|
||||
<addaction name="menuFile"/>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar"/>
|
||||
<action name="actionNew_Layout">
|
||||
<property name="text">
|
||||
<string>New Layout</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionSave">
|
||||
<property name="text">
|
||||
<string>Save</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+S</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionSave_as">
|
||||
<property name="text">
|
||||
<string>Save as..</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionSave_Templates">
|
||||
<property name="text">
|
||||
<string>Save Templates</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionLoad_Templates">
|
||||
<property name="text">
|
||||
<string>Load Templates</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionLoad">
|
||||
<property name="text">
|
||||
<string>Load</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+L</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionQuit">
|
||||
<property name="text">
|
||||
<string>Quit</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Q</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>actionQuit</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>BeamlineGUI</receiver>
|
||||
<slot>close()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>745</x>
|
||||
<y>320</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
Reference in new issue
Block a user