first commit

This commit is contained in:
2026-07-22 13:48:46 +08:00
commit c87751c3dc
2820 changed files with 726976 additions and 0 deletions
@@ -0,0 +1,62 @@
# Serialization and Replay Operation Guides
This directory documents how SimpleCADAPI serializes replayable modeling operations into the canonical low-level `model.json` operation graph.
The long-form schema reference remains [`../operation_graph_json_spec.md`](../operation_graph_json_spec.md). These files are more practical, operation-by-operation guides intended for people comparing source code with exported JSON.
## Recommended workflow
```python
import json
import simplecadapi as scad
with scad.GraphSession() as session:
body = scad.make_box_rsolid(10, 6, 2)
hole = scad.make_cylinder_rsolid(1, 4, bottom_face_center=(0, 0, -1))
result = scad.cut_rsolid(body, hole)
model_json = scad.export_model_json(session)
payload = json.loads(model_json)
rebuilt = scad.replay_model_json(model_json)
```
Inspect these fields:
- `payload["graph"]["nodes"]`: canonical operation nodes in topological order.
- `node["op"]`: stable replay operation name.
- `node["params"]`: numeric / JSON-compatible parameter snapshot.
- `node["param_exprs"]`: optional expression links into `expression_graph`.
- `node["inputs"]`: upstream node ids used by replay.
- `payload["leaf_ids"]`: explicit final result node ids.
- `payload["expression_graph"]`: expression DAG used by expression-backed parameters.
## Important rule: source API is not always graph API
Many user-facing functions are convenience APIs. During an active `GraphSession`, they lower to canonical low-level nodes:
| Source call | Serialized graph result |
| --- | --- |
| `make_box_rsolid(...)` | rectangle profile + `make_extrude_rsolid` |
| `make_cylinder_rsolid(...)` | circle face + `make_extrude_rsolid` |
| `make_sphere_rsolid(...)` | profile + `make_revolve_rsolid` |
| `make_cone_rsolid(...)` | profile + `make_revolve_rsolid` |
| `make_rectangle_rwire(...)` | line edges + `make_wire_from_edges_rwire` |
| `make_circle_rface(...)` | circle edge + wire + face |
| `make_polyline_rwire(...)` | line edges + wire |
| `linear_pattern_rsolidlist(...)` | explicit `make_translate_rshape` nodes |
| `radial_pattern_rsolidlist(...)` | explicit `make_rotate_rshape` nodes |
| `helical_sweep_rsolid(...)` | helix wire + profile face + `make_sweep_rsolid` |
## Guides
- [Primitive and profile operations](primitives-and-profiles.md)
- [Features, booleans, transforms, patterns, and selectors](features-booleans-transforms.md)
- [Expressions and replay behavior](expressions-and-replay.md)
## Example
See [`../../../examples/07_serialization_operation_tree.py`](../../../examples/07_serialization_operation_tree.py). It intentionally exercises every canonical core operation and writes:
- `examples/out/serialization_operation_tree.model.json`
- `examples/out/serialization_operation_tree.summary.md`
- `examples/out/serialization_operation_tree.step`
@@ -0,0 +1,145 @@
# Expressions and Replay Behavior
SimpleCADAPI stores expression-backed parameters in two places:
1. `node.params`: numeric / JSON-compatible snapshot used by simple replay
2. `node.param_exprs`: references into the top-level `expression_graph`
This lets consumers choose between:
- pure geometric replay using only the numeric snapshots
- parameter-aware import using `param_exprs + expression_graph`
## Source example
```python
import simplecadapi as scad
width = scad.var("width", 24.0, comment="plate width")
height = scad.var("height", 12.0, comment="plate height")
thickness = scad.var("thickness", 4.0, comment="plate thickness")
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)
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:
```json
{
"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 snapshot. `param_exprs.distance` says the value came from expression node `var_thickness`.
For tuple/list params, `param_exprs` mirrors the shape of the parameter and uses `null` where no expression is present:
```json
{
"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:
1. Build an expression table from `expression_graph.nodes`.
2. For each operation node, inspect `param_exprs`.
3. Replace or annotate corresponding numeric `params` entries with expression references.
4. Keep numeric `params` as fallback evaluated values.
Consumers that only want geometry can ignore `param_exprs` and `expression_graph`.
## 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.
In practical terms:
```python
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:
```json
{
"leaf_ids": ["node_final", "node_auxiliary"]
}
```
Replay behavior:
1. Execute every graph node in topological order.
2. Store each node output by `node_id`.
3. Return outputs for `leaf_ids` in 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
```python
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.
@@ -0,0 +1,468 @@
# Features, Booleans, Transforms, Patterns, and Selection Serialization
This guide covers replayable feature operations, boolean operations, transforms, macro pattern lowering, and detail-feature selectors.
## Extrude
Source:
```python
profile = scad.make_rectangle_rface(4.0, 2.0)
solid = scad.extrude_rsolid(profile, (0, 0, 1), 3.0)
```
Serialized node:
```json
{
"op": "make_extrude_rsolid",
"params": {
"direction": [0.0, 0.0, 1.0],
"distance": 3.0
},
"inputs": ["node_for_profile"],
"output_count": 1
}
```
Replay effect:
1. Replay the input profile node, which must output a `Wire` or `Face`.
2. Call `extrude_rsolid(profile, direction, distance)`.
## Revolve
Source:
```python
profile = scad.make_polyline_rwire(
[(0.5, 0, 0), (1.2, 0, 0), (1.0, 0, 1.6), (0.5, 0, 1.6)],
closed=True,
)
solid = scad.revolve_rsolid(
profile,
axis=(0, 0, 1),
angle=360.0,
origin=(0, 0, 0),
)
```
Serialized node:
```json
{
"op": "make_revolve_rsolid",
"params": {
"axis": [0.0, 0.0, 1.0],
"angle": 360.0,
"origin": [0.0, 0.0, 0.0]
},
"inputs": ["node_for_profile"],
"output_count": 1
}
```
Replay effect: replays the profile and calls `revolve_rsolid(profile, axis, angle, origin)`.
## Loft
Source:
```python
a = scad.make_rectangle_rwire(2.0, 1.0, center=(0, 0, 0))
b = scad.make_rectangle_rwire(1.0, 0.5, center=(0, 0, 3))
solid = scad.loft_rsolid([a, b], ruled=True)
```
Serialized node:
```json
{
"op": "make_loft_rsolid",
"params": {
"profile_count": 2,
"ruled": true
},
"inputs": ["node_for_a", "node_for_b"],
"output_count": 1
}
```
Replay effect:
1. Replay all profile input nodes.
2. Call `loft_rsolid(profiles, ruled=...)`.
Profile geometry is recovered from `inputs`; only count/options are stored in `params`.
## Sweep
Source:
```python
profile = scad.make_circle_rface((0, 0, 0), 0.3, normal=(1, 0, 0))
path = scad.make_polyline_rwire([(0, 0, 0), (2, 0, 1), (4, 1, 1)])
solid = scad.sweep_rsolid(profile, path, is_frenet=False)
```
Serialized node:
```json
{
"op": "make_sweep_rsolid",
"params": {"is_frenet": false},
"inputs": ["node_for_profile_face", "node_for_path_wire"],
"output_count": 1
}
```
Replay effect:
1. Replay profile face from input 0.
2. Replay path wire from input 1.
3. Call `sweep_rsolid(profile, path, is_frenet=...)`.
## Helical sweep macro lowering
Source:
```python
profile = scad.make_rectangle_rwire(0.25, 0.18)
solid = scad.helical_sweep_rsolid(
profile,
pitch=0.7,
height=2.2,
radius=0.9,
)
```
Lowered serialized graph:
```text
profile wire
-> make_face_from_wire_rface
make_helix_redge
-> make_wire_from_edges_rwire
profile face + helix wire
-> make_sweep_rsolid(is_frenet=true)
```
There is no canonical `helical_sweep` node. Replay rebuilds the helix and sweeps along it.
## Translate
Source:
```python
moved = scad.translate_shape(shape, (1.0, 2.0, 0.0))
```
Serialized node:
```json
{
"op": "make_translate_rshape",
"params": {"vector": [1.0, 2.0, 0.0]},
"inputs": ["node_for_shape"],
"output_count": 1
}
```
Replay effect: replays input shape and calls `translate_shape(shape, vector)`.
## Rotate
Source:
```python
rotated = scad.rotate_shape(shape, 90.0, axis=(0, 0, 1), origin=(0, 0, 0))
```
Serialized node:
```json
{
"op": "make_rotate_rshape",
"params": {
"angle": 90.0,
"axis": [0.0, 0.0, 1.0],
"origin": [0.0, 0.0, 0.0]
},
"inputs": ["node_for_shape"],
"output_count": 1
}
```
Replay effect: replays input shape and calls `rotate_shape(shape, angle, axis, origin)`.
Note: `rotate_shape(shape, 0.0)` returns the original shape and does not record a node.
## Mirror
Source:
```python
mirrored = scad.mirror_shape(
shape,
plane_origin=(0, 0, 0),
plane_normal=(1, 0, 0),
)
```
Serialized node:
```json
{
"op": "make_mirror_rshape",
"params": {
"plane_origin": [0.0, 0.0, 0.0],
"plane_normal": [1.0, 0.0, 0.0]
},
"inputs": ["node_for_shape"],
"output_count": 1
}
```
Replay effect: replays input shape and calls `mirror_shape(shape, plane_origin, plane_normal)`.
## Boolean union
Source:
```python
a = scad.make_box_rsolid(3, 2, 1)
b = scad.make_box_rsolid(3, 2, 1, bottom_face_center=(1.5, 0, 0))
result = scad.union_rsolid(a, b)
```
Serialized node:
```json
{
"op": "make_union_rsolid",
"params": {
"input_count": 2,
"clean": true,
"glue": true,
"tol": 1e-7
},
"inputs": ["node_for_a", "node_for_b"],
"output_count": 1
}
```
Replay effect:
1. Replay all input solids.
2. Call `union_rsolid(all_solids)`.
Important: `union_rsolid` expects one connected solid result. If inputs remain disconnected, runtime and replay both raise an error instead of returning a compound.
## Boolean cut
Source:
```python
body = scad.make_box_rsolid(4, 4, 2)
tool = scad.make_cylinder_rsolid(0.8, 4, bottom_face_center=(0, 0, -1))
result = scad.cut_rsolid(body, tool)
```
Serialized node:
```json
{
"op": "make_cut_rsolid",
"params": {
"tool_count": 1,
"input_count": 2
},
"inputs": ["node_for_body", "node_for_tool"],
"output_count": 1
}
```
Replay effect:
1. Replay first input as the body.
2. Replay remaining inputs as tools.
3. Call `cut_rsolid(body, tools)`.
## Boolean intersection
Source:
```python
a = scad.make_box_rsolid(2, 2, 2)
b = scad.make_box_rsolid(2, 2, 2, bottom_face_center=(1, 0, 0))
result = scad.intersect_rsolid(a, b)
```
Serialized node:
```json
{
"op": "make_intersect_rsolid",
"params": {
"input_count": 2
},
"inputs": ["node_for_a", "node_for_b"],
"output_count": 1
}
```
Replay effect: replays inputs and calls `intersect_rsolid(first, rest)`.
## Fillet
Source with serializable QL selector:
```python
from simplecadapi import ql as Q
selector = Q.edges().where(Q.curve_type("line")).take(4)
result = scad.fillet_rsolid(solid, selector, 0.25)
```
Serialized node:
```json
{
"op": "make_fillet_rsolid",
"params": {
"radius": 0.25,
"edge_count": 4,
"selected_edges": [
{
"graph_id": "graph_xxx",
"node_id": "node_xxx",
"output_slot": 0,
"kind": "EDGE",
"topo_id": "edge_...",
"selector_hint": {...}
}
],
"selected_edge_node_ids": ["node_select_edge_0", "node_select_edge_1", "node_select_edge_2", "node_select_edge_3"]
},
"inputs": ["node_for_solid", "node_select_edge_0", "node_select_edge_1", "node_select_edge_2", "node_select_edge_3"],
"output_count": 1
}
```
Each QL-selected or indexed getter-selected edge is serialized as its own `make_select_redge` node whose `geo_selector` is fixed to the runtime-selected edge geometry. `geo_selector` does not contain tags or source indices; it uses geometry facts such as `geom_type`, `length`, `center`, endpoints, bbox, and `metadata_geo`.
Replay edge resolution order:
1. Geo select nodes from `selected_edge_node_ids`
2. Legacy/fallback `selection_query`, when present
3. Explicit topo refs in `selected_edges`
4. Legacy indices in `selected_edge_indices`, when select nodes are unavailable
5. `selector_hint` fallback
Then replay calls `fillet_rsolid(solid, resolved_edges, radius)`.
## Chamfer
Source:
```python
selector = Q.edges().order_by(Q.center_axis("z"), desc=True).take(4)
result = scad.chamfer_rsolid(solid, selector, 0.15)
```
Serialized node shape is the same as fillet, except:
```json
{
"op": "make_chamfer_rsolid",
"params": {
"distance": 0.15,
"edge_count": 4,
"selected_edges": [...],
"selected_edge_node_ids": [...]
},
"inputs": ["node_for_solid", "node_select_edge_0", "..."]
}
```
Replay resolves edges using the same order and calls `chamfer_rsolid(solid, resolved_edges, distance)`.
## Shell
Source:
```python
selector = Q.faces().order_by(Q.center_axis("z"), desc=True).take(1).exactly(1)
result = scad.shell_rsolid(solid, selector, 0.25)
```
Serialized node:
```json
{
"op": "make_shell_rsolid",
"params": {
"thickness": 0.25,
"removed_face_count": 1,
"selected_faces": [...],
"selected_face_node_ids": ["node_select_face_0"]
},
"inputs": ["node_for_solid", "node_select_face_0"],
"output_count": 1
}
```
The face select node uses `make_select_rface` with a tag-free `geo_selector` fixed to the runtime-selected face geometry.
Replay face resolution order:
1. Geo select nodes from `selected_face_node_ids`
2. Legacy/fallback `selection_query`, when present
3. Explicit topo refs in `selected_faces`
4. Legacy indices in `selected_face_indices`, when select nodes are unavailable
5. `selector_hint` fallback
Then replay calls `shell_rsolid(solid, resolved_faces, thickness)`.
## Linear pattern macro lowering
Source:
```python
copies = scad.linear_pattern_rsolidlist(seed, (1, 0, 0), count=3, spacing=2.0)
```
When recording is active, this does not emit a `linear_pattern` node. It emits one translate node per generated copy:
```text
seed -> make_translate_rshape(vector=[0, 0, 0])
seed -> make_translate_rshape(vector=[2, 0, 0])
seed -> make_translate_rshape(vector=[4, 0, 0])
```
Replay effect: each generated copy is replayed as an ordinary translated shape.
## Radial pattern macro lowering
Source:
```python
copies = scad.radial_pattern_rsolidlist(
seed,
center=(0, 0, 0),
axis=(0, 0, 1),
count=4,
total_rotation_angle=360.0,
)
```
When recording is active, this emits explicit rotate nodes for non-zero rotations. The zero-angle first copy is the original shape and does not create a rotate node.
```text
seed retained as first copy
seed -> make_rotate_rshape(angle=90)
seed -> make_rotate_rshape(angle=180)
seed -> make_rotate_rshape(angle=270)
```
Replay effect: copies are ordinary rotate operations, not a pattern macro.
@@ -0,0 +1,465 @@
# Primitive and Profile Operation Serialization
This guide covers replayable primitive/profile operations in the canonical operation graph.
All examples assume:
```python
import json
import simplecadapi as scad
with scad.GraphSession() as session:
...
payload = json.loads(scad.export_model_json(session))
```
In exported JSON, each operation appears in `payload["graph"]["nodes"]` as:
```json
{
"node_id": "node_xxxxxxxx",
"op": "make_line_redge",
"params": {...},
"inputs": [],
"output_count": 1,
"tags": [...],
"display": {...},
"param_exprs": {...},
"context": {...}
}
```
`display`, `tags`, `context`, `semantic_delta`, and `topo_delta` are useful metadata. Replay primarily depends on `op`, `params`, and `inputs`.
## Point
Source:
```python
p = scad.make_point_rvertex(1.0, 2.0, 3.0)
```
Serialized node:
```json
{
"op": "make_point_rvertex",
"params": {"x": 1.0, "y": 2.0, "z": 3.0},
"inputs": [],
"output_count": 1
}
```
Replay effect: calls `make_point_rvertex(x, y, z)` and returns a `Vertex`.
## Line edge
Source:
```python
edge = scad.make_line_redge((0, 0, 0), (5, 0, 0))
```
Serialized node:
```json
{
"op": "make_line_redge",
"params": {"start": [0.0, 0.0, 0.0], "end": [5.0, 0.0, 0.0]},
"inputs": [],
"output_count": 1
}
```
Replay effect: calls `make_line_redge(start, end)` and returns an `Edge`.
### Segment aliases
`make_segment_redge(start, end)` is an alias of `make_line_redge(...)` and records the same `make_line_redge` node.
`make_segment_rwire(start, end)` lowers to:
1. `make_line_redge`
2. `make_wire_from_edges_rwire`
There is no canonical `make_segment_wire` node in model JSON.
## Circle edge, wire, and face
Source edge:
```python
edge = scad.make_circle_redge((0, 0, 0), 2.0, normal=(0, 0, 1))
```
Serialized node:
```json
{
"op": "make_circle_redge",
"params": {
"center": [0.0, 0.0, 0.0],
"radius": 2.0,
"normal": [0.0, 0.0, 1.0]
},
"inputs": [],
"output_count": 1
}
```
Replay effect: calls `make_circle_redge(center, radius, normal)`.
Source wire:
```python
wire = scad.make_circle_rwire((0, 0, 0), 2.0)
```
Lowered serialized graph:
```text
make_circle_redge -> make_wire_from_edges_rwire
```
Source face:
```python
face = scad.make_circle_rface((0, 0, 0), 2.0)
```
Lowered serialized graph:
```text
make_circle_redge -> make_wire_from_edges_rwire -> make_face_from_wire_rface
```
There is no canonical `make_circle_wire` or `make_circle_face` node.
## Three-point arc edge and wire
Source edge:
```python
arc = scad.make_three_point_arc_redge(
(0, 0, 0),
(1, 1, 0),
(2, 0, 0),
)
```
Serialized node:
```json
{
"op": "make_three_point_arc_redge",
"params": {
"start": [0.0, 0.0, 0.0],
"middle": [1.0, 1.0, 0.0],
"end": [2.0, 0.0, 0.0]
},
"inputs": [],
"output_count": 1
}
```
Replay effect: calls `make_three_point_arc_redge(start, middle, end)`.
`make_three_point_arc_rwire(...)` lowers to:
```text
make_three_point_arc_redge -> make_wire_from_edges_rwire
```
## Angle arc edge and wire
Source edge:
```python
arc = scad.make_angle_arc_redge(
center=(0, 0, 0),
radius=1.0,
start_angle=0.0,
end_angle=1.57,
normal=(0, 0, 1),
)
```
Serialized node:
```json
{
"op": "make_angle_arc_redge",
"params": {
"center": [0.0, 0.0, 0.0],
"radius": 1.0,
"start_angle": 0.0,
"end_angle": 1.57,
"normal": [0.0, 0.0, 1.0]
},
"inputs": [],
"output_count": 1
}
```
Replay effect: calls `make_angle_arc_redge(center, radius, start_angle, end_angle, normal)`.
`make_angle_arc_rwire(...)` lowers to:
```text
make_angle_arc_redge -> make_wire_from_edges_rwire
```
## Spline edge and wire
Source edge:
```python
fit = scad.fit_cubic_bspline_control_points(
[(0, 0, 0), (1, 1, 0), (2, 0, 0)],
tolerance=0.01,
)
spline = scad.make_spline_redge(
control_points=fit.control_points,
knots=fit.unique_knots,
multiplicities=fit.multiplicities,
)
```
Serialized node:
```json
{
"op": "make_spline_redge",
"params": {
"control_points": [[0.0, 0.0, 0.0], [0.6, 1.0, 0.0], [1.4, 1.0, 0.0], [2.0, 0.0, 0.0]],
"degree": 3,
"knots": [0.0, 1.0],
"multiplicities": [4, 4],
"weights": null,
"periodic": false
},
"inputs": [],
"output_count": 1
}
```
Replay effect: calls `make_spline_redge(control_points=..., degree=..., knots=..., multiplicities=..., weights=..., periodic=...)`.
`make_spline_rwire(control_points=..., ...)` lowers to:
```text
make_spline_redge -> make_wire_from_edges_rwire
```
`make_spline_redge` now stores an exact B-spline definition. It does not accept sampled/interpolated curve points directly; use `fit_cubic_bspline_control_points(...)` first when human/LLM-authored code starts from samples.
## Helix edge and wire
Source edge:
```python
helix = scad.make_helix_redge(
pitch=0.7,
height=2.2,
radius=0.9,
center=(0, 0, 0),
dir=(0, 0, 1),
)
```
Serialized node:
```json
{
"op": "make_helix_redge",
"params": {
"pitch": 0.7,
"height": 2.2,
"radius": 0.9,
"center": [0.0, 0.0, 0.0],
"dir": [0.0, 0.0, 1.0]
},
"inputs": [],
"output_count": 1
}
```
Replay effect: calls `make_helix_redge(pitch, height, radius, center, dir)`.
`make_helix_rwire(...)` lowers to:
```text
make_helix_redge -> make_wire_from_edges_rwire
```
## Wire from edges
Source:
```python
a = scad.make_line_redge((0, 0, 0), (1, 0, 0))
b = scad.make_line_redge((1, 0, 0), (1, 1, 0))
wire = scad.make_wire_from_edges_rwire([a, b])
```
Serialized node:
```json
{
"op": "make_wire_from_edges_rwire",
"params": {"edge_count": 2},
"inputs": ["node_for_a", "node_for_b"],
"output_count": 1
}
```
Replay effect:
1. Replay each input edge node.
2. Collect input edge outputs in input order.
3. Call `make_wire_from_edges_rwire(edges)`.
The actual edge geometry is not duplicated inside this node; it is recovered through `inputs`.
## Face from wire
Source:
```python
face = scad.make_face_from_wire_rface(wire, normal=(0, 0, 1))
```
Serialized node:
```json
{
"op": "make_face_from_wire_rface",
"params": {"normal": [0.0, 0.0, 1.0]},
"inputs": ["node_for_wire"],
"output_count": 1
}
```
Replay effect:
1. Replay the input wire node.
2. Call `make_face_from_wire_rface(wire, normal=...)`.
## Rectangle wire and face lowering
Source:
```python
wire = scad.make_rectangle_rwire(4.0, 2.0, center=(0, 0, 0))
face = scad.make_rectangle_rface(4.0, 2.0, center=(0, 0, 0))
```
Lowered serialized graph:
```text
make_rectangle_rwire:
make_line_redge x4 -> make_wire_from_edges_rwire
make_rectangle_rface:
make_line_redge x4 -> make_wire_from_edges_rwire -> make_face_from_wire_rface
```
There is no canonical `make_rectangle_wire` or `make_rectangle_face` node.
## Polyline wire lowering
Source:
```python
wire = scad.make_polyline_rwire(
[(0, 0, 0), (1, 0, 0), (1, 1, 0)],
closed=False,
)
```
Lowered serialized graph:
```text
make_line_redge x(number_of_segments) -> make_wire_from_edges_rwire
```
If `closed=True`, one additional closing line edge is emitted.
There is no canonical `make_polyline_wire` node.
## Box, cylinder, sphere, and cone lowering
These user-facing primitive solids are intentionally lowered to canonical profile/feature operations.
### Box
Source:
```python
box = scad.make_box_rsolid(4.0, 2.0, 1.0)
```
Lowered serialized graph:
```text
make_line_redge x4
-> make_wire_from_edges_rwire
-> make_face_from_wire_rface
-> make_extrude_rsolid
```
Replay effect: rebuilds the rectangular face, then extrudes it.
There is no canonical `make_box` node.
### Cylinder
Source:
```python
cyl = scad.make_cylinder_rsolid(1.0, 3.0)
```
Lowered serialized graph:
```text
make_circle_redge
-> make_wire_from_edges_rwire
-> make_face_from_wire_rface
-> make_extrude_rsolid
```
There is no canonical `make_cylinder` node.
### Sphere
Source:
```python
sphere = scad.make_sphere_rsolid(1.5, center=(0, 0, 0))
```
Lowered serialized graph:
```text
profile edges/wire/face -> make_revolve_rsolid
```
There is no canonical `make_sphere` node.
### Cone / truncated cone
Source:
```python
cone = scad.make_cone_rsolid(1.2, 2.0, top_radius=0.4)
```
Lowered serialized graph:
```text
profile edges/wire/face -> make_revolve_rsolid
```
There is no canonical `make_cone` node.
@@ -0,0 +1,13 @@
# Scalar Fields / SDF Status
SDF and scalar field modeling are temporarily removed from the supported SimpleCADAPI surface.
Current contract:
- `simplecadapi.field` is not exported.
- `make_field_surface_rsolid` is not exported.
- `*_rscalarfield` APIs are not generated in public API docs.
- `make_field_surface_rsolid` is not a canonical graph op.
- Model JSON replay does not rebuild scalar field surfaces.
Historical payloads or examples that rely on scalar field trees should be treated as unsupported until a new SDF contract is designed.