fun with blocks
This commit is contained in:
+24
-19
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"individual": [
|
||||
{ "name": "New Event", "type": "begin" },
|
||||
{ "name": "New Event", "type": "begin", "sub_type": "created_event" },
|
||||
{
|
||||
"name": "Average",
|
||||
"type": "middle",
|
||||
@@ -37,24 +37,29 @@
|
||||
|
||||
|
||||
{ "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" },
|
||||
{ "name": "New Calculated Joint", "type": "begin" }
|
||||
{ "name": "Nose", "type": "logic" },
|
||||
{ "name": "Left Eye", "type": "logic" },
|
||||
{ "name": "Right Eye", "type": "logic" },
|
||||
{ "name": "Left Ear", "type": "logic" },
|
||||
{ "name": "Right Ear", "type": "logic" },
|
||||
{ "name": "Left Shoulder", "type": "logic" },
|
||||
{ "name": "Right Shoulder", "type": "logic" },
|
||||
{ "name": "Left Elbow", "type": "logic" },
|
||||
{ "name": "Right Elbow", "type": "logic" },
|
||||
{ "name": "Left Wrist", "type": "logic" },
|
||||
{ "name": "Right Wrist", "type": "logic" },
|
||||
{ "name": "Left Hip", "type": "logic" },
|
||||
{ "name": "Right Hip", "type": "logic" },
|
||||
{ "name": "Left Knee", "type": "logic" },
|
||||
{ "name": "Right Knee", "type": "logic" },
|
||||
{ "name": "Left Ankle", "type": "logic" },
|
||||
{ "name": "Right Ankle", "type": "logic" },
|
||||
{ "name": "New Calculated Joint", "type": "begin", "sub_type": "created_joint" }
|
||||
|
||||
],
|
||||
|
||||
"group": [
|
||||
{ "name": "New Activity", "type": "begin" },
|
||||
{ "name": "End Activity", "type": "end" }
|
||||
]
|
||||
}
|
||||
@@ -34,7 +34,7 @@ from pose_worker import run_pose_analysis
|
||||
from batch_processing import BatchProcessorDialog
|
||||
|
||||
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,
|
||||
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
|
||||
@@ -3165,10 +3165,10 @@ from PySide6.QtWidgets import QGraphicsItem, QGraphicsSimpleTextItem, QInputDial
|
||||
class BlockSignalProxy(QObject):
|
||||
nameChanged = Signal(str, str)
|
||||
|
||||
|
||||
class PuzzleBlock(QGraphicsPathItem):
|
||||
|
||||
|
||||
def __init__(self, b_type, label, parent_item=None, fields=None):
|
||||
def __init__(self, b_type, label, parent_item=None, fields=None, sub_type="standard"):
|
||||
super().__init__(parent_item)
|
||||
|
||||
self.signals = BlockSignalProxy()
|
||||
@@ -3178,6 +3178,8 @@ class PuzzleBlock(QGraphicsPathItem):
|
||||
self.width = 160
|
||||
self.height = 50
|
||||
self.tab_size = 15
|
||||
self.fields = fields if fields is not None else []
|
||||
self.sub_type = sub_type
|
||||
|
||||
self.parent_block = None
|
||||
self.child_block = None
|
||||
@@ -3404,15 +3406,38 @@ class PuzzleBlock(QGraphicsPathItem):
|
||||
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):
|
||||
super().__init__()
|
||||
self.setDragEnabled(True)
|
||||
self.setHeaderHidden(True) # Hide the "1" header at the top
|
||||
self.setIndentation(15)
|
||||
self.setAnimated(True)
|
||||
|
||||
self.mode = stringy
|
||||
|
||||
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")
|
||||
|
||||
|
||||
@@ -3430,35 +3455,84 @@ class BlockLibrary(QListWidget):
|
||||
|
||||
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)
|
||||
self.add_item(block['name'], block['type'], block.get('fields', []), block.get('sub_type', "standard"))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to load block library: {e}")
|
||||
|
||||
|
||||
def import_individual_events(self):
|
||||
individual_config = "config_individual.json"
|
||||
if not os.path.exists(individual_config): return
|
||||
|
||||
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)
|
||||
try:
|
||||
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):
|
||||
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()
|
||||
|
||||
# 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}")
|
||||
mime_data.setText(json.dumps(drag_data)) # Send as ONE structured string
|
||||
|
||||
drag = QDrag(self)
|
||||
drag.setMimeData(mime_data)
|
||||
@@ -3468,38 +3542,141 @@ class BlockLibrary(QListWidget):
|
||||
|
||||
|
||||
def add_dynamic_logic_item(self, block_item):
|
||||
print("asdasd")
|
||||
"""
|
||||
Triggered when a 'begin' block is dropped on the canvas.
|
||||
Adds a corresponding 'logic' block to this library.
|
||||
Triggered immediately when a 'begin' block is dropped on the canvas.
|
||||
"""
|
||||
# 1. Check for duplicates
|
||||
# We don't want to add "Joint 1" five times if they drag five 'begin' blocks
|
||||
|
||||
# 1. Get the actual identity we assigned in dropEvent
|
||||
# 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
|
||||
|
||||
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.")
|
||||
# 2. Route to the correct sidebar category
|
||||
if s_type == "created_joint":
|
||||
target_category = "created_joint" # Teal
|
||||
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
|
||||
|
||||
# 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=[])
|
||||
# 4. Update the Sidebar UI immediately
|
||||
self.add_item(label, target_category, fields=[], sub_type=s_type)
|
||||
print(f"[DEBUG] Instant Sidebar Update: Added {label} to {target_category}")
|
||||
|
||||
|
||||
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}'")
|
||||
"""Rename logic blocks in the VARIABLES dropdown."""
|
||||
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)
|
||||
|
||||
|
||||
def discover_saved_blocks(self, save_file, target_subtype):
|
||||
"""Scans save file and populates categories using the helper function."""
|
||||
if not os.path.exists(save_file):
|
||||
return
|
||||
|
||||
try:
|
||||
with open(save_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
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}'")
|
||||
# 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
|
||||
@@ -3509,6 +3686,7 @@ from PySide6.QtGui import QPen, QColor, QBrush
|
||||
class TopologyCanvas(QGraphicsView):
|
||||
|
||||
beginBlockDropped = Signal(object)
|
||||
beginBlockDeleted = Signal(str, str)
|
||||
|
||||
def __init__(self):
|
||||
self.scene = QGraphicsScene()
|
||||
@@ -3521,7 +3699,7 @@ class TopologyCanvas(QGraphicsView):
|
||||
def on_new_definition_created(self, block_item):
|
||||
# Connect the block's rename signal to our canvas sync method
|
||||
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):
|
||||
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)
|
||||
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:
|
||||
@@ -3602,33 +3781,45 @@ class TopologyCanvas(QGraphicsView):
|
||||
|
||||
|
||||
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 = []
|
||||
try:
|
||||
# 1. Parse the structured JSON
|
||||
raw_data = event.mimeData().text()
|
||||
data = json.loads(raw_data)
|
||||
|
||||
# 2. Extract with safe defaults
|
||||
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
|
||||
if len(parts) > 2:
|
||||
try:
|
||||
fields = json.loads(parts[2])
|
||||
except Exception as e:
|
||||
print(f"DEBUG: Failed to parse: {e}")
|
||||
print(sub_type)
|
||||
|
||||
|
||||
new_block = PuzzleBlock(block_type, label, fields=fields)
|
||||
# 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
|
||||
|
||||
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()))
|
||||
|
||||
if block_type == "begin":
|
||||
self.beginBlockDropped.emit(new_block)
|
||||
# 4. Create the block
|
||||
new_block = PuzzleBlock(visual_type, label, fields=fields, sub_type=sub_type)
|
||||
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)
|
||||
|
||||
# 6. Library Feedback
|
||||
if visual_type == "begin":
|
||||
self.beginBlockDropped.emit(new_block)
|
||||
|
||||
event.acceptProposedAction()
|
||||
event.acceptProposedAction()
|
||||
|
||||
except Exception as e:
|
||||
print(f"CRITICAL: Failed to parse drag-and-drop JSON: {e}")
|
||||
|
||||
|
||||
def get_stack_extremity(self, block, direction):
|
||||
@@ -3644,14 +3835,6 @@ class TopologyCanvas(QGraphicsView):
|
||||
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):
|
||||
"""Crawls the logical links and prints all current stacks."""
|
||||
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):
|
||||
@@ -4023,6 +4367,7 @@ class ModelParameterConfigurationTab(QWidget):
|
||||
|
||||
self.setup_ui()
|
||||
|
||||
QTimer.singleShot(0, lambda: self.canvas.load_from_json(f"config_{self.mode}.json"))
|
||||
|
||||
|
||||
def setup_ui(self):
|
||||
@@ -4044,6 +4389,7 @@ class ModelParameterConfigurationTab(QWidget):
|
||||
self.canvas.block_library_ref = self.block_library
|
||||
|
||||
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
|
||||
self.inspector_scroll = QScrollArea()
|
||||
@@ -4058,6 +4404,11 @@ class ModelParameterConfigurationTab(QWidget):
|
||||
sidebar_layout.addWidget(QLabel("Properties"))
|
||||
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
|
||||
self.horizontal_splitter.addWidget(self.canvas)
|
||||
self.horizontal_splitter.addWidget(sidebar_container)
|
||||
|
||||
Reference in New Issue
Block a user