fun with blocks

This commit is contained in:
2026-05-15 02:51:19 -07:00
parent 17789a7615
commit 66f16d1771
2 changed files with 452 additions and 96 deletions
+24 -19
View File
@@ -1,6 +1,6 @@
{ {
"individual": [ "individual": [
{ "name": "New Event", "type": "begin" }, { "name": "New Event", "type": "begin", "sub_type": "created_event" },
{ {
"name": "Average", "name": "Average",
"type": "middle", "type": "middle",
@@ -37,24 +37,29 @@
{ "name": "End", "type": "end" }, { "name": "End", "type": "end" },
{ "name": "Joint 1", "type": "logic" }, { "name": "Nose", "type": "logic" },
{ "name": "Joint 2", "type": "logic" }, { "name": "Left Eye", "type": "logic" },
{ "name": "Joint 3", "type": "logic" }, { "name": "Right Eye", "type": "logic" },
{ "name": "Joint 4", "type": "logic" }, { "name": "Left Ear", "type": "logic" },
{ "name": "Joint 5", "type": "logic" }, { "name": "Right Ear", "type": "logic" },
{ "name": "Joint 6", "type": "logic" }, { "name": "Left Shoulder", "type": "logic" },
{ "name": "Joint 7", "type": "logic" }, { "name": "Right Shoulder", "type": "logic" },
{ "name": "Joint 8", "type": "logic" }, { "name": "Left Elbow", "type": "logic" },
{ "name": "Joint 9", "type": "logic" }, { "name": "Right Elbow", "type": "logic" },
{ "name": "Joint 10", "type": "logic" }, { "name": "Left Wrist", "type": "logic" },
{ "name": "Joint 11", "type": "logic" }, { "name": "Right Wrist", "type": "logic" },
{ "name": "Joint 12", "type": "logic" }, { "name": "Left Hip", "type": "logic" },
{ "name": "Joint 13", "type": "logic" }, { "name": "Right Hip", "type": "logic" },
{ "name": "Joint 14", "type": "logic" }, { "name": "Left Knee", "type": "logic" },
{ "name": "Joint 15", "type": "logic" }, { "name": "Right Knee", "type": "logic" },
{ "name": "Joint 16", "type": "logic" }, { "name": "Left Ankle", "type": "logic" },
{ "name": "Joint 17", "type": "logic" }, { "name": "Right Ankle", "type": "logic" },
{ "name": "New Calculated Joint", "type": "begin" } { "name": "New Calculated Joint", "type": "begin", "sub_type": "created_joint" }
],
"group": [
{ "name": "New Activity", "type": "begin" },
{ "name": "End Activity", "type": "end" }
] ]
} }
+423 -72
View File
@@ -34,7 +34,7 @@ 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, QFormLayout, QGraphicsItem, QGraphicsProxyWidget, QGraphicsTextItem, QLineEdit, QListWidget, QListWidgetItem, QMainWindow, QProgressDialog, QSizePolicy, QStyleOptionGraphicsItem, QTabBar, QWidget, QVBoxLayout, QGraphicsView, QGraphicsScene, from PySide6.QtWidgets import (QApplication, QDoubleSpinBox, QFormLayout, QGraphicsItem, QGraphicsProxyWidget, QGraphicsTextItem, QLineEdit, QListWidget, QListWidgetItem, QMainWindow, QProgressDialog, QSizePolicy, QStyleOptionGraphicsItem, QTabBar, QTreeWidget, QTreeWidgetItem, QWidget, QVBoxLayout, QGraphicsView, QGraphicsScene,
QHBoxLayout, QSplitter, QLabel, QPushButton, QComboBox, QInputDialog, QGraphicsRectItem, 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, QObject, Qt, QThread, Signal, QUrl, QRectF, QPointF, QRect, QSizeF, QTimer from PySide6.QtCore import QEvent, QObject, Qt, QThread, Signal, QUrl, QRectF, QPointF, QRect, QSizeF, QTimer
@@ -3165,10 +3165,10 @@ from PySide6.QtWidgets import QGraphicsItem, QGraphicsSimpleTextItem, QInputDial
class BlockSignalProxy(QObject): class BlockSignalProxy(QObject):
nameChanged = Signal(str, str) nameChanged = Signal(str, str)
class PuzzleBlock(QGraphicsPathItem): class PuzzleBlock(QGraphicsPathItem):
def __init__(self, b_type, label, parent_item=None, fields=None, sub_type="standard"):
def __init__(self, b_type, label, parent_item=None, fields=None):
super().__init__(parent_item) super().__init__(parent_item)
self.signals = BlockSignalProxy() self.signals = BlockSignalProxy()
@@ -3178,6 +3178,8 @@ class PuzzleBlock(QGraphicsPathItem):
self.width = 160 self.width = 160
self.height = 50 self.height = 50
self.tab_size = 15 self.tab_size = 15
self.fields = fields if fields is not None else []
self.sub_type = sub_type
self.parent_block = None self.parent_block = None
self.child_block = None self.child_block = None
@@ -3404,15 +3406,38 @@ class PuzzleBlock(QGraphicsPathItem):
self.signals.nameChanged.emit(old_text, new_text) self.signals.nameChanged.emit(old_text, new_text)
class BlockLibrary(QListWidget): def force_resize(self):
"""Recalculates height based on layout and updates the visual path."""
if hasattr(self, 'container') and self.container.layout():
# Force the layout to settle
self.container.layout().activate()
# Update height based on the new size hint
self.height = 50 + self.container.layout().sizeHint().height()
self.update_path()
class BlockLibrary(QTreeWidget):
def __init__(self, parent_tab, stringy): def __init__(self, parent_tab, stringy):
super().__init__() super().__init__()
self.setDragEnabled(True) self.setDragEnabled(True)
self.setHeaderHidden(True) # Hide the "1" header at the top
self.setIndentation(15)
self.setAnimated(True)
self.mode = stringy self.mode = stringy
self.parent_tab = parent_tab # Reference to main tab to access canvas self.parent_tab = parent_tab # Reference to main tab to access canvas
self.categories = {}
self.type_themes = {
"begin": {"label": "ENTRY EVENTS", "color": "#2e7d32"}, # Dark Green
"middle": {"label": "PROCESSORS", "color": "#1565c0"}, # Dark Blue
"end": {"label": "EXPORTS", "color": "#c62828"}, # Dark Red
"logic": {"label": "VARIABLES", "color": "#ef6c00"}, # Dark Orange
"created_event": {"label": "CREATED EVENTS", "color": "#6a1b9a"}, # Deep Purple
"created_joint": {"label": "CALCULATED JOINTS", "color": "#00695c"} # Teal/Blue-Green
}
self.load_blocks_from_json("blocks.json") self.load_blocks_from_json("blocks.json")
@@ -3430,35 +3455,84 @@ class BlockLibrary(QListWidget):
for block in blocks: for block in blocks:
# NEW: Get fields if they exist, otherwise empty list # NEW: Get fields if they exist, otherwise empty list
fields = block.get('fields', []) self.add_item(block['name'], block['type'], block.get('fields', []), block.get('sub_type', "standard"))
self.add_item(block['name'], block['type'], fields)
except Exception as e: except Exception as e:
print(f"Failed to load block library: {e}") print(f"Failed to load block library: {e}")
def add_item(self, label, block_type, fields): def import_individual_events(self):
item = QListWidgetItem(label) individual_config = "config_individual.json"
item.setData(Qt.UserRole, block_type) if not os.path.exists(individual_config): return
# NEW: Store fields list in the item
item.setData(Qt.UserRole + 1, fields) try:
self.addItem(item) with open(individual_config, 'r') as f:
save_data = json.load(f)
for block in save_data:
# Check for the specific sub_type we just defined
# We specifically want 'begin' blocks that were marked as events
if block.get('b_type') == "begin":
# Use the custom name from the inputs if available
inputs = block.get('inputs', {})
# Assuming your "New Event" block has a QLineEdit for the name
event_name = next(iter(inputs.values()), block.get('label'))
print(f"[DEBUG] Importing: {event_name}")
self.add_item(event_name, "created_event", fields=[], sub_type="standard")
except Exception as e:
print(f"Error importing individual events: {e}")
def get_or_create_category(self, b_type):
"""Helper to ensure category dropdowns exist and are styled."""
if b_type not in self.categories:
theme = self.type_themes.get(b_type, {"label": b_type.upper(), "color": "#444444"})
# Create top-level category item
cat = QTreeWidgetItem(self)
cat.setText(0, theme["label"])
cat.setBackground(0, QBrush(QColor(theme["color"])))
cat.setForeground(0, QBrush(QColor("white")))
cat.setFlags(cat.flags() & ~Qt.ItemIsDragEnabled) # Categories can't be dragged
self.categories[b_type] = cat
cat.setExpanded(True) # Start with dropdowns open
return self.categories[b_type]
def add_item(self, label, block_type, fields, sub_type):
category = self.get_or_create_category(block_type)
# Create child item under the category
item = QTreeWidgetItem(category)
item.setText(0, label)
# Store metadata
item.setData(0, Qt.UserRole, block_type)
item.setData(0, Qt.UserRole + 1, fields)
item.setData(0, Qt.UserRole + 2, sub_type)
# Optional: Subtle styling for child items
item.setForeground(0, QBrush(QColor("#dddddd")))
def startDrag(self, supportedActions): def startDrag(self, supportedActions):
item = self.currentItem() item = self.currentItem()
if not item: return if not item or item.parent() is None:
return
# Pack everything into a clean dictionary
drag_data = {
"type": item.data(0, Qt.UserRole),
"label": item.text(0),
"fields": item.data(0, Qt.UserRole + 1),
"sub_type": item.data(0, Qt.UserRole + 2) # Use the type as subtype by default
}
mime_data = QMimeData() mime_data = QMimeData()
mime_data.setText(json.dumps(drag_data)) # Send as ONE structured string
# 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)
@@ -3468,38 +3542,141 @@ class BlockLibrary(QListWidget):
def add_dynamic_logic_item(self, block_item): def add_dynamic_logic_item(self, block_item):
print("asdasd")
""" """
Triggered when a 'begin' block is dropped on the canvas. Triggered immediately when a 'begin' block is dropped on the canvas.
Adds a corresponding 'logic' block to this library.
""" """
# 1. Check for duplicates # 1. Get the actual identity we assigned in dropEvent
# We don't want to add "Joint 1" five times if they drag five 'begin' blocks # This will now be 'created_joint' or 'created_event' instead of just 'begin'
s_type = getattr(block_item, 'sub_type', 'standard')
label = block_item.label_text label = block_item.label_text
items = self.findItems(label, Qt.MatchExactly) # 2. Route to the correct sidebar category
for item in items: if s_type == "created_joint":
if item.data(Qt.UserRole) == "logic": target_category = "created_joint" # Teal
print(f"DEBUG: {label} (logic) already exists in library. Skipping.") elif s_type == "created_event":
target_category = "created_event" # Orange (Individual Variables)
else:
return # Don't add standard 'begin' blocks to the logic list
# 3. Prevent Duplicate Sidebar Entries
category_item = self.get_or_create_category(target_category)
for i in range(category_item.childCount()):
if category_item.child(i).text(0) == label:
return return
# 2. Add the item as a 'logic' type # 4. Update the Sidebar UI immediately
# In your system, logic blocks have no fields (they are just variables) self.add_item(label, target_category, fields=[], sub_type=s_type)
print(f"DEBUG: Dynamically adding '{label}' as a logic block.") print(f"[DEBUG] Instant Sidebar Update: Added {label} to {target_category}")
self.add_item(label, "logic", fields=[])
def update_item_name(self, old_name, new_name): def update_item_name(self, old_name, new_name):
print(f"[DEBUG - Library] Searching for items matching: '{old_name}'") """Rename logic blocks in the VARIABLES dropdown."""
items = self.findItems(old_name, Qt.MatchExactly) cat = self.get_or_create_category("created_joint")
for i in range(cat.childCount()):
item = cat.child(i)
if item.text(0) == old_name:
item.setText(0, new_name)
if not items:
print(f"[DEBUG - Library] No items found matching '{old_name}'")
for item in items: def discover_saved_blocks(self, save_file, target_subtype):
# Ensure we only rename the logic-type items """Scans save file and populates categories using the helper function."""
if item.data(Qt.UserRole) == "logic": if not os.path.exists(save_file):
item.setText(new_name) return
print(f"[DEBUG - Library] Successfully updated sidebar item to '{new_name}'")
try:
with open(save_file, 'r') as f:
data = json.load(f)
# Use the helper to ensure the Teal or Purple folder exists
target_cat = self.get_or_create_category(target_subtype)
if not target_cat:
print(f"[ERROR] Could not resolve category for: {target_subtype}")
return
for block_data in data:
if block_data.get('sub_type') == target_subtype:
# Resolve name (LineEdit input or generic label)
inputs = block_data.get('inputs', {})
name = next(iter(inputs.values()), block_data.get('label'))
# Scoped duplicate check
is_duplicate = False
for i in range(target_cat.childCount()):
if target_cat.child(i).text(0) == name:
is_duplicate = True
break
if not is_duplicate:
self.add_item(
label=name,
block_type=target_subtype,
fields=[],
sub_type=target_subtype
)
print(f"[DEBUG] Restored '{name}' to {target_cat.text(0)}")
except Exception as e:
print(f"[ERROR] Discovery error: {e}")
def remove_dynamic_logic_item(self, label, s_type):
"""
Surgical removal from the sidebar.
"""
target_label = str(label).strip().lower()
# BLACKLIST: Never search or delete from these category keys.
# These represent your "Factory" templates and "Nose/Eye" entries.
forbidden_categories = ["begin", "entry_events", "standard_nodes"]
# WHITELIST: Only look for the spawns/variables here.
allowed_categories = ["created_joint", "logic", "created_event"]
for cat_key in allowed_categories:
if cat_key in self.categories and cat_key not in forbidden_categories:
cat = self.categories[cat_key]
for i in range(cat.childCount() - 1, -1, -1):
child = cat_item = cat.child(i)
if child.text(0).strip().lower() == target_label:
cat.removeChild(child)
print(f"[DEBUG] Sidebar: Removed '{label}' from {cat_key}")
return # Exit once the spawn is deleted
# def add_dynamic_logic_item(self, block_item):
# """
# Triggered when a 'begin' block is dropped on the canvas.
# Adds a corresponding 'logic' block to this library.
# """
# # 1. Check for duplicates
# # We don't want to add "Joint 1" five times if they drag five 'begin' blocks
# label = block_item.label_text
# items = self.findItems(label, Qt.MatchExactly)
# for item in items:
# if item.data(Qt.UserRole) == "logic":
# print(f"DEBUG: {label} (logic) already exists in library. Skipping.")
# return
# # 2. Add the item as a 'logic' type
# # In your system, logic blocks have no fields (they are just variables)
# print(f"DEBUG: Dynamically adding '{label}' as a logic block.")
# self.add_item(label, "logic", fields=[])
# def update_item_name(self, old_name, new_name):
# print(f"[DEBUG - Library] Searching for items matching: '{old_name}'")
# items = self.findItems(old_name, Qt.MatchExactly)
# if not items:
# print(f"[DEBUG - Library] No items found matching '{old_name}'")
# for item in items:
# # Ensure we only rename the logic-type items
# if item.data(Qt.UserRole) == "logic":
# item.setText(new_name)
# print(f"[DEBUG - Library] Successfully updated sidebar item to '{new_name}'")
from PySide6.QtWidgets import QGraphicsLineItem, QGraphicsRectItem, QGraphicsSimpleTextItem from PySide6.QtWidgets import QGraphicsLineItem, QGraphicsRectItem, QGraphicsSimpleTextItem
@@ -3509,6 +3686,7 @@ from PySide6.QtGui import QPen, QColor, QBrush
class TopologyCanvas(QGraphicsView): class TopologyCanvas(QGraphicsView):
beginBlockDropped = Signal(object) beginBlockDropped = Signal(object)
beginBlockDeleted = Signal(str, str)
def __init__(self): def __init__(self):
self.scene = QGraphicsScene() self.scene = QGraphicsScene()
@@ -3521,7 +3699,7 @@ class TopologyCanvas(QGraphicsView):
def on_new_definition_created(self, block_item): def on_new_definition_created(self, block_item):
# Connect the block's rename signal to our canvas sync method # Connect the block's rename signal to our canvas sync method
print(f"[DEBUG - Canvas] Connecting signal for new block: {block_item.label_text}") print(f"[DEBUG - Canvas] Connecting signal for new block: {block_item.label_text}")
block_item.signals.nameChanged.connect(self.sync_logic_blocks) self.wire_block_signals(block_item)
def sync_logic_blocks(self, old_name, new_name): def sync_logic_blocks(self, old_name, new_name):
print(f"[DEBUG - Canvas] Syncing. Old: {old_name}, New: {new_name}") print(f"[DEBUG - Canvas] Syncing. Old: {old_name}, New: {new_name}")
@@ -3569,6 +3747,7 @@ class TopologyCanvas(QGraphicsView):
# Defer the logic to the next event loop cycle (stops the DLL crash) # 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)) QTimer.singleShot(0, lambda: self.safe_process_release(global_pos, scene_pos))
def safe_process_release(self, global_pos, scene_pos): def safe_process_release(self, global_pos, scene_pos):
self._processing_release = True self._processing_release = True
try: try:
@@ -3602,33 +3781,45 @@ class TopologyCanvas(QGraphicsView):
def dropEvent(self, event): def dropEvent(self, event):
raw_data = event.mimeData().text() try:
parts = raw_data.split("|") # 1. Parse the structured JSON
if len(parts) < 2: return # Safety check raw_data = event.mimeData().text()
data = json.loads(raw_data)
block_type, label = parts[0], parts[1] # 2. Extract with safe defaults
fields = [] b_type = data.get("type", "middle")
label = data.get("label", "New Block")
fields = data.get("fields", [])
sub_type = data.get("sub_type", "standard")
# Safe JSON parsing print(sub_type)
if len(parts) > 2:
try:
fields = json.loads(parts[2])
except Exception as e:
print(f"DEBUG: Failed to parse: {e}")
# 3. Determine the visual class of the block
# (Same logic as before, but cleaner)
if b_type == "created_event":
visual_type = "begin"
elif b_type == "created_joint":
visual_type = "logic"
else:
visual_type = b_type
new_block = PuzzleBlock(block_type, label, fields=fields) # 4. Create the block
new_block = PuzzleBlock(visual_type, label, fields=fields, sub_type=sub_type)
self.scene.addItem(new_block)
self.scene.addItem(new_block) # 5. Position
drop_pos = self.mapToScene(event.position().toPoint())
if not self.perform_snap(new_block, drop_pos):
new_block.setPos(drop_pos)
# Try snapping, otherwise place at drop location # 6. Library Feedback
if not self.perform_snap(new_block, self.mapToScene(event.position().toPoint())): if visual_type == "begin":
new_block.setPos(self.mapToScene(event.position().toPoint())) self.beginBlockDropped.emit(new_block)
if block_type == "begin": event.acceptProposedAction()
self.beginBlockDropped.emit(new_block)
event.acceptProposedAction() except Exception as e:
print(f"CRITICAL: Failed to parse drag-and-drop JSON: {e}")
def get_stack_extremity(self, block, direction): def get_stack_extremity(self, block, direction):
@@ -3644,14 +3835,6 @@ class TopologyCanvas(QGraphicsView):
return curr return curr
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): def print_logical_graphs(self):
"""Crawls the logical links and prints all current stacks.""" """Crawls the logical links and prints all current stacks."""
print(f"\n{'='*60}\nCURRENT LOGICAL GRAPHS (F8 Triggered)") print(f"\n{'='*60}\nCURRENT LOGICAL GRAPHS (F8 Triggered)")
@@ -3957,7 +4140,168 @@ class TopologyCanvas(QGraphicsView):
def save_to_json(self, file_path="config.json"):
data = []
blocks = [item for item in self.scene.items() if isinstance(item, PuzzleBlock)]
# Mapping to store IDs for linking
for block in blocks:
block_data = {
"id": id(block),
"b_type": block.b_type,
"sub_type": getattr(block, 'sub_type', 'standard'),
"label": block.label_text,
"pos": [block.pos().x(), block.pos().y()],
"parent_id": id(block.parent_block) if block.parent_block else None,
"child_id": id(block.child_block) if block.child_block else None,
"host_id": id(block.host_block) if getattr(block, 'host_block', None) else None,
"slot_key": getattr(block, 'current_slot', None),
"fields": block.fields,
"inputs": {k: v.text() for k, v in block.inputs.items() if isinstance(v, QLineEdit)}
}
data.append(block_data)
with open(file_path, 'w') as f:
json.dump(data, f, indent=4)
print(f"Configuration saved to {file_path}")
def load_from_json(self, file_path="config.json"):
if not os.path.exists(file_path):
return
self.scene.clear()
with open(file_path, 'r') as f:
data = json.load(f)
id_map = {}
# 1. Create all blocks
for b in data:
# Note: Pass the original fields if you have them,
# or ensure PuzzleBlock handles empty fields gracefully
new_block = PuzzleBlock(b['b_type'], b['label'], fields=b['fields'], sub_type=b.get('sub_type', 'standard'))
new_block.setPos(b['pos'][0], b['pos'][1])
# Restore text inputs
for key, val in b.get('inputs', {}).items():
if key in new_block.inputs:
widget = new_block.inputs[key]
if isinstance(widget, QLineEdit):
widget.setText(val)
# CRITICAL: Force the block to wrap around its new content
new_block.force_resize()
self.scene.addItem(new_block)
id_map[b['id']] = new_block
self.wire_block_signals(new_block)
# 2. Re-establish logical links
for b in data:
curr = id_map[b['id']]
# Connect Chained Stacks (Vertical)
if b.get('child_id') in id_map:
curr.child_block = id_map[b['child_id']]
if b.get('parent_id') in id_map:
curr.parent_block = id_map[b['parent_id']]
# Connect Embedded Logic Blocks (Horizontal/Internal)
if b.get('host_id') in id_map:
host = id_map[b['host_id']]
slot_key = b['slot_key']
# Re-running embed_logic_block will fix the internal Z-value and position
self.embed_logic_block(curr, host, slot_key)
if hasattr(self, 'block_library_ref') and self.block_library_ref:
# Determine what we are looking for based on current editor mode
target = "created_joint" if self.block_library_ref.mode == "individual" else "created_event"
self.block_library_ref.discover_saved_blocks(file_path, target)
# 3. Final visual refresh
self.scene.update()
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()
if event.key() in (Qt.Key_Delete, Qt.Key_Backspace):
# Get all selected items on the canvas
selected_items = self.scene.selectedItems()
for item in selected_items:
if isinstance(item, PuzzleBlock):
self.delete_block(item)
else:
# Crucial: let other keys (like arrows for moving blocks) work normally
super().keyPressEvent(event)
def delete_block(self, block):
"""
Surgical Deletion with Orphan Cleanup:
1. Removes master definition and signals sidebar.
2. Scans canvas for any 'logic' instances of this block and deletes them.
"""
if not block:
return
# 1. IDENTIFY IDENTITY
v_type = getattr(block, 'b_type', 'middle')
s_type = getattr(block, 'sub_type', 'standard')
label = block.label_text
# 2. TRIGGER SIDEBAR & ORPHAN CLEANUP
# Only if it's a master definition (Begin) of a dynamic type
if v_type == "begin" and s_type in ["created_joint", "created_event"]:
print(f"[DEBUG] Master Deleted: {label}. Cleaning orphans...")
# Signal sidebar removal
self.beginBlockDeleted.emit(label, s_type)
# CLEANUP ORPHANS: Find every logic block on the canvas with the same name
for item in self.scene.items():
# We look for blocks that are NOT the one we are currently deleting
# and match the name and the 'logic' type.
if item != block and hasattr(item, 'b_type') and item.b_type == "logic":
if item.label_text == label:
print(f"[DEBUG] Removing orphaned instance: {label}")
# We call scene.removeItem directly to avoid recursive signal loops
self.scene.removeItem(item)
# 3. HANDLE VERTICAL CONNECTIONS (STREAMS)
if hasattr(block, 'child_block') and block.child_block:
block.child_block.parent_block = None
if hasattr(block, 'parent_block') and block.parent_block:
block.parent_block.child_block = None
# 4. HANDLE SLOTS (Embedded logic blocks)
if hasattr(block, 'slots'):
for slot_key, embedded_item in list(block.slots.items()):
if embedded_item:
self.delete_block(embedded_item)
block.slots[slot_key] = None
# 5. PHYSICAL REMOVAL OF THE TARGET BLOCK
if block in self.scene.items():
self.scene.removeItem(block)
def wire_block_signals(self, block_item):
"""Ensures a block's internal changes are reflected in the UI/Sidebar."""
if hasattr(block_item, 'signals') and hasattr(block_item.signals, 'nameChanged'):
# Disconnect first to prevent double-triggering if called twice
try:
block_item.signals.nameChanged.disconnect(self.sync_logic_blocks)
except:
pass
block_item.signals.nameChanged.connect(self.sync_logic_blocks)
print(f"[DEBUG - CANVAS] Wired signals for: {block_item.label_text}")
class ModelParameterConfigurationTab(QWidget): class ModelParameterConfigurationTab(QWidget):
@@ -4023,6 +4367,7 @@ class ModelParameterConfigurationTab(QWidget):
self.setup_ui() self.setup_ui()
QTimer.singleShot(0, lambda: self.canvas.load_from_json(f"config_{self.mode}.json"))
def setup_ui(self): def setup_ui(self):
@@ -4044,6 +4389,7 @@ class ModelParameterConfigurationTab(QWidget):
self.canvas.block_library_ref = self.block_library self.canvas.block_library_ref = self.block_library
self.canvas.beginBlockDropped.connect(self.block_library.add_dynamic_logic_item) self.canvas.beginBlockDropped.connect(self.block_library.add_dynamic_logic_item)
self.canvas.beginBlockDeleted.connect(self.block_library.remove_dynamic_logic_item)
# Reuse your existing Inspector logic # Reuse your existing Inspector logic
self.inspector_scroll = QScrollArea() self.inspector_scroll = QScrollArea()
@@ -4058,6 +4404,11 @@ class ModelParameterConfigurationTab(QWidget):
sidebar_layout.addWidget(QLabel("Properties")) sidebar_layout.addWidget(QLabel("Properties"))
sidebar_layout.addWidget(self.inspector_scroll, 1) sidebar_layout.addWidget(self.inspector_scroll, 1)
self.save_btn = QPushButton("Save Configuration")
# Save specifically for this 'mode' so different tabs don't overwrite each other
self.save_btn.clicked.connect(lambda: self.canvas.save_to_json(f"config_{self.mode}.json"))
sidebar_layout.addWidget(self.save_btn)
# Add to main splitter # Add to main splitter
self.horizontal_splitter.addWidget(self.canvas) self.horizontal_splitter.addWidget(self.canvas)
self.horizontal_splitter.addWidget(sidebar_container) self.horizontal_splitter.addWidget(sidebar_container)