diff --git a/CHANGELOG.md b/CHANGELOG.md index 9feca027..aac885dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,29 @@ user-facing notes live in [`docs/updates/`](docs/updates/) (English and ## Unreleased +## 2.2.8 — 2026-11-09 + +User-facing notes: [`docs/updates/2.2.8.md`](docs/updates/2.2.8.md). + +### Added +- Editing existing curves and surfaces as recorded operations (the SDK half of NURBS control-point editing): + `inspect_curve_rcurvedata` / `inspect_surface_rsurfacedata` read degree, poles, weights, knots and trimming; + `set_*_poles`, `move_*_poles`, `set_*_weights`, `insert_*_knot`, `elevate_*_degree` (curves `_redge`, surfaces + `_rface`), `convert_to_bspline_redge/rface` (the explicit step for lines, circles, planes, cylinders) and + `rebuild_curve_redge/rface` return a new shape, record a topology delta (tags and references follow the face and + its edges) and replay from model JSON. A trimmed face keeps its trimming. `sample_curve_rcurvature` and + `sample_surface_risocurves` give an editor a curvature comb and clipped isocurves. + +### Changed +- The editor offers its code mode only. Manual modelling (features, sketches, assemblies by buttons and dialogs) is built + but switched off (`editor-web/src/lib/manual-mode.ts`) until it has a plan of its own: no mode switch, command bar or + manual keys (`/`, `W`, Space, Ctrl+Z on features, `6`). The viewport keeps what was made for it: three's WebGPURenderer + (WebGL 2 as the fallback), level of detail (a part seen up close is tessellated again by the kernel from the in-memory + BREP, `POST /api/preview/refine`), instanced parts, BVH picking, and smooth shading from exact surface normals. + +### Fixed +- A stable reference into a face or shell body (no solid) resolves; it raised `cannot resolve QL selector scope`. + ## 2.2.7 — 2026-10-30 ### Fixed diff --git a/README.md b/README.md index f573e0d4..6e43950c 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,11 @@ SimpleCADAPI repository cover

-# SimpleCADAPI +

SimpleCADAPI

-[中文说明](README.zh-CN.md) +

Parametric CAD built for agents.
Efficient · Low-overhead · Agent-native

+ +

中文 · Changelog · Skill design · Paper

**Start here.** Paste this line to your coding agent (Claude Code, Codex, OpenCode, …): @@ -12,758 +14,135 @@ Read physpace.dev/sca/landing.md and landing me on this sdk. ``` -It installs the SDK, installs the skill for itself, opens a real example in the -code-first editor, and shows you how to work with it. SimpleCADAPI is a -parametric CAD SDK built for agents: a model is a notebook of code, every -dimension is a named parameter, selection is by what geometry *is*, and every -step reports back why it worked or failed. - -## What Can It Do - -Four capability pillars. Every case below is a **real, reproducible session**: -the replay demos are single-file offline HTML pages that faithfully replay the -full recorded conversation (user turns, agent thinking, every tool call and -patch, role switches) next to the exported 3D model — open them directly in a -browser. - -### 1 · Single-Part Modeling - -Parametric parts authored as readable [Feature Tree -Convention](docs/skill/references/discipline/feature-tree-convention.md) -feature blocks — named parameters with units, verifier-first staged modeling, -deterministic face tags that survive re-parameterization, and synchronized -`.scadpkg` / STEP AP242 / STL / editable FreeCAD exports. - - - - - - - - - - - - - - - - - - - - - - -
CaseFirst-turn requirement (excerpt)Delivered model · BRep turntable
Robotic-arm U-link + motor mount
15 user turns · 601 tool calls
▶ Full session replay
"Build a parametric robotic-arm link that adapts to length changes and mounts motors generically: sweep a circular profile down D → right L → up D into a U; cut motor sockets with spheres; the mounting face must carry a deterministic name indexable by the query language; split the bottom cylinder into a half cylinder; then fillet everything."u_link turntable
Parametric flange plate
2 user turns · 66 tool calls
▶ Full session replay
"Parametric flange: OD 100, thickness 10, hub ⌀55 top +30, bore ⌀30, 6×⌀11 bolt holes on PCD 78, hub-root R3 / rim R2 fillets; every dimension a named parameter; edge-selection cards printed before each fillet; parameter-feasibility guards; one feature per block."

GIF shows the final 8-hole @PCD 84.5 state: turn 2 re-parameterized 6→8 holes; PCD 88 was rejected live by the guards and 85 excluded for fillet tangency.
flange turntable
Ribbed L-bracket (FEM main model)
2 user turns · 49 tool calls
▶ Full session replay
"Formalize the existing legacy L-bracket script through the single-part workflow: rebuild with FTC feature blocks and named parameters; geometric equivalence to the legacy model (volume delta < 0.1%); the interface.* FEM boundary tags must survive untouched — the downstream Gmsh/CalculiX pipeline selects faces by them."bracket turntable
- -Turntables are rendered with BRep shading and wide edges; one full turn = 48 -deterministic azimuth steps. - -### 2 · Assembly Modeling - -Nested durable assemblies with explicit kinematics: gear meshes, revolute -joints, and bearing interfaces are solved constraints, not eyeballed -positions. Standard parts — involute gears, ball bearings (as subassemblies -with individual balls), roller-chain sprockets, metric fasteners with real -thread profiles — come from `scad.std.*` and compose into mechanisms that -export to STEP, editable FreeCAD projects, and MJCF for physics engines. - - - - - - -
-BLDC joint actuator assembled
-Integrated BLDC joint actuator — assembled, studio render
-29 components · 51 constraints, all solved · 20:1 two-stage planetary -
-BLDC joint actuator exploded turntable
-Same model, exploding turntable — four modules (ESC · motor ·
-two-stage reducer · output shaft) separate along the axis while
-concentric parts peel into radius bands; camera circles on an inclined orbit -
- -Reproduce it: the model is the `integrated_bldc_joint_actuator.py` assembly -notebook, composed with `scad.use` from part and sub-assembly notebooks. -`uv run python examples/integrated_bldc_joint_actuator/export_all.py` emits -the package plus STEP / editable FCStd / MJCF; `render_showcase.py` renders -the views above. - -### 3 · Code-first Editor - -`sca edit` opens a browser editor over a directory of notebooks, built for -working *with* an agent. The notebook's cells are on the left and the live -model is in the middle; a slider or toggle appears for every parameter. Click a -solid, face, edge or vertex and the editor writes a `scad.ql` selection cell -into the notebook file (autosaved), so your coding agent reads what you picked -from disk and "chamfer these two edges" needs no ids. Copy a cell's reference -to tell the agent which cell to change. A log dock shows which cells are being -re-evaluated. The editor and `sca run` share one runtime and one per-cell -cache. - - - - - -
-sca edit on the u_link motor mount: a picked face, the selection cell it wrote, its properties
-u_link motor mount in sca edit — a picked cylindrical face, the -selected_face cell the editor wrote for it, and its properties; -sca example u_link copies this workspace out -
- -### 4 · Simulation Plugins - -The same parametric package feeds downstream solvers without manual rework: -AP242 STEP into Gmsh for volume meshing and CalculiX for static FEM (boundary -faces are selected by preserved `interface.*` tags, so simulation survives -model revisions), and MJCF into MuJoCo for mechanism dynamics — where a -virtual-collision pass catches assembly interference and drives the correction -before the mechanism runs clean. - - - - - - - - - -
-Bracket FEM von Mises
-L-bracket static FEM — CalculiX von Mises
-meshed via Gmsh OpenCASCADE kernel from the AP242 export -
-Four-bar assembly with links crashing into each other in MuJoCo
-Four-bar linkage + MuJoCo — first assembly pass:
-the links crash through each other mid-cycle; the assembly is wrong · -▶ full video -
-Corrected four-bar assembly articulating clean in MuJoCo
-After the virtual-collision correction — corrected assembly:
-the full crank cycle sweeps clean in the same sim · -▶ full video -
+It installs the SDK, installs the skill for itself, opens a real example in the editor, and shows you how to work with it. --- -## Update Notes (2.2.7) - -> Validate generated definitions, assembly constraints, and manufacturing -> geometry before production use. +## The idea -- **First stable release of the C++ engine**, inside prebuilt wheels (Linux x86_64 and aarch64, macOS arm64 14+, Windows - x86_64; Python 3.10-3.13; one OpenCASCADE 7.9.3). `pip install simplecadapi` needs no `cadquery-ocp`. -- **The editor is faster.** On the six-axis arm a joint angle never seen before takes 2.7 s (was 5.7 s in the beta), one seen - before 0.6 s (was 5.5 s), and the model reaches the page 0.36-0.41 s after the last cell. The kernel uses about 300 MB more - memory at idle than the beta. -- **`Var` and `Expr` behave like numbers**: `r > 3`, `int(r)`, `round(r, 2)`, `range(n)`, `max(r, 3)`, f-strings work, and - a ql predicate with a value computed from a `Var` matches. `bool(var)` and `json.dumps(var)` still raise. -- **`shape.wrapped` is an OCP shape** when OCP is installed (`simplecadapi[verify]`), a copy, so OCCT verifier scripts run as written. -- Failing fillets, chamfers and ql selections attach their picture again. Notebooks with saved parameter values rebuild once. +CAD software was made for a person with a mouse. An agent has no mouse. It has code, a context window and a token bill. +SimpleCADAPI starts from that. -Full notes for this version: [`docs/updates/2.2.7.md`](docs/updates/2.2.7.md). -Earlier versions: the [changelog](CHANGELOG.md) and [`docs/updates/`](docs/updates/). +| | | +| --- | --- | +| **Efficient** | More first attempts come out right, and fewer tokens are spent getting there. Change one parameter and only the cells that depend on it run again. | +| **Low-overhead** | One `pip install`: the geometry engine is native C++ on OpenCASCADE inside the wheel, with no CadQuery or OCP stack. A functional API with no hidden state, so every step is a cell that can be cached, checked and replayed. A skill an agent reads in one batch, not twenty. | +| **Agent-native** | Errors say what happened, give the measured numbers and say how to fix it. Geometry is selected by what it *is* (`ql`, stable `scad.ref`), not by an index that moves. Every call is recorded as a replayable graph. The editor writes what you click into the notebook, so your agent reads your selection from disk. | ---- +## The evidence -
-

SimpleCADAPI Research Artifact

-

This repository is an artifact of

-

- CADIR: A Cross-Backend Editable Intermediate Representation for Agentic CAD Generation -

-
+**Against CadQuery.** Same model, same prompt, same 200 real parts, one attempt each. A frozen, independent scorer compares each result with the original. ---- +

Same model and prompt on 200 parts: SimpleCADAPI delivers 3.5 times fewer badly wrong models than CadQuery

-SimpleCADAPI is an OpenCASCADE-native Python SDK for building CAD models with clear, -functional operations and replayable model graphs. It wraps OpenCascade geometry -in a compact public API for creating solids, applying features, tagging semantic -intent, querying topology, exporting manufacturing files, and translating recorded -models into FreeCAD workflows. - -Current release: `simplecadapi==2.2.7`. - -## What It Provides - -- Models as [marimo](https://marimo.io) notebooks: plain `.py` files that are - the only source of truth, one feature per cell, a per-cell cache for - incremental headless runs (`sca run`), and notebook composition (`scad.use`). -- A code-first web editor, `sca edit`: live model, parameter controls, viewport - picks written back as selection code, built for working alongside an agent. -- OpenCASCADE-native shape types: `Vertex`, `Edge`, `Wire`, `Face`, and `Solid`. -- Functional modeling operations for primitives, profiles, extrude, revolve, - loft, sweep, booleans, transforms, patterns, fillets, chamfers, and shells. -- Canonical `.scadpkg` product packages generated from the notebooks for - exchange and publishing; they are never read back as model input. -- Replayable operation graphs with explicit `GraphSession`, - `export_model_json(...)`, `import_model_json(...)`, and `replay_model_json(...)`. -- Expression parameters with `var(...)`, arithmetic expressions, and serialized - expression graphs. -- Physical units with automatic dimension inference, canonical CAD conversion, - and manufacturing tolerance-chain validation. -- QL selectors for geometry grounding, topology queries, and stable feature - selections. -- Semantic tags through `apply_tag(shape=..., tag=...)` and `list_tags(shape=...)`. -- STEP/STL export, editable FreeCAD product-package translation, and AP242 - product-structure export with material and named metadata properties. -- Agent-oriented STEP/BREP reconstruction with stable entity IDs, focused local - diagnostics, highlighted region renders, and measured acceptance gates. -- Replayable open and periodic interpolated B-spline Edges/Wires for freeform - profiles and Loft sections. +DeepSeek-V4.1-Flash, SimpleCADAPI 2.2.7b1 vs CadQuery 2.8.0, 200 open-hardware parts from 27 projects. "Badly wrong" is a delivered model whose surface F1 at 1 mm is below 99.99 %. Disclosed in the report: the SimpleCADAPI run also had a STEP-reconstruction workflow guide, the CadQuery run a general third-party CadQuery skill; the parts are mostly simple (median 19 faces). -## Install +**Against a mature industrial CAD.** Tokens spent per task; the industrial CAD is driven through its MCP server. -The shortest way is the line at the top of this page: paste it to your coding -agent and it does the rest. +

Tokens spent per task: SimpleCADAPI uses up to 6.5 times fewer than a mature industrial CAD through its MCP

-By hand: +## What it builds -```bash -pip install simplecadapi -``` +Two six-axis arms, each an assembly of part notebooks composed with `scad.use`. Every joint angle and link length is a named parameter: drag a slider and the model follows. -The code-first editor (`sca edit`) needs its extra, `pip install "simplecadapi[editor]"`; -`uv tool install "simplecadapi[editor]"` or `pipx install "simplecadapi[editor]"` put -`sca` on your PATH without a virtual environment. `sca example u_link` copies a -bundled example workspace out to open with `sca edit`. +

The SG90 six-axis arm in the SimpleCADAPI editor: notebook cells and joint sliders on the left, the live assembly on the right

-With `uv`: +**SG90 educational arm** · 34 parts, six revolute joints solved as constraints, 3D-printable links, reserved envelopes for the electronics. `sg90_arm_6dof.py` -```bash -uv add simplecadapi -``` +

The desktop six-axis arm with a gripper in the SimpleCADAPI editor

-For local development from this repository: +**Desktop six-axis arm with gripper** · 34 parts. The joint motor is a purchased part with its real mounting interface (34 × 20 × 19.6 mm body, six M2 holes on a Ø10.5 circle), and the links carry the matching pockets, screw channels and plug windows. `arm6.py` -```bash -uv sync --group dev -``` +The editor, `sca edit`, is code-first: cells on the left, the live model on the right, a slider for every parameter. Click a solid, face, edge or vertex and it writes a selection into the notebook. Your agent reads what you picked from the file, so "chamfer these two edges" needs no ids. The editor and `sca run` share one runtime and one per-cell cache; on the six-axis arm a joint angle already seen re-renders in 0.6 s, a new one in 2.7 s (2.2.7). -### Agent Skill +## The skill -After installing with pip above, install the bundled skill for your agent -harness: +The skill is how an agent learns the SDK. It is a router and a library of small pages, written so the agent reads exactly what a step needs, in one parallel batch. -```bash -sca skill targets -# Default location (~/.agents/skills), or select ZCode's explicitly: -sca skill install --target zcode --skills-dir ~/.zcode/skills -``` +**What it does** — seven routes, each a complete workflow with its own checks: -`targets` lists the available harness targets. `install` compiles the bundled -source and writes it to `/simplecadapi`; without `--skills-dir`, -it uses the existing addon configuration/environment resolution, falling back -to `~/.agents/skills`; `sca init` is not required. The wheel includes the -uncompiled `docs/skill/` source tree and `skillproj.toml` as package -resources — compilation happens on install, no repository checkout needed. -With `uv`, prefix these commands with `uv run`. - -`install` fails if the destination already exists. Add `--force` to replace it -only if it is a directory whose `SKILL.md` declares the matching skill name -(`simplecadapi`); unrelated directories cannot be overwritten. - -## Quick Start - -A model is a [marimo](https://marimo.io) notebook: a plain `.py` file in your -repository, which stays the only source of truth. A `[tool.simplecadapi]` -table in its PEP 723 header names the product; each cell holds one -[feature block](docs/skill/references/discipline/feature-tree-convention.md) -and records its geometry automatically — no session or decorator boilerplate. - -```python -# bracket.py -# /// script -# requires-python = ">=3.10" -# dependencies = ["simplecadapi"] -# -# [tool.simplecadapi] -# id = "bracket" -# revision = "1.0.0" -# /// -import marimo - -app = marimo.App() - -with app.setup: - import simplecadapi as scad - - -@app.cell -def _(): - # ---- params: plate ---- - width = scad.var("width", 60.0, unit="mm") - return (width,) - - -@app.cell -def _(): - # ---- params: bore ---- - hole_radius = scad.var("hole_radius", 5.0, unit="mm") - return (hole_radius,) - - -@app.cell -def _(width): - # ---- feature: base-plate (build) ---- - base = scad.make_box_rsolid( - width=width, height=36.0, depth=8.0, bottom_face_center=(0.0, 0.0, 0.0) - ) - return (base,) - - -@app.cell -def _(base, hole_radius): - # ---- feature: bore-and-slot (subtract) ---- - _bore = scad.make_cylinder_rsolid( - radius=hole_radius, height=14.0, bottom_face_center=(0.0, 0.0, -3.0) - ) - _slot = scad.make_box_rsolid( - width=18.0, height=8.0, depth=14.0, bottom_face_center=(14.0, 0.0, -3.0) - ) - drilled = scad.cut_rsolid(base, _bore, _slot) - return (drilled,) - - -@app.cell -def _(drilled): - # ---- feature: boss (add) ---- - _boss = scad.make_cylinder_rsolid( - radius=8.0, height=7.0, bottom_face_center=(-18.0, 0.0, 8.0) - ) - bossed = scad.union_rsolid(drilled, _boss) - return (bossed,) - - -@app.cell -def _(bossed): - # ---- feature: role-tag (annotate) ---- - tagged = scad.apply_tag(shape=bossed, tag="role.demo.bracket") - return (tagged,) - - -@app.cell -def _(tagged): - bracket = scad.Part(part_id="bracket", body=tagged) - return (bracket,) - - -if __name__ == "__main__": - app.run() -``` +| Ask | Route | +| --- | --- | +| Model one physical part | single-part modeling: requirements → plan → build → verify | +| Build a multi-part product or mechanism | assembly: connectors, constraints, kinematics, standard parts | +| Rebuild an editable model from a STEP file | STEP reconstruction with stable entity ids and acceptance gates | +| Rebuild from a mesh, point cloud or photos | scan reconstruction | +| Rebuild from a vector-PDF drawing | drawing reconstruction | +| Produce a GB engineering drawing (DXF) | engineering drawing from validated geometry | +| Hand the model to someone else | export and translation: STEP AP242, STL, FreeCAD, MJCF | -The product is the top-level value whose id equals the notebook id. Edit the -notebook interactively, or run it headlessly with only `simplecadapi` -installed: +**How it is organized** -```bash -marimo edit bracket.py # reactive editor -sca run bracket.py # run, print a JSON report -sca run bracket.py --set width=80 --out out/bracket.scadpkg -sca export out/bracket.scadpkg --output-dir out/exports # AP242 STEP, STL, OBJ -sca edit . # web editor over this directory of notebooks -``` - -`sca edit` (`pip install "simplecadapi[editor]"`) is the code-first editor -described above: the notebook's cells on the left, the live model in the -middle, a slider or toggle for every parameter (`# ---- params ----` cells; a -`# ui:` comment on the line picks the control), and your viewport picks written -back as `selected_*` selection cells. See -`docs/skill/references/docs/guides/notebook-runtime.md` and -`docs/skill/references/domains/editor-collaboration.md` (how an agent works with -what you picked and which cell you mean). - -## Notebook Runtime - -The same runtime runs a notebook in the marimo editor, under `sca run`, and -from Python with `simplecadapi.runtime.run_notebook()`. Headless runs cache -every cell, so a rerun only executes the cells whose code, inputs, local -modules, or child notebooks changed. `--set` overrides a top-level variable -(the cell that defines it is skipped, so group parameters into cells by what -changes together); the report lists each cell as `ran`, `cached`, or -`skipped`. - -Export and verification scripts are plain Python that load the product with -`run_notebook` and write packages from it: - -```python -from pathlib import Path - -import simplecadapi as scad -from simplecadapi.runtime import run_notebook - -out = Path("out") -run = run_notebook("bracket.py", overrides={"width": 80.0}) -print("volume", round(run.product.body.get_volume(), 3)) -print("tags", scad.list_tags(shape=run.product.body)) - -package = out / "bracket.scadpkg" -scad.capture(run.definition, package) -scad.exporter.export_product_package_to_step(package, out / "bracket.step") -scad.exporter.export_product_package_to_stl(package, out / "bracket.stl") -scad.exporter.export_product_package_to_obj(package, out / "bracket.obj") -``` - -The `.scadpkg` embeds the definition closure, evaluated scene, feature -graphs, source snapshots, topology, and render/selection assets. It is -generated for exchange and publishing, never read back as model input. - -### Composing notebooks - -An assembly notebook brings in other notebooks with `scad.use`. A relative -path is relative to the calling notebook, and keyword arguments override the -child's top-level variables: - -```python -@app.cell -def _(): - bracket = scad.use("bracket.py", width=80.0) - return (bracket,) - - -@app.cell -def _(bracket): - rig = scad.make_assembly_rassembly(assembly_id="rig", name="Rig") - rig = scad.add_component_rassembly( - assembly=rig, item=bracket, component_id="bracket", - placement=scad.identity_placement_rplacement(), - ) - return (rig,) -``` - -One notebook can describe a part family — parts that differ only in their -parameters, such as the links of a linkage. Its product takes -`part_id=scad.notebook_id()`, and each use names its member: -`scad.use("link_bar.py", id="crank", center_distance=40.0)` (or -`sca run link_bar.py --id crank`). Each member is one definition with its own -content hash and cell cache. - -`@scad.part` / `@scad.assemble` remain for reusable library builders in plain -modules next to the notebooks; cells call them and take `.value`. See the -[notebook runtime and product build workflow](docs/skill/references/docs/guides/notebook-runtime.md) -for the project layout, the cell cache, and downstream targets. - -### Migrating from `@scad.part` scripts - -The script-anchored part cache of 2.1.2 (the `cache=` builder option, the -`.simplecad` cache directories, and the incremental assembly report) is -removed; the cell is now the unit of caching. - -- Move each part into a notebook: its parameters into params cells, each FTC - block into a cell of its own (each cell binds a new name), and the final - `scad.Part(...)` / assembly into the last cell. -- Replace `main.py` entry points that build and export as a side effect with - export scripts that call `run_notebook` and `scad.capture`. -- Replace direct builder calls between products with `scad.use(...)`; keep - `@scad.part` builders only for library parts called from cells. -- Add `__marimo__/` (generated runtime state) to `.gitignore`. - -### Command-line product export - -Export the standard delivery set (AP242 STEP, binary STL, and OBJ) from a -validated product package with no wrapper script: - -```bash -uv run sca export out/bracket.scadpkg --output-dir out/exports +```text +SKILL.md router: pick one route, read its batch (≤ 130 lines) +workflows/ one lifecycle per route; roles and stages load when they start +domains/ what the SDK offers for a kind of task +discipline/ rules and invariants: core rules, feature order, verifiers, failure and repair +docs/api · stdlib · core 400+ generated signature pages; a method with no page does not exist ``` -Request additional targets explicitly. FCStd requires `FreeCADCmd` (or an -explicit `--freecad-cmd` path); `--check` validates the package, output paths, -and selected target prerequisites without writing files. +- **Small reading budget.** The first batch of any route is at most 1,200 lines. Every page has one job, and every rule has one home. A build check fails the skill if a page breaks the contract. +- **Two modes.** Every workflow runs interactive (a person can answer) or unattended (nobody will). The mode changes who decides, never what is checked. +- **One source, seven harnesses.** The same pages compile for Claude Code, Codex, OpenCode, ZCode, OMP, DSH and Kimi Code. ```bash -uv run sca export out/bracket.scadpkg \ - --format fcstd --format mjcf --output-dir out/exports --check -uv run sca export out/bracket.scadpkg \ - --format fcstd --freecad-cmd /path/to/FreeCADCmd --output-dir out/exports -``` - -STL and OBJ share one direct OpenCASCADE tessellation of the evaluated BREP. -Both outputs contain the same oriented triangles and require no optional -remeshing dependency. Control curved-surface accuracy with `linear_deflection` -and `angular_deflection_degrees`. - -## Replayable Operation Graphs - -Use an explicit `GraphSession` when a geometry flow outside a notebook should -be inspectable, serializable, or replayable. - -```python -import simplecadapi as scad -from simplecadapi import GraphSession, export_model_json, replay_model_json -from simplecadapi import ql as Q - -with GraphSession(graph_id="chamfered_block") as session: - body = scad.make_box_rsolid( - width=40.0, height=24.0, depth=10.0, - bottom_face_center=(0.0, 0.0, 0.0), - ) - cutter = scad.make_cylinder_rsolid( - radius=4.0, height=16.0, bottom_face_center=(0.0, 0.0, -3.0) - ) - drilled = scad.cut_rsolid(body, cutter) - - bottom_circle = ( - Q.edges() - .where(Q.curve_type(kind="circle")) - .order_by(Q.center_axis(axis="z")) - .take(1) - .exactly(1) - ) - final = scad.chamfer_rsolid(solid=drilled, edges=bottom_circle, distance=0.6) - session.capture_result(value=final) - model_json = export_model_json(session=session) - recorded_nodes = session.graph.node_count - -rebuilt = replay_model_json(json_str=model_json) -print("recorded_nodes", recorded_nodes) -print("replayed_outputs", len(rebuilt)) -``` - -An explicit `GraphSession` is for inspection, serialization, and replay -outside a notebook; notebook cells already record into sessions of their own, -so do not open one there. It stays in memory until an export API is called. -A durable CAD/viewer deliverable is a notebook product captured to -`.scadpkg` (see [Notebook Runtime](#notebook-runtime)). - -## STEP/BREP Inspection - -Synchronized STEP views, highlighted regions and slice overlays need nothing -beyond `pip install simplecadapi`: they are drawn by the built-in software -renderer (no GPU, no display server, no VTK). Only the engineering-drawing -family (`simplecadapi.inspect.drawing`) needs the `drawing` extra. - -Inspection lives under `simplecadapi.inspect.brep`. These APIs are diagnostic -tools, not modeling operations: they do not enter the graph and are rejected -inside `GraphSession`. Export or obtain the geometry first, then -inspect it outside the modeling script. - -Choose calls from the evidence required by the case instead of following a -fixed reverse-engineering pipeline. Start with bounded global and local facts; -add sections, component renders, boundary distance, material difference, or -strict topology comparison only when those facts answer the current question. - -```python -from simplecadapi.inspect import brep - -summary = brep.inspect_step_rsummary( - path="target.step", - include_parameter_groups=True, -) -face = brep.inspect_step_entity_rdescriptor( - path="target.step", - entity_id="face:0", -) - -print("faces", summary["face_count"]) -print("carrier", face["geometry"]["type"]) +sca skill targets # the harnesses +sca skill install # compile for yours and install ``` -Use the [Reconstruction Agent test specification](docs/skill/references/docs/guides/reconstruction-agent-test-prompt.md) -for controlled runs and the [STEP reconstruction workflow](docs/skill/references/workflows/step-reconstruction.md) -for the inspection primitives, modeling loop, replay checks, and acceptance gates. - -## Physical Units And Tolerances - -Declare nominal and manufacturing-tolerance units at the variable boundary. -SimpleCAD evaluates lengths in millimeters and angles in degrees while preserving -the declaration units in model JSON: - -```python -import simplecadapi as scad +## Plugins -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) +Modeling is the SDK's job. Checking, simulating and translating a finished model belong in a plugin, and a plugin is a repository: -analysis = scad.analyze_tolerance(diagonal) -check = scad.check_tolerance(diagonal, 0.3, tolerance_unit="mm") - -print(analysis.dimension.name, analysis.unit.symbol) -print(analysis.nominal, analysis.lower_bound, analysis.upper_bound) -print("passes", check.passed) -``` - -Addition and subtraction require matching dimensions. Multiplication, division, -integer powers, and square root derive dimensions. Trigonometric functions require -angle or dimensionless inputs as appropriate. Legacy variables without `unit` -remain supported, but cannot be mixed with unit-declared variables in one -expression. - -## Modeling Mental Model - -- Start from design intent: reference axes, critical profiles, and the features - that produce the final solid. -- Build from lower-dimensional geometry to higher-dimensional geometry: profile - wires/faces first, then solid features such as extrude, revolve, loft, and - sweep. -- Keep operations functional. Create new values with public functions such as - `make_rectangle_rface(...)`, `extrude_rsolid(...)`, `cut_rsolid(...)`, and - `fillet_rsolid(...)`. -- Use tags for semantic intent and selection anchors, for example - `role.mounting.surface`, `anchor.datum.primary`, or `group.fasteners`. -- Store numeric and geometric facts in metadata or graph payloads, not in tags. -- Use QL to ground selections by geometry facts rather than relying on topology - iteration order. -- When an indexed topology pick is intentional, pass the index to the plural - child-geometry getter, such as `get_edges(index)`, `get_faces(index)`, - `get_wires(index)`, or `get_vertices(index)`, so replayable graph workflows - preserve the pick as a geo select node. - -Use model JSON only for graph replay and inspection. External CAD translation and -file export consume the validated `.scadpkg` closure: - -```python -script = scad.translator.freecad_translator.translate_product_package_to_freecad_script( - package_path -) -scad.translator.freecad_translator.translate_product_package_to_fcstd( - package_path, "bracket.FCStd" -) -``` - -Part/Assembly models are written as editable FreeCAD assembly structure: parts are -`App::Part`, assemblies are `Assembly::AssemblyObject`, and components are links. -The exporter namespace owns neutral STEP and STL file output. - -## Examples - -Every example is a self-contained folder: part and assembly notebooks, the -plain modules they import, export and verification scripts, and fresh -artifacts under `examples//out/`. The set covers part modeling, -assemblies, reverse engineering, and FEM — see the category index in -[`examples/README.md`](examples/README.md). - -```bash -# part (quickstart FTC notebook, external verification script included) -marimo edit examples/flange_plate/flange_plate.py -uv run python examples/flange_plate/verify.py - -# assembly (two-stage planetary reducer: part-family notebooks, MJCF export) -sca run examples/compact_two_stage_planetary_reducer/compact_two_stage_planetary_reducer.py -uv run python examples/compact_two_stage_planetary_reducer/export_mjcf.py - -# Gmsh volume mesh (AP242 STEP -> volume mesh) -uv run python examples/ap242_gmsh_volume_mesh/export_step.py -uv run --extra gmsh python examples/ap242_gmsh_volume_mesh/export_fem_mesh.py +```text +sca-addon.toml machine-checked facts: name, version, which SDK versions, how to provision +skill/SKILL.md everything an agent reads about it +... its own code, binaries and docs, in any language ``` -To try the editor on a real model, `sca example u_link` copies a motor mount -(two part notebooks and the assembly that fixes them) out of the package, and -`sca edit u_link_motor_mount` opens it; from a checkout, -`sca edit examples/u_link_motor_mount`. Reconstructing a STEP file (see -`examples/bowl_connector/`) is driven by the agent through the inspection API -and the [STEP reconstruction workflow](docs/skill/references/workflows/step-reconstruction.md). - -## Documentation - -- 2.2.7 update notes: [`docs/updates/2.2.7.md`](docs/updates/2.2.7.md) -- 2.2.7b1 update notes: [`docs/updates/2.2.7b1.md`](docs/updates/2.2.7b1.md) -- 2.2.6.post2 update notes: [`docs/updates/2.2.6.post2.md`](docs/updates/2.2.6.post2.md) -- 2.2.6.post1 update notes: [`docs/updates/2.2.6.post1.md`](docs/updates/2.2.6.post1.md) -- 2.2.6 update notes: [`docs/updates/2.2.6.md`](docs/updates/2.2.6.md) -- 2.2.5 update notes: [`docs/updates/2.2.5.md`](docs/updates/2.2.5.md) -- 2.2.4 update notes: [`docs/updates/2.2.4.md`](docs/updates/2.2.4.md) -- 2.2.3 update notes: [`docs/updates/2.2.3.md`](docs/updates/2.2.3.md) -- 2.2.2 update notes: [`docs/updates/2.2.2.md`](docs/updates/2.2.2.md) -- 2.2.1 update notes: [`docs/updates/2.2.1.md`](docs/updates/2.2.1.md) -- 2.1.3b1 update notes: [`docs/updates/2.1.3b1.md`](docs/updates/2.1.3b1.md) -- 2.1.2 update notes: [`docs/updates/2.1.2.md`](docs/updates/2.1.2.md) -- 2.1.1 update notes: [`docs/updates/2.1.1.md`](docs/updates/2.1.1.md) -- 2.1.0 update notes: [`docs/updates/2.1.0.md`](docs/updates/2.1.0.md) -- Reconstruction Agent test specification: - [`docs/skill/references/docs/guides/reconstruction-agent-test-prompt.md`](docs/skill/references/docs/guides/reconstruction-agent-test-prompt.md) -- Landing script for agents: [`docs/landing.md`](docs/landing.md) -- Editor collaboration (selection, cell references, parameters): - [`docs/skill/references/domains/editor-collaboration.md`](docs/skill/references/domains/editor-collaboration.md) -- STEP reconstruction workflow: - [`docs/skill/references/workflows/step-reconstruction.md`](docs/skill/references/workflows/step-reconstruction.md) -- Notebook runtime and product build workflow: - [`docs/skill/references/docs/guides/notebook-runtime.md`](docs/skill/references/docs/guides/notebook-runtime.md) -- Public API reference: [`docs/skill/references/docs/api/`](docs/skill/references/docs/api/) -- Core type and modeling notes: [`docs/skill/references/docs/core/`](docs/skill/references/docs/core/) -- Serialization and replay details: - [`docs/skill/references/docs/core/serialization/README.md`](docs/skill/references/docs/core/serialization/README.md) -- Dimension tolerance chains: - [`docs/core/dimension-tolerance-chains.md`](docs/skill/references/docs/core/dimension-tolerance-chains.md) -- Physical units and dimension inference: - [`docs/core/physical-units.md`](docs/skill/references/docs/core/physical-units.md) -- Operation graph JSON spec: - [`docs/core/operation_graph_json_spec.md`](docs/skill/references/docs/core/operation_graph_json_spec.md) -- `.scadpkg` package format: - [`docs/skill/references/scadpkg-format.md`](docs/skill/references/scadpkg-format.md) - -## Releasing the Agent Skill - -The skill source tree lives under `docs/skill/` and stays harness-neutral. -`tools/skillbuild.py` wraps the same compiler the `sca skill` CLI uses, for -maintainer builds in this repository: it compiles per harness target (targets -and default output directories are configured in `skillproj.toml`; the outputs -under `skills/simplecadapi-*/` are regenerable build artifacts and are not -committed). Harness-specific text, when it is ever needed, is marked with -`` conditional blocks that compile per target. The wheel -itself never contains precompiled targets — end users install from the -bundled source with `sca skill install`. - -From a clean checkout, update the project version and documentation, then -build and validate the release artifacts: - ```bash -uv sync --group dev -uv run python tools/auto_docs_gen.py --quiet -uv run python tools/skillbuild.py --target omp -uv run pytest test/test_skill_build.py -tar -C skills -czf skills/simplecadapi.tar.gz simplecadapi-omp +sca addon add owner/repo # from GitHub; owner/repo@v1.2.0 pins a tag +sca addon add ./my-addon # a local checkout +sca addon list # what is installed, and whether its runtime works +sca addon update # re-fetch; code is swapped, runtime state is kept ``` -The commands regenerate the API references inside the skill tree, recompile -the omp target, and create `skills/simplecadapi.tar.gz`. Review the source -tree and the compiled output before release: - -```bash -git diff -- docs/skill -tar -tzf skills/simplecadapi.tar.gz | head -``` +- **The agent learns it on install.** The addon's skill lands beside the SDK's as `sca-`, so the agent gets the capability and the instructions together. +- **Isolated by design.** A plugin reads `.scadpkg` product packages across a process boundary and runs in its own environment, so plugin dependencies never touch the one that models geometry. +- **Already in use.** Connector verification (bolted, pinned, press-fit, welded and other joints, checked and then run through FEM) and FEM mesh preparation (fasteners removed, their holes filled, closure proven) are plugins. The AP242 → Gmsh → CalculiX and MJCF → MuJoCo paths below run off the same package. -Commit only the `docs/skill/` source tree; the release workflow builds the -harness outputs and uploads the archive to the GitHub release. Prefer -`sca skill` for installation from a pip-installed package. + + + + + + + + +
+Bracket FEM von Mises
+L-bracket static FEM
CalculiX von Mises on the Gmsh mesh of the AP242 export; faces are chosen by interface.* tags that survive model changes +
+Four-bar links crashing through each other in MuJoCo
+Four-bar linkage, first pass
MuJoCo shows the links crossing: the assembly is wrong +
+Corrected four-bar assembly in MuJoCo
+After the collision pass
the same crank cycle sweeps clean +
-## Development +## Install ```bash -uv sync --group dev -uv run python -m pytest test tests -python3 -m compileall src/simplecadapi +pip install "simplecadapi[editor]" # the SDK and the editor +sca example u_link # a real workspace to open +sca edit u_link # the editor +sca skill install # the skill for your agent ``` -## License - -Apache-2.0, see [`LICENSE`](LICENSE). - -## Community +Python 3.10–3.13 on Linux, macOS and Windows. `uv tool install "simplecadapi[editor]"` puts `sca` on your PATH without a virtual environment. `sca run model.py` runs a notebook headless; `sca export` writes STEP, STL and OBJ. -The group chat currently has too many members for direct QR-code joining. Scan the QR code below to add Teacher Du Peng on WeChat, then ask him for an invitation to the CADDesigner technical community: - -

- Teacher Du Peng's personal WeChat QR code -

+## Research artifact -## Star History +This repository is an artifact of [**CADIR: A Cross-Backend Editable Intermediate Representation for Agentic CAD Generation**](https://arxiv.org/abs/2608.00891). - - - - - Star History Chart - - +Apache-2.0. Validate generated models before production use. diff --git a/README.zh-CN.md b/README.zh-CN.md index 651de4dc..80608637 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -2,9 +2,11 @@ SimpleCADAPI 仓库封面

-# SimpleCADAPI +

SimpleCADAPI

-[English](README.md) +

为 Agent 而生的参数化 CAD。
高效率 · 低开销 · Agent Native

+ +

English · 更新日志 · Skill 设计 · 论文

**从这里开始。** 把下面这句话发给你的 coding agent(Claude Code、Codex、OpenCode 等): @@ -12,639 +14,135 @@ Read physpace.dev/sca/landing.md and landing me on this sdk. ``` -它会帮你安装 SDK、为自己安装 skill、在 code-first 编辑器里打开一个真实示例,并 -教你怎么和它一起工作。SimpleCADAPI 是为 agent 设计的参数化 CAD SDK:模型就是一个 -代码 notebook,每个尺寸都是命名参数,按“几何是什么”来选择对象,每一步都会反馈 -成功或失败的原因。 - -## 它能做什么 - -四块能力支柱。下面每个 case 都是**真实、可复现的会话**:回放 demo 是单文件离线 -HTML,忠实回放完整录制的对话(用户输入、Agent 思考、每一次工具调用与补丁、角色 -切换),旁边就是导出的 3D 模型 —— 浏览器直接打开即可。 - -### 1 · 单零件建模 - -以可读的[特征树规约](docs/skill/references/discipline/feature-tree-convention.md) -特征块编写参数化零件 —— 带单位的命名参数、验证先行的分阶段建模、改参后仍然命中的 -确定性面标签,以及同步产出的 `.scadpkg` / STEP AP242 / STL / 可编辑 FreeCAD 工程。 - - - - - - - - - - - - - - - - - - - - - - -
案例首轮需求(prompt 节选)交付模型 · BRep 旋转展示
机械臂 U 形连杆 + 电机安装槽
15 轮用户输入 · 601 次工具调用
▶ 完整会话回放
“构建参数化的机械臂连杆,能适应长度变化、通用安装电机:圆形 profile 沿『下 D → 右 L → 上 D』扫掠成 U 形;两侧圆珠 boolean cut 切出电机安装槽,安装面必须有确定命名、可被查询语言索引;底部圆柱切成半圆柱;然后全局倒角平滑。”u_link 旋转展示
参数化法兰盘
2 轮用户输入 · 66 次工具调用
▶ 完整会话回放
“参数化法兰盘:外径 100、厚 10、凸台 ⌀55 顶高 30、中心孔 ⌀30、6×⌀11 螺栓孔 @PCD 78、根部 R3 / 外缘 R2 圆角;所有尺寸走命名参数;每次圆角前打印选边卡;参数可行性守卫;一个 feature 一个块。”

GIF 为终态 8 孔 @PCD 84.5:第 2 轮改参 6→8 孔,PCD 88 被守卫当场拒绝、85 因圆角相切排除。
法兰旋转展示
筋板 L 形支架(FEM 主模型)
2 轮用户输入 · 49 次工具调用
▶ 完整会话回放
“把已有的 L 形直角连接件 legacy 脚本按 single-part-modeling 工作流正式化:FTC 特征块 + 命名参数重建;与旧版几何等价(体积偏差 < 0.1%);interface.* FEM 边界标签必须原样保留——下游 Gmsh/CalculiX 按标签选面。”支架旋转展示
- -旋转展示使用 BRep 着色加宽边线渲染;转一整圈 = 48 个确定性方位角步进。 - -### 2 · 装配体建模 - -嵌套持久装配体 + 显式运动学:齿轮啮合、转动副、轴承接口都是被求解的约束,不是目测 -摆放。标准件 —— 渐开线齿轮、带滚珠的轴承子装配、滚子链轮、真实牙型公制紧固件 —— -全部来自 `scad.std.*`,组合成机构后可导出 STEP、可编辑 FreeCAD 工程和面向物理引擎 -的 MJCF。 - - - - - - -
-BLDC 关节执行器装配态
-一体化 BLDC 关节执行器 —— 装配态(影棚渲染)
-29 个组件 · 51 条约束全部解算 · 两级行星减速 20:1 -
-BLDC 关节执行器爆炸旋转
-同一模型 —— 爆炸旋转展示:四个模块(电调 · 电机 ·
-双级减速器 · 输出轴)沿轴向分离,模块内同心零件按半径
-分层剥离;相机沿倾斜圆轨道环绕 -
- -复现方式:模型是装配 notebook `integrated_bldc_joint_actuator.py`,用 -`scad.use` 组合各零件与子装配 notebook。 -`uv run python examples/integrated_bldc_joint_actuator/export_all.py` 导出装配包和 -STEP / 可编辑 FCStd / MJCF;`render_showcase.py` 渲染上述视图。 - -### 3 · Code-first 编辑器 - -`sca edit` 在浏览器里打开一个面向“与 agent 协作”的编辑器,作用于一个 notebook -目录。左边是 notebook 的 cell,中间是实时模型,每个参数都有滑块或开关。点选实体、 -面、边或顶点后,编辑器会把一个 `scad.ql` 选择 cell 写进 notebook 文件(自动保存), -你的 coding agent 直接从磁盘读到你选了什么,“给这两条边倒角”不需要任何 id。复制 -cell 引用就能告诉 agent 要改哪个 cell。底部日志面板显示哪些 cell 正在重算。编辑器 -与 `sca run` 共用同一个运行时和同一份逐 cell 缓存。 - - - - - -
-sca edit 打开 u_link 电机座:选中的面、编辑器写出的选择 cell 及其属性
-sca edit 中的 u_link 电机座 —— 选中的圆柱面、编辑器为它写出的 -selected_face cell,以及它的属性; -sca example u_link 可把这个工作区复制出来 -
- -### 4 · 仿真插件 - -同一份参数化包直接喂给下游求解器,无需人工返工:AP242 STEP 进 Gmsh 体网格 + -CalculiX 静力 FEM(边界面按保真的 `interface.*` 标签选取,仿真链在模型改版后依然 -成立),MJCF 进 MuJoCo 做机构动力学——仿真环境里的虚拟碰撞检查会暴露装配干涉, -驱动修正,直到机构全程干净运动。 - - - - - - - - - -
-支架 FEM von Mises 云图
-L 形支架静力 FEM —— CalculiX von Mises 云图
-经 Gmsh OpenCASCADE 内核从 AP242 导出体网格 -
-MuJoCo 中四连杆相互碰撞贯穿
-四连杆 + MuJoCo —— 首次装配:连杆在运动中撞在一起,装配不对 · -▶ 完整视频 -
-修正后的四连杆在 MuJoCo 中全程干净运动
-虚拟碰撞修正后 —— 同一仿真环境,修正后的装配全程干净运动 · -▶ 完整视频 -
- ---- - -## 更新日志(2.2.7) - -> 投入生产前请验证生成的定义、装配约束和制造几何。 - -- **C++ 引擎的第一个正式版**,随预编译 wheel 发布(Linux x86_64 与 aarch64、macOS arm64 14+、Windows x86_64; - Python 3.10-3.13;统一使用 OpenCASCADE 7.9.3)。`pip install simplecadapi` 不再需要 `cadquery-ocp`。 -- **编辑器更快。** 六轴机械臂上一个没见过的关节角 2.7 s(beta 是 5.7 s),见过的 0.6 s(beta 是 5.5 s), - 模型在最后一个 cell 之后 0.36-0.41 s 到达页面。内核空闲时比 beta 多用约 300 MB 内存。 -- **`Var` 和 `Expr` 像数字一样工作**:`r > 3`、`int(r)`、`round(r, 2)`、`range(n)`、`max(r, 3)`、f-string 可用, - 用 `Var` 算出的值写 ql 谓词也能匹配。`bool(var)` 和 `json.dumps(var)` 仍然报错。 -- **装了 OCP 时 `shape.wrapped` 是 OCP 形状**(`simplecadapi[verify]`),是拷贝,OCCT 验证脚本可以原样运行。 -- fillet、chamfer 和 ql 选择失败重新附带图片。保存了参数值的笔记本会重建一次。 - -本版本的完整说明:[`docs/updates/2.2.7.zh-CN.md`](docs/updates/2.2.7.zh-CN.md)。 -更早的版本:见[更新日志](CHANGELOG.md)和 [`docs/updates/`](docs/updates/)。 - ---- - -
-

SimpleCADAPI 论文成果

-

本仓库是以下论文工作的项目产物:

-

- CADIR: A Cross-Backend Editable Intermediate Representation for Agentic CAD Generation -

-

Computer-Aided Design 2026 接收

-
+它会帮你安装 SDK、为自己安装 skill、在编辑器里打开一个真实示例,并教你怎么和它一起工作。 --- -SimpleCADAPI 是一个基于 OCP 的 Python CAD SDK,提供清晰的函数式建模操作和可重放的模型图。它在 OpenCascade 几何内核之上提供精简的公共 API,可用于创建实体、应用特征、添加语义标签、查询拓扑、导出制造文件,以及将记录的模型转换为 FreeCAD 工作流。 - -当前版本:`simplecadapi==2.2.7`。 - -## 核心能力 - -- 模型即 [marimo](https://marimo.io) notebook:普通 `.py` 文件就是唯一的真相 - 来源,一个 cell 一个特征,按 cell 缓存实现增量无头运行(`sca run`),并可用 - `scad.use` 组合 notebook。 -- Code-first 网页编辑器 `sca edit`:实时模型、参数控件,视口点选写回为选择代码, - 为与 agent 协作而设计。 -- 由 notebook 生成的规范 `.scadpkg` 产品包,用于交换和发布,不会反过来作为模型 - 输入。 -- 基于 OCP 的 `Vertex`、`Edge`、`Wire`、`Face` 和 `Solid` 类型。 -- 支持基本体、轮廓、拉伸、旋转、放样、扫掠、布尔运算、变换、阵列、圆角、倒角和抽壳等函数式建模操作。 -- 通过显式 `GraphSession`、`export_model_json(...)`、`import_model_json(...)` 和 `replay_model_json(...)` 记录并重放操作图。 -- 通过 `var(...)`、算术表达式和可序列化表达式图定义参数。 -- 使用 QL 选择器定位几何、查询拓扑并稳定选择特征。 -- 通过 `apply_tag(shape=..., tag=...)` 和 `list_tags(shape=...)` 管理语义标签。 -- 支持 STEP/STL/OBJ 导出、可编辑 FreeCAD 产品包转换,以及带材料和命名元数据 - 属性的 AP242 产品结构导出。 -- 面向 Agent 的 STEP/BREP 逆向能力,提供稳定实体 ID、局部诊断、区域高亮截图和 - 可测量的验收门槛。 -- 可回放的开放/周期插值 B 样条 Edge 和 Wire,可用于自由轮廓与 Loft 截面。 - -## 安装 - -最快的方式是页面顶部那句话:发给你的 coding agent,剩下的它来做。 - -手动安装,使用 pip: - -```bash -pip install simplecadapi -``` - -代码优先编辑器(`sca edit`)需要 `editor` extra:`pip install "simplecadapi[editor]"`; -`uv tool install "simplecadapi[editor]"` 或 `pipx install "simplecadapi[editor]"` -不用虚拟环境就能把 `sca` 放进 PATH。`sca example u_link` 把内置示例工作区复制出来, -再用 `sca edit` 打开。 +## 理念 -使用 uv: +CAD 软件是为握着鼠标的人做的。Agent 没有鼠标,它只有代码、上下文窗口和 Token 账单。 +SimpleCADAPI 从这一点出发。 -```bash -uv add simplecadapi -``` - -从本仓库进行本地开发: - -```bash -uv sync --group dev -``` - -### Agent Skill - -pip 安装完成后,为你的 agent harness 安装内置 skill: - -```bash -sca skill targets -# 默认位置(~/.agents/skills),或显式指定 ZCode 的 skills 目录: -sca skill install --target zcode --skills-dir ~/.zcode/skills -``` - -`targets` 列出可用的 harness 目标。`install` 编译内置源码并写入 -`/simplecadapi`;不传 `--skills-dir` 时,沿用既有 addon 配置/ -环境解析,默认回退到 `~/.agents/skills`,不要求先执行 `sca init`。wheel -内置的是未编译的 `docs/skill/` 源树和 `skillproj.toml` 资源,编译发生在 -安装时机;无需 checkout 本仓库。使用 `uv` 时,上述命令前缀 `uv run`。 - -`install` 在目标位置已存在时报错。加 `--force` 允许替换,但仅限 -`SKILL.md` 声明了同名 skill(`simplecadapi`)的目录;无关目录不会被覆盖。 - -## 快速开始 - -模型是一个 [marimo](https://marimo.io) notebook:仓库里的一个普通 `.py` 文件, -它始终是唯一的真相来源。PEP 723 头部的 `[tool.simplecadapi]` 表声明产品;每个 -cell 放一个[特征块](docs/skill/references/discipline/feature-tree-convention.md), -几何会被自动记录——不需要 session 或装饰器样板代码。 - -```python -# bracket.py -# /// script -# requires-python = ">=3.10" -# dependencies = ["simplecadapi"] -# -# [tool.simplecadapi] -# id = "bracket" -# revision = "1.0.0" -# /// -import marimo - -app = marimo.App() - -with app.setup: - import simplecadapi as scad - - -@app.cell -def _(): - # ---- params: plate ---- - width = scad.var("width", 60.0, unit="mm") - return (width,) - - -@app.cell -def _(): - # ---- params: bore ---- - hole_radius = scad.var("hole_radius", 5.0, unit="mm") - return (hole_radius,) - - -@app.cell -def _(width): - # ---- feature: base-plate (build) ---- - base = scad.make_box_rsolid( - width=width, height=36.0, depth=8.0, bottom_face_center=(0.0, 0.0, 0.0) - ) - return (base,) - - -@app.cell -def _(base, hole_radius): - # ---- feature: bore-and-slot (subtract) ---- - _bore = scad.make_cylinder_rsolid( - radius=hole_radius, height=14.0, bottom_face_center=(0.0, 0.0, -3.0) - ) - _slot = scad.make_box_rsolid( - width=18.0, height=8.0, depth=14.0, bottom_face_center=(14.0, 0.0, -3.0) - ) - drilled = scad.cut_rsolid(base, _bore, _slot) - return (drilled,) - - -@app.cell -def _(drilled): - # ---- feature: boss (add) ---- - _boss = scad.make_cylinder_rsolid( - radius=8.0, height=7.0, bottom_face_center=(-18.0, 0.0, 8.0) - ) - bossed = scad.union_rsolid(drilled, _boss) - return (bossed,) - - -@app.cell -def _(bossed): - # ---- feature: role-tag (annotate) ---- - tagged = scad.apply_tag(shape=bossed, tag="role.demo.bracket") - return (tagged,) - - -@app.cell -def _(tagged): - bracket = scad.Part(part_id="bracket", body=tagged) - return (bracket,) - - -if __name__ == "__main__": - app.run() -``` - -产品是 id 等于 notebook id 的那个顶层变量。可以在编辑器里交互修改,也可以只装 -`simplecadapi` 无头运行: - -```bash -marimo edit bracket.py # 响应式编辑器 -sca run bracket.py # 运行并输出 JSON 报告 -sca run bracket.py --set width=80 --out out/bracket.scadpkg -sca export out/bracket.scadpkg --output-dir out/exports # AP242 STEP、STL、OBJ -sca edit . # 这个目录 notebook 的网页编辑器 -``` +| | | +| --- | --- | +| **高效率** | 一次尝试就做对的更多,花的 Token 更少。改一个参数,只有依赖它的单元会重新运行。 | +| **低开销** | 一条 `pip install`:几何引擎是 wheel 里的原生 C++(基于 OpenCASCADE),没有 CadQuery 或 OCP 那一整套依赖。函数式 API、没有隐藏状态,每一步都是可以缓存、检查、重放的单元。Skill 让 agent 一批读完所需内容,而不是读二十次。 | +| **Agent Native** | 报错会说清发生了什么、给出带单位的实测数值、告诉你怎么修。按“几何是什么”选择对象(`ql`、稳定的 `scad.ref`),而不是靠会漂移的序号。每次调用都被记录成可重放的图。编辑器把你点选的内容写进 notebook,agent 直接从文件里读到你的选择。 | -`sca edit`(`pip install "simplecadapi[editor]"`)就是上面介绍的 code-first -编辑器:左边是 notebook 的 cell,中间是实时模型,每个参数一个滑块或开关 -(`# ---- params ----` cell;一行末尾的 `# ui:` 注释选择控件),视口点选写回为 -`selected_*` 选择 cell。详见 -`docs/skill/references/docs/guides/notebook-runtime.md` 和 -`docs/skill/references/domains/editor-collaboration.md`(agent 如何使用你的选择和 -指定的 cell)。 +## 数据 -## Notebook 运行时 +**对 CadQuery。** 同一个模型、同一套提示词、同样 200 个真实零件,各只试一次。由独立且已冻结的评分器,把每个结果与原件比对。 -marimo 编辑器、`sca run` 和 Python 里的 `simplecadapi.runtime.run_notebook()` -用的是同一个运行时。无头运行会缓存每个 cell,重跑时只执行代码、输入文件、本地 -模块或子 notebook 发生变化的 cell。`--set` 覆盖一个顶层变量(定义它的 cell -不再运行,所以要按“一起变化”的原则把参数分进不同 cell);报告里每个 cell 的 -状态是 `ran`、`cached` 或 `skipped`。 +

同一模型、同一提示词、200 个零件:SimpleCADAPI 交付的严重失真模型比 CadQuery 少 3.5 倍

-导出和验证脚本是普通 Python 脚本,用 `run_notebook` 载入产品再写出产品包: +DeepSeek-V4.1-Flash,SimpleCADAPI 2.2.7b1 对 CadQuery 2.8.0,来自 27 个项目的 200 个开源硬件零件。“严重失真”指已交付、但 1 mm 阈值下表面 F1 低于 99.99 % 的模型。报告中已披露:SimpleCADAPI 一组另有 STEP 逆向重建的工作流文档,CadQuery 一组用的是通用的第三方 CadQuery skill;零件以简单件为主(面数中位数 19)。 -```python -from pathlib import Path +**对某成熟工业软件。** 每个任务消耗的 Token;该工业软件通过其 MCP 服务驱动。 -import simplecadapi as scad -from simplecadapi.runtime import run_notebook +

每个任务消耗的 Token:SimpleCADAPI 比某成熟工业软件(经其 MCP)最多少 6.5 倍

-out = Path("out") -run = run_notebook("bracket.py", overrides={"width": 80.0}) -print("volume", round(run.product.body.get_volume(), 3)) -print("tags", scad.list_tags(shape=run.product.body)) - -package = out / "bracket.scadpkg" -scad.capture(run.definition, package) -scad.exporter.export_product_package_to_step(package, out / "bracket.step") -scad.exporter.export_product_package_to_stl(package, out / "bracket.stl") -scad.exporter.export_product_package_to_obj(package, out / "bracket.obj") -``` - -`.scadpkg` 包含完整定义闭包、求值场景、特征图、源码快照、拓扑以及渲染/选择 -资源。它由代码生成,只用于交换和发布,不会反过来作为模型输入。 +## 它能做什么 -### 组合 notebook +两台六轴机械臂,都是由零件 notebook 通过 `scad.use` 组合出来的装配体。每个关节角和连杆长度都是命名参数:拖动滑块,模型跟着动。 -装配 notebook 用 `scad.use` 引入其他 notebook。相对路径相对于调用方 notebook, -关键字参数覆盖子 notebook 的顶层变量: +

SimpleCADAPI 编辑器里的 SG90 六轴机械臂:左侧是 notebook 单元和关节滑块,右侧是实时装配体

-```python -@app.cell -def _(): - bracket = scad.use("bracket.py", width=80.0) - return (bracket,) +**SG90 教学机械臂** · 34 个零件,六个转动关节作为约束被求解,连杆可 3D 打印,为电子元件预留了空间。`sg90_arm_6dof.py` +

SimpleCADAPI 编辑器里带夹爪的桌面六轴机械臂

-@app.cell -def _(bracket): - rig = scad.make_assembly_rassembly(assembly_id="rig", name="Rig") - rig = scad.add_component_rassembly( - assembly=rig, item=bracket, component_id="bracket", - placement=scad.identity_placement_rplacement(), - ) - return (rig,) -``` +**带夹爪的桌面六轴机械臂** · 34 个零件。关节电机是带真实安装接口的采购件(机体 34 × 20 × 19.6 mm,Ø10.5 圆周上六个 M2 孔),连杆上开有对应的腔体、螺钉通道和插接窗口。`arm6.py` -一个 notebook 可以描述一个零件族——只有参数不同的一组零件,比如连杆机构里的 -各根连杆。产品写成 `part_id=scad.notebook_id()`,每处使用时给成员起名: -`scad.use("link_bar.py", id="crank", center_distance=40.0)`(或 -`sca run link_bar.py --id crank`)。每个成员是一个独立定义,有自己的内容哈希和 -cell 缓存。 +编辑器 `sca edit` 是代码优先的:左边是单元,右边是实时模型,每个参数都有滑块。点选实体、面、边或顶点,它就把一条选择写进 notebook。Agent 从文件里读到你选了什么,所以“把这两条边倒角”不需要任何 id。编辑器和 `sca run` 共用同一个运行时和同一份逐单元缓存;在六轴臂上,见过的关节角 0.6 秒重新渲染,新角度 2.7 秒(2.2.7)。 -`@scad.part` / `@scad.assemble` 保留给放在 notebook 旁边普通模块里的可复用库 -构建函数;cell 调用它们并取 `.value`。工程目录结构、cell 缓存和下游格式见 -[Notebook 运行时与产品构建工作流](docs/skill/references/docs/guides/notebook-runtime.md)。 +## Skill -### 从 `@scad.part` 脚本迁移 +Skill 是 agent 学会使用 SDK 的方式。它是一个路由页加一批小页面,写成让 agent 恰好读到一步所需的内容,并且一批并行读完。 -2.1.2 的脚本锚定 part cache(构建函数的 `cache=` 选项、`.simplecad` 缓存目录和 -增量装配报告)已删除;现在缓存的单位是 cell。 +**能做什么** —— 七条路线,每条都是带自检的完整工作流: -- 每个零件搬进一个 notebook:参数放进 params cell,每个 FTC 块单独一个 cell - (每个 cell 绑定一个新名字),最后一个 cell 构建 `scad.Part(...)` 或装配体。 -- 用调用 `run_notebook` 和 `scad.capture` 的导出脚本,替换那些顺带构建并导出的 - `main.py` 入口。 -- 产品之间直接调用构建函数的地方改成 `scad.use(...)`;`@scad.part` 构建函数只 - 保留给在 cell 中调用的库零件。 -- 把 `__marimo__/`(生成的运行时状态)加入 `.gitignore`。 +| 你要 | 路线 | +| --- | --- | +| 建一个实体零件 | 单零件建模:需求 → 规划 → 构建 → 验证 | +| 建多零件产品或机构 | 装配:连接器、约束、运动学、标准件 | +| 从 STEP 文件重建可编辑模型 | STEP 逆向重建,稳定实体 id 与验收门 | +| 从网格、点云或照片重建 | 扫描重建 | +| 从矢量 PDF 图纸重建 | 图纸重建 | +| 出国标工程图(DXF) | 由已验证的几何生成工程图 | +| 把模型交给别人 | 导出与转译:STEP AP242、STL、FreeCAD、MJCF | -### 命令行产品导出 +**怎么组织** -不写包装脚本,直接从经过验证的产品包导出标准交付集(AP242 STEP、二进制 STL -和 OBJ): - -```bash -uv run sca export out/bracket.scadpkg --output-dir out/exports +```text +SKILL.md 路由:选一条路线,读它的那一批页面(≤ 130 行) +workflows/ 每条路线一个生命周期;角色与阶段在开始时才加载 +domains/ 某类任务上 SDK 提供什么 +discipline/ 规则与不变量:核心规则、特征顺序、验证脚本、失败与修复 +docs/api · stdlib · core 400+ 页自动生成的签名;没有页面的方法就不存在 ``` -其他格式需显式指定。FCStd 需要 `FreeCADCmd`(或显式的 `--freecad-cmd` 路径); -`--check` 只校验产品包、输出路径和所选格式的前置条件,不写文件。 +- **阅读预算很小。** 任何路线的第一批最多 1,200 行;每页只做一件事,每条规则只有一个家。构建检查会在页面违反约定时让 skill 构建失败。 +- **两种模式。** 每个工作流都分交互式(有人可以回答)和无人值守(没人回答)。模式改变的是谁来决定,而不是检查什么。 +- **一份源码,七个 harness。** 同一批页面可编译给 Claude Code、Codex、OpenCode、ZCode、OMP、DSH 和 Kimi Code。 ```bash -uv run sca export out/bracket.scadpkg \ - --format fcstd --format mjcf --output-dir out/exports --check -uv run sca export out/bracket.scadpkg \ - --format fcstd --freecad-cmd /path/to/FreeCADCmd --output-dir out/exports -``` - -STL 与 OBJ 共用 OpenCASCADE 对求值后 BREP 的直接三角化。两种格式包含相同的 -定向三角面,不再需要可选 remeshing 依赖。曲面精度由 `linear_deflection` 和 -`angular_deflection_degrees` 控制。 - -## 可重放操作图 - -notebook 之外的几何流程需要检查、序列化或重放时,使用显式 `GraphSession`: - -```python -import simplecadapi as scad -from simplecadapi import GraphSession, export_model_json, replay_model_json -from simplecadapi import ql as Q - -with GraphSession(graph_id="chamfered_block") as session: - body = scad.make_box_rsolid( - width=40.0, height=24.0, depth=10.0, - bottom_face_center=(0.0, 0.0, 0.0), - ) - cutter = scad.make_cylinder_rsolid( - radius=4.0, height=16.0, bottom_face_center=(0.0, 0.0, -3.0) - ) - drilled = scad.cut_rsolid(body, cutter) - - bottom_circle = ( - Q.edges() - .where(Q.curve_type(kind="circle")) - .order_by(Q.center_axis(axis="z")) - .take(1) - .exactly(1) - ) - final = scad.chamfer_rsolid(solid=drilled, edges=bottom_circle, distance=0.6) - session.capture_result(value=final) - model_json = export_model_json(session=session) - recorded_nodes = session.graph.node_count - -rebuilt = replay_model_json(json_str=model_json) -print("recorded_nodes", recorded_nodes) -print("replayed_outputs", len(rebuilt)) -``` - -显式 `GraphSession` 用于 notebook 之外的检查、序列化和重放;notebook 的每个 -cell 已经记录进各自的 session,不要在 cell 里再开一个。它在调用导出 API 前只 -存在于内存。持久的 CAD/Viewer 交付物是 notebook 产品捕获出的 `.scadpkg`(见 -[Notebook 运行时](#notebook-运行时))。 - -## STEP/BREP 检查 - -同步 STEP 视图、区域高亮和截面叠加不需要额外安装:`pip install simplecadapi` 即可,由内置软渲染绘制 -(不依赖 GPU、显示服务和 VTK)。只有工程图系列(`simplecadapi.inspect.drawing`)需要 `drawing` 额外依赖。 - -检查 API 位于 `simplecadapi.inspect.brep`。它们是诊断工具,不是建模操作:不进入 -操作图,在 `GraphSession` 内调用会被拒绝。先导出或拿到几何,再在建模脚本之外 -检查。 - -按当前问题需要的证据选择调用,而不是走固定的逆向流程。先取有界的全局和局部 -事实;只有这些事实回答得了当前问题时,才再加截面、组件渲染、边界距离、材料差集 -或严格拓扑比较。 - -```python -from simplecadapi.inspect import brep - -summary = brep.inspect_step_rsummary( - path="target.step", - include_parameter_groups=True, -) -face = brep.inspect_step_entity_rdescriptor( - path="target.step", - entity_id="face:0", -) - -print("faces", summary["face_count"]) -print("carrier", face["geometry"]["type"]) +sca skill targets # 支持的 harness +sca skill install # 为你的 harness 编译并安装 ``` -受控测试请使用 [Reconstruction Agent 测试规范](docs/skill/references/docs/guides/reconstruction-agent-test-prompt.md), -检查原语、建模循环、回放检查和验收门槛请阅读 -[STEP 重建工作流](docs/skill/references/workflows/step-reconstruction.md)。 - -## FreeCAD 转换 +## 插件 -外部 CAD 转换只接受经过验证的 `.scadpkg` 产品包: - -```python -package_path = "out/bracket.scadpkg" -script = scad.translator.freecad_translator.translate_product_package_to_freecad_script( - package_path -) -scad.translator.freecad_translator.translate_product_package_to_fcstd( - package_path, "out/bracket.FCStd" -) -``` +建模是 SDK 的事。对建好的模型做检查、仿真、转译,属于插件;一个插件就是一个仓库: -Part/Assembly 模型写成可编辑的 FreeCAD 装配结构:零件是 `App::Part`,装配是 -`Assembly::AssemblyObject`,组件是链接。中性 STEP 与 STL 文件由 exporter 命名 -空间输出。 - -## 示例 - -每个示例都是一个自包含目录:零件和装配 notebook、它们导入的普通模块、导出与 -验证脚本,以及 `examples//out/` 下的新鲜产物。覆盖零件建模、装配、逆向 -工程和 FEM——分类索引见 [`examples/README.md`](examples/README.md)。 - -```bash -# 零件(快速入门 FTC notebook,附外部验证脚本) -marimo edit examples/flange_plate/flange_plate.py -uv run python examples/flange_plate/verify.py - -# 装配(两级行星减速器:零件族 notebook、MJCF 导出) -sca run examples/compact_two_stage_planetary_reducer/compact_two_stage_planetary_reducer.py -uv run python examples/compact_two_stage_planetary_reducer/export_mjcf.py - -# Gmsh 体网格(AP242 STEP -> 体网格) -uv run python examples/ap242_gmsh_volume_mesh/export_step.py -uv run --extra gmsh python examples/ap242_gmsh_volume_mesh/export_fem_mesh.py +```text +sca-addon.toml 可机器检查的事实:名称、版本、兼容的 SDK 版本、如何准备运行环境 +skill/SKILL.md agent 读到的关于它的一切 +... 它自己的代码、二进制和文档,任何语言都行 ``` -想在真实模型上试用编辑器:`sca example u_link` 把一个电机座(两个零件 notebook 加 -把它们固定在一起的装配)从包里复制出来,`sca edit u_link_motor_mount` 打开它; -在源码仓库里则是 `sca edit examples/u_link_motor_mount`。重建 STEP 文件(见 -`examples/bowl_connector/`)由 agent 通过检查 API 和 -[STEP 重建工作流](docs/skill/references/workflows/step-reconstruction.md)驱动。 - -## 文档 - -- 2.2.7 更新说明:[`docs/updates/2.2.7.zh-CN.md`](docs/updates/2.2.7.zh-CN.md) -- 2.2.7b1 更新说明:[`docs/updates/2.2.7b1.zh-CN.md`](docs/updates/2.2.7b1.zh-CN.md) -- 2.2.6.post2 更新说明:[`docs/updates/2.2.6.post2.zh-CN.md`](docs/updates/2.2.6.post2.zh-CN.md) -- 2.2.6.post1 更新说明:[`docs/updates/2.2.6.post1.zh-CN.md`](docs/updates/2.2.6.post1.zh-CN.md) -- 2.2.6 更新说明:[`docs/updates/2.2.6.zh-CN.md`](docs/updates/2.2.6.zh-CN.md) -- 2.2.5 更新说明:[`docs/updates/2.2.5.zh-CN.md`](docs/updates/2.2.5.zh-CN.md) -- 2.2.4 更新说明:[`docs/updates/2.2.4.zh-CN.md`](docs/updates/2.2.4.zh-CN.md) -- 2.2.3 更新说明:[`docs/updates/2.2.3.zh-CN.md`](docs/updates/2.2.3.zh-CN.md) -- 2.2.2 更新说明:[`docs/updates/2.2.2.zh-CN.md`](docs/updates/2.2.2.zh-CN.md) -- 2.2.1 更新说明:[`docs/updates/2.2.1.zh-CN.md`](docs/updates/2.2.1.zh-CN.md) -- 2.1.3b1 更新说明:[`docs/updates/2.1.3b1.zh-CN.md`](docs/updates/2.1.3b1.zh-CN.md) -- 2.1.2 更新说明:[`docs/updates/2.1.2.zh-CN.md`](docs/updates/2.1.2.zh-CN.md) -- 2.1.1 更新说明:[`docs/updates/2.1.1.zh-CN.md`](docs/updates/2.1.1.zh-CN.md) -- 2.1.0 更新说明:[`docs/updates/2.1.0.zh-CN.md`](docs/updates/2.1.0.zh-CN.md) -- Reconstruction Agent 测试规范: - [`docs/guides/reconstruction-agent-test-prompt.md`](docs/skill/references/docs/guides/reconstruction-agent-test-prompt.md) -- Agent 落地脚本:[`docs/landing.md`](docs/landing.md) -- 编辑器协作(选择、cell 引用、参数): - [`docs/skill/references/domains/editor-collaboration.md`](docs/skill/references/domains/editor-collaboration.md) -- STEP 重建工作流: - [`docs/skill/references/workflows/step-reconstruction.md`](docs/skill/references/workflows/step-reconstruction.md) -- Notebook 运行时与产品构建工作流: - [`docs/guides/notebook-runtime.md`](docs/skill/references/docs/guides/notebook-runtime.md) -- 公共 API 参考:[`docs/api/`](docs/skill/references/docs/api/) -- 核心类型与建模说明:[`docs/core/`](docs/skill/references/docs/core/) -- 序列化与重放:[`docs/core/serialization/README.md`](docs/skill/references/docs/core/serialization/README.md) -- 操作图 JSON 规范:[`docs/core/operation_graph_json_spec.md`](docs/skill/references/docs/core/operation_graph_json_spec.md) -- 示例索引:[`examples/README.md`](examples/README.md) -- `.scadpkg` 产品包格式:[`docs/skill/references/scadpkg-format.md`](docs/skill/references/scadpkg-format.md) - -## 发布 Agent Skill - -skill 源树位于 `docs/skill/`,保持 harness 中立。`tools/skillbuild.py` 是 -`sca skill` CLI 所用同一编译器的封装,供本仓库维护者构建:按 harness 目标 -编译(目标与默认输出目录配置在 `skillproj.toml`;`skills/simplecadapi-*/` -下的输出是可再生的构建产物,不提交入库)。如需 harness 特定文本,用 -`` 条件块标记,按目标编译。wheel 本身不包含任何预编译 -目标——终端用户用 `sca skill install` 从内置源码自行安装。 - -在干净的工作区中更新项目版本和文档,然后生成并验证发布产物: - ```bash -uv sync --group dev -uv run python tools/auto_docs_gen.py --quiet -uv run python tools/skillbuild.py --target omp -uv run pytest test/test_skill_build.py -tar -C skills -czf skills/simplecadapi.tar.gz simplecadapi-omp +sca addon add owner/repo # 从 GitHub 安装;owner/repo@v1.2.0 锁定标签 +sca addon add ./my-addon # 本地目录 +sca addon list # 已装了什么,运行环境是否可用 +sca addon update # 重新拉取;代码被替换,运行时状态保留 ``` -这组命令会重新生成 skill 源树内的 API 参考、重新编译 omp 目标,并生成 -`skills/simplecadapi.tar.gz`。发布前检查源树和编译产物: +- **装上即会用。** 插件的 skill 会以 `sca-<名字>` 装在 SDK 的 skill 旁边,agent 同时得到能力和使用说明。 +- **天然隔离。** 插件通过进程边界读取 `.scadpkg` 产品包,在自己的环境里运行,因此插件的依赖不会碰到做几何建模的那套环境。 +- **已经在用。** 连接器验证(螺栓、销、过盈、焊接等连接,先检查再送入有限元)和有限元网格预处理(移除紧固件、补上它们的孔、证明闭合)都是插件。下面的 AP242 → Gmsh → CalculiX 与 MJCF → MuJoCo 两条链路,用的是同一个产品包。 -```bash -git diff -- docs/skill -tar -tzf skills/simplecadapi.tar.gz | head -``` - -发布时只提交 `docs/skill/` 源树;harness 编译产物由发布工作流生成,归档 -文件上传到 GitHub Release。通过 pip 安装后,优先使用 `sca skill` 构建和安装。 + + + + + + + + +
+支架有限元 von Mises
+L 形支架静力有限元
AP242 导出经 Gmsh 划分网格,CalculiX 算出 von Mises;边界面由模型改动后仍保留的 interface.* 标签选取 +
+MuJoCo 里四连杆互相穿过
+四连杆,第一版
MuJoCo 里连杆互相穿过:装配是错的 +
+修正后的四连杆装配在 MuJoCo 里运行
+碰撞修正之后
同一个曲柄周期,干净地转完 +
-## 开发 +## 安装 ```bash -uv sync --group dev -uv run python -m pytest test tests -python3 -m compileall src/simplecadapi +pip install "simplecadapi[editor]" # SDK 和编辑器 +sca example u_link # 一个可以直接打开的真实工作区 +sca edit u_link # 编辑器 +sca skill install # 给你的 agent 装 skill ``` -## 许可证 - -本项目采用 Apache 许可证 2.0 版(Apache-2.0),详见 [`LICENSE`](LICENSE)。 +Python 3.10–3.13,Linux、macOS 和 Windows。`uv tool install "simplecadapi[editor]"` 不需要虚拟环境就能把 `sca` 放进 PATH。`sca run model.py` 无界面运行 notebook;`sca export` 导出 STEP、STL 和 OBJ。 -## 社区交流 - -由于群聊人数过多,无法直接扫码入群。请扫描下方二维码添加杜鹏老师微信,由杜鹏老师邀请加入 CADDesigner 技术交流群: - -

- 杜鹏老师个人微信二维码 -

+## 研究 artifact -## Star History +本仓库是 [**CADIR: A Cross-Backend Editable Intermediate Representation for Agentic CAD Generation**](https://arxiv.org/abs/2608.00891) 的 artifact。 - - - - - Star History Chart - - +Apache-2.0。生产使用前请验证生成的模型。 diff --git a/design-docs/multi-solid-design.md b/design-docs/multi-solid-design.md new file mode 100644 index 00000000..33e56356 --- /dev/null +++ b/design-docs/multi-solid-design.md @@ -0,0 +1,391 @@ +# Multi-body parts (several solids in one part, without an assembly) + +Status: research and spike on `research/multi-solid` (cut from `dev`). The spike is in the code and in +`test/test_multi_body_part.py`. Everything under "Staged plan" after stage 1 is a proposal. Section 9 records the +owner's decisions and how they are implemented; where it differs from an earlier section, section 9 wins. + +## 1. The decision + +**A Part holds an ordered set of named bodies. A body is a Solid with an id that is unique within its part. +A one-body Part is the special case that every existing Part already is.** + +- `make_part_rpart(part_id=..., bodies={"base": base, "lid": lid})` makes a part with two bodies. + `make_part_rpart(part_id=..., body=solid)` is unchanged. +- `part.bodies` returns `(Body, ...)` in order. `part.get_body("lid")` returns one body and `part.body_ids()` returns the ids. +- `part.body` keeps returning the Solid of a one-body part. On a multi-body part it raises a `TypeError` that names the + bodies. It never silently returns the first body: a caller that reads `part.body` would otherwise export, weigh or + draw only part of the part. +- A Body is product geometry and nothing more. It has no placement, joint, constraint or connector of its own in + stage 1. Anything that moves relative to something else is a component of an Assembly. This is the line every + surveyed CAD system draws (see section 3): Fusion says it outright, and SolidWorks says "one part = one BOM line". +- Construction solids, tool solids and "kept for a later boolean" solids are **not bodies**. They stay plain notebook + values, as they are today. The product is exactly what the product cell puts into the Part. This one rule covers + "tool bodies that must stay hidden" with no `visible` flag in the data model: hiding is an editor view state, and + being part of the product is a modelling fact. + +Why not a `Compound` inside the Part: a Compound is a new shape root and drops the tags of the solids it is built from +(verified: `scad.Compound([a, b])` of two tagged boxes has no face tags; SDK_DEFECTS.md #21). Each body therefore stays +its own Solid root, with its own tags, lineage, graph node and topology snapshot. A Compound appears only as a +geometry view (`scad.part_shape(part)`), for consumers that do not care which body a face belongs to (STEP, STL, +MJCF). + +Why not "a part studio of several Parts" (Onshape style): that would make each body a separate product definition +with its own `part_id`, BOM line and package unit. That is a real feature, but it is a different one: it amounts to +"one notebook, several products". This design keeps one product per notebook. + +## 2. The data model today (evidence) + +| Piece | Where | One-solid assumption | +|---|---|---| +| `Part` (C++) | `src/cpp/include/scad/product/part.hpp:19` (before), `part.cpp` | `Solid body_` field. Header comment: "exactly one Solid" | +| `make_part_rpart` | `bindings/python/bind_product.cpp:81`, `src/product/ops_structure.cpp:105` | `typed(body)`. Docstring: "Wrap exactly one Solid" | +| guidance | `src/cpp/src/product/specs.cpp:43` | "Union intended multiple bodies into one Solid before creating the Part." | +| `Solid` | `src/cpp/src/topo/shape.cpp:15` | the kind is checked, so a `Solid` is exactly one `TopoDS_Solid` | +| `Compound` | `include/scad/topo/shape.hpp:290` | `solids()`. A new root, so the tags of its parts are lost (#21) | +| solid lists | `cut_rsolid_list`, `boolean_rsolidlist`, `*_pattern_rsolidlist` | `list[Solid]` values in the notebook. Never a product | +| `cut_rsolid` | `src/cpp/src/ops/features/booleans.cpp:372` | refuses a split result (since bd36d58) | +| assembly flatten | `src/cpp/src/product/ops_projection.cpp:20` | `part->body()` | +| session children | `src/simplecadapi/_glue/session.py:53` | `yield item.body` | +| cell cache | `runtime/cache.py:255` (`_semantic_state`) | "A Part is its body" | +| pickling | `_glue/values.py` `_reduce` | rebuilds `Part(part_id, body, ...)` from attributes (#22) | +| `@part` builder | `build/part_builder.py:43` | "must return exactly one Solid or one Part" (still true: one Part) | +| PartDefinition | `artifacts/part_definition.py:30`, schema `contracts/part-definition-2.schema.json` | "exactly one evaluated Solid": one `solid_cache.body_ref`, one `topology_snapshot_ref` | +| definition build | `build/definition.py:169-225` | BRep, snapshot, geometry hash and validation of `part.body` | +| BRep reader | `artifacts/brep.py:44` `read_brep_solid` | "BRep root must be exactly one Solid" | +| part archive load | `artifacts/part_io.py:93` | `read_brep_solid` | +| materialize | `artifacts/assembly_io.py:762` | `Part(body=read_brep_solid(...))` | +| package blob roles | `product/packages.py:180` | one body and one topology blob per part | +| live preview | `scene/preview.py:271` (`compile_preview`) | one `PreviewNode` per part, `entity///` | +| package scene | `scene/product_scene.py:322`, `:822` (`_camera`) | one GLB and one entity document per part | +| legacy scene | `scene/compiler.py:330, 339, 697, 928` | `part.body`. "a standalone Compound must contain exactly one solid" | +| prewarm | `scene/prewarm.py:39` | one body per part | +| STEP | `exporter/step.py:76` | `"shape": value.body` | +| STL / surface mesh | `exporter/_surface_mesh.py:45` | `(value.body,)` | +| MJCF | `exporter/mjcf.py:680` | `part.body` | +| definition kind | 20 places | `"single_solid"`. The kind name means "a part" (#23) | +| editor features | `feat/gui-modeling:src/simplecadapi/editor/features.py:677` `_with_combine` | "A part holds one solid": every new solid must join, cut or intersect, or start the body | +| editor product | `feat/gui-modeling:.../gui_features.py:99, 489` | the product cell is `make_part_rpart(..., body=)`, re-pointed after each feature | +| editor defaults | `gui_features.py:_defaults` | `operation = "join" if last is not None else "new"` ("do not leave a second") | +| editor-web | `editor-web/src/components/NotebookList.tsx:155` | "a notebook that builds one solid" | + +The editor already tracks the "open bodies" of a notebook: solids that no later feature consumed +(`gui_features.py:129`). Today only one of them reaches the product. A multi-body part is the missing link between +"open bodies" and the product. + +## 3. Other CAD systems + +The research used primary documentation read in a browser. V means read on the page. I means inferred or not +confirmed. + +- **Onshape Part Studio.** One feature list "drives the shape of multiple parts" (V, + cad.onshape.com/help/Content/PartStudio/part_studios.htm). Each part is its own BOM item. Features choose + New / Add / Remove / Intersect with a merge scope; when several touching parts are ambiguous the user must choose, + and an empty scope is an error (V, .../extrude.htm). Boolean has "Keep tools", and a merge keeps the name of the + first-selected part (V, .../boolean.htm). Mass properties are additive, overlaps count twice, and parts without a + material are left out (V, .../View/mass_properties_tool.htm). Several parts export to one file by default (V, + .../File/exporting_files.htm). A composite part groups bodies into one BOM line (V, .../composite_part.htm). +- **SolidWorks multibody part.** "Part documents can contain multiple solid bodies", kept in a Solid Bodies folder, + each with its own name and colour (V, help.solidworks.com/2024 .../c_Multibody_Overview.htm). "One part (multibody + or not) should represent one part number in a Bill of Materials", and bodies are "not dynamic" (V, + .../c_Multibody_Parts_Vs_Assemblies.htm). Material is set per part; a per-body material overrides it (V, + .../c_multibody_materials.htm). Extrude has "Merge result"; without it the feature creates a distinct solid body + (V, .../r_extrude_propertymanager.htm). Feature Scope limits which bodies a feature touches (V). Combine does + Add / Subtract / Common (V). Mass properties exclude hidden bodies unless asked (V, + .../hidd_massproperty_text_dlg.htm). Export offers all bodies or selected bodies (V); Save Bodies writes each body + to its own part and can build an assembly (V, .../hidd_dve_feat_split.htm). +- **Autodesk Fusion.** "A Body is a single container for a contiguous 3D shape". Components hold bodies, and "you + cannot create joints or motion between bodies" (V, help.autodesk.com ...GUID-C1AB4941...). Extrude operations are + Join / Cut / Intersect / New Body / New Component (V, ...SLD-REF-EXTRUDE). Combine has a target, tool bodies and + "Keep Tools" (V, ...SLD-COMBINE). Per-body material and appearance are inferred (I). +- **FreeCAD.** A PartDesign Body "is intended to hold a single contiguous solid", and 1.0 adds an experimental + "Allow Compound" (V, wiki.freecad.org/PartDesign_Body). Part Compound groups shapes, and a compound whose members + touch or intersect "is invalid for Boolean operations", which is not checked (V, .../Part_Compound). PartDesign + Boolean pulls tool bodies into the active body and hides them (V, .../PartDesign_Boolean). +- **CadQuery / build123d.** `combine=False` keeps a new solid apart. Results are Compounds, and `.Solids()` lists them + (V, cadquery.readthedocs.io .../workplane.html). build123d modes are ADD / SUBTRACT / INTERSECT / REPLACE / PRIVATE. + `BuildPart.part` "may contain more than one shape" (V, build123d.readthedocs.io .../build_part.html). STEP export + carries a label and colour per node, and repeated labels are numbered (V, .../import_export.html). + +What this design takes from them: + +1. A body is named geometry inside one product item (every system). +2. "New body" is an explicit per-feature choice beside join/cut/intersect, and an ambiguous target is an error, never a + guess (Onshape, SolidWorks, Fusion). +3. A boolean between bodies says whether it consumes its tools, and the result keeps the name of the target body + (Onshape, Fusion). +4. Mass is the sum over bodies, with explicit rules for overlaps and missing material (Onshape, SolidWorks). +5. A multi-body part exports as one product holding several solids, with an option to split it (SolidWorks, Onshape). +6. A bare compound is not a body set. It must not quietly become the input of a boolean (FreeCAD). + +## 4. Design + +### 4.1 Body + +```text +Body(body_id: identifier, solid: Solid, name: str | None = None) +``` + +- **Identity.** `body_id` is unique within its part and has the same rules as a `part_id` (`validate_identifier`). In a + notebook it is normally the name of the variable or feature slug that produced the solid ("base", "lid"). A one-body + Part has one Body with the id `"body"` (`kDefaultBodyId`). +- **Order.** Bodies keep the order they are given in, which is the order of the dict. Order is part of the product: + it decides the STEP solid order and the order of the scene nodes. +- **Names.** `name` is an optional display name. +- **Colour** (implemented, `feat/body-appearance`). Optional `color` (linear RGB in [0, 1]) and `opacity` on Body, an + appearance only, set with the recorded op `set_body_appearance_rpart(part, body_id="body", *, color=None, + opacity=None, name=None, reset=False)`. The material colour is the default; a body colour overrides it for display. + It reaches the preview (node `color`/`opacity`), `scene/compiler.py` (`base_color`), the package scene (node + `properties.color`/`opacity`), the definition (optional `appearance` block, profiles 2 and 3; absent when unused, so + bytes are unchanged), STEP (the part colour for a one-body part; a named, coloured XCAF sub-shape per body of a + multi-body part, with `write.stepcaf.subshapes.name` on), the cell cache and replay. +- **Material** (stage 3). The part material applies to every body. A per-body material override follows + SolidWorks' rule: the body material wins, and bodies without one inherit the part's. Mass already reads density per + body (`measure_part_mass_rmassreport`), so stage 3 only changes where the density comes from. +- **Visibility.** None in the product. See section 1: a hidden tool is not a body. + +### 4.2 API + +Stage 1, implemented: + +| Name | Kind | Notes | +|---|---|---| +| `scad.Body(body_id, solid, name=None)` | value | C++ `scad::product::Body`. Pickles | +| `scad.make_part_rpart(part_id, body=None, name=None, *, bodies=None)` | op | exactly one of `body` / `bodies`. `bodies` is `{id: Solid}` or `[Body, ...]`. Recorded with one `body` input per body and `params.bodies = [{body_id, name}]`. Replays | +| `scad.Part(..., bodies=...)` | ctor | same rule. ValueError for no bodies, a duplicate `body_id`, or connectors on several bodies | +| `Part.bodies`, `Part.multi_body`, `Part.body_ids()`, `Part.get_body(id)` | read | | +| `Part.body` | read | the Solid of a one-body part. TypeError on a multi-body part | +| `scad.part_shape(part)` | helper | the Solid, or a Compound of every body (a geometry view, no tags) | +| `scad.measure_part_mass_rmassreport(part, *, density=None, density_unit=None)` | op (diagnostic) | per body: `volume_mm3`, `mass_kg`, `centroid_mm`; plus totals. Density comes from the material or from the arguments | + +Stage 2, proposed (each one an SDK op, so agents and the editor call the same thing): + +- `add_body_rpart(part, body_id, solid, name=None) -> Part` and `remove_body_rpart(part, body_id) -> Part`: the + functional edits the editor's bodies list needs, in the style of `add_connector_rpart`. +- `split_rpart(part, body_ids=None) -> Assembly` ("Save Bodies"): one component per body, with identity placements + and part ids `_`. This is the explicit exit from bodies to an assembly when the bodies need motion. +- `cut_rsolid_list` naming help: pieces come out in kernel order, which is not stable across OCCT versions. A + `bodies_from_pieces(prefix, pieces)` that orders pieces by centroid (x, then y, then z) gives stable ids + `piece_1..n` for "a part with disjoint pieces after a cut". + +Stage 3, proposed: + +- `combine_bodies_rpart(part, target, tools, operation="join"|"cut"|"intersect", keep_tools=False) -> Part`. The + result keeps the target's `body_id` (Onshape's rule). Consumed tools leave the part. It is needed only when a Part + arrives without its notebook (an imported multi-solid STEP). In a notebook, bodies are still plain solids before the + product cell, and `union_rsolid(base, lid)` is already "combine". A part is the end of the pipeline, not a workspace. +- Per-body material and colour (4.1). +- Connectors on multi-body parts: a connector anchor names `body_id` plus the entity within that body. The + `GeometryRef` and the binding checks in `build/definition.py` then run per body. + +### 4.3 Booleans between named bodies + +In a notebook, bodies are named before they meet the product, so booleans between them are ordinary operators on +named solids. The rules the editor and the docs follow: + +- Join, cut and intersect consume their inputs as they do today. A consumed solid is no longer an "open body", so it + leaves the product's bodies at the next re-point (4.4). +- "Keep tools" is the default behaviour of a functional SDK anyway: the tool variable still exists. Whether it stays + a body is the product cell's choice, and the editor offers that choice explicitly. +- `cut_rsolid` keeps refusing a split. `cut_rsolid_list` keeps every piece, and those pieces become bodies through + `bodies={...}` (stage 2 helper above). +- Overlapping bodies are allowed, as in SolidWorks and Onshape. Mass counts every body whole, which is the Onshape + rule, and the report says so. A stage-2 diagnostic `check_bodies_disjoint` (pairwise common volume) can warn before + export. Whether `make_part_rpart` should refuse overlaps is an open question for the owner (section 7). + +### 4.4 Editor UX (the editor only adapts SDK ops) + +- **Bodies list.** The product cell becomes the list. `make_part_rpart(part_id=..., bodies={"base": base, "lid": lid})` + is parsed like `_PRODUCT_BODY` today, with one row per entry. A notebook with one body keeps writing `body=...`, so + existing notebooks are untouched. +- **Operation choice.** Each combinable feature keeps join / cut / intersect / new body. The changes: + - "New body" becomes a real choice. The feature makes a new open body, and the editor adds it to the product's + bodies with `add_body_rpart` semantics (an edit of the dict literal in the product cell). + - The default stays "join" when a body exists (Onshape's default is New only for the first feature, too). The help + text no longer says "a part holds one solid". + - "Combine with" stays an explicit body choice. When more than one body exists the target has no default: the form + asks, which is Onshape's rule for an ambiguous merge. +- **Re-pointing.** `_repoint_product` today swaps the one body name. With bodies it does the same for the dict entry + whose solid the new feature consumed, and keeps the `body_id`. A body keeps its id while it is joined, cut and + filleted, the way a SolidWorks body keeps its name through features. +- **Construction solids.** Open solids that are not in the bodies dict are listed under "Construction", drawn ghosted + and never exported. That covers tool bodies and solids kept for a later boolean, with no flag in the product. +- **Viewer.** Preview nodes are `node//body:`, and entities are `entity//body://`. The + scene tree shows the part with its bodies as children (`node_kind: "solid"`, which the scene schema and + `editor-web/src/scene/scene2.ts:27` already accept). Picking a face says which body it belongs to. +- **Assemblies stay separate.** Assembly documents (`assembly_doc.py`) keep placing Parts. A multi-body part is one + component there. "Make components from bodies" is `split_rpart`. + +### 4.5 Export, mass, preview, package + +- **Live preview** (`scene/preview.py`, implemented): one node per body, all at the part's placement. The part key + includes the body id, so two equal-shaped bodies stay two entity namespaces. `entity_info` understands the `body:` + segment. +- **Package** (`PartDefinition`, implemented): + - A one-body part writes the same manifest bytes as before, so its content hash is unchanged. + - A multi-body part adds `solid_cache.bodies = [{body_id, name, body_ref, topology_snapshot_ref}]`, one BRep and + one snapshot per body, so every body keeps its tags. + - `solid_cache.body_ref` is then a BRep Compound of every body. That is what a geometry-only reader needs, and what a + reader that expects exactly one Solid refuses (`read_brep_solid`), so old readers fail closed instead of dropping + bodies. + - `topology_snapshot_ref` repeats the first body's snapshot, because the schema requires it. + - The geometry interface hash covers `[{body_id, geometry}]`. + - Validation (`validate_artifact_blobs`, package blob roles, `part_io`) covers every body blob and checks that the + Compound holds as many solids as there are bodies. +- **Package scene** (`scene/product_scene.py`, implemented): the part node has no geometry. Each body is a child node + of kind `solid` with its own GLB and entity document, `node_id` `node//body:`. The camera frames every + body. +- **STEP** (implemented): one PRODUCT whose shape holds every body. The test checks one `PRODUCT(` and two + `MANIFOLD_SOLID_BREP`. Stage 2 writes the body name and colour on each solid (an XCAF sub-shape label), as + build123d does. +- **STL / MJCF** (implemented): every body goes into the mesh. In MJCF a multi-body part is one rigid body with one + mesh, which is correct because bodies never move apart. +- **Mass** (implemented): `measure_part_mass_rmassreport`. Totals are sums; the centroid is volume-weighted. +- **Cell cache** (implemented): `Part` pickles with its bodies (`_glue/values.py` `_ARGUMENTS`), and the + semantic-state guard watches every body (`runtime/cache.py`). +- **Legacy `scene/compiler.py`** (`compile_scene` / `export_scene`): not converted. A multi-body part raises the + `Part.body` TypeError there (stage 2). + +### 4.6 Boundary with assemblies + +| | Bodies of one Part | Components of an Assembly | +|---|---|---| +| Product items / BOM lines | one | one per component | +| Placement | none (bodies share the part frame) | per component | +| Joints / constraints / motion | never | yes | +| Connectors | per part (stage 3: anchored on one body) | per part, public per assembly | +| Material | part (stage 3: per-body override) | per part | +| From one to the other | `split_rpart` (stage 2) | `make_compound_from_assembly_rcompound` flattens (every body of every part) | + +## 5. Staged plan + +1. **Stage 1, done in this spike:** Body, `Part.bodies`, `make_part_rpart(bodies=...)` with recording and replay, + pickling and cache, live preview, package format, package scene, STEP, STL and MJCF geometry, assembly flattening, + and the mass report. One-body parts are unchanged byte for byte. +2. **Stage 2:** + - Editor: bodies list, a real "New body", construction group, re-pointing by `body_id`. + - `add_body_rpart`, `remove_body_rpart`, `split_rpart`, ordered piece naming, `check_bodies_disjoint`. + - STEP body names and colours. + - `scene/compiler.py`. + - Body colour. + - Skill docs: when to use bodies and when to use an assembly. +3. **Stage 3:** + - Per-body material. + - Connectors on a body. + - `combine_bodies_rpart` for parts without a notebook (STEP import with several solids becomes a multi-body Part + instead of an error). + - Rename the `single_solid` definition kind at the next package schema bump. + +## 6. Risks + +- **Silent partial readers.** Any code still reading `part.body` raises on a multi-body part. That is loud, not + silent, but it is a runtime failure on paths no test covers, e.g. `scene/compiler.py:330` and the translators. The + `grep '\.body\b'` list in section 2 is the checklist. +- **Package compatibility.** A package with `solid_cache.bodies` validates under the same `part-definition-2` profile. + An old SDK reading it fails at `read_brep_solid` (Compound root) or at the closed schema (`additionalProperties: + false` on `solid_cache`), which is the right failure. A profile bump to `part-definition-3` would make the change + explicit, at the cost of every reader (open question). +- **Body id stability.** The ids come from the notebook. If a person renames a variable, the body is renamed with it. + Pieces of `cut_rsolid_list` arrive in kernel order (stage-2 helper). +- **Overlaps.** Overlapping bodies make mass and FEM meaningless and STEP ambiguous. The policy is open (section 7). +- **Editor complexity.** Re-pointing a dict entry is more state than swapping one name. The bodies list must stay a + parse of the product cell, never a second store. + +## 7. Open questions for the owner + +1. May `make_part_rpart(bodies=...)` refuse overlapping or touching bodies (FreeCAD's warning), or only warn through a + diagnostic? The spike allows overlaps. +2. Should the package profile move to `part-definition-3`, or does the additive `solid_cache.bodies` under profile 2 + stay (the spike's choice, which keeps one-body hashes)? +3. Should the default body id of a one-body part stay `"body"`, or should it be the notebook variable name? The + variable name is nicer in the editor, but changing it would change ids that exist today. +4. Does "New body" in the editor need confirmation the first time a notebook gets a second body, given that until now + a second solid was a mistake? +5. (Decided, section 9: splits are kept as pieces by default.) Should `cut_rsolid` keep refusing splits? + +## 8. The spike + +- **C++:** + - `include/scad/product/part.hpp`, `src/product/part.cpp`: Body, the bodies of a Part, the `body()` TypeError. + - `src/product/ops_structure.cpp`: the `make_part_rpart` overload and its record. + - `src/product/ops_projection.cpp`: assembly flattening takes every body. + - Bindings: `bind_product_values.cpp`, `bind_product.cpp`, `bind_product_args.hpp` (`body_list`). +- **Python:** + - `product/bodies.py`: `part_shape`, `measure_part_mass_rmassreport`. + - `_glue/values.py`, `_glue/session.py`, `runtime/cache.py`. + - `artifacts/part_definition.py` (`BodyRef`), `artifacts/part_io.py` (`read_part_bodies`), `artifacts/assembly_io.py`, + `artifacts/validation.py`, `contracts/part-definition-2.schema.json`. + - `build/definition.py`, `product/packages.py`. + - `scene/preview.py`, `scene/product_scene.py`, `scene/prewarm.py`. + - `exporter/step.py`, `exporter/_surface_mesh.py`, `exporter/mjcf.py`. + - `recording/serializer.py` (replay and the diagnostic row). + - Docs, stubs and `docstrings.json` / `modules.json` registration. +- **Tests:** `test/test_multi_body_part.py`, 20 tests: + - value rules; + - pickle and deepcopy with tags; + - connectors refused; + - assembly flattening; + - graph replay; + - mass per body and totals; + - preview nodes and entity namespaces, including two equal-shaped bodies; + - a notebook run twice, with every cell after setup cached and the same content hash; + - a package that round-trips with tags per body; + - scene solid nodes; + - STEP with one product and two solids; + - one-body definitions without `bodies`. + +## 9. Owner decisions (implemented) + +1. **Overlapping bodies warn, never refuse.** + - `inspect_body_overlaps_rlist(part)` is a diagnostic op. It lists pairs that share volume: a bounding-box + prefilter, then the OCCT common volume. Bodies that only touch along a face, an edge or a point are not reported. + - Building the definition warns once per pair with `BodyOverlapWarning`. That covers a notebook run, `@part` and a + package. `make_part_rpart` itself never runs the check, so the live editor loop stays cheap. +2. **Multi-body definitions are profile `simplecad-part-definition-3`.** + - The new schema `contracts/part-definition-3.schema.json` requires `solid_cache.bodies`. + - `part-definition-2.schema.json` is back to its dev bytes. A profile-2 reader fails schema validation on + `profile` (`const`), so it refuses the file rather than reading it wrongly. This is tested by validating a profile-3 + manifest against schema 2. + - One-body parts stay profile 2, so their bytes and content hashes do not change. + - `validate_manifest` picks the schema by the manifest's profile. +3. **Default body ids.** A one-body part keeps `"body"`; a multi-body part uses the names it was given. This is + unchanged from the spike. +4. **Choosing "New body" is the confirmation.** The editor asks nothing more. + + **Implicit Part** (the `kind = "part"` rule on `feat/gui-modeling`, `runtime/projection.py`): every open body + becomes a body of the implicit Part, named by its variable. The drop-in is `simplecadapi.product.bodies.part_from_bodies(part_id, {name: solid_or_pieces})`. It makes a one-body Part + (id `"body"`) for one open Solid, and otherwise `make_part_rpart(bodies=...)` with the pieces of a split expanded to + `_1..n`. The exact change on `feat/gui-modeling`: + - in `open_bodies`, also accept a non-empty `list`/`tuple` of `Solid` (the pieces of a split cut) as a candidate. Use + the lineage of its first element for the "made from" test, and treat `id()` of each piece as seen; + - in `_implicit_part`, drop the `len(names) > 1` error and return + `part_from_bodies(config.id, {name: values[name] for name in names})`; + - update its docstring and the test that expected the error. Expected: two open bodies give a Part with those two + body ids. +5. **A cut that splits the body is not refused.** + - `cut_rsolid(..., on_split="pieces")` is the default. A one-piece result is a `Solid`, exactly as before, with the + same record, so the hashes of existing notebooks do not change. A split returns every piece as `list[Solid]`, + ordered by centroid (x, then y, then z). It is recorded as `make_cut_rsolid_list` with + `params.piece_order = "centroid"`, and both the C++ executor and the Python serializer replay it in that order. + - `on_split="error"` keeps the strict behaviour. + - Pieces become bodies directly: `make_part_rpart(body=pieces)` gives `body_1..n`, and `bodies={"bar": pieces}` gives + `bar_1..n`. + - **Default: separate bodies, not one solid with several lumps.** OCCT has no valid single solid with several lumps. + A `TopoDS_Solid` built from the two outer shells of a split bar fails `BRepCheck_Analyzer`, and STEP writes it as + `BREP_WITH_VOIDS`, with the second lump turned into a void. This was checked with a small OCCT program. + Fusion ("a body is a single container for a contiguous 3D shape") and FreeCAD ("a single contiguous solid") agree + that a body is one closed volume. + - "Keeping the pieces together" is therefore a naming matter: they share the prefix of the value they came from + (`bar_1`, `bar_2`), and the editor can show them as one row with two bodies. + - A Body that holds several lumps would be a later, additive extension (`Body.lumps`). It is not needed for valid + geometry. + - Downstream single-solid features (a fillet on `bar`) now fail with a clear type error when `bar` is a list. The + editor should then offer to pick a piece or keep the pieces as bodies (stage 2). + +Skill pages updated with these rules: `domains/part-modeling.md` (with a new "Multi-body parts" section), +`domains/assembly-and-product.md`, `discipline/core-rules.md` R5, `discipline/failure-and-repair.md`, +`discipline/mechanical-modeling.md`, `discipline/notebook-cell-design.md` C9, `docs/core/compound.md`, +`docs/guides/notebook-runtime.md`, `workflows/single-part-modeling.md` and `role-4-build.md`, and +`workflows/drawing-reconstruction.md`. The API pages are regenerated. Tags on cut pieces: a plain `cut_rsolid` outside +a recording session carries no input face tags, a behaviour that predates this work. Tag and ref propagation belongs to +`research/stable-geometry-refs`. + diff --git a/design-docs/nurbs-editing-capability.md b/design-docs/nurbs-editing-capability.md new file mode 100644 index 00000000..abc4d62f --- /dev/null +++ b/design-docs/nurbs-editing-capability.md @@ -0,0 +1,99 @@ +# NURBS editing in a GUI: what the SDK can and cannot do today + +Status: research, 2026-11-04, branch `wip/types-surface-sketch` (cut from `feat/gui-modeling`). Nothing here is +implemented; the last section lists the C++ operations a Plasticity-level editor would need. + +## Two ways into OpenCASCADE from Python + +- **Public SDK operations** (`*_r` in `simplecadapi`, `simplecadapi.surface`). They are recorded in the graph, + replay, carry tags and appear in `simplecadapi.catalog`. A GUI or agent can only rely on these. +- **The private OCP-compat layer** `simplecadapi._cpp.occt.*` (`src/cpp/bindings/python/occt/bind_occt_{core,query,algo}.cpp`), + reached from a shape through `Shape.wrapped` (`bind_topo.cpp`, `bind_common`). It mirrors part of OCP for the internal + Python layers (inspect, translators). It is not recorded, not replayable and not documented. Edits made through it are + invisible to the graph and have no way back into topology. + +Legend: **public** = a recorded SDK op; **private** = only through `_cpp.occt`; **internal** = used inside the C++ kernel, not +exposed; **absent** = not linked anywhere. + +## Capability by capability + +| Capability | Status | Evidence | +|---|---|---| +| Read poles / weights / knots / multiplicities / degree of an existing Edge | public (read-only) | `inspect_shape_rbrepinspection` (`src/simplecadapi/inspect/brep/inspect.py`, `_edge_parameters`); native `_bspline_curve_of(edge)` (`bind_sdk_compat.cpp`, `bspline_curve_dict`) | +| Same for a Face | public (read-only) | `inspect.py` `_surface_parameters` (poles, weights, knots, mults of a B-spline surface) | +| Read through OCCT objects | private | `Geom_BSplineCurve` / `Geom_BSplineSurface` / Bezier bound in `bind_occt_query.cpp` (Poles, Weights, Knots, Multiplicities, Degree, UDegree/VDegree); `BRep_Tool.Curve_s/Surface_s/CurveOnSurface_s` in `bind_occt_core.cpp` | +| Write a pole / weight / knot of an existing curve | private only | `bind_occt_query.cpp`: `Geom_BSplineCurve.SetPole/SetWeight/SetKnot(s)/IncreaseMultiplicity`, Bezier `SetPole/SetWeight` | +| Write a pole / weight of an existing surface | private only | `Geom_BSplineSurface.SetPole/SetWeight`, `Geom_BezierSurface.SetPole/SetWeight` (`bind_occt_query.cpp`) | +| Rebuild an Edge/Face after such an edit | absent | no op rebuilds topology from a modified `Geom_*`; the modified handle is shared with the original shape (mutation in place, unrecorded) | +| Degree elevation | private only | `Geom_BSplineCurve.IncreaseDegree`, `Geom_BSplineSurface.IncreaseDegree` (`bind_occt_query.cpp`) | +| Knot insertion / removal | private, curves only | `Geom_BSplineCurve.InsertKnot/RemoveKnot` (`bind_occt_query.cpp`); no `InsertUKnot/RemoveUKnot` for surfaces | +| Fit a surface through a point grid | public | `fit_point_grid_rface` (`src/cpp/src/surface/fitter.cpp`, `GeomAPI_PointsToBSplineSurface`, smoothing weights) | +| Interpolate a curve through points | public | `make_interpolated_spline_redge/rwire`, `make_periodic_spline_rwire` (`src/cpp/src/kernel/curves.cpp`, `GeomAPI_Interpolate`) | +| Approximate a curve (least squares, tolerance) | absent | `GeomAPI_PointsToBSpline` is not linked | +| Refit / simplify an existing curve or face, convert to NURBS | private / absent | `GeomConvert.CurveToBSplineCurve_s/SurfaceToBSplineSurface_s` private (`bind_occt_query.cpp`); `BRepBuilderAPI_NurbsConvert`, `ShapeCustom`, `BSplineRestriction`, `Approx_*` absent | +| G0/G1/G2 boundary conditions when filling | public | `make_surface_patch_rface`, `fill_holes_rshell` with `SurfaceBoundary(edge, continuity="C0"/"G1"/"G2", support=face)` -> `BRepOffsetAPI_MakeFilling` (`fitter.cpp`, `order_of`) | +| Continuity analysis between two faces (G0/G1/G2 deviation along an edge) | absent (one internal probe) | `LocalAnalysis`, `GeomLProp` not linked; dihedral angle along an edge is internal (`src/cpp/src/ops/features/probes.cpp`) | +| Gordon / ruled / loft / Bezier surfaces | public | `make_gordon_surface_rface` (`gordon.cpp`), `make_ruled_surface_rface`, `loft_rshell` / `loft_rsolid`, `make_bezier_surface_rface` (`faces_from_data.cpp`), sweeps (`sweep_rsolid`, `sweep_multisection_rsolid`, ...) | +| Blend surface between two faces (free, not a solid edge) | absent | only `fillet_rsolid` / `chamfer_rsolid` on solid edges (`BRepFilletAPI_*`, `semantic/tracking_blends.cpp`); no `GeomFill_*` / `BRepBlend` | +| Offset a surface | absent | `offset_rsolid` / `shell_rsolid` are solid-only (`ops/features/offsets.cpp`, `MakeThickSolid`); no `Geom_OffsetSurface`, no face offset | +| Extend a surface | absent | no `GeomLib::ExtendSurfByLength`, no `ShapeExtend` | +| Trim a surface by curves | public | `trim_surface_rface(carrier, outer, holes)` (`src/cpp/src/surface/trim.cpp`: projects the wire, checks pcurves with `GeomLib_CheckCurveOnSurface`, `BRepAlgoAPI_Common`) | +| Split a face by a curve | absent (internal splitter) | `BRepAlgoAPI_Splitter` only inside `extrude_until`; no `BRepFeat_SplitShape` | +| Untrim / read the trimming loops in UV | internal | pcurves read in `trim.cpp` via `BRep_Tool::CurveOnSurface`; no op returns them | +| Curve on surface (edge from a 2D curve on a face) | absent | `BRepBuilderAPI_MakeEdge(Geom2d_Curve, Surface)` not bound/used (MakeEdge takes 3D curves only, `bind_occt_algo.cpp`) | +| Project a point onto a surface | internal / private | `GeomAPI_ProjectPointOnSurf` in `trim.cpp`, `ShapeAnalysis_Surface` in `probes.cpp`; private binding in `bind_occt_query.cpp` | +| Project a curve onto a surface | absent | `BRepProj_Projection` not linked; `project_rsketch` only projects onto a sketch plane (`src/cpp/src/sketch/project.cpp`) | +| Isocurves (U/V iso lines) | private | `Geom_Surface.UIso/VIso` (`bind_occt_query.cpp`); used inside the guided-loft recipe (`bind_sdk_compat.cpp`); no op | +| Evaluate a curve / surface (point, derivatives, curvature) | private (sampling public) | `Edge.sample(count)` public (`bind_ops_geometry.cpp`); `D0..DN`, `BRepLProp_SLProps` curvatures private; no curve curvature (`CLProps`), no curvature comb | +| Sketch B-splines with poles / knots / weights | public | `add_bspline_rsketch(..., control_points, degree, knots, multiplicities, weights, periodic)` (`bind_sketch.cpp`); 3D `make_spline_redge/rwire` (`bind_ops_geometry.cpp`) | +| Editor hooks for control-point editing | absent | `src/simplecadapi/editor` only maps `bspline_curve/surface` to the BSPLINE selection filter (`editor/selection.py`) | + +OCCT classes already linked: Geom_BSplineCurve/Surface, Geom_Bezier*, GeomAPI_Interpolate, GeomAPI_PointsToBSplineSurface, +BRepOffsetAPI_MakeFilling, ThruSections, MakePipeShell, MakeThickSolid, MakeOffset (wires), BRepFill, BRepFilletAPI, +GeomConvert, GeomAPI_ProjectPointOnSurf, ShapeAnalysis_Surface/FreeBounds, GeomLib_CheckCurveOnSurface, BRepAlgoAPI_Common/Splitter, +BRepLProp_SLProps, Geom2d_*, BRepExtrema. + +Not linked: GeomPlate, GeomFill, LocalAnalysis, GeomLProp, Geom_OffsetSurface, GeomLib::ExtendSurfByLength, ShapeExtend, +ShapeCustom, BRepBuilderAPI_NurbsConvert, BRepFeat_SplitShape, BRepProj_Projection, GeomAPI_PointsToBSpline, Approx_*. + +## What this means for a GUI + +The SDK can *build* every common freeform surface from data a GUI collects (control nets, curve networks, boundary +patches with G1/G2 supports, trims), and it can *read* the NURBS data of a result. It cannot *edit* an existing curve +or surface as a recorded operation: every edit a Plasticity-style control-point drag implies (move pole, set weight, +insert knot, elevate degree) exists only as unrecorded mutation through the private OCCT layer, and nothing rebuilds the +Edge/Face from the modified geometry. The editor must therefore keep the control net itself and re-run a constructor op +(`make_spline_redge`, `make_bezier_surface_rface`) on every drag, which loses the identity and tags of downstream topology +and has no answer for imported or derived (fillet, Gordon, filling) surfaces. + +## Operations that would be needed (C++ kernel ops, recorded, catalog-typed) + +Read: +1. `inspect_curve_rnurbs(edge)` / `inspect_surface_rnurbs(face)` - degree, poles (`Sequence[Point3]` / grid), weights, + knots, multiplicities, periodic/rational flags, parameter range; converts non-B-spline geometry on request + (`GeomConvert`). Today's data sits in `inspect_shape_rbrepinspection`; it needs to be a first-class typed value. +2. `evaluate_curve_rframe(edge, t)` / `evaluate_surface_rframe(face, u, v)` - point, derivatives, normal, curvatures + (`GeomLProp_CLProps/SLProps`), for curvature combs and zebra-like checks. +3. `measure_continuity_rcontinuity(face_a, face_b, edge, samples)` - G0 gap, G1 angle, G2 curvature deviation + (`LocalAnalysis_SurfaceContinuity`). + +Edit (each returns a new Edge/Face, recorded, lineage preserved for tags): +4. `edit_curve_poles_redge(edge, poles=..., weights=...)` and `edit_surface_poles_rface(face, poles=..., weights=...)` + - replace some or all poles/weights, keep the trim (re-project the face's trim loops). +5. `insert_knot_redge` / `insert_knot_rface(face, u=|v=, multiplicity)`, `remove_knot_*` (tolerance-checked). +6. `elevate_degree_redge` / `elevate_degree_rface(face, u_degree, v_degree)`. +7. `rebuild_curve_redge(edge, degree, pole_count | tolerance)` / `rebuild_surface_rface(face, u_count, v_count, degree)` + (`GeomAPI_PointsToBSpline(Surface)`, `Approx_*`) and `convert_to_nurbs_rshape` (`BRepBuilderAPI_NurbsConvert`). + +Build: +8. `blend_surface_rface(edge_a, face_a, edge_b, face_b, continuity="G1"|"G2")` (`GeomFill` / `GeomPlate`). +9. `offset_surface_rface(face, distance)` (`BRepOffsetAPI_MakeOffsetShape` on a face, `Geom_OffsetSurface`) and + `extend_surface_rface(face, edge, length, continuity)` (`GeomLib::ExtendSurfByLength`). +10. `split_face_rfacelist(face, tools)` (`BRepFeat_SplitShape` / `BRepAlgoAPI_Splitter`), `untrim_surface_rface(face)`. +11. `make_curve_on_surface_redge(face, uv_points | Geom2d spec)` and `project_curve_onto_face_redge(edge, face, + direction=None)` (`BRepProj_Projection`, `BRepAlgo_NormalProjection`). +12. `make_isocurve_redge(face, u=|v=)` (`UIso/VIso` + trim to the face). + +Most important gaps, in order: (a) recorded edits of an existing curve/surface (4-6) - without them control-point +editing is a rebuild-from-scratch hack; (b) continuity measurement and blend surfaces (3, 8) - the core of class-A style +surfacing; (c) offset/extend/split of faces (9, 10) - routine surface workflow steps that have no op at all. diff --git a/design-docs/stable-refs-design.md b/design-docs/stable-refs-design.md new file mode 100644 index 00000000..32c14bce --- /dev/null +++ b/design-docs/stable-refs-design.md @@ -0,0 +1,398 @@ +# Stable geometric references and tag propagation: design and staged plan + +Status: design + stage-0/1 spike, 2026-11-04, branch `research/stable-geometry-refs` (cut from `dev` @ `093ef71`). +Builds on `design-docs/topo-naming-research.md` (FreeCAD 1.0 ElementMap, Onshape queries), which is not repeated here. + +Legend. **[T]** = verified by a test committed on this branch (file named). **[R]** = verified by reading the code or by +a probe run in this session, no committed test. **[I]** = inference / proposal, not verified. + +--- + +## 0. Summary + +1. **A reference is a query over recorded history, not a stored entity.** The anchor names where the entity was + *born* (a stable node key: feature, op, occurrence among ancestors, plus a kernel role or topo id); resolution + walks the `TopoDelta` every later node recorded from OCCT history to the entities of the shape being built. + Onshape's model (`qCreatedBy` + `startTracking`, `OK`/`SPLIT`/`FAILED_TO_RESOLVE`) on FreeCAD 1.0's history + (Modified / Generated / IsDeleted), with typed records instead of an encoded name string. **[T]** +2. **Names are generated in the C++ kernel, per operation, from OCCT history** (`semantic/tracking_*.cpp`); the + resolver, the stable keys, the backward trace and the tag view are C++ too (`reactive/element_ref.cpp`, + `reactive/tag_view.cpp`). Python only stores references (`simplecadapi.topology.element_ref`). **[T]** +3. **Outcome is always `ok` / `split` / `merged` / `lost`**, with entities, hops, bodies, and for `lost` the node and + reason. A split is an error carrying the candidates (owner decision); a lost reference offers guard-ranked + look-alikes; nothing is chosen silently. **[T]** +4. **Tags are a view over the recorded history** (owner decision, switched directly): a tag assignment is a graph + node, and `ql.tag` sees every entity its targets became. Plain and op-kwarg face tags now survive every operation + family with history. **[T]** +5. **Every solid modelling op records history now:** `transform_rsolid`, `scale_rsolid`, `offset_rsolid` and + `draft_rsolid` gained it; a shell's kept faces are preserved (option a); N-ary booleans compose their steps. + **[T]** +6. Persistence: tags and references resolve on values restored from the cell cache (nodes and deltas travel in the + lineage records) and after pickling **[T]**; references carry a naming version **[T]**. + +--- + +## 1. Why tags-then-geometry is not enough (what the probes showed) + +| Channel today | Survives boolean | Survives fillet/chamfer | Survives translate/mirror/pattern | Evidence | +|---|---|---|---|---| +| plain `apply_tag_rselection` face tag | no | no | no | **[T]** audit, `user.top` = 0 everywhere | +| op-kwarg face tag (`top_face_tag=`) | yes | **no** | **no** | **[T]** audit, `kw.top`: cut/union/intersect 1, fillet/chamfer/translate/mirror/pattern 0 | +| editor recipe: tag if count matches, else a geometric window `.exactly(n)` | -- | -- | -- | **[R]** `feat/gui-modeling:src/simplecadapi/editor/features.py` `selection_recipe` | + +The geometric window is the dangerous part: after an upstream edit it can match a *different* face of the same size +and position class and the feature proceeds. The history resolver has none of these gaps where the kernel records +history: the same top face resolves after cut, union, intersect, fillet, chamfer, translate, mirror and pattern +**[T]**, including the cases where both tag channels are dead. + +Note the surprise in the table: the op-kwarg tag dies at `translate_shape`/`mirror_shape`/patterns even though these +record a `Modified`/continuation witness for every face **[T]**. The data is there; the projection drops it. + +--- + +## 2. The reference format + +### 2.1 Record (JSON, versioned) [I] + +```jsonc +{ + "v": 1, // record version + "naming": "scad-naming/1", // resolver + tracker algorithm version (section 5) + "anchor": { + "feature": "cell:3f9a/extrude#1", // stable node identity (2.2), not the raw node id + "op": "make_extrude_rsolid", // checked on resolve: a different op at that id = lost/anchor_mismatch + "slot": 0, + "entity": {"kind": "face", "by": "role", "role": "extrusion.end", "ordinal": 0} + // or {"by": "source", "source": , "event": "generated"} + // or {"by": "tag", "tag": "mount.face"} + // or {"by": "topo", "topo_id": "face_face_5"} // positional: only valid with a matching guard + }, + "chain": [ {"feature": "cell:7c21/cut#1", "event": "modified", "piece": {"rank": 0, "of": 2}} ], + "follow": "identity", // or "derived" (edge -> its fillet patch, Onshape startTracking) + "pick": "fail", // split policy: "fail" | "all" | "piece" (use chain[].piece) + "guard": {"kind": "face", "geom_type": "PLANE", "area": 771.73, "center": [0, 0, 10], "normal": [0, 0, 1]}, + "last": {"node_id": "c3_node_00000007", "targets": ["face_face_2"], "status": "ok"} // FreeCAD ShadowSub: cache only +} +``` + +- `anchor.entity.by` priority, most to least stable: **role** (kernel output roles are deterministic per op: + `extrusion.end`, `box.top`, `fillet.patch`, `shell.offset_face` **[R]**), **source** (a side face is the + `Generated` image of a profile edge, itself an `ElementRef` into the sketch; this is FreeCAD's `g1;SKT` root), + **tag** (a user name, itself stored as an `ElementRef`), **topo** (positional index; see 2.3). +- `chain` is what the backward trace saw at pick time (2.4). It is not needed to resolve a reference whose anchor + is unambiguous, only to say *which piece* when the entity was already split when it was picked. +- `guard` is never used to choose between history results except inside one split (2.4) and to rank candidates for + a lost reference (4.2). + +### 2.2 Stable node identity (owner decision; implemented) [T] + +Node ids are `_node_`, "the ids count from the cell's code" (`runtime/lineage.py`) +**[R]**, so an operation inserted earlier in the cell shifts every later id, and a stored id then names *another* +operation: `test_an_operation_inserted_earlier_in_the_cell_does_not_shift_a_stable_anchor` shows the raw anchor +resolving to the wrong face (the inserted stand's top) after such an edit **[T]**. + +A stable anchor names the node by **(feature, op, occurrence)** (`StableNodeKey`, `element_ref.hpp`): + +- `feature` = the graph namespace of the node, i.e. the cell key (the editor's feature slug for a feature cell); +- `op` = the op name; +- `occurrence` = how many nodes of the same feature and op are **ancestors of this node** (upstream of it in the + graph), not how many were recorded before it. + +Why ancestry and not recording order (the "even more stable" choice): with recording order, *any* same-op +operation inserted earlier in the cell shifts the anchor, including one that has nothing to do with the referenced +body (a second sketch, a fixture box). Counting ancestors only, an inserted operation shifts the anchor only when +it is the same op **and** feeds the anchored node itself, which is the case where the person really changed what +the anchor is built from. Ops of another kind never shift it. **[T]** (doctest "a stable anchor names the node by +feature, op and occurrence upstream of it"; pytest insertion, parameter-edit and ambiguity tests). + +Two nodes can then share a key without either feeding the other (two primitive boxes of one cell). The key carries +the digest of the node's own parameters (refs, picks and node ids left out) as a tie-break: the candidate with +unchanged parameters wins; when none or several match, the reference is `lost(anchor_ambiguous)`, never a guess +**[T]**. (A parameter edit of one of two same-key siblings is the one case that needs the person.) + +The entity inside the node is named by its **kernel role** (the most specific role: fewest entities, then name) +plus an ordinal among the node's entities with that role; by the positional topo id only when it has no role. +Multi-entity roles (`extrusion.side`) are ordinal-positional until sketch geometry ids name profile edges (stage 2). + +A resolve against a restored cell uses the node *object* the value carries (`shape._get_runtime("graph.node")`), +never a global id lookup, so the cache mis-binding variant (two cache entries of one cell share node ids) cannot +redirect it **[T]** for live values, **[I]** for restored ones (stage: cache test). + +### 2.3 Why the positional `topo_id` is not a name [R] + +`face_face_5` is "kind + the root's own topo id", and the root's topo id is the position in the root's +`TopExp::MapShapes` order (`src/cpp/src/topo/internal.hpp`, `Registry`: "Numbering is by position in the root's own +TopExp::MapShapes order"). It is deterministic for one input, so it is fine *inside one run* (that is what the +resolver uses to step from node to node), but after a parameter change the same index can be another face. It is +the analogue of FreeCAD's `Face12`. It may appear in a stored ref only next to a guard. + +### 2.4 Pick-time naming: backward trace (implemented) [T] + +`trace_to_birth` (C++): from the picked entity, follow its single same-kind parent back through every node that +only preserved or modified it (a selection is crossed to its source), stopping at a creator, at the node that +generated it, at a merge (several parents), or at a split (the parent has several same-kind successors: the piece +is named at the node that made it). `stable_anchor_of(face, part)` on the finished part therefore anchors at +`make_box_rsolid` / `box.top`, not at the fillet that last touched the face **[T]**. + +A piece of a face that was already split when it was picked is anchored at the splitting node by its topo id +(positional). Naming pieces by a kernel-computed geometric rank among siblings is stage 2. + +--- + +## 3. Where names come from: per operation family + +The kernel records one `TopoDelta` per node: witnesses with event `preserved`/`modified`/`generated`/`deleted`, +`parent_refs` naming input entities by the node that made them (a tag node keeps the producer as topology owner, +`keep_topology_owner` **[R]**), `origin_role` (`body`/`tool`/`contour_edge`/`profile`...), `derivation` +(`continuation`/`fragment`/`boundary`/`replacement`), and kernel roles. The resolver follows `preserved` and +`modified` of the anchor's kind (identity mode) or also `generated` and kind changes (derived mode). + +| Family | Ops | What the delta says (today) | Resolver behaviour | Status | +|---|---|---|---|---| +| Primitives | box, cylinder, cone, sphere | roles only (`box.top`, `cylinder.side`, ...) | anchors by role | **[T]** box top anchor; roles **[R]** | +| Extrude-like creators | extrude, revolve, sweep, loft, twisted sweep | roles (`extrusion.*`, `revolution.*`, `loft.*`, `sweep.*` start/end/side); side faces `Generated` from profile edges | anchor by role; sides by source edge (derived) | **[T]** end caps of all four through a union and a translate, extrude cap through a fillet; loft side correspondence is kernel-matched, not named **[R]** (playbook) | +| Booleans | cut, union, intersect, boolean list | `modified` fragments, `preserved`, `deleted`; merged faces get one entry per parent | `ok` / `split` (several fragments) / `merged` (fragment also descends from a foreign face) / `lost(deleted)` | **[T]** all four outcomes | +| Blends | fillet, chamfer | trimmed faces `modified`; blended edges `deleted` + patch `generated` (role `fillet.patch`) | face: `ok`; edge: `lost(deleted)` in identity, patch in derived | **[T]** face and edge->patch | +| Shell | shell | removed face -> its rim `modified` (`shell.closing_descendant`); offset faces `generated`; kept faces `preserved` (+ their offset face generated) | opened face: `ok` (the rim); kept face: `ok` | **[T]** (kept faces fixed, 7.2) | +| Rigid copies | translate, rotate, mirror, linear/radial pattern | every face `modified`/continuation; a pattern is one translate/rotate node per instance | `ok`; an instance is addressed by its own node | **[T]** translate, rotate, mirror, linear and radial pattern | +| Affine / offset / draft | transform_rsolid (rigid and affine), scale (uniform and per axis), offset_rsolid, draft | face and edge witnesses from the retained OCCT history (`modification_delta`): `modified`/continuation; an offset or drafted face (OCCT: generated) continues its source face | `ok` | **[T]** every op, `test_history_through_operations_that_had_none`, the draft test, `test_modification_delta.cpp` (was `no delta` before this branch) | +| Helical sweep | helical_sweep_rsolid | in a session it records helix + face + sweep nodes; the sweep is tracked | anchor by `sweep.*` roles | **[T]** | +| Identity ops | apply_tag*, make_select_* | no delta; outputs keep the source's topo ids | aliased by topo id (selection: only its `target_kind`) | **[T]** C++ and kernel (fillet through edge selections) | +| Section / split | section, split | not probed | -- | **[I]** | +| Sketch | sketch solve -> wire/face | no deltas on edge/wire/face construction nodes | a profile edge's identity is its sketch geometry id (FreeCAD's `g1;SKT`) | **[I]**, stage 2 | + +### 3.1 Tags are a view over recorded history (owner decision: switch directly) + +**Model.** A tag assignment is already a graph node: `apply_tag_rselection` and every op-kwarg tag +(`top_face_tag=`) record an `apply_tag_*` node whose `tag_binding` parameter holds the binding and its target +anchors (`explicit_refs`, or `selection_query` with `evidence.selected_refs`) **[R]**. So a tag needs no copy on +every later shape: the tags of an entity of node N's output are *computed* from N's upstream graph: + +``` +tags(N, e) = own bindings of e (the op's kernel roles; bindings set on N's own output) + + every binding B of a tag node upstream of N whose anchors reach e through the deltas, + step by step, as B's lineage policy allows +``` + +Per step (node M with a delta, for every witness whose parent carries B): + +| witness | carried when | +|---|---| +| `preserved` / `modified`, same kind | `lineage_policy_allows(B, derivation)`: continuation (`continuation`), continuation + fragment (`continuation_fragment`, the default: a split face tags *all* its pieces), explicit list | +| `generated` | never by continuation: a face a feature generates from a tagged source (fillet / chamfer patch from its edge, extrude / loft side from its profile edge) is *named* by that operation after its source when it runs (the projected binding, `semantic/autotag.cpp`), an op-local fact like a kernel role | +| `deleted`, not mentioned, or a node without history | not carried: the tag ends there, and `explain_tag` says where | + +Identity ops (tag assignments, selections) pass everything on under their own node. The view is computed +**incrementally per node** from its inputs' views and its own delta (O(delta entries) per node), memoised per node +object (nodes are immutable), so a query costs a lookup after the first one. It lives in the C++ kernel +(`scad::reactive` tag view, `src/cpp/src/reactive/tag_view.cpp`) and is installed as the topo layer's +history-witness provider, so `Shape::has_tag` / `list_tags` / `list_tag_bindings` / `explain_tag` and therefore +every `ql.tag(...)` call site see it without any API change **[T]** (`test_tag_view.cpp`, the audit). + +**One path per binding, chosen by where the binding lives.** A binding a *recorded* tag assignment made +(producer `user_operation` with a node id, on a value that is part of a recorded graph) is carried only by the view: +the per-operation copy (`carry_source_lineage`, `project_source_bindings` in `semantic/autotag.cpp`) skips it. What +only the shapes know keeps the copy: imported sidecar facts (`provenance.*` of a `.scadbrep`), sketch topology names, +tags assigned outside a session, values replayed without a recording session, and the projected names above. There +is no binding that both paths carry, so nothing is duplicated, and no compatibility switch. **[T]** (full suite: the +brep provenance, sketch exact-source-identity and replayed-connector tests exercise the shape path.) + +**Scopes.** `local` = bindings stored on the entity; `inherited` = from containing entities (downward +propagation); `lineage` = the history view; `effective` (the default of `ql.tag`) = local + inherited + lineage. +This is the visible change: a plain user face tag now survives booleans, blends, shells, copies and patterns +(it used to stop at the face-source gate), and an op-kwarg tag survives fillets and copies. + +**Persistence and versioning.** The tag lives in its graph node, so it travels with the cell cache (the lineage +record of a cell holds its nodes, deltas and `tag_binding` params) and with model JSON; nothing is pickled on +shapes. The binding has its schema version; the view algorithm is versioned with the naming version (section 5). + +**Bodies.** A view entry is keyed by (output slot, kind, topo id) of the node's output, so a multi-body node +(several slots, or one compound of bodies) keeps the body apart; references report which body (slot / solid) each +target lies in. + +### 3.2 Target API: queries, references, tags (production-grade) + +```python +# selection: a query is data (serialisable), resolution is explicit, cardinality is asserted +q = ql.faces().where(ql.tag("mount.face")) # tags through history (3.1) +faces = q.exactly(2).resolve(part) # error lists per-predicate counts and near misses +report = q.explain(part) # the same, as data: counts per predicate, candidates + +# references: stable anchors resolved through history (section 2, 4) +ref = scad.ref.of(face, part) # ElementRef: stable anchor + guard, JSON +ref.resolve(part) -> Resolution(status, entities, candidates, lost_at, reason, hops, bodies) +ref.one(part) / ref.all(part) # ElementRefError(status, candidates) unless exactly one / ok +ql.faces().where(ql.ref(ref)) # a reference as a predicate + +# tags: names over references +scad.apply_tag_rselection(part, faces, "mount.face") # unchanged; recorded as a tag node +scad.explain_tag(face, "mount.face") # binding, anchors, hops, where it stopped and why +ql.tag("mount.face", follow="derived") # also generated descendants (edge tag -> its patch) +ql.faces().where(ql.body("left")) # with research/multi-solid: per-body scoping +``` + +Stages: the view and the scopes (3.1) and the resolver (`simplecadapi.topology.element_ref`: `resolve`, +`require`, `stable_anchor_of`, `guard_of`) are on this branch; `scad.ref` as a public module, `ql.ref`, +`ql.body`, `Query.explain` follow (section 7). + +## 4. Resolution, failure statuses, and what the editor shows + +### 4.1 Algorithm (implemented, `element_ref.cpp`) [T] + +1. Take the target node's upstream closure (inputs first; works for a value restored without its session). +2. Seed the lineage with the anchor. For each later node that consumes a lineage member (a member is an entity of + one of the node's inputs): + - identity op (tag, selection) without history: alias the members under the node (same topo id); + - op without history: the member dies, reason `no_history`; + - otherwise: every witness whose `parent_refs` meet the consumed members continues the lineage (identity mode: + same kind, not `generated`); a `deleted` witness ends it (`deleted`); a member mentioned only by `generated` + witnesses ends it (`replaced`); a member not mentioned at all ends it (`no_history`, a coverage gap). A + successor that also descends from a same-kind entity outside the lineage marks the hop `merged`. +3. Targets = lineage members of the target node's slot. Only hops on the way to the targets are reported (a branch + that leads elsewhere is not this reference's history). +4. Status: none -> `lost` (latest death: node + reason); several -> `split`; one with a merged hop -> `merged`; + else `ok`. + +### 4.2 Editor states and the split rule (owner decision; implemented in the spike API) [T] + +A reference that splits is an **error the person resolves**, never "all pieces": `element_ref.require(anchor, +shape)` raises `ElementRefError(status="split", candidates=[...])`; the candidates are in a geometric order (centre, +then size), not OCCT's numbering; `require(..., piece=i)` is how the editor applies the person's choice, and the +choice is written back into the reference. `ResolvedRef.describe()` is the editor payload: status, `lost_at`, +reason, candidates (`topo_id`, kind, geometry type, centre, size, score). A lost reference with a `guard` +(`guard_of(entity)` at pick time) carries up to five look-alikes ranked by guard distance; they are offered, +never applied **[T]**. (A boolean that leaves a *body* in several pieces is not an error: that is a body split, +which the multi-body work turns into several bodies; only a *reference* that no longer names one entity is.) + +| Status | Badge | Feature behaviour | Editor offers | +|---|---|---|---| +| `ok` | green | runs | -- | +| `merged` | green with note | runs on the merged entity | "now also includes face of " | +| `split` | amber | fails with the candidates | the pieces, highlighted; picking one writes `piece` | +| `lost` | red | fails with `lost_at`, reason, look-alikes | accept a look-alike (writes a new anchor) or re-pick | + +`lost` because of `no_history` is reported differently from `deleted`: the first is a kernel gap, the second a +modelling fact. `anchor_missing` / `anchor_ambiguous` mean the anchored node itself is gone or not unique (2.2). + +### 4.3 Persistence + +- **Notebook cells.** The editor's `selection_recipe` emits `scad.ref.faces([...], source={BODY})` + instead of tag-or-window code; `extract_recipe` round-trips the literal; a repair rewrites the literal and `last`. + The tag and geometry lines move into `guard` (evidence, not mechanism). [I] +- **Cell cache.** `runtime/lineage.py` saves each cell's nodes with `graph.to_dict()`, which includes `topo_delta`, + and `OperationGraph.from_value` restores it **[R]** (`graph.cpp` `from_value` parses `topo_delta`; the header + comment "topology/semantic deltas are not restored" is stale, see SDK_DEFECTS 22). So a reference resolves through + restored cells as long as the value carries its node object **[T]** (`test_tags_and_references_resolve_on_values_restored_from_the_cell_cache`, fresh process). +- **Model JSON / packages.** `recording/serializer.py` writes deltas per node already (`"delta"`), so the resolver + can run on a deserialised graph; refs are data in the feature params. [R for deltas, I for refs] + +--- + +## 5. Versioning (implemented in the spike) [T] + +`kNamingVersion = "scad-naming/1"` (`element_ref.hpp`, `element_ref.NAMING_VERSION`) is stamped into every stable +anchor. It changes whenever a tracker's witnesses or the resolver's rules change (the shell fix of 7.2 would have +been a bump had references been persisted before it), and should be combined with the OCCT version when OCCT is +upgraded (FreeCAD versions its element map the same way). A reference made under another version is still resolved, +and the result says `version_changed`; the caller (editor) compares it against its guard: agreement rewrites the +version silently, disagreement shows the reference as `lost(version_changed)` with candidates [I]. References need no +migration of their own because they store anchors and policies, not resolved names. Tags need none either: they are +recomputed from the graph on every read. + +## 6. Audit: tag and history propagation per family (before -> now) + +All rows **[T]** in `test/test_element_ref_history.py` (`test_audit_tag_channels_and_history_per_family` and the +status tests) unless marked; **[R]** = seen in a probe, not asserted. Probe: box 40x20x10 with the top face tagged +by kwarg (`kw.top`) and by `apply_tag_rselection` (`user.top`). "Before" is `dev` @ `093ef71`. + +| Family | plain tag before -> now | kwarg tag before -> now | history resolver before -> now | +|---|---|---|---| +| cut | dies -> survives | survives -> survives | ok | +| cut through the face | dies -> both pieces **[R]** | both pieces **[R]** | split (2 pieces) | +| cut removing the face | dies | dies | lost(deleted) | +| N-ary cut (3 tools) | dies -> both pieces | -- | lost(replaced) -> split (composed steps, 7) | +| union (disjoint / coplanar) | dies -> survives | survives | ok / merged | +| intersect | dies -> survives | survives | ok | +| fillet / chamfer | dies -> survives | dies -> survives | ok | +| shell, face kept | dies -> survives | dies -> survives | lost(replaced) -> ok (7.2) | +| shell, face removed | dies -> survives (the rim) | survives | ok (the rim) | +| translate / rotate / mirror | dies -> survives | dies -> survives | ok | +| linear / radial pattern instance | dies -> survives | dies -> survives | ok | +| transform_rsolid, scale_rsolid, offset_rsolid | dies -> survives | dies -> survives | lost(no_history) -> ok | +| draft | -- | -- | lost(no_history) -> ok (drafted face and neighbours) | +| helical sweep | -- | -- | ok through its sweep node (never a gap) | +| edge -> fillet patch | the fillet names the patch after the edge (projection, unchanged) | -- | lost(deleted) identity; the patch in derived mode | +| extrude cap -> fillet | -- | dies -> survives | ok | +| creator end caps (extrude / revolve / loft / sweep) -> union -> translate | -- | -- | ok | +| tag after pickling the value | -- | -- | survives (`test_a_tag_survives_pickling_with_its_history`) | +| loft sides | -- | -- | not probed; kernel-matched (section naming, stage 2) | + +--- + +## 7. Staged plan + +### Done on this branch +- Resolver (`resolve_by_history`), role anchors, recorded-id lookup; statuses ok / split / merged / lost. +- History for transform / scale / offset / draft (`modification_delta`); shell kept faces preserved (7.2); N-ary + boolean steps composed (`merge_topo_deltas`). +- Stable anchors (feature, op, occurrence-among-ancestors, params tie-break, role) and the backward trace to birth. +- Splits as errors with candidates; lost references with guard-ranked look-alikes; bodies; naming version. +- Tags as a view over history (3.1), effective includes lineage, one path per binding. +- Tests: `test/test_element_ref_history.py` (48), `test_element_ref.cpp`, `test_tag_view.cpp`, + `test_modification_delta.cpp`; goldens reviewed one by one (commits `df38241`, `facccaa`). + +### Next +1. Public `scad.ref` (`of`, `resolve`, `one`, `all`) and `ql.ref(ref)` as a predicate, with the SDK checklist + (docstrings, modules.json, serializer row, API page); `ElementRefError` exported. +2. Editor (`feat/gui-modeling`): `selection_recipe` emits `scad.ref.faces([...])` instead of tag-or-window code; + health badges and "this piece / this candidate" actions write the reference back (4.2). +3. Sketch roots: sketch geometry ids as anchors for profile edges, so side faces have names that survive profile + edits; a kernel-computed geometric rank for pieces of a split (2.4). +4. Per-node parent index for the resolver (7.1); `Query.explain(shape)` as data (the cardinality errors already + carry the per-predicate counts and near-miss inventory). +5. Multi-body (`research/multi-solid`): `ql.body(name)` once named bodies land; references already resolve per + output slot and report the body index inside a compound. +6. Corpus: parameter sweeps of the examples (insert a feature mid-cell, change widths, add an upstream fillet), + asserting zero wrong picks. + +### 7.1 Performance budget +Measured **[R]** (ad-hoc probe, Release build, Apple silicon; one top face of a 400x20x10 plate through N +successive hole cuts; the resolved face area matched 8000 - N*pi/4 at every N): + +| N cuts | model build | resolve (C++ walk) | resolve + face lookup | +|---|---|---|---| +| 10 | 0.08 s | 0.08 ms | 0.10 ms | +| 50 | 0.65 s | 0.56 ms | 0.59 ms | +| 150 | 4.75 s | 3.0 ms | 3.1 ms | + +The cost is the scan of each consuming node's delta, whose size grows with the model (O(N) entries per cut, so +O(N^2) overall). Next: a per-node index parent -> entries, built once per node and memoised, makes a resolve +O(hops). Budget [I]: < 1 ms per reference for 200-node histories, memoised per (ref, target node), nothing added at +recording time (the deltas are recorded already). Resolution is already negligible next to modelling (0.06 % of +build time at N = 150). + +### 7.2 The shell kept-face gap: option (a), implemented [T] + +`query_history` (`src/cpp/src/semantic/tracking_core.cpp`) emitted `preserved` only when an input had neither +`Modified` nor `Generated` images, so a shell's kept outer face (unchanged *and* the seed of its offset face) had +only the `generated` witness. It now also emits `preserved` when the input is still in the result. Every changed +expectation was reviewed: + +| Expectation | Before | After | Why | +|---|---|---|---| +| golden `shell` (C++ tracking) | 0 preserved | 5 kept faces + 4 rim edges preserved, 5 `shell.body_face` roles | the kept faces and the outer rim edges are IsSame in the result | +| golden `extrude_rect`, `extrude_circle`, `loft`, `loft_ruled` | profile edges: generated only | also preserved | the start cap of a prism / loft is bounded by the very profile edges | +| `test_tracking_behaviour.cpp` loft toward a point | 4 entries | 8 | the four base edges stay as the cap boundary | +| `test_tracking_behaviour.cpp` shell roles | `shell.body_face == 0` ("every kept face was re-created": wrong) | 5 | as above | +| goldens `ops_features_shell`, `ops_features_tag_projection` (Python port) | kept faces untagged | `face.shell.body_face` on the 5 kept faces | the role is now proven | +| golden `ops_features_blend_failures.shell_role` | "requires at least one kernel-proven result" | no failure | same | +| `test_operation_output_roles` | asserted `body_faces_tag` raised | asserts it tags the 5 kept faces | same | +| audit `shell_kept_face` | lost(replaced) | ok; the kwarg tag now survives too | same | + +## 8. Risks and non-goals +- OCCT history quality bounds everything; a tracker that omits a witness makes a reference `lost`, which is the + right failure but a regression in perceived robustness until the gaps in section 6 are closed. +- `generated` follow mode can fan out (an edge generates faces and vertices); the record keeps `kind` so the SDK + filters. +- Not addressed: assemblies (references across part instances), drawings, imported STEP bodies (no history: anchors + there are geometric only, guard-only refs with `lost` on any change). diff --git a/design-docs/topo-naming-research.md b/design-docs/topo-naming-research.md new file mode 100644 index 00000000..c4b383ce --- /dev/null +++ b/design-docs/topo-naming-research.md @@ -0,0 +1,327 @@ +# Topological naming: how FreeCAD does it, and what SimpleCADAPI should do + +Status: research note, 2026-11-04. Nothing here is implemented. + +Legend. **[V]** = verified by reading the cited source in this session (ego-browser; GitHub raw files fetched from +FreeCAD `main`, wiki pages). **[I]** = inference or recollection, not verified from a source here. Line numbers refer +to the `main` branch as fetched today and will drift. + +## 1. The problem + +A parametric feature (fillet, sketch-on-face, hole) must say "this face/edge of the upstream body". Kernels expose +sub-shapes only by traversal order or geometry. When an upstream parameter or operation changes, traversal order +reshuffles and geometry moves, so "Face13" or "the face with centroid near (0,0,10)" may now denote a different face, +or none. A stable reference needs an identity for each sub-shape that is (a) derived from the modelling history, not +from position, and (b) re-resolved after every rebuild, with an explicit outcome when the identity is gone. + +## 2. How FreeCAD 1.0 does it + +### 2.1 Provenance and status + +- The algorithm is realthunder's, developed in his Link/LinkStage3 fork; it was integrated into FreeCAD 1.0 over about + a year (release notes: "Realthunder's algorithm has been carefully implemented... The TNP problem is not completely + solved") [V]. Sources: + - Release notes 1.0: https://wiki.freecad.org/Release_notes_1.0 (PR #13705 listed as the finalizing PR). + - Wiki page: https://wiki.freecad.org/Topological_naming_problem. It states the algorithm has three purposes, in + order: (1) detect and display broken references, (2) propose a likely fix (e.g. fillet/chamfer edge selection), + (3) auto-repair only when confident ("First, do no harm") [V]. It explicitly says the algorithm "is not intended + to fix every failure"; datum planes and body-origin-based sketches remain best practice [V]. + - realthunder's design notes: https://github.com/realthunder/FreeCAD_assembly3/wiki/Topological-Naming (framework, + tags, hasher, versioning) and https://github.com/realthunder/FreeCAD_assembly3/wiki/Topological-Naming-Algorithm + (the naming algorithm with pseudo-code) [V]. + +### 2.2 Data structures [V] + +- `Data::IndexedName` = type + index (`Face12`). `Data::MappedName` = a byte string (the stable name). Source: + https://github.com/FreeCAD/FreeCAD/blob/main/src/App/MappedName.h and + https://github.com/FreeCAD/FreeCAD/blob/main/src/App/IndexedName.h +- `Data::ElementMap` (https://github.com/FreeCAD/FreeCAD/blob/main/src/App/ElementMap.h): a bidirectional map + MappedName <-> IndexedName, owned by `Data::ComplexGeoData` (base of `Part::TopoShape`). Documented as + `indexedNames` (type -> queue of children) plus `mappedNames` (name -> IndexedName); children can carry a recursive + child map (for compounds). `find(MappedName)`, `find(IndexedName)`, `findAll(IndexedName)` exist; one element can + have several names, a name maps to exactly one element (realthunder notes, "setElementName"). +- `Part::TopoShape` additionally carries an integer `Tag` identifying the owner document object (unique per document, + not a UUID). Tag 0 disables automatic mapping [V, realthunder notes "Object Identification"]. The tag is appended to + names whose source comes from a different shape so that two operands do not collide. +- `App::StringHasher` (per document): long names are replaced by `#`, with the original strings stored in a + reference-counted table and persisted with the document; only strings actually used are saved by default [V, + realthunder notes "String Hasher"]. +- Element map version string, `GeoFeature::getElementMapVersion()`: concatenates the TopoShape algorithm version, OCCT + version, ComplexGeoData version and hasher threshold. On document load, a mismatch marks the feature for recompute + and resets link shadow copies, because any change in OCCT history behaviour changes generated names [V, realthunder + notes "Element Map Versioning"; `GeoFeature.cpp` line ~264 reads the version]. + +### 2.3 How names are generated per operation [V] + +All "make element" operations in +https://github.com/FreeCAD/FreeCAD/blob/main/src/Mod/Part/App/TopoShapeExpansion.cpp +(`makeElementBoolean` ~l.6069, `makeElementFillet` ~l.4148, `makeElementChamfer` ~l.4180, `makeElementPrism` ~l.4644, +`makeElementPrismUntil`, `makeElementShape`, loft/pipe/offset variants, `makeShapeWithElementMap` ~l.1420) build the +result with an OCCT `BRepBuilderAPI_MakeShape` derivative (or `BRepAlgoAPI_*` / `BRepTools_History`) and then hand +it to `TopoShape::makeShapeWithElementMap(shape, mapper, inputShapes, op)`. The `Mapper` abstraction (`MapperMaker` +at ~l.5745-5840) forwards to the OCCT history calls `Modified(s)`, `Generated(s)`, and for histories also +`IsDeleted`, each wrapped in try/catch because "OCCT being OCCT" not all makers honour the interface. +`BRepBuilderAPI_Sewing` and `BRepOffsetAPI_ThruSections` need special handling (algorithm page). + +The core algorithm (algorithm page, `makESHAPE` in the pseudo-code), four passes: + +1. Unchanged elements: copy the input names onto the identical sub-shapes of the result (`mapSubElement`). +2. Modified / Generated: for each input vertex, edge, face, ask the mapper for `Modified` and `Generated` new + sub-shapes; each new element gets a name built from the source's mapped name plus a marker, an op code, the source + tag and the source name length. If several sources map to one element, the first is the "main" source and the rest + go in a bracket group. +3. Upper-element naming: still-unnamed edges/vertices get `;:U`, possibly several names (one per + adjacent named face), "to improve stability of the next step". +4. Lower-element naming: still-unnamed faces/edges are named from the sorted set of their named lower elements, + `(edge1,edge2,...);:L`. + +Name grammar (examples from the algorithm page, verbatim shapes): +`Face6;:M2;FUS;:T1:5:F`: source `Face...` (5 chars, tag 1), `;:M2` = modified, 2nd modification index, `FUS` = fuse op +code, `;:T1:5:F` = trailing tag 1, source-name length 5, element type F. `;:G` = generated, `;:MG` = both, `;:U` = +upper-derived, `;:L` = lower-derived. A name nests: the source's whole mapped name is the prefix, so a name encodes +the history chain and can be traced back (`traceElement`, `isElementGenerated`, ~l.6344). The selection prefix `;` +marks a mapped name in the GUI (`;:M...`); a missing reference is displayed with `?` (`Data::MISSING_PREFIX`, +`GeoFeature::hasMissingElement`) [V for the existence of the missing prefix; the exact characters I did not +confirm]. + +Primitives (`Part::Box` etc.) have no map: their elements are considered fixed (`Face1`...). Sketcher names its edges +by geometry id: `g1;SKT` -> `Edge1`, `g1v1;SKT` -> `Vertex1` [V, realthunder notes], i.e. the sketch's own geometry +ids are the roots of every name derived from it. That is why sketch -> pad faces are stable. + +### 2.4 How a reference is stored and re-resolved [V] + +- A feature stores references in `App::PropertyLinkSub` / `PropertyLinkSubList` / `PropertyXLink` + (https://github.com/FreeCAD/FreeCAD/blob/main/src/App/PropertyLinks.h): object + sub-name list. Each sub-name has a + `ShadowSub = ElementNamePair {newName, oldName}`: the mapped (stable) name and the plain indexed name, both kept + current. Saves still write the indexed name, so older FreeCAD can open the file (realthunder notes "Property Link"). +- When the owner shape changes, `GeoFeature` signals dependents, and `PropertyLinkBase::_updateElementReference` + (https://github.com/FreeCAD/FreeCAD/blob/main/src/App/PropertyLinks.cpp ~l.449-600) looks up the mapped name in the + new `ElementMap` and rewrites the stored indexed name if it moved (`Face13 -> Face14`). +- If the mapped name is missing: first a geometry search, `GeoFeature::searchElementCache(oldElement, + CheckGeometry)`, then with plain tolerance, then `rescueDriftedElement`. Before the shape changes, GeoFeature + snapshots the geometry of every referenced element; after the change it searches the new shape for an element with + the same geometry (`SearchOption::CheckGeometry | SingleResult | AdaptiveTolerance`, which "widens the tolerance + stepwise and accepts only a unique match", `ComplexGeoData.h` l.60-65). Only if all that fails the reference is + marked missing (`?` prefix) and the feature errors. So the order is: name first, geometry as repair, error last. +- Dress-ups (`FeatureDressUp.cpp`, https://github.com/FreeCAD/FreeCAD/blob/main/src/Mod/PartDesign/App/FeatureDressUp.cpp) + read `ShadowSub` (`newName` else `oldName`, ~l.290) and can relink to matching sub-elements when the base feature is + replaced (`relinkToMatchingSubelements`, ~l.384): this is the "propose a candidate fix" behaviour. + +### 2.5 What still breaks [V unless marked] + +- The project itself says TNP is "not completely solved" (release notes) and the algorithm is a heuristic that + "is not intended to fix every failure" (wiki). Inserting/removing features mid-tree makes auto-repair less likely. +- Names depend on OCCT history. Any OCCT change, hasher on/off, threshold, or algorithm change alters names, hence the + version string and forced recompute (above). Names are therefore not stable across kernel upgrades; this is handled + by recomputing, not by keeping the old names. +- Splits/merges: one old face becoming two (e.g. a cut through a face) yields two elements whose names share the + source and differ by the `M` number; which one the reference follows depends on OCCT's ordering of + `Modified()` [I, consequence of the grammar]. +- Not every operation is naming-aware; shapes built by non-`makeElement*` code get no map (`ElementMapPolicy::Drop` + exists in `makeShapeWithElementMap`) [V for the policy; which workbenches still drop is I]. + +### 2.6 Cost and size [V qualitatively, no numbers measured] + +Names grow with history depth (each name embeds its source name); realthunder introduced the StringHasher for +exactly this and warns names "will quickly go out of control" without it. Cost per operation: building per-type +ancestry caches (`ShapeInfo`, `_cache->getAncestry`) and iterating every input sub-shape through the mapper +(passes 1-4 above are O(sub-shapes) with hash maps). The element map is saved with each shape-bearing object. I did +not find benchmark numbers; the 1.0 integration discussion mentions performance work but I did not read it. + +## 3. Other systems (all from recollection unless noted; none were fetched, one Onshape docs fetch was blocked) + +| System | Mechanism [I] | Stable across upstream edit? | Failure behaviour | +|---|---|---|---| +| Onshape | Each topological entity gets a deterministic ID derived from the feature that created it and its role (e.g. "the cap of this extrude"), computed from the feature's own history. FeatureScript queries (`qCreatedBy`, `qOwnerBody`, `qAdjacent`, `qGeometry`...) are stored; persistent IDs are resolved at rebuild | Yes for most; the design intent is the query, not a single ID | Reference can become empty/ambiguous; feature errors, user re-selects. | +| SolidWorks | Feature tree + internal persistent references (per-face/edge ids that are carried through the feature history) plus geometry heuristics | Mostly; known failures ("dangling" references) | Rebuild error / "lost reference" until user re-attaches. | +| Fusion 360 | Entity tokens (opaque strings) for B-Rep entities, valid for the same design state; plus a "temp ID" per session | Tokens survive many edits within a design; not guaranteed across all | Token lookup fails -> null entity. | +| CadQuery / build123d | No persistent naming; selectors by geometry/orientation (`faces(">Z")`), tags on workplane stack only within one script | n/a: re-run whole script, selector re-evaluates on new geometry | Selector returns 0 or wrong set; user script handles it. (This is where we are today.) | + +Common point [I]: all commercial kernels give the *kernel* the job of reporting per-operation history (Parasolid/ACIS +have it; OCCT has `Modified`/`Generated`/`IsDeleted`), and store a query or lineage, not a position. + +## 4. What our kernel already records (read from src/cpp, [V]) + +Good news: the C++ kernel already captures OCCT history per operation, and tag lineage is already projected through +it. What is missing is a persistent, resolvable *name* built from that history. + +- OCCT history capture: `src/cpp/include/scad/semantic/tracking.hpp` ("OCCT algorithm history -> TopoDelta"). + `src/cpp/src/semantic/tracking_detail.hpp` defines `HistorySource` with `BuilderHistory` (wraps a finished + `BRepBuilderAPI_MakeShape`: `IsDeleted`/`Modified`/`Generated`) and `RetainedHistory` (wraps `BRepTools_History`). + Per-operation trackers: `tracking_blends.cpp` (`track_blend` for fillet/chamfer, uses `op.Contour`/`op.Edge` to find + the edges actually blended and marks generated faces as blend patches; shell likewise), `tracking_booleans.cpp`, + `tracking_sweeps.cpp`, `tracking_lofts.cpp`, `tracking_transforms.cpp`, `tracking_primitives.cpp`. Booleans fill + history via `kernel::booleans.cpp` (`SetToFillHistory`, `fuse_shapes_with_history` merging the unify history). + `TrackingPolicy::Graph` turns face/edge history queries off (used for standard-part factories) [V, header comment]. +- Result of tracking: `TopoDelta` in `src/cpp/include/scad/topo/delta.hpp`: per output/deleted sub-shape a `TopoEntry` + {`TopoRef`, `TopoEvent` Preserved/Modified/Generated/Deleted, `origin_role`, `parent_refs`}, plus kernel-native + `TopoRoleEntry` roles ("extrude.first_cap"). It is stored on every graph node: `std::optional topo_delta` + in `src/cpp/include/scad/reactive/graph.hpp` (node and node-spec). +- Identity: `kernel_topo_id()` = hash of (TShape, Location), orientation ignored; it is valid only while the shape is + alive and "the recorder resolves kernel ids to stable per-root ids" such as `face_3` (`topo_index` in + `src/reactive/internal.hpp`, "face_3 -> 3") [V]. A `face_N` id is an *index* into the root's face list, i.e. + positional: the same weakness as FreeCAD's `Face12`. [I] it is stable within one executed graph run but not across + re-execution after a parameter change. +- Semantic layer on top: tags (`include/scad/topo/tagging.hpp`, `TagBinding` with 128-bit id, evidence, certainty, + lifecycle; `tag_factories.hpp` with `LineagePolicy`, `lineage_policy_allows(policy, LineageDerivation)`; + `shape.hpp` `add_tag_lineage`) projected through the delta. This is why kwarg tags survive booleans and source-edge + tags project to fillet patches (docs/skill/references/ql-playbook.md rows at l.58-62). +- Editor side: `src/simplecadapi/editor/features.py` `selection_recipe()` (l.~798) emits `scad.ql.tag(...)` first when + every picked item carries a tag with a known feature slug, else geometry (`selector_conditions` in + `selection.py`), with `.exactly(n)` as the count check; `extract_recipe` keeps it on edit. + +What is missing: + +1. No persistent identity for a *picked* sub-shape: the editor stores tags/geometry, not "this sub-shape of node N". +2. Deltas are recorded per node but there is no resolver "(node, sub-shape name) -> sub-shape of the re-executed + result", and no name that is stable across re-execution. `face_N` is an index. +3. Plain user tags on faces are blocked by a "face-source gate", and face-channel gaps exist for kwarg tags through + fillet/chamfer (playbook rows). The delta has the data (`Modified`/`Generated`) but the projection stops. +4. No fallback ranking (history, then geometry with uniqueness) and no "candidate fix" output. Geometry fallback is + in the notebook cell source and is a hard windowed match, not tolerance-widening-until-unique. +5. History is skipped under `TrackingPolicy::Graph`, and some ops (loft section correspondence, per playbook l.185) + are explicitly "kernel-matched, not stable". + +## 5. Recommendation + +### 5.1 What the stable reference should be + +A **history path**, not a tag and not a position: the reference is +`(origin node id, origin entity role, derivation chain)` where each link is +`{node id, event ∈ {Preserved, Modified, Generated}, source ref, ordinal}`. Concretely, store a small record in the +feature (not a string-encoded blob like FreeCAD's `;:M2;FUS;:T1:5:F`; we have a typed `TopoDelta`, so use it): + +``` +ElementRef = { + anchor: {node_id, kind, role?, index?}, # the "root name": where this entity was born + chain: [ {node_id, event, ord} ... ], # Modified/Generated hops up to the node the feature consumes + guard: {kind, area, centroid, normal/length} # geometric fingerprint used only to verify/repair +} +``` + +Roots (anchors) should be the things already deterministic in our kernel: a primitive's named faces, an extrude's +`first_cap`/`last_cap`/side-per-profile-edge (sketch geometry ids, as FreeCAD uses `g1;SKT`), revolve caps, and +user-pinned semantic tags. Everything else is derived by following `Modified`/`Generated` through `topo_delta`. +This is exactly FreeCAD's model (stable roots + history hops) with typed fields instead of string grammar. + +### 5.2 Where history capture belongs + +The C++ kernel, per operation, via the OCCT make-shape history; it is already there (section 4). The work is not +new capture but (a) making the kernel emit, for each result sub-shape, a **name** (root + hops) rather than only a +per-run index, and (b) a deterministic ordinal when several result sub-shapes descend from one source (sort by a +stable key: tag/role first, then a geometric key such as centroid lexicographic order at fixed tolerance, never +OCCT iteration order), because `Modified()` ordering is not a promise [I]. Use the same fall-through order FreeCAD +uses for unnamed elements (upper-derived, then lower-derived names) only where history yields nothing. +Ops that skip history (`TrackingPolicy::Graph`) are fine: the user does not reference faces of standard-part +internals; but the *consumed* body of any feature a user picks from must run with history on. + +### 5.3 How a GUI feature stores it + +The picked item's `ElementRef` (JSON, short) goes in the feature cell as a kwarg-style literal or a structured cell +comment that the editor already round-trips (`extract_recipe`), e.g. +`_edges = scad.ref.edges([ElementRef(...), ...], source={BODY})`, resolved by the SDK, not by recipe text. Keep the +current tag and geometry lines as *secondary* evidence inside the same ref (`guard`), not as the main mechanism. +Per FreeCAD's `ShadowSub`, also keep the last resolved positional name only as a cache/hint. + +### 5.4 Resolution and failure behaviour + +Resolver order (mirrors FreeCAD 2.4): + +1. Follow the chain through the new run's recorded deltas to a set of result sub-shapes. +2. If exactly one: done. If several (split): pick by ordinal; if the picked count differs from what the feature + needs, fail with the candidate list. +3. If none (deleted or chain broken): geometry search with `guard`, widening tolerance stepwise and accepting only a + **unique** match (FreeCAD's `AdaptiveTolerance`); mark the result `repaired_by_geometry` in the output. +4. Else raise a typed error `ElementRefLost` on the feature, carrying the last known name, the guard, and up to N + candidates ranked by geometric distance for the editor to offer ("propose fix", FreeCAD purpose 2). Never silently + pick another entity ("First, do no harm"); this is already our rule ("an error on the feature, never another + face"), keep it. +5. Whenever a repair happens (step 3 or user-accepted candidate), write the new name back into the cell. + +Always run the check that the resolved kind and count match, as today. + +### 5.5 Staged plan, smallest first + +1. **Spike (smallest): resolve through existing deltas, no new storage.** In the Python/C++ boundary add + `resolve_by_history(body_value, node_id, topo_id) -> [faces]` that walks `topo_delta` entries + (`parent_refs`, event) from the picked entity's birth node to the current result. Use only for fillet/chamfer + after boolean/extrude; compare against the geometric recipe on the failing cases found so far (face tags dying + after later ops; kwarg tags lost through fillet). Success = those cases resolve with zero geometry fallback. + Needs no format change. +2. **Persistent names.** Replace the per-run `face_N` with root+chain names as in 5.1; add deterministic ordinals + (5.2). Add the `ElementRef` literal and make `selection_recipe` emit it; keep tag/geometry as `guard`. +3. **Resolver with repair and errors** (5.4), including `ElementRefLost` with candidates; editor UI shows them. +4. **Close the gaps in projection:** lift the face-source gate for user tags and the fillet/chamfer face-channel gap + by feeding them from the same chain, so tags become a *view* over the names instead of a parallel system. +5. **Versioning and tests.** An element-map version string like FreeCAD's (our naming algorithm version + OCCT + version); on mismatch, force full recompute and re-resolve with guard. Regression: reuse the existing editor + selection tests and add a corpus (parameter sweep of a bracket: change widths, add a fillet upstream, insert a + feature mid-tree) asserting resolved-by-history rate and zero wrong picks. Measure delta size and resolve time + on the arm6 benchmark before enabling history on large graphs. + +### 5.6 Risks and non-goals + +- Cannot be perfect: FreeCAD, with years of work, still documents failures. Target "never silently wrong, mostly + automatic", not "never fails". +- OCCT history is imperfect for some algorithms (sewing, thru-sections, offsets, `BOPAlgo` fuzzy cases); keep per-op + coverage explicit and fall back to guard for those, as FreeCAD does with `ElementMapPolicy::Drop`/special mappers. +- Lofts: per-section correspondence is kernel-matched; do not promise stable side-face names until sections are + named by input wire edges. +- Avoid FreeCAD's string-encoded grammar and its hasher; typed records plus an interned table are simpler for us, but + record growth with chain depth still needs a cap (collapse a chain to its nearest durable anchor, e.g. a tag or a + primitive root). + +## Source list + +- https://wiki.freecad.org/Topological_naming_problem +- https://wiki.freecad.org/Release_notes_1.0 +- https://github.com/realthunder/FreeCAD_assembly3/wiki/Topological-Naming +- https://github.com/realthunder/FreeCAD_assembly3/wiki/Topological-Naming-Algorithm +- https://github.com/FreeCAD/FreeCAD/blob/main/src/App/ElementMap.h +- https://github.com/FreeCAD/FreeCAD/blob/main/src/App/MappedName.h +- https://github.com/FreeCAD/FreeCAD/blob/main/src/App/IndexedName.h +- https://github.com/FreeCAD/FreeCAD/blob/main/src/App/ComplexGeoData.h +- https://github.com/FreeCAD/FreeCAD/blob/main/src/App/GeoFeature.cpp +- https://github.com/FreeCAD/FreeCAD/blob/main/src/App/PropertyLinks.h and PropertyLinks.cpp +- https://github.com/FreeCAD/FreeCAD/blob/main/src/Mod/Part/App/TopoShapeExpansion.cpp +- https://github.com/FreeCAD/FreeCAD/blob/main/src/Mod/PartDesign/App/FeatureDressUp.cpp +- Not read: PR #13705 itself (only its mention in the release notes), the forum thread "Topological Naming, My Take", + and any Onshape/SolidWorks/Fusion documentation. + +--- + +## Addendum: Onshape, read from primary sources (ego-browser, 2026-11-04) + +This replaces the Onshape row above, which was written from memory (the first fetch was stopped by a login wall on `cad.onshape.com/FsDoc/queries.html`; +the FeatureScript standard-library reference is readable at `formlabs.onshape.com/FsDoc/library.html`, and the REST guide at +`onshape-public.github.io/docs/api-adv/associativity/`). + +**Verified** + +- **A reference is a query evaluated against the current history, never a stored entity id.** Features pass `Query` values; a feature's `id` is a + hierarchical path (`id + "extrude1"`) and `qCreatedBy(id + "extrude1", EntityType.FACE)` resolves to whatever that operation created in the + *current* regeneration. Rule from the library docs: "the full id hierarchy must reflect creation history", each Id (and every parent) must cover a + contiguous region of operations, so an id is a position in the operation sequence, not a number on the geometry. +- **Tracking through later operations is explicit and history-based:** `startTracking(context, subquery)` returns a query that evaluates to the entities + that *derive from* the subquery's result (the docs' example: a sketch line `line0` extruded yields a face and two edges); `startTrackingIdentity` + returns the entities that inherit the identity; `makeRobustQuery(context, subquery)` produces "a query robust to identity-preserving changes"; + `makeRobustQueriesBatched(..., followSplitMerge)` does it per entity, optionally through splits and merges. This is the same idea as FreeCAD's mapped + names (derivation recorded by the kernel's operation history), exposed as an API instead of an encoded string. +- **Unstable ids are handled by disambiguation:** `setExternalDisambiguation(context, id, query)` for operations whose id has an unstable component + (sketches, planes, points that track no dependency); a pattern instance takes an `identity` entity so it follows its source, not its index. +- **Failure is loud:** `verifyNonemptyQuery` raises a regen error and marks the parameter faulty when a query "no longer resolves to anything" + (the face is not silently replaced by another). +- **Externally there is no persistent id at all.** The REST guide: "Onshape does not expose a persistent ID for any of these entities. When the model + changes, the ID may change", so it offers `POST .../idtranslations` from a source microversion: each id maps to `OK`, `SPLIT` (several targets) or + `FAILED_TO_RESOLVE` (none); the guide stresses "a face may become zero, one or multiple faces". A rollback translates back to the original ids. + +**What it means for us** + +1. Onshape's answer and FreeCAD's agree in substance: the reference is *derivation through recorded history*, with a three-way outcome (same / split or + merged into several / gone) and no silent retargeting. Geometry matching is a repair step at best. +2. The shape worth copying is Onshape's API: a **reference = (anchor operation id, entity kind, optional tracking)**, resolved by walking the + per-operation history we already store (`TopoDelta`), returning *a list* with a status (`ok` / `split` / `lost`), and a `follow_split_merge` choice. + Our graph node ids are already deterministic positions in the operation sequence, which is what Onshape's hierarchical ids are. +3. The status vocabulary (`OK`, `SPLIT`, `FAILED_TO_RESOLVE`) should be the editor's reference health: green, amber with the candidates, red. +4. Still not read: Onshape's internal algorithm (not public), Parasolid's own attribute mechanism, and the FreeCAD PR threads; the Fusion and + SolidWorks rows stay unverified. diff --git a/docs/skill-design.md b/docs/skill-design.md new file mode 100644 index 00000000..1275295b --- /dev/null +++ b/docs/skill-design.md @@ -0,0 +1,136 @@ +# The skill: design contract + +`docs/skill/` is the skill an agent loads to use SimpleCADAPI. This file is the contract its pages follow. The rules here +are checked by `python tools/skillbuild.py --check` (see "Checks"), so a page that breaks one fails the build. + +## What the skill optimises for + +1. **Few agent turns.** An agent reads what a step needs in **one parallel batch** and then works. Pages are small and + have one job so they can be loaded separately, and the router tells the agent which pages belong together. +2. **A small reading budget.** The first batch of any route is at most **1,200 compiled lines** (router, the two + always-read pages, the workflow page and its `Read now` files); every later batch stays under the same bound. (Measured before this design: a median of 1,493 lines and 7 files read before the first write, p90 2,501 + lines and 20 files.) +3. **One home for each rule.** A rule is stated once, with its reason; other pages link to it. +4. **Two modes.** Every workflow runs interactive or unattended (`references/discipline/operating-modes.md`). +5. **Four curated targets.** The skill compiles to seven harness targets; `zcode`, `opencode`, `claudecode` and `codex` are + written for. `omp`, `dsh` and `kimicode` are compiled from the same sources and must pass the checks, with generic wording. + +## The layers + +```text +SKILL.md router: start here, route table with read sets, always-on rules (<= 130 lines) +references/workflows/ one lifecycle per task; each has Read now / Read when / Unattended mode +references/workflows// the role or stage pages of a long workflow, loaded when the role starts +references/domains/ capabilities (what the SDK offers for a kind of task) +references/discipline/ reasoning, invariants, rules (core-rules, operating-modes, the notebook rules ...) +references/docs/guides/ how the runtime works +references/docs/api|stdlib|core/ generated signatures (never hand-edited) +``` + +## Page contracts + +**Every hand-written page** (everything but `docs/api`, `docs/stdlib`, `docs/core`) is at most **300 lines** (`SKILL.md` +at most **130**). A longer page is split by job, not trimmed. The one exception is a versioned run contract (its first lines +say `Contract ID:`), which an external harness reads whole. + +**Domain and discipline pages** open, within their first 8 lines, with `**Read when:** .` Nothing else is +required of their body. + +**Workflow pages** (`references/workflows/*.md`) have these sections, with exactly these headings: + +```markdown +## Read now (one parallel batch) +| File | Why | +| --- | --- | +| `domains/requirement-refinement.md` | ... | + +## Read when +| When | File | +| --- | --- | +| a Role starts | `workflows//role-1-....md` | + +## Unattended mode +| Interactive gate | Unattended replacement | +| --- | --- | +| Role 1 consent gate | ... | +``` + +- `Read now` lists every file the first batch needs besides the always-read pages, **including the workflow's own role-1/first-stage page**; paths are + written relative to `references/` (for example `domains/part-modeling.md`) and in backticks, one per table row. +- `Read when` lists everything else with its trigger; nothing is read "just in case". +- `Unattended mode` has one row for **every** place the workflow asks a person, waits for one, or needs a second reviewer + (search the old text for `⛔`, "ask", "user", "consent", "confirm", "question", "review"), using the replacements in + `discipline/operating-modes.md`. A workflow with no such place says so in one sentence. +- `core-rules.md` and `operating-modes.md` are read on every route (the router says so), so `Read now` does not list them. +- The compiled lines of `SKILL.md` + `core-rules.md` + `operating-modes.md` + the workflow page + its `Read now` files are at + most **1,200** (checked per target; the lint prints the sizes with `--read-budget`). + +**The router's route table** repeats the workflow page plus its `Read now` set in its own column, so an agent can issue the batch +straight from the table without opening the workflow first. The two must agree (checked). + +## Parallel reading + +The router tells the agent to read a batch in one go. How is per target (compile blocks in `SKILL.md`): + +- `claudecode`, `zcode`: all `Read` calls of the batch in one message. +- `opencode`: all `read` calls of the batch in one response. +- `codex`: one shell command that prints the batch: `cd && tail -n +1 a.md b.md c.md` (each file is preceded by a + `==> name <==` header). +- `omp`, `dsh`, `kimicode`: the generic sentence ("request the whole batch together if your tools allow it"). + +Pages that name API pages for a step list them as one batch too (`Read now` / a `Read when` row), never "read as needed". + +## Splitting and merging rules (no loss) + +- **Move, do not rewrite.** When a long page is split, each rule, table row, example, checklist item and `skill:if` block + survives in exactly one new page (or is deleted as a verbatim duplicate of text kept elsewhere, and the report says + where). Changing meaning is a separate, labelled edit. +- The old page's name stays as the outline (workflow or guide) so links keep working; the outline links to the new pages. +- Stable file names are in the file map below. Cross-links between owners only use the names listed there. + +## File map + +Names in **bold** are new. + +```text +SKILL.md +references/README.md generated tables + this layering +references/SDK_OVERVIEW.md, ql-playbook.md, scadpkg-format.md +**references/ql-recipes.md** the recipes split out of ql-playbook.md +references/workflows/ + single-part-modeling.md outline, Role 5 (export), failure routes + single-part-modeling/**role-1-requirements.md, requirements-template.md, role-2-plan.md, + role-3-verifier.md, verifier-contract-patterns.md, role-4-build.md** + assembly-product-build.md step-reconstruction.md engineering-drawing.md export-and-translation.md + **scan-reconstruction.md** a mesh, point cloud or images as the source + drawing-reconstruction.md outline + drawing-reconstruction/**(role / stage pages, the owner chooses the names)** +references/domains/ the 11 existing pages (drawing-inspection may gain a template page) +references/discipline/ + **core-rules.md** **operating-modes.md** feature-tree-convention.md notebook-cell-design.md feature-ordering.md + geometric-validation.md datums-and-coordinate-systems.md mechanical-modeling.md manufacturing-boundaries.md + assembly-positioning.md failure-and-repair.md requirement-and-cad-brief.md verifier-scripts.md +references/docs/guides/ + notebook-runtime.md run, parameters (<= 300 lines) + **notebook-composition-and-cache.md** composing notebooks, the cell cache, library parts + **product-export.md** package export, downstream targets +``` + +## Checks (`tools/skillbuild.py --check`, tests in `test/test_skill_lint.py`) + +1. every path a page names (backticked or in a link, under `references/`) exists; +2. `references/README.md` lists exactly the workflows, domains and discipline pages that exist; +3. every hand-written page is reachable from `SKILL.md` through links; +4. line limits (300 per page, 130 for `SKILL.md`); +5. every workflow has the three sections above and every domain/discipline page has `**Read when:**`; +6. the route table's read set equals the workflow's `Read now` set; +7. the read budget (1,200 compiled lines, see "Workflow pages") for each route and each target; +8. the four curated targets' compiled `SKILL.md` contains the parallel-read instruction; +9. no unresolved `skill:if` / `skill:endif`. + +## Changing the skill + +Run `python tools/skillbuild.py --check`. To see what a harness gets, `python tools/skillbuild.py --target opencode` +(output goes to `skills/simplecadapi-opencode/`, which git ignores). Behaviour is measured, not assumed: the +agents' traces (Langfuse) give the lines read before the first write and the rules broken; a change that adds reading +needs a reason in its commit message. diff --git a/docs/skill/SKILL.md b/docs/skill/SKILL.md index 93fe6d28..4708a6df 100644 --- a/docs/skill/SKILL.md +++ b/docs/skill/SKILL.md @@ -1,227 +1,101 @@ --- name: simplecadapi -description: Build, assemble, inspect, reconstruct, and export parametric CAD models with the SimpleCADAPI Python SDK. Use for SimpleCAD geometry modeling, constrained sketches, parts and assemblies, standard gears and bearings, STEP/BREP inspection and reconstruction, vector-PDF drawing reconstruction, durable product packages, model JSON replay, and CAD backend translation. +description: Build, assemble, inspect, reconstruct, and export parametric CAD models with the SimpleCADAPI Python SDK. Use for SimpleCAD geometry modeling, constrained sketches, parts and assemblies, standard gears and bearings, STEP/BREP inspection and reconstruction, mesh or image based reconstruction, vector-PDF drawing reconstruction, durable product packages, model JSON replay, and CAD backend translation. license: Apache-2.0 metadata: project: simplecadapi - version: 2.2.7 + version: 2.2.8 package-name: simplecadapi - package-version: 2.2.7 + package-version: 2.2.8 --- # SimpleCAD SDK Skill -This file is the router: it owns load order, route selection, and -execution discipline only. Each selected workflow owns its procedure. -Classify the request, load exactly one workflow, follow its domain and -discipline references, and read exact API pages for the APIs its steps -use. - -## Mandatory load order - -Paths are relative to this file's directory. - -1. Read this file. -2. Select exactly one workflow from Task routing. If two rows - plausibly match, ask one discriminator question that names the - candidate outcomes; never pick silently between routes with - different deliverables. -3. Read the selected workflow in full. From selection on, it owns the - procedure; do not load another workflow's procedure. -4. Load each required domain/discipline file the workflow names - before the step that uses it; optional ones only when their - trigger fires. -5. Before the first call of any API, read its exact page - (`references/docs/api/.md`, `references/docs/stdlib/.md`, - `references/docs/core/.md`). A method absent from its page - does not exist, however plausible it looks. - -- You MUST NOT read the full API or stdlib index up front; the workflow - names every page its steps need. -- Routing questions are decided here; after selection the workflow owns - execution. You MUST always follow the global rules and execution - discipline below. -- Questions about an existing STEP file that change nothing enter no - workflow; you MUST use `references/domains/step-inspection.md` for them - directly. -- A person who has the code-first editor open (`sca edit`) and talks - about what they selected, a cell they copied, or a value they - tuned enters no new workflow: the modeling follows the workflow you - selected, and you MUST read - `references/domains/editor-collaboration.md` first to see what they - see (selection cell, `sca snapshot`, cell references) and how to - hand the work back. While the editor runs, `sca cell` is the only way to - change a regular cell; edit the file only when no editor is running, or - for the setup / `@app.function` cells `sca cell` refuses: the editor is - the file's writer. -- Creating, installing, updating, or removing a SimpleCADAPI addon (a - third-party skill+tooling package around `.scadpkg`) enters no - workflow; you MUST use `references/domains/addon-development.md` - directly. - -## Task routing - -Read exactly one workflow first, per the user's goal: - -| User goal | Workflow | -| --- | --- | -| Model one physical part | `references/workflows/single-part-modeling.md` | -| Multi-part product or mechanism (custom parts, stdlib gears/bearings, connectors, constraints, package) | `references/workflows/assembly-product-build.md` | -| Rebuild an editable model from a STEP file | `references/workflows/step-reconstruction.md` | -| Export/translate a validated package | `references/workflows/export-and-translation.md` | -| Produce a GB engineering drawing (DXF) from validated geometry | `references/workflows/engineering-drawing.md` | -| Rebuild a 3D model from a vector-PDF engineering drawing | `references/workflows/drawing-reconstruction.md` | - -Do not present route-choice menus when a row already matches: profile, -strategy, and tool choices inside a route belong to that workflow's -own gates, not to an up-front question. A missing prerequisite — state -it and stop that route; never invent an alternative route. - -## Routing discipline - -| Rule | Behavior | -| --- | --- | -| One lifecycle per request | Every task enters exactly one workflow; domains, disciplines, and guides refine it and are never competing routes | -| Deterministic selection | When the matrix matches, proceed without asking a route question | -| Ambiguous request | Ask one discriminator question naming the candidate deliverables (editable model vs inspection evidence vs export package), then route | -| Missing prerequisite | Name what is missing and stop that route | -| Explicit user override | Honor it only when the target route's scope actually covers the request | - -## Global rules (every task) - -1. Refine the requirement into a brief before modeling - (`references/domains/requirement-refinement.md`). -2. Use keyword arguments for every documented public API and - stdlib function, except the canonical durable export call - `capture(result, path)`, whose two required arguments are - positional. -3. Every model is a marimo notebook with a `[tool.simplecadapi]` - header (`references/docs/guides/notebook-runtime.md`): one part - per notebook; one assembly notebook per product, composing - children with `scad.use(...)`; parameters live in the notebook - that consumes them; exposed tunable parameters are `var()`/`Var` - declarations (optionally with `unit`, `tolerance`). Part sources - follow the Feature Tree Convention - (`references/discipline/feature-tree-convention.md`): block - structure `sketch → basic body op → bool → modifier`, one - feature per block and one block per cell, with a mandatory - boundary comment `# ---- feature: () ----`; 2D - profiles and planar paths go through the sketch API, primitives - only when the shape is completely contained in the basic form. - Where the cell boundaries go and which numbers are a `var()` - follow `references/discipline/notebook-cell-design.md`: read it - with the convention before writing any notebook; it states each rule - with its reason and the error you get when you break it. -4. Booleans (`union_rsolid`, `cut_rsolid`, `cut_rsolid_list`, `intersect_rsolid`) - accept mixed inputs and return exactly one `Solid`; union - defaults to `glue=False` with a conservative scale-relative - tolerance and fails explicitly when it cannot produce one - merged solid. -5. Build and validate incrementally: each major step prints small - QL-derived facts; grounding uses QL wherever possible; never - print whole solids or full model objects. -6. Topology enumeration is QL-only and strictly enforced: plural - getters (`get_edges`, `get_faces`, `get_wires`, `get_vertices`, - `get_solids`, `get_inner_wires`) accept ONLY an index — the - no-argument list form raises. Enumerate, count, and measure via - `ql.().resolve(shape)`; filter with QL predicates plus - `take`/`exactly` cardinality; never build selections by looping - over an enumerated list and indexing it. Indexed picks - (`get_edges(index)`) are reserved for intentional, named choices - and are recorded as graph selection nodes. -7. Tags: attach with `apply_tag(shape=..., tag=...)` (LOCAL scope — - it never propagates downward); inspect with - `list_tags(shape=...)`; keep numeric facts in metadata, never in - tags. -8. The notebook file is the only source of truth; `.scadpkg` is - generated from it for exchange and publishing (`sca run - product.py --out out/product.scadpkg`, or - `capture(result, "out/product.scadpkg")` in one call) and is - never read back as a source. `@scad.part` / `@scad.assemble` - are for reusable library parts in plain modules; they may be - called inside a notebook cell. -9. `simplecadapi.inspect.brep` and `simplecadapi.inspect.drawing` are - diagnostic-only and rejected inside `GraphSession`; obtain/export - geometry first, inspect outside. -10. Standard parts first: before hand-modeling a gear, ring gear, - rack, cycloidal disc, or bearing, check `scad.std.gear` / - `scad.std.bearing`. -11. Read `references/docs/guides/notebook-runtime.md` in full - before writing a notebook, composing notebooks, or reasoning - about the cell cache. Run models with `sca run` (`sca edit` opens - them in the editor for a person); never enable - marimo's `[tool.marimo.runtime] cache_cells` in the editor, and - never commit the generated `__marimo__/` directory. -12. Manage the modeling project the way software is managed: put the - workspace under git from the first file, commit at verified - milestones (a passed verification, not a wall clock) with messages - that say what changed and why, and use a branch per design - direction when alternatives are on the table. Generated state — - `__marimo__/`, `.sca-editor/`, `out/` — belongs in `.gitignore`, - never in a commit. - -## Boundaries - -- `.scadpkg` is the canonical durable product; STL/OBJ/MJCF are - point-in-time exports, never editable sources. -- Model JSON is the replay/interchange contract for explicit - `GraphSession` flows; never hand-author payloads. -- No claims of strength, fatigue, thermal, vibration, tolerance - compliance, or regulatory fitness without the corresponding - analysis actually run. - -## Execution discipline (every task) - -1. Gates bind artifacts: a validation gate passes only when its named - artifact exists — printed QL facts, targeted measurements, named - rendered views, completed brief fields. A stated feeling of success - is not a gate result. -2. Blocking waits: an Ask-or-Record blocking item resolves before any - geometry — asked once (batched into one question set) or recorded - as an explicit assumption. Proceeding with neither is the failure, - not the asking. -3. Serial phases: do not preload later-phase pages or build - later-phase artifacts early. After an interruption, re-ground from - the owning artifact (source file, brief), not from conversation - memory. -4. Repair the owning source: the named parameter, the brief field, or - the upstream selection — never the downstream symptom. - Candidate x parameter enumeration loops are diagnosis debt, not - repair (`references/discipline/failure-and-repair.md`). -5. A failed check closes by changing the design or by redefining the - check with the user — never by weakening or deleting the check. - -## Example SDK usage - -```python -import simplecadapi as scad -from simplecadapi import GraphSession, export_model_json, replay_model_json - -with GraphSession(graph_id="box") as session: - shape = scad.make_box_rsolid(width=10.0, height=20.0, depth=30.0) - session.capture_result(value=shape) - payload = export_model_json(session=session) - -rebuilt = replay_model_json(json_str=payload) -print(len(rebuilt)) -``` - -## References - -- `references/README.md` — skill layer structure -- `references/workflows/` — goal-oriented workflows, including drawing reconstruction -- `references/domains/` — capability domains, including drawing inspection -- `references/discipline/` — modeling knowledge and invariants -- `references/discipline/verifier-scripts.md` — writing a verifier script (OCP through `simplecadapi.verify`) -- `references/SDK_OVERVIEW.md` — package-level map -- `references/inspect/brep-reverse-engineering.md` -- `references/domains/addon-development.md` — `sca` addon CLI and authoring guide -- `references/domains/editor-collaboration.md` — working with a person in `sca edit`: - selection cell, snapshot, cell references, `sca cell` / `sca new`, parameters -- `references/docs/guides/reconstruction-agent-test-prompt.md` -- `references/docs/guides/reconstruction-agent-strategy.md` -- `references/docs/guides/notebook-runtime.md` -- `references/ql-playbook.md` -- `references/scadpkg-format.md` — `.scadpkg` consumer spec: member - layout, tag channel, minimal readers (addon exporters read this) -- `references/docs/api/`, `references/docs/stdlib/`, `references/docs/core/` +This file is the router. Pick the route, read its batch, work. Paths are relative to this file's directory. + +## Start here + +1. **Decide the mode** (`references/discipline/operating-modes.md`): interactive (a person can answer) or unattended (nobody + will). Write `Mode: ...` as the first line of the brief. The mode changes who decides, never what is checked. +2. **Pick one route** from the table. Do not ask a route question when a row matches; if two rows plausibly match, ask one + question naming the candidate deliverables (unattended: take the row whose deliverable the request names, record the choice). +3. **Read the whole batch of that row at once**, plus the two always-read pages `references/discipline/core-rules.md` and + `references/discipline/operating-modes.md`. Then work; the workflow's `Read when` table says when to read anything else. +4. Before the first call of an API, read its page (`references/docs/api|stdlib|core/.md`) in the same batch as the + other pages of that step. A method absent from its page does not exist. Never read the whole index. + +## Reading in parallel + +Reading is cheap when it is one request and expensive when it is ten. Ask for a whole batch together. + + +Issue every `Read` call of the batch in one message, so the harness runs them together. Do not read a file, think, then +read the next. + + +Issue every `read` call of the batch in one response, so the harness runs them together. Do not read a file, think, then +read the next. + + +Print the whole batch with one shell command from this skill's directory, for example +`tail -n +1 references/discipline/core-rules.md references/discipline/operating-modes.md references/workflows/single-part-modeling.md` +(each file is preceded by a `==> name <==` line). One command, not one per file. + + +Request the whole batch together if your tools allow several reads in one step. + + +## Routes + +Each row's batch is the workflow page plus its `Read now` set (the workflow page repeats it). + +| You are asked to | Workflow | Read now | +| --- | --- | --- | +| Model one physical part | `references/workflows/single-part-modeling.md` | `references/workflows/single-part-modeling.md` `references/workflows/single-part-modeling/role-1-requirements.md` `references/workflows/single-part-modeling/requirements-template.md` `references/domains/requirement-refinement.md` `references/discipline/requirement-and-cad-brief.md` | +| Build a multi-part product or mechanism | `references/workflows/assembly-product-build.md` | `references/workflows/assembly-product-build.md` `references/domains/requirement-refinement.md` `references/domains/assembly-and-product.md` `references/discipline/assembly-positioning.md` | +| Rebuild an editable model from a STEP file | `references/workflows/step-reconstruction.md` | `references/workflows/step-reconstruction.md` `references/domains/step-inspection.md` `references/inspect/brep-reverse-engineering.md` | +| Rebuild a model from a mesh, point cloud or photos | `references/workflows/scan-reconstruction.md` | `references/workflows/scan-reconstruction.md` `references/domains/step-inspection.md` `references/domains/part-modeling.md` | +| Rebuild a model from a vector-PDF drawing | `references/workflows/drawing-reconstruction.md` | `references/workflows/drawing-reconstruction.md` `references/workflows/drawing-reconstruction/role-1-drawing-confirmer.md` `references/domains/drawing-inspection.md` `references/domains/requirement-refinement.md` | +| Produce a GB engineering drawing (DXF) from validated geometry | `references/workflows/engineering-drawing.md` | `references/workflows/engineering-drawing.md` `references/domains/engineering-drawing.md` `references/domains/requirement-refinement.md` | +| Export or translate a validated package | `references/workflows/export-and-translation.md` | `references/workflows/export-and-translation.md` `references/domains/export-and-translation.md` | + +Routes without a workflow (no lifecycle, read the page and answer): + +- A question about an existing STEP file that changes nothing: `references/domains/step-inspection.md`. +- A person has the editor open (`sca edit`) and talks about what they selected or tuned: keep the workflow you are in and + read `references/domains/editor-collaboration.md` first. While the editor runs, `sca cell` is the only way to change a + regular cell. +- Creating, installing, updating or removing an addon (`.scadpkg` skill + tooling): `references/domains/addon-development.md`. + +An explicit request for a route is honoured only when that workflow's scope covers it. +A missing prerequisite (a file, a tool, an input): name it and stop that route; never invent another route. Unattended: record +it as a blocker in the report and do the strongest supported work. + +## Always-on rules + +`references/discipline/core-rules.md` has the full text with reasons; the IDs below are what reviews cite. + +- **R2** keyword arguments for every documented API (except `capture(result, path)`). +- **R3** a model is a marimo notebook; cell boundaries and `var()` follow `references/discipline/notebook-cell-design.md`, + blocks follow `references/discipline/feature-tree-convention.md`. Read both before writing a cell. +- **R6** topology is enumerated through QL only; plural getters take only an index (`references/ql-playbook.md`). +- **R13** an API not on its page does not exist. +- **P1** gates bind artifacts, not feelings. **P2** resolve blocking items before geometry (ask once, or record). + **P5** repair the owner, not the symptom. **P6** a failed check closes by changing the design or redefining the check in + the open, never by weakening it. +- No claim of strength, fatigue, thermal, vibration, tolerance or regulatory fitness without the analysis run. + +## Where things are + +- `references/README.md`: every page by layer (workflows, domains, discipline, guides, generated API docs). +- `references/SDK_OVERVIEW.md`: package map. `references/ql-playbook.md` and `references/ql-recipes.md`: selection. +- `references/scadpkg-format.md`: the `.scadpkg` consumer spec. +- `references/docs/api/`, `references/docs/stdlib/`, `references/docs/core/`: exact signatures. `sca ops [NAME] [--json]` + lists every operation with what each parameter means (`length`, `angle`, `point3`, `direction3`, `color`, ... read + from the signature types in `simplecadapi.types`; `simplecadapi.catalog` in code). +- Sketches and features off the base planes: a recorded workplane (`make_workplane_from_face_rworkplane`, `..._from_points_`, + `..._from_axis_`, `..._midplane_`, `..._normal_to_curve_`), then `make_sketch_rsketch(plane=wp)` or `with wp:`. See + `references/discipline/datums-and-coordinate-systems.md`. +- Colours and names for people: a body's colour, opacity and name are `set_body_appearance_rpart`; a face or edge name is + a tag (`apply_tag_rselection`, `ql.tag`). See "Colours and names for people" in `references/domains/part-modeling.md`. +- Maintainers: the contract these pages follow is `skill-design.md` in the repository's `docs/` folder. diff --git a/docs/skill/references/README.md b/docs/skill/references/README.md index b86118ab..58bd4efe 100644 --- a/docs/skill/references/README.md +++ b/docs/skill/references/README.md @@ -1,95 +1,112 @@ # Skill Documentation Structure -This directory is the task-routed skill layer. It sits between the SDK's -generated +The skill is task-routed: `SKILL.md` (one level up) is the router, and everything here is loaded in batches the router and the +workflows name. The contract the pages follow (sizes, headers, read sets, unattended mode, checks) is `skill-design.md` +in the repository's `docs/` folder; `python tools/skillbuild.py --check` enforces it. ```text -docs/skill/ - domains/ stable, reusable capability modules - workflows/ goal-oriented compositions of domains - discipline/ mechanical modeling knowledge and invariants +SKILL.md router: mode, routes with read sets, always-on rules +references/discipline/core-rules.md, operating-modes.md read on every route +references/workflows/.md one lifecycle: Read now / Read when / Unattended mode +references/workflows// the role or stage pages of a long workflow +references/domains/.md what the SDK offers for a kind of task +references/discipline/.md reasoning, invariants, rules +references/docs/guides/ how the runtime works +references/docs/api|stdlib|core/ exact signatures (generated; never edited by hand) ``` -## Layering +Load top-down and only what the task triggers; read each batch in one parallel request. -```text -SKILL.md (router: load order, routing, execution discipline) -→ workflows/.md what to run, in what order -→ domains/.md capability boundaries and API groups -→ discipline/.md why: modeling reasoning and invariants -→ docs/api|stdlib|core/ exact signatures (auto-generated) -``` +## Workflows -Load top-down and only what the current task triggers. No task reads the -whole API index up front. +| Workflow | Scenario | +| --- | --- | +| `workflows/single-part-modeling.md` | one physical part from brief to validated solid (outline: roles, role switches, export, failure routes) | +| `workflows/assembly-product-build.md` | multi-part product or mechanism: connectors, constraints, package capture | +| `workflows/step-reconstruction.md` | editable rebuild from a target STEP with tiered acceptance | +| `workflows/scan-reconstruction.md` | rebuild from a mesh, point cloud or photos: what the SDK supports and what it does not | +| `workflows/drawing-reconstruction.md` | editable rebuild from vector-PDF dimensions (outline; stage pages in `workflows/drawing-reconstruction/`) | +| `workflows/engineering-drawing.md` | GB engineering drawing (DXF) from validated geometry | +| `workflows/export-and-translation.md` | downstream formats from a validated package | -## Domains +Stage and role pages of the long workflows: -| Domain | Owns | +| Page | Read when | | --- | --- | -| `requirement-refinement` | request → brief (dimensions, units, datums, validation targets) | -| `part-modeling` | single-solid parts: primitives, profiles, features, booleans, transforms | -| `sketch-and-features` | declarative constrained sketches and promotion | -| `assembly-and-product` | parts, placements, connectors, constraints, notebooks and `scad.use`, @part/@assemble, `.scadpkg` | -| `standard-parts` | stdlib gears, ring gears, racks, cycloidal discs, bearings | -| `step-inspection` | STEP/BREP evidence, comparison, reconstruction evaluation | -| `drawing-inspection` | vector-PDF coordinates and dimensions, model-section measurements, independent evidence validation | -| `export-and-translation` | STEP/STL/OBJ/MJCF export, FreeCAD/Fusion/SolidWorks translation | -| `addon-development` | third-party addon authoring and management: `sca` CLI, `sca-addon.toml` descriptor, runtime probes, addon SKILL.md requirements | +| `workflows/single-part-modeling/requirements-template.md` | Single-Part Modeling: REQUIREMENTS.md template | +| `workflows/single-part-modeling/role-1-ask-bindings.md` | Role 1 (interactive mode) is about to send its batched ask round. | +| `workflows/single-part-modeling/role-1-requirements.md` | Single-Part Modeling: Role 1, Requirement Confirmer | +| `workflows/single-part-modeling/role-2-plan.md` | Single-Part Modeling: Role 2, Master Planner | +| `workflows/single-part-modeling/role-3-host-bindings.md` | Role 3 starts a stage (the hypothesis loop and the TODO state machine for the Verifier Planner). | +| `workflows/single-part-modeling/role-3-verifier.md` | Single-Part Modeling: Role 3, Verifier Planner | +| `workflows/single-part-modeling/role-4-build.md` | Single-Part Modeling: Role 4, Detail Modeling Planner & Builder | +| `workflows/single-part-modeling/verifier-contract-patterns.md` | Role 3 selects the checks for a stage. | +| `workflows/drawing-reconstruction/role-1-drawing-confirmer.md` | Drawing Reconstruction: Role 1, Drawing Requirement Confirmer | +| `workflows/drawing-reconstruction/role-2-master-planner.md` | Drawing Reconstruction: Role 2, Master Planner | +| `workflows/drawing-reconstruction/role-3-verifier-planner.md` | Drawing Reconstruction: Role 3, Verifier Planner | +| `workflows/drawing-reconstruction/role-4-detail-builder.md` | Drawing Reconstruction: Role 4, Detail Modeling Planner & Builder | +| `workflows/drawing-reconstruction/role-5-exporter.md` | Drawing Reconstruction: Role 5, Exporter | +| `workflows/drawing-reconstruction/roundtrip-acceptance.md` | Drawing Reconstruction: Roundtrip Acceptance | -Cross-cutting capabilities — QL grounding, semantic tags, GraphSession replay, -units/tolerances, incremental validation — are not routing targets; each -workflow pulls them in as conditions require. Verified QL selection patterns -for operation outputs (extrude-family slots, boolean seam curves, seam -fillets) live in `ql-playbook.md`. The `.scadpkg` consumer-facing format -spec (member layout, `interface.*` tag channel, minimal readers for addon -exporters) lives in `scadpkg-format.md`. - -## Workflows +## Domains -| Workflow | Scenario | +| Page | Read when | | --- | --- | -| `single-part-modeling` | one physical part from brief to validated solid — sketch tier by default, std parts first | -| `assembly-product-build` | multi-part product or mechanism with connectors/constraints and package capture; stdlib gears/bearings live here | -| `step-reconstruction` | editable rebuild from a target STEP with tiered acceptance | -| `drawing-reconstruction` | editable rebuild from vector-PDF dimensions with stage checks and independent drawing/model/output acceptance | -| `export-and-translation` | downstream formats from a validated package | - -Each workflow states: goal/scope, task decomposition (required/optional -domains, artifacts, validation gates, repair routes), ordered steps with the -domain/discipline/API references to load, failure routes, and deliverables. -`single-part-modeling` is additionally **role-structured**: five roles -(requirement confirmer → master planner → verifier planner ⇄ detail modeling -planner & builder → exporter), with Roles 3 and 4 looping per stage before -the next stage is allowed. Each role owns a disk artifact and an exit gate; -per-harness host bindings (interactive ask, TODO/plan tool, REPL-or-script -hypothesis loop, mandatory isolated image review) are declared inside the -workflow file as conditional blocks compiled per target by -`tools/skillbuild.py` (targets declared in `skillproj.toml`). +| `domains/addon-development.md` | you create, publish, install, update or remove a SimpleCADAPI addon (`sca-addon.toml`), or decide whether a capability is an addon or SDK work. | +| `domains/assembly-and-product.md` | the deliverable has more than one physical part, repeated instances, nested subassemblies, motion semantics, or a `.scadpkg` package (details under "Use when"). | +| `domains/curve-surface-editing.md` | an existing Edge or Face must be reshaped by its control points, weights, knots or degree, refitted, or sampled for a curvature comb / isocurves. | +| `domains/drawing-inspection-evidence.md` | a section view of the drawing must be verified against the rebuilt model, a DIM ledger row needs its `measurement_contract`, or the roundtrip evidence (annotations, reviews, coverage) is built or checked. | +| `domains/drawing-inspection.md` | a vector-PDF engineering drawing must be parsed into tiered evidence, or a rebuilt model is read back for the roundtrip. | +| `domains/editor-collaboration.md` | a person has the code-first editor open (`sca edit`) and you edit the same notebooks (a selection, a cell reference or a tuned value arrives). | +| `domains/editor-gui-features.md` | a person made features with the editor's buttons (`# gui:` cells, `scad.ref` selections, workplanes, canvas sketches), or you run the same operations with `sca feature` / `sca sketch`. | +| `domains/engineering-drawing.md` | a validated part or product needs a GB-standard drawing sheet (DXF + PNG). | +| `domains/export-and-translation.md` | a validated `.scadpkg` must be delivered as STEP, STL, OBJ, MJCF or an editable external CAD document. | +| `domains/part-modeling.md` | the deliverable is one physical single-solid part, or an existing part source needs a geometry edit (Role 4 of `workflows/single-part-modeling.md`). | +| `domains/requirement-refinement.md` | every task starts here: any modeling, assembly, reconstruction, or export request is refined into a brief first, and whenever inputs conflict, are ambiguous, or mix modalities. | +| `domains/sketch-and-features.md` | the part's controlling dimensions are geometric constraints, or an editable, constraint-driven profile is requested (sketch tier profiles and paths). | +| `domains/standard-parts.md` | the request names gears, ring gears, racks, cycloidal discs or ball bearings, or a mechanism built from them (details under "Use when"). | +| `domains/step-inspection.md` | the input is an existing `.step`/`.stp` file and you need facts out of it, or a reconstruction candidate must be compared against a target STEP (details under "Use when"). | ## Discipline -Modeling reasoning and invariants, independent of any single API: -`mechanical-modeling` (construction strategy), `requirement-and-cad-brief` -(input precedence, drawings), `datums-and-coordinate-systems`, -`feature-ordering` (operation order and boolean tool geometry), -`assembly-positioning` (mating/motion intent), `geometric-validation` -(validity vs volume, incremental grounding, spec-driven measurement), -`failure-and-repair` (failure classes and the repair loop), -`manufacturing-boundaries` (defaults and what geometry never proves). +| Page | Read when | +| --- | --- | +| `discipline/assembly-positioning.md` | you choose frames, connectors, placements or constraints for mating and motion, or placement comes out wrong after solving. | +| `discipline/core-rules.md` | every task, in the first batch (the router lists it). These are the rules that hold in every workflow and both modes. Each has an ID so other pages and reviews can cite it (`core-rules R6`). | +| `discipline/datums-and-coordinate-systems.md` | before the first primitive of a part or assembly, when choosing an origin, base plane or up axis, or when placing imported or factory geometry. | +| `discipline/failure-and-repair.md` | a build, solve, export or validation step fails, or the same check has failed twice. | +| `discipline/feature-ordering.md` | you plan the order of operations of a part, a fillet or chamfer fails, or you pattern a feature or loft sections. | +| `discipline/feature-tree-convention.md` | you author or edit any part source (every workflow): what a feature block is, its header and name. | +| `discipline/geometric-validation.md` | you write or run checks on a model: validity and volume gates, selection evidence before a fillet or chamfer, spec-driven measurements, replay, or what to report as having run. | +| `discipline/manufacturing-boundaries.md` | you apply default dimensions, or the request touches manufacturability, tolerance, strength or process claims. | +| `discipline/mechanical-modeling.md` | you choose a construction strategy or decompose a mechanical part or product before selecting SimpleCADAPI operations. | +| `discipline/notebook-cell-design.md` | Discipline: Notebook Cell Design | +| `discipline/operating-modes.md` | at the start of every task (the router sends you here), and again whenever a gate says to ask or to wait for a person. | +| `discipline/requirement-and-cad-brief.md` | Role 1 (or any brief-writing step) starts: prose, images or drawings are being turned into a brief, or evidence conflicts. | +| `discipline/verifier-scripts.md` | Role 3 writes the executable check for a stage, or any task needs a script that measures a finished model against the specification. | + +## Guides and references + +| Page | What | +| --- | --- | +| `docs/guides/README.md` | Engineering Guides | +| `docs/guides/notebook-composition-and-cache.md` | Notebook Composition and the Cell Cache | +| `docs/guides/notebook-runtime.md` | Notebook Runtime and Product Build Workflow | +| `docs/guides/product-export.md` | Product Package Export and Downstream Targets | +| `docs/guides/reconstruction-agent-strategy.md` | Reconstruction Agent Strategy | +| `docs/guides/reconstruction-agent-test-prompt.md` | Reconstruction Agent Test Contract | +| `ql-playbook.md` | how QL selection works, with the rules | +| `ql-recipes.md` | worked selection recipes | +| `scadpkg-format.md` | the `.scadpkg` consumer spec | +| `SDK_OVERVIEW.md` | package map | +| `inspect/brep-reverse-engineering.md` | normative BREP inspection guide | ## Authoring rules -- Domain files say what a capability owns, when to use it, when to route - elsewhere, and which API pages to read — they do not restate signatures. -- Workflow files compose domains; they never duplicate discipline content. -- Discipline files state reasoning and invariants; they avoid CLI- or - tool-specific detail that belongs in domains/workflows. -- Every `references/...` path written here must exist in the packaged skill. -- Reading an API page is an action with an output: before the first - call of any API, note its signature, return type, and documented - failure modes — one line each in the working notes. A method name - absent from the page does not exist, however plausible it looks. - Non-SDK dependencies (Gmsh, CalculiX, VTK, FreeCAD scripting) are not - covered by SDK pages: check their own documentation before the first - call. +- A page has one job and at most 300 lines. A rule is stated once, with its reason; other pages link to it. +- Domain pages say what a capability owns and which API pages to read; they do not restate signatures. +- Workflow pages compose domains and discipline; they never duplicate discipline content. +- Every workflow has `Read now`, `Read when` and `Unattended mode`, with one unattended row for every gate that asks a person. +- Every path written in a page must exist in the packaged skill. +- Reading an API page is an action with an output: note the signature, return type and documented failures before the + first call. Non-SDK dependencies (Gmsh, CalculiX, VTK, FreeCAD scripting) are not covered by SDK pages. diff --git a/docs/skill/references/SDK_OVERVIEW.md b/docs/skill/references/SDK_OVERVIEW.md index 5024cb4a..b9eb6971 100644 --- a/docs/skill/references/SDK_OVERVIEW.md +++ b/docs/skill/references/SDK_OVERVIEW.md @@ -1,12 +1,12 @@ # SDK Overview - Project: `simplecadapi` -- Version: `2.0.4b3` -- Package distribution: `simplecadapi==2.0.4b3` +- Version: `2.2.8` +- Package distribution: `simplecadapi==2.2.8` ## What this skill bundles -- Task router (`SKILL.md`) with six workflows +- Task router (`SKILL.md`) with seven workflows (including unattended mode) - Capability domains (`references/domains/`) - Modeling discipline (`references/discipline/`) - Generated API/stdlib/core references (`references/docs/`) @@ -31,3 +31,18 @@ - Record modeling steps inside `GraphSession` when you need replayable outputs. - Export session/model payloads with `export_session_json()` and `export_model_json()`. - Re-import or replay with `import_model_json()` and `replay_model_json()`. + +## Example SDK usage + +```python +import simplecadapi as scad +from simplecadapi import GraphSession, export_model_json, replay_model_json + +with GraphSession(graph_id="box") as session: + shape = scad.make_box_rsolid(width=10.0, height=20.0, depth=30.0) + session.capture_result(value=shape) + payload = export_model_json(session=session) + +rebuilt = replay_model_json(json_str=payload) +print(len(rebuilt)) +``` diff --git a/docs/skill/references/discipline/assembly-positioning.md b/docs/skill/references/discipline/assembly-positioning.md index b708666e..b1f82836 100644 --- a/docs/skill/references/discipline/assembly-positioning.md +++ b/docs/skill/references/discipline/assembly-positioning.md @@ -1,5 +1,7 @@ # Discipline: Assembly Positioning +**Read when:** you choose frames, connectors, placements or constraints for mating and motion, or placement comes out wrong after solving. + How SimpleCADAPI represents mating and motion intent, plus engineering guidance for choosing frames and placements. diff --git a/docs/skill/references/discipline/core-rules.md b/docs/skill/references/discipline/core-rules.md new file mode 100644 index 00000000..d30545d0 --- /dev/null +++ b/docs/skill/references/discipline/core-rules.md @@ -0,0 +1,81 @@ +# Discipline: Core Rules + +**Read when:** every task, in the first batch (the router lists it). These are the rules that hold in every workflow and +both modes. Each has an ID so other pages and reviews can cite it (`core-rules R6`). + +## Modelling rules + +**R1. Refine before you model.** Turn the request into a brief first (`domains/requirement-refinement.md`). Geometry +before a brief is rework. + +**R2. Keyword arguments.** Every documented public API and stdlib function takes keyword arguments. The one exception is +the durable export call `capture(result, path)`, whose two required arguments are positional. + +**R3. A model is a marimo notebook** with a `[tool.simplecadapi]` header (`docs/guides/notebook-runtime.md`): one part per +notebook; one assembly notebook per product, composing children with `scad.use(...)`; parameters live in the notebook that +consumes them. Cell boundaries, `_private` names and `var()` follow `discipline/notebook-cell-design.md`; block structure +and the mandatory header `# ---- feature: () ----` follow `discipline/feature-tree-convention.md`. Read both +before writing any cell. + +**R4. Sketch first.** 2D profiles and planar paths go through the sketch API; primitives only when the shape is entirely +the basic form. + +**R5. Booleans** (`union_rsolid`, `cut_rsolid`, `cut_rsolid_list`, `intersect_rsolid`) accept mixed inputs. Union and +intersect return exactly one `Solid`; `cut_rsolid` returns one `Solid`, or the pieces (`list[Solid]`, centroid order) +when the cut splits the body (`on_split="error"` refuses instead); `cut_rsolid_list` always returns a list. Union defaults to `glue=False` with a conservative scale-relative tolerance and fails explicitly when +it cannot produce one merged solid. + +**R6. Topology is enumerated through QL only.** Plural getters (`get_edges`, `get_faces`, `get_wires`, `get_vertices`, +`get_solids`, `get_inner_wires`) accept only an index; the no-argument list form raises. Enumerate, count and measure via +`ql.().resolve(shape)`; filter with QL predicates and `take`/`exactly`; never build a selection by looping over a +list and indexing it. An indexed pick is for an intentional, named choice. Details: `ql-playbook.md`. + +**R7. Tags.** `apply_tag(shape=..., tag=...)` is local: it never propagates downward. `list_tags(shape=...)` inspects. +Numeric facts go in metadata, never in tags. + +**R8. The notebook file is the only source.** `.scadpkg` is generated from it (`sca run product.py --out +out/product.scadpkg`, or `capture(result, "out/product.scadpkg")`) and is never read back as a source. `@scad.part` / +`@scad.assemble` are for reusable library parts in plain modules. + +**R9. Inspection is diagnostic.** `simplecadapi.inspect.brep` and `simplecadapi.inspect.drawing` are rejected inside +`GraphSession`; produce or export the geometry first, inspect outside. + +**R10. Standard parts first.** Before hand-modelling a gear, ring gear, rack, cycloidal disc or bearing, check +`scad.std.gear` / `scad.std.bearing`. + +**R11. Run with `sca run`;** `sca edit` opens the editor for a person. Never enable marimo's +`[tool.marimo.runtime] cache_cells`, never commit `__marimo__/`. + +**R12. Manage the project like software.** Put the workspace under git from the first file; commit at verified milestones +(a passed verification, not a clock) with messages that say what changed and why; use a branch per design direction when +alternatives are on the table. `__marimo__/`, `.sca-editor/` and `out/` go in `.gitignore`. + +**R13. An API that is not on its page does not exist.** Before the first call of any API read its exact page +(`docs/api/.md`, `docs/stdlib/.md`, `docs/core/.md`) in one batch with the others for that step. Never +read the whole index. + +## Process rules + +**P1. Gates bind artifacts.** A gate passes only when its named artifact exists: printed QL facts, targeted measurements, +named rendered views, completed brief fields. A feeling of success is not a result. + +**P2. Resolve a blocking item before any geometry.** Ask once, batched (interactive), or record it as an assumption with +its evidence (unattended; `discipline/operating-modes.md`). Proceeding with neither is the failure, not the asking. + +**P3. Serial phases.** Do not build later-phase artifacts early. After an interruption, re-ground from the owning +artifact (source file, brief), not from conversation memory. + +**P4. Build and check in small steps.** Each major step prints small QL-derived facts; never print whole solids or model +objects. + +**P5. Repair the owner.** Fix the named parameter, the brief field or the upstream selection, never the downstream +symptom. Candidate-by-parameter enumeration loops are diagnosis debt (`discipline/failure-and-repair.md`). + +**P6. A failed check closes by changing the design, or by redefining the check in the open** (with the person, or in the +report when unattended), never by weakening or deleting it. + +## Boundaries + +- `.scadpkg` is the durable product; STL/OBJ/MJCF are point-in-time exports, never sources. +- Model JSON is the replay contract for `GraphSession` flows; never hand-author payloads. +- No claim of strength, fatigue, thermal, vibration, tolerance or regulatory fitness without the analysis run. diff --git a/docs/skill/references/discipline/datums-and-coordinate-systems.md b/docs/skill/references/discipline/datums-and-coordinate-systems.md index 9b221dcb..c40c900a 100644 --- a/docs/skill/references/discipline/datums-and-coordinate-systems.md +++ b/docs/skill/references/discipline/datums-and-coordinate-systems.md @@ -3,6 +3,9 @@ Choosing origins, planes, and axes from function before the first primitive. The coordinate and unit statements below are SDK facts where labeled; origin choices and part-type defaults are modeling guidance. + +**Read when:** before the first primitive of a part or assembly, when choosing an origin, base plane or up axis, or when placing imported or factory geometry. + ## Core rule The part-local coordinate system is chosen from the functional datum, and the @@ -40,6 +43,26 @@ Datum features: mating faces, screw axes, centerlines, locating features `CoordinateSystem` exist for local contexts; use them when a feature is naturally authored in a local frame, and say so in the source. +## Workplanes from the model (SDK facts) + +A datum that comes from geometry is a recorded workplane, not copied numbers: +`make_workplane_from_face_rworkplane` (a planar face, optional `offset`, `x_edge`, +`flip`), `make_workplane_from_points_rworkplane` (three vertices or points), +`make_workplane_from_axis_rworkplane` (an edge or a hole's axis, turned by `angle`), +`make_workplane_midplane_rworkplane` (two parallel faces: a symmetry plane) and +`make_workplane_normal_to_curve_rworkplane` (a sweep profile's plane at a fraction of +a path). See `references/docs/core/simple_workplane.md`. + +- Give the workplane its own name in its own cell (`wp_top = ...`), then sketch on it + (`make_sketch_rsketch(..., plane=wp_top)`) or model in it (`with wp_top:`). A later + cell may enter it again; it is cached with its cell. +- A sketch or feature on a workplane made from a face follows the face when the + model changes (the notebook re-runs it; a model-JSON replay rebuilds the frame + from the recorded op). Copying `wp.origin` / `wp.normal` into literals breaks that + link: use the workplane value. +- Name the datum in the brief like any other: "boss sketch on `wp_top` (top face of + base), x along the long edge". + ## Placement semantics - Part-local coordinates equal the wrapped solid's modeling coordinates. If a diff --git a/docs/skill/references/discipline/failure-and-repair.md b/docs/skill/references/discipline/failure-and-repair.md index fd297636..015e68bd 100644 --- a/docs/skill/references/discipline/failure-and-repair.md +++ b/docs/skill/references/discipline/failure-and-repair.md @@ -3,6 +3,8 @@ The loop for generation, solve, export, or validation failures. API behavior is marked as contract; diagnosis heuristics are marked as modeling guidance. +**Read when:** a build, solve, export or validation step fails, or the same check has failed twice. + ## Evidence labels - **SDK contract** is limited to behavior exposed by the public API or reports. @@ -22,6 +24,17 @@ marked as contract; diagnosis heuristics are marked as modeling guidance. Never suppress the symptom: no exception swallowing, no special-cased input, no patching exported or solved artifacts. +## Stopping rule + +If the same check fails twice for the same cause, do not retry the same thing a third time. Change the design (the +operation order, the selection, the feature hypothesis, the parameter that was guessed), or redefine the check **in the +open**: state the old definition, the new one and the reason (in the brief, the plan or the report). A check is never +weakened or deleted to make it pass. Count by cause, not by wording: two different exceptions from one tangent +contact are one failure. If the new design fails the same check again, stop and report what failed, what was tried and +which claims cannot be made (see "Reporting failed repairs"). Interactive: the redefinition is agreed with the person. +**Unattended:** decide it yourself from the evidence and record it in the report +(`discipline/operating-modes.md`: Stopping). + ## Failure classes ### Boolean failure @@ -34,9 +47,10 @@ no patching exported or solved artifacts. - Isolate and simplify the failing base/tool pair. - `cut_rsolid(..., skip_non_intersecting=False)` is the strict diagnostic mode; with the default `True`, non-intersecting tools may be ignored. -- `cut_rsolid` expected a single Solid but the kernel returned several: the cut - split the body. If that is intended, call `cut_rsolid_list`; if not, the tool - crosses the body completely or touches a wall tangentially — resize or +- `cut_rsolid` returned a list instead of a Solid: the cut split the body + (with `on_split="error"` it raises instead). If that is intended, pass the + pieces on as bodies (`make_part_rpart(part_id=..., body=pieces)`); if not, the + tool crosses the body completely or touches a wall tangentially — resize or reposition it. - After repeated failure, reconsider operation order — not just retry the same cut harder. @@ -46,7 +60,9 @@ no patching exported or solved artifacts. `union_rsolid` requires a one-manifold-`Solid` result and fails if it cannot produce one. If the design is one body, use a contact or overlap that is topologically capable of producing one solid, then recompute. If the design is -actually multiple parts, the answer is an assembly, not a union. +actually multiple parts, the answer is an assembly, not a union; if it is one +part whose bodies stay apart, make a multi-body part +(`make_part_rpart(bodies=...)`). ### Fillet/chamfer failure @@ -159,6 +175,6 @@ A loop over candidate sets x parameter values (radii, zoom levels, being guessed that should be derived. Stop enumerating and derive it: radii from wall thickness or feature size, framing from the bounding box, cardinality from feature intent. If a value truly cannot be -derived, the check's definition is wrong — narrow it or ask. Swallowing +derived, the check's definition is wrong — narrow it or ask (unattended: narrow it and record the new definition in the report). Swallowing exceptions inside an enumeration loop (`except: pass`) converts a search into silent damage. diff --git a/docs/skill/references/discipline/feature-ordering.md b/docs/skill/references/discipline/feature-ordering.md index 16171928..bcd0419f 100644 --- a/docs/skill/references/discipline/feature-ordering.md +++ b/docs/skill/references/discipline/feature-ordering.md @@ -3,6 +3,8 @@ Operation order is a robustness recommendation, not an SDK-enforced canonical sequence. Put fragile steps late when that improves diagnosis and repair. +**Read when:** you plan the order of operations of a part, a fillet or chamfer fails, or you pattern a feature or loft sections. + ## Evidence labels - **SDK contract:** documented operation results and failure behavior. diff --git a/docs/skill/references/discipline/feature-tree-convention.md b/docs/skill/references/discipline/feature-tree-convention.md index 3c41c109..85d000a7 100644 --- a/docs/skill/references/discipline/feature-tree-convention.md +++ b/docs/skill/references/discipline/feature-tree-convention.md @@ -1,5 +1,7 @@ # Discipline: Feature Tree Convention (FTC) +**Read when:** you author or edit any part source (every workflow): what a feature block is, its header and name. + Part sources are block-structured feature chains: every block is one feature in the sense of a commercial CAD feature tree (Onshape / SolidWorks / Fusion 360), readable by humans and LLMs, and translatable by the package @@ -7,9 +9,8 @@ translators. This convention is mandatory for every part source authored in any workflow. A model is a marimo notebook (`docs/guides/notebook-runtime.md`), and each -block is one cell of it: the cell is the unit the runtime caches and re-runs, -so one feature per cell makes an edit re-run that feature and its downstream -features only. +block is one cell of it. Why the cell is the unit, and where cell boundaries go, +is in `notebook-cell-design.md` (rule C1 and the facts it rests on). ## The paradigm @@ -113,36 +114,22 @@ design parameters) stay in the source; geometric verification never does ## Blocks are cells -In a notebook every block is one `@app.cell`. This section is the structure; -`notebook-cell-design.md` gives the rules for where boundaries go, when a number -is a `Var`, and the reason for each (read it too): +In a notebook every block is one `@app.cell`. This section owns what a block +looks like inside its cell (header and name); `notebook-cell-design.md` owns the +rest, each rule with its reason: one feature per cell (C1) and when cells may +merge (C4), `_private` temporaries (C5), the parameters cell (C8), the product +cell (C9), local-module imports (C12). Read it too. - The block header is the first line of the cell body. - The cell returns the body under the block's slug in snake_case (`base-plate` → `base_plate`), and the next block reads that name. The variable names then spell the feature tree, and an override - (`sca run --set`) or a failure points at one feature. -- Each block binds a new name because marimo gives every top-level - variable exactly one defining cell: that is how it orders cells by data - dependency and keys each cell's cache by the values it reads. Rebinding - `body` in a second cell is a multiple-definition error. -- Tools, profiles, and other temporaries are cell-local: prefix them with - `_` (`_pockets = ...`) so marimo keeps them out of the notebook's - namespace. -- Several small blocks that always change together may share one cell, - rebinding the body inside it; the cell returns the body under the last - block's slug. They are then cached and re-run as one unit, so keep this - the exception. -- Parameters live in their own cell(s) at the top (`# ---- params ----`), - grouped by what is overridden together: an override replaces every - variable of its cell. A trailing `# ui: slider(min, max, step=, unit=, - label=)` comment (or `number(...)`, `toggle()`) on a parameter's line - chooses the control an editor offers; see the notebook runtime guide. The product — the `Part` or `Assembly` whose id is - the notebook id — is made in the last cell. A notebook that describes a part family names the - product `scad.notebook_id()`, and each use gives the member its id + (`sca run --set`) or a failure points at one feature. (Each block binds a new + name because a notebook name has one defining cell: `notebook-cell-design.md`, + fact 3.) +- A notebook that describes a part family names the product + `scad.notebook_id()`, and each use gives the member its id (`scad.use("link_bar.py", id="crank", ...)`). -- Imports from local modules go in the cell that uses them, or in the setup - cell; either way, editing the module invalidates the notebook's cache. ## Library parts: the `@scad.part` decorator @@ -162,8 +149,8 @@ the source itself: - `tolerance_profile` — kernel tolerance fingerprint (default `'simplecad-default'`). -A builder never caches; each call builds afresh. Inside a notebook the -calling cell is cached, and editing the module invalidates it. +A builder never caches; each call builds afresh. How the calling cell is cached +is in `docs/guides/notebook-composition-and-cache.md`. **Delivered sources state the anchor explicitly.** A translator-emitted or standalone `.ftc.py` passes `project_root=Path(__file__).parent` diff --git a/docs/skill/references/discipline/geometric-validation.md b/docs/skill/references/discipline/geometric-validation.md index e9724db3..aaf69e5b 100644 --- a/docs/skill/references/discipline/geometric-validation.md +++ b/docs/skill/references/discipline/geometric-validation.md @@ -3,6 +3,8 @@ Deterministic checks decide pass/fail; scale them to the specification. Every user-specified dimension requires a targeted measurement. +**Read when:** you write or run checks on a model: validity and volume gates, selection evidence before a fillet or chamfer, spec-driven measurements, replay, or what to report as having run. + ## Evidence labels - **SDK contract:** `Solid.get_volume()` returns a volume measurement; @@ -51,8 +53,10 @@ review. picks are preserved as graph geo-select nodes. - `apply_tag(shape=..., tag=...)` attaches a tag to exactly the given shape with LOCAL scope — it never propagates downward; `list_tags(shape=...)` - reads effective tags. Downward/topology-scoped propagation is only exposed - by `apply_tag_rselection` with an explicit propagation policy. Keep numeric + reads effective tags, which include the tags the recorded history carries + to the entity (a tagged face stays tagged through later booleans, blends + and copies). Downward/topology-scoped propagation is only exposed by + `apply_tag_rselection` with an explicit propagation policy. Keep numeric facts in metadata, never in tags. - A QL-selected face or edge reused by a later feature is preserved as a stable geo-select node in replayable graphs. @@ -133,6 +137,8 @@ check. 3. A failed check closes by changing the design, or by changing the check's definition with the user — never by weakening the check or deleting the interfering material out of the verification body. + **Unattended:** redefine the check yourself, in the open: write the old and the new definition and why in the + report (`discipline/operating-modes.md`), and never weaken it silently. 4. Convert review feedback into a predicate before implementing it: "it occludes the hole in the render" -> does measured material intersect the hole cylinder? Run the cheap experiment that diff --git a/docs/skill/references/discipline/manufacturing-boundaries.md b/docs/skill/references/discipline/manufacturing-boundaries.md index 0dda96f0..f187c789 100644 --- a/docs/skill/references/discipline/manufacturing-boundaries.md +++ b/docs/skill/references/discipline/manufacturing-boundaries.md @@ -1,5 +1,7 @@ # Discipline: Manufacturing Boundaries +**Read when:** you apply default dimensions, or the request touches manufacturability, tolerance, strength or process claims. + What geometric modeling may and may not conclude about manufacturing. Numeric defaults in this document are first-pass design heuristics, not SDK defaults. @@ -9,6 +11,7 @@ defaults in this document are first-pass design heuristics, not SDK defaults. boundaries, and tolerance-analysis APIs. - **Modeling guidance:** wall thickness, fillet, clearance-hole, and backlash starting values. They require user confirmation or an explicit assumption. + **Unattended:** record the value as an assumption in the brief's ledger (`discipline/operating-modes.md`). ## Defaults are first-pass, not claims diff --git a/docs/skill/references/discipline/mechanical-modeling.md b/docs/skill/references/discipline/mechanical-modeling.md index dc5de37d..0bbc491a 100644 --- a/docs/skill/references/discipline/mechanical-modeling.md +++ b/docs/skill/references/discipline/mechanical-modeling.md @@ -1,5 +1,7 @@ # Discipline: Mechanical Modeling +**Read when:** you choose a construction strategy or decompose a mechanical part or product before selecting SimpleCADAPI operations. + How to choose a construction strategy before selecting SimpleCADAPI operations. This document separates SDK contracts from mechanical design guidance. @@ -20,7 +22,9 @@ This document separates SDK contracts from mechanical design guidance. - **Decide part vs assembly before modeling.** Bodies that are separately manufactured, purchased, or movable belong in an assembly with connectors and placements; monolithic manufacturing intent gets a single fused solid. - Never leave a multi-body result where one labeled product was intended. + Several bodies of one part are deliberate: name them in + `make_part_rpart(bodies={...})`. Never leave stray solids out of the product, + or overlapping bodies, where one labeled product was intended. - **Pick the origin and orientation from the functional datum** — the mating interface, mounting plane, or symmetry axis — not from wherever the first primitive happened to land. See `datums-and-coordinate-systems.md`. diff --git a/docs/skill/references/discipline/notebook-cell-design.md b/docs/skill/references/discipline/notebook-cell-design.md index cd39c4fc..5db26d79 100644 --- a/docs/skill/references/discipline/notebook-cell-design.md +++ b/docs/skill/references/discipline/notebook-cell-design.md @@ -1,7 +1,7 @@ # Discipline: Notebook Cell Design -Where the cell boundaries go, and when a number is a `scad.var`. Read it with -`feature-tree-convention.md`: that file says what a feature block is and how it +Where the cell boundaries go, and when a number is a `scad.var`. **Read when:** you cut a notebook into cells, split or merge cells, or decide whether a number is a `scad.var`. +Read it with `feature-tree-convention.md`: that file says what a feature block is and how it is headed; this one says how to cut a notebook into cells and why. Every rule carries its reason and the symptom you see when you break it, so you can decide the cases the rules do not name. @@ -49,7 +49,9 @@ Fillets, chamfers, shells, large booleans, patterns: follow cached, and you retry only that cell. **C4. Merge cells only for things that are cheap and always edited together** -— a sketch and the extrusion of that same sketch are one feature anyway. Do +— a sketch and the extrusion of that same sketch are one feature anyway. Several +small blocks that always change together may share one cell, rebinding the body inside it; the cell returns the body +under the last block's slug, and they are cached and re-run as one unit, so keep this the exception. Do not merge two features to save a cell. *Why:* a cell costs one cache lookup, which is small; a merged cell re-runs both features on every edit, and a failure names neither. @@ -61,8 +63,8 @@ that, and returning it clutters the namespace with names that are not part of the feature tree. **C6. Whatever another cell needs is public, returned, and named for what it -is.** `base_plate`, not `body` or `shape2`; the slug of the block in -snake_case. *Why:* every top-level name has one defining cell (fact 3), and +is.** `base_plate`, not `body` or `shape2`; the naming rule (the block's slug in +snake_case) is in `feature-tree-convention.md`. *Why:* every top-level name has one defining cell (fact 3), and the names are the feature tree that a person and the next edit read. **C7. Do not change a value you received.** `apply_tag(...)`, @@ -76,13 +78,23 @@ disk copy instead, so a cell that tags what it received costs you the speed-up of the cell it tagged. **C8. Parameters live in their own cell(s) at the top, and hold no geometry.** -Group them by meaning (`# ---- params: flange ----`). *Why:* each reader +Group them by meaning (`# ---- params: flange ----`), because an override replaces every +variable of its cell. A trailing `# ui: slider(min, max, step=, unit=, label=)` +comment (or `number(...)`, `toggle()`) on a parameter's line chooses the control an +editor offers (`docs/guides/notebook-runtime.md`). *Why:* each reader depends only on the parameters it reads, so many parameters in one cell cost nothing; a geometry line in the params cell would re-run with every parameter change. -**C9. The product is made in the last cell.** Its id is the notebook id -(`scad.notebook_id()`); a family of parts uses `scad.use(path, id=...)`. +**C9. A part notebook (`kind = "part"`) needs no product cell: the runtime +makes the Part from its open bodies** (each public top-level solid no other cell +reads becomes a body named by its variable; the pieces of a split cut become +`_1..n`). Write the product cell (last, id `scad.notebook_id()`) only for +connectors, a material, or to choose and name the bodies yourself: +`make_part_rpart(part_id=scad.notebook_id(), bodies={"base": base, "lid": lid})`; +solids left out of it (tools, construction) are not part of the product. An +assembly notebook ends with its layout cell. A family of parts uses +`scad.use(path, id=...)`. **C10. In an assembly notebook, one `scad.use(...)` per child, the mates in their own cell, the solve in its own cell.** Do not carry the whole assembly @@ -90,14 +102,18 @@ through a chain of cells that each return it. *Why:* every cell that returns the assembly caches a full copy of it (fact 2). **C11. Make every edit step leave the names consistent.** A cell that reads a -name nothing defines fails, and so do all cells after it. The `sca new` -template has a params cell with `RADIUS` and `HEIGHT`, a `base` cell that -reads them, and a product cell that reads `base`. Replacing the params cell +name nothing defines fails, and so do all cells after it. The `sca new --kind sample` +notebook has a params cell with `RADIUS` and `HEIGHT` and a `base` cell that +reads them (the Part is implicit). Replacing the params cell with `sca cell put 1` while `base` still reads `RADIUS` fails with `NameError: name 'RADIUS' is not defined` until `base` is replaced too. Replace or delete the cells that read a name before, or in the same step as, the cell that defines it; when no editor is running, write the whole file once. +**C12. Import local modules in the cell that uses them, or in the setup cell.** +Either way, editing the module invalidates the notebook's cache +(`docs/guides/notebook-composition-and-cache.md`). + ## `Var` or a plain value A `scad.var(name, default, unit=..., comment=..., tolerance=...)` is a named diff --git a/docs/skill/references/discipline/operating-modes.md b/docs/skill/references/discipline/operating-modes.md new file mode 100644 index 00000000..3985afde --- /dev/null +++ b/docs/skill/references/discipline/operating-modes.md @@ -0,0 +1,68 @@ +# Discipline: Operating Modes + +**Read when:** at the start of every task (the router sends you here), and again whenever a gate says to ask or to wait +for a person. + +Every workflow runs in one of two modes. The mode changes who decides, never what is checked. + +## The two modes + +- **Interactive.** A person is in the loop (a conversation, or the editor open). You may ask questions, and a consent + gate waits for them. +- **Unattended.** No person will answer (a service, a batch run, a job queue). You never ask and never wait. Everything + a person would have decided, you decide from evidence and record. + +## Which mode am I in + +Decide once, at the start. The first line that matches wins: + +1. The task or the runtime instructions say the run is unattended, forbid questions, or say nobody will answer + ("unattended", "do not ask questions", "make evidence-based assumptions instead of asking") → **unattended**. +2. A person is talking to you, or has the editor open (`sca edit`) → **interactive**. +3. You have no tool for asking a question → **unattended**. +4. Otherwise → **interactive**. + +Write the result as the first line of the brief: `Mode: unattended (the task says the run is unattended)`. +Do not switch to interactive because a step is hard. In unattended mode the only way to reach a person is the report. + +## What never changes + +- A gate binds an artifact: it passes only when the artifact exists (printed facts, measurements, named views, filled + brief fields). A feeling of success is not a gate result, in either mode. +- A failed check closes by changing the design, or by redefining the check in the open. Never by weakening or deleting it. +- No claim of strength, fatigue, thermal behaviour, tolerance compliance or regulatory fitness without the analysis run. +- Every modelling rule (feature tree, QL selection, keyword arguments, cells and `Var`) is the same. +- Host-specific blocks about asking tools and plan tools are for interactive runs; in unattended mode skip the ones that + ask a person. + +## What changes: each gate and its replacement + +| In interactive mode | In unattended mode | +| --- | --- | +| An **Ask-or-Record** item: ask once, batched | **Record it.** Choose the value the evidence supports and write it in the brief's assumption ledger: `value / evidence / how to change it / the check that would show it wrong` | +| A **consent gate** (⛔: the person approves the brief or the plan) | A **self-review gate.** Re-read the brief against the request and the evidence, write one paragraph of decision with the open risks, and go on only when every required field is filled (a recorded assumption fills a field; an empty field does not) | +| A choice between design directions | Take the one that meets the stated requirements and is easier to change later; record the other in the plan | +| A missing prerequisite (file, tool, input): name it and stop that route | Record the blocker in the report, do the strongest work the available evidence supports (say which parts it supports), and go on. Never invent the missing input | +| An independent review (isolated reviewer, second pair of eyes) | A fresh-context check of your own against a written checklist, quoting what you observed; use a subagent if the host offers one. If neither is possible, the report says the review was not independent | +| Asking the person to look at a render | Look at it yourself: name the views, what you looked for, and quote the observation | +| Conflicting information | Measured evidence over prose, prose over inference; record the conflict and the choice | +| A closing message to the person | The report (below) | + +## The report (unattended) + +The deliverable that replaces every conversation. Name and place come from the task; the default is `report.md` next to +the source. It lists: + +- the assumptions, each as in the ledger above; +- what was reconstructed, what was omitted, and what is uncertain (with the evidence tier of each statement); +- the checks that ran with their results, and the checks that were not run, with the reason; +- any review that was not independent; +- the commands that reproduce the result. + +## Stopping + +End the run only after the source runs and you have inspected its result and written the report. The service that +receives the run validates independently; your closing text approves nothing. + +If the same check fails twice for the same cause, change the design or redefine the check in the report: do not retry the +same thing (`failure-and-repair.md`). diff --git a/docs/skill/references/discipline/requirement-and-cad-brief.md b/docs/skill/references/discipline/requirement-and-cad-brief.md index fbdc1b8e..98f2ab89 100644 --- a/docs/skill/references/discipline/requirement-and-cad-brief.md +++ b/docs/skill/references/discipline/requirement-and-cad-brief.md @@ -5,6 +5,9 @@ modeling brief. The operational steps live in `../domains/requirement-refinement.md`; this file carries reasoning and evidence-handling guidance, not automatic SDK behavior. +**Read when:** Role 1 (or any brief-writing step) starts: prose, images or drawings are being turned into a brief, or +evidence conflicts. + ## Every modality funnels into one brief Prose, images, drawings, and conflicts all reduce to the same artifact: a @@ -45,6 +48,9 @@ A drawing is a dimensioned contract: Success for a drawing-driven model: every drawing dimension is either verified by a measurement after generation, or explicitly reported as not verified. +**Unattended:** where this page says to flag or ask the user, record the conflict, the choice and the evidence in the +brief's assumption ledger instead (`discipline/operating-modes.md`); the checks and the brief quality gate are unchanged. + ## Assumptions are load-bearing only when reported An assumption silently applied is a future defect. Each one in the brief @@ -70,6 +76,9 @@ review invalidates an assumption, revisit every feature derived from it in place — a rebuild that re-encodes the same unchecked assumption reproduces the same rejection. +**Unattended:** the one question that would be asked is recorded as an assumption (value, evidence, how to change it, the +check that would show it wrong); nothing is asked. + ## Brief quality gate The brief is ready when it defines: source path(s), units, coordinate diff --git a/docs/skill/references/discipline/verifier-scripts.md b/docs/skill/references/discipline/verifier-scripts.md index 0b5d6c88..ec598f67 100644 --- a/docs/skill/references/discipline/verifier-scripts.md +++ b/docs/skill/references/discipline/verifier-scripts.md @@ -1,5 +1,8 @@ # Discipline: Verifier Scripts +**Read when:** Role 3 writes the executable check for a stage, or any task needs a script that measures a finished model +against the specification. + A verifier script measures a finished model against the specification and exits non-zero when a requirement fails. It is separate from the modeling script: it imports the model, measures it independently, and prints one line per check. diff --git a/docs/skill/references/docs/api/Body.md b/docs/skill/references/docs/api/Body.md new file mode 100644 index 00000000..288851b6 --- /dev/null +++ b/docs/skill/references/docs/api/Body.md @@ -0,0 +1,24 @@ +# Body + +## Class Definition + +```python +class Body(body_id, solid, name=None, color=None, opacity=None) +``` + +*Source: product/part.py* + +## Import Surface + +- top-level: `from simplecadapi import Body` + +## Description + +One named solid of a Part. + +``Body(body_id, solid, name=None, color=None, opacity=None)``: ``body_id`` is an +identifier unique within the part, ``solid`` the body's Solid, ``name`` an optional +display name, ``color`` an optional display colour (RGB floats in [0, 1]) and +``opacity`` an optional display opacity in [0, 1]. A one-body Part has one Body with +the id ``"body"``. The colour overrides the Part's material colour for display only; +set it on an existing Part with ``set_body_appearance_rpart``. diff --git a/docs/skill/references/docs/api/CellReport.md b/docs/skill/references/docs/api/CellReport.md index b95d4985..7a072560 100644 --- a/docs/skill/references/docs/api/CellReport.md +++ b/docs/skill/references/docs/api/CellReport.md @@ -3,7 +3,7 @@ ## Class Definition ```python -class CellReport(name: str, key: str, status: CellStatus) +class CellReport(name: str, key: str, status: CellStatus, tag_splits: tuple[dict, ...] = ()) ``` *Source: runtime/runner.py* diff --git a/docs/skill/references/docs/api/ElementRef.md b/docs/skill/references/docs/api/ElementRef.md new file mode 100644 index 00000000..7e800da2 --- /dev/null +++ b/docs/skill/references/docs/api/ElementRef.md @@ -0,0 +1,26 @@ +# ElementRef + +## Class Definition + +```python +class ElementRef(kind: str, feature: str = '', op: str = '', role: Optional[str] = None, ordinal: int = 0, topo: Optional[str] = None, between: Optional[Tuple[Union[str, 'ElementRef'], Union[str, 'ElementRef']]] = None, occurrence: int = 0, slot: int = 0, piece: Optional[int] = None, follow: str = 'identity', guard: Optional[Guard] = None, digest: Optional[str] = None, naming: int = 1) +``` + +*Source: ref.py* + +## Import Surface + +- top-level: `from simplecadapi import ElementRef` + +## Description + +A stable reference to one face, edge, vertex or solid; ``repr()`` is the code that makes it. + +Fields: ``kind``; ``feature`` and ``op`` (where the entity was born); then exactly one of ``role`` (its kernel +role, with ``ordinal`` saying which of the entities with that role), ``topo`` (its position in that output, for an +entity without a role; give a ``guard`` with it) or, for an edge, ``between`` (the two faces it lies between: role +names of faces of the same ``op``, or face references); ``occurrence`` (which of the feature's ``op`` nodes +upstream of the shape); ``slot`` (the output of that node); ``piece`` (the chosen piece when the entity has been +split); ``follow`` (``"identity"``, or ``"derived"`` to also follow into what it generated: an edge into its fillet +patch); ``guard``; ``digest`` (the parameters of the node, only when two nodes share its name); ``naming`` (the +naming version). diff --git a/docs/skill/references/docs/api/ElementRefError.md b/docs/skill/references/docs/api/ElementRefError.md new file mode 100644 index 00000000..c505991b --- /dev/null +++ b/docs/skill/references/docs/api/ElementRefError.md @@ -0,0 +1,19 @@ +# ElementRefError + +## Class Definition + +```python +class ElementRefError(resolved: Resolution) +``` + +*Source: topology/element_ref.py* + +## Import Surface + +- top-level: `from simplecadapi import ElementRefError` + +## Description + +A reference that does not name exactly one entity: ``split`` (choose a piece) or ``lost``. + +``status`` and ``candidates`` say which and what to choose from; ``resolved`` is the whole :class:`Resolution`. diff --git a/docs/skill/references/docs/api/NotebookConfig.md b/docs/skill/references/docs/api/NotebookConfig.md index 8e14e58f..e6766217 100644 --- a/docs/skill/references/docs/api/NotebookConfig.md +++ b/docs/skill/references/docs/api/NotebookConfig.md @@ -3,7 +3,7 @@ ## Class Definition ```python -class NotebookConfig(path: Path, id: str, revision: str, tolerance_profile: str, inputs: tuple[FileInput, ...]) +class NotebookConfig(path: Path, id: str, revision: str, tolerance_profile: str, inputs: tuple[FileInput, ...], kind: str | None = None) ``` *Source: runtime/config.py* diff --git a/docs/skill/references/docs/api/Part.md b/docs/skill/references/docs/api/Part.md index 13ea84da..52b6bc33 100644 --- a/docs/skill/references/docs/api/Part.md +++ b/docs/skill/references/docs/api/Part.md @@ -3,7 +3,7 @@ ## Class Definition ```python -class Part(part_id, body, name=None, material=None, connectors=()) +class Part(part_id, body=None, name=None, material=None, connectors=(), bodies=None) ``` *Source: product/part.py* @@ -14,4 +14,11 @@ class Part(part_id, body, name=None, material=None, connectors=()) ## Description -Single-body product item wrapping exactly one Solid. +Product item holding one Solid (``body``) or several named bodies (``bodies``). + +A one-body Part is the common case: ``part.body`` is its Solid. A multi-body +Part keeps several solids apart inside one product item (a case and its lid +modelled side by side, the pieces of a split cut): ``part.bodies`` lists them +as ``Body`` values in order, ``part.get_body(body_id)`` finds one, and +``part.body`` raises ``TypeError``. Bodies of one Part are never placed +relative to each other and carry no joints; use an Assembly for that. diff --git a/docs/skill/references/docs/api/README.md b/docs/skill/references/docs/api/README.md index 8e028f68..d0f2c5fe 100644 --- a/docs/skill/references/docs/api/README.md +++ b/docs/skill/references/docs/api/README.md @@ -37,6 +37,7 @@ This index includes generated docs for the public SimpleCAD API surface, includi - [make_face_from_sketch_rface](make_face_from_sketch_rface.md) *(from operators/sketch.py)* `top-level` - [make_face_from_wire_rface](make_face_from_wire_rface.md) *(from operators/geometry.py)* `top-level` - [make_face_from_wires_rface](make_face_from_wires_rface.md) *(from operators/geometry.py)* `top-level` +- [make_faces_from_sketch_rfacelist](make_faces_from_sketch_rfacelist.md) *(from operators/sketch.py)* `top-level` - [make_gordon_surface_rface](make_gordon_surface_rface.md) *(from operators/geometry.py)* `top-level` - [make_helix_redge](make_helix_redge.md) *(from operators/geometry.py)* `top-level` - [make_helix_rwire](make_helix_rwire.md) *(from operators/geometry.py)* `top-level` @@ -67,6 +68,13 @@ This index includes generated docs for the public SimpleCAD API surface, includi - [make_vertex_connector_rconnector](make_vertex_connector_rconnector.md) *(from operators/product.py)* `top-level` - [make_wire_from_edges_rwire](make_wire_from_edges_rwire.md) *(from operators/geometry.py)* `top-level` - [make_wire_from_sketch_rwire](make_wire_from_sketch_rwire.md) *(from operators/sketch.py)* `top-level` +- [make_workplane_from_axis_rworkplane](make_workplane_from_axis_rworkplane.md) *(from operators/geometry.py)* `top-level` +- [make_workplane_from_face_rworkplane](make_workplane_from_face_rworkplane.md) *(from operators/geometry.py)* `top-level` +- [make_workplane_from_points_rworkplane](make_workplane_from_points_rworkplane.md) *(from operators/geometry.py)* `top-level` +- [make_workplane_midplane_rworkplane](make_workplane_midplane_rworkplane.md) *(from operators/geometry.py)* `top-level` +- [make_workplane_normal_to_curve_rworkplane](make_workplane_normal_to_curve_rworkplane.md) *(from operators/geometry.py)* `top-level` +- [make_workplane_offset_rworkplane](make_workplane_offset_rworkplane.md) *(from operators/geometry.py)* `top-level` +- [make_workplane_rworkplane](make_workplane_rworkplane.md) *(from operators/geometry.py)* `top-level` ## Transforms @@ -187,7 +195,9 @@ This index includes generated docs for the public SimpleCAD API surface, includi ## Advanced Features +- [chamfer_corner_rsketch](chamfer_corner_rsketch.md) *(from operators/sketch.py)* `top-level` - [chamfer_rsolid](chamfer_rsolid.md) *(from operators/features.py)* `top-level` +- [fillet_corner_rsketch](fillet_corner_rsketch.md) *(from operators/sketch.py)* `top-level` - [fillet_rsolid](fillet_rsolid.md) *(from operators/features.py)* `top-level` - [helical_sweep_rsolid](helical_sweep_rsolid.md) *(from operators/features.py)* `top-level` - [shell_rsolid](shell_rsolid.md) *(from operators/features.py)* `top-level` @@ -314,6 +324,7 @@ This index includes generated docs for the public SimpleCAD API surface, includi ## Other - [Assembly](Assembly.md) *(from product/assembly.py)* `top-level` +- [Body](Body.md) *(from product/part.py)* `top-level` - [CaptureResult](CaptureResult.md) *(from product/capture.py)* `top-level` - [Component](Component.md) *(from product/assembly.py)* `top-level` - [Connector](Connector.md) *(from product/connector.py)* `top-level` @@ -322,6 +333,8 @@ This index includes generated docs for the public SimpleCAD API surface, includi - [Constraint](Constraint.md) *(from product/constraint.py)* `top-level` - [ConstraintReport](ConstraintReport.md) *(from product/constraint.py)* `top-level` - [ConstraintResidual](ConstraintResidual.md) *(from product/constraint.py)* `top-level` +- [ElementRef](ElementRef.md) *(from ref.py)* `top-level` +- [ElementRefError](ElementRefError.md) *(from topology/element_ref.py)* `top-level` - [GeometryRef](GeometryRef.md) *(from product/connector.py)* `top-level` - [Material](Material.md) *(from product/material.py)* `top-level` - [Part](Part.md) *(from product/part.py)* `top-level` @@ -380,8 +393,16 @@ This index includes generated docs for the public SimpleCAD API surface, includi - [constrain_symmetric_rsketch](constrain_symmetric_rsketch.md) *(from operators/sketch.py)* `top-level` - [constrain_tangent_rsketch](constrain_tangent_rsketch.md) *(from operators/sketch.py)* `top-level` - [constrain_vertical_rsketch](constrain_vertical_rsketch.md) *(from operators/sketch.py)* `top-level` +- [convert_spline_to_anchors_rsketch](convert_spline_to_anchors_rsketch.md) *(from operators/sketch.py)* `top-level` +- [convert_to_bspline_redge](convert_to_bspline_redge.md) *(from operators/geometry.py)* `top-level` +- [convert_to_bspline_rface](convert_to_bspline_rface.md) *(from operators/geometry.py)* `top-level` +- [diff_sketch_rsketchdiff](diff_sketch_rsketchdiff.md) *(from operators/sketch.py)* `top-level` - [draft_rsolid](draft_rsolid.md) *(from operators/features.py)* `top-level` +- [elevate_curve_degree_redge](elevate_curve_degree_redge.md) *(from operators/geometry.py)* `top-level` +- [elevate_surface_degree_rface](elevate_surface_degree_rface.md) *(from operators/geometry.py)* `top-level` - [explain_tag](explain_tag.md) *(from operators/selection.py)* `top-level` +- [explode_rassembly](explode_rassembly.md) *(from operators/product.py)* `top-level` +- [extend_rsketch](extend_rsketch.md) *(from operators/sketch.py)* `top-level` - [fill_holes_rshell](fill_holes_rshell.md) *(from operators/geometry.py)* `top-level` - [fit_point_grid_rface](fit_point_grid_rface.md) *(from operators/geometry.py)* `top-level` - [free_boundaries_rwirelist](free_boundaries_rwirelist.md) *(from operators/geometry.py)* `top-level` @@ -390,14 +411,27 @@ This index includes generated docs for the public SimpleCAD API surface, includi - [get_sketch_point_rsketchref](get_sketch_point_rsketchref.md) *(from operators/sketch.py)* `top-level` - [ground_component_rassembly](ground_component_rassembly.md) *(from operators/product.py)* `top-level` - [identity_placement_rplacement](identity_placement_rplacement.md) *(from operators/product.py)* `top-level` +- [insert_curve_knot_redge](insert_curve_knot_redge.md) *(from operators/geometry.py)* `top-level` +- [insert_spline_anchor_rsketch](insert_spline_anchor_rsketch.md) *(from operators/sketch.py)* `top-level` +- [insert_surface_knot_rface](insert_surface_knot_rface.md) *(from operators/geometry.py)* `top-level` +- [inset_face_rsolid](inset_face_rsolid.md) *(from operators/features.py)* `top-level` - [inspect_assembly_constraints_rconstraintreport](inspect_assembly_constraints_rconstraintreport.md) *(from operators/product.py)* `top-level` +- [inspect_body_overlaps_rlist](inspect_body_overlaps_rlist.md) *(from operators/product.py)* `top-level` +- [inspect_curve_rcurvedata](inspect_curve_rcurvedata.md) *(from operators/geometry.py)* `top-level` +- [inspect_sketch_rsketchcrossings](inspect_sketch_rsketchcrossings.md) *(from operators/sketch.py)* `top-level` +- [inspect_sketch_rsketchprofiles](inspect_sketch_rsketchprofiles.md) *(from operators/sketch.py)* `top-level` - [inspect_sketch_rsketchresult](inspect_sketch_rsketchresult.md) *(from operators/sketch.py)* `top-level` +- [inspect_surface_rsurfacedata](inspect_surface_rsurfacedata.md) *(from operators/geometry.py)* `top-level` - [key](key.md) *(from ql.py)* `submodule:ql` - [linear_pattern_rsolidlist](linear_pattern_rsolidlist.md) *(from operators/transform.py)* `top-level` +- [list_tag_splits](list_tag_splits.md) *(from operators/selection.py)* `top-level` - [load_brep_region_rshell](load_brep_region_rshell.md) *(from operators/geometry.py)* `top-level` - [load_brep_region_rsolid](load_brep_region_rsolid.md) *(from operators/geometry.py)* `top-level` - [measure_constraint_residual_rconstraintresidual](measure_constraint_residual_rconstraintresidual.md) *(from operators/product.py)* `top-level` +- [measure_part_mass_rmassreport](measure_part_mass_rmassreport.md) *(from operators/product.py)* `top-level` - [meta](meta.md) *(from ql.py)* `submodule:ql` +- [move_curve_poles_redge](move_curve_poles_redge.md) *(from operators/geometry.py)* `top-level` +- [move_surface_poles_rface](move_surface_poles_rface.md) *(from operators/geometry.py)* `top-level` - [not_](not_.md) *(from ql.py)* `submodule:ql` - [offset_rsolid](offset_rsolid.md) *(from operators/features.py)* `top-level` - [operation_event](operation_event.md) *(from ql.py)* `submodule:ql` @@ -408,13 +442,36 @@ This index includes generated docs for the public SimpleCAD API surface, includi - [placement_frame_mm](placement_frame_mm.md) *(from product/placement.py)* `submodule:product/placement` - [placement_from_canonical](placement_from_canonical.md) *(from product/placement.py)* `submodule:product/placement` - [placement_ticks](placement_ticks.md) *(from product/placement.py)* `submodule:product/placement` +- [press_pull_rsolid](press_pull_rsolid.md) *(from operators/features.py)* `top-level` +- [project_rsketch](project_rsketch.md) *(from operators/sketch.py)* `top-level` - [prop](prop.md) *(from ql.py)* `submodule:ql` +- [ql.ref](ql.ref.md) *(from ql.py)* `submodule:ql` - [radial_pattern_rsolidlist](radial_pattern_rsolidlist.md) *(from operators/transform.py)* `top-level` +- [rebuild_curve_redge](rebuild_curve_redge.md) *(from operators/geometry.py)* `top-level` +- [rebuild_surface_rface](rebuild_surface_rface.md) *(from operators/geometry.py)* `top-level` +- [ref.coerce](ref.coerce.md) *(from ref.py)* `submodule:ref` +- [ref.edge](ref.edge.md) *(from ref.py)* `submodule:ref` +- [ref.edges](ref.edges.md) *(from ref.py)* `submodule:ref` +- [ref.face](ref.face.md) *(from ref.py)* `submodule:ref` +- [ref.faces](ref.faces.md) *(from ref.py)* `submodule:ref` +- [ref.of](ref.of.md) *(from ref.py)* `submodule:ref` +- [ref.parse](ref.parse.md) *(from ref.py)* `submodule:ref` +- [ref.resolve](ref.resolve.md) *(from ref.py)* `submodule:ref` +- [ref.vertex](ref.vertex.md) *(from ref.py)* `submodule:ref` +- [ref.vertices](ref.vertices.md) *(from ref.py)* `submodule:ref` - [register_property_resolver](register_property_resolver.md) *(from ql.py)* `submodule:ql` +- [remove_spline_anchor_rsketch](remove_spline_anchor_rsketch.md) *(from operators/sketch.py)* `top-level` - [render_screenshot_rpath](render_screenshot_rpath.md) *(from operators/features.py)* `top-level` +- [sample_curve_rcurvature](sample_curve_rcurvature.md) *(from operators/geometry.py)* `top-level` +- [sample_surface_risocurves](sample_surface_risocurves.md) *(from operators/geometry.py)* `top-level` - [scale_rsolid](scale_rsolid.md) *(from operators/transform.py)* `top-level` - [select](select.md) *(from ql.py)* `submodule:ql` +- [set_body_appearance_rpart](set_body_appearance_rpart.md) *(from operators/product.py)* `top-level` +- [set_curve_poles_redge](set_curve_poles_redge.md) *(from operators/geometry.py)* `top-level` +- [set_curve_weights_redge](set_curve_weights_redge.md) *(from operators/geometry.py)* `top-level` - [set_public_connector_rassembly](set_public_connector_rassembly.md) *(from operators/product.py)* `top-level` +- [set_surface_poles_rface](set_surface_poles_rface.md) *(from operators/geometry.py)* `top-level` +- [set_surface_weights_rface](set_surface_weights_rface.md) *(from operators/geometry.py)* `top-level` - [sew_faces_rshell](sew_faces_rshell.md) *(from operators/geometry.py)* `top-level` - [shells](shells.md) *(from ql.py)* `submodule:ql` - [solve_assembly_constraints_rassembly](solve_assembly_constraints_rassembly.md) *(from operators/product.py)* `top-level` @@ -422,6 +479,7 @@ This index includes generated docs for the public SimpleCAD API surface, includi - [source_topology](source_topology.md) *(from ql.py)* `submodule:ql` - [tag](tag.md) *(from ql.py)* `submodule:ql` - [transform_rsolid](transform_rsolid.md) *(from operators/transform.py)* `top-level` +- [trim_rsketch](trim_rsketch.md) *(from operators/sketch.py)* `top-level` - [trim_surface_rface](trim_surface_rface.md) *(from operators/geometry.py)* `top-level` - [twisted_sweep_rsolid](twisted_sweep_rsolid.md) *(from operators/features.py)* `top-level` - [unground_component_rassembly](unground_component_rassembly.md) *(from operators/product.py)* `top-level` diff --git a/docs/skill/references/docs/api/apply_tag_rselection.md b/docs/skill/references/docs/api/apply_tag_rselection.md index d389a20b..c817661a 100644 --- a/docs/skill/references/docs/api/apply_tag_rselection.md +++ b/docs/skill/references/docs/api/apply_tag_rselection.md @@ -15,3 +15,47 @@ def apply_tag_rselection(scope, targets, tag, topology_propagation==", 0.999)), "lid_seat")`` +names the top face; ``scad.ql.faces().where(scad.ql.tag("lid_seat"))`` selects it +later. A body of a Part is named with ``set_body_appearance_rpart(part, body_id, name=...)``. + +## Parameters + +### scope + +- **Description**: The shape the tag is assigned in (a Solid, usually the body). + +### targets + +- **Description**: What to tag: a ``ql`` selection (``scad.ql.faces().where(...)``) or a list of sub-shapes of ``scope``. + +### tag + +- **Description**: The name, a normalized token such as ``"lid_seat"`` or ``"role.mounting_surface"``. + +### topology_propagation + +- **Description**: ``"local"`` tags the targets only; ``"downward"`` also their edges and vertices. + +### lineage_policy + +- **Description**: How the tag follows the targets through later operations + +### (default ``"continuation_fragment"`` + +- **Description**: kept on continued faces; the pieces of a split face carry ``.split1`` .. ``.split`` instead, see ``list_tag_splits``). + +## Returns + +AnyShape: A view of ``scope`` with the tag assigned (the same geometry). + +## Raises + +- **SimpleCADError**: The selection resolved nothing, a target is not inside the scope, or the tag is malformed. diff --git a/docs/skill/references/docs/api/chamfer_corner_rsketch.md b/docs/skill/references/docs/api/chamfer_corner_rsketch.md new file mode 100644 index 00000000..45135a5b --- /dev/null +++ b/docs/skill/references/docs/api/chamfer_corner_rsketch.md @@ -0,0 +1,21 @@ +# chamfer_corner_rsketch + +## API Definition + +```python +def chamfer_corner_rsketch(sketch, a, b, distance) +``` + +*Source: operators/sketch.py* + +## Import Surface + +- top-level: `from simplecadapi import chamfer_corner_rsketch` + +## Description + +Cut the corner where lines ``a`` and ``b`` meet with a straight line ``distance`` from the corner along each line. + +Same rules as fillet_corner_rsketch: the lines must share an end point, both are shortened, the corner point goes +away unless something else uses it. The new line ``l`` joins the two points, with a driving length (its chord) +and a driving angle to ``a``. ``distance`` must be shorter than the shorter line (the message names it). diff --git a/docs/skill/references/docs/api/chamfer_rsolid.md b/docs/skill/references/docs/api/chamfer_rsolid.md index ebf5ef27..f920a569 100644 --- a/docs/skill/references/docs/api/chamfer_rsolid.md +++ b/docs/skill/references/docs/api/chamfer_rsolid.md @@ -22,3 +22,5 @@ face and the reference face — the chamfer is asymmetric with the second leg at ``distance * tan(angle)`` on the non-reference side; ``reference_direction`` picks, per edge, the adjacent face whose outward normal best matches it as the reference face. + +``solid`` may also be the list of pieces a cut that split a body returns: each piece gets the picked edges that lie on it, a piece with none is kept as it is, and the result is the list of pieces in the same order. A selector is resolved on every piece and its cardinality (``exactly``/``at_least``/``at_most``) is checked on the edges of all of them. diff --git a/docs/skill/references/docs/api/constrain_point_on_rsketch.md b/docs/skill/references/docs/api/constrain_point_on_rsketch.md index e87b01b7..2f9fb234 100644 --- a/docs/skill/references/docs/api/constrain_point_on_rsketch.md +++ b/docs/skill/references/docs/api/constrain_point_on_rsketch.md @@ -14,4 +14,4 @@ def constrain_point_on_rsketch(sketch, point, entity, *, constraint_id=None) ## Description -Constrain a sketch point to lie on a line, circle, or circular arc. +Constrain a sketch point to lie on a line, circle, circular arc, ellipse or (non-periodic) B-spline. diff --git a/docs/skill/references/docs/api/convert_spline_to_anchors_rsketch.md b/docs/skill/references/docs/api/convert_spline_to_anchors_rsketch.md new file mode 100644 index 00000000..50fa0783 --- /dev/null +++ b/docs/skill/references/docs/api/convert_spline_to_anchors_rsketch.md @@ -0,0 +1,23 @@ +# convert_spline_to_anchors_rsketch + +## API Definition + +```python +def convert_spline_to_anchors_rsketch(sketch, spline) +``` + +*Source: operators/sketch.py* + +## Import Surface + +- top-level: `from simplecadapi import convert_spline_to_anchors_rsketch` + +## Description + +Write a B-spline in anchor form (Bezier spans: anchors on the curve, a handle on each side). + +A non-rational B-spline of degree 3 or less keeps its exact shape (degree elevation and knot insertion); a rational one +or one of a higher degree is approximated within 1e-4 and the notes of metadata ``last_edit`` say how close. The end +points keep their ids; an inner control point that does not move keeps its id, the others are new points ``p``, and +control points no entity uses any more are deleted with their constraints. A B-spline already in anchor form is left +as it is (a note says so). Refused for a periodic B-spline. See ``insert_spline_anchor_rsketch`` for the form. diff --git a/docs/skill/references/docs/api/convert_to_bspline_redge.md b/docs/skill/references/docs/api/convert_to_bspline_redge.md new file mode 100644 index 00000000..f0dcaa42 --- /dev/null +++ b/docs/skill/references/docs/api/convert_to_bspline_redge.md @@ -0,0 +1,19 @@ +# convert_to_bspline_redge + +## API Definition + +```python +def convert_to_bspline_redge(edge, *, tag_prefix=None) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import convert_to_bspline_redge` + +## Description + +Convert an edge's curve to a B-spline, so the curve edits can work on it. + +The explicit, recorded step for analytic edges (a line becomes a degree-1 B-spline, a circle or arc a rational quadratic); the shape does not change, its parameter range may. Returns a new Edge; the input is not changed. Recorded: the Edge continues in the node's history, so tags and references follow. diff --git a/docs/skill/references/docs/api/convert_to_bspline_rface.md b/docs/skill/references/docs/api/convert_to_bspline_rface.md new file mode 100644 index 00000000..cb5faab4 --- /dev/null +++ b/docs/skill/references/docs/api/convert_to_bspline_rface.md @@ -0,0 +1,19 @@ +# convert_to_bspline_rface + +## API Definition + +```python +def convert_to_bspline_rface(face, *, tag_prefix=None) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import convert_to_bspline_rface` + +## Description + +Convert a face's surface (and its boundary curves) to B-splines, so the surface edits can work on it. + +The explicit, recorded step for analytic faces (planes, cylinders, cones, spheres, ...); the shape and the trimming do not change, the (u, v) parametrisation may. Returns a new Face; the input is not changed. Recorded: the Face and its boundary edges continue in the node's history, so tags and references follow. diff --git a/docs/skill/references/docs/api/cut_rsolid.md b/docs/skill/references/docs/api/cut_rsolid.md index 880fa62c..0c63a0d7 100644 --- a/docs/skill/references/docs/api/cut_rsolid.md +++ b/docs/skill/references/docs/api/cut_rsolid.md @@ -3,7 +3,7 @@ ## API Definition ```python -def cut_rsolid(*solids, skip_non_intersecting=True, tracking_policy='full') +def cut_rsolid(*solids, skip_non_intersecting=True, tracking_policy='full', on_split='pieces') ``` *Source: operators/boolean.py* @@ -33,10 +33,28 @@ sequences, and returns a single `Solid`. - **Description**: FULL computes topology history and lineage. GRAPH keeps the replayable operation node without computing a TopoDelta. +### on_split + +- **Description**: What a cut that splits the body (a slot through a bar) returns. + +### ``"pieces"`` + +- **Type**: `default` +- **Description**: every piece, as ``list[Solid]`` ordered by centroid + +### (x, then y, then z), recorded like ``cut_rsolid_list``. ``"error"`` + +- **Description**: raise, for code that needs exactly one Solid. + ## Returns -Solid: The cut result solid. +Solid: The cut result solid, when the result is one piece. +list[Solid]: The pieces (a ``scad.Pieces``), when the cut split the body +(``on_split="pieces"``). Operations that take one shape run on every piece +(``cut_rsolid(pieces, tool)``, ``translate_shape(pieces, ...)``) and return +``Pieces``; a selector's cardinality is checked over all pieces. Pass them on as bodies: ``make_part_rpart(part_id=..., body=pieces)`` makes the +bodies ``body_1 .. body_n``, ``bodies={"bar": pieces}`` makes ``bar_1 .. bar_n``. ## Raises -- **SimpleCADError**: The cut produced no solid, or split the body into several (a slot through a bar). Use ``cut_rsolid_list`` to keep every piece. +- **SimpleCADError**: The cut produced no solid, or (``on_split="error"``) split the body. Use ``cut_rsolid_list`` to always get a list. diff --git a/docs/skill/references/docs/api/diff_sketch_rsketchdiff.md b/docs/skill/references/docs/api/diff_sketch_rsketchdiff.md new file mode 100644 index 00000000..0a1c1136 --- /dev/null +++ b/docs/skill/references/docs/api/diff_sketch_rsketchdiff.md @@ -0,0 +1,20 @@ +# diff_sketch_rsketchdiff + +## API Definition + +```python +def diff_sketch_rsketchdiff(before, after) +``` + +*Source: operators/sketch.py* + +## Import Surface + +- top-level: `from simplecadapi import diff_sketch_rsketchdiff` + +## Description + +Report what an edit did: entity and constraint ids created, removed or changed between two sketches. + +Returns a dict with ``created``, ``removed``, ``changed`` (entity ids, points included) and +``created_constraints``, ``removed_constraints``, ``changed_constraints``. Not recorded in the graph. diff --git a/docs/skill/references/docs/api/draft_rsolid.md b/docs/skill/references/docs/api/draft_rsolid.md index e02d8b02..9062cadc 100644 --- a/docs/skill/references/docs/api/draft_rsolid.md +++ b/docs/skill/references/docs/api/draft_rsolid.md @@ -25,7 +25,9 @@ unchanged result. Other surface families are explicitly rejected. The operation is recorded as ``make_draft_rsolid``; the body and selected faces are ordered inputs, and the angle, neutral plane and face selection -nodes are parameters, so model JSON replay is strict. +nodes are parameters, so model JSON replay is strict. The recorded node carries the kernel's face and edge history, so element +references follow faces through it (a +drafted face continues its source face). ## Parameters diff --git a/docs/skill/references/docs/api/elevate_curve_degree_redge.md b/docs/skill/references/docs/api/elevate_curve_degree_redge.md new file mode 100644 index 00000000..fc87c71a --- /dev/null +++ b/docs/skill/references/docs/api/elevate_curve_degree_redge.md @@ -0,0 +1,19 @@ +# elevate_curve_degree_redge + +## API Definition + +```python +def elevate_curve_degree_redge(edge, degree, *, tag_prefix=None) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import elevate_curve_degree_redge` + +## Description + +Raise the degree of an edge's B-spline curve to ``degree``. + +The shape does not change; poles are added. Only B-spline and Bezier curves are edited; a line, circle or other analytic edge is refused with a message naming ``convert_to_bspline_redge``, the explicit conversion step. Returns a new Edge; the input is not changed. Recorded: the Edge continues in the node's history, so tags and references follow. diff --git a/docs/skill/references/docs/api/elevate_surface_degree_rface.md b/docs/skill/references/docs/api/elevate_surface_degree_rface.md new file mode 100644 index 00000000..877ebacf --- /dev/null +++ b/docs/skill/references/docs/api/elevate_surface_degree_rface.md @@ -0,0 +1,19 @@ +# elevate_surface_degree_rface + +## API Definition + +```python +def elevate_surface_degree_rface(face, u_degree=None, v_degree=None, *, tag_prefix=None) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import elevate_surface_degree_rface` + +## Description + +Raise the U and/or V degree of a face's B-spline surface. + +Give ``u_degree``, ``v_degree`` or both; a degree that is given is at least the current one and one of them is higher. The shape does not change; poles are added. The face keeps its trimming exactly: its boundary loops stay where they are in the surface's (u, v) space and the 3D boundary edges are recomputed on the edited surface. Only B-spline and Bezier surfaces are edited; a plane, cylinder or other analytic face is refused with a message naming ``convert_to_bspline_rface``, the explicit conversion step. Returns a new Face; the input is not changed. Recorded: the Face and its boundary edges continue in the node's history, so tags and references follow. diff --git a/docs/skill/references/docs/api/explode_rassembly.md b/docs/skill/references/docs/api/explode_rassembly.md new file mode 100644 index 00000000..de6d11f8 --- /dev/null +++ b/docs/skill/references/docs/api/explode_rassembly.md @@ -0,0 +1,28 @@ +# explode_rassembly + +## API Definition + +```python +def explode_rassembly(assembly, factor, *, center=None, axis=None, directions=None, weights=None) +``` + +*Source: operators/product.py* + +## Import Surface + +- top-level: `from simplecadapi import explode_rassembly` + +## Description + +Return a display copy of an assembly with its components pushed apart for an exploded view. + +Each top-level component moves by ``factor * weight * direction * size``: ``size`` is the diagonal of the box that +holds every component's bodies, ``weight`` is ``weights[component_id]`` (default 1; list a component with 0 to keep +it, for example the grounded base) and ``direction`` is ``directions[component_id]`` (normalised), else ``axis`` +(signed by the side of the centre the component is on), else the vector from the centre to the component's own +centre (the centre of the box of its bodies). ``center`` overrides the assembly centre (the centre of the box of +all components). A component with no direction (it is at the centre, or on the centre plane of ``axis``) stays and +is listed in ``get_metadata("explode")``, together with factor, size, centre and every displacement. ``factor=0`` +gives the same placements. Parts, constraints, grounding and public connectors are untouched and the constraints +are not re-solved: the result is a placement set for display, rendering and export, and solving it moves the +components back. diff --git a/docs/skill/references/docs/api/extend_rsketch.md b/docs/skill/references/docs/api/extend_rsketch.md new file mode 100644 index 00000000..a5c20922 --- /dev/null +++ b/docs/skill/references/docs/api/extend_rsketch.md @@ -0,0 +1,25 @@ +# extend_rsketch + +## API Definition + +```python +def extend_rsketch(sketch, entity, end, *, to=None) +``` + +*Source: operators/sketch.py* + +## Import Surface + +- top-level: `from simplecadapi import extend_rsketch` + +## Description + +Lengthen a sketch line or B-spline at ``end`` ('start' or 'end') until it meets ``to`` (default: the nearest entity ahead). + +The sketch is solved first; the end point moves onto the other entity (a line, circle, arc, ellipse or B-spline) +and is tied to it with a point_on constraint. A line goes straight on. A B-spline grows curvature-continuously +(GeomLib.ExtendCurveToPoint) towards where its end tangent meets the target and stops at the first crossing: its +first part keeps its shape, its control points change near the moved end (unmoved ones keep their ids, the others +are new points) and a cubic Bezier chain stays one. A closed B-spline has no end to extend. Refused when the end +point is shared with another entity. The length dimension of the entity, dimensions or fix on the moved point, and +for a B-spline its fix and its tangent at the moved end, no longer hold and are dropped. diff --git a/docs/skill/references/docs/api/fillet_corner_rsketch.md b/docs/skill/references/docs/api/fillet_corner_rsketch.md new file mode 100644 index 00000000..6d175d33 --- /dev/null +++ b/docs/skill/references/docs/api/fillet_corner_rsketch.md @@ -0,0 +1,27 @@ +# fillet_corner_rsketch + +## API Definition + +```python +def fillet_corner_rsketch(sketch, a, b, radius) +``` + +*Source: operators/sketch.py* + +## Import Surface + +- top-level: `from simplecadapi import fillet_corner_rsketch` + +## Description + +Round the corner where lines ``a`` and ``b`` meet with an arc of ``radius``. + +The lines must share an end point (the corner). The sketch is solved first; both lines are shortened to the +tangent points and the arc (counter-clockwise from its start to its end around its centre) is tied to each line +with a tangent constraint and to ``radius`` with a driving radius dimension, so the result survives a re-solve. +The corner point goes away unless a constraint or another entity uses it (then it stays as the sharp corner and +the cut-off parts of the lines stay as construction lines). horizontal/vertical and the other constraints that +hold for a shorter line stay; lengths and other dimensions of the lines are dropped (see +diff_sketch_rsketchdiff). Refused: parallel lines, lines that share no point, a radius too large for the +shorter line (the message names the largest radius). New ids: points ``p``, arc ``a``, legs ``l``, +constraints ``c__``. diff --git a/docs/skill/references/docs/api/fillet_rsolid.md b/docs/skill/references/docs/api/fillet_rsolid.md index de959d06..b2006e78 100644 --- a/docs/skill/references/docs/api/fillet_rsolid.md +++ b/docs/skill/references/docs/api/fillet_rsolid.md @@ -15,3 +15,5 @@ def fillet_rsolid(solid, edges, radius, *, result_tag=None, generated_faces_tag= ## Description Apply fillets, with optional tagging of kernel-proven patch faces. + +``solid`` may also be the list of pieces a cut that split a body returns: each piece gets the picked edges that lie on it, a piece with none is kept as it is, and the result is the list of pieces in the same order. A selector is resolved on every piece and its cardinality (``exactly``/``at_least``/``at_most``) is checked on the edges of all of them. diff --git a/docs/skill/references/docs/api/insert_curve_knot_redge.md b/docs/skill/references/docs/api/insert_curve_knot_redge.md new file mode 100644 index 00000000..0f52ba40 --- /dev/null +++ b/docs/skill/references/docs/api/insert_curve_knot_redge.md @@ -0,0 +1,19 @@ +# insert_curve_knot_redge + +## API Definition + +```python +def insert_curve_knot_redge(edge, parameter, multiplicity=1, *, tag_prefix=None) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import insert_curve_knot_redge` + +## Description + +Insert a knot into an edge's B-spline curve at ``parameter`` (strictly inside the edge's parameter range). + +The shape does not change; the curve gains ``multiplicity`` poles near that parameter, for finer local editing. The knot's total multiplicity may not exceed the degree. Only B-spline and Bezier curves are edited; a line, circle or other analytic edge is refused with a message naming ``convert_to_bspline_redge``, the explicit conversion step. Returns a new Edge; the input is not changed. Recorded: the Edge continues in the node's history, so tags and references follow. diff --git a/docs/skill/references/docs/api/insert_spline_anchor_rsketch.md b/docs/skill/references/docs/api/insert_spline_anchor_rsketch.md new file mode 100644 index 00000000..e27da1ce --- /dev/null +++ b/docs/skill/references/docs/api/insert_spline_anchor_rsketch.md @@ -0,0 +1,30 @@ +# insert_spline_anchor_rsketch + +## API Definition + +```python +def insert_spline_anchor_rsketch(sketch, spline, at) +``` + +*Source: operators/sketch.py* + +## Import Surface + +- top-level: `from simplecadapi import insert_spline_anchor_rsketch` + +## Description + +Insert an anchor into a B-spline in anchor form, at the curve point nearest ``at``, without changing the curve. + +The anchor form is a pen tool's curve: anchors on the curve, each with an in-handle and an out-handle. It is a cubic +B-spline whose control points are ``[A0, out0, in1, A1, out1, in2, A2, ...]`` (every one a sketch point) and whose +inner knots have multiplicity 3, so every span is a cubic Bezier from one anchor to the next; the editor's spline tool +draws them, ``convert_spline_to_anchors_rsketch`` turns another B-spline into one. Moving an anchor or a handle is +moving its point (the solver moves every control point of such a spline). + +The span under ``at`` is split at that place (de Casteljau), so the curve is the same: the span's two handles get +shorter (they keep their ids; a dimension or fix on them no longer holds and is dropped, see the notes) and the new +anchor and its two handles are new points ``p`` (the lowest free numbers). The new knot is the curve parameter of +the place, with multiplicity 3. ``at`` is a point (x, y) in sketch coordinates or a point id. The sketch is solved +first. Refused for a B-spline that is not in anchor form and at an existing anchor. Metadata ``last_edit`` notes name +the new anchor and its handles; ``diff_sketch_rsketchdiff(before, after)`` lists them too. diff --git a/docs/skill/references/docs/api/insert_surface_knot_rface.md b/docs/skill/references/docs/api/insert_surface_knot_rface.md new file mode 100644 index 00000000..0effb9c3 --- /dev/null +++ b/docs/skill/references/docs/api/insert_surface_knot_rface.md @@ -0,0 +1,19 @@ +# insert_surface_knot_rface + +## API Definition + +```python +def insert_surface_knot_rface(face, direction, parameter, multiplicity=1, *, tag_prefix=None) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import insert_surface_knot_rface` + +## Description + +Insert a U or V knot into a face's B-spline surface at ``parameter`` (strictly inside the surface's domain in that direction). + +The shape does not change; the pole grid gains ``multiplicity`` rows (``"U"``) or columns (``"V"``). The face keeps its trimming exactly: its boundary loops stay where they are in the surface's (u, v) space and the 3D boundary edges are recomputed on the edited surface. Only B-spline and Bezier surfaces are edited; a plane, cylinder or other analytic face is refused with a message naming ``convert_to_bspline_rface``, the explicit conversion step. Returns a new Face; the input is not changed. Recorded: the Face and its boundary edges continue in the node's history, so tags and references follow. diff --git a/docs/skill/references/docs/api/inset_face_rsolid.md b/docs/skill/references/docs/api/inset_face_rsolid.md new file mode 100644 index 00000000..22246a86 --- /dev/null +++ b/docs/skill/references/docs/api/inset_face_rsolid.md @@ -0,0 +1,65 @@ +# inset_face_rsolid + +## API Definition + +```python +def inset_face_rsolid(solid, face, distance) +``` + +*Source: operators/features.py* + +## Import Surface + +- top-level: `from simplecadapi import inset_face_rsolid` + +## Description + +Imprint the loops of one planar face moved a distance inside it. + +The outline of the face (and the outline of each of its holes) is offset in +the face's plane by ``distance`` towards the inside, and the offset loops are +imprinted on the solid: the face becomes an inner face and the ring around it +(a ring around each hole too). The solid keeps its shape and volume; it only +has one face more, which the next operation can pull out, push in or open, so +steps, bosses and walls are built by insetting and pulling again. + +The operation is recorded as ``make_inset_face_rsolid``; the body and the face +are ordered inputs and the distance is a parameter, so model JSON replay is +strict. The split pieces are fragments of the face (element references and +tags follow them as a split) and carry the kernel roles ``inset.inner`` and +``inset.ring``, so ``scad.ref.face(..., role="inset.inner")`` names the inner +face whatever the face was. + +## Parameters + +### solid + +- **Description**: The body whose face is inset. + +### face + +- **Description**: One planar face of ``solid``, or a QL selector that picks exactly one. + +### distance + +- **Description**: How far inside the face the loops run, in mm; positive. + +## Returns + +The same solid with the face split in its inner face and ring. + +## Raises + +- **TypeError**: ``solid`` is not a Solid. +- **ValueError**: Not exactly one face, a face of another solid, a curved face, a non-positive distance, or a distance at which nothing of the face is left inside the loops. + +## Examples + +```python +import simplecadapi as scad +ql = scad.ql +box = scad.make_box_rsolid(40, 30, 20) +top = ql.faces().where(ql.prop("geom.center.z", ">=", 19.9)).exactly(1) +inset = scad.inset_face_rsolid(box, top, 3) +inner = scad.ref.face("", "make_inset_face_rsolid", role="inset.inner").one(inset) +``` diff --git a/docs/skill/references/docs/api/inspect_body_overlaps_rlist.md b/docs/skill/references/docs/api/inspect_body_overlaps_rlist.md new file mode 100644 index 00000000..d7b4f906 --- /dev/null +++ b/docs/skill/references/docs/api/inspect_body_overlaps_rlist.md @@ -0,0 +1,43 @@ +# inspect_body_overlaps_rlist + +## API Definition + +```python +def inspect_body_overlaps_rlist(part: 'Part', *, min_volume_mm3: 'float' = 1e-06) -> 'list[BodyOverlap]' +``` + +*Source: operators/product.py* + +## Import Surface + +- top-level: `from simplecadapi import inspect_body_overlaps_rlist` + +## Description + +List the pairs of bodies of a Part that share volume. + +Overlapping bodies are allowed (a Part does not fuse them), but mass counts the +shared volume once per body and STEP holds both copies. Bodies that only touch +(a face, an edge or a point in common) do not overlap. + +``scad.inspect_body_overlaps_rlist(case)`` before export. Building the part's +definition (a notebook run, ``@part``, a package) warns with ``BodyOverlapWarning`` +for the same pairs. + +## Parameters + +### part + +- **Description**: The Part to inspect; a one-body Part has no overlaps. + +### min_volume_mm3 + +- **Description**: Shared volume below which a pair is not reported. + +## Returns + +list[BodyOverlap]: ``(first, second, volume_mm3)`` per overlapping pair, in body order. + +## Raises + +- **SimpleCADError**: The value is not a Part. diff --git a/docs/skill/references/docs/api/inspect_curve_rcurvedata.md b/docs/skill/references/docs/api/inspect_curve_rcurvedata.md new file mode 100644 index 00000000..4e601339 --- /dev/null +++ b/docs/skill/references/docs/api/inspect_curve_rcurvedata.md @@ -0,0 +1,19 @@ +# inspect_curve_rcurvedata + +## API Definition + +```python +def inspect_curve_rcurvedata(edge) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import inspect_curve_rcurvedata` + +## Description + +Read the NURBS data of an edge's curve: what the curve edits work on. + +Returns ``{type, analytic, editable, conversion, degree, poles, weights, knots, multiplicities, periodic, rational, closed, domain, reversed, parameter_range}``. ``poles`` are points in the current coordinate system, indices are 0-based (the ``moves`` of ``move_curve_poles_redge``); ``parameter_range`` is the edge's own range (where ``insert_curve_knot_redge`` may insert). For a line, circle or other analytic curve ``editable`` is false, ``conversion`` is ``"convert_to_bspline_redge"`` and the data is what that conversion would produce. Not recorded in the graph. diff --git a/docs/skill/references/docs/api/inspect_sketch_rsketchcrossings.md b/docs/skill/references/docs/api/inspect_sketch_rsketchcrossings.md new file mode 100644 index 00000000..9c50672c --- /dev/null +++ b/docs/skill/references/docs/api/inspect_sketch_rsketchcrossings.md @@ -0,0 +1,19 @@ +# inspect_sketch_rsketchcrossings + +## API Definition + +```python +def inspect_sketch_rsketchcrossings(sketch) +``` + +*Source: operators/sketch.py* + +## Import Surface + +- top-level: `from simplecadapi import inspect_sketch_rsketchcrossings` + +## Description + +List where the non-construction lines, circles, arcs, ellipses and B-splines of the solved sketch cross, away from every point of the sketch. + +Returns ``[{"at": [x, y], "entities": [a, b]}]`` in sketch coordinates: the same crossings ``trim_rsketch`` cuts at (a tangent touch is one crossing). A crossing that is already a point of the sketch (a shared end) is not listed. A conflicting sketch is solved as far as it goes. Not recorded in the graph. diff --git a/docs/skill/references/docs/api/inspect_sketch_rsketchprofiles.md b/docs/skill/references/docs/api/inspect_sketch_rsketchprofiles.md new file mode 100644 index 00000000..11f95d78 --- /dev/null +++ b/docs/skill/references/docs/api/inspect_sketch_rsketchprofiles.md @@ -0,0 +1,19 @@ +# inspect_sketch_rsketchprofiles + +## API Definition + +```python +def inspect_sketch_rsketchprofiles(sketch, *, strict=True, tolerance=1e-07, max_iterations=80) +``` + +*Source: operators/sketch.py* + +## Import Surface + +- top-level: `from simplecadapi import inspect_sketch_rsketchprofiles` + +## Description + +List the profiles a sketch offers: the indices ``make_face_from_sketch_rface`` and ``make_wire_from_sketch_rwire`` take. + +Returns ``{"profiles": [...], "unusable": [...]}``. Each profile is ``{index, id, kind, closed, entity_ids, point_ids, reversed}``: ``kind`` is ``circle``, ``edge_loop`` or ``edge_chain``; ``entity_ids`` are in walking order and ``reversed`` says which edges are walked from end to start. ``unusable`` words why an edge set is no profile (a branch point carrying more than two edges, for one). A free circle is a profile; a loop is a connected set of edges whose points each carry exactly two. Not recorded in the graph; pass ``strict=False`` for a sketch that does not solve cleanly. diff --git a/docs/skill/references/docs/api/inspect_surface_rsurfacedata.md b/docs/skill/references/docs/api/inspect_surface_rsurfacedata.md new file mode 100644 index 00000000..b35605b0 --- /dev/null +++ b/docs/skill/references/docs/api/inspect_surface_rsurfacedata.md @@ -0,0 +1,19 @@ +# inspect_surface_rsurfacedata + +## API Definition + +```python +def inspect_surface_rsurfacedata(face) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import inspect_surface_rsurfacedata` + +## Description + +Read the NURBS data of a face's surface: what the surface edits work on. + +Returns ``{type, analytic, editable, conversion, u_degree, v_degree, poles, weights, u_knots, v_knots, u_multiplicities, v_multiplicities, u_periodic, v_periodic, rational, domain, reversed, trimmed, uv_bounds}``. ``poles`` is a grid of rows along U, each row running along V (as ``make_bezier_surface_rface`` takes it), in the current coordinate system; indices are 0-based ``(row, column)``. ``domain`` is the surface's ``[u1, u2, v1, v2]``, ``uv_bounds`` the face's; ``trimmed`` says whether the face is bounded by anything but the surface's own domain. For a plane, cylinder or other analytic surface ``editable`` is false, ``conversion`` is ``"convert_to_bspline_rface"`` and the data is what that conversion would produce. Not recorded in the graph. diff --git a/docs/skill/references/docs/api/list_tag_splits.md b/docs/skill/references/docs/api/list_tag_splits.md new file mode 100644 index 00000000..204a2b6c --- /dev/null +++ b/docs/skill/references/docs/api/list_tag_splits.md @@ -0,0 +1,45 @@ +# list_tag_splits + +## API Definition + +```python +def list_tag_splits(shape) +``` + +*Source: operators/selection.py* + +## Import Surface + +- top-level: `from simplecadapi import list_tag_splits` + +## Description + +The tagged entities that the operations behind a shape split into pieces. + +A tag follows its entity the way a reference does: while the entity stays one entity it keeps the tag; when an +operation splits it (a slot cut across a tagged face, a cut through a tagged body), the tag does not go to every +piece. Each piece carries ``.split1`` .. ``.split`` instead, numbered as the operation recorded them (by +centre, then size), and ``ql.tag("")`` finds none of them. Name the pieces you mean with tags of your own after the +split, or select them with ``ql.tag(".split1")`` / ``ql.tag(".*")``. + +## Parameters + +### shape + +- **Description**: A shape made in a recording session (its history is read). + +## Returns + +list[dict]: One row per split, upstream first: ``tag`` (the tag the entity had), ``into`` (the tags of the pieces, +in piece order), ``node_id`` and ``op`` (the operation that split it), ``source_topo_id`` and ``pieces`` +(``output_slot``, ``kind``, ``topo_id`` of each piece in that operation's output). Empty for a shape without +recorded history. + +## Examples + +```python +>>> top = scad.apply_tag_rselection(box, scad.ql.faces().where(scad.ql.prop("geom.center.z", ">=", 9.9)), "mount.top") +>>> halves = scad.cut_rsolid(top, slot) # the slot splits the top face (and the body) +>>> [row["into"] for row in scad.list_tag_splits(halves[0])] +[['mount.top.split1', 'mount.top.split2']] +``` diff --git a/docs/skill/references/docs/api/make_cylindrical_surface_rface.md b/docs/skill/references/docs/api/make_cylindrical_surface_rface.md index c77fa59d..4c92040a 100644 --- a/docs/skill/references/docs/api/make_cylindrical_surface_rface.md +++ b/docs/skill/references/docs/api/make_cylindrical_surface_rface.md @@ -3,7 +3,7 @@ ## API Definition ```python -def make_cylindrical_surface_rface(radius, u_range, v_range, origin=[0.0, 0.0, 0.0], axis=[0.0, 0.0, 1.0], x_direction=None, *, tolerance=1e-07, tag_prefix=None) +def make_cylindrical_surface_rface(radius, u_range, v_range, origin=(0.0, 0.0, 0.0), axis=(0.0, 0.0, 1.0), x_direction=None, *, tolerance=1e-07, tag_prefix=None) ``` *Source: operators/geometry.py* diff --git a/docs/skill/references/docs/api/make_faces_from_sketch_rfacelist.md b/docs/skill/references/docs/api/make_faces_from_sketch_rfacelist.md new file mode 100644 index 00000000..333ea633 --- /dev/null +++ b/docs/skill/references/docs/api/make_faces_from_sketch_rfacelist.md @@ -0,0 +1,28 @@ +# make_faces_from_sketch_rfacelist + +## API Definition + +```python +def make_faces_from_sketch_rfacelist(sketch, profiles=None, *, require_fully_constrained=False, strict=True, tolerance=1e-07, max_iterations=80) +``` + +*Source: operators/sketch.py* + +## Import Surface + +- top-level: `from simplecadapi import make_faces_from_sketch_rfacelist` + +## Description + +Make one face per region of the sketch: a closed outer loop minus the loops directly inside it. + +Holes come by themselves: a loop inside an outer loop is a hole of that region, and a loop inside a hole is the +outer loop of another region (``inspect_sketch_rsketchprofiles`` reports each closed profile's ``parent`` and +``depth`` and the ``regions`` as ``{"outer": i, "holes": [...]}``). ``profiles`` names the regions to make by their +outer profile (an index of ``inspect_sketch_rsketchprofiles`` or a profile id); leave it out for every region, in +profile order. An open chain, a hole or an unknown profile is refused by name. + +Returns the faces as ``scad.Pieces`` (a list) in that order: a one-shape operation such as ``extrude_rsolid`` runs on +every face and returns ``Pieces`` (one solid per region; ``union_rsolid`` joins them, ``make_part_rpart`` makes them +bodies). Each face is recorded as ``make_face_from_sketch_rface(sketch, outer, inner_profiles=holes)`` and replays +from model JSON. diff --git a/docs/skill/references/docs/api/make_part_rpart.md b/docs/skill/references/docs/api/make_part_rpart.md index 99d53ff1..719955ec 100644 --- a/docs/skill/references/docs/api/make_part_rpart.md +++ b/docs/skill/references/docs/api/make_part_rpart.md @@ -3,7 +3,7 @@ ## API Definition ```python -def make_part_rpart(part_id, body, name=None) +def make_part_rpart(part_id, body=None, name=None, bodies=None) ``` *Source: operators/product.py* @@ -14,4 +14,41 @@ def make_part_rpart(part_id, body, name=None) ## Description -Wrap exactly one Solid as a semantic single-body Part. +Wrap one Solid, or several named solids, as a semantic Part. + +``scad.make_part_rpart(part_id=scad.notebook_id(), body=bracket)`` for one solid; +``scad.make_part_rpart(part_id=scad.notebook_id(), bodies={"case": case, "lid": lid})`` +for a case and its lid kept as two bodies of one part. Material applies to every +body. Connectors are not supported on multi-body parts yet. Union the solids +first when they must become one body. + +## Parameters + +### part_id + +- **Description**: Stable identifier of the part (``scad.notebook_id()`` in a notebook). + +### body + +- **Description**: The Solid of a one-body Part. The pieces of a split cut (a list of Solids) become the bodies ``body_1 .. body_n``. + +### name + +- **Description**: Optional display name. + +### bodies + +- **Description**: Instead of ``body``: several solids kept apart in one Part, as a mapping ``body_id -> Solid`` (in its order) or a sequence of ``Body`` values. A mapping value may be the pieces of a split cut (a list of + +### Solids) + +- **Description**: they become ``_1 .. _n``. Body ids are identifiers, unique within the part. Overlapping bodies are allowed; building the definition warns (``BodyOverlapWarning``). + +## Returns + +Part: The new Part. ``part.bodies`` lists its bodies; ``part.body`` is the +Solid of a one-body Part. + +## Raises + +- **SimpleCADError**: Neither or both of ``body`` and ``bodies`` were given, a body is not a Solid, a body id is malformed or repeated, or the part_id is already used. diff --git a/docs/skill/references/docs/api/make_sketch_rsketch.md b/docs/skill/references/docs/api/make_sketch_rsketch.md index 9afa8220..a0527740 100644 --- a/docs/skill/references/docs/api/make_sketch_rsketch.md +++ b/docs/skill/references/docs/api/make_sketch_rsketch.md @@ -18,3 +18,8 @@ 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. + +``plane`` is ``"XY"``, ``"XZ"``, ``"YZ"`` or a frame mapping ``{origin, x_axis, y_axis}`` (read in the current +coordinate system), or a Workplane value (``make_workplane_*_rworkplane`` / ``SimpleWorkplane``), whose world frame is used +as it is. A sketch on a recorded workplane names its node: a replay takes the frame from the replayed workplane, so a +sketch on a face's workplane follows the face. diff --git a/docs/skill/references/docs/api/make_workplane_from_axis_rworkplane.md b/docs/skill/references/docs/api/make_workplane_from_axis_rworkplane.md new file mode 100644 index 00000000..47d9e5c0 --- /dev/null +++ b/docs/skill/references/docs/api/make_workplane_from_axis_rworkplane.md @@ -0,0 +1,52 @@ +# make_workplane_from_axis_rworkplane + +## API Definition + +```python +def make_workplane_from_axis_rworkplane(axis, angle=0.0, *, face=None) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import make_workplane_from_axis_rworkplane` + +## Description + +Create a workplane through an axis, turned about it by an angle. + +The axis is a straight edge (from its start, along it) or a circular edge (its centre line: through the centre, +along the circle's normal). x = the axis direction. At ``angle=0`` the plane is the plane of ``face`` (a planar face +containing the axis direction, usually one the edge bounds) or, without one, the plane whose normal is global Z +(global X for an axis along Z) made square to the axis; ``angle`` turns it right-handed about the axis. + +Recorded: the node takes the inputs as ordered references, so a model-JSON replay builds the frame again from the +geometry as it is then, and a sketch on the workplane (``plane=wp``) or an operation inside ``with wp:`` follows it. + +``scad.make_workplane_from_axis_rworkplane(edge, 30, face=top)``: a plane hinged on an edge of the top +face, tilted 30 degrees. + +## Parameters + +### axis + +- **Description**: A straight or circular Edge. + +### angle + +- **Description**: The turn about the axis, in degrees. + +### face + +- **Description**: The planar Face the plane starts from at angle 0. + +## Returns + +SimpleWorkplane: a frame in world coordinates (``origin``, ``normal``, ``x_dir``, ``y_dir``, ``plane``) that is +also a context manager: ``with wp:`` makes it the current coordinate system, ``make_sketch_rsketch(plane=wp)`` +draws on it. It is a value: enter it again in later code, pickle it (the notebook cell cache does). + +## Raises + +- **SimpleCADError**: The edge is neither straight nor circular, the face is not planar, or the face's normal is along the axis. diff --git a/docs/skill/references/docs/api/make_workplane_from_face_rworkplane.md b/docs/skill/references/docs/api/make_workplane_from_face_rworkplane.md new file mode 100644 index 00000000..9b81563a --- /dev/null +++ b/docs/skill/references/docs/api/make_workplane_from_face_rworkplane.md @@ -0,0 +1,64 @@ +# make_workplane_from_face_rworkplane + +## API Definition + +```python +def make_workplane_from_face_rworkplane(face, *, origin=None, x_edge=None, x_dir=None, offset=0.0, flip=False) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import make_workplane_from_face_rworkplane` + +## Description + +Create a workplane on a planar face of the model. + +normal = the face's outward normal (``flip=True`` reverses it); origin = the face's centre, or ``origin`` +projected onto the face's plane; x = along ``x_edge`` (its direction at its start, projected onto the plane), or +along ``x_dir`` (projected), or the x direction of the face's own plane. ``offset`` then moves the origin along +the normal (an offset datum plane). + +Recorded: the node takes the inputs as ordered references, so a model-JSON replay builds the frame again from the +geometry as it is then, and a sketch on the workplane (``plane=wp``) or an operation inside ``with wp:`` follows it. + +``top = scad.ql.faces().where(scad.ql.tag("base.face.top")).exactly(1).resolve(base)[0]``; +``wp = scad.make_workplane_from_face_rworkplane(top, offset=2)``. + +## Parameters + +### face + +- **Description**: A planar Face (picked with ``ql``, ``get_faces(i)`` or a tag). + +### origin + +- **Description**: A point (current coordinate system) projected onto the face; ``None`` = the face's centre. + +### x_edge + +- **Description**: An Edge giving the x direction (usually an edge of the face). + +### x_dir + +- **Description**: A direction giving x; give ``x_edge`` or ``x_dir``, not both. + +### offset + +- **Description**: Distance along the normal from the face (mm). + +### flip + +- **Description**: Reverse the normal (draw from inside the body). + +## Returns + +SimpleWorkplane: a frame in world coordinates (``origin``, ``normal``, ``x_dir``, ``y_dir``, ``plane``) that is +also a context manager: ``with wp:`` makes it the current coordinate system, ``make_sketch_rsketch(plane=wp)`` +draws on it. It is a value: enter it again in later code, pickle it (the notebook cell cache does). + +## Raises + +- **SimpleCADError**: The face is not planar, both ``x_edge`` and ``x_dir`` were given, or the x direction is normal to the face. diff --git a/docs/skill/references/docs/api/make_workplane_from_points_rworkplane.md b/docs/skill/references/docs/api/make_workplane_from_points_rworkplane.md new file mode 100644 index 00000000..a0d5ac9d --- /dev/null +++ b/docs/skill/references/docs/api/make_workplane_from_points_rworkplane.md @@ -0,0 +1,47 @@ +# make_workplane_from_points_rworkplane + +## API Definition + +```python +def make_workplane_from_points_rworkplane(a, b, c) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import make_workplane_from_points_rworkplane` + +## Description + +Create a workplane through three points: origin at ``a``, x towards ``b``, normal = (b - a) x (c - a). + +Recorded: the node takes the inputs as ordered references, so a model-JSON replay builds the frame again from the +geometry as it is then, and a sketch on the workplane (``plane=wp``) or an operation inside ``with wp:`` follows it. + +``scad.make_workplane_from_points_rworkplane(v0, v1, v2)`` with three picked vertices; points given as +numbers are recorded as numbers. + +## Parameters + +### a + +- **Description**: A Vertex of the model or a point (current coordinate system): the origin. + +### b + +- **Description**: A Vertex or a point: x points from ``a`` towards it. + +### c + +- **Description**: A Vertex or a point on the positive-y side of the plane. + +## Returns + +SimpleWorkplane: a frame in world coordinates (``origin``, ``normal``, ``x_dir``, ``y_dir``, ``plane``) that is +also a context manager: ``with wp:`` makes it the current coordinate system, ``make_sketch_rsketch(plane=wp)`` +draws on it. It is a value: enter it again in later code, pickle it (the notebook cell cache does). + +## Raises + +- **SimpleCADError**: The three points lie on one line or two coincide. diff --git a/docs/skill/references/docs/api/make_workplane_midplane_rworkplane.md b/docs/skill/references/docs/api/make_workplane_midplane_rworkplane.md new file mode 100644 index 00000000..6c0e5312 --- /dev/null +++ b/docs/skill/references/docs/api/make_workplane_midplane_rworkplane.md @@ -0,0 +1,45 @@ +# make_workplane_midplane_rworkplane + +## API Definition + +```python +def make_workplane_midplane_rworkplane(face_a, face_b) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import make_workplane_midplane_rworkplane` + +## Description + +Create the workplane halfway between two parallel planar faces (a symmetry plane). + +normal and x come from ``face_a``; origin = the middle between the centre of ``face_a`` and its projection onto the +plane of ``face_b``. + +Recorded: the node takes the inputs as ordered references, so a model-JSON replay builds the frame again from the +geometry as it is then, and a sketch on the workplane (``plane=wp``) or an operation inside ``with wp:`` follows it. + +``mid = scad.make_workplane_midplane_rworkplane(left_wall, right_wall)``; mirror or sketch on it. + +## Parameters + +### face_a + +- **Description**: A planar Face; it gives the normal and x. + +### face_b + +- **Description**: A planar Face parallel to it. + +## Returns + +SimpleWorkplane: a frame in world coordinates (``origin``, ``normal``, ``x_dir``, ``y_dir``, ``plane``) that is +also a context manager: ``with wp:`` makes it the current coordinate system, ``make_sketch_rsketch(plane=wp)`` +draws on it. It is a value: enter it again in later code, pickle it (the notebook cell cache does). + +## Raises + +- **SimpleCADError**: A face is not planar or the faces are not parallel. diff --git a/docs/skill/references/docs/api/make_workplane_normal_to_curve_rworkplane.md b/docs/skill/references/docs/api/make_workplane_normal_to_curve_rworkplane.md new file mode 100644 index 00000000..daf336b1 --- /dev/null +++ b/docs/skill/references/docs/api/make_workplane_normal_to_curve_rworkplane.md @@ -0,0 +1,51 @@ +# make_workplane_normal_to_curve_rworkplane + +## API Definition + +```python +def make_workplane_normal_to_curve_rworkplane(curve, at=0.0, *, x_dir=None) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import make_workplane_normal_to_curve_rworkplane` + +## Description + +Create a workplane normal to a curve: the plane of a sweep profile. + +origin = the point at ``at`` (a fraction of the curve's length: 0 = start, 1 = end); normal = the direction of +travel there; x = ``x_dir`` projected onto the plane, or global Z made square to the normal (global X when the +tangent is near Z). + +Recorded: the node takes the inputs as ordered references, so a model-JSON replay builds the frame again from the +geometry as it is then, and a sketch on the workplane (``plane=wp``) or an operation inside ``with wp:`` follows it. + +``start = scad.make_workplane_normal_to_curve_rworkplane(path, 0)``; +``profile = ... make_sketch_rsketch("profile", plane=start) ...``; ``sweep_rsolid(profile, path)``. + +## Parameters + +### curve + +- **Description**: An Edge or a Wire (the sweep path). + +### at + +- **Description**: The fraction of the length, in [0, 1]. + +### x_dir + +- **Description**: The direction of x, projected onto the plane. + +## Returns + +SimpleWorkplane: a frame in world coordinates (``origin``, ``normal``, ``x_dir``, ``y_dir``, ``plane``) that is +also a context manager: ``with wp:`` makes it the current coordinate system, ``make_sketch_rsketch(plane=wp)`` +draws on it. It is a value: enter it again in later code, pickle it (the notebook cell cache does). + +## Raises + +- **SimpleCADError**: ``at`` is outside [0, 1], the curve has no length, or ``x_dir`` is along the tangent. diff --git a/docs/skill/references/docs/api/make_workplane_offset_rworkplane.md b/docs/skill/references/docs/api/make_workplane_offset_rworkplane.md new file mode 100644 index 00000000..0cfecad4 --- /dev/null +++ b/docs/skill/references/docs/api/make_workplane_offset_rworkplane.md @@ -0,0 +1,44 @@ +# make_workplane_offset_rworkplane + +## API Definition + +```python +def make_workplane_offset_rworkplane(base='XY', offset=(0.0, 0.0, 0.0), rx=0.0, ry=0.0, rz=0.0) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import make_workplane_offset_rworkplane` + +## Description + +Create a workplane moved and turned relative to a workplane or a named plane. + +The editor's move and turn handles write this operation: a plane placed by hand on a base, without any model geometry. + +## Parameters + +### base + +- **Description**: The plane it is placed on: a workplane value (made by a ``make_workplane_*_rworkplane`` operation, recorded as this node's input so the new plane follows it when it moves) or a named plane ``"XY"`` / ``"XZ"`` / ``"YZ"`` (axes as sketches read them). + +### offset + +- **Description**: How far the origin moves, ``(dx, dy, dz)`` along the base's own x, y and normal. + +### rx, ry, rz + +- **Description**: Turns in degrees about the base's own axes, applied in that order, each about the axis as the previous turn left it (R = Rx(rx) Ry(ry) Rz(rz) in the base's frame). + +## Returns + +SimpleWorkplane: The new frame. Use it as a sketch plane (``plane=wp``) or as a context (``with wp:``). + +## Examples + +```python +>>> wp = scad.make_workplane_offset_rworkplane("XY", offset=(0, 0, 20), rx=30) +>>> lid = scad.make_workplane_offset_rworkplane(wp, offset=(0, 0, 5)) # follows wp +``` diff --git a/docs/skill/references/docs/api/make_workplane_rworkplane.md b/docs/skill/references/docs/api/make_workplane_rworkplane.md new file mode 100644 index 00000000..7fd749f5 --- /dev/null +++ b/docs/skill/references/docs/api/make_workplane_rworkplane.md @@ -0,0 +1,48 @@ +# make_workplane_rworkplane + +## API Definition + +```python +def make_workplane_rworkplane(origin=(0, 0, 0), normal=(0, 0, 1), x_dir=(1, 0, 0)) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import make_workplane_rworkplane` + +## Description + +Create a recorded workplane from an origin, a normal and an x direction. + +The recorded spelling of ``SimpleWorkplane(origin, normal, x_dir)``: the three vectors are read in the current +coordinate system (inside ``with other_wp:`` they are local to it). An ``x_dir`` along the normal is replaced by a +perpendicular; otherwise x is ``x_dir`` made square to the normal. + +``wp = scad.make_workplane_rworkplane((0, 0, 40), (0, 1, 0))`` then ``with wp: ...`` or +``scad.make_sketch_rsketch("rib", plane=wp)``. + +## Parameters + +### origin + +- **Description**: The origin of the plane. + +### normal + +- **Description**: The plane's normal (its local z axis); its length does not matter. + +### x_dir + +- **Description**: The direction of the local x axis, projected onto the plane. + +## Returns + +SimpleWorkplane: a frame in world coordinates (``origin``, ``normal``, ``x_dir``, ``y_dir``, ``plane``) that is +also a context manager: ``with wp:`` makes it the current coordinate system, ``make_sketch_rsketch(plane=wp)`` +draws on it. It is a value: enter it again in later code, pickle it (the notebook cell cache does). + +## Raises + +- **SimpleCADError**: The normal is zero or a value is not finite. diff --git a/docs/skill/references/docs/api/measure_part_mass_rmassreport.md b/docs/skill/references/docs/api/measure_part_mass_rmassreport.md new file mode 100644 index 00000000..5068c3fa --- /dev/null +++ b/docs/skill/references/docs/api/measure_part_mass_rmassreport.md @@ -0,0 +1,45 @@ +# measure_part_mass_rmassreport + +## API Definition + +```python +def measure_part_mass_rmassreport(part: 'Part', *, density: 'float | None' = None, density_unit: 'str | None' = None) -> 'MassReport' +``` + +*Source: operators/product.py* + +## Import Surface + +- top-level: `from simplecadapi import measure_part_mass_rmassreport` + +## Description + +Measure the volume, mass and centre of mass of every body of a Part, and of the Part as a whole. + +``scad.measure_part_mass_rmassreport(case).body("lid").mass_kg`` is the mass of +the lid body; ``.mass_kg`` the mass of the whole part. Every body has the +Part's material. + +## Parameters + +### part + +- **Description**: The Part to measure (one body or several). + +### density + +- **Description**: Density to use instead of the material's; ``None`` uses the Part's material, and without a material density every mass is ``None``. + +### density_unit + +- **Description**: Unit of ``density``: ``kg/m^3`` (default), ``g/cm^3``, ``kg/mm^3`` or ``g/mm^3``. + +## Returns + +MassReport: ``bodies`` (one ``BodyMass`` per body, in body order: ``body_id``, +``volume_mm3``, ``mass_kg``, ``centroid_mm``) and the totals of the part +(``volume_mm3``, ``mass_kg``, volume-weighted ``centroid_mm``). + +## Raises + +- **SimpleCADError**: The value is not a Part, or the density or its unit is invalid. diff --git a/docs/skill/references/docs/api/move_curve_poles_redge.md b/docs/skill/references/docs/api/move_curve_poles_redge.md new file mode 100644 index 00000000..96971d72 --- /dev/null +++ b/docs/skill/references/docs/api/move_curve_poles_redge.md @@ -0,0 +1,19 @@ +# move_curve_poles_redge + +## API Definition + +```python +def move_curve_poles_redge(edge, moves, *, tag_prefix=None) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import move_curve_poles_redge` + +## Description + +Move some poles of an edge's B-spline curve: ``moves`` maps a 0-based pole index to its new position. + +The sparse form of ``set_curve_poles_redge`` (what a control-point drag records). Only B-spline and Bezier curves are edited; a line, circle or other analytic edge is refused with a message naming ``convert_to_bspline_redge``, the explicit conversion step. Returns a new Edge; the input is not changed. Recorded: the Edge continues in the node's history, so tags and references follow. diff --git a/docs/skill/references/docs/api/move_surface_poles_rface.md b/docs/skill/references/docs/api/move_surface_poles_rface.md new file mode 100644 index 00000000..96b26021 --- /dev/null +++ b/docs/skill/references/docs/api/move_surface_poles_rface.md @@ -0,0 +1,19 @@ +# move_surface_poles_rface + +## API Definition + +```python +def move_surface_poles_rface(face, moves, *, tag_prefix=None) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import move_surface_poles_rface` + +## Description + +Move some poles of a face's B-spline surface: ``moves`` maps a 0-based ``(row, column)`` to the pole's new position. + +The sparse form of ``set_surface_poles_rface`` (what a control-point drag records). The face keeps its trimming exactly: its boundary loops stay where they are in the surface's (u, v) space and the 3D boundary edges are recomputed on the edited surface. Only B-spline and Bezier surfaces are edited; a plane, cylinder or other analytic face is refused with a message naming ``convert_to_bspline_rface``, the explicit conversion step. Returns a new Face; the input is not changed. Recorded: the Face and its boundary edges continue in the node's history, so tags and references follow. diff --git a/docs/skill/references/docs/api/offset_rsolid.md b/docs/skill/references/docs/api/offset_rsolid.md index 1136a851..d56d89a4 100644 --- a/docs/skill/references/docs/api/offset_rsolid.md +++ b/docs/skill/references/docs/api/offset_rsolid.md @@ -20,7 +20,9 @@ The amount must be finite and nonzero. Faces passed as ``openings`` form an open shell; with no openings the result is the closed offset solid. The join ``kind`` is ``ARC`` or ``INTERSECTION``. Invalid kernel results raise an error. The operation is recorded as ``make_offset_rsolid`` with explicit -face selections; topology tag lineage is conservatively partial. +face selections. The recorded node carries the kernel's face and edge history, so element +references follow faces through it (an offset face continues its source +face); tag lineage on the result is conservatively partial. ``shell_rsolid`` keeps its own contract (a positive inward wall thickness); this one supports both signs. diff --git a/docs/skill/references/docs/api/press_pull_rsolid.md b/docs/skill/references/docs/api/press_pull_rsolid.md new file mode 100644 index 00000000..2dd709a3 --- /dev/null +++ b/docs/skill/references/docs/api/press_pull_rsolid.md @@ -0,0 +1,67 @@ +# press_pull_rsolid + +## API Definition + +```python +def press_pull_rsolid(solid, face, distance) +``` + +*Source: operators/features.py* + +## Import Surface + +- top-level: `from simplecadapi import press_pull_rsolid` + +## Description + +Pull one planar face out along its normal, or press it in. + +A positive ``distance`` pulls the face out: the prism of the face along its +outward normal is joined to the solid (a boss, a step). A negative one presses +it in: the prism into the solid is cut (a pocket, a recess; through the solid it +cuts a hole). Zero leaves the solid as it is. One signed number covers both, so +a parameter can be tuned across zero without changing the code. + +Pair it with ``inset_face_rsolid``: inset a face, then press or pull its inner +face, to build steps, bosses and walls without a sketch. + +The operation is recorded as its steps: ``make_extrude_rsolid`` of the face, +then ``make_union_rsolid`` (pull) or ``make_cut_rsolid`` (press), so the new +faces carry the extrusion's roles (``extrusion.end`` is the moved face, +``extrusion.side`` its walls) and element references follow them. + +## Parameters + +### solid + +- **Description**: The body whose face is pressed or pulled. + +### face + +- **Description**: One planar face of ``solid``, or a QL selector that picks exactly one. + +### distance + +- **Description**: How far, in mm, along the face's outward normal: positive pulls, negative presses. + +## Returns + +The solid with the face pulled out or pressed in. + +## Raises + +- **TypeError**: ``solid`` is not a Solid. +- **ValueError**: Not exactly one face, a face of another solid, a curved face, a non-finite distance, or a press that removes the whole solid. + +## Examples + +```python +import simplecadapi as scad +ql = scad.ql +box = scad.make_box_rsolid(40, 30, 20) +top = ql.faces().where(ql.prop("geom.center.z", ">=", 19.9)).exactly(1) +stepped = scad.inset_face_rsolid(box, top, 3) +inner = scad.ref.faces(scad.ref.face("", "make_inset_face_rsolid", role="inset.inner")) +boss = scad.press_pull_rsolid(stepped, inner, 10) +pocket = scad.press_pull_rsolid(stepped, inner, -4) +``` diff --git a/docs/skill/references/docs/api/project_rsketch.md b/docs/skill/references/docs/api/project_rsketch.md new file mode 100644 index 00000000..cb439cd3 --- /dev/null +++ b/docs/skill/references/docs/api/project_rsketch.md @@ -0,0 +1,31 @@ +# project_rsketch + +## API Definition + +```python +def project_rsketch(sketch, source, *, entities=None, construction=True) +``` + +*Source: operators/sketch.py* + +## Import Surface + +- top-level: `from simplecadapi import project_rsketch` + +## Description + +Bring geometry from another sketch, or from the edges of shapes, into the sketch as reference geometry. + +``source`` is a Sketch (any plane; it is solved first), a Solid / Face / Shell / Wire / Edge / Compound or a list +of shapes (what ``ql.edges().resolve(shape)`` returns). Points go through the plane frames: u = (P - origin) . +x_axis, v = (P - origin) . y_axis. Points, lines, circles and arcs in a parallel plane are copied (an arc keeps its +counter-clockwise sense); a circle or arc seen edge on becomes a line, a line along the sketch normal a point, an +oblique full circle an ellipse; an oblique arc is refused by name, and so is an edge that is not a line or a circle when +it is passed on its own (an Edge); inside a face, wire, shell, solid or compound such an edge is skipped and noted, so +``project_rsketch(sketch, face)`` brings the face's boundary in (its lines and arcs) for a sketch on that face to snap +and constrain to; in a cell the face comes from the model, so the projection follows it. +``entities`` limits the source sketch entities (default: all lines, circles, arcs and free points; ellipses and +B-splines are skipped and listed in ``get_metadata("last_edit")``); it is not used with shapes. The copies are +``construction`` (default True) and fixed at their projected coordinates. Ids are ``proj__`` +(``proj_shape_`` for shapes); ``get_metadata("projections")`` lists {source, entity, projected} and projecting +the same source again reuses the entities already there. diff --git a/docs/skill/references/docs/api/ql.ref.md b/docs/skill/references/docs/api/ql.ref.md new file mode 100644 index 00000000..8da8b987 --- /dev/null +++ b/docs/skill/references/docs/api/ql.ref.md @@ -0,0 +1,38 @@ +# ql.ref + +## API Definition + +```python +def ql.ref(reference: 'Union[ElementRef, Mapping[str, Any], str]') -> 'Any' +``` + +*Source: ql.py* + +## Import Surface + +- submodule: `from simplecadapi.ql import ref` or `simplecadapi.ql.ref` + +## Description + +A stable reference as a ``ql`` predicate (``scad.ql.ref``): keeps the entities it names in the scope. + +The reference is resolved through the recorded history of the shape the selector is resolved against, once per +query run; a split reference (without ``piece``) or a lost one raises ``ElementRefError`` there, it never +matches nothing silently. The predicate stores the reference as data, so the selector serialises and replays. + +## Parameters + +### reference + +- **Description**: An :class:`ElementRef`, its record (``to_dict()``) or its code (``'scad.ref.face(...)'``). + +## Returns + +SerializablePredicate: Use it in ``where(...)`` of a selector resolved against one shape. + +## Examples + +```python +>>> top = scad.ref.face("base", "make_box_rsolid", role="box.top") +>>> scad.ql.faces().where(scad.ql.ref(top)).exactly(1).resolve(part) +``` diff --git a/docs/skill/references/docs/api/rebuild_curve_redge.md b/docs/skill/references/docs/api/rebuild_curve_redge.md new file mode 100644 index 00000000..474ebb03 --- /dev/null +++ b/docs/skill/references/docs/api/rebuild_curve_redge.md @@ -0,0 +1,19 @@ +# rebuild_curve_redge + +## API Definition + +```python +def rebuild_curve_redge(edge, degree=3, pole_count=None, *, tolerance=0.001, tag_prefix=None) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import rebuild_curve_redge` + +## Description + +Refit an edge's curve as a B-spline of ``degree`` with ``pole_count`` poles, or with the fewest poles that keep within ``tolerance``. + +A least-squares fit with uniform clamped knots in the edge's own parameter range; the end points are kept. Works on any edge, analytic ones too (the result is a B-spline). Returns a new Edge; the input is not changed. Recorded: the Edge continues in the node's history, so tags and references follow. diff --git a/docs/skill/references/docs/api/rebuild_surface_rface.md b/docs/skill/references/docs/api/rebuild_surface_rface.md new file mode 100644 index 00000000..bdf914d9 --- /dev/null +++ b/docs/skill/references/docs/api/rebuild_surface_rface.md @@ -0,0 +1,19 @@ +# rebuild_surface_rface + +## API Definition + +```python +def rebuild_surface_rface(face, u_degree=3, v_degree=3, u_pole_count=None, v_pole_count=None, *, tolerance=0.001, tag_prefix=None) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import rebuild_surface_rface` + +## Description + +Refit a face's surface as a B-spline of the given degrees and pole counts, or with the fewest poles that keep within ``tolerance``. + +Give both pole counts or neither. A least-squares fit in the face's own (u, v) parametrisation (over the surface's domain for a B-spline, over the face's bounds otherwise), so the face keeps its trimming. Works on any face. Returns a new Face; the input is not changed. Recorded: the Face and its boundary edges continue in the node's history, so tags and references follow. diff --git a/docs/skill/references/docs/api/ref.coerce.md b/docs/skill/references/docs/api/ref.coerce.md new file mode 100644 index 00000000..132fc828 --- /dev/null +++ b/docs/skill/references/docs/api/ref.coerce.md @@ -0,0 +1,17 @@ +# ref.coerce + +## API Definition + +```python +def ref.coerce(reference: Any) -> ElementRef +``` + +*Source: ref.py* + +## Import Surface + +- stable references: `import simplecadapi as scad` then `scad.ref.coerce(...)` + +## Description + +An :class:`ElementRef` from a reference, its record (:meth:`ElementRef.to_dict`) or its code. diff --git a/docs/skill/references/docs/api/ref.edge.md b/docs/skill/references/docs/api/ref.edge.md new file mode 100644 index 00000000..bd71db19 --- /dev/null +++ b/docs/skill/references/docs/api/ref.edge.md @@ -0,0 +1,31 @@ +# ref.edge + +## API Definition + +```python +def ref.edge(feature: Optional[str] = None, op: Optional[str] = None, *, role: Optional[str] = None, ordinal: int = 0, topo: Optional[str] = None, between: Optional[Tuple[Union[str, ElementRef], Union[str, ElementRef]]] = None, occurrence: int = 0, slot: int = 0, piece: Optional[int] = None, follow: Follow = 'identity', guard: Optional[Guard] = None, digest: Optional[str] = None, naming: int = 1) -> ElementRef +``` + +*Source: ref.py* + +## Import Surface + +- stable references: `import simplecadapi as scad` then `scad.ref.edge(...)` + +## Description + +A reference to an edge: born in *feature* by *op*, or lying ``between`` two faces. + +An edge is best named by the two faces it lies between: the faces' history is recorded by every operation, so the +edge is found again after booleans, blends and copies, and the name reads as what it is. ``between`` holds two +role names of faces made by *op* in *feature* (``("box.top", "box.front")``), or two face references (then +*feature* and *op* may be left out). The other arguments are :func:`face`'s (``guard`` size = length). + +## Examples + +```python +>>> rim = scad.ref.edge("base", "make_box_rsolid", between=("box.top", "box.front")) +>>> lip = scad.ref.edge(between=(scad.ref.face("base", "make_box_rsolid", role="box.top"), +... scad.ref.face("bore", "make_cylinder_rsolid", role="cylinder.side"))) +>>> rounded = scad.fillet_rsolid(part, scad.ref.edges(rim, lip), 1.0) +``` diff --git a/docs/skill/references/docs/api/ref.edges.md b/docs/skill/references/docs/api/ref.edges.md new file mode 100644 index 00000000..bd2351ae --- /dev/null +++ b/docs/skill/references/docs/api/ref.edges.md @@ -0,0 +1,24 @@ +# ref.edges + +## API Definition + +```python +def ref.edges(*refs: Union[ElementRef, Sequence[ElementRef]]) -> Any +``` + +*Source: ref.py* + +## Import Surface + +- stable references: `import simplecadapi as scad` then `scad.ref.edges(...)` + +## Description + +The selection of the edges the references name (see :func:`faces`). + +## Examples + +```python +>>> rims = scad.ref.edges(scad.ref.edge("base", "make_box_rsolid", topo="edge_1", guard=("LINE", (-20, 0, 10), 20))) +>>> rounded = scad.fillet_rsolid(base, rims, 1.0) +``` diff --git a/docs/skill/references/docs/api/ref.face.md b/docs/skill/references/docs/api/ref.face.md new file mode 100644 index 00000000..004c4f94 --- /dev/null +++ b/docs/skill/references/docs/api/ref.face.md @@ -0,0 +1,78 @@ +# ref.face + +## API Definition + +```python +def ref.face(feature: str, op: str, *, role: Optional[str] = None, ordinal: int = 0, topo: Optional[str] = None, occurrence: int = 0, slot: int = 0, piece: Optional[int] = None, follow: Follow = 'identity', guard: Optional[Guard] = None, digest: Optional[str] = None, naming: int = 1) -> ElementRef +``` + +*Source: ref.py* + +## Import Surface + +- stable references: `import simplecadapi as scad` then `scad.ref.face(...)` + +## Description + +A reference to a face born in *feature* by *op*. + +## Parameters + +### feature + +- **Description**: The feature (cell) the face was made in: the editor's feature slug, or the cell's namespace. + +### op + +- **Description**: The operation that made it, as recorded (``"make_box_rsolid"``, ``"make_extrude_rsolid"``). + +### role + +- **Description**: Its kernel role (``"box.top"``, ``"extrusion.end"``); ``ordinal`` says which when several share it. + +### ordinal + +- **Description**: Which of the op's faces with that role (0 first). + +### topo + +- **Description**: For a face without a role: its position in the op's output (``"face_5"``); give a ``guard`` with it. + +### occurrence + +- **Description**: Which of the feature's *op* operations feeding the shape (0: the first). + +### slot + +- **Description**: The output of that operation (a multi-body result has one per body). + +### piece + +- **Description**: The chosen piece once the face has been split (in the order the split error lists them). + +### follow + +- **Description**: ``"identity"``; ``"derived"`` also follows into what the face generated. + +### guard + +- **Description**: ``(geometry type, (x, y, z), area[, normal])`` of the face when it was picked. + +### digest + +- **Description**: The operation's parameter digest, only when two operations share its name in the feature. + +### naming + +- **Description**: The naming version the reference was made under (1). + +## Returns + +ElementRef: The reference; resolve it with ``.one(shape)`` / ``.all(shape)`` / ``.resolve(shape)``. + +## Examples + +```python +>>> top = scad.ref.face("base", "make_box_rsolid", role="box.top") +>>> top.one(part).get_area() +``` diff --git a/docs/skill/references/docs/api/ref.faces.md b/docs/skill/references/docs/api/ref.faces.md new file mode 100644 index 00000000..1c95c7fe --- /dev/null +++ b/docs/skill/references/docs/api/ref.faces.md @@ -0,0 +1,28 @@ +# ref.faces + +## API Definition + +```python +def ref.faces(*refs: Union[ElementRef, Sequence[ElementRef]]) -> Any +``` + +*Source: ref.py* + +## Import Surface + +- stable references: `import simplecadapi as scad` then `scad.ref.faces(...)` + +## Description + +The selection of the faces the references name: ``ql.faces().where(ql.ref(r1) | ql.ref(r2) ...)``. + +Pass it where an operation takes faces (``shell_rsolid(faces_to_remove=...)``, ``apply_tag_rselection``): the +operation resolves it in the shape it receives, and each reference raises ``ElementRefError`` there if it is split +(without ``piece``) or lost. + +## Examples + +```python +>>> opening = scad.ref.faces(scad.ref.face("base", "make_box_rsolid", role="box.top")) +>>> cup = scad.shell_rsolid(base, faces_to_remove=opening, thickness=1.0) +``` diff --git a/docs/skill/references/docs/api/ref.of.md b/docs/skill/references/docs/api/ref.of.md new file mode 100644 index 00000000..40e376ad --- /dev/null +++ b/docs/skill/references/docs/api/ref.of.md @@ -0,0 +1,47 @@ +# ref.of + +## API Definition + +```python +def ref.of(entity: Any, shape: Any, *, follow: Follow = 'identity', guard: bool = True) -> ElementRef +``` + +*Source: ref.py* + +## Import Surface + +- stable references: `import simplecadapi as scad` then `scad.ref.of(...)` + +## Description + +The stable reference of *entity*, a face, edge or vertex of *shape* (or of a shape *shape* descends from). + +The entity is traced back through the recorded history to where it was born, so a face picked on the finished +part is named by the box or extrusion that made it, not by the fillet that last trimmed it. An edge is named by +the two faces it lies between when that names it in *shape* (``between=``), else by its own birth. + +## Parameters + +### entity + +- **Description**: The picked face / edge / vertex. + +### shape + +- **Description**: A recorded shape (made inside a ``GraphSession`` or a notebook cell) it belongs to. + +### follow + +- **Description**: Stored in the reference (see :func:`face`). + +### guard + +- **Description**: Store the entity's geometric fingerprint (the default; needed for an entity without a kernel role). + +## Returns + +ElementRef: ``repr()`` is the code that makes the same reference. + +## Raises + +- **ValueError**: *shape* was not recorded, or *entity* is not part of a recorded output upstream of it. diff --git a/docs/skill/references/docs/api/ref.parse.md b/docs/skill/references/docs/api/ref.parse.md new file mode 100644 index 00000000..b33320c0 --- /dev/null +++ b/docs/skill/references/docs/api/ref.parse.md @@ -0,0 +1,17 @@ +# ref.parse + +## API Definition + +```python +def ref.parse(text: str) -> ElementRef +``` + +*Source: ref.py* + +## Import Surface + +- stable references: `import simplecadapi as scad` then `scad.ref.parse(...)` + +## Description + +The reference spelled by *text* (what :meth:`ElementRef.code` writes). diff --git a/docs/skill/references/docs/api/ref.resolve.md b/docs/skill/references/docs/api/ref.resolve.md new file mode 100644 index 00000000..4134e420 --- /dev/null +++ b/docs/skill/references/docs/api/ref.resolve.md @@ -0,0 +1,17 @@ +# ref.resolve + +## API Definition + +```python +def ref.resolve(reference: Any, shape: Any) -> Resolution +``` + +*Source: ref.py* + +## Import Surface + +- stable references: `import simplecadapi as scad` then `scad.ref.resolve(...)` + +## Description + +``coerce(reference).resolve(shape)``. diff --git a/docs/skill/references/docs/api/ref.vertex.md b/docs/skill/references/docs/api/ref.vertex.md new file mode 100644 index 00000000..5a2668d7 --- /dev/null +++ b/docs/skill/references/docs/api/ref.vertex.md @@ -0,0 +1,17 @@ +# ref.vertex + +## API Definition + +```python +def ref.vertex(feature: str, op: str, *, role: Optional[str] = None, ordinal: int = 0, topo: Optional[str] = None, occurrence: int = 0, slot: int = 0, piece: Optional[int] = None, guard: Optional[Guard] = None, digest: Optional[str] = None, naming: int = 1) -> ElementRef +``` + +*Source: ref.py* + +## Import Surface + +- stable references: `import simplecadapi as scad` then `scad.ref.vertex(...)` + +## Description + +A reference to a vertex born in *feature* by *op* (the arguments are :func:`face`'s). diff --git a/docs/skill/references/docs/api/ref.vertices.md b/docs/skill/references/docs/api/ref.vertices.md new file mode 100644 index 00000000..13909ea2 --- /dev/null +++ b/docs/skill/references/docs/api/ref.vertices.md @@ -0,0 +1,17 @@ +# ref.vertices + +## API Definition + +```python +def ref.vertices(*refs: Union[ElementRef, Sequence[ElementRef]]) -> Any +``` + +*Source: ref.py* + +## Import Surface + +- stable references: `import simplecadapi as scad` then `scad.ref.vertices(...)` + +## Description + +The selection of the vertices the references name (see :func:`faces`). diff --git a/docs/skill/references/docs/api/remove_spline_anchor_rsketch.md b/docs/skill/references/docs/api/remove_spline_anchor_rsketch.md new file mode 100644 index 00000000..0c29d5e0 --- /dev/null +++ b/docs/skill/references/docs/api/remove_spline_anchor_rsketch.md @@ -0,0 +1,24 @@ +# remove_spline_anchor_rsketch + +## API Definition + +```python +def remove_spline_anchor_rsketch(sketch, spline, anchor) +``` + +*Source: operators/sketch.py* + +## Import Surface + +- top-level: `from simplecadapi import remove_spline_anchor_rsketch` + +## Description + +Remove an inner anchor (and its two handles) from a B-spline in anchor form. + +``anchor`` is the point id of an anchor that is not an end. The spans on either side become one span: its handles are +the outer handles of the two spans, which keep their ids and directions and get the lengths that fit the old curve best +(least squares, never reversed), so removing an anchor that ``insert_spline_anchor_rsketch`` made gives back the curve +it split. A handle that a constraint or another entity holds keeps its place. The removed points are deleted unless +another entity uses them, and the constraints on deleted points are dropped (the notes of metadata ``last_edit`` name +them). Refused for an end anchor, a handle, a point that is not on the spline, and a B-spline not in anchor form. diff --git a/docs/skill/references/docs/api/render_screenshot_rpath.md b/docs/skill/references/docs/api/render_screenshot_rpath.md index 25117dfc..21366c8e 100644 --- a/docs/skill/references/docs/api/render_screenshot_rpath.md +++ b/docs/skill/references/docs/api/render_screenshot_rpath.md @@ -3,7 +3,7 @@ ## API Definition ```python -def render_screenshot_rpath(shapes: 'Any', 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', views: 'Optional[Sequence[Tuple[float, float, str]]]' = None, show_axes: 'bool' = True, show_legend: 'bool' = True, zoom: 'float' = 4.0, show_callouts: 'bool' = True, linear_deflection: 'Optional[float]' = None, angular_deflection: 'Optional[float]' = None, style: 'str' = 'studio', edge_width_scale: 'Optional[float]' = None, view_up: 'Optional[Sequence[float]]' = None, supersample: 'int' = 2, highlight_edges: 'Optional[Sequence[Any]]' = None, highlight_edge_width: 'float' = 4.5) -> 'str' +def render_screenshot_rpath(shapes: 'Solid | Sequence[Solid]', output_path: 'str | PathLike[str]', highlight_tags: 'Sequence[str] | None' = None, tag_labels: 'dict[str, str] | None' = None, image_size: 'tuple[int, int]' = (1400, 900), view: "Literal['auto', 'iso', 'isometric', 'top', 'bottom', 'front', 'back', 'left', 'right', 'front_right', 'front_left', 'rear_right', 'rear_left'] | tuple[Angle, Angle] | None" = 'auto', views: 'Sequence[tuple[Angle, Angle, str]] | None' = None, show_axes: 'bool' = True, show_legend: 'bool' = True, zoom: 'float' = 4.0, show_callouts: 'bool' = True, linear_deflection: 'Length | None' = None, angular_deflection: 'float | None' = None, style: "Literal['studio', 'standard']" = 'studio', edge_width_scale: 'float | None' = None, view_up: 'Direction3 | None' = None, supersample: 'int' = 2, highlight_edges: 'Sequence[AnyShape] | None' = None, highlight_edge_width: 'float' = 4.5) -> 'str' ``` *Source: operators/features.py* diff --git a/docs/skill/references/docs/api/sample_curve_rcurvature.md b/docs/skill/references/docs/api/sample_curve_rcurvature.md new file mode 100644 index 00000000..1d499861 --- /dev/null +++ b/docs/skill/references/docs/api/sample_curve_rcurvature.md @@ -0,0 +1,19 @@ +# sample_curve_rcurvature + +## API Definition + +```python +def sample_curve_rcurvature(edge, count=64) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import sample_curve_rcurvature` + +## Description + +Sample an edge for a curvature comb: ``count`` points evenly in its parameter range. + +Returns ``{parameters, points, tangents, normals, curvatures, max_curvature}`` in the current coordinate system; a normal points to the centre of curvature and is zero where the curve is straight. Not recorded in the graph. diff --git a/docs/skill/references/docs/api/sample_surface_risocurves.md b/docs/skill/references/docs/api/sample_surface_risocurves.md new file mode 100644 index 00000000..8b28760e --- /dev/null +++ b/docs/skill/references/docs/api/sample_surface_risocurves.md @@ -0,0 +1,19 @@ +# sample_surface_risocurves + +## API Definition + +```python +def sample_surface_risocurves(face, u_count=5, v_count=5, samples=48) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import sample_surface_risocurves` + +## Description + +Sample isocurves of a face, clipped to its trimming. + +``u_count`` lines of constant u and ``v_count`` of constant v, evenly inside the face's (u, v) bounds, each sampled at ``samples`` points. Returns ``{u: [{parameter, segments}], v: [...], uv_bounds}``; ``segments`` are the runs of points that lie on the face. Not recorded in the graph. diff --git a/docs/skill/references/docs/api/scale_rsolid.md b/docs/skill/references/docs/api/scale_rsolid.md index 39616c57..454853dc 100644 --- a/docs/skill/references/docs/api/scale_rsolid.md +++ b/docs/skill/references/docs/api/scale_rsolid.md @@ -19,7 +19,8 @@ Scale a solid along the current coordinate system's axes about a center. The three factors must be finite and nonzero. A uniform scale preserves analytic curves and surfaces; a nonuniform scale uses the CAD kernel's general affine transform. The source solid stays unchanged. The operation -is recorded as ``make_scale_rsolid`` for graph replay; topology tag lineage +is recorded as ``make_scale_rsolid`` for graph replay. The recorded node carries the kernel's face and edge history, so element +references follow faces through it; tag lineage on the result is conservatively partial. ## Parameters diff --git a/docs/skill/references/docs/api/set_body_appearance_rpart.md b/docs/skill/references/docs/api/set_body_appearance_rpart.md new file mode 100644 index 00000000..73a73cbd --- /dev/null +++ b/docs/skill/references/docs/api/set_body_appearance_rpart.md @@ -0,0 +1,64 @@ +# set_body_appearance_rpart + +## API Definition + +```python +def set_body_appearance_rpart(part, body_id='body', *, color=None, opacity=None, name=None, reset=False) +``` + +*Source: operators/product.py* + +## Import Surface + +- top-level: `from simplecadapi import set_body_appearance_rpart` + +## Description + +Set the display colour, opacity and display name of one body of a Part. + +A Part is a value: the result is a new Part, the input is unchanged. The colour is an +appearance only. A body without a colour shows its Part's material colour +(``Material.color``), and without either it shows the default grey: the body colour +overrides the material colour for display (the live preview, the package scene, the STEP +surface colour of that solid). Mass, density and geometry never read it. + +``case = scad.set_body_appearance_rpart(case, "lid", color=(0.9, 0.3, 0.2), name="Lid")`` +paints the lid body of a multi-body part; ``scad.set_body_appearance_rpart(part, +color=(0.2, 0.4, 0.8))`` paints a one-body part. Recorded and replayed like +``assign_material_rpart``. To name a face or an edge for people, tag it +(``scad.apply_tag_rselection`` / ``ql.tag``): a tag is the name. + +## Parameters + +### part + +- **Description**: The Part (one body or several). + +### body_id + +- **Description**: The body to change: an id from ``part.body_ids()``. A one-body Part has the one body ``"body"`` (the default). + +### color + +- **Description**: Display colour as RGB floats in [0.0, 1.0], e.g. ``(0.8, 0.2, 0.2)``; linear RGB, as ``Material.color``, OpenCASCADE (so STEP) and three.js read it (the editor's colour picker converts its sRGB ``#rrggbb``). ``None`` keeps the body's colour. + +### opacity + +- **Description**: Display opacity in [0.0, 1.0] (1 is opaque). ``None`` keeps it. STEP export writes the colour, not the opacity. + +### name + +- **Description**: Display name of the body (the editor's bodies list, the scene node, the STEP solid). ``None`` keeps it. The ``body_id`` never changes. + +### reset + +- **Description**: Clear the colour, opacity and name first, so the body shows the material colour, opaque, under its id; then apply what is given. + +## Returns + +Part: The Part with that body's appearance changed; ``part.get_body(body_id).color``, +``.opacity`` and ``.name`` read it back. + +## Raises + +- **SimpleCADError**: The value is not a Part, the part has no such body, the colour is not three numbers in [0, 1], the opacity is outside [0, 1], or the name is empty. diff --git a/docs/skill/references/docs/api/set_curve_poles_redge.md b/docs/skill/references/docs/api/set_curve_poles_redge.md new file mode 100644 index 00000000..a0d99fc3 --- /dev/null +++ b/docs/skill/references/docs/api/set_curve_poles_redge.md @@ -0,0 +1,19 @@ +# set_curve_poles_redge + +## API Definition + +```python +def set_curve_poles_redge(edge, poles, *, tag_prefix=None) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import set_curve_poles_redge` + +## Description + +Set every pole of an edge's B-spline curve. + +``poles`` lists exactly as many points as the curve has poles (``inspect_curve_rcurvedata``), in order, in the current coordinate system. Degree, knots and weights stay. Only B-spline and Bezier curves are edited; a line, circle or other analytic edge is refused with a message naming ``convert_to_bspline_redge``, the explicit conversion step. Returns a new Edge; the input is not changed. Recorded: the Edge continues in the node's history, so tags and references follow. diff --git a/docs/skill/references/docs/api/set_curve_weights_redge.md b/docs/skill/references/docs/api/set_curve_weights_redge.md new file mode 100644 index 00000000..1f461fde --- /dev/null +++ b/docs/skill/references/docs/api/set_curve_weights_redge.md @@ -0,0 +1,19 @@ +# set_curve_weights_redge + +## API Definition + +```python +def set_curve_weights_redge(edge, weights, *, tag_prefix=None) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import set_curve_weights_redge` + +## Description + +Set the weights of an edge's B-spline curve: one positive value per pole. + +A weight above 1 pulls the curve toward its pole; any weight other than 1 makes the curve rational. Only B-spline and Bezier curves are edited; a line, circle or other analytic edge is refused with a message naming ``convert_to_bspline_redge``, the explicit conversion step. Returns a new Edge; the input is not changed. Recorded: the Edge continues in the node's history, so tags and references follow. diff --git a/docs/skill/references/docs/api/set_surface_poles_rface.md b/docs/skill/references/docs/api/set_surface_poles_rface.md new file mode 100644 index 00000000..7d1bc2dc --- /dev/null +++ b/docs/skill/references/docs/api/set_surface_poles_rface.md @@ -0,0 +1,19 @@ +# set_surface_poles_rface + +## API Definition + +```python +def set_surface_poles_rface(face, poles, *, tag_prefix=None) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import set_surface_poles_rface` + +## Description + +Set every pole of a face's B-spline surface. + +``poles`` is the full grid (rows along U, each row along V), the same size as ``inspect_surface_rsurfacedata`` reports, in the current coordinate system. The face keeps its trimming exactly: its boundary loops stay where they are in the surface's (u, v) space and the 3D boundary edges are recomputed on the edited surface. Only B-spline and Bezier surfaces are edited; a plane, cylinder or other analytic face is refused with a message naming ``convert_to_bspline_rface``, the explicit conversion step. Returns a new Face; the input is not changed. Recorded: the Face and its boundary edges continue in the node's history, so tags and references follow. diff --git a/docs/skill/references/docs/api/set_surface_weights_rface.md b/docs/skill/references/docs/api/set_surface_weights_rface.md new file mode 100644 index 00000000..71a0ea67 --- /dev/null +++ b/docs/skill/references/docs/api/set_surface_weights_rface.md @@ -0,0 +1,19 @@ +# set_surface_weights_rface + +## API Definition + +```python +def set_surface_weights_rface(face, weights, *, tag_prefix=None) +``` + +*Source: operators/geometry.py* + +## Import Surface + +- top-level: `from simplecadapi import set_surface_weights_rface` + +## Description + +Set the weights of a face's B-spline surface: a grid of positive values, one per pole. + +The face keeps its trimming exactly: its boundary loops stay where they are in the surface's (u, v) space and the 3D boundary edges are recomputed on the edited surface. Only B-spline and Bezier surfaces are edited; a plane, cylinder or other analytic face is refused with a message naming ``convert_to_bspline_rface``, the explicit conversion step. Returns a new Face; the input is not changed. Recorded: the Face and its boundary edges continue in the node's history, so tags and references follow. diff --git a/docs/skill/references/docs/api/shell_rsolid.md b/docs/skill/references/docs/api/shell_rsolid.md index 9fb98f50..94ee64c7 100644 --- a/docs/skill/references/docs/api/shell_rsolid.md +++ b/docs/skill/references/docs/api/shell_rsolid.md @@ -15,3 +15,10 @@ def shell_rsolid(solid, faces_to_remove, thickness, *, result_tag=None, body_fac ## Description Shell a solid, with optional kernel-role-based face tags. + +The kernel roles are ``shell.body_face`` (a face of the input that stays in +the result), ``shell.offset_face`` (the inner face it generates), +``shell.closing_descendant`` (what is left of a removed face: the rim) and +``shell.wall`` (the edges that become the walls of an opening). +``body_faces_tag``, ``offset_faces_tag``, ``closing_faces_tag`` and +``wall_edges_tag`` tag exactly the entities with those roles. diff --git a/docs/skill/references/docs/api/tag.md b/docs/skill/references/docs/api/tag.md index 6effaaa3..84dd0e10 100644 --- a/docs/skill/references/docs/api/tag.md +++ b/docs/skill/references/docs/api/tag.md @@ -16,6 +16,15 @@ def tag(pattern, scope='effective') Build a tag predicate for QL filtering. +Tags are a view over the recorded history: a tag assigned in a +``GraphSession`` reaches every entity its target became through later +operations as its lineage policy allows. A tag follows its entity as a +reference does: an entity an operation splits keeps it on none of its +pieces, which carry ``.split1`` .. ``.split`` instead +(``list_tag_splits``); ``ql.tag(".*")`` finds them all. ``local`` is what the entity itself stores, ``inherited`` +what containing entities pass down, ``lineage`` what the history carries, +and ``effective`` (the default) all three. + ## Parameters ### pattern diff --git a/docs/skill/references/docs/api/transform_rsolid.md b/docs/skill/references/docs/api/transform_rsolid.md index aaff9a71..2097ed36 100644 --- a/docs/skill/references/docs/api/transform_rsolid.md +++ b/docs/skill/references/docs/api/transform_rsolid.md @@ -19,7 +19,9 @@ Apply a finite nonsingular 4x4 affine matrix in the current coordinate system. The matrix may include shear, reflection and nonuniform scale. Perspective matrices and singular maps cannot define a solid and are rejected. The input geometry stays unchanged. The operation is recorded as -``make_transform_rsolid`` for graph replay; topology tag lineage is partial. +``make_transform_rsolid`` for graph replay. The recorded node carries the kernel's face and edge history, so element +references follow faces through it; +tag lineage on the result is partial. ## Parameters diff --git a/docs/skill/references/docs/api/trim_rsketch.md b/docs/skill/references/docs/api/trim_rsketch.md new file mode 100644 index 00000000..bdb777b4 --- /dev/null +++ b/docs/skill/references/docs/api/trim_rsketch.md @@ -0,0 +1,30 @@ +# trim_rsketch + +## API Definition + +```python +def trim_rsketch(sketch, entity, at, *, against=None) +``` + +*Source: operators/sketch.py* + +## Import Surface + +- top-level: `from simplecadapi import trim_rsketch` + +## Description + +Remove the part of a line, circle, arc or B-spline that contains ``at``, between the nearest cuts. + +A cut is a crossing with another non-construction line, circle, arc, ellipse or B-spline (``against`` limits +which ones) or an end of the entity; a tangent touch is one cut. ``at`` is a point (x, y) in sketch coordinates +or a point id. Lines, arcs and open B-splines keep the pieces on either side; a circle keeps the arc left over and +a closed B-spline (periodic, or starting and ending on one point) the piece left over (both need two cuts); an +entity nothing crosses is removed whole. A B-spline piece is a new B-spline (ids s1, s2, ...) of the same curve: +its first and last control points are its end points, its inner control points are sketch points (an original +control point the piece keeps unmoved keeps its id, the others are new points p1, p2, ...), and its knots are no +longer uniform. Cut points are tied to the cutter with point_on; line pieces inherit horizontal/vertical; +constraints on an end point stay with the piece that keeps that point, a tangent at a B-spline end (at_a/at_b) +moves to the piece that keeps that end, other constraints that name the removed entity are removed, and so are +the original control points no piece uses, with their constraints. Use diff_sketch_rsketchdiff(before, after) to +see what changed. diff --git a/docs/skill/references/docs/core/compound.md b/docs/skill/references/docs/core/compound.md index 1f138d3b..f7a5c000 100644 --- a/docs/skill/references/docs/core/compound.md +++ b/docs/skill/references/docs/core/compound.md @@ -19,4 +19,7 @@ Use `Compound` when a workflow intentionally needs a flattened geometry projection. Do not use it as a substitute for `Assembly` product structure. For normal part modeling, keep using `Solid` as the body-level geometry. A -single-body `Part` wraps exactly one `Solid`. +`Part` wraps one `Solid`, or several named bodies (`Part.bodies`), each its own +`Solid`. A `Compound` built from solids does not keep their tags, so it is not +a way to hold the bodies of a part (`scad.part_shape(part)` gives that view for +geometry-only use). diff --git a/docs/skill/references/docs/core/operation_graph_json_spec.md b/docs/skill/references/docs/core/operation_graph_json_spec.md index fe997c89..b0936824 100644 --- a/docs/skill/references/docs/core/operation_graph_json_spec.md +++ b/docs/skill/references/docs/core/operation_graph_json_spec.md @@ -910,8 +910,12 @@ fields when their target expression is unitless. - 多输出 graph 的最终结果集由 `leaf_ids` 显式声明 - replay 默认 strict;permissive 仅通过 API 参数显式 opt-in - importer 仍识别冻结的 `2.0` contract,但 `2.0` graph 不得使用 - `load_brep_region_rshell`、`load_brep_region_rsolid` 或 - `make_solid_from_shell_rsolid`。 + `load_brep_region_rshell`、`load_brep_region_rsolid`、 + `make_solid_from_shell_rsolid`、`trim_rsketch`、`extend_rsketch`、 + `fillet_corner_rsketch`、`chamfer_corner_rsketch`、`project_rsketch`、 + `insert_spline_anchor_rsketch`、`remove_spline_anchor_rsketch`、`convert_spline_to_anchors_rsketch`、 + `explode_rassembly` 或曲线/曲面编辑操作(`convert_to_bspline_*`、`set_*_poles_*`、 + `move_*_poles_*`、`set_*_weights_*`、`insert_*_knot_*`、`elevate_*_degree_*`、`rebuild_*`)。 - 为兼容早期 `2.0` model payload,importer 将缺失的 `canonical_contract` 视为最小 legacy `{"contract_version": "2.0"}`; 新导出的 model payload 仍必须显式包含完整 contract。 @@ -1012,6 +1016,14 @@ New canonical profile nodes use the `make_*_r*` names listed in `canonical_contr - `add_circle_rsketch` - `add_arc_rsketch` - `add_bspline_rsketch` +- `trim_rsketch` +- `extend_rsketch` +- `fillet_corner_rsketch` +- `chamfer_corner_rsketch` +- `project_rsketch` +- `insert_spline_anchor_rsketch` +- `remove_spline_anchor_rsketch` +- `convert_spline_to_anchors_rsketch` - `make_constrain_coincident_rsketch` - `make_constrain_point_on_rsketch` - `make_constrain_horizontal_rsketch` @@ -1056,6 +1068,20 @@ New canonical profile nodes use the `make_*_r*` names listed in `canonical_contr - `make_gordon_surface_rface` - `make_surface_patch_rface` - `trim_surface_rface` +- `convert_to_bspline_redge` +- `convert_to_bspline_rface` +- `set_curve_poles_redge` +- `move_curve_poles_redge` +- `set_curve_weights_redge` +- `insert_curve_knot_redge` +- `elevate_curve_degree_redge` +- `set_surface_poles_rface` +- `move_surface_poles_rface` +- `set_surface_weights_rface` +- `insert_surface_knot_rface` +- `elevate_surface_degree_rface` +- `rebuild_curve_redge` +- `rebuild_surface_rface` - `make_loft_rshell` - `sew_faces_rshell` - `make_solid_from_shell_rsolid` @@ -1309,6 +1335,60 @@ Notes: - Inputs: 1 `Sketch` - Outputs: 1 updated `Sketch` - Params: `sketch_id`, `entity_id`, `start`, `end`, `control_points`, `degree`, `knots`, `multiplicities`, `weights`, `periodic`, `construction` +#### `trim_rsketch` + +- Inputs: 1 `Sketch` +- Outputs: 1 updated `Sketch` +- Params: `sketch_id`, `entity`, `at`, `against` +- `at` is `[x, y]` in sketch coordinates or a point id; `against` is a list of entity ids or `null` (every non-construction entity). The edit is made on the solved geometry, so replay is deterministic. + +#### `extend_rsketch` + +- Inputs: 1 `Sketch` +- Outputs: 1 updated `Sketch` +- Params: `sketch_id`, `entity`, `end` (`start` or `end`), `to` (entity id or `null`) + +#### `fillet_corner_rsketch` / `chamfer_corner_rsketch` + +- Inputs: 1 `Sketch` +- Outputs: 1 updated `Sketch` +- Params: `sketch_id`, `a`, `b` (line ids), `radius` (fillet) or `distance` (chamfer) +- The edit is made on the solved geometry and picks its own ids (`p`, `a`, `l`, `c__`), so replay is deterministic. `radius` / `distance` are recorded as symbolic parameters when they were `Var` / `Expr`. + +#### `project_rsketch` + +- Inputs: the target `Sketch` and either the source `Sketch` (`source_kind: "sketch"`: inputs `[target, source]`) or the projected shapes (`source_kind: "shapes"`: the shape inputs, then the target `Sketch`) +- Outputs: 1 updated `Sketch` +- Params: `sketch_id`, `source_kind`, `source` (source sketch id, or the number of shapes), `entities` (source entity ids or `null`; sketch sources only), `construction` +- Projected ids are `proj__` (`proj_shape_` for shapes); the target sketch's `projections` metadata lists `{source, entity, projected}`. + +#### `insert_spline_anchor_rsketch` / `remove_spline_anchor_rsketch` / `convert_spline_to_anchors_rsketch` + +- Inputs: 1 `Sketch` +- Outputs: 1 updated `Sketch` +- Params: `sketch_id`, `spline` (B-spline id); insert: `at` (`[x, y]` or a point id); remove: `anchor` (point id) +- The edit is made on the solved geometry and picks its own ids (`p`), so replay is deterministic. + +#### `explode_rassembly` + +- Inputs: 1 `Assembly` +- Outputs: 1 `Assembly` (a display copy: only component placements differ) +- Params: `assembly_id`, `factor`, `center`, `axis` (3-vectors or `null`), `directions` (component id to 3-vector), `weights` (component id to number) +- `factor` is recorded as a symbolic parameter when it was a `Var` / `Expr`. Constraints are not re-solved; solving the result moves the components back. + +#### Curve and surface edits (`*_redge` / `*_rface`) + +`convert_to_bspline_redge`, `convert_to_bspline_rface`, `set_curve_poles_redge`, `move_curve_poles_redge`, +`set_curve_weights_redge`, `insert_curve_knot_redge`, `elevate_curve_degree_redge`, `set_surface_poles_rface`, +`move_surface_poles_rface`, `set_surface_weights_rface`, `insert_surface_knot_rface`, `elevate_surface_degree_rface`, +`rebuild_curve_redge`, `rebuild_surface_rface`. + +- Inputs: 1 `Edge` (`*_redge`) or 1 `Face` (`*_rface`), named by `params.input_refs` (a pick from a solid becomes a selection node first) +- Outputs: 1 new `Edge` / `Face` (the input is not changed) +- Params (besides `input_refs` and `tag_prefix`): `poles` (list of points / grid of rows along U), `moves` (`[[index, point], ...]` for a curve, `[[row, column, point], ...]` for a surface; indices 0-based), `weights` (list / grid), `parameter`, `multiplicity`, `direction` (`"U"` / `"V"`), `degree`, `u_degree` / `v_degree` (`null` = unchanged for `elevate_surface_degree_rface`), `pole_count` / `u_pole_count` / `v_pole_count` (`null` = from `tolerance`), `tolerance`. Points are in the node's `context` coordinate system. +- `topo_delta`: the face and its edges (an edge: itself) are `MODIFIED`, derivation `continuation`, each with its input entity as parent, so tags and references follow. +- `inspect_curve_rcurvedata`, `inspect_surface_rsurfacedata`, `sample_curve_rcurvature`, `sample_surface_risocurves` are diagnostic: no node. + #### `make_constrain_*_rsketch` - Inputs: 1 `Sketch` diff --git a/docs/skill/references/docs/core/simple_workplane.md b/docs/skill/references/docs/core/simple_workplane.md index 7d95b97a..39d9af59 100644 --- a/docs/skill/references/docs/core/simple_workplane.md +++ b/docs/skill/references/docs/core/simple_workplane.md @@ -23,6 +23,32 @@ SimpleWorkplane( - Keep existing shape inputs in global coordinates. Entering a workplane does not move a shape; only the operation's point and vector parameters use the active frame. - Bind declarative sketches to their creation frame, so they can be promoted after the workplane exits without changing position. +## A workplane is a value + +- `SimpleWorkplane(...)` (alias `Workplane`) and the recorded constructors below return the same type. It is a frame in + world coordinates: `wp.origin`, `wp.normal`, `wp.x_dir`, `wp.y_dir` (tuples), `wp.plane` (the sketch-plane mapping + `{origin, x_axis, y_axis}`), `wp.cs` (its `CoordinateSystem`). +- It can be entered again: `with wp:` in one cell and again in a later cell. Entering the same workplane twice at once is + an error. +- It pickles (the notebook cell cache stores a cell that defines one), copies, and keeps its graph lineage through both. +- `make_sketch_rsketch(name, plane=wp)` draws a sketch on it: the world frame is used as it is, whatever workplane is + entered at the time. + +## Recorded constructors + +| Constructor | The frame | +| --- | --- | +| `make_workplane_rworkplane(origin, normal, x_dir)` | the recorded `SimpleWorkplane(...)` (read in the current system) | +| `make_workplane_from_face_rworkplane(face, *, origin=None, x_edge=None, x_dir=None, offset=0, flip=False)` | on a planar face: its centre (or `origin` projected), outward normal, x along an edge / a direction / the face's own x, moved `offset` along the normal | +| `make_workplane_from_points_rworkplane(a, b, c)` | origin `a`, x towards `b`, normal (b - a) x (c - a); each a `Vertex` or a point | +| `make_workplane_from_axis_rworkplane(axis, angle=0, *, face=None)` | through a straight edge (or a circle's centre line), x along it, turned `angle` degrees about it from `face`'s plane (or the plane square to global Z) | +| `make_workplane_midplane_rworkplane(face_a, face_b)` | halfway between two parallel planar faces | +| `make_workplane_normal_to_curve_rworkplane(curve, at=0, *, x_dir=None)` | normal to an edge or wire at a fraction `at` of its length (a sweep profile's plane) | + +Each records its geometry inputs as references. A model-JSON replay builds the frame again from the replayed geometry, so a +sketch on `plane=wp` and every operation made inside `with wp:` follow the face (each such node's context names the +workplane's node; `plane` is recorded as `{"workplane": true, ...frame}` with the workplane node as an input). + ## Example ```python @@ -43,3 +69,22 @@ with scad.SimpleWorkplane( print(moved.get_volume()) ``` + +A workplane on a face, a sketch on it, and a boss inside it: + +```python +import simplecadapi as scad + +base = scad.make_box_rsolid(60, 40, 10, tag_prefix="base") +top = scad.ql.faces().where(scad.ql.tag("base.face.top")).exactly(1).resolve(base)[0] +wp_top = scad.make_workplane_from_face_rworkplane(top, offset=0) + +sketch = scad.make_sketch_rsketch("boss", plane=wp_top) +sketch = scad.add_point_rsketch(sketch, "c", 0, 0) +sketch = scad.add_circle_rsketch(sketch, "circle", "c", 8) +boss = scad.extrude_rsolid(scad.make_face_from_sketch_rface(sketch), wp_top.normal, 5) + +with wp_top: # local x, y on the top face, z out of it + pin = scad.make_cylinder_rsolid(2, 12, bottom_face_center=(20, 0, 0)) +``` + diff --git a/docs/skill/references/docs/guides/README.md b/docs/skill/references/docs/guides/README.md index c6e21cb0..6c7ab9b9 100644 --- a/docs/skill/references/docs/guides/README.md +++ b/docs/skill/references/docs/guides/README.md @@ -1,14 +1,14 @@ # Engineering Guides -- [STEP BREP Reverse Engineering](step-brep-reverse-engineering.md): full workflow from STEP inspection, geometric fingerprint analysis, feature-tree inference, and candidate iteration to geometric point-set, BREP topology, parameter history, and model replay acceptance. +- [STEP BREP Reverse Engineering](../../inspect/brep-reverse-engineering.md): full workflow from STEP inspection, geometric fingerprint analysis, feature-tree inference, and candidate iteration to geometric point-set, BREP topology, parameter history, and model replay acceptance. - [Reconstruction Agent Test Prompt](reconstruction-agent-test-prompt.md): compact, versioned run contract for controlled STEP reconstruction trials. - [Reconstruction Agent Strategy](reconstruction-agent-strategy.md): advisory inspection, feature-selection, Sketch, Boolean, and iteration tactics. -- [Notebook Runtime and Product Build Workflow](notebook-runtime.md): - models as marimo notebooks, `sca run`, `scad.use` composition, the cell - cache, `@part`/`@assemble` library parts, `.scadpkg` delivery, - FreeCAD/AP242 targets, and optional Gmsh volume meshing. +- [Notebook Runtime](notebook-runtime.md): models as marimo notebooks, `sca run`, parameters, the editor. +- [Notebook Composition and Cache](notebook-composition-and-cache.md): `scad.use` composition, part families, the cell + cache, `@part`/`@assemble` library parts. +- [Product Export](product-export.md): `.scadpkg` delivery, FreeCAD/AP242 targets, STL/OBJ, optional Gmsh volume meshing. The corresponding runnable module and examples live in: diff --git a/docs/skill/references/docs/guides/notebook-composition-and-cache.md b/docs/skill/references/docs/guides/notebook-composition-and-cache.md new file mode 100644 index 00000000..07a3dcad --- /dev/null +++ b/docs/skill/references/docs/guides/notebook-composition-and-cache.md @@ -0,0 +1,141 @@ +# Notebook Composition and the Cell Cache + +**Read when:** a notebook uses another notebook (`scad.use`), one notebook describes a part family, a run is unexpectedly cached or stale, or a reusable part is built with `@scad.part` / `@scad.assemble`. Running a notebook and parameters are in `notebook-runtime.md`. + +## Composing notebooks + +`scad.use(path, **overrides)` runs another notebook and returns its product, +ready to be a component of an assembly. A relative path is relative to the +calling notebook; keyword arguments override the child's top-level variables. + +```python +@app.cell +def _(): + bracket = scad.use("bracket.py", width=24.0) + return (bracket,) + + +@app.cell +def _(bracket): + rig = scad.make_assembly_rassembly(assembly_id="rig", name="Rig") + rig = scad.add_component_rassembly( + assembly=rig, item=bracket, component_id="bracket", + placement=scad.identity_placement_rplacement(), + ) + return (rig,) +``` + +The child runs with its own cell cache, and the calling notebook's cache +depends on the child from then on. + +### Part families + +One notebook may describe several parts that differ only in their +parameters — the links of a linkage, the planet gears of two stages. Name the +product with `scad.notebook_id()` instead of a literal: + +```python +# link_bar.py — [tool.simplecadapi] id = "link_bar" +@app.cell +def _(web): + link_bar = scad.Part(part_id=scad.notebook_id(), body=web) + return (link_bar,) +``` + +and give each member an id of its own where it is used: + +```python +@app.cell +def _(): + crank = scad.use("link_bar.py", id="crank", center_distance=40.0) + rocker = scad.use("link_bar.py", id="rocker", center_distance=90.0) + return crank, rocker +``` + +Each id is one definition, with its own content hash and its own cell cache. +Used twice under one id with different parameters, the two products would be +two definitions with the same id, which an assembly rejects +(`reference_identity_conflict`). Run alone, the notebook builds the member +named in its header. + +## The cell cache + +Every cell except the setup cell is cached, in headless runs and in the +editor's kernel alike (what one computed the other restores). marimo keys a +cell's entry by its code and what it reads; SimpleCAD adds a digest of what +the cells cannot show, **per cell**, over the files *that cell* depends on: + +- **local modules** — Python files under the notebook's directory, found from + the `import` statements (setup cell included), and the local modules those + import in turn. A cell depends on a module when it reads a name an `import` + bound to it, or calls a function the notebook defines (an `@app.function` + cell, a `def` in the setup cell) that does; +- **everything the cells it reads from depend on**: marimo keys a cell that + reads an object (a solid, a part) by the code of the cells that made it, so + it cannot see a module that changed upstream; +- **child notebooks** run with `scad.use` (and what they depend on), for the + cells that call `use` and those that read what they make; +- the files listed in `inputs`, for every cell; +- the SimpleCAD, OpenCASCADE, and marimo versions. + +So editing `dimensions.py` recomputes the cells that read the dimensions (and +what is made from them) and restores the rest, and going back to an earlier +value restores everything. A run keeps the entries of its own digests and of +the previous run's. Local modules are imported afresh when their files changed +since the last run in the same process. + +- Imports through `importlib` or `__import__` are not seen; import local + modules with `import` statements, or list the files in `inputs`. +- Values that cannot be pickled cannot be restored: the run then executes the + whole notebook without the cache. Keep such values (locks, open files, + handles) out of top-level variables. +- The editor kernel does not cache the cells that run child notebooks (only a + headless run records what they depend on; the children themselves are + cached), and keys the cells downstream of a value an editor saved by that + value. `SCA_NO_KERNEL_CACHE=1` turns the kernel's cache off. +- The state lives in `__marimo__/simplecad///` + (`deps.json` and `cache//`), one directory per part-family member. It + is generated state: do not commit `__marimo__/`. Entries are unsigned unless + the `cryptography` package is installed; treat the directory as trusted local + files. + +**In the marimo editor, leave `[tool.marimo.runtime] cache_cells` off.** That +is marimo's own cache: it does not know these digests, so after a local module +or child notebook changes it would restore stale values. Under `sca edit` the +SimpleCAD cache above is already in use. + +## Library parts: `@scad.part` and `@scad.assemble` + +Reusable parts belong in plain `.py` modules next to the notebooks, built by +decorated builders. A call builds the part in a session of its own and returns +a `PartBuildResult(value, definition, feature_graph)`; `@scad.assemble` +returns an `AssemblyBuildResult` with the same fields. Calls may happen inside +a notebook cell or another builder: the built part enters the calling +recording as an external definition. + +```python +# lib/plates.py +import simplecadapi as scad + + +@scad.part(id="mounting_plate", revision="1.0.0") +def build_plate(width: float = 30.0) -> scad.Part: + body = scad.make_box_rsolid(width=width, height=20.0, depth=3.0) + return scad.make_part_rpart(part_id="mounting_plate", body=body) +``` + +```python +@app.cell +def _(): + from lib.plates import build_plate + + plate = build_plate(width=40.0).value + return (plate,) +``` + +The builders do not cache; inside a notebook the calling cell is cached, and +editing `lib/plates.py` invalidates it through *D*. Declare every non-Python +file a builder reads with `scad.file_input(...)`; its content snapshot is +recorded in the definition. `@scad.assemble(definitions=...)` names the +external part and assembly definitions an assembly references; its assembly +is solved strictly. diff --git a/docs/skill/references/docs/guides/notebook-runtime.md b/docs/skill/references/docs/guides/notebook-runtime.md index 51710d2e..5559ec39 100644 --- a/docs/skill/references/docs/guides/notebook-runtime.md +++ b/docs/skill/references/docs/guides/notebook-runtime.md @@ -78,6 +78,24 @@ if __name__ == "__main__": app.run() ``` +- **The kind** is fixed when the notebook is made: the header says + `kind = "part"` or `kind = "assembly"` (`sca new` and the editor write it). + A `kind = "part"` notebook needs **no product cell**: when no top-level + `Part` has the notebook id, the runtime makes one from the notebook's + *open bodies* (public top-level `Solid`s, or the piece lists of split cuts, + that no other cell reads and that no other such value was built from; a cell + that only makes construction geometry -- workplanes, wires, edges, e.g. a path + through the body's vertices -- does not count as reading it), with + the notebook id. One open body is a one-body Part (body id `"body"`); several + are the bodies of one Part, named by their variables (pieces `_1..n`). + Write a `make_part_rpart` cell only to add connectors or a material, or to + choose and name the bodies yourself; it then wins. The headless run and the + editor use the same rule (`runtime/projection.py`, `find_product`). + A notebook with no Part, Assembly or solid yet (a new one, or only sketches, + curves and workplanes so far) raises `NoProductYetError` (a + `ProductNotFoundError`) where a product is needed (export, a headless run + with `product=True`); the editor previews it as an empty model, not an error. + A Part or Assembly with another id is still a `ProductNotFoundError`. - **The product** is the top-level variable whose id (`Part.part_id` or `Assembly.assembly_id`) equals the notebook id. An assembly built up over several cells (components, then constraints, then the solve) carries the @@ -86,7 +104,13 @@ if __name__ == "__main__": given another id (`run_notebook(..., id=...)`, `scad.use(..., id=...)`, `sca run --id`); cells read the id of their run with `scad.notebook_id()` — see - [Part families](#part-families). + [Part families](notebook-composition-and-cache.md#part-families). +- **A part with several bodies** is one product: + `scad.make_part_rpart(part_id=scad.notebook_id(), bodies={"base": base, "lid": lid})`. + Each body keeps its own BRep, topology snapshot and tags in the package, whose + part definition then has profile `simplecad-part-definition-3` (an older SDK + refuses it instead of reading one body). One-body parts are unchanged. + The cell cache restores a multi-body part like any other value. - **Parameters** are ordinary top-level variables. Overriding one replaces it, and the cell that defines it does not run, so all variables of that cell must be overridden together. Group parameters into cells by what changes @@ -126,7 +150,9 @@ sca run bracket.py --no-cache # run every cell `--set` values are Python literals, else strings; the option may repeat. The report lists the definition id, kind, revision, `content_hash`, and every cell's status: `ran`, `cached` (restored from the cell cache), or `skipped` -(replaced by an override). +(replaced by an override). A cell whose operations split a tagged entity also +lists `tag_splits` (`{tag, into, op}`: the pieces carry `.split1` ..), ran or +restored; a cell that runs prints a `TagSplitWarning` at its place in the file. From Python: @@ -208,6 +234,9 @@ The editor and a coding agent share the notebook file: selection, each cell's state and errors, the parameter values, and the path of the viewport image (`.sca-editor/viewport.png`) the agent can look at. The editor keeps `.sca-editor/snapshot.json` current while it runs. + When a cell failed, the editor previews the model before it and the snapshot + says so in `partial` (the failing cell, its error, the cells that did not run, + the bodies shown; `runtime.live.preview_product`); `null` when the model is whole. - The local modules a notebook imports (`from dimensions import DIMS`) are listed under *modules* in the workspace tab, with the notebooks that use them. A module opens in a module view: its source, autosaved, and the fields @@ -219,225 +248,7 @@ The editor and a coding agent share the notebook file: and can run everything above it; `@app.function` cells are shown read-only (edit them in the file). -## Composing notebooks +## Composing notebooks, the cell cache, library parts, export -`scad.use(path, **overrides)` runs another notebook and returns its product, -ready to be a component of an assembly. A relative path is relative to the -calling notebook; keyword arguments override the child's top-level variables. - -```python -@app.cell -def _(): - bracket = scad.use("bracket.py", width=24.0) - return (bracket,) - - -@app.cell -def _(bracket): - rig = scad.make_assembly_rassembly(assembly_id="rig", name="Rig") - rig = scad.add_component_rassembly( - assembly=rig, item=bracket, component_id="bracket", - placement=scad.identity_placement_rplacement(), - ) - return (rig,) -``` - -The child runs with its own cell cache, and the calling notebook's cache -depends on the child from then on. - -### Part families - -One notebook may describe several parts that differ only in their -parameters — the links of a linkage, the planet gears of two stages. Name the -product with `scad.notebook_id()` instead of a literal: - -```python -# link_bar.py — [tool.simplecadapi] id = "link_bar" -@app.cell -def _(web): - link_bar = scad.Part(part_id=scad.notebook_id(), body=web) - return (link_bar,) -``` - -and give each member an id of its own where it is used: - -```python -@app.cell -def _(): - crank = scad.use("link_bar.py", id="crank", center_distance=40.0) - rocker = scad.use("link_bar.py", id="rocker", center_distance=90.0) - return crank, rocker -``` - -Each id is one definition, with its own content hash and its own cell cache. -Used twice under one id with different parameters, the two products would be -two definitions with the same id, which an assembly rejects -(`reference_identity_conflict`). Run alone, the notebook builds the member -named in its header. - -## The cell cache - -Every cell except the setup cell is cached, in headless runs and in the -editor's kernel alike (what one computed the other restores). marimo keys a -cell's entry by its code and what it reads; SimpleCAD adds a digest of what -the cells cannot show, **per cell**, over the files *that cell* depends on: - -- **local modules** — Python files under the notebook's directory, found from - the `import` statements (setup cell included), and the local modules those - import in turn. A cell depends on a module when it reads a name an `import` - bound to it, or calls a function the notebook defines (an `@app.function` - cell, a `def` in the setup cell) that does; -- **everything the cells it reads from depend on**: marimo keys a cell that - reads an object (a solid, a part) by the code of the cells that made it, so - it cannot see a module that changed upstream; -- **child notebooks** run with `scad.use` (and what they depend on), for the - cells that call `use` and those that read what they make; -- the files listed in `inputs`, for every cell; -- the SimpleCAD, OpenCASCADE, and marimo versions. - -So editing `dimensions.py` recomputes the cells that read the dimensions (and -what is made from them) and restores the rest, and going back to an earlier -value restores everything. A run keeps the entries of its own digests and of -the previous run's. Local modules are imported afresh when their files changed -since the last run in the same process. - -- Imports through `importlib` or `__import__` are not seen; import local - modules with `import` statements, or list the files in `inputs`. -- Values that cannot be pickled cannot be restored: the run then executes the - whole notebook without the cache. Keep such values (locks, open files, - handles) out of top-level variables. -- The editor kernel does not cache the cells that run child notebooks (only a - headless run records what they depend on; the children themselves are - cached), and keys the cells downstream of a value an editor saved by that - value. `SCA_NO_KERNEL_CACHE=1` turns the kernel's cache off. -- The state lives in `__marimo__/simplecad///` - (`deps.json` and `cache//`), one directory per part-family member. It - is generated state: do not commit `__marimo__/`. Entries are unsigned unless - the `cryptography` package is installed; treat the directory as trusted local - files. - -**In the marimo editor, leave `[tool.marimo.runtime] cache_cells` off.** That -is marimo's own cache: it does not know these digests, so after a local module -or child notebook changes it would restore stale values. Under `sca edit` the -SimpleCAD cache above is already in use. - -## Library parts: `@scad.part` and `@scad.assemble` - -Reusable parts belong in plain `.py` modules next to the notebooks, built by -decorated builders. A call builds the part in a session of its own and returns -a `PartBuildResult(value, definition, feature_graph)`; `@scad.assemble` -returns an `AssemblyBuildResult` with the same fields. Calls may happen inside -a notebook cell or another builder: the built part enters the calling -recording as an external definition. - -```python -# lib/plates.py -import simplecadapi as scad - - -@scad.part(id="mounting_plate", revision="1.0.0") -def build_plate(width: float = 30.0) -> scad.Part: - body = scad.make_box_rsolid(width=width, height=20.0, depth=3.0) - return scad.make_part_rpart(part_id="mounting_plate", body=body) -``` - -```python -@app.cell -def _(): - from lib.plates import build_plate - - plate = build_plate(width=40.0).value - return (plate,) -``` - -The builders do not cache; inside a notebook the calling cell is cached, and -editing `lib/plates.py` invalidates it through *D*. Declare every non-Python -file a builder reads with `scad.file_input(...)`; its content snapshot is -recorded in the definition. `@scad.assemble(definitions=...)` names the -external part and assembly definitions an assembly references; its assembly -is solved strictly. - -## Product package export - -A notebook's product, or a builder's result, is written as one canonical -product package: - -```python -scad.capture(run.definition, "out/bracket.scadpkg") -scad.capture(build_plate(), "out/plate.scadpkg") - -root = scad.load_product_package("out/bracket.scadpkg") -rebuilt = scad.materialize_definition(root) -``` - -Use `build_product_package(...)`, `encode_product_package(...)`, -`read_product_package(...)`, and `load_product_package(...)` only for explicit -in-memory package handling. `validate_product_package(...)` verifies every object -hash and size, the complete recursive definition graph, cycle/depth limits, and -rejects missing or unreferenced objects. Use `export_part_definition(...)` or -`export_assembly_definition(...)` only for low-level definition exchange or -inspection; neither is the product delivery format. - -## Downstream CAD and mesh targets - -Keep `.scadpkg` as the canonical SimpleCAD delivery artifact. Choose a -downstream target from the consumer contract: - -| Target | Use when | Preserved contract | Boundary | -| --- | --- | --- | --- | -| `.scadpkg` | Rebuild, replay, or further SimpleCAD tooling is required | Complete durable definition closure, feature graphs, source snapshots, topology, connectors, constraints, and materials | SimpleCAD-specific archive | -| `.FCStd` | A FreeCAD user needs an editable native document | Definition-owned, dependency-first native feature graphs; final body links; repeated instances; nested assemblies; names; solved placements; materials; connectors; constraints; grounding; revision; and content hashes | Requires FreeCADCmd/FreeCAD | -| AP242 `.step` | Neutral CAD exchange or downstream OpenCASCADE tooling is required | Evaluated BREP, product hierarchy, shared definitions, occurrence names/placements, materials, colors, density, and named SimpleCAD property payloads | Canonical feature history is not reconstructed as AP242 features | -| Triangle `.obj` | Surface inspection or DCC exchange needs an indexed mesh | Direct OpenCASCADE tessellation of evaluated BREP with shared vertices and oriented triangles | Evaluated surface mesh only; no CAD hierarchy, feature semantics, quads, or materials | -| Binary `.stl` | Additive manufacturing or triangle-only consumers | The same direct OpenCASCADE BREP triangles | Facet soup only; no shared vertices, CAD hierarchy, feature semantics, or materials | - -```python -package_path = "out/product.scadpkg" -scad.capture(run.definition, package_path) - -fcstd_path = scad.translator.freecad_translator.translate_product_package_to_fcstd( - package_path, - "out/product.FCStd", -) -step_report = scad.exporter.export_product_package_to_step( - package_path, - "out/product.step", -) -stl_report = scad.exporter.export_product_package_to_stl( - package_path, - "out/product.stl", -) -obj_report = scad.exporter.export_product_package_to_obj( - package_path, - "out/product.obj", -) -print(step_report.definition_ids, step_report.occurrence_count) -print(stl_report.solid_count, stl_report.triangle_count) -print(obj_report.vertex_count, obj_report.triangle_count) -``` - -STL and OBJ use the same direct OpenCASCADE tessellation and parameters. Set -`linear_deflection` to the maximum chordal deviation in product units and -`angular_deflection_degrees` to the curved-surface angular limit. Both formats -contain oriented triangles; OBJ preserves shared vertex indices while binary STL -stores each facet independently. No remeshing or optional dependency is involved. - -AP242 definition and occurrence metadata is stored in one standard -`PROPERTY_DEFINITION_REPRESENTATION`. Its named -`DESCRIPTIVE_REPRESENTATION_ITEM` records use -`SimpleCAD:definition::` and -`SimpleCAD:occurrence::`. Each description -is canonical sorted JSON containing durable IDs, revision, content hash, -connectors, constraints, grounding, name, and solved placement. -`ProductSTEPExportReport` reports the schema, definition IDs, occurrence -count, material IDs, metadata item count, and explicit limitations. - -For tetrahedral meshing, run the split example directly in dependency order: -the model is the `bracket.py` notebook, `export_step.py` captures its product -to `.scadpkg` (through `bracket_package.py`) and exports AP242 STEP, and -`export_fem_mesh.py` imports that STEP with Gmsh's OpenCASCADE kernel. The FEM -stage rejects imports with no 3D volumes, generates a dimension-3 mesh, writes -`.msh`, and always finalizes Gmsh. Install and invoke the optional dependency -with `uv run --extra gmsh python -examples/ap242_gmsh_volume_mesh/export_fem_mesh.py`; Gmsh is never imported -by the core SDK or product exporters. +- Composing notebooks (`scad.use`), part families (`scad.notebook_id()`), the cell cache, and library parts (`@scad.part`, `@scad.assemble`): `notebook-composition-and-cache.md`. +- Product package export (`scad.capture`), downstream targets (`.FCStd`, STEP, STL, OBJ) and the Gmsh meshing example: `product-export.md`. diff --git a/docs/skill/references/docs/guides/product-export.md b/docs/skill/references/docs/guides/product-export.md new file mode 100644 index 00000000..bd2cfddf --- /dev/null +++ b/docs/skill/references/docs/guides/product-export.md @@ -0,0 +1,88 @@ +# Product Package Export and Downstream Targets + +**Read when:** a notebook's product must be written as a `.scadpkg` or delivered to FreeCAD, STEP, STL, OBJ or a mesher. Running a notebook and parameters are in `notebook-runtime.md`. + +## Product package export + +A notebook's product, or a builder's result, is written as one canonical +product package: + +```python +scad.capture(run.definition, "out/bracket.scadpkg") +scad.capture(build_plate(), "out/plate.scadpkg") + +root = scad.load_product_package("out/bracket.scadpkg") +rebuilt = scad.materialize_definition(root) +``` + +Use `build_product_package(...)`, `encode_product_package(...)`, +`read_product_package(...)`, and `load_product_package(...)` only for explicit +in-memory package handling. `validate_product_package(...)` verifies every object +hash and size, the complete recursive definition graph, cycle/depth limits, and +rejects missing or unreferenced objects. Use `export_part_definition(...)` or +`export_assembly_definition(...)` only for low-level definition exchange or +inspection; neither is the product delivery format. + +## Downstream CAD and mesh targets + +Keep `.scadpkg` as the canonical SimpleCAD delivery artifact. Choose a +downstream target from the consumer contract: + +| Target | Use when | Preserved contract | Boundary | +| --- | --- | --- | --- | +| `.scadpkg` | Rebuild, replay, or further SimpleCAD tooling is required | Complete durable definition closure, feature graphs, source snapshots, topology, connectors, constraints, and materials | SimpleCAD-specific archive | +| `.FCStd` | A FreeCAD user needs an editable native document | Definition-owned, dependency-first native feature graphs; final body links; repeated instances; nested assemblies; names; solved placements; materials; connectors; constraints; grounding; revision; and content hashes | Requires FreeCADCmd/FreeCAD | +| AP242 `.step` | Neutral CAD exchange or downstream OpenCASCADE tooling is required | Evaluated BREP, product hierarchy, shared definitions, occurrence names/placements, materials, colors, density, and named SimpleCAD property payloads | Canonical feature history is not reconstructed as AP242 features | +| Triangle `.obj` | Surface inspection or DCC exchange needs an indexed mesh | Direct OpenCASCADE tessellation of evaluated BREP with shared vertices and oriented triangles | Evaluated surface mesh only; no CAD hierarchy, feature semantics, quads, or materials | +| Binary `.stl` | Additive manufacturing or triangle-only consumers | The same direct OpenCASCADE BREP triangles | Facet soup only; no shared vertices, CAD hierarchy, feature semantics, or materials | + +```python +package_path = "out/product.scadpkg" +scad.capture(run.definition, package_path) + +fcstd_path = scad.translator.freecad_translator.translate_product_package_to_fcstd( + package_path, + "out/product.FCStd", +) +step_report = scad.exporter.export_product_package_to_step( + package_path, + "out/product.step", +) +stl_report = scad.exporter.export_product_package_to_stl( + package_path, + "out/product.stl", +) +obj_report = scad.exporter.export_product_package_to_obj( + package_path, + "out/product.obj", +) +print(step_report.definition_ids, step_report.occurrence_count) +print(stl_report.solid_count, stl_report.triangle_count) +print(obj_report.vertex_count, obj_report.triangle_count) +``` + +STL and OBJ use the same direct OpenCASCADE tessellation and parameters. Set +`linear_deflection` to the maximum chordal deviation in product units and +`angular_deflection_degrees` to the curved-surface angular limit. Both formats +contain oriented triangles; OBJ preserves shared vertex indices while binary STL +stores each facet independently. No remeshing or optional dependency is involved. + +AP242 definition and occurrence metadata is stored in one standard +`PROPERTY_DEFINITION_REPRESENTATION`. Its named +`DESCRIPTIVE_REPRESENTATION_ITEM` records use +`SimpleCAD:definition::` and +`SimpleCAD:occurrence::`. Each description +is canonical sorted JSON containing durable IDs, revision, content hash, +connectors, constraints, grounding, name, and solved placement. +`ProductSTEPExportReport` reports the schema, definition IDs, occurrence +count, material IDs, metadata item count, and explicit limitations. + +For tetrahedral meshing, run the split example directly in dependency order: +the model is the `bracket.py` notebook, `export_step.py` captures its product +to `.scadpkg` (through `bracket_package.py`) and exports AP242 STEP, and +`export_fem_mesh.py` imports that STEP with Gmsh's OpenCASCADE kernel. The FEM +stage rejects imports with no 3D volumes, generates a dimension-3 mesh, writes +`.msh`, and always finalizes Gmsh. Install and invoke the optional dependency +with `uv run --extra gmsh python +examples/ap242_gmsh_volume_mesh/export_fem_mesh.py`; Gmsh is never imported +by the core SDK or product exporters. diff --git a/docs/skill/references/docs/stdlib/make_ball_bearing_rassembly.md b/docs/skill/references/docs/stdlib/make_ball_bearing_rassembly.md index 4a349a0a..55e34e18 100644 --- a/docs/skill/references/docs/stdlib/make_ball_bearing_rassembly.md +++ b/docs/skill/references/docs/stdlib/make_ball_bearing_rassembly.md @@ -3,7 +3,7 @@ ## API Definition ```python -def make_ball_bearing_rassembly(bore_diameter: float, outer_diameter: float, bearing_width: float, ball_diameter: float, ball_count: Optional[int] = None, raceway_clearance: float = 0.02, edge_chamfer: float = 0.0, assembly_id: str = 'ball_bearing', drive_angle_degrees: Optional[float] = None, fuse_rolling_elements: bool = False, rolling_element_fuse_overlap: float = 0.01, material: Optional[Material] = None) -> Assembly +def make_ball_bearing_rassembly(bore_diameter: Length, outer_diameter: Length, bearing_width: Length, ball_diameter: Length, ball_count: Count | None = None, raceway_clearance: Length = 0.02, edge_chamfer: Length = 0.0, assembly_id: str = 'ball_bearing', drive_angle_degrees: Angle | None = None, fuse_rolling_elements: bool = False, rolling_element_fuse_overlap: Length = 0.01, material: Material | None = None) -> Assembly ``` *Source: std/bearing.py* diff --git a/docs/skill/references/domains/addon-development.md b/docs/skill/references/domains/addon-development.md index bc2370cb..e2a80284 100644 --- a/docs/skill/references/domains/addon-development.md +++ b/docs/skill/references/domains/addon-development.md @@ -4,6 +4,8 @@ Author, install, and manage SimpleCADAPI addons: third-party packages that add non-modeling capabilities — simulation, analysis, downstream tooling — by consuming `.scadpkg` product packages. +**Read when:** you create, publish, install, update or remove a SimpleCADAPI addon (`sca-addon.toml`), or decide whether a capability is an addon or SDK work. + ## Use when - Creating or publishing an addon (a repository with `sca-addon.toml` and diff --git a/docs/skill/references/domains/assembly-and-product.md b/docs/skill/references/domains/assembly-and-product.md index 649e5361..1455b8f9 100644 --- a/docs/skill/references/domains/assembly-and-product.md +++ b/docs/skill/references/domains/assembly-and-product.md @@ -1,5 +1,7 @@ # Task Domain: Assembly and Product +**Read when:** the deliverable has more than one physical part, repeated instances, nested subassemblies, motion semantics, or a `.scadpkg` package (details under "Use when"). + Compose multi-part products with explicit placement, connectors, and declarative constraints; compose notebooks with `scad.use`, build durable definitions (notebook products, `@part`/`@assemble` library parts), and @@ -20,12 +22,22 @@ deliver `.scadpkg` packages. - Monolithic single-body manufacturing intent: fuse with `union_rsolid` in `domains/part-modeling.md`. +- Several bodies of one part that never move apart and are one BOM item + (a multi-body part, `make_part_rpart(bodies=...)`): `domains/part-modeling.md`. - Export or translation decisions: they branch from the package in `domains/export-and-translation.md`. ## Structure model -- `Part` wraps exactly one `Solid` plus identity and optional material - (`make_part_rpart`, `assign_material_rpart`, `make_material_rmaterial`). +- `Part` wraps one `Solid` (or several named bodies, `bodies=`) plus identity + and optional material (`make_part_rpart`, `assign_material_rpart`, + `make_material_rmaterial`). In an assembly a multi-body part is one component; + its bodies are never placed or jointed separately. +- Display colour: `Material.color` is the default for every body of a part; + `set_body_appearance_rpart(part, body_id, color=..., opacity=..., name=...)` + overrides it per body for display (preview, package scene node + `properties.color`, STEP surface colour). It is recorded and replayed, and a + part definition stores it in an optional `appearance` block (a part without + one keeps its bytes). Assembly components keep the colours of their parts. - `Assembly` holds component instances with placements (`make_assembly_rassembly`, `add_component_rassembly`, `place_component_rassembly`, `make_placement_rplacement`, @@ -85,10 +97,35 @@ composing notebooks. `NotebookConfig`, `sca run` CLI. - Product build: `part`, `assemble`, `file_input`, `PartBuildResult`, `AssemblyBuildResult`. +- Exploded view: `explode_rassembly(assembly, factor, *, center, axis, + directions, weights)` returns a display copy whose component placements are + pushed apart (see "Exploded view" below). - Packages: `capture`, `CaptureResult`, `ProductPackage`, `build_product_package`, `read_product_package`, `load_product_package`, `validate_product_package`. +## Exploded view + +`explode_rassembly(assembly, factor, ...)` is a pure function of the assembly: +each top-level component moves by `factor * weight * direction * size`, where +`size` is the diagonal of the box that holds all component bodies, so +`factor=1` is a clear explosion and `factor=0` gives the same placements. +`direction` is `directions[id]`, else `axis` (signed by the side of the centre), +else from the assembly centre (`center`, default the centre of the bodies' box) +to the component's own centre. A component with no direction (at the centre) +stays and is listed in `get_metadata("explode")`; to keep a component, such as +the grounded base, put it in `weights` as `0` (nothing is special-cased). + +```python +view = scad.explode_rassembly(assembly, 1.0, weights={"base": 0.0}) +view = scad.explode_rassembly(assembly, 0.6, axis=(0, 0, 1), directions={"cover": (1, 0, 0)}) +``` + +The result is for display, rendering and export of an exploded view: parts, +constraints, grounding and public connectors are unchanged and the constraints +are not re-solved, so solving the result moves the components back. It is +recorded in the graph and replays from model JSON. + ## Validation gates - Placement semantics: moving a component changes placement only; the part's diff --git a/docs/skill/references/domains/curve-surface-editing.md b/docs/skill/references/domains/curve-surface-editing.md new file mode 100644 index 00000000..b39e384e --- /dev/null +++ b/docs/skill/references/domains/curve-surface-editing.md @@ -0,0 +1,72 @@ +# Task Domain: Editing Curves and Surfaces (control points, weights, knots, degree) + +Change an existing curve or surface the way a NURBS editor does (drag a pole, weight it, add a knot for local control, +raise the degree), as ordinary recorded operations. Each one returns a NEW Edge or Face; the input is never changed. + +**Read when:** an existing Edge or Face must be reshaped by its control points, weights, knots or degree, refitted +(`rebuild_*`), or sampled for a curvature comb / isocurves; or a freeform face built with `make_bezier_surface_rface`, +`fit_point_grid_rface`, `trim_surface_rface`, Gordon or a patch must be adjusted after it was built. + +## Use when + +- A freeform face or spline edge is close but must be adjusted locally ("lift this corner", "flatten this edge"). +- A surface needs more control in one region: insert a knot there, then move the new poles. +- An analytic face or edge (plane, cylinder, line, arc) must become freeform so it can be bent. + +## Do not use + +- The shape is better rebuilt from its inputs: change the profile / point grid / curve network of the constructor + instead (parametric intent stays in the constructor's arguments). +- A face that is part of a solid must stay part of a closed body: an edit returns a free face (no shell or solid is + rebuilt around it). Sew the edited faces again (`sew_faces_rshell`, `make_solid_from_shell_rsolid`) when needed. + +## The loop: read, edit, check + +1. `inspect_curve_rcurvedata(edge)` / `inspect_surface_rsurfacedata(face)`: degree(s), `poles`, `weights`, knots, + multiplicities, domain, `editable`, `trimmed`. Pole indices are 0-based; a surface's `poles` are rows along U, each + row along V (the shape `make_bezier_surface_rface` takes). Coordinates are in the current coordinate system. +2. If `editable` is false (`type` is `line`, `circle`, `plane`, `cylinder`, ...), convert explicitly: + `convert_to_bspline_redge` / `convert_to_bspline_rface` (recorded; the shape does not change). The edits refuse + analytic geometry with a message naming the conversion; nothing converts silently. +3. Edit (each recorded, each returns a new shape): + +| Goal | Curve (Edge) | Surface (Face) | +| --- | --- | --- | +| Move some poles | `move_curve_poles_redge(e, {1: (x, y, z)})` | `move_surface_poles_rface(f, {(1, 2): (x, y, z)})` | +| Set all poles | `set_curve_poles_redge(e, poles)` | `set_surface_poles_rface(f, grid)` | +| Weights (rational) | `set_curve_weights_redge(e, ws)` | `set_surface_weights_rface(f, ws_grid)` | +| Local control | `insert_curve_knot_redge(e, t, multiplicity=1)` | `insert_surface_knot_rface(f, "U", u)` | +| Smoother / more poles | `elevate_curve_degree_redge(e, 4)` | `elevate_surface_degree_rface(f, u_degree=3)` | +| Refit (degree, pole count or tolerance) | `rebuild_curve_redge(e, 3, 8)` | `rebuild_surface_rface(f, 3, 3, 6, 6)` | + +4. Check: `inspect_*` again; `sample_curve_rcurvature(edge, 64)` for a curvature comb (points, normals, + curvatures, `max_curvature`); `sample_surface_risocurves(face, 5, 5)` for isocurves clipped to the trimming. + +Knot insertion and degree elevation never change the shape (only the control net). A knot parameter must lie strictly +inside the edge's `parameter_range` / the surface's `domain`; the knot's total multiplicity is at most the degree. + +## Trimmed faces + +A trimmed face (holes, an outer loop that is not the surface's own boundary) keeps its trimming exactly: the loops stay +where they are in (u, v), and the 3D boundary edges are recomputed on the edited surface. So a hole moves with the +surface as it bends. `rebuild_surface_rface` keeps the parametrisation for the same reason. An edit that makes the +boundary invalid (folds the surface over itself) fails with the operation's error; nothing is returned half-built. + +## Tags and references + +Every edit records its history: the face and its boundary edges are `modified` (continuation). A tag assigned with +`apply_tag_rselection` on the face or on one of its edges, and a stable reference (`scad.ref`), follow the +entity through any chain of edits (with or without a `GraphSession`), and `ql.tag(...)` finds it on the result. +`tag_prefix=` only names the result of the operation it is passed to (`.face` / `.edge`), as for every +other operation: name the entities that must keep their name with `apply_tag_rselection`. + +```python +face = scad.make_bezier_surface_rface(grid) +face = scad.apply_tag_rselection(face, [face], "skin") # a name that follows the face +data = scad.inspect_surface_rsurfacedata(face) # degrees, poles, knots ... +face = scad.insert_surface_knot_rface(face, "U", 0.5) # one more row of poles +face = scad.move_surface_poles_rface(face, {(2, 1): (1.0, 1.0, 2.5)}) +assert ql.faces().where(ql.tag("skin")).exactly(1).resolve(face) +``` + +Read the API page of each operation under `references/docs/api/` before the first call. diff --git a/docs/skill/references/domains/drawing-inspection-evidence.md b/docs/skill/references/domains/drawing-inspection-evidence.md new file mode 100644 index 00000000..8f4dd3f3 --- /dev/null +++ b/docs/skill/references/domains/drawing-inspection-evidence.md @@ -0,0 +1,133 @@ +# Task Domain: Drawing Inspection Evidence (model sections and executable checks) + +**Read when:** a section view of the drawing must be verified against the rebuilt model, a DIM ledger row needs its +`measurement_contract`, or the roundtrip evidence (annotations, reviews, coverage) is built or checked. + +Companion of `domains/drawing-inspection.md` (the parsing SOP, tiers and report skeleton). Everything below feeds the +roundtrip in `workflows/drawing-reconstruction/roundtrip-acceptance.md`. + +## Model-section evidence group + +Cut the rebuilt model where the drawing cuts it; these functions take a live +shape (pass ``solid.wrapped``) or a STEP path and an explicit plane, and stay +factual — plane semantics belong to the caller: + +- `measure_model_section_rdimensions` — distinguish analytic circle geometry, + fitted-circle candidates, continuous geometry extrema/distances and sampled + approximations by each record's method and accuracy fields. A fitted circle's + residual is not a dimensional error bound. Section intersection tolerance is + not total measurement uncertainty. Unknown error bounds stay unknown. + Circle diagnostics include conditioning, angular coverage/max gap and both + RMS/max residual. Local arcs, open/gapped contours and nearly collinear + samples stay unverified; do not modify CAD dimensions from unreliable fits. +- `render_model_section_rpath` — annotated section render (45° hatch on + material only, dimension leaders with `dim_id` + measured value + sheet + value, mm scale bar). Fixed output: the section PNG and sidecar. The + renderer displays caller data, including wrong numbers; rendering success + does not prove measurement correctness. Formal evidence must use the bound + annotation builder and independent validator. The caller owns the comparison: put the render next to the drawing's own + viewport crop (same evidence directory) and judge — no composite is + generated for you. + +### Explicit section strategy and integrity evidence + +Result version **2.2** adds `section_strategy="auto"|"material"|"edges"`. +Auto uses material-face intersection for solids even if edge contours would +already be closed, and edge intersection for non-solid diagnostic inputs. +Material mode retains the actual edges and topological outer/inner wire roles +from the same intersection. Edges mode remains available; with formal checks +it also obtains an independent material-face reference. Environment variable +`SIMPLECADAPI_MATERIAL_SECTION` no longer selects the workflow strategy. +Archive both the requested strategy and the actual `contour_method`. + +Each formal DIM ledger row must reference the same independently reviewed +`section_checks` contract as its measurement. For a tube cut at z=10, an example +contract is: + +```json +{ + "expected_origin": [0, 0, 10], + "expected_normal": [0, 0, 1], + "expected_x_direction": [0, -1, 0], + "expected_y_direction": [1, 0, 0], + "expected_solid_count": 1, + "expected_face_count": 1, + "expected_hole_count": 1, + "material_points": [{"id": "wall", "point": [9, 0]}], + "void_points": [{"id": "bore", "point": [0, 0]}], + "point_tolerance_mm": 0.0000001, + "tolerance_basis": "Point-classification precision for this fixture", + "evidence": "Drawing view and independent feature-association review" +} +``` + +The example's points assume an outer radius of 10 and inner radius of 8; +choose actual points from the intended drawing geometry. Expected counts and +directions come from the drawing/confirmed requirements, never from copying the +current result. Points are section-local mm. Every material face needs an +interior material point; every hole needs an interior void point. Boundary or +indeterminate classifications stay pending. Empty void lists are allowed only +when no topological hole needs coverage; include points in any additional +drawing-defined open cavity or gap. The coordinate-frame direction comparison +uses the recorded numerical threshold 1e-10; point tolerance is not a dimensional +error bound. + +`section_validation` separately reports source validity/closure/solid count, +section closure, valid material faces and inner/outer rings, and each point's +classification against the original solid, material faces, and displayed +polygons. Formal rendering checks its own actual sampling density. Open or +incomplete sections may render as `diagnostic_only`, but cannot become formal +evidence. Missing section checks are not auto-filled from measured geometry. + +On failure, inspect the section definition, intersection and display first. +`section_definition`, `section_intersection` and `section_display` failures +do not justify changing the model. Only independent solid evidence, confirmed +against the drawing, can establish a model-geometry error. Mass, center of mass, +inertia and high-precision volume matching remain outside drawing-reconstruction +acceptance unless the user separately requests them. + +### Executable evidence checks + +Read the exact API pages for `inspect_drawing_coordinates_rreport`, +`assess_dimension_rverdict`, `build_section_annotation_rrecord`, +`validate_section_annotations_rreport`, and +`summarize_dimension_coverage_rreport`. The latter requires the complete expected +DIM list; omitted rows count as missing in every coverage column. + +Result version **2.1** requires an independent `measurement_contract` in each +ledger row. For example, a section's width is +`{"kind":"extent_width","definition":"axis_aligned_extent","direction":[1,0], +"point":null,"units":"mm","coordinate_space":"section_local"}`. +Height uses `extent_height` and `[0,1]`. Diameter uses `circle_diameter`, +minimum gap uses `minimum_boundary_gap`, both with null direction/point. +Directional material thickness uses `material_length_on_line`, a normalized +local direction and the specified local point. Geometry IDs bind the selected +contour; this separate contract binds what is measured on it. Missing contracts +cannot be inferred from whichever measurement happens to fall inside tolerance. + +The repository script `tools/verify_drawing_evidence.py --model part.step +--evidence evidence.json --out verification.json` independently remeasures and +reports all three gates. Its input contains `plane`, the complete `ledger`, +bound `annotations`, and `output_reviews` keyed by DIM ID. An output review +contains `layout_status: reviewed`, `association_status: verified` and independent +`evidence` references, plus the `dim_id`, `measurement_id`, `model_hash` and +`section_id` captured **when the image was reviewed**. Its `render_artifact` +contains absolute `image_path`, `sidecar_path`, and the reviewed +`image_sha256`, `sidecar_sha256`. The renderer records the image hash and each +annotation's content-derived ID in the sidecar. Validate both file hashes, +annotation content (including anchors and precision) and measurement identity. +Never replace old review IDs or hashes with current values during verification. +Old or missing identities require a new review of the actual current artifact. +Missing visual review remains pending even when numeric checks pass. + +Formal annotations also check the actual formatted value: the display step +must be no wider than the tolerance band, rounding error no larger than half +its width, and the displayed number itself inside the band. Zero-width bands +require exact representation. For 10.4 mm in [10.39,10.41], zero decimals are +rejected; one decimal also lacks the required resolution. Geometric conformance +continues to use the unrounded value and its uncertainty interval. + +An external total-error assessment must be tied to the exact `measurement_id` +and include `bound_mm`, `basis`, and `evidence`. These fields record a separate +metrology assessment; the SDK cannot establish its truth just by reading them. +Do not turn a fitted-circle residual or kernel edge tolerance into this bound. diff --git a/docs/skill/references/domains/drawing-inspection.md b/docs/skill/references/domains/drawing-inspection.md index 3845e0a8..2a6ea993 100644 --- a/docs/skill/references/domains/drawing-inspection.md +++ b/docs/skill/references/domains/drawing-inspection.md @@ -1,5 +1,8 @@ # Task Domain: Engineering-Drawing Inspection (Vector PDF) +**Read when:** a vector-PDF engineering drawing must be parsed into tiered evidence, or a rebuilt model is read back +for the roundtrip. + Extract traceable evidence from vector-PDF engineering drawings with the `simplecadapi.inspect.drawing` namespace: health probing, text-layer inventory, rendered viewports, and calibrated vector measurement. Everything @@ -92,85 +95,9 @@ Read the exact page under `references/docs/api/` for every API used: verdict) and `measure_drawing_rmeasurements` (point distances, stroke lengths, mm conversion through an accepted calibration). -## Model-section evidence group - -Cut the rebuilt model where the drawing cuts it; these functions take a live -shape (pass ``solid.wrapped``) or a STEP path and an explicit plane, and stay -factual — plane semantics belong to the caller: - -- `measure_model_section_rdimensions` — distinguish analytic circle geometry, - fitted-circle candidates, continuous geometry extrema/distances and sampled - approximations by each record's method and accuracy fields. A fitted circle's - residual is not a dimensional error bound. Section intersection tolerance is - not total measurement uncertainty. Unknown error bounds stay unknown. - Circle diagnostics include conditioning, angular coverage/max gap and both - RMS/max residual. Local arcs, open/gapped contours and nearly collinear - samples stay unverified; do not modify CAD dimensions from unreliable fits. -- `render_model_section_rpath` — annotated section render (45° hatch on - material only, dimension leaders with `dim_id` + measured value + sheet - value, mm scale bar). Fixed output: the section PNG and sidecar. The - renderer displays caller data, including wrong numbers; rendering success - does not prove measurement correctness. Formal evidence must use the bound - annotation builder and independent validator. The caller owns the comparison: put the render next to the drawing's own - viewport crop (same evidence directory) and judge — no composite is - generated for you. - -### Explicit section strategy and integrity evidence - -Result version **2.2** adds `section_strategy="auto"|"material"|"edges"`. -Auto uses material-face intersection for solids even if edge contours would -already be closed, and edge intersection for non-solid diagnostic inputs. -Material mode retains the actual edges and topological outer/inner wire roles -from the same intersection. Edges mode remains available; with formal checks -it also obtains an independent material-face reference. Environment variable -`SIMPLECADAPI_MATERIAL_SECTION` no longer selects the workflow strategy. -Archive both the requested strategy and the actual `contour_method`. - -Each formal DIM ledger row must reference the same independently reviewed -`section_checks` contract as its measurement. For a tube cut at z=10, an example -contract is: - -```json -{ - "expected_origin": [0, 0, 10], - "expected_normal": [0, 0, 1], - "expected_x_direction": [0, -1, 0], - "expected_y_direction": [1, 0, 0], - "expected_solid_count": 1, - "expected_face_count": 1, - "expected_hole_count": 1, - "material_points": [{"id": "wall", "point": [9, 0]}], - "void_points": [{"id": "bore", "point": [0, 0]}], - "point_tolerance_mm": 0.0000001, - "tolerance_basis": "Point-classification precision for this fixture", - "evidence": "Drawing view and independent feature-association review" -} -``` - -The example's points assume an outer radius of 10 and inner radius of 8; -choose actual points from the intended drawing geometry. Expected counts and -directions come from the drawing/confirmed requirements, never from copying the -current result. Points are section-local mm. Every material face needs an -interior material point; every hole needs an interior void point. Boundary or -indeterminate classifications stay pending. Empty void lists are allowed only -when no topological hole needs coverage; include points in any additional -drawing-defined open cavity or gap. The coordinate-frame direction comparison -uses the recorded numerical threshold 1e-10; point tolerance is not a dimensional -error bound. - -`section_validation` separately reports source validity/closure/solid count, -section closure, valid material faces and inner/outer rings, and each point's -classification against the original solid, material faces, and displayed -polygons. Formal rendering checks its own actual sampling density. Open or -incomplete sections may render as `diagnostic_only`, but cannot become formal -evidence. Missing section checks are not auto-filled from measured geometry. - -On failure, inspect the section definition, intersection and display first. -`section_definition`, `section_intersection` and `section_display` failures -do not justify changing the model. Only independent solid evidence, confirmed -against the drawing, can establish a model-geometry error. Mass, center of mass, -inertia and high-precision volume matching remain outside drawing-reconstruction -acceptance unless the user separately requests them. +Model-section evidence (`measure_model_section_rdimensions`, `render_model_section_rpath`, the `section_checks` +contract), the `measurement_contract` of each ledger row and the executable evidence checks are in +`domains/drawing-inspection-evidence.md`. ## Section-plane parsing rules (deterministic, never asked) @@ -287,51 +214,6 @@ interpretation, model measurement and output-expression verdicts. Coverage counts `value_verified`, `association_verified`, and `layout_reviewed` separately, with missing DIM IDs for each column. -### Executable evidence checks - -Read the exact API pages for `inspect_drawing_coordinates_rreport`, -`assess_dimension_rverdict`, `build_section_annotation_rrecord`, -`validate_section_annotations_rreport`, and -`summarize_dimension_coverage_rreport`. The latter requires the complete expected -DIM list; omitted rows count as missing in every coverage column. - -Result version **2.1** requires an independent `measurement_contract` in each -ledger row. For example, a section's width is -`{"kind":"extent_width","definition":"axis_aligned_extent","direction":[1,0], -"point":null,"units":"mm","coordinate_space":"section_local"}`. -Height uses `extent_height` and `[0,1]`. Diameter uses `circle_diameter`, -minimum gap uses `minimum_boundary_gap`, both with null direction/point. -Directional material thickness uses `material_length_on_line`, a normalized -local direction and the specified local point. Geometry IDs bind the selected -contour; this separate contract binds what is measured on it. Missing contracts -cannot be inferred from whichever measurement happens to fall inside tolerance. - -The repository script `tools/verify_drawing_evidence.py --model part.step ---evidence evidence.json --out verification.json` independently remeasures and -reports all three gates. Its input contains `plane`, the complete `ledger`, -bound `annotations`, and `output_reviews` keyed by DIM ID. An output review -contains `layout_status: reviewed`, `association_status: verified` and independent -`evidence` references, plus the `dim_id`, `measurement_id`, `model_hash` and -`section_id` captured **when the image was reviewed**. Its `render_artifact` -contains absolute `image_path`, `sidecar_path`, and the reviewed -`image_sha256`, `sidecar_sha256`. The renderer records the image hash and each -annotation's content-derived ID in the sidecar. Validate both file hashes, -annotation content (including anchors and precision) and measurement identity. -Never replace old review IDs or hashes with current values during verification. -Old or missing identities require a new review of the actual current artifact. -Missing visual review remains pending even when numeric checks pass. - -Formal annotations also check the actual formatted value: the display step -must be no wider than the tolerance band, rounding error no larger than half -its width, and the displayed number itself inside the band. Zero-width bands -require exact representation. For 10.4 mm in [10.39,10.41], zero decimals are -rejected; one decimal also lacks the required resolution. Geometric conformance -continues to use the unrounded value and its uncertainty interval. - -An external total-error assessment must be tied to the exact `measurement_id` -and include `bound_mm`, `basis`, and `evidence`. These fields record a separate -metrology assessment; the SDK cannot establish its truth just by reading them. -Do not turn a fitted-circle residual or kernel edge tolerance into this bound. ## Discipline notes diff --git a/docs/skill/references/domains/editor-collaboration.md b/docs/skill/references/domains/editor-collaboration.md index 841de786..7feb256b 100644 --- a/docs/skill/references/domains/editor-collaboration.md +++ b/docs/skill/references/domains/editor-collaboration.md @@ -1,11 +1,18 @@ # Editor collaboration: working with a person in `sca edit` +**Read when:** a person has the code-first editor open (`sca edit`) and you edit the same notebooks (a selection, a cell reference or a tuned value arrives). + Use this domain when a person has the code-first editor open (`sca edit`) and you work on the same notebooks: they click, drag and tune; you read files and edit code. It is not a workflow. The modeling you do still follows the workflow you selected; this page says how to see what the person sees and how to hand work back. +**Unattended:** there is no person and no editor. Do not start `sca edit`; edit the notebook file directly, check it with +`sca run ` and read its report instead of the snapshot, the viewport image or a selection cell. A selection +you were handed in the task is resolved and checked like any selector. Never wait for a person to run, pick or tune +anything (`discipline/operating-modes.md`). + The notebook file is the shared state. The editor autosaves every edit to it, and every message the person gives you (a selection, a cell reference, a tuned value) is also something you can read from disk. Never rely on what the @@ -64,7 +71,7 @@ sca cell insert --after 3 -f new.py # add a cell (--after start|end|CELL, --nam sca cell rm 4 sca cell run [CELL ...] # run cells (default all) and what they feed sca cell open other.py # have the editor open another notebook (waits for its kernel) -sca new bracket --kind part # a new notebook; the open editor lists it, the person's view stays +sca new bracket --kind part # a new, empty notebook; the open editor lists it, the person's view stays ``` - A cell is named by its number in `sca cell ls`, its `key`, its function name, or its id. @@ -90,7 +97,7 @@ sca new bracket --kind part # a new notebook; the open editor lists it, `sca snapshot` only has an answer while the editor runs; it reports the last state the editor wrote. If the snapshot is old (`updated`), ask the person to -move the viewport or re-select, or read the notebook file instead. +move the viewport or re-select, or read the notebook file instead. (Unattended: read the notebook file.) ## The selection cell @@ -126,7 +133,8 @@ selected_item = [selected_edge_1, selected_edge_2] - A selector says where, not what: confirm before changing geometry. Resolve it and print small QL facts (count, a measurement) per the global rules. If it matches nothing the person's model changed since the pick: ask them to pick - again rather than guessing. + again rather than guessing. (Unattended: no selection will arrive; derive the selector from the task's evidence, print + its card, and record it in the report.) - Do not rename `selected_*` variables or delete the cell; the editor finds it again by its first line. When the person is done with the selection, they clear it themselves. @@ -140,7 +148,7 @@ selected_item = [selected_edge_1, selected_edge_2] name. Keep one feature per cell; give it a feature header. 4. Save the file. The editor shows the change (cells you edited show `stale` until run). Ask the person to run it, or run `sca run ` yourself - to check the model builds, and read the errors. + to check the model builds, and read the errors. (Unattended: always `sca run`.) ## Cell references @@ -189,12 +197,36 @@ Errors there are the same errors `sca snapshot` reports per cell. A module that does not compile is reported as `setup failed` with the file and line; fix that file first, since every cell is blocked until it compiles. +## When a step fails: the model before it + +A cell that fails (a parameter change that breaks a fillet, an edit) leaves no +values, and the cells after it do not run, so the product is not made. The +viewport then shows **the model before the failing step**, not the last model +that built: for a part notebook the open bodies of the cells that are left (the +body the failing feature was applied to); for an assembly built up over cells, +the assembly before the failing cell. A red notice in the viewport says which +step failed and why, and the snapshot carries it as `partial`: +`{failed: [{cell, name, status, error}], skipped: [cell ids that did not run], +shown: [bodies on screen]}` (`null` when the model is whole). Fix the failing +cell; the whole model replaces it. When nothing before the failing cell makes a +model (a broken params cell, an assembly made in one cell), the last model stays +on screen and the error names the failing cell. Exports never use a partial +model: they fail until the cell is fixed. + +## Features the person made with buttons + +Cells with a `# gui:` line, the `sca feature` / `sca sketch` commands, sketches, workplanes and `scad.ref` selections +made in the editor: read `editor-gui-features.md`. + ## Rules of thumb - Re-read the cell before editing (`sca cell get`, or the file): the person edits and autosaves too, and the editor rewrites the selection cell. - Edit by cell, with `sca cell`. A change that touches many cells at once makes the person's screen reflow and loses their place. +- A notebook the person deletes in the workspace tab (manual or code mode) is moved to + `/.sca-editor/trash/