Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
292045d
editor: probe the editor pid without signalling it (os.kill(pid, 0) i…
NiJingzhe Nov 9, 2026
654a6d3
part: a Part holds exactly one solid (the constructor refuses several…
NiJingzhe Nov 9, 2026
2c99826
python glue + replay: follow the one-solid Part (pickle through Body,…
NiJingzhe Nov 9, 2026
9fb9401
artifacts: a part definition holds one solid (drop BodyRef, `solid_ca…
NiJingzhe Nov 9, 2026
6c49c88
design-docs: record the decision that a Part holds exactly one solid …
NiJingzhe Nov 9, 2026
90fe84a
runtime + preview: several open solids are shown side by side, but ar…
NiJingzhe Nov 9, 2026
2029bee
scene + exporters: a Part is drawn, exported and measured from its on…
NiJingzhe Nov 9, 2026
84aab45
api: remove the helpers that only existed for a Part of several bodies
NiJingzhe Nov 9, 2026
d415647
editor: several open solids have no product until the author picks on…
NiJingzhe Nov 9, 2026
f016a14
editor-web: the "model shown" log says what the header says (open sol…
NiJingzhe Nov 9, 2026
910df75
AGENT.md: record how to rebuild the C++ module here, and why the obvi…
NiJingzhe Nov 9, 2026
0307aaa
tests: replace the multi-body Part tests by tests of the one-solid ru…
NiJingzhe Nov 9, 2026
7e2e996
cpp: comments no longer describe multi-body Parts
NiJingzhe Nov 9, 2026
ba19f43
cleanup: remove the package's per-body blob path check, and comments …
NiJingzhe Nov 9, 2026
6441107
docs/api: the API pages say a Part holds exactly one solid (drop `bod…
NiJingzhe Nov 9, 2026
fb50a97
docs/skill: domain, discipline, workflow and guide pages follow the o…
NiJingzhe Nov 9, 2026
01852ed
docs/skill: scadpkg-format says a part definition holds one solid (no…
NiJingzhe Nov 9, 2026
d0040a7
changelog + SDK_DEFECTS: record that a Part holds one solid again (an…
NiJingzhe Nov 9, 2026
a0923a6
docstrings: the source of the API pages say a Part holds one solid (a…
NiJingzhe Nov 9, 2026
b56634e
tests: the cell-cache test of restored Pieces makes its Part from one…
NiJingzhe Nov 9, 2026
f60f698
tests: the editor's API index test expects Part to take `body` and no…
NiJingzhe Nov 9, 2026
0a961d1
AGENT.md: write down how commits are organised (one decision per comm…
NiJingzhe Nov 9, 2026
07e0ca2
Merge pull request #82 from PhySpace/fix/editor-pid-probe
NiJingzhe Nov 9, 2026
695a03a
cpp: format part.hpp and bind_product_values.cpp with clang-format 18…
NiJingzhe Nov 9, 2026
5efd5bb
tests: do not depend on the order of the names a single cell defines …
NiJingzhe Nov 9, 2026
e01016c
Merge pull request #83 from PhySpace/fix/part-holds-one-solid
NiJingzhe Nov 9, 2026
d88289e
release: 2.2.9 (replaces the withdrawn 2.2.8)
NiJingzhe Nov 9, 2026
51c60a6
Merge pull request #84 from PhySpace/release/2.2.9
NiJingzhe Nov 9, 2026
453bad3
build: a plain uv sync builds the C++ module on Windows (toolchain an…
NiJingzhe Nov 10, 2026
342aae5
fix(artifacts): fingerprint quantization no longer splits a value tha…
NiJingzhe Nov 11, 2026
46635ce
fix(runtime, exporter): a script can model and export STEP in a read-…
NiJingzhe Nov 11, 2026
35a4e5e
fix(kernel): cut and intersect unify what they leave split, and a ble…
NiJingzhe Nov 11, 2026
af696ba
docs: regenerate the export API pages (the exporters now take a build…
NiJingzhe Nov 11, 2026
ee5668c
test(cpp): follow the clean-up in the goldens, guard the diagnosis bu…
NiJingzhe Nov 11, 2026
e86b9a3
Merge pull request #90 from PhySpace/fix/fingerprint-quantization-bou…
NiJingzhe Nov 12, 2026
ddafe56
Merge pull request #91 from PhySpace/fix/readonly-source-dir
NiJingzhe Nov 12, 2026
c1868f0
Merge pull request #92 from PhySpace/fix/boolean-clean-and-fillet-check
NiJingzhe Nov 12, 2026
8c48236
Merge pull request #86 from PhySpace/fix/windows-uv-sync
NiJingzhe Nov 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,72 @@ BAD: Quoting only human-team effort: "This would take 2 weeks." (Say: "2 weeks h
# Before editing or adding code, plan for module splitting rather than putting code in one file which is not human readable.

# If you have any experience during working, you can write it down here, with detailed reasons.

## Experience: rebuilding the C++ module on this Windows machine (2026-11-10)

Why this is written down: changing anything under `src/cpp` needs a rebuilt `_native` module, and the obvious command
failed three times before it worked. Each failure had a cause that is not visible from the error text.

- UPDATE (2026-11-10, later): none of the flags below are needed any more. `pyproject.toml` builds with
`tools/build_backend/scad_build.py`, which on Windows picks Ninja + the gcc on PATH when Visual Studio is absent, finds
OCCT (`SCAD_OCCT_PREFIX`, `C:/occt`, or `%LOCALAPPDATA%/simplecadapi/occt-<tag>`, building it there once if missing) and
limits the job count by RAM. A bare `uv sync` / `uv run` works; anything you set yourself (CMAKE_GENERATOR, CC, CXX,
CMAKE_ARGS, CMAKE_BUILD_PARALLEL_LEVEL) still wins. `[tool.uv] cache-keys` makes uv rebuild only when `src/cpp` changes,
not on a version bump. What remains true: a running editor locks the installed `.pyd` (rename it aside, see below).
- (Older note) Use the project's own path, `uv pip install --python .venv/Scripts/python.exe -e . --no-deps -v`. It reuses
`src/cpp/build/wheel/<tag>/` (incremental when the compiler flags did not change). Plain `ninja _native` in that
directory fails: its CMakeCache points into a temporary isolated build environment (ninja, pybind11, python) that was
deleted when the last build ended, so CMake cannot regenerate.
- A bare run picks the NMake generator (no `nmake` here) and then a stale OpenCASCADE from `%TEMP%\occt-build-*`. Pass
the MinGW toolchain and the real OCCT explicitly, in the same shell:
`CMAKE_GENERATOR=Ninja CC=<scoop gcc>/gcc.exe CXX=<scoop gcc>/g++.exe
CMAKE_ARGS="-DCMAKE_MAKE_PROGRAM=<scoop shims>/ninja.exe -DSCAD_OCCT_PREFIX=C:/occt -DOpenCASCADE_DIR=C:/occt/lib/cmake/opencascade"`.
(The first full rebuild after the environment changed took about 4 minutes; the next, after one binding file, 1m16s.)
- The build ends with "failed to remove ... Scripts/sca.exe" when an editor (`sca edit`) is running: that is only the
install step. The compiled module is already at `src/cpp/build/wheel/<tag>/bindings/python/_native.<tag>.pyd`.
The installed copy in `.venv/lib/site-packages/simplecadapi/_cpp/` is locked by the running processes (copy fails with
"Device or resource busy") but Windows lets you RENAME a loaded DLL: rename the old `.pyd` aside, copy the new one in.
New processes load the new module, running editors keep the old one. Do not kill the user's editor to install.
- The built web UI (`src/simplecadapi/editor/webui/`) is git-ignored: after changing `editor-web/src`, rebuild it with
`editor-web/node_modules/.bin/vite.exe build`, and typecheck with `node editor-web/node_modules/typescript/bin/tsc
--noEmit -p editor-web/tsconfig.json` (`npx tsc` downloads an unrelated package named tsc).
- `design-docs/` is git-ignored but some files in it are tracked: a new file there needs `git add -f`.
- `docstrings.json` is read back over the docstring in the Python source at import time, so a docstring edit is
invisible in `help()` until `_glue/docstrings.json` is updated too (edit the one line, do not re-dump the file).

## How to commit: one decision per commit, with the reasons (the owner's rule, 2026-11-10)

Why this rule exists: a change that reverses an earlier design (for example "a Part holds exactly one solid") is
made of many small edits across layers. Someone who takes over later sees the edits, not the conversation that
caused them. The commit history is where that conversation has to survive, so that handing over and tracing back
are possible without asking anyone.

1. **One commit per decision, split as finely as is sensible.** A decision is a choice that could have gone the
other way (a rule, a format, an API removal, a place where a feature lives). Edits that only follow from a
decision (tests, docs, comments, dead code) are their own commits, each stating which decision they follow.
Do not mix two decisions in one commit; do not split one decision so far that no commit holds its reason.
2. **Every message states the chain of the decision and WHY, and the why carries the core of the discussion**
between the owner and the assistant that led to it: the need that started it, the owner's judgement, the
analysis that was agreed, and what was rejected. Write it as what was said, not as a summary of the diff. The
reader must be able to see why the decision exists from the message alone.
3. **Message layout** (subject line, then paragraphs; the subject says the decision, not only the edit):
- `DECISION:` what is decided, in one or two sentences.
- `WHY` (name the discussion and its date; point to the decision record): the need, the judgement, the
analysis, and facts found by checking (with where and how they were checked).
- `ALTERNATIVE CONSIDERED AND REJECTED:` when there was one, and why.
- `WHAT CHANGES:` the files / behaviour, only as far as the reason needs; and what is kept on purpose.
- `Checked:` what was run and what it showed, including what was not verified.
- Last line: `Co-Authored-By: ...` as the session's attribution reminder says.
4. **A decision that outlives one commit gets a decision record** in `design-docs/<name>.md` (the whole
discussion, the audit table, the rules after the decision, the list of commits). The first commit of the
series carries the full account; later commits carry the short form and name that commit or the record.
`design-docs/` is git-ignored: add a new file with `git add -f`. If an older design document is replaced, mark
it superseded at its top and keep its body.
5. **Refer to other commits by subject line, not by hash.** Hashes change when history is rewritten (rewording a
message with `filter-branch` changed every hash of the series once); a subject survives. A hash is acceptable
only for a commit that is already published or will not be rewritten.
6. **Say the true thing.** If a message claims a check passed or a set of files is equal, run the check first; if
a claim turns out wrong, `git commit --amend` while the commit is still the newest and unpublished.
7. **Mechanics that worked here**: write the message to a file with the file tool and use `git commit -F file`
(long heredocs with quotes broke in this shell); stage by explicit paths, never `git add -A` (the untracked
`STILL` file is not ours); work on a branch, not on `master`; do not push or merge unless asked.
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,25 @@ All notable changes to SimpleCADAPI are documented here. Per-version
user-facing notes live in [`docs/updates/`](docs/updates/) (English and
中文).

## Unreleased
## 2.2.9 — 2026-11-10

Replaces 2.2.8, which was withdrawn (yanked on PyPI). User-facing notes: [`docs/updates/2.2.9.md`](docs/updates/2.2.9.md).

### Changed
- A `Part` holds exactly one solid again. The multi-body Part of the withdrawn 2.2.8 release (`make_part_rpart(bodies=...)`,
`Part.multi_body`, `scad.part_shape`, `inspect_body_overlaps_rlist`, `BodyOverlapWarning`, part definition profile
`simplecad-part-definition-3` with `solid_cache.bodies`) is removed: a Part is the unit an assembly places, mates and gives
connectors to, and a Part of several bodies could do none of that (no connectors, `explode_rassembly` raised). Several
solids that exist side by side while a model is built (a tool next to its body) are ordinary notebook values: a part
notebook with one open solid still gets the implicit Part; with several there is no product and the run raises
`SeveralOpenBodiesError` naming them. The editor draws them side by side ("N open solids") and its `use_as_product`
makes the Part from the one you choose. Several solids as a deliverable are one Part each in an Assembly.

### Fixed
- On Windows `sca cell` no longer kills the editor it talks to: the editor pid is probed without signalling it
(`os.kill(pid, 0)` terminates the process on Windows).
- Building from source with GCC 14 (`operators_promote.cpp` includes `<algorithm>`) and MinGW (`_USE_MATH_DEFINES`; `build_occt.py`
picks Ninja when a MinGW `g++` is the compiler); the macOS wheels are built on `macos-15` (still targeting macOS 14.0).

## 2.2.8 — 2026-11-09

Expand Down
4 changes: 4 additions & 0 deletions design-docs/multi-solid-design.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Multi-body parts (several solids in one part, without an assembly)

> **SUPERSEDED on 2026-11-10 by `part-holds-one-solid.md`.** The multi-body Part described here was withdrawn: a Part
> holds exactly one solid again. Read that document for the decision, the reasons and the discussion behind it. This
> one is kept only as the record of what was tried.

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.
Expand Down
98 changes: 98 additions & 0 deletions design-docs/part-holds-one-solid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# A Part holds exactly one solid (the multi-body Part is withdrawn)

Status: decided on 2026-11-10 and implemented on branch `fix/part-holds-one-solid`. This document **supersedes**
`multi-solid-design.md`. Read it first; the older document is kept only as the record of what was tried.

It is written so that someone who was not in the conversation can see what was decided, why, what was rejected,
and which commit carries which part. The commit messages of the series repeat the reasons in short.

## 1. What was decided

**A `Part` is one solid. Nothing else is a Part.**

- Several solids that exist side by side while a model is built (a tool and its target, the tools of a pattern before
the cut) are **loose values of a notebook**. The editor and the preview show them; they are not a product item, so
nothing places, mates, exports or weighs them as one thing.
- A deliverable that really is several solids is an **Assembly of Parts**: one Part per solid, identity placements.
- The Part constructor refuses anything but one solid, in the lowest layer (C++), so that no consumer has to branch.

## 2. The discussion it comes from (owner and assistant, 2026-11-10)

This is the reasoning that matters; keep it when the code changes.

**The need that created the multi-body Part (the owner).** The editor must support several solids existing in one
notebook, with no assembly constraint between them. This is easy to understand: to pattern a tool and then cut, there
is necessarily a moment where more than one tool and the body exist together. At the time a Part was the only product
value a notebook could end in, so the Part was made to hold several named bodies.

**The owner's judgement.** That choice makes assembly hard. The whole design was made for a human-friendly editor,
and it led to a very wrong decision: designing a multi-solid Part. The owner asked whether we should go back to
"a Part has exactly one solid" and support the human-friendly editor by some other, better means.

**The assistant's analysis, accepted by the owner.**

1. The solids side by side in the pattern-then-cut example are *intermediate values of a notebook*, not product
items. A Part is a line of the bill of materials and the unit an Assembly places, mates and gives connectors to.
"The working set of solids while modelling" and "the product" are two levels that were mixed into one type.
2. So the editor's need is a *preview / notebook-value* need (show every open solid), not a Part need. The runtime
already had the pieces: `cut_rsolid` returns `Pieces`, a pattern returns a solid list, and the scene had a
shape-root path.
3. The honest exit from "several solids" to a deliverable is an Assembly (`split_rpart` in the old design was
exactly that exit).
4. Keeping multi-body and finishing connectors (a connector anchored by `body_id` + entity) was costed and rejected:
it changes the part-definition contract, the interface / binding hashes and the entity bindings, and still leaves
a type that overlaps with Assembly ("an assembly without constraints").

**Urgency (the owner).** The multi-body Part might be a malignant bug of the released 2.2.8, so the audit and the
change were to start at once, and the GitHub release `v2.2.8` was deleted (the tag `v2.2.8` still exists on origin;
a PyPI upload, if there was one, is not undone by that).

**How to work (the owner).** One commit per decision, and every commit message states the chain of the decision and
its reasons, so that anyone can take over and trace it later. Split decisions as finely as sensible.

## 3. What the audit found in 2.2.8 (code read and run, 2026-11-10)

| Finding | Where | Effect |
|---|---|---|
| Connectors on a multi-body Part are refused | `Part` constructor (`part.cpp`) | no mate, public connector or `ConnectorRef` can reach one; it joins an assembly only as a rigid blob placed by its origin |
| `explode_rassembly` raises `TypeError` | `ops_explode.cpp` `collect_box` calls `Part::body()` | any assembly holding a multi-body Part cannot be exploded |
| the scene compiler reads `part.body` | `scene/compiler.py` (4 places) | `compile_scene` fails on a multi-body Part |
| `build/definition.py` passed `part.body` to the connector interface | `definition.py` | would break the moment connectors were allowed |

What still worked: a multi-body Part as a component with a placement, flattening an assembly to a compound, the
preview, STEP, mass, the package and the cell cache. Not checked: joint / constraint solving, interference, MJCF
(they depend on connectors, which a multi-body Part could never have).

One-body Parts were not affected by any of this.

## 4. The design after the decision

| Concern | Rule |
|---|---|
| Part | exactly one solid; `Part.body` always works; `Part.bodies` is a one-element tuple kept because `Body` carries the display name, colour and opacity (`set_body_appearance_rpart`) |
| `make_part_rpart` | `body=` a Solid, a `Body`, or a list of exactly one piece; `bodies=` does not exist; several pieces raise `ValueError` that says what to do |
| Part definition (durable) | profile 2, one solid; profile 3 and `solid_cache.bodies` are removed (they only ever shipped in the withdrawn 2.2.8) |
| Notebook with several open solids | no product is chosen for the author: `find_product` raises `SeveralOpenBodiesError` naming the solids and what to do |
| Preview of such a notebook | the open solids are drawn side by side (`compile_open_bodies`, node ids `node/<notebook id>/body:<name>`), so the editor keeps what it needed |
| Several solids as a deliverable | an Assembly of Parts |
| `inspect_body_overlaps_rlist`, `BodyOverlapWarning`, `part_shape`, `part_from_bodies` | removed: they only existed for a Part of several bodies |
| `measure_part_mass_rmassreport` | kept; one row per Part |

`Pieces` (the result of a splitting `cut_rsolid`, and fillet / chamfer / per-piece operations) is unaffected: it is a
notebook value, which is where several solids belong.

## 5. The commits of the series (decision by decision)

Each line is one decision; the commit message holds its reasons.

1. `part: a Part holds exactly one solid` - the invariant, in C++ and the bindings.
2. `python glue + replay: follow the one-solid Part` - pickling through `Body`; replay refuses a recorded multi-solid Part.
3. `artifacts: a part definition holds one solid` - drop profile 3 and `solid_cache.bodies`.
4. further commits (preview of open solids, runtime product rule, removal of the helpers, scene / exporters, editor,
tests, docs) are listed in `git log fix/part-holds-one-solid` after the commit that adds this file.

## 6. Open points

- Whether anything outside this repository (a PyPI upload of 2.2.8, the editor plugins) must be withdrawn or
re-released is a release decision, not made here.
- The tag `v2.2.8` was left in place; moving or deleting it is the owner's call.
4 changes: 2 additions & 2 deletions docs/skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ description: Build, assemble, inspect, reconstruct, and export parametric CAD mo
license: Apache-2.0
metadata:
project: simplecadapi
version: 2.2.8
version: 2.2.9
package-name: simplecadapi
package-version: 2.2.8
package-version: 2.2.9
---

# SimpleCAD SDK Skill
Expand Down
4 changes: 2 additions & 2 deletions docs/skill/references/SDK_OVERVIEW.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# SDK Overview

- Project: `simplecadapi`
- Version: `2.2.8`
- Package distribution: `simplecadapi==2.2.8`
- Version: `2.2.9`
- Package distribution: `simplecadapi==2.2.9`

## What this skill bundles

Expand Down
4 changes: 3 additions & 1 deletion docs/skill/references/discipline/core-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ the durable export call `capture(result, path)`, whose two required arguments ar
notebook; one assembly notebook per product, composing children with `scad.use(...)`; parameters live in the notebook that
consumes them. Cell boundaries, `_private` names and `var()` follow `discipline/notebook-cell-design.md`; block structure
and the mandatory header `# ---- feature: <slug> (<role>) ----` follow `discipline/feature-tree-convention.md`. Read both
before writing any cell.
before writing any cell. The one exception is a task that itself asks for a single script
(`python model.py --output part.step`, an unattended batch): that is a plain script, see "One-shot script" in
`domains/export-and-translation.md`; the notebook rules do not apply to it.

**R4. Sketch first.** 2D profiles and planar paths go through the sketch API; primitives only when the shape is entirely
the basic form.
Expand Down
5 changes: 2 additions & 3 deletions docs/skill/references/discipline/failure-and-repair.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,8 @@ 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; if it is one
part whose bodies stay apart, make a multi-body part
(`make_part_rpart(bodies=...)`).
actually multiple parts, the answer is an assembly, not a union: a Part holds
exactly one solid, so make one Part per solid and put them in an Assembly.

### Fillet/chamfer failure

Expand Down
7 changes: 4 additions & 3 deletions docs/skill/references/discipline/mechanical-modeling.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ 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.
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.
A Part holds exactly one solid: several solids that belong together are one
Part per solid in an Assembly (identity placements if they only sit side by
side). Never leave stray solids (tools, construction) out of the product 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`.
Expand Down
Loading
Loading