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
+17
View File
@@ -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
+11 -12
View File
@@ -16,16 +16,15 @@ 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 when composing
or testing lower-level graph workflows. `result_node_ids` reports the graph
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)
```
+64
View File
@@ -0,0 +1,64 @@
# 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 keeps the ordinary Python return value together with the owned session and
the durable graph artifacts produced for that model invocation.
## Attributes
- `value`: The value returned by the model function. It may be a shape, a
product assembly, or a tuple containing application-level reports and a
captured preview.
- `session`: The completed `GraphSession` that recorded the model.
- `result_node_ids`: Explicitly captured graph node ids used as model outputs.
- `model_json`: Canonical low-level operation graph JSON containing the captured
result leaves.
- `session_json`: Session JSON containing the complete session state.
- `artifact_paths`: Files written by automatic export. It contains the `scene`
key when captured geometry or product values produced a Scene ZIP.
## Artifact Export
```python
result = build_model()
exported = result.export_artifacts(output_dir="examples/out/bracket")
print(exported.artifact_paths["scene"])
```
The same export runs automatically when `@scad.model(export_dir=...)` is used.
Only values explicitly passed to `capture_result(...)` are considered final
geometry/product outputs. Automatic export writes one self-contained
`<graph_id>.scene.zip` containing the model JSON, mapped Python sources, and
render/selection assets; it does not write adjacent model/session JSON, STEP,
STL, or FCStd files. Without an export directory, no files are written.
## Replay
```python
result = build_model()
rebuilt = result.replay()
```
`replay()` is equivalent to replaying `result.model_json`. Pass
`strict=False` only when intentionally relaxing replay validation.
+54
View File
@@ -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`
+9
View File
@@ -22,3 +22,12 @@ 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, not geometry guesses. During
`make_wire_from_sketch_rwire(...)` or `make_face_from_sketch_rface(...)`, the
promotion map binds each ordered profile entity to exactly one generated Edge.
The canonical topology-identity tags are
`sketch.<sketch-name>.entity.<entity-id>` and
`sketch.<sketch-name>.profile.<profile-id>`, with `topology_name` evidence.
Downstream features can project those tags only when their kernel history
proves one-source/one-target 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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+20
View File
@@ -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.
+6 -2
View File
@@ -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.
+3 -1
View File
@@ -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. During
profile promotion, `entity_id` becomes the local segment of the canonical
topology-identity tag for the corresponding 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.
+8 -5
View File
@@ -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``.
+34
View File
@@ -0,0 +1,34 @@
# 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 the explicit final outputs of the
active model session. The value is returned unchanged, so it can be used inline
or assigned back to a local variable.
```python
@scad.model(graph_id="multi_output_demo")
def build_model():
primary = scad.make_box_rsolid(width=10.0, height=4.0, depth=2.0)
secondary = scad.make_cylinder_rsolid(radius=2.0, height=5.0)
return scad.capture_result(value=(primary, secondary))
```
Explicit capture prevents unrelated intermediate graph leaves from becoming
model outputs. When the enclosing `@model` uses `export_dir=...`, captured
geometry and product values become roots in the single self-contained Scene ZIP.
The package embeds model JSON and mapped Python sources; automatic export does
not create adjacent model/session JSON, STEP, STL, or FCStd files. It must be
called inside `@model` or another active `GraphSession`.
+15 -2
View File
@@ -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.
+22
View File
@@ -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.
+17
View File
@@ -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.
+9 -1
View File
@@ -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.
+27
View File
@@ -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.
+11 -1
View File
@@ -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.
+35 -2
View File
@@ -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,27 @@ 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 semantic role
tag arguments to attach user tags to those exact role sets.
When `tag_prefix` is supplied, profile Edge tags are recommended at creation
time through the profile API (`edge_tags` and `tag_prefix`). The feature
produces `<tag_prefix>.face.start`, `<tag_prefix>.face.end`, and one
`<tag_prefix>.face.side.<profile_edge_tag>` for each kernel-proven profile Edge.
The cap Face tags are inherited by their boundary Edges, so an exact Edge
can be selected from two tagged neighboring Faces with QL `incident_to` or
`shared_boundary` rather than an enumeration index.
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.
+15 -2
View File
@@ -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.
+17
View File
@@ -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``.
+21
View File
@@ -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.
+13 -2
View File
@@ -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.
+21 -2
View File
@@ -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.
+31 -2
View File
@@ -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,19 @@ def make_box_rsolid(width: ScalarLike, height: ScalarLike, depth: ScalarLike, bo
## Description
Create a box solid.
Create a native box solid. The exact kernel-backed Face roles are `box.bottom`,
`box.top`, `box.front`, `box.back`, `box.left`, and `box.right`. Each role has
exactly one Face.
The face tag arguments attach tags to those roles. `result_tag` targets the
result Solid. `tag_prefix="housing"` creates the topology tags
`housing.solid`, `housing.face.bottom`,
`housing.face.top`, `housing.face.front`, `housing.face.back`,
`housing.face.left`, and `housing.face.right`.
The roles and topology tags come directly from OCC Box Face witnesses, not Face
enumeration or geometric classification. The current OCP Box builder does not
expose equivalent direct Edge witnesses, so Box Edge output roles are
unsupported. Select an exact Edge from two tagged incident Faces with
`Q.edges().incident_to(face_a, face_b, distinct=True)` or
`face_a.shared_boundary(face_b)`.
+9 -2
View File
@@ -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`.
+13 -2
View File
@@ -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.
+13 -2
View File
@@ -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.
+32 -2
View File
@@ -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,19 @@ 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. The kernel-backed Face roles are `cone.start`,
`cone.end`, and `cone.side`. The Edge roles are `cone.start_boundary`,
`cone.end_boundary`, and `cone.seam`.
For a pointed cone (`top_radius=0`), no top cap exists, so `cone.end` is absent
and requesting `end_face_tag` fails. `cone.end_boundary` remains available as
the kernel's degenerate apex Edge. A frustum (`top_radius>0`) has all six roles.
The role tag arguments attach tags to exact roles. `result_tag` targets the
Solid. `tag_prefix="adapter"` creates `adapter.solid`, the
Face tags `adapter.face.start`, `adapter.face.end` when present, and
`adapter.face.side`, plus `adapter.edge.start`, `adapter.edge.end`, and
`adapter.edge.seam`.
All roles and topology tags use direct OCC Cone witnesses. They are not inferred from
topology enumeration, size, normal, or position.
+33 -2
View File
@@ -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,21 @@ def make_cylinder_rsolid(radius: ScalarLike, height: ScalarLike, bottom_face_cen
## Description
Create a cylinder solid.
Create a native cylinder solid. The kernel-backed output roles are:
- `cylinder.start`, `cylinder.end`, and `cylinder.side` for Faces.
- `cylinder.start_boundary`, `cylinder.end_boundary`, and `cylinder.seam` for Edges.
The role tag arguments attach tags to those exact roles.
`tag_prefix="shaft"` creates the topology tag prefix `shaft`: the cap and
lateral Faces receive `shaft.face.start`, `shaft.face.end`, and
`shaft.face.side`; their boundary Edges inherit the corresponding Face tags.
The three native Edge roles additionally receive `shaft.edge.start`,
`shaft.edge.end`, and `shaft.edge.seam`.
Topology tag prefixes must be supplied while creating the profile or feature. The implementation
uses OCC primitive witnesses and exact incident topology, not face or edge
enumeration, area, normal, or position heuristics. Use QL relation/set queries
to disambiguate an Edge shared by two tagged Faces, for example
`Q.edges().incident_to(face_a, face_b, distinct=True)` or
`face_a.shared_boundary(face_b)`.
@@ -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 promoted
Face receives the canonical profile topology-identity tag and each boundary
Edge receives the exact Sketch entity tag from the promotion map. For example,
a Sketch with `name="rect"` and profile `bottom` produces `sketch.rect.profile.bottom` and
`sketch.rect.entity.bottom`.
These are creation-time tags with `topology_name` evidence backed by the solved Sketch
promotion map. They replay from the Sketch payload and promotion parameters;
ordinary compatibility tags such as `sketch_entity.bottom` remain separate.
@@ -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`.
+10 -2
View File
@@ -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.
+15 -2
View File
@@ -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.
+15 -2
View File
@@ -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.
+2 -1
View File
@@ -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,11 @@ 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 the exact ordered `entity_id` to Edge correspondence. A Sketch
with `name="rect"` and profile `bottom` produces the tag
`sketch.rect.profile.bottom`; entity ID `right` produces
`sketch.rect.entity.right` with `topology_name` evidence.
Promotion fails if the kernel returns a different Edge count, because the SDK
does not guess correspondence from Edge order, geometry, or measurements.
+50
View File
@@ -0,0 +1,50 @@
# 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/model.json`, mapped project-relative Python source files under
`sources/`, and the GLB/entity assets required for rendering and selection. It
does not create adjacent model/session JSON, STEP, STL, or FCStd files.
```python
import simplecadapi as scad
@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
result = build_bracket()
print(result.result_node_ids)
```
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.
+23
View File
@@ -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.
+28
View File
@@ -0,0 +1,28 @@
# 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`. The decorator reuses the session owned by the enclosing
`@model` function and validates that returned graph values belong to it.
```python
@scad.requires_session
def make_bracket_body():
return scad.make_box_rsolid(width=20.0, height=10.0, depth=3.0)
```
Calling a `@requires_session` builder without an active session raises
`RuntimeError`. Builders must not create their own `GraphSession`.
+21 -2
View File
@@ -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,12 @@ 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.
The semantic role tag arguments attach tags to the proven cap or side Faces.
`result_tag` tags the resulting Solid. These assignments are replayable
semantic nodes.
+12
View File
@@ -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(...)`.
+8 -2
View File
@@ -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.
+8 -2
View File
@@ -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.
+24 -2
View File
@@ -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.
+19
View File
@@ -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.
+19
View File
@@ -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.
+19
View File
@@ -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.
+18 -2
View File
@@ -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.
+11 -1
View File
@@ -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.
+11 -2
View File
@@ -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`。
@@ -0,0 +1,101 @@
# Translator Backend Contract
This contract defines the package structure and dependency boundaries for every
SimpleCAD translator backend.
## Backend Naming
Backend packages live under `simplecadapi.translator` and use the name
`<backend>_translator`, where `<backend>` is a stable lowercase backend ID.
Examples:
- `freecad_translator`
- `openscad_translator`
- `onshape_translator`
Backends are exported explicitly from `simplecadapi.translator`. Importing a
backend package must not require the target CAD runtime to be installed.
## Required Files
Every backend package contains:
| File | Responsibility |
| --- | --- |
| `__init__.py` | Defines the complete public backend surface through `__all__`. |
| `api.py` | Contains public convenience functions and user-facing error boundaries. |
| `translator.py` | Contains the `<Backend>Translator` implementation. |
| `capabilities.py` | Declares backend targets and support for every canonical operation. |
The translator class must inherit `BaseTranslator`. `capabilities.py` exports
`BACKEND_NAME`, `CAPABILITIES`, and `OP_SUPPORT`. The operation support map must
contain exactly the canonical operation set. Unsupported operations require a
non-empty reason.
## Conditional Files
Use these standard names when the corresponding responsibility exists:
| File or directory | Responsibility |
| --- | --- |
| `exporter.py` | File output, external process or remote API execution, and output validation. |
| `context.py` | State owned by one translation invocation. |
| `analysis.py` | Backend-specific graph analysis and lowering decisions. |
| `codegen.py` | Pure target-code formatting and literal helpers. |
| `emitters/` | Canonical operation emitters and their single registry. |
| `runtime/` | Source fragments embedded into an artifact for execution in the target runtime. |
An `emitters/registry.py` file is the only operation-to-emitter mapping. Runtime
fragments must remain valid Python source, must not execute target APIs when the
SimpleCAD package is imported, and must be assembled in one declared order.
## Translation And Export
Translation is an in-memory operation. It consumes canonical model JSON or an
already imported canonical payload and returns a `TranslationArtifact`.
Export is an effectful operation. It may write files, invoke an executable, or
call a remote API. Those effects belong in `exporter.py`, not in the translator
or emitters.
Public naming follows these forms:
- `translate_model_json_to_<artifact>` for in-memory conversion.
- `export_model_json_to_<format>` for file or external-runtime output.
Existing public names may remain as compatibility aliases.
## Dependencies
The allowed dependency direction is:
```text
backend/__init__.py -> api.py, translator.py, capabilities.py
api.py -> translator.py, exporter.py
translator.py -> context.py, analysis.py, emitters/, runtime/
emitters/ -> context and pure code-generation helpers
exporter.py -> shared types/errors and external execution
```
The following reverse dependencies are prohibited:
- Translator or emitters importing `api.py`.
- Translator importing `exporter.py`.
- Emitters importing the public translator class.
- Runtime fragments importing the backend package.
- Capabilities importing translator implementation modules.
## Compatibility
The generated artifact and its persisted target metadata are treated as a
compatibility boundary. Pure module moves must not rename generated runtime
helpers, registries, target object properties, or public import paths. Behavior
fixes are made separately from structural migrations.
## Verification
The shared backend contract test verifies required files, public exports,
translator inheritance, backend naming, and canonical operation coverage.
Backend tests additionally verify generated artifact syntax and, where the
target runtime is available, real exported files.
+16 -8
View File
@@ -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,256 @@
# Dimension Tolerance Chains
SimpleCADAPI can attach units and manufacturing tolerances to declared dimension
variables, infer dimensions through the expression DAG, propagate source
variation, 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. A dimension tolerance describes permitted manufacturing variation around a nominal design value; it never changes a CAD operation's numerical robustness settings.
## Declare Source Dimensions
Every variable that participates in a tolerance chain must declare a tolerance:
```python
import simplecadapi as scad
width = scad.var("width", 10.0, unit="mm", tolerance=0.1)
shaft = scad.var(
"shaft",
0.315,
unit="in",
tolerance=(-0.05, 0.0),
tolerance_unit="mm",
)
```
A scalar tolerance is symmetric. `tolerance=0.1` means `-0.1/+0.1`
around the nominal value. A two-value sequence contains signed
`(lower_deviation, upper_deviation)` values. The lower deviation must be at most
zero, the upper deviation must be at least zero, and all values must be finite.
`tolerance_unit` defaults to `unit`. It may differ from the nominal unit, but the
dimensions must match. Geometry and tolerance propagation use canonical
millimeters for length and degrees for angle. Declaration-space values remain on
the `Var` for display and serialization.
The same values can be represented explicitly:
```python
tolerance = scad.DimensionTolerance(
lower_deviation=-0.05,
upper_deviation=0.0,
)
shaft = scad.var("shaft", 8.0, unit="mm", tolerance=tolerance)
```
Tolerance identity follows `expr_id`, not the human-readable variable name. Two variables with the same name remain separate tolerance sources.
## Propagate A Chain
```python
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
result = scad.analyze_tolerance(clearance, method="worst_case")
print(result.nominal)
print(result.lower_bound, result.upper_bound)
print(result.lower_deviation, result.upper_deviation)
for contribution in result.contributions:
print(contribution.variable_name, contribution.lower_deviation, contribution.upper_deviation)
```
`ToleranceAnalysis` contains absolute bounds, deviations from nominal, inferred
`dimension`, canonical `unit`, and one contribution record per source variable.
Each contribution reports its nominal and source tolerance in canonical units.
The unit system validates the expression before propagation. This permits
physically meaningful nonlinear chains such as:
```python
width = scad.var("width", 30.0, unit="mm", tolerance=0.1)
height = scad.var("height", 40.0, unit="mm", tolerance=0.2)
diagonal = scad.sqrt(width**2 + height**2)
analysis = scad.analyze_tolerance(diagonal)
assert analysis.dimension == scad.LENGTH
assert analysis.unit == scad.MM
```
Area and volume expressions can be inferred and analyzed. Persisted manufacturing
requirements currently accept final Length and Angle results only.
## Propagation Methods
### Worst Case
`method="worst_case"` is the default and the safety-oriented validation method.
- Affine chains preserve variable identity and combine coefficients exactly. Repeated use is not treated as an independent source, so `x - x` has zero propagated tolerance.
- Nonlinear chains use conservative interval propagation.
- Multiplication and division consider all endpoint sign combinations.
- Integer, negative, fractional, and varying powers validate their mathematical domains.
- `sin` and `cos` include interior extrema; `tan` rejects intervals crossing a discontinuity.
- `sqrt`, `asin`, and `acos` validate the entire input interval.
- Division rejects denominator intervals containing zero.
- `atan2` rejects tolerance regions containing its undefined origin.
Conservative interval propagation can intentionally overestimate a strongly correlated nonlinear expression. It must not underestimate a safety bound.
### RSS
`method="rss"` uses first-order analytic sensitivities and root-sum-square combination:
```python
result = scad.analyze_tolerance(clearance, method="rss")
```
Distinct variables are assumed independent. Repeated occurrences of the same variable are merged before RSS, so `x - x` still has zero sensitivity and `x + x` has twice the sensitivity of `x`.
RSS validates the full declared tolerance interval before calculating the first-order estimate. A nominal point cannot hide a division singularity, trigonometric discontinuity, or invalid function domain elsewhere in the source range.
Covariance matrices, correlation groups, probability distributions, and Monte Carlo analysis are not represented by the current API. Use `worst_case` when the independence assumption is unavailable or when a guaranteed envelope is required.
## Check One Requirement
`check_tolerance()` returns a result without raising when the derived tolerance exceeds the permitted deviations:
```python
check = scad.check_tolerance(
clearance,
tolerance=(-0.25, 0.24),
method="worst_case",
name="axial_clearance",
tolerance_unit="mm",
)
print(check.passed)
print(check.lower_margin, check.upper_margin)
```
A non-negative lower and upper margin means the requirement passes, subject to a
small floating-point comparison epsilon. Requirement deviations are converted
from `tolerance_unit` to the target's canonical unit before comparison.
## Session Requirements And Automatic Validation
Use `GraphSession.require_tolerance()` for design requirements that must travel with the model:
```python
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",
requirement_id="req.axial_clearance",
tolerance_unit="mm",
)
report = session.validate_tolerances(raise_on_failure=True)
model_json = scad.export_model_json(session)
```
Automatic validation occurs when:
1. `validate_tolerances(raise_on_failure=True)` is called.
2. A session or model JSON payload is exported.
3. A model JSON payload is imported or replayed.
4. A model is translated to FreeCAD through the model importer.
A failed requirement raises `ToleranceValidationError` at the tolerance layer. Model import, export, and replay expose it through the existing structured `SimpleCADError` harness where applicable.
Declaring a requirement validates that the chain is complete and mathematically defined, but it still records a failing requirement so callers can inspect its margins. Export and replay are the enforcement boundaries.
## Serialization
Variable tolerances are stored on variable nodes in `expression_graph`:
```json
{
"expr_id": "var_width",
"kind": "var",
"name": "width",
"default": 1.0,
"unit": "in",
"tolerance": {
"lower_deviation": -0.1,
"upper_deviation": 0.1
},
"tolerance_unit": "mm"
}
```
Design requirements and their latest validation evidence are stored in the top-level `tolerance_graph`:
```json
{
"requirements": [
{
"requirement_id": "req.axial_clearance",
"target_expr_id": "expr_clearance",
"tolerance": {
"lower_deviation": -0.25,
"upper_deviation": 0.24
},
"method": "worst_case",
"name": "axial_clearance",
"tolerance_unit": "mm",
"target_dimension": {
"length": 1,
"angle": 0
}
}
],
"validation": {
"passed": true,
"checks": []
}
}
```
Validation evidence is recomputed during import; serialized evidence is not
trusted as an authority. The target expression dimension is inferred again and
must match `target_dimension`; `tolerance_unit` must have the same dimension.
Payloads created before units or `tolerance_graph` existed remain valid as legacy
unitless expressions and import with an empty tolerance graph when it is absent.
Nominal geometry replay still uses the numeric snapshots in operation-node `params`. Tolerance validation does not sample or regenerate worst-case geometry.
## FreeCAD Translation
FreeCAD translation keeps the full tolerance graph as document metadata. The
`SimpleCADExpressions` spreadsheet stores lower/upper deviations in columns E/F,
nominal unit in G, tolerance unit in H, and inferred dimension in I. Spreadsheet
values and formulas use canonical CAD values so inch/radian declarations remain
consistent with operation-node snapshots. The translator preserves tolerance
intent but does not convert it into FreeCAD geometric-tolerance objects or
statistical solvers.
## Failure Conditions
Tolerance analysis fails explicitly for:
- a source variable without a declared tolerance
- non-finite nominal values or deviations
- malformed signed lower/upper deviations
- unknown, malformed, non-finite, incompatible, overflowing, or underflowing units
- addition/subtraction or `atan2` with incompatible dimensions
- invalid powers, square roots, or trigonometric dimensions
- mixing unit-declared and legacy variables in one expression
- a requirement unit or persisted target dimension that disagrees with the target
- an Area, Volume, Dimensionless, or compound-dimension requirement target
- duplicate requirement IDs
- unknown or dangling expression references
- malformed, cyclic, duplicate-ID, or unsupported expression nodes
- an undefined expression anywhere in the declared tolerance interval
- an RSS derivative at a non-differentiable nominal point
- an unsupported propagation method
See [Physical Units And Dimension Inference](physical-units.md) for the complete
unit registry, dimension algebra, custom-unit payload, and legacy 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,
@@ -128,7 +128,8 @@ source API 名字不等于 canonical graph op 名字。Composite source API 可
"topology_delta_summary": false,
"assembly_graph": false,
"scalar_field_graph": false,
"expression_graph": true
"expression_graph": true,
"dimension_tolerances": true
},
"graph_id": "graph_xxxxxxxx",
"nodes": [...],
@@ -163,6 +164,7 @@ source API 名字不等于 canonical graph op 名字。Composite source API 可
| `assembly_graph` | `bool` | 当前 graph JSON 本身不承载 assembly graph |
| `scalar_field_graph` | `bool` | 当前为 `false`;SDF / scalar field graph 暂时不在支持范围内 |
| `expression_graph` | `bool` | session/model payload 支持 expression graph |
| `dimension_tolerances` | `bool` | session/model payload supports variable tolerances and a tolerance requirement graph |
## 5. Operation Node Schema
@@ -334,7 +336,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
@@ -667,7 +669,13 @@ outer_edges = Q.faces().where(Q.tag("face.top")).boundary("wire").where(Q.tag("w
"expr_id": "var_119b16e4",
"kind": "var",
"name": "r",
"default": 2.0
"default": 2.0,
"unit": "mm",
"tolerance": {
"lower_deviation": -0.1,
"upper_deviation": 0.2
},
"tolerance_unit": "mm"
}
]
}
@@ -692,7 +700,13 @@ outer_edges = Q.faces().where(Q.tag("face.top")).boundary("wire").where(Q.tag("w
"expr_id": "var_xxx",
"kind": "var",
"name": "radius",
"default": 2.0
"default": 2.0,
"unit": "mm",
"tolerance": {
"lower_deviation": -0.05,
"upper_deviation": 0.1
},
"tolerance_unit": "mm"
}
```
@@ -722,6 +736,82 @@ outer_edges = Q.faces().where(Q.tag("face.top")).boundary("wire").where(Q.tag("w
- `cos`
- `tan`
- `sqrt`
- `acos`
- `asin`
- `atan`
- `atan2`
### 10.3 Unit And Dimension Semantics
Variable nodes may include:
| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `unit` | `string | unit object` | no | nominal declaration unit |
| `tolerance` | `object` | no | signed deviations in `tolerance_unit` |
| `tolerance_unit` | `string | unit object` | no | source tolerance unit; defaults to `unit` in Python declarations |
Built-in units serialize as symbols such as `mm`, `in`, `deg`, or `rad`. Custom
units serialize in full:
```json
{
"symbol": "thou",
"dimension": {"length": 1, "angle": 0},
"scale_to_canonical": 0.0254
}
```
Dimensions contain required integer `length` and `angle` exponents. Canonical
numeric values used by operation-node `params` and tolerance analysis are `mm`,
`mm^2`, `mm^3`, `deg`, or `1` for the named dimensions.
Import rebuilds the complete DAG and reruns dimension inference. Addition and
subtraction require matching dimensions; multiplication/division combine
exponents; dimensioned powers require supported constant exponents; square root
requires even exponents; and trigonometric operations enforce Angle/Dimensionless
inputs. A graph cannot mix unit-declared variables with legacy variables lacking
units. Unitless legacy graphs remain accepted.
### 10.4 Dimension Tolerance Graph
Variable `tolerance` values are signed deviations from `default`. A scalar source dimension must use `lower_deviation <= 0 <= upper_deviation`.
Session/model payloads may include a sibling `tolerance_graph`:
```json
{
"requirements": [
{
"requirement_id": "req.clearance",
"target_expr_id": "expr_clearance",
"tolerance": {
"lower_deviation": -0.2,
"upper_deviation": 0.3
},
"method": "worst_case",
"name": "clearance",
"tolerance_unit": "mm",
"target_dimension": {
"length": 1,
"angle": 0
}
}
],
"validation": {
"passed": true,
"checks": []
}
}
```
Supported methods are `worst_case` and `rss`. Unit-aware requirements must target
Length or Angle. `tolerance_unit` must match the inferred target dimension and is
converted to the canonical unit before comparison. Importers recompute validation
from `expression_graph`, compare the inferred result to `target_dimension`, and do
not trust serialized `validation` evidence. Missing `tolerance_graph` is treated as
an empty graph for backward compatibility. Legacy requirements may omit both unit
fields when their target expression is unitless.
## 11. Frame Graph Schema
@@ -765,6 +855,7 @@ outer_edges = Q.faces().where(Q.tag("face.top")).boundary("wire").where(Q.tag("w
"canonical_contract": {...},
"graph": {...},
"expression_graph": {...},
"tolerance_graph": {...},
"frame_graph": {...},
"geometry_registry": [...],
"semantic_entity_registry": [...],
@@ -784,6 +875,7 @@ outer_edges = Q.faces().where(Q.tag("face.top")).boundary("wire").where(Q.tag("w
| `graph` | `graph object` | yes | canonical low-level graph and only source of truth |
| `leaf_ids` | `array<string>` | yes | explicit result set for multi-output graph replay/export |
| `expression_graph` | `object` | yes | expression DAG |
| `tolerance_graph` | `object` | no | dimension-chain requirements and validation evidence; defaults to an empty graph |
| `frame_graph` | `object` | yes | frame snapshots |
| `geometry_registry` | `array<object>` | yes | output geometry registry |
| `semantic_entity_registry` | `array<object>` | yes | semantic entity registry |
@@ -936,6 +1028,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 +1389,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`
+202
View File
@@ -0,0 +1,202 @@
# Physical Units And Dimension Inference
SimpleCADAPI attaches physical meaning at `Var` declarations and infers the
dimension of every derived scalar expression. This catches invalid formulas before
they reach geometry, tolerance analysis, model export, replay, or FreeCAD
translation.
## Canonical CAD Units
Declaration values are preserved on each `Var`, but evaluation converts them to a
single CAD coordinate system:
| Dimension | Canonical unit |
| --- | --- |
| Dimensionless | `1` |
| Length | `mm` |
| Area | `mm^2` |
| Volume | `mm^3` |
| Angle | `deg` |
Degrees are canonical because existing SimpleCAD rotation and angular APIs use
degrees. Trigonometric evaluation converts to radians internally and converts
inverse-trigonometric results back to degrees.
```python
import math
import simplecadapi as scad
width = scad.var("width", 1.0, unit="in")
angle = scad.var("angle", math.pi / 2, unit="rad")
assert width.default == 1.0
assert width.evaluate() == 25.4
assert angle.evaluate() == 90.0
assert math.isclose(scad.sin(angle).evaluate(), 1.0)
```
Bindings use the variable's declaration unit. `width.evaluate({"width": 2.0})`
therefore returns `50.8` millimeters for an inch-declared variable.
## Declaring Units And Tolerances
```python
width = scad.var(
"width",
1.0,
unit="in",
tolerance=(-0.1, 0.2),
tolerance_unit="mm",
)
```
- `default` is in `unit`.
- `tolerance` is in `tolerance_unit`.
- `tolerance_unit` defaults to `unit` when a tolerance is present.
- Nominal and tolerance units may differ, but their dimensions must match.
- A `tolerance_unit` requires both `unit` and `tolerance`.
- Values must be finite and representable after canonical conversion.
`width.default` and `width.tolerance` preserve declaration-space values.
`width.canonical_default` and `width.canonical_tolerance` expose values used by
geometry and tolerance propagation.
## Built-In Units
| Dimension | Symbols |
| --- | --- |
| Dimensionless | `1`, `%` |
| Length | `mm`, `cm`, `m`, `in`, `ft` |
| Area | `mm^2`, `cm^2`, `m^2`, `in^2`, `ft^2` |
| Volume | `mm^3`, `cm^3`, `m^3`, `in^3`, `ft^3` |
| Angle | `deg`, `rad` |
Common singular/plural names are accepted by `get_unit()`, including
`millimeters`, `inches`, `feet`, `degrees`, `radians`, `square feet`, and
`cubic inches`. `ml` aliases `cm^3`.
Use constants such as `MM`, `INCH`, `DEGREE`, `RADIAN`, `LENGTH`, and `ANGLE`, or
resolve strings:
```python
assert scad.get_unit("inch") == scad.INCH
assert scad.convert_value(1.0, "in", "mm") == 25.4
assert math.isclose(scad.convert_value(180.0, "deg", "rad"), math.pi)
```
Incompatible conversion raises `UnitValidationError`.
## Dimension Algebra
`Dimension` stores integer exponents for length and angle. Named dimensions are:
- `DIMENSIONLESS = Dimension()`
- `LENGTH = Dimension(length=1)`
- `AREA = Dimension(length=2)`
- `VOLUME = Dimension(length=3)`
- `ANGLE = Dimension(angle=1)`
`infer_dimension(expression)` applies these rules:
| Operation | Rule |
| --- | --- |
| `a + b`, `a - b` | dimensions must match |
| `a * b` | add dimension exponents |
| `a / b` | subtract dimension exponents |
| `a ** n` | multiply exponents by constant integer `n` |
| `sqrt(a)` or `a ** 0.5` | every exponent must be even |
| unary `-a`, `abs(a)` | preserve dimension |
| `sin`, `cos`, `tan` | input must be Angle; result is Dimensionless |
| `asin`, `acos`, `atan` | input must be Dimensionless; result is Angle |
| `atan2(y, x)` | inputs must have the same dimension; result is Angle |
Arbitrary and varying powers are permitted for dimensionless bases. A dimensioned
base requires a constant integer exponent, except `0.5` is accepted when all base
exponents are even.
```python
width = scad.var("width", 30.0, unit="mm")
height = scad.var("height", 40.0, unit="mm")
area = width * height
diagonal = scad.sqrt(width**2 + height**2)
assert scad.infer_dimension(area) == scad.AREA
assert scad.infer_dimension(diagonal) == scad.LENGTH
assert diagonal.evaluate() == 50.0
```
## Numeric Constants
Numeric literals are dimensionless coefficients in multiplication and division.
For addition and subtraction, a literal adopts the other operand's dimension as a
contextual offset:
```python
length = scad.var("length", 10.0, unit="mm")
assert scad.infer_dimension(length * 2.0) == scad.LENGTH
assert scad.infer_dimension(length + 2.0) == scad.LENGTH
```
The literal is already expressed in the canonical result unit. `length + 2.0`
therefore means two millimeters, not two units of `length.unit`. Prefer explicit
variables when declaration-unit intent must be retained.
## Legacy Unitless Expressions
Variables without `unit` retain the previous behavior:
- `infer_dimension()` returns `None`.
- Trigonometric inputs and results use radians.
- Existing arbitrary expression and tolerance behavior remains available.
- A legacy variable cannot be mixed with a unit-declared variable in one
expression because no safe physical meaning can be inferred.
Pure numeric constant expressions infer `Dimensionless`.
## Custom Units
Custom linear-scale units use the same canonical system:
```python
thou = scad.Unit("thou", scad.LENGTH, 0.0254)
width = scad.var("width", 1000.0, unit=thou)
assert width.evaluate() == 25.4
```
Built-in units serialize as symbols. Custom units serialize a definition:
```json
{
"symbol": "thou",
"dimension": {"length": 1, "angle": 0},
"scale_to_canonical": 0.0254
}
```
Units are scale-only. Offset units such as Celsius/Fahrenheit are not represented.
## Validation Boundaries
Unit and dimension validation runs when:
1. A `Var`, `Dimension`, or `Unit` is created.
2. An expression is directly evaluated.
3. An expression is registered or imported through `ExpressionGraph`.
4. A tolerance is analyzed or a requirement is declared.
5. Session/model JSON is imported, exported, replayed, or translated.
Malformed units, cyclic graphs, duplicate expression IDs, mixed legacy/typed
variables, incompatible dimensions, invalid roots, and invalid trigonometric inputs
are rejected before graph mutation or geometry replay.
## Manufacturing Requirement Scope
Area, volume, inverse length, and other compound dimensions can be inferred and
analyzed. Manufacturing requirements created by `check_tolerance()` or
`GraphSession.require_tolerance()` currently accept final `Length` and `Angle`
results only. This prevents an area or volume variation from being presented as a
linear dimension requirement without explicit engineering semantics.
See [Dimension Tolerance Chains](dimension-tolerance-chains.md) for propagation,
RSS assumptions, enforcement boundaries, and serialized requirement fields.
+30 -12
View File
@@ -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:
@@ -29,6 +34,17 @@ Inspect these fields:
- `node["inputs"]`: upstream node ids used by replay.
- `payload["leaf_ids"]`: explicit final result node ids.
- `payload["expression_graph"]`: expression DAG used by expression-backed parameters.
- `payload["tolerance_graph"]`: dimension-chain requirements and validation evidence.
For new top-level models, `ModelResult.model_json` is the preferred artifact
accessor. Use `@scad.requires_session` for reusable builders and
`scad.capture_result(...)` when the final output should not be inferred from
all graph leaves. 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/model.json`, mapped project-relative Python sources, and the 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
@@ -52,11 +68,13 @@ Many user-facing functions are convenience APIs. During an active `GraphSession`
- [Primitive and profile operations](primitives-and-profiles.md)
- [Features, booleans, transforms, patterns, and selectors](features-booleans-transforms.md)
- [Expressions and replay behavior](expressions-and-replay.md)
- [Physical units and dimension inference](../physical-units.md)
- [Dimension tolerance chains](../dimension-tolerance-chains.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.
@@ -15,14 +15,15 @@ This lets consumers choose between:
```python
import simplecadapi as scad
width = scad.var("width", 24.0, comment="plate width")
height = scad.var("height", 12.0, comment="plate height")
thickness = scad.var("thickness", 4.0, comment="plate thickness")
width = scad.var("width", 24.0, unit="mm", comment="plate width", tolerance=0.1)
height = scad.var("height", 12.0, unit="mm", comment="plate height", tolerance=0.1)
thickness = scad.var("thickness", 4.0, unit="mm", comment="plate thickness", tolerance=(-0.05, 0.1))
with scad.GraphSession() as session:
plate = scad.make_box_rsolid(width, height, thickness)
rib = scad.make_box_rsolid(width / 4.0, height, thickness * 2.0)
part = scad.union_rsolid(plate, rib)
session.require_tolerance(width + height, 0.2, tolerance_unit="mm", name="plate_envelope")
model_json = scad.export_model_json(session)
```
@@ -48,7 +49,9 @@ A node with expression-backed params may look like:
}
```
`params.distance` is the evaluated snapshot. `param_exprs.distance` says the value came from expression node `var_thickness`.
`params.distance` is the evaluated canonical snapshot. Unit-aware lengths are
stored in millimeters and angles in degrees. `param_exprs.distance` says the value
came from expression node `var_thickness` and preserves its declaration metadata.
For tuple/list params, `param_exprs` mirrors the shape of the parameter and uses `null` where no expression is present:
@@ -78,12 +81,24 @@ Consumers that want parameterization should:
Consumers that only want geometry can ignore `param_exprs` and `expression_graph`.
Variable nodes may contain `unit`, `tolerance`, and `tolerance_unit`. Registered
units use string symbols; custom units use `{symbol, dimension,
scale_to_canonical}` objects. Import reconstructs the expression graph and reruns
dimension inference rather than trusting external dimension claims.
Session/model payloads store derived-dimension requirements in `tolerance_graph`.
See [Physical Units](../physical-units.md) and [Dimension Tolerance
Chains](../dimension-tolerance-chains.md) for inference, propagation, and
validation semantics.
## Replay policy in current implementation
`replay_model_json(model_json)` currently uses the canonical low-level `graph` and the numeric values in `node.params`.
That means replay is deterministic with respect to the exported snapshot. It does not currently re-solve expressions with changed variable values.
Replay does validate stored tolerance requirements before rebuilding the nominal geometry. A failing tolerance chain blocks replay, but passing bounds do not cause replay to sample or regenerate limit geometry.
In practical terms:
```python
@@ -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:
+34 -12
View File
@@ -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))
```
+11
View File
@@ -0,0 +1,11 @@
# Engineering Guides
- [STEP BREP 逆向工程方法](step-brep-reverse-engineering.md):从 STEP 检查、解析几何指纹、特征树推断、候选迭代,到几何点集、BREP 拓扑、参数历史和模型回放的完整工作流。
对应的可运行模块和案例:
```text
src/simplecadapi/inverse_engineer/brep/
examples/out/xzby_reverse/README.md
examples/out/xzby_reverse/reverse_model.py
```
@@ -0,0 +1,49 @@
# STEP BREP 逆向工程方法
该方法论是 SimpleCAD skill 的任务专用参考,规范版本位于:
```text
skills/simplecadapi/references/inverse_engineer/brep-reverse-engineering.md
```
正式检查工具位于:
```text
src/simplecadapi/inverse_engineer/brep/
```
Python API:
```python
from simplecadapi.inverse_engineer import brep
report = brep.inspect_step(path="part.step")
comparison = brep.compare_steps(target_path="target.step", candidate_path="candidate.step")
brep.render_step_views(step_path="candidate.step", output_path="candidate-views.png")
brep.compare_step_slices(
target_path="target.step",
candidate_path="candidate.step",
output_path="slice-overlay.png",
)
```
渲染和图片截面功能需要可选依赖:
```bash
uv sync --extra inverse-engineer
```
CLI:
```bash
uv run simplecad-brep inspect part.step -o part-report.json
uv run simplecad-brep compare target.step candidate.step -o comparison.json
uv run simplecad-brep render candidate.step candidate-views.png
uv run simplecad-brep slices target.step candidate.step slice-overlay.png
```
具体逆向案例见:
```text
examples/out/xzby_reverse/README.md
```
+15 -2
View File
@@ -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.