feat: integrate SimpleCADAPI 2.0.2 CAD workflows
This commit is contained in:
@@ -62,7 +62,7 @@ SimpleWorkplane ← local modeling context
|
||||
- **Shape-first API**: users work with `Vertex`, `Edge`, `Wire`, `Face`, and `Solid`, not graph nodes.
|
||||
- **Functional modeling style**: public operations return new geometry values, e.g. `make_box_rsolid(...)`, `cut_rsolid(...)`, `fillet_rsolid(...)`.
|
||||
- **OCP-native runtime**: geometry construction, topology traversal, properties, booleans, transforms, and export use OCP/OpenCascade helpers.
|
||||
- **Replayable graph workflows**: `GraphSession` can record a canonical low-level operation graph and `export_model_json()` can serialize it for `replay_model_json()`.
|
||||
- **Replayable graph workflows**: `@scad.model` owns one `GraphSession` and returns a `ModelResult`; `@scad.requires_session` composes child builders, and `scad.capture_result()` selects canonical output nodes for replay and export.
|
||||
- **Tags and metadata**: tags are useful for lightweight semantics; structured numeric facts should be stored in metadata such as `metadata["geo"]`.
|
||||
- **Indexed topology access**: use plural methods such as `get_edges()` and `get_faces()` for enumeration, and pass an index to the same getter, such as `get_edges(index)` or `get_faces(index)`, for intentional indexed picks that should become graph selection nodes.
|
||||
|
||||
@@ -74,11 +74,14 @@ import simplecadapi as scad
|
||||
with scad.SimpleWorkplane(origin=(0, 0, 0)):
|
||||
box = scad.make_box_rsolid(width=5, height=3, depth=2)
|
||||
|
||||
scad.apply_tag(box, "role.bracket")
|
||||
scad.apply_tag(shape=box, tag="role.bracket")
|
||||
box.set_metadata("material", "6061-T6")
|
||||
box.auto_tag_faces("box")
|
||||
|
||||
top_faces = [face for face in box.get_faces() if "face.top" in scad.list_tags(face)]
|
||||
top_faces = [
|
||||
face for face in box.get_faces()
|
||||
if "face.top" in scad.list_tags(shape=face)
|
||||
]
|
||||
print(len(top_faces))
|
||||
```
|
||||
|
||||
@@ -87,13 +90,18 @@ print(len(top_faces))
|
||||
```python
|
||||
import simplecadapi as scad
|
||||
|
||||
with scad.GraphSession() as session:
|
||||
body = scad.make_box_rsolid(10, 10, 4)
|
||||
hole = scad.make_cylinder_rsolid(1.5, 8, bottom_face_center=(0, 0, -2))
|
||||
@scad.model(graph_id="drilled_block")
|
||||
def build_model():
|
||||
body = scad.make_box_rsolid(width=10, height=10, depth=4)
|
||||
hole = scad.make_cylinder_rsolid(
|
||||
radius=1.5, height=8, bottom_face_center=(0, 0, -2)
|
||||
)
|
||||
part = scad.cut_rsolid(body, hole)
|
||||
scad.capture_result(value=part)
|
||||
return part
|
||||
|
||||
payload = scad.export_model_json(session)
|
||||
rebuilt = scad.replay_model_json(payload)
|
||||
result = build_model()
|
||||
rebuilt = result.replay()
|
||||
print(len(rebuilt))
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# Dimension Tolerance Chains
|
||||
|
||||
SimpleCADAPI can attach manufacturing tolerances to declared dimension variables, propagate them through the expression DAG, and verify derived dimensions against design requirements.
|
||||
|
||||
This feature is separate from sketch-solver residual tolerances, boolean fuzzy tolerances, mesh resolution, and geometric fitting tolerances.
|
||||
|
||||
## Core Workflow
|
||||
|
||||
```python
|
||||
import simplecadapi as scad
|
||||
|
||||
housing = scad.var("housing", 100.0, unit="mm", tolerance=0.15)
|
||||
bearing = scad.var("bearing", 2.0, unit="cm", tolerance=(-0.04, 0.05), tolerance_unit="mm")
|
||||
spacer = scad.var("spacer", 79.4, unit="mm", tolerance=0.05)
|
||||
clearance = housing - bearing - spacer
|
||||
|
||||
worst_case = scad.analyze_tolerance(clearance, method="worst_case")
|
||||
rss = scad.analyze_tolerance(clearance, method="rss")
|
||||
|
||||
with scad.GraphSession() as session:
|
||||
body = scad.make_box_rsolid(housing, 10.0, 10.0)
|
||||
session.require_tolerance(
|
||||
clearance,
|
||||
(-0.25, 0.24),
|
||||
method="worst_case",
|
||||
name="axial_clearance",
|
||||
tolerance_unit="mm",
|
||||
)
|
||||
|
||||
report = session.validate_tolerances(raise_on_failure=True)
|
||||
model_json = scad.export_model_json(session)
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- A scalar tolerance is symmetric: `0.1` means `-0.1/+0.1`.
|
||||
- A two-value sequence contains signed `(lower_deviation, upper_deviation)` values.
|
||||
- Every variable in a tolerance chain must declare a finite tolerance.
|
||||
- `tolerance_unit` defaults to `unit`; different nominal/tolerance units must
|
||||
have the same dimension.
|
||||
- Length and angle propagate in canonical `mm` and `deg` units.
|
||||
- Expression dimensions are inferred and validated before propagation.
|
||||
- Area and volume can be analyzed, while requirements currently accept final
|
||||
Length and Angle results only.
|
||||
- Tolerance sources are identified by `expr_id`, not by variable name.
|
||||
- `worst_case` uses exact dependency-aware affine propagation and conservative nonlinear interval propagation.
|
||||
- `rss` uses analytic first-order sensitivities, assumes distinct variables are independent, and combines repeated occurrences of the same variable before RSS.
|
||||
- Both methods reject a chain that is undefined anywhere inside its declared source intervals.
|
||||
- Export, import, replay, and FreeCAD translation validate stored requirements automatically.
|
||||
- Legacy model JSON without `tolerance_graph` imports with an empty graph.
|
||||
- Nominal replay continues to use numeric operation snapshots; tolerance analysis does not regenerate worst-case geometry.
|
||||
- Import recomputes dimensions and rejects tampered unit or target-dimension data.
|
||||
|
||||
Use `check_tolerance()` for a standalone non-raising check. Use `GraphSession.require_tolerance()` when the requirement must be serialized and enforced.
|
||||
|
||||
See the source-checkout guide at `docs/core/dimension-tolerance-chains.md` for propagation details, failure conditions, JSON shapes, and FreeCAD behavior.
|
||||
@@ -117,7 +117,7 @@ source API 名字不等于 canonical graph op 名字。Composite source API 可
|
||||
```json
|
||||
{
|
||||
"schema_version": "2.0",
|
||||
"producer_version": "2.0.1b1",
|
||||
"producer_version": "2.0.2",
|
||||
"capabilities": {
|
||||
"selection_ref_strategies": true,
|
||||
"geo_select_nodes": true,
|
||||
@@ -334,7 +334,7 @@ source API 名字不等于 canonical graph op 名字。Composite source API 可
|
||||
|
||||
- `make_*_face` 与 `make_face_from_wire` -> `Sketch`
|
||||
- `make_*_edge` / `make_*_wire` -> `Profile`
|
||||
- `make_extrude_rsolid` / `make_revolve_rsolid` / `make_loft_rsolid` / `make_sweep_rsolid` / `make_fillet_rsolid` / `make_chamfer_rsolid` / `make_shell_rsolid` / `make_cut_rsolid` / `make_union_rsolid` / `make_intersect_rsolid` -> `Feature`
|
||||
- `make_extrude_rsolid` / `make_revolve_rsolid` / `make_loft_rsolid` / `make_sweep_rsolid` / `make_twisted_sweep_rsolid` / `make_fillet_rsolid` / `make_chamfer_rsolid` / `make_shell_rsolid` / `make_cut_rsolid` / `make_union_rsolid` / `make_intersect_rsolid` -> `Feature`
|
||||
|
||||
## 7. Topology Delta Schema
|
||||
|
||||
@@ -936,6 +936,7 @@ New canonical profile nodes use the `make_*_r*` names listed in `canonical_contr
|
||||
- `make_revolve_rsolid`
|
||||
- `make_loft_rsolid`
|
||||
- `make_sweep_rsolid`
|
||||
- `make_twisted_sweep_rsolid`
|
||||
- `make_translate_rshape`
|
||||
- `make_rotate_rshape`
|
||||
- `make_mirror_rshape`
|
||||
@@ -1296,6 +1297,23 @@ Important:
|
||||
| --- | --- | --- |
|
||||
| `is_frenet` | `bool` | sweep orientation mode |
|
||||
|
||||
#### `make_twisted_sweep_rsolid`
|
||||
|
||||
- Inputs: 1 profile `Face`
|
||||
- Outputs: 1 `Solid`
|
||||
- Params:
|
||||
|
||||
| Key | Type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `axis` | 3-number array | sweep and rotation-axis direction in caller coordinates |
|
||||
| `origin` | 3-number array | sweep start and a point on the rotation axis |
|
||||
| `distance` | positive number | axial sweep distance |
|
||||
| `twist_angle` | number | signed total rotation in degrees |
|
||||
| `guide_radius` | positive number | internal auxiliary-spine radius |
|
||||
|
||||
Replay rebuilds the auxiliary spine deterministically inside the operation. It
|
||||
does not infer section counts or lower to transient loft nodes.
|
||||
|
||||
### 14.6 Boolean Ops
|
||||
|
||||
#### `make_union_rsolid`
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Physical Units And Dimension Inference
|
||||
|
||||
Declare physical meaning on variables:
|
||||
|
||||
```python
|
||||
width = scad.var(
|
||||
"width",
|
||||
1.0,
|
||||
unit="in",
|
||||
tolerance=0.1,
|
||||
tolerance_unit="mm",
|
||||
)
|
||||
height = scad.var("height", 40.0, unit="mm", tolerance=0.2)
|
||||
diagonal = scad.sqrt(width**2 + height**2)
|
||||
```
|
||||
|
||||
## Canonical Rules
|
||||
|
||||
- Length, area, volume, and angle evaluate in `mm`, `mm^2`, `mm^3`, and `deg`.
|
||||
- Declaration values remain available on `Var.default` and `Var.tolerance`.
|
||||
- `tolerance_unit` defaults to `unit` and must have the same dimension.
|
||||
- Bindings use the variable's declaration unit.
|
||||
- Numeric constants are dimensionless coefficients; in addition/subtraction they
|
||||
are contextual offsets in the canonical result unit.
|
||||
|
||||
## Inference Rules
|
||||
|
||||
- Addition and subtraction require matching dimensions.
|
||||
- Multiplication/division add/subtract dimension exponents.
|
||||
- Constant integer powers multiply exponents.
|
||||
- Square root requires every exponent to be even.
|
||||
- `sin`, `cos`, and `tan` require Angle and return Dimensionless.
|
||||
- `asin`, `acos`, and `atan` require Dimensionless and return Angle.
|
||||
- `atan2` requires matching input dimensions and returns Angle.
|
||||
- Unit-declared variables cannot mix with legacy variables without units.
|
||||
|
||||
Built-in symbols are `1`, `%`, `mm`, `cm`, `m`, `in`, `ft`, their square/cubic
|
||||
forms, `deg`, and `rad`. Use `get_unit()`, `convert_value()`, and
|
||||
`infer_dimension()` for explicit inspection.
|
||||
|
||||
Area and volume can be inferred and analyzed. Persisted manufacturing requirements
|
||||
currently require a final Length or Angle result.
|
||||
|
||||
Read the source-checkout `docs/core/physical-units.md` for custom-unit JSON,
|
||||
complete operation tables, failure conditions, and legacy behavior.
|
||||
@@ -10,14 +10,19 @@ The long-form schema reference remains [`../operation_graph_json_spec.md`](../op
|
||||
import json
|
||||
import simplecadapi as scad
|
||||
|
||||
with scad.GraphSession() as session:
|
||||
body = scad.make_box_rsolid(10, 6, 2)
|
||||
hole = scad.make_cylinder_rsolid(1, 4, bottom_face_center=(0, 0, -1))
|
||||
@scad.model(graph_id="drilled_block")
|
||||
def build_model():
|
||||
body = scad.make_box_rsolid(width=10, height=6, depth=2)
|
||||
hole = scad.make_cylinder_rsolid(
|
||||
radius=1, height=4, bottom_face_center=(0, 0, -1)
|
||||
)
|
||||
result = scad.cut_rsolid(body, hole)
|
||||
scad.capture_result(value=result)
|
||||
return result
|
||||
|
||||
model_json = scad.export_model_json(session)
|
||||
payload = json.loads(model_json)
|
||||
rebuilt = scad.replay_model_json(model_json)
|
||||
model = build_model()
|
||||
payload = json.loads(model.model_json)
|
||||
rebuilt = model.replay()
|
||||
```
|
||||
|
||||
Inspect these fields:
|
||||
@@ -30,6 +35,16 @@ Inspect these fields:
|
||||
- `payload["leaf_ids"]`: explicit final result node ids.
|
||||
- `payload["expression_graph"]`: expression DAG used by expression-backed parameters.
|
||||
|
||||
For new top-level models, `ModelResult.model_json` is the preferred artifact
|
||||
accessor. Use `@scad.requires_session` for reusable builders and
|
||||
`scad.capture_result(...)` to keep intermediate graph leaves out of the model
|
||||
outputs. If a model invocation also needs durable CAD/viewer files, pass
|
||||
`export_dir=...` to `@scad.model`; its captured geometry/product values then
|
||||
produce one self-contained `<graph_id>.scene.zip`. It embeds model JSON, mapped
|
||||
project-relative Python sources, and evaluated render/selection assets; it does
|
||||
not create adjacent model/session JSON, STEP, STL, or FCStd files. No files are
|
||||
written when `export_dir` is omitted.
|
||||
|
||||
## Important rule: source API is not always graph API
|
||||
|
||||
Many user-facing functions are convenience APIs. During an active `GraphSession`, they lower to canonical low-level nodes:
|
||||
@@ -53,10 +68,10 @@ Many user-facing functions are convenience APIs. During an active `GraphSession`
|
||||
- [Features, booleans, transforms, patterns, and selectors](features-booleans-transforms.md)
|
||||
- [Expressions and replay behavior](expressions-and-replay.md)
|
||||
|
||||
## Example
|
||||
## Examples
|
||||
|
||||
See [`../../../examples/07_serialization_operation_tree.py`](../../../examples/07_serialization_operation_tree.py). It intentionally exercises every canonical core operation and writes:
|
||||
|
||||
- `examples/out/serialization_operation_tree.model.json`
|
||||
- `examples/out/serialization_operation_tree.summary.md`
|
||||
- `examples/out/serialization_operation_tree.step`
|
||||
The retained examples use the same model/session contract. See
|
||||
[`../../../examples/08_constrained_sketch.py`](../../../examples/08_constrained_sketch.py)
|
||||
for sketch promotion and replay, and
|
||||
[`../../../examples/10_part_assembly.py`](../../../examples/10_part_assembly.py)
|
||||
for product hierarchy and automatic artifact export.
|
||||
|
||||
+34
@@ -122,6 +122,40 @@ Replay effect:
|
||||
2. Replay path wire from input 1.
|
||||
3. Call `sweep_rsolid(profile, path, is_frenet=...)`.
|
||||
|
||||
## Twisted Sweep
|
||||
|
||||
Source:
|
||||
|
||||
```python
|
||||
profile = scad.make_rectangle_rface(width=2.0, height=1.0)
|
||||
solid = scad.twisted_sweep_rsolid(
|
||||
profile=profile,
|
||||
distance=8.0,
|
||||
twist_angle=30.0,
|
||||
)
|
||||
```
|
||||
|
||||
Serialized node:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "make_twisted_sweep_rsolid",
|
||||
"params": {
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"origin": [0.0, 0.0, 0.0],
|
||||
"distance": 8.0,
|
||||
"twist_angle": 30.0,
|
||||
"guide_radius": 1.0
|
||||
},
|
||||
"inputs": ["node_for_profile_face"],
|
||||
"output_count": 1
|
||||
}
|
||||
```
|
||||
|
||||
Replay reconstructs the continuous auxiliary-spine rotation law from the
|
||||
recorded parameters and invokes `twisted_sweep_rsolid(...)`. No sampled loft
|
||||
sections are stored or inferred.
|
||||
|
||||
## Helical sweep macro lowering
|
||||
|
||||
Source:
|
||||
|
||||
@@ -2,21 +2,24 @@
|
||||
|
||||
## Overview
|
||||
|
||||
`TaggedMixin` is the internal tag and metadata storage mixin used by `Vertex`, `Edge`, `Wire`, `Face`, and `Solid`. It owns the shared `_tags`, `_metadata`, and `_runtime` stores for topology wrappers.
|
||||
`TaggedMixin` is the internal semantic binding and metadata mixin used by topology wrappers. Canonical tag ownership is source-preserving `TagBinding` data. `_tags` is only an effective-scope compatibility cache; user code must not treat it as writable truth.
|
||||
|
||||
User code should not call member tag mutators. The public tag API is functional:
|
||||
|
||||
- `apply_tag(shape, tag)` attaches one normalized tag.
|
||||
- `list_tags(shape)` returns tags in deterministic sorted order.
|
||||
- `apply_tag_rselection(scope, targets, tag, ...)` returns an independent semantic view and exposes explicit propagation policies.
|
||||
- `list_tags(shape, scope=...)` returns tags in deterministic sorted order.
|
||||
- `explain_tag(shape, tag, scope=...)` preserves binding and producer evidence.
|
||||
- `select_faces_by_tag(...)`, `select_edges_by_tag(...)`, and QL predicates such as `ql.tag("role.*")` provide selection/query helpers.
|
||||
|
||||
## Tagging Mental Model
|
||||
|
||||
- Tags are normalized lowercase dot-separated semantic tokens.
|
||||
- Examples: `role.mounting_surface`, `anchor.datum.primary`, `group.fasteners`, `face.top`, `edge.boundary`, `wire.outer`, `solid.boolean.cut`.
|
||||
- `apply_tag(shape, tag)` does not expose propagation controls.
|
||||
- The standard policy propagates `role.*`, `anchor.*`, `group.*`, and a few legacy bare semantic tags downward.
|
||||
- Topology-specific tags such as `face.*`, `edge.*`, `wire.*`, `vertex.*`, and `solid.*` stay local.
|
||||
- New user assignments default to local topology propagation regardless of prefix.
|
||||
- Downward inheritance is explicit and computed dynamically; bindings are not copied into every child.
|
||||
- `effective` means local plus inherited and does not include lineage.
|
||||
- `lineage` requires complete topology history and only follows derivations allowed by the binding policy.
|
||||
- Numeric dimensions, measurements, and rich descriptive payloads belong in metadata, not tags.
|
||||
- Geometry builders store structured geometry facts under `metadata["geo"]`.
|
||||
|
||||
@@ -34,16 +37,29 @@ top_faces = [face for face in box.get_faces() if "face.top" in scad.list_tags(fa
|
||||
print(len(top_faces))
|
||||
```
|
||||
|
||||
## Propagation Example
|
||||
## Explicit Propagation Example
|
||||
|
||||
```python
|
||||
import simplecadapi as scad
|
||||
|
||||
body = scad.make_box_rsolid(10, 10, 2)
|
||||
scad.apply_tag(body, "role.mounting_plate")
|
||||
body = scad.make_box_rsolid(width=10, height=10, depth=2)
|
||||
tagged = scad.apply_tag_rselection(
|
||||
scope=body,
|
||||
targets=[body],
|
||||
tag="role.mounting_plate",
|
||||
topology_propagation=scad.TopologyPropagation.DOWNWARD,
|
||||
)
|
||||
|
||||
face_hits = [face for face in body.get_faces() if "role.mounting_plate" in scad.list_tags(face)]
|
||||
edge_hits = [edge for edge in body.get_edges() if "role.mounting_plate" in scad.list_tags(edge)]
|
||||
face_hits = scad.select_faces_by_tag(
|
||||
solid=tagged,
|
||||
tag="role.mounting_plate",
|
||||
scope=scad.TagScope.INHERITED,
|
||||
)
|
||||
edge_hits = scad.select_edges_by_tag(
|
||||
shape=tagged,
|
||||
tag="role.mounting_plate",
|
||||
scope=scad.TagScope.INHERITED,
|
||||
)
|
||||
|
||||
print(len(face_hits), len(edge_hits))
|
||||
```
|
||||
@@ -55,7 +71,13 @@ Primitives and modeling operations may attach normalized tags automatically:
|
||||
- Primitive tags such as `geom.primitive.box`, `geom.primitive.cylinder`, and `geom.primitive.sphere`.
|
||||
- Face tags from `auto_tag_faces(...)`, such as `face.top`, `face.bottom`, `face.side`, and `face.surface`.
|
||||
- Wire tags such as `wire.outer` and `wire.inner`.
|
||||
- Operation/tracking tags such as `solid.boolean.cut`, `op.cut.modified`, or `op.extrude.generated`.
|
||||
- Operation-level categorical tags such as `solid.boolean.cut` may remain local annotations.
|
||||
|
||||
Operation events and source roles are not tags. Proven `preserved`, `modified`,
|
||||
or `generated` events and `body`/`tool` origins live in typed
|
||||
`metadata["track"]`. Query them with `ql.operation_event(...)` and
|
||||
`ql.origin_role(...)`. Missing correspondence remains `coverage="partial"` and
|
||||
`status="unknown"`; it is never promoted to `generated` by default.
|
||||
|
||||
## Metadata Methods
|
||||
|
||||
@@ -84,7 +106,7 @@ scad.apply_tag(body, "role.mounting_plate")
|
||||
body.auto_tag_faces("box")
|
||||
|
||||
top_faces = Q.select(body.get_faces()).where(Q.tag("face.top")).all()
|
||||
role_faces = Q.select(body.get_faces()).where(Q.tag("role.*")).all()
|
||||
role_faces = Q.select(body.get_faces()).where(Q.tag("role.*", scope="effective")).all()
|
||||
|
||||
print(len(top_faces), len(role_faces))
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user