config editor changes

This commit is contained in:
2026-05-04 00:43:05 -07:00
parent 6c1c4596df
commit 5b062611c6
2 changed files with 387 additions and 199 deletions
+17
View File
@@ -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" }
]
}
+370 -199
View File
@@ -34,11 +34,11 @@ from pose_worker import run_pose_analysis
from batch_processing import BatchProcessorDialog from batch_processing import BatchProcessorDialog
import PySide6 import PySide6
from PySide6.QtWidgets import (QApplication, QDoubleSpinBox, QGraphicsItem, QLineEdit, QListWidget, QListWidgetItem, QMainWindow, QProgressDialog, QStyleOptionGraphicsItem, QTabBar, QWidget, QVBoxLayout, QGraphicsView, QGraphicsScene, 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, QHBoxLayout, QSplitter, QLabel, QPushButton, QComboBox, QInputDialog, QGraphicsRectItem,
QFileDialog, QScrollArea, QMessageBox, QSlider, QTextEdit, QGroupBox, QGridLayout, QCheckBox, QTabWidget, QProgressBar) 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.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.QtMultimedia import QMediaPlayer, QAudioOutput
from PySide6.QtMultimediaWidgets import QGraphicsVideoItem from PySide6.QtMultimediaWidgets import QGraphicsVideoItem
@@ -2008,13 +2008,16 @@ class PremiereWindow(QMainWindow):
train_act = QAction("Train Model", self) train_act = QAction("Train Model", self)
train_act.triggered.connect(self.open_train_model_dialog) train_act.triggered.connect(self.open_train_model_dialog)
config_act = QAction("Configuration Editor", self) config_act = QAction("Configuration (Ind.)", self)
config_act.triggered.connect(self.open_model_configuration_tab) 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(load_act)
menu.addAction(train_act) menu.addAction(train_act)
menu.addAction(config_act) menu.addAction(config_act)
menu.addAction(config_act2)
# Show the menu right under the mouse cursor # Show the menu right under the mouse cursor
menu.exec(QCursor.pos()) 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.btn_train.clicked.connect(self.handle_start_training)
self.train_window.show() self.train_window.show()
def open_model_configuration_tab(self): def open_model_configuration_tab(self, stringy):
self.handle_model_configuration_session() self.handle_model_configuration_session(stringy)
def open_export_data_dialog(self): def open_export_data_dialog(self):
if self.export_window is None or not self.export_window.isVisible(): if self.export_window is None or not self.export_window.isVisible():
@@ -2169,11 +2172,11 @@ class PremiereWindow(QMainWindow):
self.tabs.setCurrentIndex(new_idx) self.tabs.setCurrentIndex(new_idx)
def handle_model_configuration_session(self): def handle_model_configuration_session(self, stringy):
new_tab = ModelParameterConfigurationTab() new_tab = ModelParameterConfigurationTab(stringy)
# 4. Handle Tab Placement (Keep '+' at the end) # 4. Handle Tab Placement (Keep '+' at the end)
tab_name = "Configuration Editor" tab_name = f"Configuration Editor - {stringy}"
plus_idx = self.tabs.count() - 1 plus_idx = self.tabs.count() - 1
new_idx = self.tabs.insertTab(plus_idx, new_tab, tab_name) 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): 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) super().__init__(parent_item)
self.b_type = b_type # "begin", "middle", "end" self.b_type = b_type # "begin", "middle", "end"
self.label_text = label self.label_text = label
@@ -3178,19 +3181,108 @@ class PuzzleBlock(QGraphicsPathItem):
QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges 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 # 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.setBrush(QBrush(QColor(colors.get(b_type, "#555"))))
self.setPen(QPen(QColor("#ffffff"), 1)) self.setPen(QPen(QColor("#ffffff"), 1))
self.label_item = QGraphicsSimpleTextItem(self.label_text, self) self.label_item = QGraphicsSimpleTextItem(self.label_text, self)
self.label_item.setBrush(QColor("white")) 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): def update_path(self):
"""This changes how they visually appear. Likely no logic broken here."""
self.prepareGeometryChange() 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 = QPainterPath()
path.moveTo(0, 0) path.moveTo(0, 0)
@@ -3200,18 +3292,18 @@ class PuzzleBlock(QGraphicsPathItem):
path.lineTo(30, 0) path.lineTo(30, 0)
# Nub (Male) - Sweep is negative to curve OUTWARD # Nub (Male) - Sweep is negative to curve OUTWARD
path.arcTo(30, -self.tab_size/2, self.tab_size, self.tab_size, 180, -180) 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 # 2. RIGHT EDGE
path.lineTo(self.width, self.height) path.lineTo(display_width, display_height)
# 3. BOTTOM EDGE # 3. BOTTOM EDGE
# Begin and Middle blocks now have a "Hole" (Female) on bottom # Begin and Middle blocks now have a "Hole" (Female) on bottom
if self.b_type in ["begin", "middle"]: 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 # 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.arcTo(30, display_height - self.tab_size/2, self.tab_size, self.tab_size, 0, 180)
path.lineTo(0, self.height) path.lineTo(0, display_height)
# 4. LEFT EDGE # 4. LEFT EDGE
path.lineTo(0, 0) path.lineTo(0, 0)
@@ -3219,7 +3311,9 @@ class PuzzleBlock(QGraphicsPathItem):
path.closeSubpath() path.closeSubpath()
self.setPath(path) self.setPath(path)
def mouseDoubleClickEvent(self, event): def mouseDoubleClickEvent(self, event):
'''Simply to edit the name of a start box.'''
if self.b_type == "begin": if self.b_type == "begin":
new_name, ok = QInputDialog.getText(None, "Rename Step", "Enter name:", text=self.label_text) new_name, ok = QInputDialog.getText(None, "Rename Step", "Enter name:", text=self.label_text)
if ok and new_name: if ok and new_name:
@@ -3227,63 +3321,94 @@ class PuzzleBlock(QGraphicsPathItem):
self.label_item.setText(new_name) self.label_item.setText(new_name)
super().mouseDoubleClickEvent(event) 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): def mousePressEvent(self, event):
# Only handle visual layering here. Do NOT detach links. # Only handle visual layering here. Do NOT detach links.
self.setZValue(100) self.setZValue(100)
super().mousePressEvent(event) super().mousePressEvent(event)
def mouseReleaseEvent(self, event): def mouseReleaseEvent(self, event):
# Drop it back down to standard Z-level # Drop it back down to standard Z-level
self.setZValue(0) self.setZValue(0)
super().mouseReleaseEvent(event) super().mouseReleaseEvent(event)
def itemChange(self, change, value): def itemChange(self, change, value):
if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged: if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
# If this block is moving, check if its neighbors are NOT moving with it. if self.scene() and self.scene().mouseGrabberItem() is self:
# 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
# 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) return super().itemChange(change, value)
class BlockLibrary(QListWidget): class BlockLibrary(QListWidget):
def __init__(self, parent_tab): def __init__(self, parent_tab, stringy):
super().__init__() super().__init__()
self.setDragEnabled(True) self.setDragEnabled(True)
self.add_item("Begin Event", "begin")
self.add_item("Process Step", "middle") self.mode = stringy
self.add_item("Finish/Export", "end")
self.parent_tab = parent_tab # Reference to main tab to access canvas 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 = QListWidgetItem(label)
item.setData(Qt.UserRole, block_type) item.setData(Qt.UserRole, block_type)
# NEW: Store fields list in the item
item.setData(Qt.UserRole + 1, fields)
self.addItem(item) self.addItem(item)
def startDrag(self, supportedActions): def startDrag(self, supportedActions):
item = self.currentItem() item = self.currentItem()
if not item: return
mime_data = QMimeData() 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 = QDrag(self)
drag.setMimeData(mime_data) drag.setMimeData(mime_data)
@@ -3293,9 +3418,13 @@ class BlockLibrary(QListWidget):
from PySide6.QtWidgets import QGraphicsLineItem, QGraphicsRectItem, QGraphicsSimpleTextItem from PySide6.QtWidgets import QGraphicsLineItem, QGraphicsRectItem, QGraphicsSimpleTextItem
from PySide6.QtGui import QPen, QColor, QBrush from PySide6.QtGui import QPen, QColor, QBrush
class TopologyCanvas(QGraphicsView): class TopologyCanvas(QGraphicsView):
def __init__(self): def __init__(self):
self.scene = QGraphicsScene() self.scene = QGraphicsScene()
@@ -3303,58 +3432,97 @@ class TopologyCanvas(QGraphicsView):
self.setAcceptDrops(True) self.setAcceptDrops(True)
self.scene.setSceneRect(0, 0, 2000, 2000) 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): def mouseReleaseEvent(self, event):
# 1. Native release # Let the native C++ event finish first
super().mouseReleaseEvent(event) super().mouseReleaseEvent(event)
if getattr(self, '_processing_release', False): if self._processing_release:
return 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 self._processing_release = True
# ---------------------------
try: try:
# 1. Cleaner Trash Check (Assume you have a ref to your sidebar)
# 2. Sidebar Trash Logic # Replacing the 'while widget' loop with a geometry check is much safer
global_pos = event.globalPosition().toPoint() if hasattr(self, 'library_ref') and self.library_ref.rect().contains(self.library_ref.mapFromGlobal(global_pos)):
widget_under_mouse = QApplication.widgetAt(global_pos) print("Action: Trashing items")
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")
for item in self.scene.selectedItems(): for item in self.scene.selectedItems():
if isinstance(item, PuzzleBlock): self.scene.removeItem(item)
self.scene.removeItem(item)
return return
item_under_mouse = self.itemAt(event.position().toPoint()) # 2. Logic for Snapping
items_at_pos = self.scene.items(scene_pos)
if isinstance(item_under_mouse, PuzzleBlock): driver = next((i for i in items_at_pos if isinstance(i, PuzzleBlock) and i.isSelected()), None)
print(f"DEBUG: Snapping initiated via {item_under_mouse.label_text}") target = next((i for i in items_at_pos if isinstance(i, PuzzleBlock) and not i.isSelected()), None)
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()
if driver and target:
self.perform_snap(driver, scene_pos)
finally: finally:
# Always reset the guard so the next real click works
self._processing_release = False 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): def get_stack_extremity(self, block, direction):
curr = block curr = block
@@ -3369,49 +3537,126 @@ class TopologyCanvas(QGraphicsView):
return curr 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): 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 for item in potential_targets:
search_rect = QRectF(mouse_pos.x() - 10, mouse_pos.y() - 10, 20, 20) # Ignore the item we are dragging
items = self.scene.items(search_rect) if item is dragged_item:
continue
entry_target = None
for item in items: # PROXY CHECK: If it's a proxy, we want the block it belongs to
if isinstance(item, PuzzleBlock) and not item.isSelected(): # Instead of walking up, check if the item itself IS the block
entry_target = item if item.data(0) == "p_block":
target_block = item
break 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: # 1. Find the ultimate roots of both stacks
print(f"DEBUG: No valid target under mouse at {mouse_pos}. Snap failed.") 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 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] Processing stack-to-stack connection: {dragged_item.label_text} -> {target_block.label_text}")
print(f"DEBUG: Stack Extremities -> Head: {head.label_text} | Tail: {tail.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 dragged_item.b_type != "end" and head.parent_block is None:
if head.b_type != "begin": if head.b_type != "begin":
print(f"DEBUG: Logic Trigger -> Snap {dragged_item.label_text} ABOVE {head.label_text}") print(f"[DEBUG] Snap Trigger: {dragged_item.label_text} ABOVE {head.label_text}")
return self.connect_blocks(dragged_item, head, mode="upward") return self.connect_blocks(dragged_item, head, mode="upward")
else: 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 dragged_item.b_type != "begin" and tail.child_block is None:
if tail.b_type != "end": 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") return self.connect_blocks(tail, dragged_item, mode="downward")
else: 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 return False
@@ -3424,7 +3669,6 @@ class TopologyCanvas(QGraphicsView):
return False return False
if mode == "upward": if mode == "upward":
# The 'child' (already on canvas) is the anchor
anchor_pos = child.scenePos() anchor_pos = child.scenePos()
if anchor_pos.isNull(): if anchor_pos.isNull():
print(f"DEBUG: UPWARD ERROR -> {child.label_text} scenePos is Null. Aborting.") 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})") print(f"DEBUG: Physical Move -> {parent.label_text} set to {new_parent_pos} (Above {child.label_text})")
else: else:
# The 'parent' (already on canvas) is the anchor
anchor_pos = parent.scenePos() anchor_pos = parent.scenePos()
if anchor_pos.isNull(): if anchor_pos.isNull():
print(f"DEBUG: DOWNWARD ERROR -> {parent.label_text} scenePos is Null. Aborting.") print(f"DEBUG: DOWNWARD ERROR -> {parent.label_text} scenePos is Null. Aborting.")
@@ -3445,122 +3688,50 @@ class TopologyCanvas(QGraphicsView):
child.setPos(new_child_pos) child.setPos(new_child_pos)
print(f"DEBUG: Physical Move -> {child.label_text} set to {new_child_pos} (Below {parent.label_text})") print(f"DEBUG: Physical Move -> {child.label_text} set to {new_child_pos} (Below {parent.label_text})")
# Logical Link
parent.child_block = child parent.child_block = child
child.parent_block = parent child.parent_block = parent
print(f"DEBUG: Logical Link Established -> {parent.label_text}.child = {child.label_text}") print(f"DEBUG: Logical Link Established -> {parent.label_text}.child = {child.label_text}")
# Chain Sync
self.ripple_move(child) self.ripple_move(child)
print(f"SUCCESS: {parent.label_text} and {child.label_text} are now connected.") print(f"SUCCESS: {parent.label_text} and {child.label_text} are now connected.")
return True return True
def ripple_move(self, start_block): def ripple_move(self, start_block):
"""Forces all downstream blocks to align with their parents."""
curr = start_block curr = start_block
visited = set() # Track to prevent infinite loops
print(f"DEBUG: Starting Ripple Move from {start_block.label_text}") print(f"DEBUG: Starting Ripple Move from {start_block.label_text}")
while curr and curr.child_block: 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 next_block = curr.child_block
expected_pos = curr.scenePos() + QPointF(0, curr.height) expected_pos = curr.scenePos() + QPointF(0, curr.height)
if next_block.scenePos() != expected_pos: if next_block.scenePos() != expected_pos:
print(f"DEBUG: Ripple Sync -> Moving {next_block.label_text} to {expected_pos}") print(f"DEBUG: Ripple Sync -> Moving {next_block.label_text} to {expected_pos}")
next_block.setPos(expected_pos) next_block.setPos(expected_pos)
curr = next_block curr = next_block
print("DEBUG: Ripple Move Complete.") 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"<font color='red'>{status_text}</font>")
else:
pass
#self.parent().info_label.setText("<font color='#00ff00'>✅ Configuration Valid</font>")
class ModelParameterConfigurationTab(QWidget): class ModelParameterConfigurationTab(QWidget):
def __init__(self): def __init__(self, stringy):
super().__init__() super().__init__()
self.mode = stringy
self.setStyleSheet(""" self.setStyleSheet("""
QMainWindow, QWidget#centralWidget { QMainWindow, QWidget#centralWidget {
@@ -3635,7 +3806,7 @@ class ModelParameterConfigurationTab(QWidget):
sidebar_container = QWidget() sidebar_container = QWidget()
sidebar_layout = QVBoxLayout(sidebar_container) 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 # Reuse your existing Inspector logic
self.inspector_scroll = QScrollArea() self.inspector_scroll = QScrollArea()