5.6 KiB
Expressions and Replay Behavior
SimpleCADAPI stores expression-backed parameters in two places:
node.params: numeric / JSON-compatible snapshot used by simple replaynode.param_exprs: references into the top-levelexpression_graph
This lets consumers choose between:
- pure geometric replay using only the numeric snapshots
- parameter-aware import using
param_exprs + expression_graph
Source example
import simplecadapi as scad
width = scad.var("width", 24.0, unit="mm", comment="plate width", tolerance=0.1)
height = scad.var("height", 12.0, unit="mm", comment="plate height", tolerance=0.1)
thickness = scad.var("thickness", 4.0, unit="mm", comment="plate thickness", tolerance=(-0.05, 0.1))
with scad.GraphSession() as session:
plate = scad.make_box_rsolid(width, height, thickness)
rib = scad.make_box_rsolid(width / 4.0, height, thickness * 2.0)
part = scad.union_rsolid(plate, rib)
session.require_tolerance(width + height, 0.2, tolerance_unit="mm", name="plate_envelope")
model_json = scad.export_model_json(session)
Because make_box_rsolid(...) lowers to profile + extrude nodes, the expressions appear on the lowered line/profile/extrude nodes rather than on a make_box node.
Node-level JSON shape
A node with expression-backed params may look like:
{
"op": "make_extrude_rsolid",
"params": {
"direction": [0.0, 0.0, 1.0],
"distance": 4.0
},
"param_exprs": {
"distance": {"expr_id": "var_thickness"}
},
"inputs": ["node_for_profile"],
"output_count": 1
}
params.distance is the evaluated canonical snapshot. Unit-aware lengths are
stored in millimeters and angles in degrees. param_exprs.distance says the value
came from expression node var_thickness and preserves its declaration metadata.
For tuple/list params, param_exprs mirrors the shape of the parameter and uses null where no expression is present:
{
"params": {
"start": [-12.0, -6.0, 0.0],
"end": [12.0, -6.0, 0.0]
},
"param_exprs": {
"start": [{"expr_id": "expr_a"}, {"expr_id": "expr_b"}, null],
"end": [{"expr_id": "expr_c"}, {"expr_id": "expr_b"}, null]
}
}
Top-level expression graph
payload["expression_graph"] contains expression nodes for variables, constants, and arithmetic operations. The exact ids are stable within one exported payload but should not be treated as human-authored names.
Consumers that want parameterization should:
- Build an expression table from
expression_graph.nodes. - For each operation node, inspect
param_exprs. - Replace or annotate corresponding numeric
paramsentries with expression references. - Keep numeric
paramsas fallback evaluated values.
Consumers that only want geometry can ignore param_exprs and expression_graph.
Variable nodes may contain unit, tolerance, and tolerance_unit. Registered
units use string symbols; custom units use {symbol, dimension, scale_to_canonical} objects. Import reconstructs the expression graph and reruns
dimension inference rather than trusting external dimension claims.
Session/model payloads store derived-dimension requirements in tolerance_graph.
See Physical Units and Dimension Tolerance
Chains for inference, propagation, and
validation semantics.
Replay policy in current implementation
replay_model_json(model_json) currently uses the canonical low-level graph and the numeric values in node.params.
That means replay is deterministic with respect to the exported snapshot. It does not currently re-solve expressions with changed variable values.
Replay does validate stored tolerance requirements before rebuilding the nominal geometry. A failing tolerance chain blocks replay, but passing bounds do not cause replay to sample or regenerate limit geometry.
In practical terms:
width = scad.var("width", 24.0)
with scad.GraphSession() as session:
box = scad.make_box_rsolid(width, 10, 2)
payload = scad.export_model_json(session)
rebuilt = scad.replay_model_json(payload)
Replay rebuilds using width 24.0, because that is the value stored in params.
Expression metadata is still important
Even though replay uses snapshots today, param_exprs and expression_graph are important for external tools:
- FreeCAD or CAD translators can reconstruct spreadsheet bindings.
- UI tools can display which dimensions are driven by variables.
- Future parametric replay can use the same expression references.
- Diffs can distinguish numeric constants from expression-derived values.
Leaf ids and replayed outputs
The top-level leaf_ids field determines which node outputs are returned by replay:
{
"leaf_ids": ["node_final", "node_auxiliary"]
}
Replay behavior:
- Execute every graph node in topological order.
- Store each node output by
node_id. - Return outputs for
leaf_idsin order.
If an example creates many independent showcase shapes, leaf_ids may contain many node ids. This is expected: the graph is not required to have a single final part.
Unsupported / lossy expression cases
- Python callables are not serialized as expressions.
- Some discrete selector data, topology refs, and counts are intentionally treated as JSON data rather than scalar expressions.
Practical inspection snippet
import json
payload = json.loads(model_json)
for node in payload["graph"]["nodes"]:
if node.get("param_exprs"):
print(node["node_id"], node["op"])
print(" params:", node["params"])
print(" param_exprs:", node["param_exprs"])
Use this to show the source-to-JSON relationship for expression-backed dimensions.