From 5b062611c66d80d0c2aa861925bca40f80a52bc0 Mon Sep 17 00:00:00 2001 From: tyler Date: Mon, 4 May 2026 00:43:05 -0700 Subject: [PATCH] config editor changes --- blocks.json | 17 ++ main.py | 569 ++++++++++++++++++++++++++++++++++------------------ 2 files changed, 387 insertions(+), 199 deletions(-) create mode 100644 blocks.json diff --git a/blocks.json b/blocks.json new file mode 100644 index 0000000..e383ff4 --- /dev/null +++ b/blocks.json @@ -0,0 +1,17 @@ +{ + "individual": [ + { "name": "Start Node", "type": "begin" }, + { + "name": "Hybrid Processor", + "type": "middle", + "fields": [ + {"label": "Threshold", "type": "number"}, + {"label": "Tag", "type": "string"}, + {"label": "Sub-Logic", "type": "slot"} + ] + }, + { "name": "Export Node", "type": "end" }, + { "name": "like this", "type": "logic" } + + ] +} \ No newline at end of file diff --git a/main.py b/main.py index 3ee0b47..b9a6b6d 100644 --- a/main.py +++ b/main.py @@ -34,11 +34,11 @@ from pose_worker import run_pose_analysis from batch_processing import BatchProcessorDialog import PySide6 -from PySide6.QtWidgets import (QApplication, QDoubleSpinBox, QGraphicsItem, QLineEdit, QListWidget, QListWidgetItem, QMainWindow, QProgressDialog, QStyleOptionGraphicsItem, QTabBar, QWidget, QVBoxLayout, QGraphicsView, QGraphicsScene, - QHBoxLayout, QSplitter, QLabel, QPushButton, QComboBox, QInputDialog, +from PySide6.QtWidgets import (QApplication, QDoubleSpinBox, QFormLayout, QGraphicsItem, QGraphicsProxyWidget, QGraphicsTextItem, QLineEdit, QListWidget, QListWidgetItem, QMainWindow, QProgressDialog, QSizePolicy, QStyleOptionGraphicsItem, QTabBar, QWidget, QVBoxLayout, QGraphicsView, QGraphicsScene, + QHBoxLayout, QSplitter, QLabel, QPushButton, QComboBox, QInputDialog, QGraphicsRectItem, QFileDialog, QScrollArea, QMessageBox, QSlider, QTextEdit, QGroupBox, QGridLayout, QCheckBox, QTabWidget, QProgressBar) from PySide6.QtCore import QEvent, Qt, QThread, Signal, QUrl, QRectF, QPointF, QRect, QSizeF, QTimer -from PySide6.QtGui import QCursor, QGuiApplication, QPainter, QColor, QFont, QPen, QBrush, QAction, QKeySequence, QIcon, QTextOption, QImage, QPixmap +from PySide6.QtGui import QCursor, QDoubleValidator, QGuiApplication, QPainter, QColor, QFont, QPen, QBrush, QAction, QKeySequence, QIcon, QTextOption, QImage, QPixmap, QTransform from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput from PySide6.QtMultimediaWidgets import QGraphicsVideoItem @@ -2008,13 +2008,16 @@ class PremiereWindow(QMainWindow): train_act = QAction("Train Model", self) train_act.triggered.connect(self.open_train_model_dialog) - config_act = QAction("Configuration Editor", self) - config_act.triggered.connect(self.open_model_configuration_tab) + config_act = QAction("Configuration (Ind.)", self) + config_act.triggered.connect(lambda: self.open_model_configuration_tab("individual")) + config_act2 = QAction("Configuration (Group)", self) + config_act2.triggered.connect(lambda: self.open_model_configuration_tab("group")) menu.addAction(load_act) menu.addAction(train_act) menu.addAction(config_act) + menu.addAction(config_act2) # Show the menu right under the mouse cursor menu.exec(QCursor.pos()) @@ -2090,8 +2093,8 @@ class PremiereWindow(QMainWindow): self.train_window.btn_train.clicked.connect(self.handle_start_training) self.train_window.show() - def open_model_configuration_tab(self): - self.handle_model_configuration_session() + def open_model_configuration_tab(self, stringy): + self.handle_model_configuration_session(stringy) def open_export_data_dialog(self): if self.export_window is None or not self.export_window.isVisible(): @@ -2169,11 +2172,11 @@ class PremiereWindow(QMainWindow): self.tabs.setCurrentIndex(new_idx) - def handle_model_configuration_session(self): - new_tab = ModelParameterConfigurationTab() + def handle_model_configuration_session(self, stringy): + new_tab = ModelParameterConfigurationTab(stringy) # 4. Handle Tab Placement (Keep '+' at the end) - tab_name = "Configuration Editor" + tab_name = f"Configuration Editor - {stringy}" plus_idx = self.tabs.count() - 1 new_idx = self.tabs.insertTab(plus_idx, new_tab, tab_name) @@ -3161,7 +3164,7 @@ from PySide6.QtWidgets import QGraphicsItem, QGraphicsSimpleTextItem, QInputDial class PuzzleBlock(QGraphicsPathItem): - def __init__(self, b_type, label, parent_item=None): + def __init__(self, b_type, label, parent_item=None, fields=None): super().__init__(parent_item) self.b_type = b_type # "begin", "middle", "end" self.label_text = label @@ -3178,19 +3181,108 @@ class PuzzleBlock(QGraphicsPathItem): QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges ) - self.update_path() + self.setData(0, "p_block") + + self.inputs = {} + self.original_inputs = {} + + print(f"[DEBUG] - {fields}") + + if fields: + self.container = QWidget() + self.container.setObjectName("blockContainer") + self.container.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + if self.b_type == "logic": + self.container.setFixedSize(130, 20) + self.container.setStyleSheet("background: transparent; border: none;") + else: + self.container.setFixedWidth(self.width) + self.container.setStyleSheet("background: transparent; color: white;") + + # Use FormLayout for Side-by-Side placement + layout = QFormLayout(self.container) + layout.setContentsMargins(10, 5, 10, 5) + layout.setSpacing(5) + layout.setLabelAlignment(Qt.AlignmentFlag.AlignLeft) + layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) + + for f in fields: + if f['type'] in ["number", "string"]: + # Create the label + field_label = QLabel(f.get('label', 'Field:')) + field_label.setStyleSheet("font-size: 9px; font-weight: bold;") + + # Create the input + edit = QLineEdit() + edit.setFixedHeight(18) # Keep it slim + edit.setStyleSheet("background: rgba(255, 255, 255, 0.2); border: 1px solid #aaa; color: white;") + + if f['type'] == "number": + edit.setValidator(QDoubleValidator()) + + # Add to the form: Label is left, Edit is right + layout.addRow(field_label, edit) + self.inputs[f.get('label')] = edit + + elif f['type'] == "slot": + slot_label = QLabel("") + slot_label.setStyleSheet(""" + color: #FFA500; + font-size: 10px; + border: 1px dashed #FFA500; + padding: 2px; + border-radius: 2px; + min-height: 20px; /* CRITICAL: Gives the mouse something to hit */ + background: rgba(255, 165, 0, 0.05); /* Slight tint helps visual hit-testing */ + """) + # Force it to expand so it's not a tiny dot in the corner + slot_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + + # Add as a full-width row + layout.addRow(slot_label) + # Make sure it's in the inputs dict so perform_snap can find the key + key = f.get('label', 'slot') + self.inputs[key] = slot_label + self.original_inputs[key] = slot_label + + self.proxy = QGraphicsProxyWidget(self) + self.proxy.setZValue(10) + self.proxy.setWidget(self.container) + # Center the 160px container inside your (potentially) 180px block + # Or set self.width = 160 to match exactly + self.width = 160 + self.proxy.setPos(0, 35) + + # Calculate height based on the form's hint + self.height = 50 + layout.sizeHint().height() # Color Coding - colors = {"begin": "#2e7d32", "middle": "#1565c0", "end": "#c62828"} + self.update_path() + + colors = {"begin": "#2e7d32", "middle": "#1565c0", "end": "#c62828", "logic": "#ef6c00"} self.setBrush(QBrush(QColor(colors.get(b_type, "#555")))) self.setPen(QPen(QColor("#ffffff"), 1)) self.label_item = QGraphicsSimpleTextItem(self.label_text, self) self.label_item.setBrush(QColor("white")) - self.label_item.setPos(25, 15) + + if self.b_type == "logic": + self.label_item.setPos(2, 1) + else: + self.label_item.setPos(25, 15) + def update_path(self): + """This changes how they visually appear. Likely no logic broken here.""" self.prepareGeometryChange() + + if self.b_type == "logic": + display_width = 130 # Slimmer profile + display_height = 20 # Shorter default height + else: + display_width = self.width + display_height = self.height + path = QPainterPath() path.moveTo(0, 0) @@ -3200,18 +3292,18 @@ class PuzzleBlock(QGraphicsPathItem): path.lineTo(30, 0) # Nub (Male) - Sweep is negative to curve OUTWARD path.arcTo(30, -self.tab_size/2, self.tab_size, self.tab_size, 180, -180) - path.lineTo(self.width, 0) + path.lineTo(display_width, 0) # 2. RIGHT EDGE - path.lineTo(self.width, self.height) + path.lineTo(display_width, display_height) # 3. BOTTOM EDGE # Begin and Middle blocks now have a "Hole" (Female) on bottom if self.b_type in ["begin", "middle"]: - path.lineTo(30 + self.tab_size, self.height) + path.lineTo(30 + self.tab_size, display_height) # Socket (Female) - Sweep is positive to curve INWARD - path.arcTo(30, self.height - self.tab_size/2, self.tab_size, self.tab_size, 0, 180) - path.lineTo(0, self.height) + path.arcTo(30, display_height - self.tab_size/2, self.tab_size, self.tab_size, 0, 180) + path.lineTo(0, display_height) # 4. LEFT EDGE path.lineTo(0, 0) @@ -3219,7 +3311,9 @@ class PuzzleBlock(QGraphicsPathItem): path.closeSubpath() self.setPath(path) + def mouseDoubleClickEvent(self, event): + '''Simply to edit the name of a start box.''' if self.b_type == "begin": new_name, ok = QInputDialog.getText(None, "Rename Step", "Enter name:", text=self.label_text) if ok and new_name: @@ -3227,63 +3321,94 @@ class PuzzleBlock(QGraphicsPathItem): self.label_item.setText(new_name) super().mouseDoubleClickEvent(event) - def detach_from_neighbors(self): - """Sever logical connections safely so we don't leave ghost references.""" - if self.parent_block: - self.parent_block.child_block = None - self.parent_block = None - if self.child_block: - self.child_block.parent_block = None - self.child_block = None def mousePressEvent(self, event): # Only handle visual layering here. Do NOT detach links. self.setZValue(100) super().mousePressEvent(event) + def mouseReleaseEvent(self, event): # Drop it back down to standard Z-level self.setZValue(0) super().mouseReleaseEvent(event) + def itemChange(self, change, value): if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged: - # If this block is moving, check if its neighbors are NOT moving with it. - # If a neighbor is stationary while we move, the link must break. - - # 1. Check Parent Link - if self.parent_block and not self.parent_block.isSelected(): - print(f"DEBUG: Tearing away from stationary parent: {self.parent_block.label_text}") - self.parent_block.child_block = None - self.parent_block = None - - # 2. Check Child Link - if self.child_block and not self.child_block.isSelected(): - print(f"DEBUG: Tearing away from stationary child: {self.child_block.label_text}") - self.child_block.parent_block = None - self.child_block = None + if self.scene() and self.scene().mouseGrabberItem() is self: + # 1. Sever Parent Link (Both ways) + if self.parent_block: + print(f"DEBUG: Tearing away from parent: {self.parent_block.label_text}") + self.parent_block.child_block = None # Tell parent I'm gone + self.parent_block = None # Forget my parent + + # 2. Sever Child Link (Both ways) + if self.child_block and not self.child_block.isSelected(): + print(f"DEBUG: Tearing away from stationary child: {self.child_block.label_text}") + self.child_block.parent_block = None # Tell child I'm gone + self.child_block = None # Forget my child + return super().itemChange(change, value) - + + + class BlockLibrary(QListWidget): - def __init__(self, parent_tab): + def __init__(self, parent_tab, stringy): super().__init__() self.setDragEnabled(True) - self.add_item("Begin Event", "begin") - self.add_item("Process Step", "middle") - self.add_item("Finish/Export", "end") + + self.mode = stringy + self.parent_tab = parent_tab # Reference to main tab to access canvas - def add_item(self, label, block_type): + self.load_blocks_from_json("blocks.json") + + + def load_blocks_from_json(self, file_path): + if not os.path.exists(file_path): + print(f"Error: {file_path} not found.") + return + + try: + with open(file_path, 'r') as f: + data = json.load(f) + + # Pull the list assigned to the current mode (e.g., "individual") + blocks = data.get(self.mode, []) + + for block in blocks: + # NEW: Get fields if they exist, otherwise empty list + fields = block.get('fields', []) + self.add_item(block['name'], block['type'], fields) + + except Exception as e: + print(f"Failed to load block library: {e}") + + + def add_item(self, label, block_type, fields): item = QListWidgetItem(label) item.setData(Qt.UserRole, block_type) + # NEW: Store fields list in the item + item.setData(Qt.UserRole + 1, fields) self.addItem(item) + def startDrag(self, supportedActions): item = self.currentItem() + if not item: return + mime_data = QMimeData() - # Pass the type and label as string - mime_data.setText(f"{item.data(Qt.UserRole)}|{item.text()}") + + # NEW: We must include the fields in the drag data! + # Format: type | label | fields_as_json_string + b_type = item.data(Qt.UserRole) + label = item.text() + fields = item.data(Qt.UserRole + 1) + fields_str = json.dumps(fields) # Convert list to string for transfer + + mime_data.setText(f"{b_type}|{label}|{fields_str}") drag = QDrag(self) drag.setMimeData(mime_data) @@ -3293,9 +3418,13 @@ class BlockLibrary(QListWidget): + + + from PySide6.QtWidgets import QGraphicsLineItem, QGraphicsRectItem, QGraphicsSimpleTextItem from PySide6.QtGui import QPen, QColor, QBrush + class TopologyCanvas(QGraphicsView): def __init__(self): self.scene = QGraphicsScene() @@ -3303,58 +3432,97 @@ class TopologyCanvas(QGraphicsView): self.setAcceptDrops(True) self.scene.setSceneRect(0, 0, 2000, 2000) - self.chain = [] # Track items if needed + + + def mousePressEvent(self, event): + item = self.itemAt(event.position().toPoint()) + # Toggle drag mode based on whether an item was clicked + mode = QGraphicsView.DragMode.NoDrag if item else QGraphicsView.DragMode.RubberBandDrag + self.setDragMode(mode) + super().mousePressEvent(event) + + #needed? + self.chain = [] + self._processing_release = False + def mouseReleaseEvent(self, event): - # 1. Native release + # Let the native C++ event finish first super().mouseReleaseEvent(event) - - if getattr(self, '_processing_release', False): + + if self._processing_release: return + # Capture the data we need + global_pos = event.globalPosition().toPoint() + scene_pos = self.mapToScene(event.position().toPoint()) + + # Defer the logic to the next event loop cycle (stops the DLL crash) + QTimer.singleShot(0, lambda: self.safe_process_release(global_pos, scene_pos)) + + def safe_process_release(self, global_pos, scene_pos): self._processing_release = True - # --------------------------- - try: - - # 2. Sidebar Trash Logic - global_pos = event.globalPosition().toPoint() - widget_under_mouse = QApplication.widgetAt(global_pos) - - print("huh") - - is_over_library = False - current_widget = widget_under_mouse - while current_widget: - if isinstance(current_widget, BlockLibrary): - is_over_library = True - break - current_widget = current_widget.parentWidget() - - if is_over_library: - print("Action: Trashing selected items") + # 1. Cleaner Trash Check (Assume you have a ref to your sidebar) + # Replacing the 'while widget' loop with a geometry check is much safer + if hasattr(self, 'library_ref') and self.library_ref.rect().contains(self.library_ref.mapFromGlobal(global_pos)): + print("Action: Trashing items") for item in self.scene.selectedItems(): - if isinstance(item, PuzzleBlock): - self.scene.removeItem(item) + self.scene.removeItem(item) return - item_under_mouse = self.itemAt(event.position().toPoint()) - - if isinstance(item_under_mouse, PuzzleBlock): - print(f"DEBUG: Snapping initiated via {item_under_mouse.label_text}") - mouse_scene_pos = self.mapToScene(event.position().toPoint()) - self.perform_snap(item_under_mouse, mouse_scene_pos) - else: - print("DEBUG: Drop in empty space. Maintaining current stack integrity.") - - self.validate_topology() - + # 2. Logic for Snapping + items_at_pos = self.scene.items(scene_pos) + driver = next((i for i in items_at_pos if isinstance(i, PuzzleBlock) and i.isSelected()), None) + target = next((i for i in items_at_pos if isinstance(i, PuzzleBlock) and not i.isSelected()), None) + if driver and target: + self.perform_snap(driver, scene_pos) + finally: - # Always reset the guard so the next real click works self._processing_release = False + def dragEnterEvent(self, event): + if event.mimeData().hasText(): + event.acceptProposedAction() + + def dragMoveEvent(self, event): + if event.mimeData().hasText(): + event.acceptProposedAction() + + + def dropEvent(self, event): + raw_data = event.mimeData().text() + parts = raw_data.split("|") + if len(parts) < 2: return # Safety check + + block_type, label = parts[0], parts[1] + fields = [] + + # Safe JSON parsing + if len(parts) > 2: + try: + fields = json.loads(parts[2]) + except Exception as e: + print(f"DEBUG: Failed to parse: {e}") + + # Spawn selection + if block_type == "value": + # new_block = ValueBlock(value="0") + print("Spawned ValueBlock (Placeholder)") + return # Exit early if not implemented to avoid setPos error + else: + new_block = PuzzleBlock(block_type, label, fields=fields) + + self.scene.addItem(new_block) + + # Try snapping, otherwise place at drop location + if not self.perform_snap(new_block, self.mapToScene(event.position().toPoint())): + new_block.setPos(self.mapToScene(event.position().toPoint())) + + event.acceptProposedAction() + def get_stack_extremity(self, block, direction): curr = block @@ -3369,49 +3537,126 @@ class TopologyCanvas(QGraphicsView): return curr + def keyPressEvent(self, event): + if event.key() == Qt.Key.Key_F7: + self.diagnostic_dump() + super().keyPressEvent(event) + + + def diagnostic_dump(self): + print(f"\n{'-'*20} LOGICAL STATE DUMP {'-'*20}") + blocks = [item for item in self.scene.items() if hasattr(item, 'data') and item.data(0) == "p_block"] + + if not blocks: + print("No blocks found in scene.") + return + + for i, block in enumerate(blocks): + parent_name = block.parent_block.label_text if block.parent_block else "None" + child_name = block.child_block.label_text if block.child_block else "None" + + # Check for 'Half-Broken' links (Asymmetry) + warning = "" + if block.parent_block and block.parent_block.child_block is not block: + warning = " [!] ASYMMETRY: Parent doesn't recognize this child" + if block.child_block and block.child_block.parent_block is not block: + warning = " [!] ASYMMETRY: Child doesn't recognize this parent" + + print(f"[{i}] BLOCK: {block.label_text}") + print(f" - Parent: {parent_name}") + print(f" - Child: {child_name}{warning}") + print(f" - Selected: {block.isSelected()}") + print(f"{'-'*60}\n") + + + def perform_snap(self, dragged_item, mouse_pos): - print(f"\n{'='*60}\nSNAP ATTEMPT: {dragged_item.label_text} ({dragged_item.b_type})") + print(f"\n{'='*60}\n[DEBUG] STARTING SNAP: {dragged_item.label_text}") + print(f"[DEBUG] Mouse Release Pos: {mouse_pos}") + + potential_targets = self.scene.items(mouse_pos) + target_block = None - # 1. Identify target block on canvas - search_rect = QRectF(mouse_pos.x() - 10, mouse_pos.y() - 10, 20, 20) - items = self.scene.items(search_rect) - - entry_target = None - for item in items: - if isinstance(item, PuzzleBlock) and not item.isSelected(): - entry_target = item + for item in potential_targets: + # Ignore the item we are dragging + if item is dragged_item: + continue + + # PROXY CHECK: If it's a proxy, we want the block it belongs to + # Instead of walking up, check if the item itself IS the block + if item.data(0) == "p_block": + target_block = item break + + # If it's a child of a block (like a label), get the parent safely + p = item.parentItem() + if p and p.data(0) == "p_block": + target_block = p + break + + if not target_block: + return False - if not entry_target: - print(f"DEBUG: No valid target under mouse at {mouse_pos}. Snap failed.") + # 1. Find the ultimate roots of both stacks + dragged_root = self.get_stack_extremity(dragged_item, "up") + dragged_family = set() + curr = dragged_root + while curr: + dragged_family.add(curr) + curr = curr.child_block + if curr == dragged_root: break + + # 2. Find a target that is NOT in that family + potential_targets = self.scene.items(mouse_pos) + target_block = None + + for item in potential_targets: + # Safely get the block object + candidate = None + if item.data(0) == "p_block": + candidate = item + else: + p = item.parentItem() + if p and p.data(0) == "p_block": + candidate = p + + # CRITICAL: Target cannot be part of the stack we are holding + if candidate and candidate not in dragged_family: + target_block = candidate + break + + if not target_block: return False - print(f"DEBUG: Entry target identified: {entry_target.label_text} ({entry_target.b_type}) at {entry_target.scenePos()}") + # 3. Double-check logical roots (The "Atomic" check) + target_root = self.get_stack_extremity(target_block, "up") + if dragged_root == target_root: + print(f"DEBUG: Still shares root {dragged_root.label_text}") + return False - # 2. Find the head/tail of the targeted stack - head = self.get_stack_extremity(entry_target, "up") - tail = self.get_stack_extremity(entry_target, "down") - print(f"DEBUG: Stack Extremities -> Head: {head.label_text} | Tail: {tail.label_text}") + + + print(f"[DEBUG] Processing stack-to-stack connection: {dragged_item.label_text} -> {target_block.label_text}") + + head = self.get_stack_extremity(target_block, "up") + tail = self.get_stack_extremity(target_block, "down") + print(f"[DEBUG] Stack Extremities -> Head: {head.label_text} | Tail: {tail.label_text}") - # CASE 1: Snap ABOVE the stack (Dragged item becomes PARENT) - # Condition: Dragged item isn't a Finish, and the stack head is open. if dragged_item.b_type != "end" and head.parent_block is None: - if head.b_type != "begin": - print(f"DEBUG: Logic Trigger -> Snap {dragged_item.label_text} ABOVE {head.label_text}") + if head.b_type != "begin": + print(f"[DEBUG] Snap Trigger: {dragged_item.label_text} ABOVE {head.label_text}") return self.connect_blocks(dragged_item, head, mode="upward") else: - print("DEBUG: Snap Rejected -> Cannot snap above a 'Begin' block.") + print("[DEBUG] SNAP REJECTED: Cannot snap above a 'Begin' block.") - # CASE 2: Snap BELOW the stack (Dragged item becomes CHILD) - # Condition: Dragged item isn't a Begin, and the stack tail is open. if dragged_item.b_type != "begin" and tail.child_block is None: if tail.b_type != "end": - print(f"DEBUG: Logic Trigger -> Snap {dragged_item.label_text} BELOW {tail.label_text}") + print(f"[DEBUG] Snap Trigger: {dragged_item.label_text} BELOW {tail.label_text}") return self.connect_blocks(tail, dragged_item, mode="downward") else: - print("DEBUG: Snap Rejected -> Cannot snap below a 'Finish' block.") + print("[DEBUG] SNAP REJECTED: Cannot snap below a 'Finish' block.") - print("DEBUG: No valid connection rules met for this pair.") + print(f"[DEBUG] SNAP FAILED: {dragged_item.label_text} found no valid connection point on {target_block.label_text}") return False @@ -3424,7 +3669,6 @@ class TopologyCanvas(QGraphicsView): return False if mode == "upward": - # The 'child' (already on canvas) is the anchor anchor_pos = child.scenePos() if anchor_pos.isNull(): print(f"DEBUG: UPWARD ERROR -> {child.label_text} scenePos is Null. Aborting.") @@ -3435,7 +3679,6 @@ class TopologyCanvas(QGraphicsView): print(f"DEBUG: Physical Move -> {parent.label_text} set to {new_parent_pos} (Above {child.label_text})") else: - # The 'parent' (already on canvas) is the anchor anchor_pos = parent.scenePos() if anchor_pos.isNull(): print(f"DEBUG: DOWNWARD ERROR -> {parent.label_text} scenePos is Null. Aborting.") @@ -3445,122 +3688,50 @@ class TopologyCanvas(QGraphicsView): child.setPos(new_child_pos) print(f"DEBUG: Physical Move -> {child.label_text} set to {new_child_pos} (Below {parent.label_text})") - # Logical Link parent.child_block = child child.parent_block = parent print(f"DEBUG: Logical Link Established -> {parent.label_text}.child = {child.label_text}") - # Chain Sync self.ripple_move(child) print(f"SUCCESS: {parent.label_text} and {child.label_text} are now connected.") return True + def ripple_move(self, start_block): - """Forces all downstream blocks to align with their parents.""" curr = start_block + visited = set() # Track to prevent infinite loops print(f"DEBUG: Starting Ripple Move from {start_block.label_text}") + while curr and curr.child_block: + if curr in visited: + print("CRITICAL: Circular link detected in ripple! Breaking.") + break + visited.add(curr) + next_block = curr.child_block expected_pos = curr.scenePos() + QPointF(0, curr.height) if next_block.scenePos() != expected_pos: print(f"DEBUG: Ripple Sync -> Moving {next_block.label_text} to {expected_pos}") + next_block.setPos(expected_pos) curr = next_block print("DEBUG: Ripple Move Complete.") - - def keyPressEvent(self, event): - """Catches the F7 key to print current logical graphs.""" - if event.key() == Qt.Key_F7: - self.print_logical_graphs() - else: - # Ensure normal key events (like Ctrl for multi-select) still work - super().keyPressEvent(event) - def print_logical_graphs(self): - """Crawls the logical links and prints all current stacks.""" - print(f"\n{'='*60}\nCURRENT LOGICAL GRAPHS (F7 Triggered)") - - # Grab all puzzle blocks currently in the scene - blocks = [item for item in self.scene.items() if hasattr(item, 'b_type')] - - # Find the 'Heads' (blocks that have no parent) - heads = [b for b in blocks if getattr(b, 'parent_block', None) is None] - - if not heads: - print("No standalone blocks or stacks found on canvas.") - print(f"{'='*60}\n") - return - # Trace down from every head to map the stacks - for i, head in enumerate(heads): - chain = [] - curr = head - while curr: - # Format: [Label (type)] - chain.append(f"[{curr.label_text} ({curr.b_type})]") - curr = getattr(curr, 'child_block', None) - - print(f"Stack {i + 1}: " + " -> ".join(chain)) - - print(f"{'='*60}\n") - def dropEvent(self, event): - raw_data = event.mimeData().text() - if "|" not in raw_data: return - - if isinstance(event.source(), BlockLibrary): - block_type, label = raw_data.split("|") - drop_pos = self.mapToScene(event.position().toPoint()) - - new_block = PuzzleBlock(block_type, label) - # Add to scene first (invisible/no pos set yet) - self.scene.addItem(new_block) - - # Attempt to snap it immediately - snapped = self.perform_snap(new_block, drop_pos) - - # If it didn't snap to a stack, place it exactly at the mouse - if not snapped: - new_block.setPos(drop_pos) - - self.validate_topology() - event.acceptProposedAction() - def dragEnterEvent(self, event): - if event.mimeData().hasText(): - event.acceptProposedAction() - def dragMoveEvent(self, event): - if event.mimeData().hasText(): - event.acceptProposedAction() - - def validate_topology(self): - all_items = [i for i in self.scene.items() if isinstance(i, PuzzleBlock)] - has_begin = any(i.b_type == "begin" for i in all_items) - has_end = any(i.b_type == "end" for i in all_items) - - warnings = [] - - if not has_begin: - warnings.append("⚠️ Missing 'Begin' block (No data source defined).") - if not has_end: - warnings.append("⚠️ Missing 'Finish' block (Data will not be saved).") - - if warnings: - status_text = "\n".join(warnings) - #self.parent().info_label.setText(f"{status_text}") - else: - pass - #self.parent().info_label.setText("✅ Configuration Valid") class ModelParameterConfigurationTab(QWidget): - def __init__(self): + def __init__(self, stringy): super().__init__() + + self.mode = stringy self.setStyleSheet(""" QMainWindow, QWidget#centralWidget { @@ -3635,7 +3806,7 @@ class ModelParameterConfigurationTab(QWidget): sidebar_container = QWidget() sidebar_layout = QVBoxLayout(sidebar_container) - self.block_library = BlockLibrary(parent_tab=self) + self.block_library = BlockLibrary(parent_tab=self, stringy=self.mode) # Reuse your existing Inspector logic self.inspector_scroll = QScrollArea()