Files
cad-Integration/SimpleCADAPI-master/examples/07_serialization_operation_tree.py
T
2026-07-22 13:48:46 +08:00

313 lines
14 KiB
Python

"""Show how source code maps to the serializable operation tree.
Run from the repository root with:
uv run python examples/07_serialization_operation_tree.py
This example intentionally keeps the geometry simple. Its main purpose is to
show that user-facing calls such as `make_box_rsolid()` and
`helical_sweep_rsolid()` are lowered into the canonical, replayable operation
nodes stored in `model.json`.
Generated files:
examples/out/serialization_operation_tree.model.json
examples/out/serialization_operation_tree.summary.md
examples/out/serialization_operation_tree.step
"""
from __future__ import annotations
import json
from collections import Counter
from pathlib import Path
from textwrap import dedent
import simplecadapi as scad
from simplecadapi import ql as Q
OUT = Path("examples/out")
OUT.mkdir(parents=True, exist_ok=True)
MODEL_JSON_PATH = OUT / "serialization_operation_tree.model.json"
SUMMARY_PATH = OUT / "serialization_operation_tree.summary.md"
STEP_PATH = OUT / "serialization_operation_tree.step"
def source_step(name: str):
"""Print a readable marker while building the recorded model."""
print(f"SOURCE STEP: {name}")
# ---------------------------------------------------------------------------
# Expression parameters: model JSON stores numeric snapshots in node.params and
# expression references in node.param_exprs / expression_graph.
# ---------------------------------------------------------------------------
plate_w = scad.var("plate_w", 36.0, comment="main plate width")
plate_h = scad.var("plate_h", 18.0, comment="main plate height")
plate_t = scad.var("plate_t", 3.0, comment="main plate thickness")
hole_r = scad.var("hole_r", 2.2, comment="through-hole radius")
rib_t = scad.var("rib_t", 1.6, comment="rib thickness")
fillet_r = scad.var("fillet_r", 0.45, comment="small edge fillet radius")
with scad.GraphSession() as session:
# Basic construction and primitive lowering:
# make_box_rsolid -> rectangle face -> four line edges -> wire -> face -> extrude
source_step("01 make_box_rsolid(expr dimensions) -> lowered profile + extrude")
plate = scad.make_box_rsolid(plate_w, plate_h, plate_t)
plate = scad.apply_tag(plate, "demo.main_plate")
# make_cylinder_rsolid is also serializable via lowering:
# circle edge -> wire -> face -> extrude
source_step("02 make_cylinder_rsolid(expr radius) -> lowered circle face + extrude")
hole = scad.make_cylinder_rsolid(
hole_r,
plate_t + 2.0,
bottom_face_center=(0.0, 0.0, -1.0),
)
drilled_plate = scad.cut_rsolid(plate, hole)
# Core wire/profile API: point, line, circle, arc, spline, helix, wire construction,
# face construction. These are kept small and placed away from the plate so
# they are easy to inspect in the graph without making the shape complicated.
source_step("03 make_point_rvertex")
marker_point = scad.make_point_rvertex(-18.0, -9.0, 6.0)
source_step("04 explicit edges + make_wire_from_edges_rwire + make_face_from_wire_rface")
e1 = scad.make_line_redge((-8.0, 0.0, plate_t), (-6.0, 0.0, plate_t))
e2 = scad.make_three_point_arc_redge(
(-6.0, 0.0, plate_t), (-5.0, 1.0, plate_t), (-4.0, 0.0, plate_t)
)
e3 = scad.make_angle_arc_redge(
(-3.0, 0.0, plate_t), 1.0, 3.14159, 0.0, normal=(0.0, 0.0, 1.0)
)
spline_fit = scad.fit_cubic_bspline_control_points(
[(-2.0, 0.0, plate_t), (-1.0, 0.8, plate_t), (0.0, 0.0, plate_t)],
tolerance=0.01,
)
e4 = scad.make_spline_redge(
control_points=spline_fit.control_points,
knots=spline_fit.unique_knots,
multiplicities=spline_fit.multiplicities,
)
# The four edges above are intentionally separate leaf examples. A valid
# `make_wire_from_edges_rwire` example follows with a closed triangle.
# A closed profile built explicitly from lines, then converted to a face.
tri_a = scad.make_line_redge((8.0, -2.0, plate_t), (11.0, -2.0, plate_t))
tri_b = scad.make_line_redge((11.0, -2.0, plate_t), (9.5, 1.0, plate_t))
tri_c = scad.make_line_redge((9.5, 1.0, plate_t), (8.0, -2.0, plate_t))
triangle_wire = scad.make_wire_from_edges_rwire([tri_a, tri_b, tri_c])
triangle_face = scad.make_face_from_wire_rface(triangle_wire)
triangle_boss = scad.extrude_rsolid(triangle_face, (0.0, 0.0, 1.0), rib_t)
# Convenience wire/face builders are included too; inside GraphSession they
# lower to the same canonical low-level edge/wire/face operations.
source_step("05 convenience wires/faces -> lowered canonical edge/wire/face nodes")
rectangle_wire = scad.make_rectangle_rwire(4.0, 2.0, center=(-13.0, 0.0, plate_t))
rectangle_face = scad.make_rectangle_rface(4.0, 2.0, center=(-13.0, 4.0, plate_t))
circle_wire = scad.make_circle_rwire((13.0, 4.0, plate_t), 1.0)
circle_face = scad.make_circle_rface((13.0, 0.0, plate_t), 1.0)
segment_wire = scad.make_segment_rwire((-13.0, -4.0, plate_t), (-9.0, -4.0, plate_t))
polyline_wire = scad.make_polyline_rwire(
[(-3.0, -5.0, plate_t), (-1.0, -4.0, plate_t), (1.0, -5.0, plate_t)]
)
arc_wire = scad.make_three_point_arc_rwire(
(3.0, -5.0, plate_t), (4.0, -4.0, plate_t), (5.0, -5.0, plate_t)
)
angle_arc_wire = scad.make_angle_arc_rwire((7.0, -5.0, plate_t), 1.0, 0.0, 1.57)
wire_spline_fit = scad.fit_cubic_bspline_control_points(
[(9.0, -5.0, plate_t), (10.0, -4.0, plate_t), (11.0, -5.0, plate_t)],
tolerance=0.01,
)
spline_wire = scad.make_spline_rwire(
control_points=wire_spline_fit.control_points,
knots=wire_spline_fit.unique_knots,
multiplicities=wire_spline_fit.multiplicities,
)
# Basic solid constructors that lower to replayable core operations.
source_step("06 make_sphere_rsolid, make_cone_rsolid")
sphere = scad.make_sphere_rsolid(1.0, center=(-7.0, 6.0, plate_t + 1.0))
cone = scad.make_cone_rsolid(
1.2,
2.0,
top_radius=0.4,
bottom_face_center=(-3.0, 6.0, plate_t),
)
# Feature operations.
source_step("07 revolve_rsolid, loft_rsolid, sweep_rsolid")
revolve_profile = scad.make_polyline_rwire(
[(0.5, 0.0, 0.0), (1.2, 0.0, 0.0), (1.0, 0.0, 1.6), (0.5, 0.0, 1.6)],
closed=True,
)
revolved_pin = scad.revolve_rsolid(
revolve_profile,
axis=(0.0, 0.0, 1.0),
angle=360.0,
origin=(0.0, 0.0, 0.0),
)
revolved_pin = scad.translate_shape(revolved_pin, (4.0, 6.0, plate_t))
loft_a = scad.make_rectangle_rwire(1.8, 1.2, center=(8.0, 6.0, plate_t))
loft_b = scad.make_rectangle_rwire(1.0, 0.8, center=(8.0, 6.0, plate_t + 2.0))
lofted_post = scad.loft_rsolid([loft_a, loft_b], ruled=True)
sweep_profile = scad.make_circle_rface((12.0, 6.0, plate_t), 0.35, normal=(1.0, 0.0, 0.0))
sweep_path = scad.make_polyline_rwire(
[(12.0, 6.0, plate_t), (14.0, 6.0, plate_t + 1.0), (15.5, 7.0, plate_t + 1.5)]
)
swept_pipe = scad.sweep_rsolid(sweep_profile, sweep_path, is_frenet=False)
# Composite operation: helical_sweep_rsolid is serialized as helix + face + sweep,
# not as a dedicated `helical_sweep` graph node.
source_step("08 helical_sweep_rsolid macro -> make_helix_redge + wire + face + sweep")
thread_profile = scad.make_rectangle_rwire(0.25, 0.18, center=(0.0, 0.0, 0.0))
helical_thread = scad.helical_sweep_rsolid(
thread_profile,
pitch=0.7,
height=2.2,
radius=0.9,
center=(13.0, -6.0, plate_t),
)
# Transforms and patterns. Pattern helpers serialize as explicit translate /
# rotate nodes instead of `linear_pattern` / `radial_pattern` macro nodes.
source_step("09 translate_shape, rotate_shape, mirror_shape")
rib = scad.make_box_rsolid(rib_t, plate_h * 0.55, plate_t * 1.4)
rib = scad.translate_shape(rib, (-plate_w / 4.0, 0.0, plate_t))
rib = scad.rotate_shape(rib, 0.0) # zero-angle shortcut, intentionally not recorded
rib_copy = scad.mirror_shape(rib, plane_origin=(0.0, 0.0, 0.0), plane_normal=(1.0, 0.0, 0.0))
source_step("10 linear_pattern_rsolidlist and radial_pattern_rsolidlist macro lowering")
lug_seed = scad.make_box_rsolid(1.2, 1.2, 1.0, bottom_face_center=(-12.0, -7.0, plate_t))
linear_lugs = scad.linear_pattern_rsolidlist(lug_seed, (1.0, 0.0, 0.0), count=3, spacing=3.0)
spoke_seed = scad.make_box_rsolid(0.8, 2.0, 0.8, bottom_face_center=(0.0, 5.2, plate_t))
radial_spokes = scad.radial_pattern_rsolidlist(
spoke_seed,
center=(0.0, 0.0, plate_t),
axis=(0.0, 0.0, 1.0),
count=4,
total_rotation_angle=360.0,
)
# Boolean operations. Boolean union must produce one connected solid, so
# this tiny demo uses overlapping boxes instead of trying to merge every
# separate showcase solid above.
source_step("11 union_rsolid, intersect_rsolid, cut_rsolid")
union_a = scad.make_box_rsolid(3.0, 2.0, 1.0, bottom_face_center=(-4.0, -7.0, 0.0))
union_b = scad.make_box_rsolid(3.0, 2.0, 1.0, bottom_face_center=(-2.5, -7.0, 0.0))
union_demo = scad.union_rsolid(union_a, union_b)
overlap_a = scad.make_box_rsolid(2.0, 2.0, 2.0, bottom_face_center=(12.0, -2.0, plate_t))
overlap_b = scad.make_box_rsolid(2.0, 2.0, 2.0, bottom_face_center=(13.0, -2.0, plate_t))
intersection_demo = scad.intersect_rsolid(overlap_a, overlap_b)
# Detail operations use QL selectors so the graph contains stable, serializable
# selection hints rather than Python object identity from source code.
source_step("12 fillet_rsolid, chamfer_rsolid, shell_rsolid with serializable selectors")
vertical_edges = Q.edges().where(Q.curve_type("line")).take(4)
final = scad.fillet_rsolid(union_demo, vertical_edges, fillet_r)
chamfer_box = scad.make_box_rsolid(3.0, 2.0, 1.0, bottom_face_center=(2.0, -7.0, 0.0))
top_outer_edges = Q.edges().order_by(Q.center_axis("z"), desc=True).take(4)
chamfer_demo = scad.chamfer_rsolid(chamfer_box, top_outer_edges, 0.15)
# Keep shell separate so the demo includes shell without making the main part
# fragile. It remains a replayable leaf in model.json.
shell_box = scad.make_box_rsolid(4.0, 3.0, 2.0, bottom_face_center=(18.0, -7.0, 0.0))
top_face = Q.faces().order_by(Q.center_axis("z"), desc=True).take(1).exactly(1)
shell_demo = scad.shell_rsolid(shell_box, top_face, 0.25)
# Export the canonical model JSON and inspect how the graph maps back to source.
model_json = scad.export_model_json(session)
payload = json.loads(model_json)
MODEL_JSON_PATH.write_text(model_json, encoding="utf-8")
# Replay from model JSON to prove that the stored operation tree is sufficient.
rebuilt = scad.replay_model_json(model_json)
scad.export_step(rebuilt, str(STEP_PATH))
ops = [node["op"] for node in payload["graph"]["nodes"]]
op_counts = Counter(ops)
expr_nodes = payload["expression_graph"]["nodes"]
nodes_with_exprs = [
node for node in payload["graph"]["nodes"] if node.get("param_exprs")
]
# Build a compact source-to-graph explanation. This file is easier to read than
# the full JSON and is meant to be opened side by side with this Python source.
summary = dedent(
f"""
# Serialization Operation Tree Example
Source file: `examples/07_serialization_operation_tree.py`
Generated model JSON: `{MODEL_JSON_PATH}`
Generated STEP replay output: `{STEP_PATH}`
## What to compare
1. Read the `SOURCE STEP` comments / print output in the Python source.
2. Open the JSON and inspect `graph.nodes[*].op`, `params`, `param_exprs`, and `inputs`.
3. Notice that convenience API calls are lowered to canonical replayable operations.
## Basic counts
- graph nodes: `{len(payload['graph']['nodes'])}`
- graph edges: `{len(payload['graph']['edges'])}`
- leaf ids: `{len(payload['leaf_ids'])}` -> `{payload['leaf_ids']}`
- expression graph nodes: `{len(expr_nodes)}`
- operation nodes with `param_exprs`: `{len(nodes_with_exprs)}`
- replayed outputs: `{len(rebuilt)}`
## Canonical operation set observed
"""
).lstrip()
for op, count in sorted(op_counts.items()):
summary += f"- `{op}`: {count}\n"
summary += dedent(
"""
## Important source-code to graph mappings
- `make_box_rsolid(...)` does **not** appear as `make_box` in model JSON.
It lowers to `make_line_redge` + `make_wire_from_edges_rwire` +
`make_face_from_wire_rface` + `make_extrude_rsolid`.
- `make_cylinder_rsolid(...)` lowers to a circle face plus `make_extrude_rsolid`.
- `make_sphere_rsolid(...)` and `make_cone_rsolid(...)` lower to revolve chains.
- `make_rectangle_rwire`, `make_circle_rwire`, `make_polyline_rwire`, and
single-arc/spline/helix wire helpers lower to edge + wire operations.
- `linear_pattern_rsolidlist(...)` lowers to explicit `make_translate_rshape`
nodes.
- `radial_pattern_rsolidlist(...)` lowers to explicit `make_rotate_rshape`
nodes.
- `helical_sweep_rsolid(...)` lowers to helix + face + `make_sweep_rsolid`;
there is no `helical_sweep` node.
- Expression values are snapshotted into `params`; the symbolic links live in
`param_exprs` and the top-level `expression_graph`.
## Nodes that reference expressions
"""
)
for node in nodes_with_exprs[:40]:
summary += f"- `{node['node_id']}` `{node['op']}` param_exprs={json.dumps(node['param_exprs'], sort_keys=True)}\n"
if len(nodes_with_exprs) > 40:
summary += f"- ... {len(nodes_with_exprs) - 40} more expression-backed nodes\n"
SUMMARY_PATH.write_text(summary, encoding="utf-8")
print("wrote", MODEL_JSON_PATH)
print("wrote", SUMMARY_PATH)
print("wrote", STEP_PATH)
print("graph_nodes", len(payload["graph"]["nodes"]))
print("expression_nodes", len(expr_nodes))
print("leaf_ids", payload["leaf_ids"])
print("replayed_outputs", len(rebuilt))
print("observed_ops", ", ".join(sorted(op_counts)))