From 67243649ef4270eb2d64c43bf211d895c4d651c8 Mon Sep 17 00:00:00 2001 From: Tyler Date: Mon, 4 May 2026 16:10:37 -0700 Subject: [PATCH] config editor further progress --- blocks.json | 53 +++++++++++++-- main.py | 181 +++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 213 insertions(+), 21 deletions(-) diff --git a/blocks.json b/blocks.json index e383ff4..1cd235f 100644 --- a/blocks.json +++ b/blocks.json @@ -2,16 +2,57 @@ "individual": [ { "name": "Start Node", "type": "begin" }, { - "name": "Hybrid Processor", + "name": "Average", "type": "middle", "fields": [ - {"label": "Threshold", "type": "number"}, - {"label": "Tag", "type": "string"}, - {"label": "Sub-Logic", "type": "slot"} + {"label": "Joint 1", "type": "slot"}, + {"label": "Joint 2", "type": "slot"} + ] + }, + { + "name": "Distance", + "type": "middle", + "fields": [ + {"label": "Joint 1", "type": "slot"}, + {"label": "Joint 2", "type": "slot"} + ] + }, + { + "name": "Z-Diff", + "type": "middle", + "fields": [ + {"label": "Joint 1", "type": "slot"}, + {"label": "Joint 2", "type": "slot"} + ] + }, + { + "name": "Angle", + "type": "middle", + "fields": [ + {"label": "Joint 1", "type": "slot"}, + {"label": "Joint 2", "type": "slot"}, + {"label": "Joint 3", "type": "slot"} ] }, - { "name": "Export Node", "type": "end" }, - { "name": "like this", "type": "logic" } + + { "name": "End", "type": "end" }, + { "name": "Joint 1", "type": "logic" }, + { "name": "Joint 2", "type": "logic" }, + { "name": "Joint 3", "type": "logic" }, + { "name": "Joint 4", "type": "logic" }, + { "name": "Joint 5", "type": "logic" }, + { "name": "Joint 6", "type": "logic" }, + { "name": "Joint 7", "type": "logic" }, + { "name": "Joint 8", "type": "logic" }, + { "name": "Joint 9", "type": "logic" }, + { "name": "Joint 10", "type": "logic" }, + { "name": "Joint 11", "type": "logic" }, + { "name": "Joint 12", "type": "logic" }, + { "name": "Joint 13", "type": "logic" }, + { "name": "Joint 14", "type": "logic" }, + { "name": "Joint 15", "type": "logic" }, + { "name": "Joint 16", "type": "logic" }, + { "name": "Joint 17", "type": "logic" } ] } \ No newline at end of file diff --git a/main.py b/main.py index b9a6b6d..545542c 100644 --- a/main.py +++ b/main.py @@ -3174,6 +3174,9 @@ class PuzzleBlock(QGraphicsPathItem): self.parent_block = None self.child_block = None + + self.host_block = None + self.slots = {} self.setFlags( QGraphicsItem.GraphicsItemFlag.ItemIsMovable | @@ -3244,6 +3247,8 @@ class PuzzleBlock(QGraphicsPathItem): key = f.get('label', 'slot') self.inputs[key] = slot_label self.original_inputs[key] = slot_label + + self.slots[key] = None self.proxy = QGraphicsProxyWidget(self) self.proxy.setZValue(10) @@ -3324,18 +3329,28 @@ class PuzzleBlock(QGraphicsPathItem): def mousePressEvent(self, event): # Only handle visual layering here. Do NOT detach links. + self._press_offset = event.pos() self.setZValue(100) super().mousePressEvent(event) def mouseReleaseEvent(self, event): + if getattr(self, '_detach_host', None): + grab_offset = self._detach_grab_offset + + self.setParentItem(None) + self.setPos(event.scenePos() - grab_offset) + + self._detach_host = None + self._detach_grab_offset = None + # Drop it back down to standard Z-level - self.setZValue(0) + self.setZValue(20 if getattr(self, 'host_block', None) else 0) super().mouseReleaseEvent(event) def itemChange(self, change, value): - if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged: + if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange: if self.scene() and self.scene().mouseGrabberItem() is self: # 1. Sever Parent Link (Both ways) @@ -3350,6 +3365,21 @@ class PuzzleBlock(QGraphicsPathItem): self.child_block.parent_block = None # Tell child I'm gone self.child_block = None # Forget my child + if getattr(self, 'host_block', None): + host = self.host_block + + if not getattr(self, '_detach_host', None): + self._detach_host = host + self._detach_grab_offset = self._press_offset + + for k, v in host.slots.items(): + if v == self: + host.slots[k] = None + host.inputs[k].show() + break + + self.host_block = None + return super().itemChange(change, value) @@ -3507,13 +3537,8 @@ class TopologyCanvas(QGraphicsView): 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) + + new_block = PuzzleBlock(block_type, label, fields=fields) self.scene.addItem(new_block) @@ -3540,11 +3565,48 @@ class TopologyCanvas(QGraphicsView): def keyPressEvent(self, event): if event.key() == Qt.Key.Key_F7: self.diagnostic_dump() + if event.key() == Qt.Key.Key_F8: + self.print_logical_graphs() super().keyPressEvent(event) + def print_logical_graphs(self): + """Crawls the logical links and prints all current stacks.""" + print(f"\n{'='*60}\nCURRENT LOGICAL GRAPHS (F8 Triggered)") + + blocks = [item for item in self.scene.items() if hasattr(item, 'b_type')] + + # Heads are blocks with no parent AND no host (filtering out embedded logic blocks) + heads = [b for b in blocks if getattr(b, 'parent_block', None) is None and getattr(b, 'host_block', None) is None] + + if not heads: + print("No standalone blocks or stacks found on canvas.") + print(f"{'='*60}\n") + return + + for i, head in enumerate(heads): + chain = [] + curr = head + while curr: + # Format: [Label (type)] + node_str = f"[{curr.label_text} ({curr.b_type})]" + + # Check for embedded logic blocks and append them visually + if getattr(curr, 'slots', None): + embedded = [f"{k}: {v.label_text}" for k, v in curr.slots.items() if v] + if embedded: + node_str += f" [Slots -> {', '.join(embedded)}]" + + chain.append(node_str) + curr = getattr(curr, 'child_block', None) + + print(f"Stack {i + 1}: " + " -> ".join(chain)) + + print(f"{'='*60}\n") + + def diagnostic_dump(self): - print(f"\n{'-'*20} LOGICAL STATE DUMP {'-'*20}") + print(f"\n{'-'*20} LOGICAL STATE DUMP (F7 Triggered) {'-'*20}") blocks = [item for item in self.scene.items() if hasattr(item, 'data') and item.data(0) == "p_block"] if not blocks: @@ -3554,18 +3616,29 @@ class TopologyCanvas(QGraphicsView): 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" + host_name = block.host_block.label_text if getattr(block, 'host_block', None) 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" + 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" + warning += " [!] ASYMMETRY: Child doesn't recognize this parent." + if getattr(block, 'host_block', None) and block not in block.host_block.slots.values(): + warning += " [!] ASYMMETRY: Host doesn't recognize this logic block in its slots." - print(f"[{i}] BLOCK: {block.label_text}") + print(f"[{i}] BLOCK: {block.label_text} ({block.b_type})") print(f" - Parent: {parent_name}") - print(f" - Child: {child_name}{warning}") - print(f" - Selected: {block.isSelected()}") + print(f" - Child: {child_name}") + + if block.b_type == "logic": + print(f" - Host: {host_name}") + elif hasattr(block, 'slots') and block.slots: + slots_info = {k: (v.label_text if v else "Empty") for k, v in block.slots.items()} + print(f" - Slots: {slots_info}") + + if warning: + print(f" - WARNING:{warning}") print(f"{'-'*60}\n") @@ -3576,6 +3649,45 @@ class TopologyCanvas(QGraphicsView): potential_targets = self.scene.items(mouse_pos) target_block = None + + if dragged_item.b_type == "logic": + for item in potential_targets: + if item is dragged_item: continue + + # Safely locate the block object behind whatever was hit + host_candidate = None + if item.data(0) == "p_block": + host_candidate = item + else: + p = item.parentItem() + if p and p.data(0) == "p_block": + host_candidate = p + + if host_candidate and hasattr(host_candidate, 'slots'): + for slot_key, slot_label in host_candidate.inputs.items(): + # Check if this specific slot widget contains the mouse release point + + + proxy = host_candidate.proxy + + # 2. Map from Scene -> Proxy -> Widget + # mapFromScene gives us the position relative to the proxy + point_in_proxy = proxy.mapFromScene(mouse_pos) + + # mapFromParent translates from proxy space to the slot_label's internal space + local_mouse_pos = slot_label.mapFromParent(point_in_proxy) + + # 3. Perform the hit test + if slot_label.rect().contains(local_mouse_pos.toPoint()): + # STRICTURE: Only proceed if the slot dictionary explicitly shows None + if host_candidate.slots.get(slot_key) is None: + return self.embed_logic_block(dragged_item, host_candidate, slot_key) + else: + print(f"[DEBUG] Slot {slot_key} is already occupied!") + return None + + print("[DEBUG] SNAP FAILED: No empty slot found for logic block.") + return False for item in potential_targets: # Ignore the item we are dragging @@ -3659,6 +3771,45 @@ class TopologyCanvas(QGraphicsView): print(f"[DEBUG] SNAP FAILED: {dragged_item.label_text} found no valid connection point on {target_block.label_text}") return False + + def embed_logic_block(self, logic_block, host_block, slot_key): + # 1. Logical Link + host_block.slots[slot_key] = logic_block + logic_block.host_block = host_block + logic_block.current_slot = slot_key + + # 2. Re-parent + logic_block.setParentItem(host_block) + + # 3. Precise Coordinate Mapping + target_widget = host_block.inputs[slot_key] + proxy = host_block.proxy + + # Get the rect of the QLineEdit relative to the proxy widget + widget_rect = proxy.subWidgetRect(target_widget) + + # Map that rectangle's top-left point from "Proxy Space" to "Host Block Space" + # This accounts for title bars, margins, and internal layout shifts + slot_origin_in_host = proxy.mapToItem(host_block, widget_rect.topLeft()) + + # 4. Alignment Calculation + # X: Set a fixed margin from the left edge of the host block + final_x = 10 + + # Y: Center the orange block vertically within the slot's actual height + # Use boundingRect().height() to ensure we have the real rendered size + logic_height = logic_block.boundingRect().height() + y_center_offset = (target_widget.height() - logic_height) / 2 + + final_y = slot_origin_in_host.y() + y_center_offset + + # 5. Apply Position + logic_block.setPos(final_x, final_y) + logic_block.setZValue(host_block.zValue() + 1) + + return True + + def connect_blocks(self, parent, child, mode="downward"): """Links blocks and forces physical alignment based on a stable anchor."""