Files
cadSet/designir-pipeline/scripts/feature_tree.py
T

1590 lines
58 KiB
Python

#!/usr/bin/env python3
"""Generate and replay SolidWorks FeatureManager-style CAD feature trees.
The exported ``feature_tree.json`` is a normalized, user-facing construction
history. It is intentionally not a claim that an uploaded STEP file contained
native SolidWorks history. STEP/SurfaceIR inputs fall back to a legal
SolidWorks-style imported feature that embeds the replay payload.
"""
from __future__ import annotations
import argparse
import base64
import copy
import gzip
import hashlib
import importlib.util
import json
import math
import re
import sys
from pathlib import Path
from typing import Any
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
from designir_codec import read_designir, write_designir
SCHEMA_VERSION = "1.0"
TREE_KIND = "solidworks_feature_manager"
SUPPORTED_SW_TYPES = {
"Part",
"HistoryFolder",
"OriginProfileFeature",
"RefPlane",
"RefAxis",
"ProfileFeature",
"BossExtrude",
"CutExtrude",
"Revolve",
"RevolvedCut",
"HoleWizard",
"Fillet",
"Chamfer",
"LinearPattern",
"CircularPattern",
"MirrorPattern",
"ImportedFeature",
"UnsupportedFeature",
}
DESIGNIR_TO_SW = {
"extrude_circle": "BossExtrude",
"extrude_rectangle": "BossExtrude",
"add_cylinder": "BossExtrude",
"through_hole": "CutExtrude",
"polar_hole_pattern": "CircularPattern",
"revolve": "Revolve",
"revolved_cut": "RevolvedCut",
"hole_wizard": "HoleWizard",
"fillet": "Fillet",
"chamfer": "Chamfer",
"linear_pattern": "LinearPattern",
"mirror_pattern": "MirrorPattern",
}
DISPLAY_PREFIX = {
"BossExtrude": "凸台-拉伸",
"CutExtrude": "切除-拉伸",
"Revolve": "旋转凸台/基体",
"RevolvedCut": "旋转切除",
"HoleWizard": "孔向导",
"Fillet": "圆角",
"Chamfer": "倒角",
"LinearPattern": "线性阵列",
"CircularPattern": "圆周阵列",
"MirrorPattern": "镜像",
"ImportedFeature": "导入",
"UnsupportedFeature": "未支持特征",
}
SKETCH_PREFIX = "草图"
STANDARD_NODES = [
("sw:history", "HistoryFolder", "历史", "History"),
("sw:origin_folder", "HistoryFolder", "原点", "Origin"),
("sw:front_plane", "RefPlane", "前视基准面", "Front Plane"),
("sw:top_plane", "RefPlane", "上视基准面", "Top Plane"),
("sw:right_plane", "RefPlane", "右视基准面", "Right Plane"),
("sw:origin", "OriginProfileFeature", "原点", "Origin"),
]
class FeatureTreeError(ValueError):
pass
def _json_pointer(parts: list[str | int]) -> str:
def encode(part: str | int) -> str:
return str(part).replace("~", "~0").replace("/", "~1")
return "/" + "/".join(encode(part) for part in parts)
def _slug(value: Any, fallback: str = "item") -> str:
text = re.sub(r"[^a-zA-Z0-9_.-]+", "_", str(value or "").strip())
return text.strip("._-") or fallback
def _history_kind(payload: dict[str, Any]) -> str:
mode = payload.get("reconstruction_mode")
if mode == "fully_semantic_parametric":
return "authored_semantic"
if mode == "hybrid_semantic_surface_parametric":
return "hybrid_semantic_surface"
if mode == "surface_parametric":
return "inferred_from_step"
return "unknown"
def _require_designir_3(payload: dict[str, Any]) -> dict[str, Any]:
if payload.get("schema_version") != "3.0":
raise FeatureTreeError(
"feature_tree.json only accepts DesignIR 3.0; migrate legacy DesignIR before generating a tree"
)
if payload.get("designir_kind") != "independent_parametric_cad":
raise FeatureTreeError("DesignIR kind must be independent_parametric_cad")
result = copy.deepcopy(payload)
semantic = result.setdefault("semantic_layer", {})
if not isinstance(semantic, dict):
raise FeatureTreeError("semantic_layer must be an object")
semantic.setdefault("datums", {})
semantic.setdefault("parameters", {})
semantic.setdefault("expressions", {})
semantic.setdefault("sketches", [])
semantic.setdefault("features", [])
semantic.setdefault("patterns", [])
semantic.setdefault("attachments", [])
semantic.setdefault("construction_stages", [])
return result
def _node(
*,
node_id: str,
order: int,
solidworks_type: str,
display_name: str,
english_name: str,
children: list[str] | None = None,
inputs: dict[str, Any] | None = None,
parameters: list[dict[str, Any]] | None = None,
definition: dict[str, Any] | None = None,
dimensions: list[dict[str, Any]] | None = None,
references: dict[str, Any] | None = None,
selection_sets: dict[str, Any] | None = None,
operation_spec: dict[str, Any] | None = None,
result: dict[str, Any] | None = None,
rebuild: dict[str, Any] | None = None,
provenance: str | dict[str, Any] = "generated_from_designir",
confidence: float | None = 1.0,
rebuildable: bool = True,
) -> dict[str, Any]:
if solidworks_type not in SUPPORTED_SW_TYPES:
solidworks_type = "UnsupportedFeature"
provenance_payload = (
{"source": provenance} if isinstance(provenance, str) else provenance
)
node_inputs = inputs or {}
node_children = children or []
node_parameters = parameters or []
node_operation_spec = operation_spec or {}
node_rebuildable = bool(rebuildable)
return {
"id": node_id,
"order": order,
"solidworks_type": solidworks_type,
"display_name": display_name,
"english_name": english_name,
"children": node_children,
"inputs": node_inputs,
"parameters": node_parameters,
"definition": definition or _default_definition(
solidworks_type,
display_name,
node_inputs,
node_operation_spec,
),
"dimensions": dimensions or [],
"references": references or _default_references(node_inputs, node_children),
"selection_sets": selection_sets or _empty_selection_sets(),
"operation_spec": node_operation_spec,
"result": result or {},
"rebuild": rebuild
or {
"suppressed": False,
"rollback_order": order,
"status": "rebuildable" if node_rebuildable else "unsupported",
},
"provenance": provenance_payload,
"confidence": confidence,
"rebuildable": node_rebuildable,
}
def _empty_selection_sets() -> dict[str, list[Any]]:
return {
"selected_faces": [],
"selected_edges": [],
"selected_contours": [],
}
def _default_references(
inputs: dict[str, Any], children: list[str]
) -> dict[str, Any]:
parent_features = []
host = inputs.get("host")
if isinstance(host, str) and host:
parent_features.append(host)
return {
"parent": inputs.get("parent"),
"sketch": inputs.get("sketch"),
"plane": inputs.get("plane"),
"parent_features": parent_features,
"child_features": list(children),
}
def _default_definition(
solidworks_type: str,
display_name: str,
inputs: dict[str, Any],
operation_spec: dict[str, Any],
) -> dict[str, Any]:
if solidworks_type in {"Part", "HistoryFolder"}:
return {"feature_type": solidworks_type, "name": display_name}
if solidworks_type in {"RefPlane", "RefAxis", "OriginProfileFeature"}:
return {
"feature_type": solidworks_type,
"name": display_name,
"standard_reference_geometry": True,
}
return {
"feature_type": solidworks_type,
"name": display_name,
"sketch": inputs.get("sketch"),
"parameters_defined": bool(operation_spec),
}
def _display_name(item: dict[str, Any], fallback: str) -> str:
for key in ("display_name", "label", "name", "title"):
value = item.get(key)
if isinstance(value, str) and value.strip():
return value.strip()
return fallback
def _parameter_refs(value: Any) -> set[str]:
refs: set[str] = set()
if isinstance(value, dict):
parameter = value.get("parameter")
if isinstance(parameter, str):
refs.add(parameter)
for child in value.values():
refs.update(_parameter_refs(child))
elif isinstance(value, list):
for child in value:
refs.update(_parameter_refs(child))
return refs
def _expression_parameter_refs(value: Any, candidates: set[str]) -> set[str]:
refs: set[str] = set()
if isinstance(value, dict):
expression = value.get("expression")
if isinstance(expression, str):
for candidate in candidates:
if re.search(rf"\b{re.escape(candidate)}\b", expression):
refs.add(candidate)
for child in value.values():
refs.update(_expression_parameter_refs(child, candidates))
elif isinstance(value, list):
for child in value:
refs.update(_expression_parameter_refs(child, candidates))
return refs
def _value_for_ref(value: Any, parameters: dict[str, Any]) -> Any:
if isinstance(value, dict) and isinstance(value.get("parameter"), str):
parameter = parameters.get(value["parameter"], {})
if isinstance(parameter, dict):
return parameter.get("value")
if isinstance(value, dict) and isinstance(value.get("expression"), str):
return value.get("expression")
return value
def _rebuild_value(value: Any, default: Any = None) -> Any:
if value is None:
return default
return copy.deepcopy(value)
def _parameter_display_name(name: str, payload: dict[str, Any]) -> str:
display = payload.get("display_name") or payload.get("label") or payload.get("name")
if isinstance(display, str) and display.strip():
return display.strip()
replacements = {
"outer": "外",
"inner": "内",
"radius": "半径",
"diameter": "直径",
"height": "高度",
"depth": "深度",
"width": "宽度",
"thickness": "厚度",
"hole": "孔",
"count": "数量",
"pitch": "分布圆",
"bore": "中心孔",
}
words = []
for part in re.split(r"[_\s-]+", name):
words.append(replacements.get(part.lower(), part))
return "".join(words)
def _feature_parameters(
feature: dict[str, Any], parameters: dict[str, Any], explicit_refs: set[str] | None = None
) -> list[dict[str, Any]]:
refs = set(explicit_refs if explicit_refs is not None else _parameter_refs(feature))
refs.update(_expression_parameter_refs(feature, set(parameters)))
refs = sorted(refs)
result: list[dict[str, Any]] = []
for name in refs:
source = parameters.get(name, {})
source_payload = source if isinstance(source, dict) else {"value": source}
result.append(
{
"id": f"param:{name}",
"name": name,
"display_name": _parameter_display_name(name, source_payload),
"value": source_payload.get("value"),
"default_value": source_payload.get(
"default_value", source_payload.get("value")
),
"unit": source_payload.get("unit"),
"binding_id": name,
"editable": bool(source_payload.get("editable", True)),
"edit_state": source_payload.get("edit_state"),
"source_pointer": _json_pointer(
["semantic_layer", "parameters", name]
),
}
)
return result
def _sketch_profile_for_feature(
feature: dict[str, Any], parameters: dict[str, Any]
) -> dict[str, Any]:
operation = feature.get("operation")
if operation in {"extrude_circle", "add_cylinder"}:
return {
"type": "circle",
"center": _rebuild_value(feature.get("center", [0, 0, 0])),
"radius": _rebuild_value(feature.get("radius")),
}
if operation == "extrude_rectangle":
return {
"type": "rectangle",
"center": _rebuild_value(feature.get("center", [0, 0, 0])),
"width": _rebuild_value(feature.get("width")),
"depth": _rebuild_value(feature.get("depth")),
}
if operation == "through_hole":
return {
"type": "circle",
"center": _rebuild_value(feature.get("center", [0, 0, 0])),
"diameter": _rebuild_value(feature.get("diameter")),
}
if operation == "polar_hole_pattern":
return {
"type": "circle",
"center": {
"x": {
"expression": "pitch_diameter / 2",
"value": _rebuild_value(feature.get("pitch_diameter")),
},
"y": 0,
"z": 0,
},
"diameter": _rebuild_value(feature.get("diameter")),
}
return {"type": "unknown"}
def _feature_ref(feature: dict[str, Any], key: str, default: Any = None) -> Any:
return _rebuild_value(feature.get(key), default)
def _operation_spec_for_feature(
feature: dict[str, Any],
sw_type: str,
*,
sketch_id: str | None,
seed_feature_id: str | None = None,
) -> dict[str, Any]:
operation = str(feature.get("operation") or "unsupported")
axis = _feature_ref(feature, "axis", "primary_axis")
if sw_type == "BossExtrude":
return {
"kind": "boss_extrude",
"designir_operation": operation,
"profile": _sketch_profile_for_feature(feature, {}),
"sketch": sketch_id,
"depth": _feature_ref(feature, "height"),
"direction": axis,
"center": _feature_ref(feature, "center", [0, 0, 0]),
"anchor": _feature_ref(feature, "anchor", "base_center"),
"end_condition": "Blind",
"merge_result": True,
}
if sw_type == "CutExtrude":
return {
"kind": "cut_extrude",
"designir_operation": "through_hole",
"profile": _sketch_profile_for_feature(feature, {}),
"sketch": sketch_id,
"diameter": _feature_ref(feature, "diameter"),
"center": _feature_ref(feature, "center", [0, 0, 0]),
"direction": axis,
"host": _feature_ref(feature, "host"),
"end_condition": "ThroughAll",
}
if sw_type == "CircularPattern":
return {
"kind": "circular_pattern",
"designir_operation": "polar_hole_pattern",
"seed_feature": seed_feature_id,
"count": _feature_ref(feature, "count"),
"diameter": _feature_ref(feature, "diameter"),
"pitch_diameter": _feature_ref(feature, "pitch_diameter"),
"axis": axis,
"host": _feature_ref(feature, "host"),
"total_angle": 360,
"equal_spacing": True,
}
return {
"kind": "unsupported",
"designir_operation": operation,
}
def _profile_entities(profile: dict[str, Any]) -> list[dict[str, Any]]:
kind = profile.get("type")
if kind == "circle":
entity = {
"id": "entity:circle1",
"type": "Circle",
"center": _rebuild_value(profile.get("center", [0, 0, 0])),
}
if profile.get("radius") is not None:
entity["radius"] = _rebuild_value(profile.get("radius"))
if profile.get("diameter") is not None:
entity["diameter"] = _rebuild_value(profile.get("diameter"))
return [entity]
if kind == "rectangle":
return [
{
"id": "entity:rectangle1",
"type": "CenterRectangle",
"center": _rebuild_value(profile.get("center", [0, 0, 0])),
"width": _rebuild_value(profile.get("width")),
"height": _rebuild_value(profile.get("depth")),
}
]
return []
def _dimensions_from_parameters(
owner_display_name: str, parameters: list[dict[str, Any]]
) -> list[dict[str, Any]]:
dimensions: list[dict[str, Any]] = []
for index, parameter in enumerate(parameters, start=1):
if not isinstance(parameter, dict):
continue
binding_id = parameter.get("binding_id") or parameter.get("name")
dimensions.append(
{
"id": f"dimension:{binding_id or index}",
"name": f"D{index}@{owner_display_name}",
"display_name": parameter.get("display_name") or str(binding_id or f"D{index}"),
"value": parameter.get("value"),
"default_value": parameter.get("default_value"),
"unit": parameter.get("unit"),
"parameter_binding": binding_id,
"driven": False,
}
)
return dimensions
def _sketch_definition(
feature: dict[str, Any],
parameters: dict[str, Any],
*,
plane: str,
) -> dict[str, Any]:
profile = _sketch_profile_for_feature(feature, parameters)
return {
"feature_type": "ProfileFeature",
"sketch_type": "2DProfile",
"plane": plane,
"profile": profile,
"entities": _profile_entities(profile),
"relations": [],
"fully_defined": False,
}
def _solidworks_definition_for_feature(
sw_type: str,
display_name: str,
operation_spec: dict[str, Any],
*,
sketch_id: str | None,
seed_feature_id: str | None = None,
) -> dict[str, Any]:
if sw_type == "BossExtrude":
return {
"feature_type": "BossExtrude",
"name": display_name,
"sketch": sketch_id,
"end_condition": operation_spec.get("end_condition", "Blind"),
"depth": _rebuild_value(operation_spec.get("depth")),
"direction": _rebuild_value(operation_spec.get("direction"), "primary_axis"),
"merge_result": bool(operation_spec.get("merge_result", True)),
"thin_feature": False,
"draft": {"enabled": False, "angle": None},
}
if sw_type == "CutExtrude":
return {
"feature_type": "CutExtrude",
"name": display_name,
"sketch": sketch_id,
"end_condition": operation_spec.get("end_condition", "ThroughAll"),
"depth": _rebuild_value(operation_spec.get("depth")),
"direction": _rebuild_value(operation_spec.get("direction"), "primary_axis"),
"flip_side_to_cut": False,
"draft": {"enabled": False, "angle": None},
}
if sw_type == "CircularPattern":
return {
"feature_type": "CircularPattern",
"name": display_name,
"seed_features": [seed_feature_id] if seed_feature_id else [],
"axis": _rebuild_value(operation_spec.get("axis"), "primary_axis"),
"instance_count": _rebuild_value(operation_spec.get("count")),
"total_angle": operation_spec.get("total_angle", 360),
"equal_spacing": bool(operation_spec.get("equal_spacing", True)),
"geometry_pattern": False,
}
return {
"feature_type": sw_type,
"name": display_name,
"supported_definition": False,
}
def _sketch_node(
*,
sketch_index: int,
feature: dict[str, Any],
parameters: dict[str, Any],
source_pointer: str,
order: int,
) -> dict[str, Any]:
feature_id = _slug(feature.get("id"), f"feature_{sketch_index}")
sketch_id = f"sketch:{feature_id}"
plane = feature.get("plane") or feature.get("sketch_plane") or "Top Plane"
feature_parameters = _feature_parameters(feature, parameters)
return _node(
node_id=sketch_id,
order=order,
solidworks_type="ProfileFeature",
display_name=f"{SKETCH_PREFIX}{sketch_index}",
english_name=f"Sketch{sketch_index}",
inputs={"plane": plane},
parameters=feature_parameters,
definition=_sketch_definition(feature, parameters, plane=plane),
dimensions=_dimensions_from_parameters(f"{SKETCH_PREFIX}{sketch_index}", feature_parameters),
references={
"parent": "sw:history",
"sketch": None,
"plane": plane,
"parent_features": [],
"child_features": [],
},
selection_sets=_empty_selection_sets(),
operation_spec={
"kind": "sketch",
"profile": _sketch_profile_for_feature(feature, parameters),
"source_pointer": source_pointer,
},
result={"profile_available": True},
provenance="generated_from_designir",
confidence=feature.get("confidence", 1.0),
rebuildable=True,
)
def _surface_summary(surface_layer: Any) -> dict[str, Any]:
if not isinstance(surface_layer, dict):
return {
"solid_count": 0,
"free_shell_count": 0,
"vertex_count": 0,
"surface_vocabulary": [],
"curve_vocabulary": [],
}
solids = surface_layer.get("solids")
free_shells = surface_layer.get("free_shells")
vertices = surface_layer.get("vertices")
return {
"solid_count": len(solids) if isinstance(solids, list) else 0,
"free_shell_count": len(free_shells) if isinstance(free_shells, list) else 0,
"vertex_count": len(vertices) if isinstance(vertices, list) else 0,
"surface_vocabulary": surface_layer.get("surface_vocabulary", []),
"curve_vocabulary": surface_layer.get("curve_vocabulary", []),
}
def _encode_payload(payload: dict[str, Any]) -> str:
raw = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
return base64.b64encode(gzip.compress(raw)).decode("ascii")
def _decode_payload(encoded: str) -> dict[str, Any]:
try:
raw = gzip.decompress(base64.b64decode(encoded.encode("ascii")))
decoded = json.loads(raw.decode("utf-8"))
except Exception as exc: # pragma: no cover - exact exception varies by zlib/base64
raise FeatureTreeError(f"Cannot decode embedded replay payload: {exc}") from exc
if not isinstance(decoded, dict):
raise FeatureTreeError("Embedded replay payload must be a JSON object")
return decoded
def _ordered_features(semantic: dict[str, Any]) -> list[dict[str, Any]]:
features = semantic.get("features", [])
if isinstance(features, dict):
normalized = []
for key, value in features.items():
if isinstance(value, dict):
normalized.append({"id": key, **value})
return normalized
return [item for item in features if isinstance(item, dict)] if isinstance(features, list) else []
def _base_tree(
payload: dict[str, Any],
*,
designir_path: str | None,
step_path: str | None,
parameters_path: str | None,
backend: str | None,
compiled: bool,
validated: bool,
) -> dict[str, Any]:
model_id = payload.get("model_id") or "part"
history_kind = _history_kind(payload)
units = payload.get("units") or "mm"
root = _node(
node_id="sw:part",
order=0,
solidworks_type="Part",
display_name=str(model_id),
english_name=str(model_id),
children=["sw:history"],
inputs={},
operation_spec={"document_type": "part"},
result={"units": units},
provenance={"source": "generated_from_designir", "history_kind": history_kind},
confidence=1.0,
rebuildable=True,
)
nodes = [root]
for order, (node_id, sw_type, display, english) in enumerate(STANDARD_NODES, start=1):
parent_children = []
if node_id == "sw:history":
parent_children = ["sw:origin_folder"]
if node_id == "sw:origin_folder":
parent_children = ["sw:front_plane", "sw:top_plane", "sw:right_plane", "sw:origin"]
nodes.append(
_node(
node_id=node_id,
order=order,
solidworks_type=sw_type,
display_name=display,
english_name=english,
children=parent_children,
inputs={
"parent": (
"sw:part"
if node_id == "sw:history"
else "sw:history"
if node_id == "sw:origin_folder"
else "sw:origin_folder"
)
},
operation_spec={"standard_reference_geometry": True},
result={},
provenance="solidworks_standard_tree",
confidence=1.0,
rebuildable=True,
)
)
return {
"schema_version": SCHEMA_VERSION,
"tree_kind": TREE_KIND,
"model": {
"model_id": model_id,
"family": payload.get("family"),
"units": units,
"document_type": "part",
},
"source": {
"authority": "designir-3.0",
"designir_path": designir_path,
"step_path": step_path,
"parameters_path": parameters_path,
"reconstruction_mode": payload.get("reconstruction_mode"),
"history_kind": history_kind,
"backend": backend or payload.get("backend_hint"),
"claims_original_solidworks_history": False,
},
"feature_manager": {
"root_id": "sw:part",
"standard_root": {
"part": "sw:part",
"history_folder": "sw:history",
"origin_folder": "sw:origin_folder",
"front_plane": "sw:front_plane",
"top_plane": "sw:top_plane",
"right_plane": "sw:right_plane",
"origin": "sw:origin",
},
"nodes": nodes,
},
"rebuild_contract": {},
"validation": {
"structure": {"status": "pending", "errors": []},
"replay": {
"status": "not_run",
"compiled_by_generation_pipeline": bool(compiled),
},
"geometry": {
"status": "external_pipeline_validated" if validated else "not_run",
"step_path": step_path,
},
"brep": {"status": "not_run"},
"feature_recognition": {
"status": "not_run",
"coverage": None,
"unrecognized_reasons": [],
},
},
}
def _append_semantic_nodes(tree: dict[str, Any], payload: dict[str, Any]) -> None:
semantic = payload["semantic_layer"]
parameters = semantic.get("parameters", {})
if not isinstance(parameters, dict):
parameters = {}
nodes = tree["feature_manager"]["nodes"]
history_node = next(node for node in nodes if node["id"] == "sw:history")
history_children = history_node["children"]
counters = {
"sketch": 0,
"BossExtrude": 0,
"CutExtrude": 0,
"Revolve": 0,
"RevolvedCut": 0,
"HoleWizard": 0,
"Fillet": 0,
"Chamfer": 0,
"LinearPattern": 0,
"CircularPattern": 0,
"MirrorPattern": 0,
"UnsupportedFeature": 0,
}
order = max(node["order"] for node in nodes) + 1
sketch_required_ops = {
"extrude_circle",
"extrude_rectangle",
"add_cylinder",
"through_hole",
"polar_hole_pattern",
"revolve",
"revolved_cut",
"hole_wizard",
}
last_body_node: str | None = None
for index, feature in enumerate(_ordered_features(semantic), start=1):
operation = str(feature.get("operation") or "unsupported")
feature_source_pointer = _json_pointer(["semantic_layer", "features", index - 1])
host_node_id = (
f"feature:{_slug(feature.get('host'), 'host')}"
if isinstance(feature.get("host"), str) and feature.get("host")
else last_body_node
)
sketch_id: str | None = None
if operation in sketch_required_ops:
counters["sketch"] += 1
sketch = _sketch_node(
sketch_index=counters["sketch"],
feature=feature,
parameters=parameters,
source_pointer=feature_source_pointer,
order=order,
)
order += 1
nodes.append(sketch)
history_children.append(sketch["id"])
sketch_id = sketch["id"]
sw_type = DESIGNIR_TO_SW.get(operation, "UnsupportedFeature")
seed_id: str | None = None
if operation == "polar_hole_pattern":
seed_id = f"feature:{_slug(feature.get('id'), f'feature_{index}')}:seed_cut"
counters["CutExtrude"] += 1
seed_display_name = f"{DISPLAY_PREFIX['CutExtrude']}{counters['CutExtrude']}"
seed_parameters = _feature_parameters(feature, parameters)
seed_operation_spec = _operation_spec_for_feature(
feature,
"CutExtrude",
sketch_id=sketch_id,
) | {
"kind": "cut_extrude_seed",
"participates_in": "CircularPattern",
"source_pointer": feature_source_pointer,
}
seed_node = _node(
node_id=seed_id,
order=order,
solidworks_type="CutExtrude",
display_name=seed_display_name,
english_name=f"Cut-Extrude{counters['CutExtrude']}",
children=[],
inputs={"sketch": sketch_id, "host": host_node_id},
parameters=seed_parameters,
definition=_solidworks_definition_for_feature(
"CutExtrude",
seed_display_name,
seed_operation_spec,
sketch_id=sketch_id,
),
dimensions=_dimensions_from_parameters(seed_display_name, seed_parameters),
references={
"parent": "sw:history",
"sketch": sketch_id,
"plane": None,
"parent_features": [host_node_id] if host_node_id else [],
"child_features": [],
},
selection_sets=_empty_selection_sets(),
operation_spec=seed_operation_spec,
result={"creates_seed_for": "CircularPattern"},
provenance="generated_from_designir",
confidence=feature.get("confidence", 1.0),
rebuildable=True,
)
nodes.append(seed_node)
history_children.append(seed_id)
order += 1
if sketch_id:
next(node for node in nodes if node["id"] == sketch_id)["children"].append(seed_id)
counters[sw_type] = counters.get(sw_type, 0) + 1
number = counters[sw_type]
prefix = DISPLAY_PREFIX.get(sw_type, "特征")
feature_id = f"feature:{_slug(feature.get('id'), f'feature_{index}')}"
feature_display_name = f"{prefix}{number}"
feature_parameters = _feature_parameters(feature, parameters)
feature_operation_spec = _operation_spec_for_feature(
feature,
sw_type,
sketch_id=sketch_id,
seed_feature_id=seed_id,
) | {"source_pointer": feature_source_pointer}
feature_node = _node(
node_id=feature_id,
order=order,
solidworks_type=sw_type,
display_name=feature_display_name,
english_name=(
"Boss-Extrude"
if sw_type == "BossExtrude"
else "Cut-Extrude"
if sw_type == "CutExtrude"
else sw_type
)
+ str(number),
children=[],
inputs={
"sketch": sketch_id,
"host": host_node_id if sw_type in {"CutExtrude", "Fillet", "Chamfer"} else None,
},
parameters=feature_parameters,
definition=_solidworks_definition_for_feature(
sw_type,
feature_display_name,
feature_operation_spec,
sketch_id=sketch_id,
seed_feature_id=seed_id,
),
dimensions=_dimensions_from_parameters(feature_display_name, feature_parameters),
references={
"parent": "sw:history",
"sketch": sketch_id,
"plane": None,
"parent_features": (
[host_node_id]
if host_node_id and sw_type in {"CutExtrude", "Fillet", "Chamfer"}
else [seed_id]
if seed_id and sw_type == "CircularPattern"
else []
),
"child_features": [],
},
selection_sets=_empty_selection_sets(),
operation_spec=feature_operation_spec,
result={"operation": "add" if sw_type == "BossExtrude" else "modify"},
provenance={
"source": "generated_from_designir",
"original_history_recovered": bool(
feature.get("original_history_recovered", False)
),
},
confidence=feature.get("confidence", 1.0),
rebuildable=sw_type != "UnsupportedFeature",
)
nodes.append(feature_node)
history_children.append(feature_id)
if sketch_id:
next(node for node in nodes if node["id"] == sketch_id)["children"].append(feature_id)
if sw_type == "BossExtrude":
last_body_node = feature_id
order += 1
tree["rebuild_contract"] = {
"kind": "feature_manager_native",
"entrypoint": "feature_tree.compile_feature_manager",
"backend": tree["source"].get("backend") or payload.get("backend_hint") or "build123d",
}
tree["validation"]["feature_recognition"] = {
"status": "semantic_authority",
"coverage": 1.0,
"unrecognized_reasons": [],
}
def _append_imported_feature(tree: dict[str, Any], payload: dict[str, Any]) -> None:
nodes = tree["feature_manager"]["nodes"]
history_node = next(node for node in nodes if node["id"] == "sw:history")
history_children = history_node["children"]
encoded = _encode_payload(payload)
digest = hashlib.sha256(encoded.encode("ascii")).hexdigest()
summary = _surface_summary(payload.get("surface_layer"))
imported = _node(
node_id="feature:Imported1",
order=max(node["order"] for node in nodes) + 1,
solidworks_type="ImportedFeature",
display_name="导入1",
english_name="Imported1",
children=[],
inputs={"source": "embedded_surfaceir"},
parameters=[],
definition={
"feature_type": "ImportedFeature",
"name": "导入1",
"imported_body": True,
"native_history_recovered": False,
"surface_body_source": "SurfaceIR",
},
dimensions=[],
references={
"parent": "sw:history",
"sketch": None,
"plane": None,
"parent_features": [],
"child_features": [],
},
selection_sets=_empty_selection_sets(),
operation_spec={
"kind": "surfaceir_import",
"payload_encoding": "gzip+base64+json",
"payload_sha256": digest,
"payload": encoded,
"surface_summary": summary,
},
result={"geometry_source": "SurfaceIR", **summary},
provenance={
"source": "inferred_from_step",
"original_history_recovered": False,
"note": "STEP does not carry authoritative native CAD feature history; this is a SolidWorks-style imported feature.",
},
confidence=1.0,
rebuildable=True,
)
nodes.append(imported)
history_children.append(imported["id"])
tree["rebuild_contract"] = {
"kind": "surfaceir_imported_feature",
"entrypoint": "surfaceir_pipeline.build_surfaceir",
"payload_node": imported["id"],
"payload_encoding": "gzip+base64+json",
"payload_sha256": digest,
}
tree["validation"]["feature_recognition"] = {
"status": "fallback_imported_feature",
"coverage": 0.0,
"unrecognized_reasons": [
"Uploaded STEP does not provide native SolidWorks FeatureManager history.",
"Native feature decomposition is only publishable after independent replay and geometry validation.",
],
}
def _backfill_reference_children(tree: dict[str, Any]) -> None:
nodes = _nodes_by_id(tree)
for node in nodes.values():
references = node.setdefault("references", _default_references(node.get("inputs", {}), node.get("children", [])))
child_features = references.setdefault("child_features", [])
for child_id in node.get("children", []):
if child_id not in child_features:
child_features.append(child_id)
for node in nodes.values():
node_id = node.get("id")
references = node.get("references", {})
sketch_id = references.get("sketch")
if isinstance(sketch_id, str) and sketch_id in nodes:
sketch_children = nodes[sketch_id].setdefault("references", {}).setdefault("child_features", [])
if node_id not in sketch_children:
sketch_children.append(node_id)
for parent_id in references.get("parent_features", []):
if isinstance(parent_id, str) and parent_id in nodes:
parent_children = nodes[parent_id].setdefault("references", {}).setdefault("child_features", [])
if node_id not in parent_children:
parent_children.append(node_id)
def validate_feature_tree(tree: dict[str, Any]) -> dict[str, Any]:
errors: list[str] = []
if not isinstance(tree, dict):
raise FeatureTreeError("Feature tree must be a JSON object")
if tree.get("schema_version") != SCHEMA_VERSION:
errors.append("schema_version must be 1.0")
if tree.get("tree_kind") != TREE_KIND:
errors.append(f"tree_kind must be {TREE_KIND}")
manager = tree.get("feature_manager")
nodes = manager.get("nodes") if isinstance(manager, dict) else None
if not isinstance(nodes, list) or not nodes:
errors.append("feature_manager.nodes must be a non-empty array")
return {"valid": False, "errors": errors}
ids: set[str] = set()
display_names: set[str] = set()
order_values: set[int] = set()
required = {
"id",
"order",
"solidworks_type",
"display_name",
"english_name",
"children",
"inputs",
"parameters",
"definition",
"dimensions",
"references",
"selection_sets",
"operation_spec",
"result",
"rebuild",
"provenance",
"confidence",
"rebuildable",
}
for node in nodes:
if not isinstance(node, dict):
errors.append("Every feature_manager node must be an object")
continue
missing = sorted(required - set(node))
if missing:
errors.append(f"{node.get('id', '<unknown>')} missing fields: {', '.join(missing)}")
node_id = node.get("id")
if not isinstance(node_id, str) or not node_id:
errors.append("Every node requires a non-empty id")
elif node_id in ids:
errors.append(f"Duplicate node id: {node_id}")
else:
ids.add(node_id)
order = node.get("order")
if not isinstance(order, int):
errors.append(f"{node_id} order must be an integer")
elif order in order_values:
errors.append(f"Duplicate node order: {order}")
else:
order_values.add(order)
sw_type = node.get("solidworks_type")
if sw_type not in SUPPORTED_SW_TYPES:
errors.append(f"{node_id} unsupported solidworks_type: {sw_type}")
display = node.get("display_name")
if isinstance(display, str) and display:
if display in display_names and display not in {"原点"}:
errors.append(f"Duplicate display_name: {display}")
display_names.add(display)
if not isinstance(node.get("children"), list):
errors.append(f"{node_id} children must be an array")
if not isinstance(node.get("parameters"), list):
errors.append(f"{node_id} parameters must be an array")
if not isinstance(node.get("definition"), dict):
errors.append(f"{node_id} definition must be an object")
if not isinstance(node.get("dimensions"), list):
errors.append(f"{node_id} dimensions must be an array")
if not isinstance(node.get("references"), dict):
errors.append(f"{node_id} references must be an object")
if not isinstance(node.get("selection_sets"), dict):
errors.append(f"{node_id} selection_sets must be an object")
if not isinstance(node.get("rebuild"), dict):
errors.append(f"{node_id} rebuild must be an object")
for parameter in node.get("parameters", []):
if not isinstance(parameter, dict):
errors.append(f"{node_id} parameter entries must be objects")
continue
if not parameter.get("binding_id"):
errors.append(f"{node_id} parameter missing binding_id")
for node in nodes:
if not isinstance(node, dict):
continue
for child_id in node.get("children", []):
if child_id not in ids:
errors.append(f"{node.get('id')} references missing child {child_id}")
graph = {
node["id"]: [child for child in node.get("children", []) if child in ids]
for node in nodes
if isinstance(node, dict) and isinstance(node.get("id"), str)
}
visiting: set[str] = set()
visited: set[str] = set()
def visit(node_id: str) -> None:
if node_id in visiting:
errors.append(f"Cycle detected at {node_id}")
return
if node_id in visited:
return
visiting.add(node_id)
for child in graph.get(node_id, []):
visit(child)
visiting.remove(node_id)
visited.add(node_id)
root_id = manager.get("root_id") if isinstance(manager, dict) else None
if root_id not in ids:
errors.append("feature_manager.root_id is missing or unknown")
elif isinstance(root_id, str):
visit(root_id)
return {"valid": not errors, "errors": errors}
def _nodes_by_id(tree: dict[str, Any]) -> dict[str, dict[str, Any]]:
return {
node["id"]: node
for node in tree.get("feature_manager", {}).get("nodes", [])
if isinstance(node, dict) and isinstance(node.get("id"), str)
}
def _feature_id_from_node(node: dict[str, Any]) -> str:
raw_id = str(node.get("id") or "")
if raw_id.startswith("feature:"):
raw_id = raw_id[len("feature:") :]
return _slug(raw_id.replace(":seed_cut", "_seed_cut"), "feature")
def _collect_tree_parameters(tree: dict[str, Any]) -> dict[str, dict[str, Any]]:
parameters: dict[str, dict[str, Any]] = {}
for node in tree.get("feature_manager", {}).get("nodes", []):
if not isinstance(node, dict):
continue
for parameter in node.get("parameters", []):
if not isinstance(parameter, dict):
continue
name = parameter.get("binding_id") or parameter.get("name")
if not isinstance(name, str) or not name:
continue
existing = parameters.get(name, {})
parameters[name] = {
"value": parameter.get("value", existing.get("value")),
"unit": parameter.get("unit", existing.get("unit", "mm")),
"editable": bool(parameter.get("editable", existing.get("editable", True))),
}
if parameter.get("display_name"):
parameters[name]["display_name"] = parameter["display_name"]
if parameter.get("default_value") is not None:
parameters[name]["default_value"] = parameter["default_value"]
if parameter.get("edit_state") is not None:
parameters[name]["edit_state"] = parameter["edit_state"]
return parameters
def _profile_for_node(
node: dict[str, Any], nodes: dict[str, dict[str, Any]]
) -> dict[str, Any]:
spec = node.get("operation_spec", {})
profile = spec.get("profile") if isinstance(spec, dict) else None
if isinstance(profile, dict) and profile.get("type"):
return copy.deepcopy(profile)
sketch_id = node.get("inputs", {}).get("sketch")
sketch = nodes.get(sketch_id) if isinstance(sketch_id, str) else None
sketch_spec = sketch.get("operation_spec", {}) if isinstance(sketch, dict) else {}
sketch_profile = sketch_spec.get("profile") if isinstance(sketch_spec, dict) else None
if isinstance(sketch_profile, dict) and sketch_profile.get("type"):
return copy.deepcopy(sketch_profile)
return {"type": "unknown"}
def _normalize_center(value: Any, default: list[Any] | None = None) -> list[Any]:
if default is None:
default = [0, 0, 0]
if not isinstance(value, list):
return copy.deepcopy(default)
if len(value) == 2:
return [copy.deepcopy(value[0]), copy.deepcopy(value[1]), 0]
if len(value) >= 3:
return [copy.deepcopy(value[0]), copy.deepcopy(value[1]), copy.deepcopy(value[2])]
return copy.deepcopy(default)
def _compile_boss_extrude_node(
node: dict[str, Any], nodes: dict[str, dict[str, Any]]
) -> dict[str, Any]:
spec = node.get("operation_spec", {})
if not isinstance(spec, dict):
spec = {}
profile = _profile_for_node(node, nodes)
operation = str(spec.get("designir_operation") or "")
if operation not in {"extrude_circle", "add_cylinder", "extrude_rectangle"}:
operation = "extrude_rectangle" if profile.get("type") == "rectangle" else "extrude_circle"
feature: dict[str, Any] = {
"id": _feature_id_from_node(node),
"operation": operation,
"height": _rebuild_value(spec.get("depth")),
"axis": _rebuild_value(spec.get("direction"), "primary_axis"),
"center": _normalize_center(spec.get("center", profile.get("center"))),
}
if spec.get("anchor"):
feature["anchor"] = spec["anchor"]
if operation == "extrude_rectangle":
feature["width"] = _rebuild_value(profile.get("width"))
feature["depth"] = _rebuild_value(profile.get("depth"))
else:
feature["radius"] = _rebuild_value(profile.get("radius"))
return feature
def _compile_cut_extrude_node(
node: dict[str, Any], nodes: dict[str, dict[str, Any]]
) -> dict[str, Any] | None:
spec = node.get("operation_spec", {})
if not isinstance(spec, dict):
spec = {}
if spec.get("kind") == "cut_extrude_seed":
return None
profile = _profile_for_node(node, nodes)
return {
"id": _feature_id_from_node(node),
"operation": "through_hole",
"diameter": _rebuild_value(spec.get("diameter", profile.get("diameter"))),
"axis": _rebuild_value(spec.get("direction"), "primary_axis"),
"center": _normalize_center(spec.get("center", profile.get("center"))),
"host": _rebuild_value(spec.get("host") or node.get("inputs", {}).get("host")),
}
def _compile_circular_pattern_node(node: dict[str, Any]) -> dict[str, Any]:
spec = node.get("operation_spec", {})
if not isinstance(spec, dict):
spec = {}
return {
"id": _feature_id_from_node(node),
"operation": "polar_hole_pattern",
"count": _rebuild_value(spec.get("count")),
"diameter": _rebuild_value(spec.get("diameter")),
"pitch_diameter": _rebuild_value(spec.get("pitch_diameter")),
"axis": _rebuild_value(spec.get("axis"), "primary_axis"),
"host": _rebuild_value(spec.get("host") or node.get("inputs", {}).get("host")),
}
def _compile_feature_manager_native(tree: dict[str, Any]) -> dict[str, Any]:
nodes = _nodes_by_id(tree)
ordered_nodes = sorted(nodes.values(), key=lambda item: int(item.get("order", 0)))
features: list[dict[str, Any]] = []
unsupported: list[str] = []
for node in ordered_nodes:
sw_type = node.get("solidworks_type")
if sw_type in {
"Part",
"HistoryFolder",
"OriginProfileFeature",
"RefPlane",
"RefAxis",
"ProfileFeature",
}:
continue
if sw_type == "BossExtrude":
features.append(_compile_boss_extrude_node(node, nodes))
elif sw_type == "CutExtrude":
cut = _compile_cut_extrude_node(node, nodes)
if cut is not None:
features.append(cut)
elif sw_type == "CircularPattern":
features.append(_compile_circular_pattern_node(node))
elif sw_type == "ImportedFeature":
unsupported.append(f"{node.get('display_name')} must use surfaceir_imported_feature replay")
else:
unsupported.append(
f"{node.get('display_name') or node.get('id')} ({sw_type}) is not supported by the FeatureTreeCompiler"
)
if unsupported:
raise FeatureTreeError("; ".join(unsupported))
if not features:
raise FeatureTreeError("FeatureManager tree does not contain rebuildable semantic features")
parameters = _collect_tree_parameters(tree)
editable_parameters = [
name for name, parameter in parameters.items() if parameter.get("editable", True)
]
model = tree.get("model", {})
source = tree.get("source", {})
return {
"schema_version": "3.0",
"designir_kind": "independent_parametric_cad",
"model_id": model.get("model_id") or "feature_tree_replay",
"family": model.get("family") or "general_mechanical_part",
"units": model.get("units") or "mm",
"document_status": "geometry_present",
"reconstruction_mode": "fully_semantic_parametric",
"authoring_mode": "feature_manager_tree_replay",
"backend_hint": source.get("backend") or tree.get("rebuild_contract", {}).get("backend") or "build123d",
"semantic_layer": {
"reconstruction_status": "ready",
"coordinate_system": {
"origin": [0, 0, 0],
"x_axis": [1, 0, 0],
"y_axis": [0, 1, 0],
"z_axis": [0, 0, 1],
},
"datums": {
"primary_axis": {"kind": "axis", "axis": "z"},
},
"parameters": parameters,
"expressions": {},
"sketches": [],
"constraints": [],
"features": features,
"patterns": [],
"attachments": [],
"construction_stages": [],
},
"edit_interface": {
"semantic_parameters": editable_parameters,
"surface_parameter_groups": [],
"modification_levels": ["semantic_feature"],
"preserved_interfaces": [],
},
"validation_contract": {
"source_independence": True,
"geometry_checks": ["solid_count", "bounding_box", "volume", "center_of_mass"],
"edit_checks": ["parameter_perturbation", "constraint_preservation"],
"invariants": [],
"perturbations": [],
"thresholds": {},
},
}
def generate_feature_tree(
designir: dict[str, Any],
*,
designir_path: str | None = None,
step_path: str | None = None,
parameters_path: str | None = None,
backend: str | None = None,
compiled: bool = False,
validated: bool = False,
) -> dict[str, Any]:
payload = _require_designir_3(designir)
tree = _base_tree(
payload,
designir_path=designir_path,
step_path=step_path,
parameters_path=parameters_path,
backend=backend,
compiled=compiled,
validated=validated,
)
if payload.get("reconstruction_mode") == "surface_parametric":
_append_imported_feature(tree, payload)
else:
_append_semantic_nodes(tree, payload)
_backfill_reference_children(tree)
validation = validate_feature_tree(tree)
tree["validation"]["structure"] = {
"status": "valid" if validation["valid"] else "invalid",
"errors": validation["errors"],
}
if not validation["valid"]:
raise FeatureTreeError("; ".join(validation["errors"]))
return tree
def designir_from_feature_tree(tree: dict[str, Any]) -> dict[str, Any]:
validation = validate_feature_tree(tree)
if not validation["valid"]:
raise FeatureTreeError("; ".join(validation["errors"]))
contract = tree.get("rebuild_contract", {})
if not isinstance(contract, dict):
raise FeatureTreeError("rebuild_contract must be an object")
if contract.get("kind") == "feature_manager_native":
return _compile_feature_manager_native(tree)
if contract.get("kind") == "surfaceir_imported_feature":
payload_node_id = contract.get("payload_node")
nodes = tree.get("feature_manager", {}).get("nodes", [])
imported = next(
(
node
for node in nodes
if isinstance(node, dict) and node.get("id") == payload_node_id
),
None,
)
if not isinstance(imported, dict):
raise FeatureTreeError("surface rebuild contract references a missing payload node")
operation_spec = imported.get("operation_spec", {})
if operation_spec.get("payload_encoding") != "gzip+base64+json":
raise FeatureTreeError("unsupported imported feature payload encoding")
return _decode_payload(str(operation_spec.get("payload") or ""))
raise FeatureTreeError(f"Unsupported rebuild_contract kind: {contract.get('kind')}")
def _load_module(name: str, path: Path) -> Any:
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
if spec is None or spec.loader is None:
raise FeatureTreeError(f"Cannot load module {path}")
spec.loader.exec_module(module)
return module
def compile_feature_tree(
tree: dict[str, Any],
output_step: Path,
*,
output_designir: Path | None = None,
backend: str | None = None,
) -> dict[str, Any]:
designir = designir_from_feature_tree(tree)
output_step = output_step.expanduser().resolve()
output_step.parent.mkdir(parents=True, exist_ok=True)
if output_designir is not None:
write_designir(output_designir.expanduser().resolve(), designir)
if designir.get("reconstruction_mode") == "surface_parametric":
surfaceir = _load_module("_feature_tree_surfaceir_pipeline", SCRIPT_DIR / "surfaceir_pipeline.py")
shape = surfaceir.build_surfaceir(designir)
surfaceir.export_step_shape(shape, output_step)
return {
"valid": True,
"output_step": str(output_step),
"output_designir": str(output_designir) if output_designir else None,
"backend": "surfaceir_occt",
"replay_kind": "surfaceir_imported_feature",
}
designir_pipeline = _load_module("_feature_tree_designir_pipeline", SCRIPT_DIR / "designir_pipeline.py")
result = designir_pipeline.compile_designir(designir, output_step, backend=backend)
return {
"valid": True,
"output_step": str(output_step),
"output_designir": str(output_designir) if output_designir else None,
"backend": result.get("backend"),
"replay_kind": "feature_manager_native",
"facts": result.get("facts"),
}
def write_feature_tree(path: Path, tree: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(tree, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
generate = commands.add_parser("generate")
generate.add_argument("designir", type=Path)
generate.add_argument("--output", type=Path, required=True)
generate.add_argument("--designir-path")
generate.add_argument("--step")
generate.add_argument("--parameters")
generate.add_argument("--backend")
generate.add_argument("--compiled", action="store_true")
generate.add_argument("--validated", action="store_true")
validate = commands.add_parser("validate")
validate.add_argument("tree", type=Path)
compile_cmd = commands.add_parser("compile")
compile_cmd.add_argument("tree", type=Path)
compile_cmd.add_argument("--output-step", type=Path, required=True)
compile_cmd.add_argument("--output-designir", type=Path)
compile_cmd.add_argument("--backend")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
try:
if args.command == "generate":
designir_path = args.designir.expanduser().resolve()
output_path = args.output.expanduser().resolve()
tree = generate_feature_tree(
read_designir(designir_path),
designir_path=args.designir_path or args.designir.name,
step_path=args.step,
parameters_path=args.parameters,
backend=args.backend,
compiled=args.compiled,
validated=args.validated,
)
write_feature_tree(output_path, tree)
print(
json.dumps(
{
"valid": True,
"output": str(output_path),
"node_count": len(tree["feature_manager"]["nodes"]),
"tree_kind": tree["tree_kind"],
"history_kind": tree["source"]["history_kind"],
},
ensure_ascii=False,
)
)
elif args.command == "validate":
tree = json.loads(args.tree.expanduser().read_text(encoding="utf-8"))
result = validate_feature_tree(tree)
print(json.dumps(result, ensure_ascii=False))
return 0 if result["valid"] else 1
elif args.command == "compile":
tree = json.loads(args.tree.expanduser().read_text(encoding="utf-8"))
result = compile_feature_tree(
tree,
args.output_step,
output_designir=args.output_designir,
backend=args.backend,
)
print(json.dumps(result, ensure_ascii=False))
except Exception as exc:
print(json.dumps({"valid": False, "error": str(exc)}, ensure_ascii=False))
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())