feat: integrate SimpleCADAPI 2.0.2 CAD workflows

This commit is contained in:
Jerry
2026-08-03 11:17:05 +08:00
parent b5738e9109
commit c3a0f269b7
481 changed files with 110229 additions and 12826 deletions
@@ -6,35 +6,40 @@
- Use the standard parts library first when a requested standard component is available and does not need complex custom geometry changes.
- Start from profiles and reference geometry, then create solids with features such as extrude, revolve, loft, and sweep.
- Use booleans and detail features after the base form is clear: cut openings, union intended merged bodies, then apply fillets, chamfers, or shell operations.
- Use `GraphSession` whenever the result should be replayable, inspectable, serialized, or translated.
- Use `@scad.model` for a top-level replayable entry point. It owns one `GraphSession` and returns a `ModelResult`; use `@scad.requires_session` for child builders.
- Use QL for grounding and selection. Query the facts you need, such as face normals, centers, areas, edge lengths, curve types, and tags.
- Use indexed child-geometry getters such as `get_edges(index)` and `get_faces(index)` when an indexed topology pick is intentional.
- Use semantic tags for design intent and anchors. Keep numeric measurements and geometry facts in metadata or model JSON payloads.
- Treat `export_model_json()` as the interchange boundary for replay and CAD translation.
- Treat `ModelResult.model_json` as the interchange boundary for new model entry points. Use `export_model_json(session=...)` for lower-level direct sessions.
- Declare directly controlled dimensions with `var(..., unit=..., tolerance=..., tolerance_unit=...)`, and attach derived requirements with `GraphSession.require_tolerance(...)` when manufacturing variation matters.
- Validate incrementally: after each major step, print small QL-derived facts such as selected face count, top face center, edge count, volume, or replay result count.
## 1) Capture a replayable modeling flow
```python
from simplecadapi import GraphSession, export_model_json
import simplecadapi as scad
with GraphSession() as session:
...
@scad.model(graph_id="bracket")
def build_bracket():
body = scad.make_box_rsolid(width=20.0, height=10.0, depth=3.0)
scad.capture_result(value=body)
return body
payload = export_model_json(session=session)
result = build_bracket()
payload = result.model_json
rebuilt = result.replay()
```
## 2) Import and use in Python
```python
import simplecadapi as scad
from simplecadapi import GraphSession, export_model_json
```
## 3) Keep replay payloads as the interchange boundary
- Prefer `export_model_json()` output instead of hand-written payloads.
- Use `replay_model_json()` when you need deterministic reconstruction.
- Use `ModelResult.replay()` for a model invocation, or `replay_model_json(json_str=...)` when consuming standalone model JSON.
- Use `import_model_json()` when consuming previously exported payloads.
## 4) Use standard parts when they fit
@@ -49,10 +54,10 @@ gear = scad.std.gear.make_spur_gear_rsolid(
)
rack = scad.std.gear.make_spur_rack_rsolid(module=1.5, n_teeth=18)
bearing = scad.std.bearing.make_ball_bearing_rassembly(
8.0,
22.0,
7.0,
3.5,
bore_diameter=8.0,
outer_diameter=22.0,
bearing_width=7.0,
ball_diameter=3.5,
)
```
@@ -66,25 +71,30 @@ bearing = scad.std.bearing.make_ball_bearing_rassembly(
import simplecadapi as scad
from simplecadapi import ql
with scad.GraphSession() as session:
@scad.model(graph_id="swept_profile")
def build_model():
profile = scad.make_circle_rface(center=(0, 0, 0), radius=1.0)
body = scad.extrude_rsolid(
profile=profile,
direction=(0, 0, 1),
distance=4.0,
end_face_tag="role.sweep_profile",
result_tag="part.body",
)
end_face = (
ql.faces()
.where(ql.tag("face.extrusion.end"))
.where(ql.output_role(role_name="extrusion.end"))
.exactly(1)
.resolve(body)[0]
)
print("end face center", end_face.get_center())
path = scad.make_segment_rwire(start=(0, 0, 4), end=(0, 0, 8))
swept = scad.sweep_rsolid(profile=end_face, path=path)
scad.capture_result(value=swept)
return swept
payload = scad.export_model_json(session=session)
rebuilt = scad.replay_model_json(json_str=payload)
result = build_model()
rebuilt = result.replay()
print("rebuilt", len(rebuilt))
```
@@ -92,14 +102,129 @@ print("rebuilt", len(rebuilt))
- Prefer QL selectors for semantic/geometric feature input selection.
- Use `get_edges(index)`, `get_faces(index)`, `get_wires(index)`, or `get_vertices(index)` for intentional indexed picks in examples.
- Attach semantic tags with `apply_tag(shape=..., tag=...)` and inspect with `list_tags(shape=...)`.
- Use tags for intent, roles, anchors, groups, and topology names.
- Store dimensions, positions, measured geometry, and descriptive payloads in metadata or model JSON, not in tags.
- Attach local semantic tags with `apply_tag(shape=..., tag=...)`. Use `apply_tag_rselection(...)` for selector targets, explicit downward inheritance, or immutable semantic branches.
- Inspect with `list_tags(shape=..., scope=...)` and `explain_tag(...)`; `effective` excludes `lineage`.
- Use tags for topology identity, intent, roles, anchors, and groups.
- Store dimensions, positions, measured geometry, operation events, source roles, and descriptive payloads in metadata or model JSON, not in tags.
- Keep QL result prints concise: selected count, centers, normals, areas, lengths, or tags.
## 7) Boolean and body discipline
## 7) Kernel-proven feature output roles
- Feature output roles are typed tracking evidence, not user tag strings. Query them with `ql.output_role(role_name=...)`.
- The feature APIs expose one named tag argument per output role. Every requested role is strict: `one` roles require exactly one proven target, and `many` roles require at least one and tag the full proven set.
- There is no generic role-to-tag mapping. Unknown or unavailable roles and non-normalized tags fail before returning geometry.
- `result_tag` targets the one result Solid. Role tags target Faces except for `shell.wall`, which targets Edges.
- `tag_prefix` creates topology-identity tags such as `body.face.side.right`; role-specific tag parameters and `result_tag` create tags such as `role.mounting_surface` and `part.body`. They share one tag API, while their evidence and projection policies differ. Tag text alone never establishes topology identity.
- Box exposes the exact `box.bottom`, `box.top`, `box.front`, `box.back`, `box.left`, and `box.right` Face roles. It does not expose Edge roles; use tagged incident Faces with QL `incident_to(...)` or `shared_boundary(...)` for exact Box Edge selection.
- Cone exposes `cone.start`, optional `cone.end`, and `cone.side` Face roles plus `cone.start_boundary`, `cone.end_boundary`, and `cone.seam` Edge roles. A pointed cone has no end cap Face, but its end boundary is retained as the kernel's degenerate apex Edge.
- A 360-degree revolve has no distinct start/end caps. Requesting those tags fails rather than inventing cap roles.
- Shell role availability follows actual OCC evidence. For example, an operation may prove offset faces, closing descendants, and wall edges without proving a `shell.body_face` set.
- Sweep currently rejects a profile with inner wires rather than silently sweeping only its outer wire.
```python
body = scad.extrude_rsolid(
profile=profile,
direction=(0, 0, 1),
distance=10.0,
start_face_tag="anchor.base",
end_face_tag="role.mounting_surface",
side_faces_tag="group.outer_walls",
result_tag="part.body",
)
mounting_face = (
ql.faces()
.where(ql.output_role(role_name="extrusion.end"))
.exactly(1)
.resolve(body)[0]
)
print(scad.list_tags(shape=mounting_face, scope="local"))
```
In a `GraphSession`, each requested assignment becomes a canonical
`apply_tag_rselection` semantic node. The feature node remains geometry-only, and
replay validates both the recomputed role set and the exact selected refs.
## 8) Replay-safe source projection
When a feature supports source projection, keep the returned semantic view in the
feature's input chain:
```python
profile = scad.make_rectangle_rface(width=5.0, height=3.0)
source_edge = profile.get_edges(0)
profile = scad.apply_tag_rselection(
scope=profile,
targets=[source_edge],
tag="role.source_edge",
)
source_edge = scad.select_edges_by_tag(
shape=profile,
tag="role.source_edge",
scope="local",
)[0]
source_binding_id = scad.explain_tag(
shape=source_edge,
tag="role.source_edge",
scope="local",
)[0]["binding_id"]
body = scad.extrude_rsolid(
profile=profile,
direction=(0, 0, 1),
distance=2.0,
)
projected_face = (
ql.faces()
.where(ql.source_binding(binding_id=source_binding_id))
.exactly(1)
.resolve(body)[0]
)
print(scad.list_tags(shape=projected_face, scope="local"))
```
The projected binding preserves the exact source binding ID, source topology ID,
target topology ID, operation, role, and evidence method. `ql.source_binding(...)`
and `ql.source_topology(...)` inspect this local evidence. They do not search tag
text or infer ancestry from geometry. Calling `apply_tag_rselection(...)` but then
passing the original profile to the feature creates a detached semantic branch and
does not authorize hidden coupling.
## 9) Constrained Sketch topology tags
- Give every profile entity a stable explicit `entity_id` when constructing a constrained Sketch.
- Promote through `make_wire_from_sketch_rwire(...)` or `make_face_from_sketch_rface(...)`; the promotion map is the exact entity-to-Edge correspondence.
- Inspect canonical topology-identity tags with `ql.tag("sketch.rect.entity.right")` or `explain_tag(...)`; the evidence contains `topology_name` and `SketchPromotionMap`.
- A promoted profile receives `sketch.<sketch-name>.profile.<profile-id>` and each boundary Edge receives `sketch.<sketch-name>.entity.<entity-id>`.
- If the generated Edge count differs from the promotion map, fail rather than guessing correspondence.
- A feature can project a Sketch entity topology tag only when kernel history proves one source Edge to one result Face.
## 10) Boolean and body discipline
- Use `union_rsolid(...)` when multiple solids should become one integrated body.
- Ensure bodies that should union into one solid have real geometric overlap or embedding.
- Use `cut_rsolid(...)` for subtractive features and `intersect_rsolid(...)` for common-volume workflows.
- Validate body count and volume after major boolean operations.
## 11) Dimension tolerance chains
```python
width = scad.var("width", 10.0, unit="mm", tolerance=0.1)
gap = scad.var("gap", 0.5, unit="mm", tolerance=(-0.05, 0.1))
overall = width + gap
with scad.GraphSession() as session:
body = scad.make_box_rsolid(width, 2.0, 1.0)
session.require_tolerance(overall, (-0.15, 0.2), tolerance_unit="mm", name="overall")
report = session.validate_tolerances(raise_on_failure=True)
```
- Use `worst_case` for a guaranteed conservative envelope.
- Use `rss` only when distinct source dimensions can be treated as independent.
- Every source variable in a chain must have a declared tolerance.
- Unit-aware expressions infer dimensions automatically and cannot mix legacy
variables without units.
- Length and angle use canonical `mm` and `deg` values in CAD operations.
- Read `references/docs/core/physical-units.md` for unit and inference rules.
- Read `references/docs/core/dimension-tolerance-chains.md` for complete rules.
@@ -1,8 +1,8 @@
# SDK Overview
- Project: `simplecadapi`
- Version: `2.0.1b1`
- Package distribution: `simplecadapi==2.0.1b1`
- Version: `2.0.2`
- Package distribution: `simplecadapi==2.0.2`
## What this skill bundles
@@ -20,11 +20,13 @@
## Main SDK surfaces
- Geometry and modeling operations in `docs/api/`.
- Standard parts library in `docs/stdlib/`, including `scad.std.gear` gear, ring gear, rack, and cycloidal disc factories plus `scad.std.bearing` bearing assembly factories.
- Standard parts library in `docs/stdlib/`, including `scad.std.gear`, `scad.std.bearing`, `scad.std.chain`, and `scad.std.fastener` factories.
- Core shape/type semantics in `docs/core/`.
- Graph/model serialization and replay APIs.
- Expression, parameter, and semantic reference types.
- Functional tagging with `apply_tag(shape=..., tag=...)`, `list_tags(shape=...)`, and QL tag predicates.
- Source-preserving tagging with `apply_tag(...)`, `apply_tag_rselection(...)`, scoped `list_tags(...)`/`explain_tag(...)`, and scoped QL tag predicates.
- Kernel-proven primitive and feature output roles with strict role-based output tag assignment for box, cylinder, extrude, revolve, fillet, chamfer, shell, loft, sweep, and twisted sweep.
- Serializable QL predicates for `output_role`, projected `source_binding`, and projected `source_topology` evidence.
## Preferred replayable workflow
@@ -1,14 +1,14 @@
# SDK Package Summary
- Project: `simplecadapi`
- Version: `2.0.1b1`
- Version: `2.0.2`
- Summary: A simplified OCP-native CAD modeling Python API
## Scope
- OCP-native public CAD Python SDK for geometry and replayable modeling.
- Includes generated API, standard-library, and core type references under `references/docs/`.
- Includes a standard parts library for reusable mechanical parts such as bearings, gears, internal ring gears, racks, and cycloidal discs.
- Includes a standard parts library for reusable bearings, gears, roller-chain sprockets, bolts, nuts, racks, and cycloidal discs.
- Emphasizes public surfaces rather than repository operations.
## Main reference entry points
@@ -50,7 +50,99 @@ top_faces = [face for face in body.get_faces() if "face.top" in scad.list_tags(s
print(len(top_faces))
```
Use `apply_tag(shape=..., tag=...)` for user-authored semantic tags and `list_tags(shape=...)` for deterministic inspection. Keep numeric dimensions, measurements, and rich descriptive data in metadata rather than tags.
Use `apply_tag(shape=..., tag=...)` for a local user-authored tag. Use `apply_tag_rselection(...)` when a selector, explicit downward inheritance, or an independent semantic shape view is required. Inspect `local`, `inherited`, `effective`, or `lineage` with `list_tags(shape=..., scope=...)` and `explain_tag(...)`. `effective` excludes lineage. Keep numeric dimensions, measurements, operation events, source roles, and rich descriptive data in typed metadata rather than tags.
## Feature Output Role Surface
The following roles are operation-owned, kernel-proven sets. `one` means exactly
one result is required when the role is requested; `many` means at least one
result is required and all proven results are tagged.
| Operation | Role | Kind | Cardinality | Named tag argument |
| --- | --- | --- | --- | --- |
| Box | `box.bottom` | Face | one | `bottom_face_tag` |
| Box | `box.top` | Face | one | `top_face_tag` |
| Box | `box.front` | Face | one | `front_face_tag` |
| Box | `box.back` | Face | one | `back_face_tag` |
| Box | `box.left` | Face | one | `left_face_tag` |
| Box | `box.right` | Face | one | `right_face_tag` |
| Cone | `cone.start` | Face | one | `start_face_tag` |
| Cone | `cone.end` | Face | one | `end_face_tag` |
| Cone | `cone.side` | Face | one | `side_face_tag` |
| Cone | `cone.start_boundary` | Edge | one | `start_edge_tag` |
| Cone | `cone.end_boundary` | Edge | one | `end_edge_tag` |
| Cone | `cone.seam` | Edge | one | `seam_edge_tag` |
| Extrude | `extrusion.start` | Face | one | `start_face_tag` |
| Extrude | `extrusion.end` | Face | one | `end_face_tag` |
| Extrude | `extrusion.side` | Face | many | `side_faces_tag` |
| Revolve | `revolution.start` | Face | one | `start_face_tag` |
| Revolve | `revolution.end` | Face | one | `end_face_tag` |
| Revolve | `revolution.side` | Face | many | `side_faces_tag` |
| Fillet | `fillet.patch` | Face | many | `generated_faces_tag` |
| Chamfer | `chamfer.patch` | Face | many | `generated_faces_tag` |
| Shell | `shell.body_face` | Face | many | `body_faces_tag` |
| Shell | `shell.offset_face` | Face | many | `offset_faces_tag` |
| Shell | `shell.closing_descendant` | Face | many | `closing_faces_tag` |
| Shell | `shell.wall` | Edge | many | `wall_edges_tag` |
| Loft | `loft.start` | Face | one | `start_face_tag` |
| Loft | `loft.end` | Face | one | `end_face_tag` |
| Loft | `loft.side` | Face | many | `side_faces_tag` |
| Sweep | `sweep.start` | Face | one | `start_face_tag` |
| Sweep | `sweep.end` | Face | one | `end_face_tag` |
| Sweep | `sweep.side` | Face | many | `side_faces_tag` |
| Twisted sweep | `twisted_sweep.start` | Face | one | `start_face_tag` |
| Twisted sweep | `twisted_sweep.end` | Face | one | `end_face_tag` |
| Twisted sweep | `twisted_sweep.side` | Face | many | `side_faces_tag` |
Every feature also accepts `result_tag` for its one result Solid. Each output
role has one semantic tag argument named for the target, such as
`start_face_tag`, `side_faces_tag`, or `generated_faces_tag`; there is no generic
role-to-tag mapping. Malformed tags, unavailable roles, and cardinality
mismatches fail the operation. A full revolve has no
separate start/end cap roles. Shell roles vary with actual OCC history and are
not synthesized when unavailable. Box Edge roles are unsupported because the
kernel builder has no equivalent direct Edge witnesses. Sweep and twisted sweep
reject profiles with inner wires. A pointed cone has no `cone.end` cap Face, but its
`cone.end_boundary` remains the kernel-proven degenerate apex Edge.
Use `ql.output_role(role_name=...)` to query operation role evidence. Use
`ql.source_binding(binding_id=...)` and `ql.source_topology(topo_id=...)` only for
projected local `TagBinding` evidence.
## Unified Tag Contract
Topology identity and user semantics use the same `TagBinding` and public query
surfaces. The authoring path and evidence determine projection behavior, not the
tag text:
- `tag_prefix="housing"` creates topology-identity tags such as
`housing.face.top` and `housing.solid`. These bindings carry `topology_name`
evidence and identify kernel-proven result topology.
- `top_face_tag="role.cover"` attaches `role.cover` to the kernel-proven top
Face. Its binding carries operation output role evidence.
- `result_tag="part.housing"` attaches `part.housing` to the returned Solid.
Its binding carries operation result evidence.
All three are discoverable with `list_tags(...)` and `explain_tag(...)` and
queryable with `ql.tag(...)`. A topology object may carry all three. Only a
topology-identity binding projects through the stricter exact-correspondence
path; a tag string does not opt into that behavior by resembling a topology tag.
## Constrained Sketch Tag Surface
Sketch entity IDs are creation-time local identifiers. When a constrained
profile is promoted, the exact ordered promotion map creates topology-identity
`TagBinding`s with `topology_name` evidence:
- Profile `bottom` in sketch `rect`: `sketch.rect.profile.bottom` on the Face or Wire.
- Entity `right` in sketch `rect`: `sketch.rect.entity.right` on its promoted Edge.
The promotion path validates that the generated Edge count equals the entity
map before attaching tags. It does not use geometric similarity, area,
normal, position, or enumeration order to repair a mismatch. Compatibility
tags such as `sketch_entity.right` remain ordinary compatibility tags.
Downstream features project topology-identity bindings only when kernel history
proves exact source-to-target correspondence.
## Recommended reading order
@@ -64,12 +156,16 @@ Use `apply_tag(shape=..., tag=...)` for user-authored semantic tags and `list_ta
## Typical replayable surface
```python
from simplecadapi import GraphSession, export_model_json, replay_model_json
import simplecadapi as scad
with GraphSession() as session:
...
@scad.model(graph_id="demo")
def build_model():
result = ...
scad.capture_result(value=result)
return result
model_json = export_model_json(session=session)
rebuilt = replay_model_json(json_str=model_json)
model = build_model()
model_json = model.model_json
rebuilt = model.replay()
print(len(rebuilt))
```
@@ -0,0 +1,17 @@
# Dimension
## Class Definition
```python
class Dimension(length: int = 0, angle: int = 0)
```
*Source: units.py*
## Import Surface
- top-level: `from simplecadapi import Dimension`
## Description
Physical dimension represented by integer length and angle exponents.
@@ -0,0 +1,20 @@
# DimensionTolerance
## Class Definition
```python
class DimensionTolerance(lower_deviation: float, upper_deviation: float)
```
*Source: expr.py*
## Import Surface
- top-level: `from simplecadapi import DimensionTolerance`
## Description
Permitted lower and upper deviations from a nominal dimension.
Deviations are signed: the lower deviation must be less than or equal to
zero and the upper deviation must be greater than or equal to zero.
@@ -21,6 +21,7 @@ Current design goals:
- Translate only from the canonical low-level `graph` IR
- Preserve node metadata and graph lineage as FreeCAD custom properties
- Preserve `expression_graph` as explicit translator metadata
- Preserve dimension tolerances and tolerance-chain requirements as metadata
- Preserve exported assembly constraints as document metadata objects
- Keep assembly metadata from the full model payload alongside the IR-driven
geometry translation
@@ -16,16 +16,14 @@ class GraphSession(graph_id: Optional[str] = None)
Context manager that records CAD operations into a DAG.
with GraphSession() as session:
n1 = record_operation(
"make_line_redge", {"start": (0, 0, 0), "end": (1, 0, 0)}
)
n2 = record_operation(
"make_line_redge", {"start": (1, 0, 0), "end": (1, 1, 0)}
)
record_operation(
"make_wire_from_edges_rwire", {"edge_count": 2}, inputs=[n1, n2]
)
For new replayable model entry points, prefer `@scad.model`, which owns the
session and returns a `ModelResult`. Use `GraphSession` directly for lower-level
graph workflows. `result_node_ids` reports nodes selected by `capture_result`:
# Access the graph after the session
print(session.graph.topological_order())
```python
with GraphSession(graph_id="demo") as session:
body = make_box_rsolid(width=10.0, height=6.0, depth=2.0)
session.capture_result(value=body)
print(session.result_node_ids)
```
@@ -0,0 +1,39 @@
# ModelResult
## Class Definition
```python
@dataclass(frozen=True)
class ModelResult:
value: Any
session: GraphSession
result_node_ids: Tuple[str, ...]
model_json: str
session_json: str
artifact_paths: Mapping[str, Path] = field(default_factory=dict)
```
*Source: graph.py*
## Import Surface
- top-level: `from simplecadapi import ModelResult`
## Description
Immutable result returned by a function decorated with `@scad.model`.
It contains the ordinary Python return value, the completed graph session, the
explicit result node ids, the model/session JSON artifacts, and any paths
written by automatic artifact export.
```python
result = build_model()
rebuilt = result.replay()
```
`replay()` replays `result.model_json`. Use `result.value` for the application
return value and `result.result_node_ids` to inspect the captured graph outputs.
Use `result.export_artifacts(output_dir=...)` to write one self-contained
`<graph_id>.scene.zip` after the model runs. The package embeds model JSON,
mapped project-relative Python sources, and render/selection assets; automatic
export does not write adjacent model/session JSON, STEP, STL, or FCStd files.
@@ -65,14 +65,38 @@ This index includes generated docs for the public SimpleCAD API surface, includi
- [loft_rsolid](loft_rsolid.md) *(from operations.py)* `top-level`
- [revolve_rsolid](revolve_rsolid.md) *(from operations.py)* `top-level`
- [sweep_rsolid](sweep_rsolid.md) *(from operations.py)* `top-level`
- [twisted_sweep_rsolid](twisted_sweep_rsolid.md) *(from operations.py)* `top-level`
## Tagging and Selection
- [apply_tag](apply_tag.md) *(from operations.py)* `top-level`
- [apply_tag_rselection](apply_tag_rselection.md) *(from operations.py)* `top-level`
- [explain_tag](explain_tag.md) *(from operations.py)* `top-level`
- [list_tags](list_tags.md) *(from operations.py)* `top-level`
- [select_edges_by_tag](select_edges_by_tag.md) *(from operations.py)* `top-level`
- [select_faces_by_tag](select_faces_by_tag.md) *(from operations.py)* `top-level`
Creation-time topology-identity tags are supported by profile constructors and
native feature primitives through `tag_prefix`. QL selectors also support `shared_boundary(...)`,
`intersection(...)`, `incident_to(...)`, `incident_face_count(...)`, and
`solids()` for relation-aware Edge selection.
## Unified Tag Contract
Topology identity and user semantics share one public tag model. Every binding
is inspected with `list_tags(...)` and `explain_tag(...)` and queried with
`ql.tag(...)`; one topology object may carry several tags for different uses.
`tag_prefix="housing"` creates topology-identity tags such as
`housing.face.top` and `housing.solid`. These bindings carry `topology_name`
evidence and project only when kernel history proves exact correspondence.
Role parameters such as `top_face_tag`, `side_faces_tag`, and
`generated_faces_tag`, plus `result_tag`, create tags whose evidence identifies
the kernel-proven role or result. Their tag text alone does not establish
topology identity. There is one public tag parameter per target role, not a
generic role-to-tag mapping.
## Boolean Operations
- [cut_rsolid](cut_rsolid.md) *(from operations.py)* `top-level`
@@ -98,6 +122,10 @@ This index includes generated docs for the public SimpleCAD API surface, includi
## Modeling Graph and Replay
- [GraphSession](GraphSession.md) *(from graph.py)* `top-level`
- [ModelResult](ModelResult.md) *(from graph.py)* `top-level`
- [capture_result](capture_result.md) *(from graph.py)* `top-level`
- [model](model.md) *(from graph.py)* `top-level`
- [requires_session](requires_session.md) *(from graph.py)* `top-level`
- [export_graph_json](export_graph_json.md) *(from serializer.py)* `top-level`
- [export_model_json](export_model_json.md) *(from serializer.py)* `top-level`
- [export_session_json](export_session_json.md) *(from serializer.py)* `top-level`
@@ -111,12 +139,34 @@ This index includes generated docs for the public SimpleCAD API surface, includi
## Expressions and Parameters
- [Const](Const.md) *(from expr.py)* `top-level`
- [DimensionTolerance](DimensionTolerance.md) *(from expr.py)* `top-level`
- [Expr](Expr.md) *(from expr.py)* `top-level`
- [ExpressionGraph](ExpressionGraph.md) *(from expr.py)* `top-level`
- [ToleranceAnalysis](ToleranceAnalysis.md) *(from tolerance.py)* `top-level`
- [ToleranceAnalysisError](ToleranceAnalysisError.md) *(from tolerance.py)* `top-level`
- [ToleranceCheck](ToleranceCheck.md) *(from tolerance.py)* `top-level`
- [ToleranceContribution](ToleranceContribution.md) *(from tolerance.py)* `top-level`
- [ToleranceGraph](ToleranceGraph.md) *(from tolerance.py)* `top-level`
- [ToleranceReport](ToleranceReport.md) *(from tolerance.py)* `top-level`
- [ToleranceRequirement](ToleranceRequirement.md) *(from tolerance.py)* `top-level`
- [ToleranceValidationError](ToleranceValidationError.md) *(from tolerance.py)* `top-level`
- [Var](Var.md) *(from expr.py)* `top-level`
- [analyze_tolerance](analyze_tolerance.md) *(from tolerance.py)* `top-level`
- [check_tolerance](check_tolerance.md) *(from tolerance.py)* `top-level`
- [const](const_function.md) *(from expr.py)* `top-level`
- [var](var_function.md) *(from expr.py)* `top-level`
## Physical Units
- [Dimension](Dimension.md) *(from units.py)* `top-level`
- [Unit](Unit.md) *(from units.py)* `top-level`
- [UnitValidationError](UnitValidationError.md) *(from units.py)* `top-level`
- [canonical_unit_for_dimension](canonical_unit_for_dimension.md) *(from units.py)* `top-level`
- [convert_value](convert_value.md) *(from units.py)* `top-level`
- [expression_uses_units](expression_uses_units.md) *(from units.py)* `top-level`
- [get_unit](get_unit.md) *(from units.py)* `top-level`
- [infer_dimension](infer_dimension.md) *(from units.py)* `top-level`
## Types and Errors
- [SimpleCADError](SimpleCADError.md) *(from errors.py)* `top-level`
@@ -207,11 +257,15 @@ This index includes generated docs for the public SimpleCAD API surface, includi
- [meta](meta.md) *(from ql.py)* `submodule:ql`
- [not_](not_.md) *(from ql.py)* `submodule:ql`
- [or_](or_.md) *(from ql.py)* `submodule:ql`
- [output_role](output_role.md) *(from ql.py)* `submodule:ql`
- [place_component_rassembly](place_component_rassembly.md) *(from operations.py)* `top-level`
- [radial_pattern_rsolidlist](radial_pattern_rsolidlist.md) *(from operations.py)* `top-level`
- [render_screenshot_rpath](render_screenshot_rpath.md) *(from operations.py)* `top-level`
- [select](select.md) *(from ql.py)* `submodule:ql`
- [solids](solids.md) *(from ql.py)* `submodule:ql`
- [solve_assembly_constraints_rassembly](solve_assembly_constraints_rassembly.md) *(from operations.py)* `top-level`
- [source_binding](source_binding.md) *(from ql.py)* `submodule:ql`
- [source_topology](source_topology.md) *(from ql.py)* `submodule:ql`
- [tag](tag.md) *(from ql.py)* `submodule:ql`
- [unground_component_rassembly](unground_component_rassembly.md) *(from operations.py)* `top-level`
- [value](value.md) *(from ql.py)* `submodule:ql`
@@ -22,3 +22,10 @@ Use `make_sketch_rsketch(...)`, `add_point_rsketch(...)`,
profiles. Public sketch construction APIs are functional and return an
updated `Sketch` document. The legacy `curves` constructor remains only for
reading already-built wire/edge containers.
Entity IDs are creation-time local identifiers. Constrained profile promotion uses
the ordered promotion map to bind each entity to exactly one generated Edge,
creating `sketch.<sketch-name>.entity.<entity-id>` and
`sketch.<sketch-name>.profile.<profile-id>` tags with `topology_name` evidence.
Downstream features may project these tags only when their kernel history proves exact
correspondence.
@@ -0,0 +1,17 @@
# ToleranceAnalysis
## Class Definition
```python
class ToleranceAnalysis(target_expr_id: str, method: ToleranceMethod, nominal: float, lower_bound: float, upper_bound: float, lower_deviation: float, upper_deviation: float, dimension: Dimension | None = None, unit: Unit | None = None, contributions: Tuple[ToleranceContribution, ...] = ())
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import ToleranceAnalysis`
## Description
Nominal value and propagated limits for an expression.
@@ -0,0 +1,17 @@
# ToleranceAnalysisError
## Class Definition
```python
class ToleranceAnalysisError
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import ToleranceAnalysisError`
## Description
Raised when a tolerance chain cannot be propagated safely.
@@ -0,0 +1,17 @@
# ToleranceCheck
## Class Definition
```python
class ToleranceCheck(requirement: ToleranceRequirement, analysis: ToleranceAnalysis, passed: bool, lower_margin: float, upper_margin: float)
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import ToleranceCheck`
## Description
Validation result for one tolerance requirement.
@@ -0,0 +1,17 @@
# ToleranceContribution
## Class Definition
```python
class ToleranceContribution(variable_expr_id: str, variable_name: str, nominal: float, source_tolerance: DimensionTolerance, sensitivity: float | None, lower_deviation: float, upper_deviation: float, source_unit: Unit | None = None)
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import ToleranceContribution`
## Description
One source dimension's propagated contribution to a result.
@@ -0,0 +1,17 @@
# ToleranceGraph
## Class Definition
```python
class ToleranceGraph(expression_graph: ExpressionGraph)
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import ToleranceGraph`
## Description
Tolerance requirements attached to one expression graph.
@@ -0,0 +1,17 @@
# ToleranceReport
## Class Definition
```python
class ToleranceReport(checks: Tuple[ToleranceCheck, ...] = ())
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import ToleranceReport`
## Description
Validation report for every requirement in a tolerance graph.
@@ -0,0 +1,17 @@
# ToleranceRequirement
## Class Definition
```python
class ToleranceRequirement(requirement_id: str, target_expr_id: str, tolerance: DimensionTolerance, method: ToleranceMethod = 'worst_case', name: str = '', tolerance_unit: Unit | None = None, target_dimension: Dimension | None = None)
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import ToleranceRequirement`
## Description
Permitted result deviations for one derived dimension.
@@ -0,0 +1,17 @@
# ToleranceValidationError
## Class Definition
```python
class ToleranceValidationError(report: 'ToleranceReport')
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import ToleranceValidationError`
## Description
Raised when one or more declared tolerance requirements fail.
@@ -0,0 +1,20 @@
# Unit
## Class Definition
```python
class Unit(symbol: str, dimension: Dimension, scale_to_canonical: float)
```
*Source: units.py*
## Import Surface
- top-level: `from simplecadapi import Unit`
## Description
Named unit with a scale to SimpleCAD's canonical numeric units.
Custom units are supported and serialize their symbol, dimension, and scale.
Registered built-in units serialize as compact symbols.
@@ -0,0 +1,17 @@
# UnitValidationError
## Class Definition
```python
class UnitValidationError
```
*Source: units.py*
## Import Surface
- top-level: `from simplecadapi import UnitValidationError`
## Description
Raised when units or expression dimensions are physically inconsistent.
@@ -3,7 +3,7 @@
## Class Definition
```python
class Var(name: str, default: float, comment: str | None = None, expr_id: str = field(default_factory=lambda : _make_expr_id('var')))
class Var(name: str, default: float, comment: str | None = None, expr_id: str = field(default_factory=lambda : _make_expr_id('var')), tolerance: DimensionTolerance | None = None, unit: Unit | None = None, tolerance_unit: Unit | None = None)
```
*Source: expr.py*
@@ -14,4 +14,8 @@ class Var(name: str, default: float, comment: str | None = None, expr_id: str =
## Description
Named scalar parameter with a default fallback value.
Named scalar parameter with optional physical-unit and tolerance intent.
``default`` and ``tolerance`` remain in their declared units. Evaluation,
geometry parameters, and tolerance propagation convert them to SimpleCAD's
canonical CAD units: millimeters for length and degrees for angle.
@@ -14,4 +14,6 @@ def add_line_rsketch(sketch: Sketch, entity_id: str, start: Union[SketchRef, str
## Description
Add a named line entity and return an updated sketch document.
Add an identified line entity and return an updated sketch document. Its
`entity_id` becomes the local segment of the canonical topology-identity tag for
the corresponding promoted profile Edge.
@@ -0,0 +1,27 @@
# analyze_tolerance
## API Definition
```python
def analyze_tolerance(value: ScalarLike, *, method: ToleranceMethod = 'worst_case') -> ToleranceAnalysis
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import analyze_tolerance`
## Description
Propagate source manufacturing tolerances through a scalar expression.
``worst_case`` returns guaranteed interval bounds. Affine chains are
dependency-aware, so repeated variables such as ``x - x`` cancel exactly;
nonlinear chains use conservative interval arithmetic. ``rss`` performs a
first-order root-sum-square calculation using analytic sensitivities.
Unit-aware variables are converted to canonical CAD units before
propagation. The returned analysis reports the inferred physical dimension
and canonical result unit. Every variable in the expression must declare a
source tolerance.
@@ -14,10 +14,13 @@ def apply_tag(shape: AnyShape, tag: str) -> AnyShape
## Description
Attach a normalized tag to a shape using the standard propagation policy.
Attach a normalized local user tag to a shape.
Tags must already be normalized lowercase tokens such as
``role.mounting_surface`` or ``group.fasteners``. Propagation is intentionally
not configurable from the public API; the default tag policy propagates
semantic role/anchor/group tags downward and keeps topology-specific tags
local.
`role.mounting_surface` or `group.fasteners`. The default topology policy is
`local` for every tag; token prefixes do not imply downward propagation.
Lineage visibility is limited to proven continuation and fragment witnesses.
`apply_tag(...)` preserves its historical in-place wrapper behavior. Use
`apply_tag_rselection(...)` when you need an independent semantic shape view,
explicit topology propagation, or a replayable multi-entity assignment.
@@ -0,0 +1,36 @@
# apply_tag_rselection
## API Definition
```python
def apply_tag_rselection(
scope: AnyShape,
targets: Union[ShapeSelector, Sequence[AnyShape]],
tag: str,
topology_propagation: str | TopologyPropagation = TopologyPropagation.LOCAL,
lineage_policy: str | LineagePolicy = LineagePolicy.CONTINUATION_FRAGMENT,
) -> AnyShape
```
*Source: operations.py*
## Import Surface
- top-level: `from simplecadapi import apply_tag_rselection`
## Description
Return an independent semantic view over the same geometry with one canonical
`TagBinding` attached to the selected entities. `targets` may be a serializable
QL `ShapeSelector` or a non-empty sequence of topology objects belonging to
`scope`.
Topology propagation defaults to `local`. Set `topology_propagation="downward"`
only when descendants should inherit the binding. Lineage defaults to proven
continuation and fragment derivations; it never makes lineage part of the
`effective` scope.
Inside `GraphSession`, the operation records the complete binding, target intent,
and selected-reference evidence. Replay re-resolves the target and checks that
the evidence has not drifted. The semantic node does not replace geometry-owned
topology references.
@@ -0,0 +1,19 @@
# canonical_unit_for_dimension
## API Definition
```python
def canonical_unit_for_dimension(dimension: Dimension) -> Unit
```
*Source: units.py*
## Import Surface
- top-level: `from simplecadapi import canonical_unit_for_dimension`
## Description
Return the canonical unit used by CAD and tolerance calculations.
Length, area, volume, and angle use ``mm``, ``mm^2``, ``mm^3``, and ``deg``.
@@ -0,0 +1,30 @@
# capture_result
## API Definition
```python
def capture_result(*, value: Any) -> Any
```
*Source: graph.py*
## Import Surface
- top-level: `from simplecadapi import capture_result`
## Description
Mark the graph nodes represented by `value` as explicit final outputs of the
active model session. The value is returned unchanged. Explicit capture keeps
intermediate/debug graph leaves out of `ModelResult.model_json`. Captured
geometry/product values are also the inputs to automatic artifact export when
the model decorator receives `export_dir=...`.
```python
@scad.model(graph_id="demo")
def build_demo():
body = scad.make_box_rsolid(width=10.0, height=4.0, depth=2.0)
return scad.capture_result(value=body)
```
Call it inside `@model` or another active `GraphSession`.
@@ -3,7 +3,14 @@
## API Definition
```python
def chamfer_rsolid(solid: Solid, edges: Union[Sequence[Edge], ShapeSelector], distance: ScalarLike) -> Solid
def chamfer_rsolid(
solid: Solid,
edges: Union[Sequence[Edge], ShapeSelector],
distance: ScalarLike,
*,
result_tag: Optional[str] = None,
generated_faces_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +21,10 @@ def chamfer_rsolid(solid: Solid, edges: Union[Sequence[Edge], ShapeSelector], di
## Description
Apply chamfers to selected solid edges.
Apply chamfers to selected solid edges. `generated_faces_tag` targets every face
with the kernel-proven `chamfer.patch` role. OCC contour expansion is included
rather than treating only the seed edge as the feature boundary.
The operation fails if a requested patch role has no proven result. `result_tag`
tags the resulting solid, and graph recording lowers assignments to replayable
semantic nodes.
@@ -0,0 +1,22 @@
# check_tolerance
## API Definition
```python
def check_tolerance(value: ScalarLike, tolerance: ToleranceLike, *, method: ToleranceMethod = 'worst_case', name: str | None = None, tolerance_unit: UnitLike | None = None) -> ToleranceCheck
```
*Source: tolerance.py*
## Import Surface
- top-level: `from simplecadapi import check_tolerance`
## Description
Propagate and verify one Length or Angle requirement.
``tolerance_unit`` defaults to the target dimension's canonical unit. When
provided, it must be dimensionally compatible and is converted before the
comparison. Legacy unitless requirements remain supported when no unit is
supplied.
@@ -0,0 +1,17 @@
# convert_value
## API Definition
```python
def convert_value(value: int | float, from_unit: UnitLike, to_unit: UnitLike) -> float
```
*Source: units.py*
## Import Surface
- top-level: `from simplecadapi import convert_value`
## Description
Convert a finite numeric value between dimensionally compatible units.
@@ -3,7 +3,11 @@
## API Definition
```python
def cut_rsolid(*solids: Union[Solid, Sequence[Solid]], skip_non_intersecting: bool = True) -> Solid
def cut_rsolid(
*solids: Union[Solid, Sequence[Solid]],
skip_non_intersecting: bool = True,
tracking_policy: TrackingPolicy | str = TrackingPolicy.FULL,
) -> Solid
```
*Source: operations.py*
@@ -29,6 +33,10 @@ sequences, and returns a single `Solid`.
- **Description**: When True, tools with no meaningful intersection are ignored for interactive convenience. Graph replay records this flag and should use False for strict diagnostic workflows.
### tracking_policy
- **Description**: `TrackingPolicy.FULL` computes topology history and lineage. `TrackingPolicy.GRAPH` preserves the canonical cut node, parameters, inputs, result topology references, and replay while omitting `TopoDelta` and history-derived topology lineage. Intersection validation and `skip_non_intersecting` behavior are unchanged.
## Returns
Solid: The cut result solid.
@@ -0,0 +1,27 @@
# explain_tag
## API Definition
```python
def explain_tag(
shape: AnyShape,
tag: str,
scope: str | TagScope = TagScope.EFFECTIVE,
) -> List[Dict[str, Any]]
```
*Source: operations.py*
## Import Surface
- top-level: `from simplecadapi import explain_tag`
## Description
Return every visible canonical binding that produces `tag` in the requested
scope. Explanations preserve binding identity, producer, attachment, evidence,
and policy-allowed lineage witnesses, so equal tag tokens from different
producers remain distinguishable.
As with `list_tags(...)`, `effective` excludes lineage. A lineage explanation
requires complete topology-history coverage.
@@ -3,7 +3,12 @@
## API Definition
```python
def export_model_json(session: 'GraphSession', indent: int = 2) -> str
def export_model_json(
session: 'GraphSession',
indent: int = 2,
*,
result_node_ids: Optional[Sequence[str]] = None,
) -> str
```
*Source: serializer.py*
@@ -20,3 +25,8 @@ Current Phase 1 scope uses the active session as the container of:
- operation graph
- expression graph
- capabilities/schema metadata
When `result_node_ids` is omitted, explicitly captured session results are used
when available; otherwise export falls back to graph leaves. New top-level model
code normally reads `ModelResult.model_json` instead of calling this function
directly.
@@ -0,0 +1,17 @@
# expression_uses_units
## API Definition
```python
def expression_uses_units(value: 'ScalarLike') -> bool
```
*Source: units.py*
## Import Surface
- top-level: `from simplecadapi import expression_uses_units`
## Description
Return whether an expression contains an explicit unit declaration.
@@ -3,7 +3,17 @@
## API Definition
```python
def extrude_rsolid(profile: Union[Wire, Face], direction: Tuple[float, float, float], distance: ScalarLike) -> Solid
def extrude_rsolid(
profile: Union[Wire, Face],
direction: Tuple[float, float, float],
distance: ScalarLike,
*,
tag_prefix: Optional[str] = None,
result_tag: Optional[str] = None,
start_face_tag: Optional[str] = None,
end_face_tag: Optional[str] = None,
side_faces_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +24,26 @@ def extrude_rsolid(profile: Union[Wire, Face], direction: Tuple[float, float, fl
## Description
Create a solid by extruding a profile.
Create a solid by extruding a profile. Kernel history assigns the output roles
`extrusion.start`, `extrusion.end`, and `extrusion.side`. Use the role tag
arguments to attach tags to those exact role sets.
Use creation-time profile topology tags (`tag_prefix` and `edge_tags`) when
durable feature Face/Edge identity is required. A tagged profile Edge yields a
corresponding `<tag_prefix>.face.side.<edge_tag>` when OCC proves the generated
side Face. Tagged Faces expose their effective tags to boundary Edge queries; use QL
`incident_to(..., distinct=True)` or `shared_boundary(...)` to disambiguate
an Edge by its two neighboring Faces.
Start and end roles require exactly one proven face. The side role requires one
or more proven faces and tags all of them. Missing, ambiguous, or unsupported
roles fail the whole operation instead of returning an untagged result.
`result_tag` attaches a local tag to the resulting solid. In a `GraphSession`,
all requested tags lower to replayable `apply_tag_rselection` semantic nodes;
they are not stored as geometry parameters.
Topology tags produced by `tag_prefix` remain directly queryable after
booleans when OCC provides a complete preserved or modified Face history. The
result binding retains the original semantic binding ID and source topology ID;
new boundary faces do not inherit the topology tag.
@@ -3,7 +3,14 @@
## API Definition
```python
def fillet_rsolid(solid: Solid, edges: Union[Sequence[Edge], ShapeSelector], radius: ScalarLike) -> Solid
def fillet_rsolid(
solid: Solid,
edges: Union[Sequence[Edge], ShapeSelector],
radius: ScalarLike,
*,
result_tag: Optional[str] = None,
generated_faces_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +21,10 @@ def fillet_rsolid(solid: Solid, edges: Union[Sequence[Edge], ShapeSelector], rad
## Description
Apply fillets to selected solid edges.
Apply fillets to selected solid edges. `generated_faces_tag` targets every face
with the kernel-proven `fillet.patch` role. OCC contour expansion is included,
so the role is not limited to the original seed edge.
The operation fails if a requested patch role has no proven result. `result_tag`
tags the resulting solid. In a `GraphSession`, assignments are separate replayable
semantic nodes with asserted user provenance.
@@ -0,0 +1,17 @@
# get_unit
## API Definition
```python
def get_unit(value: UnitLike) -> Unit
```
*Source: units.py*
## Import Surface
- top-level: `from simplecadapi import get_unit`
## Description
Resolve a built-in unit name/alias or return an existing ``Unit``.
@@ -0,0 +1,21 @@
# infer_dimension
## API Definition
```python
def infer_dimension(value: 'ScalarLike') -> Dimension | None
```
*Source: units.py*
## Import Surface
- top-level: `from simplecadapi import infer_dimension`
## Description
Infer and validate an expression's result dimension.
``None`` means the expression uses only legacy variables without unit
declarations. Expressions that contain explicit units are validated
strictly and cannot mix in legacy variables.
@@ -3,7 +3,10 @@
## API Definition
```python
def list_tags(shape: AnyShape) -> List[str]
def list_tags(
shape: AnyShape,
scope: str | TagScope = TagScope.EFFECTIVE,
) -> List[str]
```
*Source: operations.py*
@@ -14,4 +17,12 @@ def list_tags(shape: AnyShape) -> List[str]
## Description
Return shape tags in deterministic sorted order.
Return shape tags in deterministic sorted order for one semantic scope.
- `local`: bindings attached directly to the entity.
- `inherited`: bindings visible through explicit downward topology propagation.
- `effective`: local plus inherited bindings. Lineage is not included.
- `lineage`: bindings visible through complete, policy-allowed topology history.
Lineage queries fail with a semantic capability error when complete topology
history is unavailable; they do not guess from geometry or enumeration order.
@@ -3,7 +3,17 @@
## API Definition
```python
def loft_rsolid(profiles: List[Wire], ruled: bool = False) -> Solid
def loft_rsolid(
profiles: List[Wire],
ruled: bool = False,
*,
tracking_policy: TrackingPolicy | str = TrackingPolicy.FULL,
tag_prefix: Optional[str] = None,
result_tag: Optional[str] = None,
start_face_tag: Optional[str] = None,
end_face_tag: Optional[str] = None,
side_faces_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +24,13 @@ def loft_rsolid(profiles: List[Wire], ruled: bool = False) -> Solid
## Description
Create a solid by lofting multiple profiles.
Create a solid by lofting multiple profiles. Kernel history assigns
`loft.start`, `loft.end`, and `loft.side` roles. Start and end tags require one
proven face each; side tags apply to all proven side faces. `result_tag` targets
the solid. Recorded assignments are replayable semantic nodes.
`TrackingPolicy.FULL` is the default and preserves complete kernel topology
history. `TrackingPolicy.GRAPH` skips topology-history queries while still
recording and replaying the `make_loft_rsolid` graph node. In `GRAPH` mode,
`result_tag` remains available, but face-role tags and `tag_prefix` require
`FULL` tracking.
@@ -3,7 +3,21 @@
## API Definition
```python
def make_box_rsolid(width: ScalarLike, height: ScalarLike, depth: ScalarLike, bottom_face_center: Tuple[float, float, float] = (0, 0, 0)) -> Solid
def make_box_rsolid(
width: ScalarLike,
height: ScalarLike,
depth: ScalarLike,
bottom_face_center: Tuple[float, float, float] = (0, 0, 0),
*,
tag_prefix: Optional[str] = None,
result_tag: Optional[str] = None,
bottom_face_tag: Optional[str] = None,
top_face_tag: Optional[str] = None,
front_face_tag: Optional[str] = None,
back_face_tag: Optional[str] = None,
left_face_tag: Optional[str] = None,
right_face_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +28,12 @@ def make_box_rsolid(width: ScalarLike, height: ScalarLike, depth: ScalarLike, bo
## Description
Create a box solid.
Create a native box with six exact kernel-backed Face roles: `box.bottom`,
`box.top`, `box.front`, `box.back`, `box.left`, and `box.right`.
`tag_prefix="housing"` creates `housing.solid` and corresponding
`housing.face.<role>` topology-identity tags. Use the face tag arguments for
role tags and `result_tag` for the Solid.
Box Edge roles are unsupported because the current OCP builder does not expose
equivalent direct Edge witnesses. Select an exact Edge from two tagged incident
Faces with QL `incident_to(..., distinct=True)` or `shared_boundary(...)`.
@@ -3,7 +3,13 @@
## API Definition
```python
def make_circle_redge(center: Tuple[float, float, float], radius: ScalarLike, normal: Tuple[float, float, float] = (0, 0, 1)) -> Edge
def make_circle_redge(
center: Tuple[float, float, float],
radius: ScalarLike,
normal: Tuple[float, float, float] = (0, 0, 1),
*,
tag_prefix: Optional[str] = None,
) -> Edge
```
*Source: operations.py*
@@ -14,4 +20,5 @@ def make_circle_redge(center: Tuple[float, float, float], radius: ScalarLike, no
## Description
Create a circular edge.
Create a circular edge. `tag_prefix` optionally creates the topology tag
`<tag_prefix>.edge`.
@@ -3,7 +3,14 @@
## API Definition
```python
def make_circle_rface(center: Tuple[float, float, float], radius: ScalarLike, normal: Tuple[float, float, float] = (0, 0, 1)) -> Face
def make_circle_rface(
center: Tuple[float, float, float],
radius: ScalarLike,
normal: Tuple[float, float, float] = (0, 0, 1),
*,
tag_prefix: Optional[str] = None,
edge_tag: Optional[str] = None,
) -> Face
```
*Source: operations.py*
@@ -14,4 +21,8 @@ def make_circle_rface(center: Tuple[float, float, float], radius: ScalarLike, no
## Description
Create a circular face.
Create a circular face. `tag_prefix` creates `<tag_prefix>.face`, while
`edge_tag` supplies the final segment of `<tag_prefix>.edge.<edge_tag>` for its
boundary Edge, or the complete Edge tag when `tag_prefix` is omitted. The Face
topology tag is visible to effective boundary-Edge QL queries without copying
arbitrary local Face tags to every Edge.
@@ -3,7 +3,14 @@
## API Definition
```python
def make_circle_rwire(center: Tuple[float, float, float], radius: ScalarLike, normal: Tuple[float, float, float] = (0, 0, 1)) -> Wire
def make_circle_rwire(
center: Tuple[float, float, float],
radius: ScalarLike,
normal: Tuple[float, float, float] = (0, 0, 1),
*,
tag_prefix: Optional[str] = None,
edge_tag: Optional[str] = None,
) -> Wire
```
*Source: operations.py*
@@ -14,4 +21,8 @@ def make_circle_rwire(center: Tuple[float, float, float], radius: ScalarLike, no
## Description
Create a circular wire.
Create a circular wire. `tag_prefix` creates `<tag_prefix>.wire`, while
`edge_tag` supplies the final segment of `<tag_prefix>.edge.<edge_tag>` for its
single circular Edge, or the complete Edge tag when `tag_prefix` is omitted.
These topology tags are preserved only where a downstream operation has
complete, kernel-proven correspondence.
@@ -3,7 +3,22 @@
## API Definition
```python
def make_cone_rsolid(bottom_radius: ScalarLike, height: ScalarLike, top_radius: ScalarLike = 0.0, bottom_face_center: Tuple[float, float, float] = (0, 0, 0), axis: Tuple[float, float, float] = (0, 0, 1)) -> Solid
def make_cone_rsolid(
bottom_radius: ScalarLike,
height: ScalarLike,
top_radius: ScalarLike = 0.0,
bottom_face_center: Tuple[float, float, float] = (0, 0, 0),
axis: Tuple[float, float, float] = (0, 0, 1),
*,
tag_prefix: Optional[str] = None,
result_tag: Optional[str] = None,
start_face_tag: Optional[str] = None,
end_face_tag: Optional[str] = None,
side_face_tag: Optional[str] = None,
start_edge_tag: Optional[str] = None,
end_edge_tag: Optional[str] = None,
seam_edge_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +29,12 @@ def make_cone_rsolid(bottom_radius: ScalarLike, height: ScalarLike, top_radius:
## Description
Create a cone or truncated cone solid.
Create a native cone or frustum with direct kernel-backed `cone.start`,
`cone.end`, and `cone.side` Face roles and `cone.start_boundary`,
`cone.end_boundary`, and `cone.seam` Edge roles.
A pointed cone (`top_radius=0`) has no `cone.end` Face, so requesting that role
fails. Its `cone.end_boundary` is the kernel's degenerate apex Edge. A frustum
has all six roles. `tag_prefix="adapter"` creates corresponding
`adapter.face.*` and `adapter.edge.*` topology-identity tags; use the role tag
arguments for role assignments.
@@ -3,7 +3,21 @@
## API Definition
```python
def make_cylinder_rsolid(radius: ScalarLike, height: ScalarLike, bottom_face_center: Tuple[float, float, float] = (0, 0, 0), axis: Tuple[float, float, float] = (0, 0, 1)) -> Solid
def make_cylinder_rsolid(
radius: ScalarLike,
height: ScalarLike,
bottom_face_center: Tuple[float, float, float] = (0, 0, 0),
axis: Tuple[float, float, float] = (0, 0, 1),
*,
tag_prefix: Optional[str] = None,
result_tag: Optional[str] = None,
start_face_tag: Optional[str] = None,
end_face_tag: Optional[str] = None,
side_face_tag: Optional[str] = None,
start_edge_tag: Optional[str] = None,
end_edge_tag: Optional[str] = None,
seam_edge_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +28,9 @@ def make_cylinder_rsolid(radius: ScalarLike, height: ScalarLike, bottom_face_cen
## Description
Create a cylinder solid.
Create a native cylinder with kernel-backed topology tags. `tag_prefix="shaft"` produces
`shaft.face.start`, `shaft.face.end`, `shaft.face.side`, and corresponding
`shaft.edge.start`, `shaft.edge.end`, and `shaft.edge.seam` tags. Face tags
are inherited by their boundary Edges. Role tags use the native
`cylinder.*` Face/Edge roles. Use QL `incident_to(..., distinct=True)` or
`shared_boundary(...)` to select an Edge from its two neighboring tagged Faces.
@@ -14,4 +14,12 @@ def make_face_from_sketch_rface(sketch: Sketch, profile: int | str = 0, *, requi
## Description
Promote a sketch profile to a concrete face, solving internally.
Promote a sketch profile to a concrete face, solving internally. The Face and
each boundary Edge receive canonical tags with `topology_name` evidence from the
exact Sketch promotion map. For a Sketch with `name="rect"`, profile `bottom`,
and entity ID `right`, the tags are `sketch.rect.profile.bottom` and
`sketch.rect.entity.right`.
The bindings replay from the Sketch payload and promotion parameters. Legacy
compatibility tags such as `sketch_entity.right` remain separate from the
canonical topology-identity tags.
@@ -3,7 +3,12 @@
## API Definition
```python
def make_face_from_wire_rface(wire: Wire, normal: Tuple[float, float, float] = (0, 0, 1)) -> Face
def make_face_from_wire_rface(
wire: Wire,
normal: Tuple[float, float, float] = (0, 0, 1),
*,
tag_prefix: Optional[str] = None,
) -> Face
```
*Source: operations.py*
@@ -14,4 +19,6 @@ def make_face_from_wire_rface(wire: Wire, normal: Tuple[float, float, float] = (
## Description
Create a face from a closed wire.
Create a face from a closed wire. Existing proven Edge topology tags on the
wire are copied to corresponding Face boundary Edges. `tag_prefix` optionally
adds the Face tag `<tag_prefix>.face`.
@@ -3,7 +3,12 @@
## API Definition
```python
def make_line_redge(start: Tuple[ScalarLike, ScalarLike, ScalarLike], end: Tuple[ScalarLike, ScalarLike, ScalarLike]) -> Edge
def make_line_redge(
start: Tuple[ScalarLike, ScalarLike, ScalarLike],
end: Tuple[ScalarLike, ScalarLike, ScalarLike],
*,
tag_prefix: Optional[str] = None,
) -> Edge
```
*Source: operations.py*
@@ -14,4 +19,7 @@ def make_line_redge(start: Tuple[ScalarLike, ScalarLike, ScalarLike], end: Tuple
## Description
Create a straight edge between two points.
Create a straight edge between two points. When `tag_prefix` is provided, the
edge receives the topology tag `<tag_prefix>.edge`. Downstream profile and
feature operations may preserve that tag when kernel history proves the
correspondence.
@@ -3,7 +3,15 @@
## API Definition
```python
def make_rectangle_rface(width: ScalarLike, height: ScalarLike, center: Tuple[ScalarLike, ScalarLike, ScalarLike] = (0, 0, 0), normal: Tuple[ScalarLike, ScalarLike, ScalarLike] = (0, 0, 1)) -> Face
def make_rectangle_rface(
width: ScalarLike,
height: ScalarLike,
center: Tuple[ScalarLike, ScalarLike, ScalarLike] = (0, 0, 0),
normal: Tuple[ScalarLike, ScalarLike, ScalarLike] = (0, 0, 1),
*,
tag_prefix: Optional[str] = None,
edge_tags: Optional[Sequence[str]] = None,
) -> Face
```
*Source: operations.py*
@@ -14,4 +22,9 @@ def make_rectangle_rface(width: ScalarLike, height: ScalarLike, center: Tuple[Sc
## Description
Create a rectangular face.
Create a rectangular face. `tag_prefix` creates `<tag_prefix>.face`, and
`edge_tags` supplies one tag for each of its four boundary Edges. With
`tag_prefix`, each is a local segment under `<tag_prefix>.edge`; without it,
each is a complete Edge tag. These topology tags can be projected to proven
feature Faces and queried with the same `list_tags(...)` and `ql.tag(...)`
surfaces as other tags.
@@ -3,7 +3,15 @@
## API Definition
```python
def make_rectangle_rwire(width: ScalarLike, height: ScalarLike, center: Tuple[ScalarLike, ScalarLike, ScalarLike] = (0, 0, 0), normal: Tuple[ScalarLike, ScalarLike, ScalarLike] = (0, 0, 1)) -> Wire
def make_rectangle_rwire(
width: ScalarLike,
height: ScalarLike,
center: Tuple[ScalarLike, ScalarLike, ScalarLike] = (0, 0, 0),
normal: Tuple[ScalarLike, ScalarLike, ScalarLike] = (0, 0, 1),
*,
tag_prefix: Optional[str] = None,
edge_tags: Optional[Sequence[str]] = None,
) -> Wire
```
*Source: operations.py*
@@ -14,4 +22,9 @@ def make_rectangle_rwire(width: ScalarLike, height: ScalarLike, center: Tuple[Sc
## Description
Create a rectangular wire.
Create a rectangular wire. `tag_prefix` creates `<tag_prefix>.wire`.
`edge_tags` must contain one tag for each generated profile Edge, in kernel
construction order. With `tag_prefix`, each value is the local segment of
`<tag_prefix>.edge.<edge_tag>`; without it, each value is the complete Edge tag.
These topology tags are stable anchors for operations such as `extrude_rsolid`
when correspondence is proven.
@@ -17,4 +17,5 @@ def make_sketch_rsketch(name: Optional[str] = None, *, plane: Any = 'XY', sketch
Create an empty declarative sketch document.
Use this API, not concrete edge/wire constructors, when the intent is to
build a sketch profile with constraints.
build a sketch profile with constraints. The sketch name and explicit entity
IDs are stable local identifiers used by constrained profile promotion.
@@ -3,7 +3,7 @@
## API Definition
```python
def make_wire_from_edges_rwire(edges: List[Edge]) -> Wire
def make_wire_from_edges_rwire(edges: List[Edge], *, tag_prefix: Optional[str] = None) -> Wire
```
*Source: operations.py*
@@ -14,4 +14,6 @@ def make_wire_from_edges_rwire(edges: List[Edge]) -> Wire
## Description
Create a wire from a list of connected edges.
Create a wire from a list of connected edges. Existing proven Edge topology
tags are preserved by exact topology identity; `tag_prefix` optionally adds
`<tag_prefix>.wire` to the resulting wire.
@@ -14,4 +14,10 @@ def make_wire_from_sketch_rwire(sketch: Sketch, profile: int | str = 0, *, requi
## Description
Promote a sketch profile to a concrete wire, solving internally.
Promote a sketch profile to a concrete wire, solving internally. The promotion
map preserves exact ordered entity-to-Edge correspondence and creates tags with
`topology_name` evidence, such as `sketch.rect.profile.bottom` and
`sketch.rect.entity.right`.
Promotion fails when the generated Edge count does not match the promotion map;
the SDK does not infer identity from geometry or enumeration heuristics.
@@ -0,0 +1,44 @@
# model
## API Definition
```python
def model(
func=None,
*,
graph_id: Optional[str] = None,
export_dir: Optional[str | Path] = None,
) -> Callable
```
*Source: graph.py*
## Import Surface
- top-level: `from simplecadapi import model`
## Description
Decorate the single top-level entry point of a replayable model. Each invocation
creates and owns exactly one `GraphSession`, activates it while the function
runs, captures model/session JSON in memory, and returns a `ModelResult`. When
`export_dir` is provided, explicitly captured geometry/product values produce
one `<graph_id>.scene.zip` in that directory. The package embeds model JSON,
mapped project-relative Python source files, and render/selection assets; it
does not create adjacent model/session JSON, STEP, STL, or FCStd files.
```python
@scad.model(graph_id="bracket")
def build_bracket():
body = scad.make_box_rsolid(width=20.0, height=10.0, depth=3.0)
scad.capture_result(value=body)
return body
```
Use `result.artifact_paths["scene"]` to locate the package, or call
`result.export_artifacts(output_dir=...)` after a model has run without an
export directory. The explicit `export_dir` opt-in avoids unexpected filesystem
writes for library callers and tests.
Do not nest `@model` functions or create another `GraphSession` inside a model
function. Use `@requires_session` for child builders.
@@ -0,0 +1,23 @@
# output_role
## API Definition
```python
def output_role(role_name: str) -> SerializablePredicate
```
*Source: ql.py*
## Import Surface
- submodule: `from simplecadapi import ql`
## Description
Return a serializable predicate matching a kernel-proven operation output role
in `metadata["track"]`. Role matching never falls back to face order, geometry,
or flat tags. Use it with a typed selector, for example:
```python
end = ql.faces().where(ql.output_role(role_name="extrusion.end")).exactly(1)
```
@@ -3,7 +3,7 @@
## API Definition
```python
def render_screenshot_rpath(shapes: Union[Solid, Sequence[Solid]], output_path: str, highlight_tags: Optional[Sequence[str]] = None, tag_labels: Optional[Dict[str, str]] = None, image_size: Tuple[int, int] = (1400, 900), view: Union[Tuple[float, float], str] = 'auto', show_axes: bool = True, show_legend: bool = True, zoom: float = 4.0) -> str
def render_screenshot_rpath(shapes: Union[Solid, Sequence[Solid]], output_path: str, highlight_tags: Optional[Sequence[str]] = None, tag_labels: Optional[Dict[str, str]] = None, image_size: Tuple[int, int] = (1400, 900), view: Union[Tuple[float, float], str] = 'auto', show_axes: bool = True, show_legend: bool = True, zoom: float = 4.0, show_callouts: bool = True) -> str
```
*Source: operations.py*
@@ -14,4 +14,6 @@ def render_screenshot_rpath(shapes: Union[Solid, Sequence[Solid]], output_path:
## Description
Render a screenshot of shapes and save it to a file.
Render a screenshot of shapes and save it to a file. Set `show_callouts=False`
to retain highlighted material colors and the legend without placing tag labels
over the model.
@@ -0,0 +1,22 @@
# requires_session
## API Definition
```python
def requires_session(func=None) -> Callable
```
*Source: graph.py*
## Import Surface
- top-level: `from simplecadapi import requires_session`
## Description
Decorate a reusable graph-producing builder that must run inside the caller's
active `GraphSession`. It reuses the session owned by the enclosing `@model`
function and validates returned graph values against that session.
Calling a `@requires_session` builder without an active session raises
`RuntimeError`. Builders must not create their own `GraphSession`.
@@ -3,7 +3,18 @@
## API Definition
```python
def revolve_rsolid(profile: Union[Wire, Face], axis: Tuple[float, float, float] = (0, 0, 1), angle: ScalarLike = 360, origin: Tuple[float, float, float] = (0, 0, 0)) -> Solid
def revolve_rsolid(
profile: Union[Wire, Face],
axis: Tuple[float, float, float] = (0, 0, 1),
angle: ScalarLike = 360,
origin: Tuple[float, float, float] = (0, 0, 0),
*,
tag_prefix: Optional[str] = None,
result_tag: Optional[str] = None,
start_face_tag: Optional[str] = None,
end_face_tag: Optional[str] = None,
side_faces_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +25,10 @@ def revolve_rsolid(profile: Union[Wire, Face], axis: Tuple[float, float, float]
## Description
Create a solid by revolving a profile around an axis.
Create a solid by revolving a profile around an axis. Kernel history assigns
`revolution.start`, `revolution.end`, and `revolution.side` output roles.
Start and end roles require exactly one proven face; side tags apply to every
proven side face. A full 360-degree revolve normally has no separate start or end
face, so requesting either cap tag raises a capability error rather than guessing.
`result_tag` tags the resulting solid. Role tags are replayable semantic nodes.
@@ -15,3 +15,15 @@ def select(items: Iterable[Any]) -> Query
## Description
Start a QL query over a shape collection or selector scope.
For topology-aware selection, use `ql.faces()`, `ql.edges()`, `ql.wires()`,
`ql.vertices()`, or `ql.solids()`. `ShapeSelector.intersection(other)` forms a
serializable set intersection. `selector.shared_boundary(other,
to_kind="edge")` intersects the boundaries of two selectors. Edge selectors
also support `incident_to(face_selector, ..., distinct=True)` and
`incident_face_count(exactly=2)` to select edges by exact incident Face
witnesses and reject open or non-manifold edges.
These selectors resolve by topology identity, not enumeration order, area,
normal, or position heuristics. Their `to_dict()` payloads can be restored with
`ql.selector_from_dict(...)`.
@@ -3,7 +3,11 @@
## API Definition
```python
def select_edges_by_tag(shape: Union[Face, Solid], tag: str) -> List[Edge]
def select_edges_by_tag(
shape: Union[Face, Solid],
tag: str,
scope: str | TagScope = TagScope.EFFECTIVE,
) -> List[Edge]
```
*Source: operations.py*
@@ -14,4 +18,6 @@ def select_edges_by_tag(shape: Union[Face, Solid], tag: str) -> List[Edge]
## Description
Select edges by tag.
Select edges by an exact normalized tag in the requested semantic scope.
`effective` does not include lineage; request `scope="lineage"` explicitly when
selection depends on complete topology-history evidence.
@@ -3,7 +3,11 @@
## API Definition
```python
def select_faces_by_tag(solid: Solid, tag: str) -> List[Face]
def select_faces_by_tag(
solid: Solid,
tag: str,
scope: str | TagScope = TagScope.EFFECTIVE,
) -> List[Face]
```
*Source: operations.py*
@@ -14,4 +18,6 @@ def select_faces_by_tag(solid: Solid, tag: str) -> List[Face]
## Description
Select faces by tag.
Select faces by an exact normalized tag in the requested semantic scope.
`effective` does not include lineage; request `scope="lineage"` explicitly when
selection depends on complete topology-history evidence.
@@ -3,7 +3,17 @@
## API Definition
```python
def shell_rsolid(solid: Solid, faces_to_remove: Union[Sequence[Face], ShapeSelector], thickness: ScalarLike) -> Solid
def shell_rsolid(
solid: Solid,
faces_to_remove: Union[Sequence[Face], ShapeSelector],
thickness: ScalarLike,
*,
result_tag: Optional[str] = None,
body_faces_tag: Optional[str] = None,
offset_faces_tag: Optional[str] = None,
closing_faces_tag: Optional[str] = None,
wall_edges_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +24,16 @@ def shell_rsolid(solid: Solid, faces_to_remove: Union[Sequence[Face], ShapeSelec
## Description
Shell a solid to create a hollow part.
Shell a solid to create a hollow part. The operation can expose these exact
kernel roles:
- `shell.body_face`: surviving or modified source body faces.
- `shell.offset_face`: generated offset faces.
- `shell.closing_descendant`: descendants of removed closing faces.
- `shell.wall`: generated closing-boundary edges.
The named arguments map directly to those roles. A role is available only when
OCC provides a complete witness;
requesting an unavailable role fails instead of deriving one from enumeration or
geometry. `result_tag` tags the resulting solid. Recorded assignments replay as
semantic nodes and preserve face versus edge target kinds.
@@ -0,0 +1,19 @@
# solids
## API Definition
```python
def solids() -> ShapeSelector
```
*Source: ql.py*
## Import Surface
- submodule: `from simplecadapi import ql`
## Description
Create a serializable selector over Solid topology. Solid selectors can be
traversed to boundary Edges and combined with `intersection(...)` or
`shared_boundary(...)` to find topology common to two named Solid selectors.
@@ -0,0 +1,19 @@
# source_binding
## API Definition
```python
def source_binding(binding_id: str) -> SerializablePredicate
```
*Source: ql.py*
## Import Surface
- submodule: `from simplecadapi import ql`
## Description
Match a local projected `TagBinding` whose topology-change evidence preserves the
exact source `binding_id`. Objects without canonical local binding evidence raise
an unsupported-query capability error instead of consulting flat tags.
@@ -0,0 +1,19 @@
# source_topology
## API Definition
```python
def source_topology(topo_id: str) -> SerializablePredicate
```
*Source: ql.py*
## Import Surface
- submodule: `from simplecadapi import ql`
## Description
Match a local projected `TagBinding` by the exact source topology identity stored
in its kernel-history evidence. This predicate queries source-preserving evidence;
it does not infer ancestry from geometry.
@@ -3,7 +3,17 @@
## API Definition
```python
def sweep_rsolid(profile: Face, path: Wire, is_frenet: bool = False) -> Solid
def sweep_rsolid(
profile: Face,
path: Wire,
is_frenet: bool = False,
*,
tag_prefix: Optional[str] = None,
result_tag: Optional[str] = None,
start_face_tag: Optional[str] = None,
end_face_tag: Optional[str] = None,
side_faces_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
@@ -14,4 +24,10 @@ def sweep_rsolid(profile: Face, path: Wire, is_frenet: bool = False) -> Solid
## Description
Create a solid by sweeping a profile along a path.
Create a solid by sweeping a profile along a path. Kernel history assigns
`sweep.start`, `sweep.end`, and `sweep.side` roles. Start and end tags require one
proven face each; side tags apply to all proven side faces. `result_tag` targets
the solid, and recorded assignments are replayable semantic nodes.
Profiles with inner wires are rejected because the current PipeShell operation
receives only the outer wire; silently dropping profile holes is not allowed.
@@ -0,0 +1,46 @@
# twisted_sweep_rsolid
## API Definition
```python
def twisted_sweep_rsolid(
profile: Face,
distance: ScalarLike,
twist_angle: ScalarLike,
axis: Tuple[float, float, float] = (0.0, 0.0, 1.0),
origin: Tuple[float, float, float] = (0.0, 0.0, 0.0),
*,
guide_radius: ScalarLike = 1.0,
tag_prefix: Optional[str] = None,
result_tag: Optional[str] = None,
start_face_tag: Optional[str] = None,
end_face_tag: Optional[str] = None,
side_faces_tag: Optional[str] = None,
) -> Solid
```
*Source: operations.py*
## Import Surface
- top-level: `from simplecadapi import twisted_sweep_rsolid`
## Description
Sweep a planar profile along a straight axis while rotating it linearly by the
signed total `twist_angle` in degrees. The profile must lie at the sweep start,
be planar, and be normal to `axis`.
The OCP implementation uses a one-edge straight spine and a one-edge
cylindrical auxiliary spine. This normally creates one continuous side face per
profile edge rather than splitting every side at intermediate loft sections.
Profiles with inner wires are rejected.
Kernel history assigns `twisted_sweep.start`, `twisted_sweep.end`, and
`twisted_sweep.side` roles. The operation records one canonical
`make_twisted_sweep_rsolid` graph node containing `axis`, `origin`, `distance`,
`twist_angle`, and `guide_radius`; strict replay invokes the same public
operation with the recorded parameters.
`guide_radius` controls only the auxiliary orientation guide and must be a
positive finite value. It does not set the swept profile radius.
@@ -3,7 +3,13 @@
## API Definition
```python
def union_rsolid(*solids: Union[Solid, Sequence[Solid]], clean: bool = True, glue: bool = _DEFAULT_UNION_GLUE, tol: Optional[float] = None) -> Solid
def union_rsolid(
*solids: Union[Solid, Sequence[Solid]],
clean: bool = True,
glue: bool = _DEFAULT_UNION_GLUE,
tol: Optional[float] = None,
tracking_policy: TrackingPolicy | str = TrackingPolicy.FULL,
) -> Solid
```
*Source: operations.py*
@@ -40,6 +46,10 @@ returning multiple pieces.
- **Type**: `Optional fuzzy-boolean tolerance used by the OCC union kernel. When`
- **Description**: omitted, SimpleCAD chooses a conservative scale-aware tolerance.
### tracking_policy
- **Description**: `TrackingPolicy.FULL` computes topology history and lineage. `TrackingPolicy.GRAPH` preserves the canonical union node, parameters, inputs, result topology references, and replay while omitting `TopoDelta` and history-derived topology lineage. Geometry options `clean`, `glue`, and `tol` are unchanged.
## Returns
Solid: The merged union result.
@@ -3,7 +3,7 @@
## API Definition
```python
def var(name: str, default: int | float, comment: str | None = None) -> Var
def var(name: str, default: int | float, comment: str | None = None, tolerance: ToleranceLike | None = None, *, unit: UnitLike | None = None, tolerance_unit: UnitLike | None = None) -> Var
```
*Source: expr.py*
@@ -14,4 +14,13 @@ def var(name: str, default: int | float, comment: str | None = None) -> Var
## Description
Create a named variable node for v2 expression-driven parameters.
Create a physical or legacy scalar variable.
``tolerance=0.1`` declares a symmetric ``+/-0.1`` tolerance. Use a
``(lower_deviation, upper_deviation)`` pair for an asymmetric tolerance.
``tolerance_unit`` defaults to ``unit`` when a nominal unit is declared.
Values are converted to canonical CAD units only when evaluated, so the
declaration and serialized expression node preserve the user's units.
Variables without ``unit`` retain legacy unitless behavior. A unit-aware
expression cannot mix declared-unit variables with legacy variables.
@@ -33,7 +33,7 @@ SimpleCADAPI 的顶层 API 选择是正确的:它没有把用户拖进传统 C
- historical scalar-field/SDF implementation, now removed from the active public/support surface
- `docs/core/serialization/`
- `docs/core/operation_graph_json_spec.md`
- `examples/07_serialization_operation_tree.py`
- retained replayable examples under `examples/`
- `test/` 与 `tests/`
## 顶层 API 评价
@@ -46,7 +46,7 @@ SimpleCADAPI 的顶层 API 选择是正确的:它没有把用户拖进传统 C
- `__init__.py` 明确导出建模函数、核心类型、graph/session、serializer、expression 和 `ql` 子模块,公开面可见性好。参考:`src/simplecadapi/__init__.py`。
- 大多数核心创建函数遵守返回类型后缀,例如 `make_point_rvertex -> Vertex`、`make_line_redge -> Edge`、`make_circle_rwire -> Wire`、`make_box_rsolid -> Solid`。参考:`src/simplecadapi/operations.py:691`、`src/simplecadapi/operations.py:723`、`src/simplecadapi/operations.py:1465`。
- composite convenience API 在 `GraphSession` 内降低为 canonical low-level graph,这个设计非常正确。用户调用 `make_box_rsolid`,图里记录 rectangle/profile/extrude,这是 CAD DSL 和 replay IR 分层的正确方式。参考:`src/simplecadapi/operations.py:1480`、`examples/07_serialization_operation_tree.py:52`、`examples/07_serialization_operation_tree.py:272`。
- composite convenience API 在 `GraphSession` 内降低为 canonical low-level graph,这个设计非常正确。用户调用 `make_box_rsolid`,图里记录 rectangle/profile/extrude,这是 CAD DSL 和 replay IR 分层的正确方式。参考:`src/simplecadapi/operations.py` 与 `test/test_serialization.py`。
- `union_rsolid(...)` 明确返回单个 `Solid`,失败就报错,不默默返回多个实体。这对机械 CAD 是正确的默认语义。参考:`src/simplecadapi/operations.py:2856`、`src/simplecadapi/operations.py:2876`。
- `GraphSession` + `export_model_json` + `replay_model_json` 的主线是对的。它把脚本建模提升为可交换、可 replay 的模型记录。参考:`src/simplecadapi/graph.py:44`、`src/simplecadapi/serializer.py:416`、`src/simplecadapi/serializer.py:732`。
- `ql` 被放到子模块而非全部塞进顶层,是正确的边界意识。参考:`src/simplecadapi/__init__.py`、`docs/api/README.md:5`。
@@ -154,7 +154,7 @@ QL 是架构上最应该继续投资的部分。
- `OperationGraph` 是 DAG,节点保存 op、params、param_exprs、inputs、context、semantic_delta、topo_delta、tags。结构完整。参考:`src/simplecadapi/topology.py:318`、`src/simplecadapi/topology.py:476`。
- `export_model_json` 输出 graph、leaf_ids、expression_graph、frame_graph、registries、delta logs、canonical contract。方向很对。参考:`src/simplecadapi/serializer.py:627`。
- `_assert_graph_is_canonical` 在 export model 前拒绝非 canonical ops,这是保持 IR 干净的正确动作。参考:`src/simplecadapi/serializer.py:246`、`src/simplecadapi/serializer.py:624`。
- `examples/07_serialization_operation_tree.py` 非常有价值。它把 source API 到 operation tree 的映射讲清楚,是 SDK 最该保留和扩展的示例。参考:`examples/07_serialization_operation_tree.py:1`、`examples/07_serialization_operation_tree.py:217`。
- `test/test_serialization.py` 系统覆盖 source API 到 operation tree 的映射,是该契约的主要回归入口。
严重问题:
@@ -164,7 +164,7 @@ QL 是架构上最应该继续投资的部分。
- graph schema version 是 `1.0`,model schema 是 `2.0-draft`,canonical contract 是 `2.0-final-state`。这三个版本信号混在一起,会让外部消费者不知道哪个才是稳定承诺。参考:`src/simplecadapi/topology.py:19`、`src/simplecadapi/serializer.py:221`、`src/simplecadapi/serializer.py:628`。
- node.context 和 frame_graph 被记录,但 replay 不恢复 frame/workplane;所有 ops 按当前 ambient world context 执行。参考:`src/simplecadapi/graph.py:132`、`src/simplecadapi/serializer.py:991`。
- `leaf_ids` 自动来自 graph leaves,而不是用户显式声明的 final outputs。debug/intermediate 独立节点会成为 replay 输出。参考:`src/simplecadapi/serializer.py:624`、`src/simplecadapi/topology.py:563`。
- expression replay 是 numeric snapshot,不是 parametric replay。这个可以接受,但必须在 SDK 主文档里反复强调。参考:`examples/07_serialization_operation_tree.py:40`、`examples/07_serialization_operation_tree.py:287`。
- expression replay 是 numeric snapshot,不是 parametric replay。这个可以接受,但必须在 SDK 主文档里反复强调。参考:`test/test_serialization.py`。
建议:replay 应该默认 strict。所有 missing input、missing param、unknown op、leaf missing output、selection cardinality mismatch 都应该 hard failure。需要宽松模式时显式 `strict=False`。
@@ -62,7 +62,7 @@ SimpleWorkplane ← local modeling context
- **Shape-first API**: users work with `Vertex`, `Edge`, `Wire`, `Face`, and `Solid`, not graph nodes.
- **Functional modeling style**: public operations return new geometry values, e.g. `make_box_rsolid(...)`, `cut_rsolid(...)`, `fillet_rsolid(...)`.
- **OCP-native runtime**: geometry construction, topology traversal, properties, booleans, transforms, and export use OCP/OpenCascade helpers.
- **Replayable graph workflows**: `GraphSession` can record a canonical low-level operation graph and `export_model_json()` can serialize it for `replay_model_json()`.
- **Replayable graph workflows**: `@scad.model` owns one `GraphSession` and returns a `ModelResult`; `@scad.requires_session` composes child builders, and `scad.capture_result()` selects canonical output nodes for replay and export.
- **Tags and metadata**: tags are useful for lightweight semantics; structured numeric facts should be stored in metadata such as `metadata["geo"]`.
- **Indexed topology access**: use plural methods such as `get_edges()` and `get_faces()` for enumeration, and pass an index to the same getter, such as `get_edges(index)` or `get_faces(index)`, for intentional indexed picks that should become graph selection nodes.
@@ -74,11 +74,14 @@ import simplecadapi as scad
with scad.SimpleWorkplane(origin=(0, 0, 0)):
box = scad.make_box_rsolid(width=5, height=3, depth=2)
scad.apply_tag(box, "role.bracket")
scad.apply_tag(shape=box, tag="role.bracket")
box.set_metadata("material", "6061-T6")
box.auto_tag_faces("box")
top_faces = [face for face in box.get_faces() if "face.top" in scad.list_tags(face)]
top_faces = [
face for face in box.get_faces()
if "face.top" in scad.list_tags(shape=face)
]
print(len(top_faces))
```
@@ -87,13 +90,18 @@ print(len(top_faces))
```python
import simplecadapi as scad
with scad.GraphSession() as session:
body = scad.make_box_rsolid(10, 10, 4)
hole = scad.make_cylinder_rsolid(1.5, 8, bottom_face_center=(0, 0, -2))
@scad.model(graph_id="drilled_block")
def build_model():
body = scad.make_box_rsolid(width=10, height=10, depth=4)
hole = scad.make_cylinder_rsolid(
radius=1.5, height=8, bottom_face_center=(0, 0, -2)
)
part = scad.cut_rsolid(body, hole)
scad.capture_result(value=part)
return part
payload = scad.export_model_json(session)
rebuilt = scad.replay_model_json(payload)
result = build_model()
rebuilt = result.replay()
print(len(rebuilt))
```
@@ -0,0 +1,56 @@
# Dimension Tolerance Chains
SimpleCADAPI can attach manufacturing tolerances to declared dimension variables, propagate them through the expression DAG, and verify derived dimensions against design requirements.
This feature is separate from sketch-solver residual tolerances, boolean fuzzy tolerances, mesh resolution, and geometric fitting tolerances.
## Core Workflow
```python
import simplecadapi as scad
housing = scad.var("housing", 100.0, unit="mm", tolerance=0.15)
bearing = scad.var("bearing", 2.0, unit="cm", tolerance=(-0.04, 0.05), tolerance_unit="mm")
spacer = scad.var("spacer", 79.4, unit="mm", tolerance=0.05)
clearance = housing - bearing - spacer
worst_case = scad.analyze_tolerance(clearance, method="worst_case")
rss = scad.analyze_tolerance(clearance, method="rss")
with scad.GraphSession() as session:
body = scad.make_box_rsolid(housing, 10.0, 10.0)
session.require_tolerance(
clearance,
(-0.25, 0.24),
method="worst_case",
name="axial_clearance",
tolerance_unit="mm",
)
report = session.validate_tolerances(raise_on_failure=True)
model_json = scad.export_model_json(session)
```
## Rules
- A scalar tolerance is symmetric: `0.1` means `-0.1/+0.1`.
- A two-value sequence contains signed `(lower_deviation, upper_deviation)` values.
- Every variable in a tolerance chain must declare a finite tolerance.
- `tolerance_unit` defaults to `unit`; different nominal/tolerance units must
have the same dimension.
- Length and angle propagate in canonical `mm` and `deg` units.
- Expression dimensions are inferred and validated before propagation.
- Area and volume can be analyzed, while requirements currently accept final
Length and Angle results only.
- Tolerance sources are identified by `expr_id`, not by variable name.
- `worst_case` uses exact dependency-aware affine propagation and conservative nonlinear interval propagation.
- `rss` uses analytic first-order sensitivities, assumes distinct variables are independent, and combines repeated occurrences of the same variable before RSS.
- Both methods reject a chain that is undefined anywhere inside its declared source intervals.
- Export, import, replay, and FreeCAD translation validate stored requirements automatically.
- Legacy model JSON without `tolerance_graph` imports with an empty graph.
- Nominal replay continues to use numeric operation snapshots; tolerance analysis does not regenerate worst-case geometry.
- Import recomputes dimensions and rejects tampered unit or target-dimension data.
Use `check_tolerance()` for a standalone non-raising check. Use `GraphSession.require_tolerance()` when the requirement must be serialized and enforced.
See the source-checkout guide at `docs/core/dimension-tolerance-chains.md` for propagation details, failure conditions, JSON shapes, and FreeCAD behavior.
@@ -117,7 +117,7 @@ source API 名字不等于 canonical graph op 名字。Composite source API 可
```json
{
"schema_version": "2.0",
"producer_version": "2.0.1b1",
"producer_version": "2.0.2",
"capabilities": {
"selection_ref_strategies": true,
"geo_select_nodes": true,
@@ -334,7 +334,7 @@ source API 名字不等于 canonical graph op 名字。Composite source API 可
- `make_*_face` 与 `make_face_from_wire` -> `Sketch`
- `make_*_edge` / `make_*_wire` -> `Profile`
- `make_extrude_rsolid` / `make_revolve_rsolid` / `make_loft_rsolid` / `make_sweep_rsolid` / `make_fillet_rsolid` / `make_chamfer_rsolid` / `make_shell_rsolid` / `make_cut_rsolid` / `make_union_rsolid` / `make_intersect_rsolid` -> `Feature`
- `make_extrude_rsolid` / `make_revolve_rsolid` / `make_loft_rsolid` / `make_sweep_rsolid` / `make_twisted_sweep_rsolid` / `make_fillet_rsolid` / `make_chamfer_rsolid` / `make_shell_rsolid` / `make_cut_rsolid` / `make_union_rsolid` / `make_intersect_rsolid` -> `Feature`
## 7. Topology Delta Schema
@@ -936,6 +936,7 @@ New canonical profile nodes use the `make_*_r*` names listed in `canonical_contr
- `make_revolve_rsolid`
- `make_loft_rsolid`
- `make_sweep_rsolid`
- `make_twisted_sweep_rsolid`
- `make_translate_rshape`
- `make_rotate_rshape`
- `make_mirror_rshape`
@@ -1296,6 +1297,23 @@ Important:
| --- | --- | --- |
| `is_frenet` | `bool` | sweep orientation mode |
#### `make_twisted_sweep_rsolid`
- Inputs: 1 profile `Face`
- Outputs: 1 `Solid`
- Params:
| Key | Type | Meaning |
| --- | --- | --- |
| `axis` | 3-number array | sweep and rotation-axis direction in caller coordinates |
| `origin` | 3-number array | sweep start and a point on the rotation axis |
| `distance` | positive number | axial sweep distance |
| `twist_angle` | number | signed total rotation in degrees |
| `guide_radius` | positive number | internal auxiliary-spine radius |
Replay rebuilds the auxiliary spine deterministically inside the operation. It
does not infer section counts or lower to transient loft nodes.
### 14.6 Boolean Ops
#### `make_union_rsolid`
@@ -0,0 +1,45 @@
# Physical Units And Dimension Inference
Declare physical meaning on variables:
```python
width = scad.var(
"width",
1.0,
unit="in",
tolerance=0.1,
tolerance_unit="mm",
)
height = scad.var("height", 40.0, unit="mm", tolerance=0.2)
diagonal = scad.sqrt(width**2 + height**2)
```
## Canonical Rules
- Length, area, volume, and angle evaluate in `mm`, `mm^2`, `mm^3`, and `deg`.
- Declaration values remain available on `Var.default` and `Var.tolerance`.
- `tolerance_unit` defaults to `unit` and must have the same dimension.
- Bindings use the variable's declaration unit.
- Numeric constants are dimensionless coefficients; in addition/subtraction they
are contextual offsets in the canonical result unit.
## Inference Rules
- Addition and subtraction require matching dimensions.
- Multiplication/division add/subtract dimension exponents.
- Constant integer powers multiply exponents.
- Square root requires every exponent to be even.
- `sin`, `cos`, and `tan` require Angle and return Dimensionless.
- `asin`, `acos`, and `atan` require Dimensionless and return Angle.
- `atan2` requires matching input dimensions and returns Angle.
- Unit-declared variables cannot mix with legacy variables without units.
Built-in symbols are `1`, `%`, `mm`, `cm`, `m`, `in`, `ft`, their square/cubic
forms, `deg`, and `rad`. Use `get_unit()`, `convert_value()`, and
`infer_dimension()` for explicit inspection.
Area and volume can be inferred and analyzed. Persisted manufacturing requirements
currently require a final Length or Angle result.
Read the source-checkout `docs/core/physical-units.md` for custom-unit JSON,
complete operation tables, failure conditions, and legacy behavior.
@@ -10,14 +10,19 @@ The long-form schema reference remains [`../operation_graph_json_spec.md`](../op
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))
@scad.model(graph_id="drilled_block")
def build_model():
body = scad.make_box_rsolid(width=10, height=6, depth=2)
hole = scad.make_cylinder_rsolid(
radius=1, height=4, bottom_face_center=(0, 0, -1)
)
result = scad.cut_rsolid(body, hole)
scad.capture_result(value=result)
return result
model_json = scad.export_model_json(session)
payload = json.loads(model_json)
rebuilt = scad.replay_model_json(model_json)
model = build_model()
payload = json.loads(model.model_json)
rebuilt = model.replay()
```
Inspect these fields:
@@ -30,6 +35,16 @@ Inspect these fields:
- `payload["leaf_ids"]`: explicit final result node ids.
- `payload["expression_graph"]`: expression DAG used by expression-backed parameters.
For new top-level models, `ModelResult.model_json` is the preferred artifact
accessor. Use `@scad.requires_session` for reusable builders and
`scad.capture_result(...)` to keep intermediate graph leaves out of the model
outputs. If a model invocation also needs durable CAD/viewer files, pass
`export_dir=...` to `@scad.model`; its captured geometry/product values then
produce one self-contained `<graph_id>.scene.zip`. It embeds model JSON, mapped
project-relative Python sources, and evaluated render/selection assets; it does
not create adjacent model/session JSON, STEP, STL, or FCStd files. No files are
written when `export_dir` is omitted.
## 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:
@@ -53,10 +68,10 @@ Many user-facing functions are convenience APIs. During an active `GraphSession`
- [Features, booleans, transforms, patterns, and selectors](features-booleans-transforms.md)
- [Expressions and replay behavior](expressions-and-replay.md)
## Example
## Examples
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`
The retained examples use the same model/session contract. See
[`../../../examples/08_constrained_sketch.py`](../../../examples/08_constrained_sketch.py)
for sketch promotion and replay, and
[`../../../examples/10_part_assembly.py`](../../../examples/10_part_assembly.py)
for product hierarchy and automatic artifact export.
@@ -122,6 +122,40 @@ Replay effect:
2. Replay path wire from input 1.
3. Call `sweep_rsolid(profile, path, is_frenet=...)`.
## Twisted Sweep
Source:
```python
profile = scad.make_rectangle_rface(width=2.0, height=1.0)
solid = scad.twisted_sweep_rsolid(
profile=profile,
distance=8.0,
twist_angle=30.0,
)
```
Serialized node:
```json
{
"op": "make_twisted_sweep_rsolid",
"params": {
"axis": [0.0, 0.0, 1.0],
"origin": [0.0, 0.0, 0.0],
"distance": 8.0,
"twist_angle": 30.0,
"guide_radius": 1.0
},
"inputs": ["node_for_profile_face"],
"output_count": 1
}
```
Replay reconstructs the continuous auxiliary-spine rotation law from the
recorded parameters and invokes `twisted_sweep_rsolid(...)`. No sampled loft
sections are stored or inferred.
## Helical sweep macro lowering
Source:
@@ -2,21 +2,24 @@
## Overview
`TaggedMixin` is the internal tag and metadata storage mixin used by `Vertex`, `Edge`, `Wire`, `Face`, and `Solid`. It owns the shared `_tags`, `_metadata`, and `_runtime` stores for topology wrappers.
`TaggedMixin` is the internal semantic binding and metadata mixin used by topology wrappers. Canonical tag ownership is source-preserving `TagBinding` data. `_tags` is only an effective-scope compatibility cache; user code must not treat it as writable truth.
User code should not call member tag mutators. The public tag API is functional:
- `apply_tag(shape, tag)` attaches one normalized tag.
- `list_tags(shape)` returns tags in deterministic sorted order.
- `apply_tag_rselection(scope, targets, tag, ...)` returns an independent semantic view and exposes explicit propagation policies.
- `list_tags(shape, scope=...)` returns tags in deterministic sorted order.
- `explain_tag(shape, tag, scope=...)` preserves binding and producer evidence.
- `select_faces_by_tag(...)`, `select_edges_by_tag(...)`, and QL predicates such as `ql.tag("role.*")` provide selection/query helpers.
## Tagging Mental Model
- Tags are normalized lowercase dot-separated semantic tokens.
- Examples: `role.mounting_surface`, `anchor.datum.primary`, `group.fasteners`, `face.top`, `edge.boundary`, `wire.outer`, `solid.boolean.cut`.
- `apply_tag(shape, tag)` does not expose propagation controls.
- The standard policy propagates `role.*`, `anchor.*`, `group.*`, and a few legacy bare semantic tags downward.
- Topology-specific tags such as `face.*`, `edge.*`, `wire.*`, `vertex.*`, and `solid.*` stay local.
- New user assignments default to local topology propagation regardless of prefix.
- Downward inheritance is explicit and computed dynamically; bindings are not copied into every child.
- `effective` means local plus inherited and does not include lineage.
- `lineage` requires complete topology history and only follows derivations allowed by the binding policy.
- Numeric dimensions, measurements, and rich descriptive payloads belong in metadata, not tags.
- Geometry builders store structured geometry facts under `metadata["geo"]`.
@@ -34,16 +37,29 @@ top_faces = [face for face in box.get_faces() if "face.top" in scad.list_tags(fa
print(len(top_faces))
```
## Propagation Example
## Explicit Propagation Example
```python
import simplecadapi as scad
body = scad.make_box_rsolid(10, 10, 2)
scad.apply_tag(body, "role.mounting_plate")
body = scad.make_box_rsolid(width=10, height=10, depth=2)
tagged = scad.apply_tag_rselection(
scope=body,
targets=[body],
tag="role.mounting_plate",
topology_propagation=scad.TopologyPropagation.DOWNWARD,
)
face_hits = [face for face in body.get_faces() if "role.mounting_plate" in scad.list_tags(face)]
edge_hits = [edge for edge in body.get_edges() if "role.mounting_plate" in scad.list_tags(edge)]
face_hits = scad.select_faces_by_tag(
solid=tagged,
tag="role.mounting_plate",
scope=scad.TagScope.INHERITED,
)
edge_hits = scad.select_edges_by_tag(
shape=tagged,
tag="role.mounting_plate",
scope=scad.TagScope.INHERITED,
)
print(len(face_hits), len(edge_hits))
```
@@ -55,7 +71,13 @@ Primitives and modeling operations may attach normalized tags automatically:
- Primitive tags such as `geom.primitive.box`, `geom.primitive.cylinder`, and `geom.primitive.sphere`.
- Face tags from `auto_tag_faces(...)`, such as `face.top`, `face.bottom`, `face.side`, and `face.surface`.
- Wire tags such as `wire.outer` and `wire.inner`.
- Operation/tracking tags such as `solid.boolean.cut`, `op.cut.modified`, or `op.extrude.generated`.
- Operation-level categorical tags such as `solid.boolean.cut` may remain local annotations.
Operation events and source roles are not tags. Proven `preserved`, `modified`,
or `generated` events and `body`/`tool` origins live in typed
`metadata["track"]`. Query them with `ql.operation_event(...)` and
`ql.origin_role(...)`. Missing correspondence remains `coverage="partial"` and
`status="unknown"`; it is never promoted to `generated` by default.
## Metadata Methods
@@ -84,7 +106,7 @@ scad.apply_tag(body, "role.mounting_plate")
body.auto_tag_faces("box")
top_faces = Q.select(body.get_faces()).where(Q.tag("face.top")).all()
role_faces = Q.select(body.get_faces()).where(Q.tag("role.*")).all()
role_faces = Q.select(body.get_faces()).where(Q.tag("role.*", scope="effective")).all()
print(len(top_faces), len(role_faces))
```
@@ -4,12 +4,12 @@ This index includes generated docs for standard part factory functions. Use thes
## Import Surfaces
- Recommended package-level module export: `import simplecadapi as scad`, then call functions through submodules such as `scad.std.gear.<function>(...)` and `scad.std.bearing.<function>(...)`.
- Recommended package-level module export: `import simplecadapi as scad`, then call functions through submodules such as `scad.std.gear.<function>(...)`, `scad.std.fastener.<function>(...)`, and `scad.std.bearing.<function>(...)`.
- Direct submodule import is also supported, for example `from simplecadapi.std.gear import make_spur_gear_rsolid` or `from simplecadapi.std.bearing import make_ball_bearing_rassembly`.
## Usage Guidance
- Prefer standard-library factories for standard bearings, gears, ring gears, and racks before hand-modeling profiles with core geometry APIs.
- Prefer standard-library factories for standard bearings, fasteners, spur gears, straight bevel gears, ring gears, and racks before hand-modeling profiles with core geometry APIs.
- Standard parts return normal SimpleCAD shapes or product assemblies, so they can be transformed, tagged, assembled, exported, or combined with core geometry operations.
- Switch to core geometry APIs only when the requested standard part needs substantial custom geometry beyond the factory parameters.
@@ -23,6 +23,19 @@ This index includes generated docs for standard part factory functions. Use thes
- [make_herringbone_gear_rsolid](make_herringbone_gear_rsolid.md) *(from std/gear.py)* `stdlib`
- [make_spur_gear_rsolid](make_spur_gear_rsolid.md) *(from std/gear.py)* `stdlib`
## Bevel Gears
- [make_straight_bevel_gear_rsolid](make_straight_bevel_gear_rsolid.md) *(from std/gear.py)* `stdlib`
## Roller Chain
- [make_roller_chain_sprocket_rsolid](make_roller_chain_sprocket_rsolid.md) *(from std/chain.py)* `stdlib`
## Fasteners
- [make_bolt_rsolid](make_bolt_rsolid.md) *(from std/fastener.py)* `stdlib`
- [make_nut_rsolid](make_nut_rsolid.md) *(from std/fastener.py)* `stdlib`
## Internal Ring Gears
- [make_helical_ring_gear_rsolid](make_helical_ring_gear_rsolid.md) *(from std/gear.py)* `stdlib`
@@ -0,0 +1,41 @@
# make_bolt_rsolid
## API Definition
```python
def make_bolt_rsolid(
diameter: float,
length: float,
head_style: str = "hex",
thread_style: str = "auto",
thread_detail: str = "modeled",
thread_form: str = "v",
thread_pitch: Optional[float] = None,
thread_depth: Optional[float] = None,
thread_length: Optional[float] = None,
head_width: Optional[float] = None,
head_height: Optional[float] = None,
drive_style: str = "none",
drive_size: Optional[float] = None,
drive_depth: Optional[float] = None,
underhead_fillet_radius: Optional[float] = None,
) -> Solid
```
*Source: std/fastener.py*
## Import Surface
- standard library: `import simplecadapi as scad` then `scad.std.fastener.make_bolt_rsolid(...)`; direct submodule import: `from simplecadapi.std.fastener import make_bolt_rsolid`
## Description
Create a parameterized bolt along `+Z`, with the head underside on `Z=0`. Head styles are `hex`, `square`, `cylindrical`, `button`, and `countersunk`. Drive styles are `none`, `slot`, `cross`, and `hex_socket`.
Thread styles are `auto`, `full`, `partial`, and `none`. `auto` selects full thread for `length <= 3 * diameter`; otherwise it uses the ISO-style piecewise partial-thread length. The default `thread_detail="modeled"` creates a visible replayable helical `v` or `trapezoidal` thread. Set `thread_detail="cosmetic"` explicitly for a smooth shank with thread intent only in metadata. For partial threads, `thread_length` is measured back from the tip at `Z=length`.
For metric coarse-series diameters, the factory derives the default pitch from the standard series and records `d2 = d - 0.6495P` and `d1 = d - 1.0825P` in metadata. Hex-head defaults use catalog-like `S` and `k` values where available. The default underhead fillet is `0.06d`; `underhead_fillet_radius` can override it. The metadata also reports a minimum recommended mating-hole chamfer equal to the fillet radius.
Modeled threads may rotate the helical seam to an equivalent kernel-stable phase. The selected phase is recorded as `thread_phase_degrees` in `std.fastener.bolt` metadata and does not change thread dimensions or handedness.
Default dimensions are useful parametric proportions, not a claim of compliance with a specific ISO, DIN, ASME, or supplier catalog. Set catalog dimensions explicitly for released hardware.
@@ -16,10 +16,9 @@ def make_helical_gear_rsolid(n_teeth: int, module: float, pressure_angle: float
Create an involute helical gear.
Non-zero helix angles are modeled as small-step ruled lofts through rotated
copies of one profile. The small angular step keeps closed-wire section
correspondence stable while ruled faces avoid smooth loft bulging in STEP
exports.
Non-zero helix angles use a continuous twisted sweep along the gear axis. An
auxiliary-spine rotation law preserves the profile while generating one
continuous side face per profile edge instead of one face per loft interval.
## Parameters
@@ -16,10 +16,8 @@ def make_herringbone_gear_rsolid(n_teeth: int, module: float, pressure_angle: fl
Create an involute herringbone (double-helical) gear.
Each half is modeled as a small-step ruled loft through rotated copies of
one profile, with a shared center section forming the herringbone ridge.
This keeps closed-wire section correspondence stable while avoiding smooth
loft bulging in STEP exports.
Each half uses a continuous twisted sweep with opposite handedness. The two
halves share the rotated center profile and are fused into one solid.
## Parameters
@@ -0,0 +1,35 @@
# make_nut_rsolid
## API Definition
```python
def make_nut_rsolid(
diameter: float,
width: float,
height: float,
nut_style: str = "hex",
hole_style: str = "through",
thread_detail: str = "modeled",
thread_form: str = "v",
thread_pitch: Optional[float] = None,
thread_depth: Optional[float] = None,
hole_depth: Optional[float] = None,
knurl_count: int = 24,
) -> Solid
```
*Source: std/fastener.py*
## Import Surface
- standard library: `import simplecadapi as scad` then `scad.std.fastener.make_nut_rsolid(...)`; direct submodule import: `from simplecadapi.std.fastener import make_nut_rsolid`
## Description
Create a parameterized nut along `+Z`, with its bottom face on `Z=0`. Nut styles are `hex`, `square`, `round`, and `knurled`. Hole styles are `through` and `blind`; a blind hole opens from the top face and uses `hole_depth` as its axial depth.
The default `thread_detail="modeled"` creates replayable internal `v` or `trapezoidal` helical teeth. Set `thread_detail="cosmetic"` explicitly for a smooth major-diameter hole with thread intent only in metadata. For metric coarse-series diameters, the factory derives the default pitch and records `d2 = d - 0.6495P` and `d1 = d - 1.0825P` in metadata. `knurl_count` controls the lobe count of the printable knurled approximation.
Modeled threads may rotate the helical seam to an equivalent kernel-stable phase. The selected phase is recorded as `thread_phase_degrees` in `std.fastener.nut` metadata and does not change thread dimensions or handedness.
Default thread proportions are useful for parametric models, not a substitute for catalog tolerances, thread class, lead-in, prevailing-torque features, or manufacturing checks.
@@ -0,0 +1,19 @@
# make_roller_chain_sprocket_rsolid
## API Definition
```python
def make_roller_chain_sprocket_rsolid(n_teeth: int, chain_pitch: float, roller_diameter: float, sprocket_thickness: float, *, bore_radius: float = 0.0, roller_clearance: float = 0.15) -> Solid
```
*Source: std/chain.py*
## Import Surface
- standard library: `import simplecadapi as scad` then `scad.std.chain.make_roller_chain_sprocket_rsolid(...)`; direct submodule import: `from simplecadapi.std.chain import make_roller_chain_sprocket_rsolid`
## Description
Create a roller-chain sprocket from tooth count, chain pitch, roller diameter, and tooth-plate thickness. The pitch radius follows the regular pitch polygon. Circular roller seats are cut at every pitch point and opened through the engineering outside-diameter envelope.
The result preserves assembly-level engagement dimensions. Manufacturing release still requires the selected chain standard's permitted tooth-form range, tooth width, hub, material, heat treatment, runout, and supplier checks.
@@ -0,0 +1,30 @@
# make_straight_bevel_gear_rsolid
## API Definition
```python
def make_straight_bevel_gear_rsolid(n_teeth: int, module: float, pitch_angle: float = 45.0, pressure_angle: float = 20.0, face_width: float = 8.0, *, addendum_factor: float = 1.0, clearance_factor: float = 0.25, backlash: float = 0.0) -> Solid
```
*Source: std/gear.py*
## Import Surface
- standard library: `import simplecadapi as scad` then `scad.std.gear.make_straight_bevel_gear_rsolid(...)`; direct submodule import: `from simplecadapi.std.gear import make_straight_bevel_gear_rsolid`
## Description
Create a straight bevel gear with standard metric tooth proportions. The large-end transverse section uses an analytic involute profile. A similar small-end section is placed on the pitch cone and connected with ruled straight tooth surfaces.
The returned solid contains nominal tooth geometry. Releasing a mating pair still requires mounting-distance, contact-pattern, backlash, material, heat-treatment, and strength checks.
## Parameters
- `n_teeth`: Number of teeth, at least 3.
- `module`: Large-end transverse module in millimetres.
- `pitch_angle`: Pitch-cone angle in degrees, greater than 0 and less than 90.
- `pressure_angle`: Transverse pressure angle in degrees.
- `face_width`: Tooth face width along the pitch-cone generator in millimetres; must be smaller than the outer pitch-cone distance.
- `addendum_factor`: Large-end tooth addendum divided by module.
- `clearance_factor`: Root clearance beyond the addendum divided by module.
- `backlash`: Large-end circumferential tooth-thickness reduction at the pitch circle in millimetres.
@@ -0,0 +1,761 @@
# STEP BREP 逆向工程方法
本文记录一种面向精确 BREP 的 STEP 逆向工作流。目标不是做出“看起来相似”的模型,而是恢复一个满足以下硬约束的可编辑参数模型:
1. 几何点集一致;
2. BREP 拓扑一致;
3. 解析曲面的参数一致;
4. 边界曲线和关键参数范围一致;
5. 模型能够稳定重建和回放。
当多种特征树都满足这些硬约束时,再用工程师的建模习惯筛选:优先对称、整数或标准尺寸、少量草图、稳定基准、清晰特征依赖和容易修改的参数。
## 1. 先区分五种“一致”
STEP 逆向中最容易犯的错误,是把不同层次的一致混为一谈。
### 1.1 外观一致
截图、网格或渲染结果相似。它只能用于快速发现大方向错误,不能用于验收。
截图看不出:
- 两个相邻面是否已合并;
- 一个圆柱面是整圆还是两个半圆;
- 隐藏孔、沉孔深度和内部台阶;
- 半径的微小差异;
- BREP 邻接关系;
- 曲面的参数原点和方向。
### 1.2 数值摘要一致
包围盒、体积、表面积、质心一致。这比外观更强,但仍不充分。不同实体可能碰巧拥有相同的体积和包围盒。
用途:
- 快速检查单位和坐标系;
- 定位哪个特征导致体积误差;
- 验证孔、槽、台阶的总体材料增减;
- 在迭代过程中排除明显错误。
### 1.3 几何点集一致
两个实体占据完全相同的空间。对实体可用双向布尔差集验证:
```text
A - B 的体积 = 0
B - A 的体积 = 0
```
这是几何一致的硬检查,但它不保证拓扑一致。例如一个完整圆柱面和两个半圆柱面可以表示相同点集。
### 1.4 BREP 拓扑一致
面、边、顶点数量相同,并且面—边—顶点邻接图同构。
仅比较数量还不够。两个模型都可能有 20 个面,但连接方式不同。可靠做法是给图节点添加几何标签,再检查图同构:
- 面标签:类型、面积、几何中心、半径、轴线、球心等;
- 边标签:类型、长度、几何中心、半径、轴线等;
- 顶点标签:三维坐标;
- 图边:Face—Edge 和 Edge—Vertex 关联。
### 1.5 参数化表示一致
同一个几何载体仍可能具有不同参数表示:
- 圆柱轴方向相反;
- 平面原点不同;
- 圆弧起始角不同;
- UV 范围平移或反向;
- 一条整圆边与两条半圆边;
- 相同孔通过不同特征顺序产生不同的曲面参数原点。
有些参数差异只是非唯一表示,不应误判为几何差异;有些则是建模历史的有效证据。必须结合设计语义判断。
## 2. 检查手段速查表
| 检查手段 | 最适合回答的问题 | 能提供的直觉 | 不能单独证明什么 |
|---|---|---|---|
| STEP 实体类型计数 | 文件主要由什么几何组成 | 是机械解析体、自由曲面还是混合模型 | 导入后的唯一拓扑数量 |
| BREP 有效性 | 文件能否作为可靠实体继续运算 | 是否存在开壳、坏边、容差问题 | 几何是否符合设计意图 |
| 唯一 Face/Edge/Vertex 数量 | 拓扑复杂度和分段程度 | 圆是否被拆弧、面是否被切分 | 邻接关系是否一致 |
| 拓扑出现次数 | 面如何重复引用边和顶点 | 是否像闭合流形、是否有非流形连接 | 两模型是否同构 |
| 包围盒 | 单位、整体尺寸和主方向 | 哪个方向可能是厚度或长度 | 内部孔槽和局部形状 |
| 体积/面积/质心 | 材料增减是否正确 | 漏掉的是大特征还是表面细节 | 几何点集和拓扑一致 |
| 解析面类型统计 | 特征家族是否正确 | 孔、倒角、圆角、自由曲面来源 | 每个面的具体位置和连接 |
| 面参数与包围盒 | 单个特征的尺寸和位置 | 半径、轴线、台阶、角度、深度 | 建模历史唯一解 |
| 边类型与端点 | 草图分段和切点 | 整圆/半圆、45 度线、圆角切线 | 隐藏面的完整形状 |
| 面-边-顶点邻接图 | BREP 连接结构是否一致 | 哪个局部特征造成分段差异 | 几何点集必然一致 |
| 双向布尔差集 | 两实体是否占据相同空间 | 几何已经完成,可转向拓扑检查 | 面分段和参数化一致 |
| 多视图截图 | 大方向和明显特征是否正确 | 最快发现轴向、孔位、台阶错误 | 精确尺寸、内部结构和拓扑 |
| 物理截面 | 隐藏结构和不同深度轮廓 | 沉孔、槽、壁厚、局部层级 | 未采样位置必然一致 |
| 截面 XOR | 候选差异出现在哪个区域 | 直接定位下一步应修改的特征 | 连续空间上的严格零差异 |
| UV/曲线参数范围 | 参数历史和特征顺序 | 工具从哪个基准面开始、何时被后续特征裁剪 | 非唯一坐标架必然相同 |
| 模型图回放 | 建模过程能否稳定复现 | 是否依赖交互状态或临时对象 | 回放结果与目标自动一致 |
推荐检查顺序是从便宜到昂贵:
```text
有效性和摘要
-> 解析几何清点
-> 多视图和少量关键截面
-> 面边参数与邻接
-> 候选构建
-> 双向差集
-> 图同构
-> 参数历史
-> 回放
```
不要一开始就对所有点做高密度分类,也不要一开始就逐个比较 STEP 文本实体。前者成本高且信息增量有限,后者会被导出器编号和非唯一参数化干扰。
## 3. 推荐的分阶段工作流
### 3.1 阶段 A:建立不可变基准
在写建模代码前,先从原始 STEP 导出一份机器可读报告。可以先用文本清点快速了解文件组成:
```bash
rg -o '^[[:space:]]*#[0-9]+[[:space:]]*=[[:space:]]*[A-Z0-9_]+' model.step \
| sed -E 's/.*=[[:space:]]*//' \
| sort \
| uniq -c \
| sort -nr
```
这能快速发现 `ADVANCED_FACE`、`PLANE`、`CYLINDRICAL_SURFACE`、`B_SPLINE_SURFACE_WITH_KNOTS` 等实体,但它只是 STEP 表示层清点。`EDGE_CURVE` 数量不一定等于内核导入后的唯一 Edge 数量;退化边、缝边和表示方式都会造成差异。最终基准必须来自导入后的 BREP。
机器可读报告至少包括:
- STEP 是否能读取;
- 转移根数量;
- BREP 是否有效;
- Solid、Shell、Face、Edge、Vertex 数量;
- 唯一拓扑数量和拓扑出现次数;
- 包围盒、体积、表面积、质心;
- 面类型统计;
- 边类型统计;
- 每个面的面积、UV 范围、解析曲面参数和邻接边;
- 每条边的类型、参数范围和邻接面。
唯一数量和出现次数必须分开:
```text
unique_edges BREP 中不同的边对象数量
edge_occurrences 遍历所有面时边被引用的总次数
```
在闭合流形实体中,一条普通边通常被两个面引用,因此出现次数通常接近唯一边数的两倍。
正式实现位于:
```text
src/simplecadapi/inverse_engineer/brep/inspect.py
```
Python API:
```python
from simplecadapi.inverse_engineer import brep
report = brep.inspect_step(path="model.step")
report.write_json("model-report.json")
```
CLI:
```bash
uv run simplecad-brep inspect model.step -o model-report.json
```
### 基础属性代码片段
```python
from OCP.BRepCheck import BRepCheck_Analyzer
from OCP.BRepGProp import BRepGProp
from OCP.BRepBndLib import BRepBndLib
from OCP.Bnd import Bnd_Box
from OCP.GProp import GProp_GProps
assert BRepCheck_Analyzer(shape).IsValid()
bbox = Bnd_Box()
BRepBndLib.AddOptimal_s(shape, bbox)
print("bbox", bbox.Get())
volume_props = GProp_GProps()
BRepGProp.VolumeProperties_s(shape, volume_props)
print("volume", volume_props.Mass())
print("center", volume_props.CentreOfMass().Coord())
area_props = GProp_GProps()
BRepGProp.SurfaceProperties_s(shape, area_props)
print("area", area_props.Mass())
```
### 3.2 阶段 B:恢复坐标语义
不要根据 `X/Y/Z` 名称直接假设左右、前后和高度。
应综合使用:
- 包围盒三个跨度;
- 主成分分析;
- 平面和圆柱轴线分布;
- 对称平面拟合;
- 正交视图和典型截面;
- 机械功能,例如孔轴通常对应装配或加工方向。
坐标语义判断错误会导致整个特征解释反转。比如一个方向上的 U 形截面可能是左右对称结构,也可能被错误解释为前后凹槽。
### 3.3 阶段 C:做解析几何清点
解析面类型是最有价值的“建模指纹”之一。
| 几何类型 | 常见来源 | 可恢复的信息 |
|---|---|---|
| Plane | 拉伸端面、切割平面、倒角 | 基准方向、台阶位置、厚度、角度 |
| Cylinder | 孔、轴、拉伸圆弧、直边圆角 | 半径、轴线、深度、圆弧分段 |
| Sphere | 三条圆角边交汇、球切割 | 圆角半径、特征交汇方式 |
| Torus | 圆弧轮廓的边圆角、环形过渡 | 主半径、次半径、圆角中心 |
| Cone | 锥孔、沉头、拔模 | 锥角、轴线、顶点 |
| BSpline | 放样、填充、自由曲面 | 次数、节点、控制点、连续性 |
例如:
```text
4 个 R1 Sphere
4 个 (R2, R1) Torus
14 个 R1 Cylinder
```
通常不是手工拼出的 22 个面,而是一次连续局部 R1 圆角的结果。解析面组合比截图更能揭示特征操作。
### 读取解析曲面参数
```python
from OCP.BRepAdaptor import BRepAdaptor_Surface
surface = BRepAdaptor_Surface(face, True)
kind = surface.GetType()
cylinder = surface.Cylinder()
print("radius", cylinder.Radius())
print("axis", cylinder.Axis().Direction().Coord())
print("origin", cylinder.Location().Coord())
sphere = surface.Sphere()
print("radius", sphere.Radius())
print("center", sphere.Location().Coord())
torus = surface.Torus()
print("major", torus.MajorRadius())
print("minor", torus.MinorRadius())
```
### 3.4 阶段 D:从平面和边恢复尺寸关系
不要只读包围盒。每个解析面的平面方程、轴线和边界端点能够恢复:
- 台阶高度;
- 壁厚;
- 孔深;
- 孔距;
- 对称位置;
- 45 度倒角;
- 圆角切点;
- 草图中的直线方程。
例如一组斜面端点满足:
```text
x + |z| = 1.25
```
这比“看起来大约 45 度”更强,说明草图很可能使用了对称 45 度约束。
### 读取边端点和类型
```python
from OCP.BRepAdaptor import BRepAdaptor_Curve
curve = BRepAdaptor_Curve(edge)
first = curve.FirstParameter()
last = curve.LastParameter()
p0 = curve.Value(first)
p1 = curve.Value(last)
print("type", curve.GetType())
print("start", p0.Coord())
print("end", p1.Coord())
if "Circle" in str(curve.GetType()):
circle = curve.Circle()
print("radius", circle.Radius())
print("center", circle.Location().Coord())
```
### 3.5 阶段 E:建立直观图像,但只让它承担合适的职责
推荐同时生成:
1. 等轴视图;
2. 三个正交视图;
3. 关键高度或轴向位置的占用截面;
4. 目标/候选叠加图;
5. XOR 差异图。
多视图适合发现:
- 坐标方向错误;
- 漏掉大特征;
- 孔位错误;
- 台阶方向错误;
- 圆角范围过大;
- 草图轮廓错误。
截面图适合发现:
- 内孔层级;
- 沉孔深度;
- 不同高度处的轮廓变化;
- 隐藏槽;
- 两实体的局部差异。
正式实现:
```text
src/simplecadapi/inverse_engineer/brep/render.py
src/simplecadapi/inverse_engineer/brep/slices.py
```
渲染图片需要可选依赖:
```bash
uv sync --extra inverse-engineer
```
Python API:
```python
from simplecadapi.inverse_engineer import brep
brep.render_step_views(step_path="candidate.step", output_path="candidate-views.png")
comparison = brep.compare_step_slices(
target_path="target.step",
candidate_path="candidate.step",
output_path="slice-overlay.png",
)
```
CLI:
```bash
uv run simplecad-brep render candidate.step candidate-views.png
uv run simplecad-brep slices target.step candidate.step slice-overlay.png
# 默认自动检查联合包围盒的 X/Y/Z 中心截面;特征边界应显式追加:
uv run simplecad-brep slices target.step candidate.step feature-slices.png \
--slice xz:-1.6 --slice xz:4.0 --slice xy:0
```
截面分类示例:
```python
from OCP.BRepClass3d import BRepClass3d_SolidClassifier
from OCP.TopAbs import TopAbs_IN, TopAbs_ON
from OCP.gp import gp_Pnt
classifier = BRepClass3d_SolidClassifier()
classifier.Load(shape)
classifier.Perform(gp_Pnt(x, y, z), 1e-8)
inside = classifier.State() in (TopAbs_IN, TopAbs_ON)
```
默认截面只用于快速建立直觉。逆向时应进一步选择穿过特征边界、台阶高度、沉孔肩部、对称面和疑似差异区域的物理截面。
注意:逐点 BREP 分类较慢。截面图是诊断工具,应控制分辨率;几何硬验收应使用布尔差集,而不是靠提高图片分辨率。
### 3.6 阶段 F:提出最小特征假设
先写出一个人类可读的候选特征树,不要马上编码大量细节。
好的候选通常包含:
- 一个主草图和一次拉伸;
- 对称约束或镜像;
- 少量孔、槽、台阶;
- 局部圆角和倒角;
- 清晰的加工顺序。
每个参数都应能由原始 BREP 的证据解释。例如:
```text
R1 来自 14 个 R1 圆柱、4 个 R1 球和 4 个次半径 R1 的圆环
R3.25 来自沉孔圆柱
R1.7 来自贯穿孔圆柱
X=3.25 来自台阶平面
Z=±6 来自对称孔轴
```
避免在没有证据时使用:
- 随机小偏移;
- 大量补偿布尔体;
- 任意样条;
- 网格拟合;
- 按最终面逐面拼壳。
### 3.7 阶段 G:按特征逐步落地
每一步都打印少量有意义的基准:
```python
print(
"ground opening fillet",
body.get_volume(),
len(body.get_faces()),
len(body.get_edges()),
)
```
推荐顺序:
1. 主轮廓;
2. 主拉伸;
3. 大型切除或并集;
4. 台阶;
5. 孔;
6. 圆角和倒角,或根据几何指纹调整到正确历史位置;
7. 输出 STEP;
8. 生成报告和截图;
9. 对比;
10. 只修改造成当前差异的特征。
不要一次添加所有特征。否则拓扑不一致时无法知道是哪一步造成的。
## 4. 从差异反推建模历史
### 4.1 面数少但几何相同:检查草图分段
典型情况:
```text
目标:每个孔壁由两个半圆柱面组成
候选:每个孔壁是一个完整圆柱面
```
二者几何点集相同,但拓扑不同。原因通常不是布尔算法随机拆面,而是草图边界不同:
```text
完整圆边拉伸 -> 一个圆柱侧面
两条半圆弧闭合拉伸 -> 两个半圆柱侧面
```
因此,拓扑差异常常能反推草图实体数量。
### 4.2 面数多且出现额外平面:检查错误的布尔拆分
把一个圆柱工具拆成左右两个半圆实体分别切除,会得到两个半圆柱面,但也会引入径向分隔平面。
这是一条重要经验:
> 想得到分段侧面,应在草图边上分段,而不是把实体工具分成多个带内部平面的实体。
### 4.3 几何和拓扑相同但 UV 不同:检查特征顺序
典型沉孔:
```text
方案 A:先沉孔,再从肩部开始切小孔
方案 B:先切贯穿小孔,再切大沉孔
```
两者最终几何可能相同,但小孔圆柱支撑面的参数原点不同。
如果目标小孔的圆柱原点位于入口平面,UV 还保留了被沉孔切掉的参数区间,这通常说明:
```text
先贯穿孔
再沉孔
```
特征顺序不仅影响工程可读性,也会影响 STEP 中保留的曲面参数。
### 4.4 球面和圆环面组合:检查三维圆角交汇
二维草图圆角后拉伸,常产生圆柱面;对三维实体的多条相交边做圆角,会进一步产生球面和圆环面。
因此:
- 只有圆柱圆角面:可能是二维圆角轮廓拉伸;
- 同时出现球面和圆环面:更像三维成组选边圆角;
- 球面半径和圆环次半径相同:通常直接给出圆角半径。
## 5. 严格验收方法
### 5.1 双向布尔差集
```python
from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut
from OCP.BRepGProp import BRepGProp
from OCP.GProp import GProp_GProps
def cut_volume(a, b):
operation = BRepAlgoAPI_Cut(a, b)
operation.SetFuzzyValue(1e-9)
operation.Build()
props = GProp_GProps()
BRepGProp.VolumeProperties_s(operation.Shape(), props)
return props.Mass()
assert cut_volume(target, candidate) < 1e-9
assert cut_volume(candidate, target) < 1e-9
```
这证明实体点集相同,但仍需拓扑检查。
### 5.2 几何标记的邻接图同构
正式实现位于:
```text
src/simplecadapi/inverse_engineer/brep/compare.py
```
Python API:
```python
from simplecadapi.inverse_engineer import brep
comparison = brep.compare_steps(target_path="target.step", candidate_path="candidate.step")
assert comparison.hard_gate_passed
```
CLI:
```bash
uv run simplecad-brep compare target.step candidate.step -o comparison.json
```
报告应同时满足:
```text
same_geometric_point_set = true
geometry_labelled_incidence_graph_isomorphic = true
```
### 规范化非唯一参数
比较几何载体时,不要直接比较平面原点或轴向符号。
平面可规范为:
```text
单位法向 n
有符号距离 d = n dot p
```
轴线可规范为:
```text
单位方向 d,统一符号
原点到世界原点的最近点 p - d(d dot p)
```
这样可以识别“同一个平面但原点不同”和“同一根轴但方向相反”。
### 5.3 参数化表示检查
完成几何和拓扑硬检查后,再检查:
- UV 范围;
- 圆柱、圆锥、圆环坐标架;
- 圆弧参数范围;
- B 样条次数、节点、重数、控制点和权重;
- 边方向和面方向;
- 曲面原点是否反映预期特征顺序。
不要把所有参数差异都当成错误。先问:
1. 这是非唯一坐标架选择吗?
2. 它是否改变几何点集?
3. 它是否改变 BREP 邻接?
4. 它是否揭示不同的草图分段或特征顺序?
5. 用户要求的是精确 BREP,还是 STEP 文本字节一致?
STEP 实体编号和文本顺序通常不属于 BREP 本体,不应作为几何逆向的默认验收项。
### 5.4 回放验收
逆向模型不能只在当前 Python 进程中成功。应记录模型图,重新播放,再对回放结果执行相同检查。
```python
import simplecadapi as scad
with scad.GraphSession(graph_id="reverse_part") as session:
body = build_body()
scad.capture_result(value=body)
payload = scad.export_model_json(session)
rebuilt = scad.replay_model_json(payload)[0]
scad.export_step(rebuilt, "replayed.step")
```
需要分别比较:
```text
原始 vs 直接构建
原始 vs 回放构建
直接构建 vs 回放构建
```
## 6. 高效迭代策略
### 6.1 用差异类型决定下一步
| 当前差异 | 优先检查 |
|---|---|
| 包围盒错误 | 坐标系、单位、基准位置 |
| 体积差很大 | 漏掉主切除/并集、特征方向错误 |
| 体积接近但面积差大 | 圆角、倒角、孔壁或隐藏台阶 |
| 几何相同但面数少 | 草图边分段不足、同域面被合并 |
| 几何相同但面数多 | 多余切割平面、重复布尔、错误拆工具 |
| Sphere/Torus 数量错误 | 圆角选边范围或圆角时序 |
| Cylinder 数量错误 | 圆/圆弧草图分段、孔特征顺序 |
| 拓扑计数相同但图不同 | 面邻接、边界方向、错误局部连接 |
| UV 原点或范围错误 | 特征创建顺序、工具起始平面 |
### 6.2 先匹配低维输入,再匹配高维结果
工程特征一般遵循:
```text
点/尺寸 -> 草图边 -> 闭合轮廓 -> 面 -> 拉伸/旋转 -> 布尔 -> 圆角
```
当最终圆柱面分段不对时,优先检查草图圆弧数量,而不是在最终实体上补切割面。低维输入通常才是拓扑来源。
### 6.3 用解析面统计做中间目标
完整拓扑还没对齐时,解析面统计是非常有效的阶段目标。例如:
```text
目标:Plane 16, Cylinder 22, Sphere 4, Torus 4
```
候选若为:
```text
Plane 16, Cylinder 18, Sphere 4, Torus 4
```
说明主体、台阶和圆角网络很可能已正确,剩余问题集中在圆柱类特征,不应重做整个主体。
### 6.4 保存候选,但不要让试验污染最终入口
建议保留:
- 每轮 STEP;
- 每轮报告;
- 每轮截图;
- 差异日志;
- 被否定的假设及原因。
最终应收敛为一个独立、可读、可回放的入口,不能依赖多个临时试验脚本互相导入。
## 7. 本次实践中走过的弯路
### 7.1 只根据截图猜主特征
问题:截图遮挡内部结构,容易把缺口、孔和圆角顺序解释错。
改进:截图只用于提出假设;立即用解析曲面、面边界和截面验证。
### 7.2 对所有边做圆角
问题:`fillet(all_edges)` 产生了过多球面,且破坏目标的平面和圆环统计。
改进:根据设计语义分组选边。先识别“左侧开口轮廓”这一语义组,再验证它生成的解析面组合。
### 7.3 用两个半圆实体切出分段孔
问题:虽然圆柱面数量增加到目标值,却额外产生了两个径向平面。
改进:用两条半圆弧组成一个闭合草图,再做一次拉伸切除。
### 7.4 把体积和面积一致当作完成
问题:完整圆柱孔和两个半圆柱孔具有相同几何、体积和面积,但拓扑不同。
改进:加入唯一拓扑数量、解析面统计和邻接图同构。
### 7.5 直接比较平面原点和轴向
问题:同一个平面可选择任意平面内原点,同一根轴可反向,导致大量假差异。
改进:比较规范化平面方程和规范化轴线,不比较任意坐标架选择。
### 7.6 截面采样过密
问题:逐点 BREP 分类成本高,六张高分辨率截面会超时。
改进:截面图保持适中分辨率用于直观诊断;精确几何一致由双向布尔差集负责。
### 7.7 先做沉孔再从肩部做小孔
问题:最终几何和拓扑正确,但小孔圆柱参数原点与目标不同。
改进:先从入口平面做贯穿孔,再做沉孔。这个顺序既符合常见加工意图,也复现目标 UV 历史。
## 8. 推荐的自动化产物
每个逆向任务建议固定生成:
```text
reference_report.json
candidate_report.json
candidate.step
candidate_replayed.step
candidate.model.json
candidate_views.png
candidate_slice_overlay.png
candidate_brep_compare.json
```
最终验收至少包括:
```text
BREP valid
双向差集为零
唯一 Face/Edge/Vertex 数量一致
解析面和边类型统计一致
几何标记邻接图同构
关键解析参数一致
模型图可重放
回放结果再次通过全部检查
```
## 9. 核心方法总结
最有效的逆向顺序不是“看图后直接建模”,而是:
```text
建立精确基准
-> 恢复坐标语义
-> 清点解析几何
-> 从平面、轴线和边界恢复尺寸关系
-> 用多视图与截面建立直觉
-> 提出最小工程特征树
-> 逐特征构建和输出
-> 用差异类型指导下一轮
-> 双向差集验证几何
-> 几何标记图验证拓扑
-> UV/曲线参数验证特征历史
-> 模型图回放并重复全部验收
```
最重要的判断原则是:
> 几何和拓扑是硬约束;工程师先验只用于在满足硬约束的多种过程之间做选择。解析几何告诉我们“是什么”,拓扑分段告诉我们“草图如何画”,参数范围和坐标架经常告诉我们“特征按什么顺序做”。