From bfc8d437f2ec315a2edbd685b1cab9d231be350d Mon Sep 17 00:00:00 2001 From: isayahc Date: Fri, 1 May 2026 23:17:04 -0400 Subject: [PATCH] fixed repeat nodes in backend --- backend/opencad/part.py | 30 ++++++++++------ backend/opencad/runtime.py | 36 ++++++++++++------- backend/opencad_tree/models.py | 18 +++++++--- .../src/components/FeatureTreePanel.tsx | 16 ++++----- opencad_viewport/src/types.ts | 2 ++ 5 files changed, 66 insertions(+), 36 deletions(-) diff --git a/backend/opencad/part.py b/backend/opencad/part.py index 5370364..2378ee0 100644 --- a/backend/opencad/part.py +++ b/backend/opencad/part.py @@ -27,19 +27,28 @@ def _require_shape(self) -> tuple[str, str]: return self.feature_id, self.shape_id def _apply( - self, - operation: str, - payload: dict, - *, - feature_name: str, - depends_on: list[str], - tree_parameters: dict | None = None, - ) -> Self: + self, + operation: str, + payload: dict, + *, + feature_name: str, + parent_id: str | None = None, + tool_refs: list[str] | None = None, + tree_parameters: dict | None = None, + # back-compat shim during migration: + depends_on: list[str] | None = None, +) -> Self: + # If a caller still passes depends_on, treat first as parent, rest as tools. + if depends_on is not None and parent_id is None and tool_refs is None: + parent_id = depends_on[0] if depends_on else None + tool_refs = list(depends_on[1:]) + feature_id, shape_id = self._context.execute_operation( operation, payload, feature_name=feature_name, - depends_on=depends_on, + parent_id=parent_id, + tool_refs=tool_refs or [], tree_parameters=tree_parameters, ) self.feature_id = feature_id @@ -116,7 +125,8 @@ def cut(self, other: Part, *, name: str = "Cut") -> Self: "boolean_cut", {"shape_a_id": left_shape, "shape_b_id": right_shape}, feature_name=name, - depends_on=[left_feature, right_feature], + parent_id=left_feature, # target → lineage + tool_refs=[right_feature], # tool → reference only tree_parameters={"shape_a_id": left_feature, "shape_b_id": right_feature}, ) diff --git a/backend/opencad/runtime.py b/backend/opencad/runtime.py index b66f400..8a7dd24 100644 --- a/backend/opencad/runtime.py +++ b/backend/opencad/runtime.py @@ -66,17 +66,26 @@ def _sync_counters(self) -> None: self._sketch_counter = max(self._sketch_counter, int(tail) + 1) def execute_operation( - self, - operation: str, - payload: dict[str, Any], - *, - feature_name: str, - depends_on: list[str] | None = None, - tree_parameters: dict[str, Any] | None = None, - feature_id: str | None = None, - ) -> tuple[str, str]: + self, + operation: str, + payload: dict[str, Any], + *, + feature_name: str, + parent_id: str | None = None, + tool_refs: list[str] | None = None, + tree_parameters: dict[str, Any] | None = None, + feature_id: str | None = None, + depends_on: list[str] | None = None, # back-compat shim +) -> tuple[str, str]: """Execute a kernel operation and append a built feature node.""" - depends = depends_on or [] + # Migration shim: if a caller still passes depends_on, derive roles + # positionally — first entry is the lineage parent, rest are tool refs. + if depends_on is not None and parent_id is None and tool_refs is None: + parent_id = depends_on[0] if depends_on else None + tool_refs = list(depends_on[1:]) + + tool_refs = tool_refs or [] + if self._external_kernel_call is not None: response = self._external_kernel_call(operation, payload) else: @@ -86,18 +95,19 @@ def execute_operation( shape_id = response.get("shape_id") if not shape_id: raise RuntimeError(f"Operation '{operation}' returned no shape_id.") - + node_id = feature_id if node_id is None: node_id = self._new_sketch_id() if operation == "create_sketch" else self._new_feature_id() - + params = dict(tree_parameters) if tree_parameters is not None else dict(payload) node = FeatureNode( id=node_id, name=feature_name, operation=operation, parameters=params, - depends_on=depends, + parent_id=parent_id, + tool_refs=tool_refs, shape_id=str(shape_id), status="built", sketch_id=node_id if operation == "create_sketch" else None, diff --git a/backend/opencad_tree/models.py b/backend/opencad_tree/models.py index 9c95348..4c89ae6 100644 --- a/backend/opencad_tree/models.py +++ b/backend/opencad_tree/models.py @@ -3,7 +3,7 @@ from datetime import datetime, timezone from typing import Any, Literal -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, computed_field NodeStatus = Literal["pending", "built", "failed", "stale", "suppressed"] ParameterType = Literal["int", "float", "bool", "string", "shape_ref", "json"] @@ -31,13 +31,21 @@ class FeatureNode(BaseModel): typed_parameters: dict[str, TypedParameter] = Field(default_factory=dict) parameter_bindings: list[ParameterBinding] = Field(default_factory=list) sketch_id: str | None = None - depends_on: list[str] = Field(default_factory=list) + parent_id: str | None = None # NEW + tool_refs: list[str] = Field(default_factory=list) # NEW shape_id: str | None = None status: NodeStatus = "pending" suppressed: bool = False - # Assembly mate metadata (Phase 1) - mate_id: str | None = None # links to kernel MateStore entry - is_assembly_mate: bool = False # convenience flag for tree traversal + mate_id: str | None = None + is_assembly_mate: bool = False + + @computed_field # type: ignore[misc] + @property + def depends_on(self) -> list[str]: + """Back-compat: derived from parent_id + tool_refs.""" + if self.parent_id is None: + return list(self.tool_refs) + return [self.parent_id, *self.tool_refs] class FeatureTree(BaseModel): diff --git a/opencad_viewport/src/components/FeatureTreePanel.tsx b/opencad_viewport/src/components/FeatureTreePanel.tsx index 5e82780..25e45d7 100644 --- a/opencad_viewport/src/components/FeatureTreePanel.tsx +++ b/opencad_viewport/src/components/FeatureTreePanel.tsx @@ -32,15 +32,14 @@ export function FeatureTreePanel({ tree, selectedNodeId, onSelectNode }: Feature const rootCandidates: string[] = []; Object.entries(tree.nodes).forEach(([nodeId, node]) => { - if (node.depends_on.length === 0) { + if (!node.parent_id) { rootCandidates.push(nodeId); - } - node.depends_on.forEach((parentId) => { - if (!children[parentId]) { - children[parentId] = []; + } else { + if (!children[node.parent_id]) { + children[node.parent_id] = []; } - children[parentId].push(nodeId); - }); + children[node.parent_id].push(nodeId); + } }); Object.values(children).forEach((ids) => ids.sort()); @@ -65,7 +64,8 @@ export function FeatureTreePanel({ tree, selectedNodeId, onSelectNode }: Feature return; } toExpand.add(nodeId); - tree.nodes[nodeId].depends_on.forEach(visit); + const parentId = tree.nodes[nodeId].parent_id; + if (parentId) visit(parentId); }; visit(selectedNodeId); diff --git a/opencad_viewport/src/types.ts b/opencad_viewport/src/types.ts index cec6a7b..2682589 100644 --- a/opencad_viewport/src/types.ts +++ b/opencad_viewport/src/types.ts @@ -24,6 +24,8 @@ export interface FeatureNodeView { typed_parameters: Record; parameter_bindings: ParameterBinding[]; sketch_id?: string | null; + parent_id: string | null; + tool_refs: string[]; depends_on: string[]; shape_id?: string | null; status: FeatureNodeStatus;