From 6c1c4596dfeaaa55818dcbb5898bedb89bc699af Mon Sep 17 00:00:00 2001 From: tyler Date: Fri, 1 May 2026 15:04:33 -0700 Subject: [PATCH] more changes to config editor + funny bug --- main.py | 311 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 244 insertions(+), 67 deletions(-) diff --git a/main.py b/main.py index 5136c88..3ee0b47 100644 --- a/main.py +++ b/main.py @@ -3168,7 +3168,10 @@ class PuzzleBlock(QGraphicsPathItem): self.width = 160 self.height = 50 self.tab_size = 15 - + + self.parent_block = None + self.child_block = None + self.setFlags( QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | @@ -3224,6 +3227,43 @@ 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 + + return super().itemChange(change, value) class BlockLibrary(QListWidget): def __init__(self, parent_tab): @@ -3251,21 +3291,6 @@ class BlockLibrary(QListWidget): print(f"DEBUG: Drag finished with result: {result}") - if result != Qt.IgnoreAction: - self.finalize_drop_snapping() - - def finalize_drop_snapping(self): - # Grab all blocks and sort by their index in the scene (newest is usually last) - all_items = self.parent_tab.canvas.scene.items() - puzzle_blocks = [i for i in all_items if isinstance(i, PuzzleBlock)] - - if puzzle_blocks: - # We want the block that was just dropped - # Usually the first in scene.items() is the top-most/newest - newest_block = puzzle_blocks[0] - - # Call the method that was missing - self.parent_tab.canvas.perform_snap(newest_block, newest_block.scenePos()) from PySide6.QtWidgets import QGraphicsLineItem, QGraphicsRectItem, QGraphicsSimpleTextItem @@ -3284,72 +3309,224 @@ class TopologyCanvas(QGraphicsView): # 1. Native release super().mouseReleaseEvent(event) - # 2. Sidebar Trash Logic - global_pos = event.globalPosition().toPoint() - widget_under_mouse = QApplication.widgetAt(global_pos) - - print("huh") + if getattr(self, '_processing_release', False): + return - 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() + self._processing_release = True + # --------------------------- - if is_over_library: - print("Action: Trashing selected items") - for item in self.scene.selectedItems(): - if isinstance(item, PuzzleBlock): - self.scene.removeItem(item) + 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") + for item in self.scene.selectedItems(): + if isinstance(item, PuzzleBlock): + 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() + + + finally: + # Always reset the guard so the next real click works + self._processing_release = False + + + + def get_stack_extremity(self, block, direction): + curr = block + visited = {curr} + for _ in range(50): + next_node = curr.parent_block if direction == "up" else curr.child_block + if next_node and next_node not in visited: + curr = next_node + visited.add(curr) + else: + break + return curr + + + def perform_snap(self, dragged_item, mouse_pos): + print(f"\n{'='*60}\nSNAP ATTEMPT: {dragged_item.label_text} ({dragged_item.b_type})") + + # 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 + break + + if not entry_target: + print(f"DEBUG: No valid target under mouse at {mouse_pos}. Snap failed.") + return False + + print(f"DEBUG: Entry target identified: {entry_target.label_text} ({entry_target.b_type}) at {entry_target.scenePos()}") + + # 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}") + + # 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}") + return self.connect_blocks(dragged_item, head, mode="upward") + else: + 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}") + return self.connect_blocks(tail, dragged_item, mode="downward") + else: + print("DEBUG: Snap Rejected -> Cannot snap below a 'Finish' block.") + + print("DEBUG: No valid connection rules met for this pair.") + return False + + + def connect_blocks(self, parent, child, mode="downward"): + """Links blocks and forces physical alignment based on a stable anchor.""" + print(f"DEBUG: Entering connect_blocks | Parent: {parent.label_text} | Child: {child.label_text} | Mode: {mode}") + + if not parent or not child: + print("DEBUG: Connection Aborted -> Missing reference to parent or child.") + 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.") + return False + + new_parent_pos = anchor_pos - QPointF(0, parent.height) + parent.setPos(new_parent_pos) + 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.") + return False + + new_child_pos = anchor_pos + QPointF(0, parent.height) + 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 + print(f"DEBUG: Starting Ripple Move from {start_block.label_text}") + while curr and curr.child_block: + 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 - selected = [i for i in self.scene.selectedItems() if isinstance(i, PuzzleBlock)] - if selected: - dragged_item = selected[0] - # Use scenePos() for the internal coordinate math - self.perform_snap(dragged_item, dragged_item.scenePos()) - - self.validate_topology() - - - def perform_snap(self, dragged_item, pos): - """ This is the method the library is looking for. """ - overlap = 0 - # Create a search area around the block's current position - snap_rect = QRectF(pos.x() - 25, pos.y() - 25, 200, 100) - - items_nearby = self.scene.items(snap_rect) - for target in items_nearby: - if isinstance(target, PuzzleBlock) and target != dragged_item: - - # SNAP BELOW: Dragged Item is BELOW the target - if dragged_item.b_type in ["middle", "end"] and target.b_type in ["begin", "middle"]: - dragged_item.setPos(target.scenePos() + QPointF(0, target.height - overlap)) - print(f"DEBUG: {dragged_item.label_text} snapped to BOTTOM of {target.label_text}") - return True - - # SNAP ABOVE: Dragged Item is ABOVE the target - elif dragged_item.b_type in ["begin", "middle"] and target.b_type in ["middle", "end"]: - dragged_item.setPos(target.scenePos() - QPointF(0, dragged_item.height - overlap)) - print(f"DEBUG: {dragged_item.label_text} snapped to TOP of {target.label_text}") - return True - return False + # 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 - # Only process drops from the Library (New blocks) if isinstance(event.source(), BlockLibrary): block_type, label = raw_data.split("|") - pos = self.mapToScene(event.position().toPoint()) + drop_pos = self.mapToScene(event.position().toPoint()) new_block = PuzzleBlock(block_type, label) - new_block.setPos(pos) + # 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()