diff --git a/CHANGELOG.md b/CHANGELOG.md index 9feca027..da6ab6f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,25 @@ user-facing notes live in [`docs/updates/`](docs/updates/) (English and ## Unreleased +### 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/SKILL.md b/docs/skill/SKILL.md index 653b4257..06627b6a 100644 --- a/docs/skill/SKILL.md +++ b/docs/skill/SKILL.md @@ -90,5 +90,12 @@ it as a blocker in the report and do the strongest supported work. - `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. +- `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 d295b8c8..58bd4efe 100644 --- a/docs/skill/references/README.md +++ b/docs/skill/references/README.md @@ -54,9 +54,11 @@ Stage and role pages of the long workflows: | --- | --- | | `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`). | diff --git a/docs/skill/references/discipline/core-rules.md b/docs/skill/references/discipline/core-rules.md index df4e1e95..d30545d0 100644 --- a/docs/skill/references/discipline/core-rules.md +++ b/docs/skill/references/discipline/core-rules.md @@ -20,8 +20,9 @@ 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 and return -exactly one `Solid`. Union defaults to `glue=False` with a conservative scale-relative tolerance and fails explicitly when +**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`, diff --git a/docs/skill/references/discipline/datums-and-coordinate-systems.md b/docs/skill/references/discipline/datums-and-coordinate-systems.md index 5cf5c4c1..c40c900a 100644 --- a/docs/skill/references/discipline/datums-and-coordinate-systems.md +++ b/docs/skill/references/discipline/datums-and-coordinate-systems.md @@ -43,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 41f386f4..015e68bd 100644 --- a/docs/skill/references/discipline/failure-and-repair.md +++ b/docs/skill/references/discipline/failure-and-repair.md @@ -47,9 +47,10 @@ which claims cannot be made (see "Reporting failed repairs"). Interactive: the r - 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. @@ -59,7 +60,9 @@ which claims cannot be made (see "Reporting failed repairs"). Interactive: the r `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 diff --git a/docs/skill/references/discipline/geometric-validation.md b/docs/skill/references/discipline/geometric-validation.md index e3e0fe77..aaf69e5b 100644 --- a/docs/skill/references/discipline/geometric-validation.md +++ b/docs/skill/references/discipline/geometric-validation.md @@ -53,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. diff --git a/docs/skill/references/discipline/mechanical-modeling.md b/docs/skill/references/discipline/mechanical-modeling.md index 22b7b4e5..0bbc491a 100644 --- a/docs/skill/references/discipline/mechanical-modeling.md +++ b/docs/skill/references/discipline/mechanical-modeling.md @@ -22,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 1ccae00c..5db26d79 100644 --- a/docs/skill/references/discipline/notebook-cell-design.md +++ b/docs/skill/references/discipline/notebook-cell-design.md @@ -86,8 +86,15 @@ 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 @@ -95,9 +102,9 @@ 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 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/notebook-runtime.md b/docs/skill/references/docs/guides/notebook-runtime.md index fa182137..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 @@ -87,6 +105,12 @@ if __name__ == "__main__": (`run_notebook(..., id=...)`, `scad.use(..., id=...)`, `sca run --id`); cells read the id of their run with `scad.notebook_id()` — see [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 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/assembly-and-product.md b/docs/skill/references/domains/assembly-and-product.md index 7f314743..1455b8f9 100644 --- a/docs/skill/references/domains/assembly-and-product.md +++ b/docs/skill/references/domains/assembly-and-product.md @@ -22,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`, @@ -87,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/editor-collaboration.md b/docs/skill/references/domains/editor-collaboration.md index 6a59c31e..7feb256b 100644 --- a/docs/skill/references/domains/editor-collaboration.md +++ b/docs/skill/references/domains/editor-collaboration.md @@ -71,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. @@ -197,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/