Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions backend/opencad/part.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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},
)

Expand Down
36 changes: 23 additions & 13 deletions backend/opencad/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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,
Expand Down
18 changes: 13 additions & 5 deletions backend/opencad_tree/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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):
Expand Down
16 changes: 8 additions & 8 deletions opencad_viewport/src/components/FeatureTreePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions opencad_viewport/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export interface FeatureNodeView {
typed_parameters: Record<string, TypedParameter>;
parameter_bindings: ParameterBinding[];
sketch_id?: string | null;
parent_id: string | null;
tool_refs: string[];
depends_on: string[];
shape_id?: string | null;
status: FeatureNodeStatus;
Expand Down
Loading