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()