Repository navigation
Conversation
…s a POSIX liveness probe; on Windows os.kill maps signal 0 to TerminateProcess, so every sca cell call killed the editor it was talking to and then failed with WinError 87)
…; make_part_rpart and Part lose `bodies=`) DECISION: the product item `Part` is one solid again. The multi-body Part added on 2026-11-04 (b607c85, design-docs/multi-solid-design.md) is withdrawn. This commit is the C++ half; the Python consumers, the editor and the docs follow in the later commits of this series, one decision each. WHY - THE DISCUSSION THIS DECISION COMES FROM (owner and assistant, 2026-11-10) 1. Why a Part was ever allowed several solids (the owner's account). The editor must support several solids existing in one notebook with no assembly constraint between them: to pattern a tool and then cut, there is necessarily a moment where more than one tool and the body exist together. That is easy to understand and it is a real need. At the time the Part was the only product value a notebook could end in, so the Part was made to hold several named bodies. 2. Why it is the wrong place (the owner's judgement). It makes assembly hard. The whole design was made for a human-friendly editor and it led to a wrong decision: a multi-solid Part. The question put was: should we go back to "a Part has exactly one solid" and support the editor some other, better way? 3. The assistant's analysis, which the owner accepted. 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" were two levels that got mixed. The editor's need is a PREVIEW / notebook-value need (show every open solid), and a deliverable that really is several solids is an Assembly of Parts. So: Part = one solid; the editor shows open solids as loose values (later commit). 4. Urgency. The owner asked for the audit and the change at once because the multi-body Part might be a malignant bug in the released 2.2.8; its GitHub release was deleted on 2026-11-10 while this was done. WHAT THE AUDIT FOUND (checked on 2026-11-10 against the code and by running it) - Connectors on a multi-body Part were refused outright (Part constructor, part.cpp), so no mate, no public connector and no ConnectorRef could ever reach one: it could join an assembly only as a rigid blob placed by its origin. - explode_rassembly raised TypeError on any assembly that held one (ops_explode.cpp collect_box called Part::body(), which throws for a multi-body Part). - The scene compiler scene/compiler.py read part.body in four places (compile_scene fails on a multi-body Part). Every consumer needed a second code path, and some were missed. Not yet checked: joint / constraint solving, interference and MJCF paths (they depend on connectors anyway). ALTERNATIVE CONSIDERED AND REJECTED: keep multi-body Parts and finish stage 3 of the design (a connector anchored by body_id + entity). It changes the part-definition contract, the interface and binding hashes and the entity bindings, and it still leaves a concept that overlaps with Assembly (a multi-body part is "an assembly without constraints"). WHAT THIS COMMIT DOES - The invariant is enforced in the lowest layer so that every consumer can rely on it instead of each branching on `multi_body`: Part(std::vector<Body>) throws ValueError unless there is exactly one body, and the message says what to do (one Part per solid in an Assembly, or union / cut to one solid). Part::multi_body() is gone. - Binding: make_part_rpart(part_id, body, name) and Part(part_id, body, ...) take one Solid, one Body, or a list of exactly one piece. `bodies=` is removed (TypeError: unexpected keyword); more than one piece is a ValueError with the same guidance. Part(body=Body) is how a body that carries a name / colour / opacity survives pickling and copying (it replaces Part(bodies=(Body,))). - The make_part_rpart(vector<Body>) overload and the mapping / sequence parsing helpers are removed. KEPT ON PURPOSE: Body (it carries the body's display name, colour and opacity), Part::bodies() (always one element), body_ids(), get_body(), set_body_appearance_rpart: one-body Parts already use them and removing them would only churn the one-body path, which did not break. Verified with the native module directly: a Solid, a one-element list and a Body are accepted; `bodies=`, two solids and Part(body=[a, b]) are refused with the guidance. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… refuse a recorded multi-solid make_part_rpart) DECISION: the Python value layer and the model-JSON replay follow the rule of 654a6d3 (a Part holds exactly one solid). No behaviour is added; two places that knew about several bodies are narrowed. WHY (the discussion in short; the full account is in 654a6d3): the several-solids-in-one-Part design existed so that the editor could hold a tool and its target side by side with no assembly constraint (pattern a tool, then cut). The owner judged that a wrong design for an editor's convenience, because it makes assembly hard; the conclusion reached with the assistant is that those solids are intermediate values of a notebook, not product items, so a Part is one solid again. Everything below only removes what assumed the opposite. - _glue/values.py (pickle / deepcopy of a Part): a Part used to be rebuilt from either `body` or `bodies`. Now `body` is always the argument: the Solid when the body is plain, and the `Body` when it carries a name, colour or opacity, so an appearance still survives the notebook cell cache (Part(body=Body) is accepted by the binding for exactly this). - _glue/session.py: a Part's children for lineage are its one body solid, its connectors and material. - recording/serializer.py (replay of make_part_rpart): the multi-body branch is gone. A model JSON recorded by a development build that held several solids in one Part (params["bodies"] with more than one row; only SDK 2.2.8 development builds ever recorded it, 2.2.7 never did) now fails with a message that says what to do instead of being replayed into something the SDK no longer has. The unused Body import is removed. Why refuse instead of converting such a model to one Part per solid: the part ids and the assembly that would hold them are the author's decision, not something replay can invent. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…che.bodies` and profile 3) DECISION: the persistent PartDefinition goes back to exactly one evaluated Solid, which is what it was in SDK 2.2.7. The multi-body extension (b607c85, 9778a8e) is removed together with the Part rule of 654a6d3. WHY (the discussion in short; the full account is in 654a6d3): the owner explained that a Part was made multi-body only because the editor must show several solids side by side in one notebook with no assembly constraint (pattern a tool, then cut), and that this was a wrong design because it makes assembly hard. The conclusion with the assistant: those solids are notebook intermediates, not product items; a Part is one solid and an Assembly is how several solids are delivered. The durable format of a Part therefore has no reason to describe several bodies. Why remove the reader instead of keeping it for files already written - profile `simplecad-part-definition-3` and `solid_cache.bodies` were added after 2.2.7 and first shipped in 2.2.8, whose GitHub release was withdrawn on 2026-11-10 (the multi-body Part could not join an assembly). Nothing durable in the wild is written with it that we promise to read. - A reader for a profile that the writer can never produce again is dead code that must be kept consistent (connector checks, appearance rules, blob membership) for ever. What changes - artifacts/part_definition.py: BodyRef and the `bodies` field are gone; the profile is always simplecad-part-definition-2; the body appearance block (`appearance.bodies`, from set_body_appearance_rpart) stays, at most one row, and that row carries the name again. with_definition_appearance gives a Solid, or a Body when the definition has an appearance. - artifacts/canonical.py, artifacts/validation.py: the multi-body profile constant and the per-profile schema dispatch are removed; contracts/part-definition-3.schema.json is deleted. - artifacts/part_io.py: read_part_bodies -> read_part_body (one Solid, restored with its topology snapshot); the archive member set no longer lists per-body blobs. assembly_io.py builds the Part with Part(body=...). - build/definition.py: one BRep and one snapshot; no overlap warning, no Compound view, no body blobs. part-definition-2.schema.json is untouched: it already allows the appearance block and its solid_cache has additionalProperties false, so it never allowed `solid_cache.bodies`. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…(and mark multi-solid-design.md superseded) DECISION: write the decision down in the repository, with the discussion it came from, so that a person who was not in the conversation can take over and trace it. WHY: the multi-body Part was a design choice made for the editor's convenience, withdrawn on 2026-11-10 after the owner and the assistant talked it through (the owner's reasons: the editor needs several solids side by side with no assembly constraint, e.g. pattern a tool and cut; but making that a property of the Part makes assembly hard and was a wrong decision; the assistant's analysis: those solids are notebook intermediates, not product items, so Part = one solid and the editor shows loose solids). The owner also asked that every decision be its own commit with its reasons. Commit messages carry the short form; this file carries the whole chain, the audit table of what was broken in 2.2.8, the rules after the decision, and the list of commits. - design-docs/part-holds-one-solid.md: new; supersedes the older document. (design-docs/ is git-ignored, but the older design document is tracked, so this one is added the same way, with -f.) - design-docs/multi-solid-design.md: a note at the top says it is superseded and where to read instead; the body is left as it was (the record of what was tried). Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…e no longer a Part
DECISION: the editor's need (several solids existing in one notebook with no assembly constraint) is met
in the notebook / preview layer, not by the Part. A `kind = "part"` notebook that ends with exactly one open
solid still gets the implicit one-body Part; with several open solids no product is chosen for the author.
WHY - THE DISCUSSION (owner and assistant, 2026-11-10; full account in design-docs/part-holds-one-solid.md)
The owner's need: the editor must support several solids in one notebook with no assembly constraint, e.g.
pattern a tool and then cut - for a moment there are more than one tool and the body. The multi-body Part
was invented to hold them and was judged wrong because it makes assembly hard. The agreed analysis: those
solids are intermediate values of a notebook, not product items, so the editor's real need is to SHOW every
open solid. This commit is that replacement: it keeps what the editor needed and removes it from the Part.
WHAT CHANGES
- runtime/projection.py
- the implicit Part of a part notebook is made from its one open body. Several open solids (a variable
per solid, the pieces of a split cut counted as solids) raise the new SeveralOpenBodiesError, a
ProductNotFoundError that names the solids and says what to do: make the Part from the solid that is
the part, or make a Part of each and an Assembly of them. `sca run`, export and every caller that needs
a product therefore stop with an actionable message instead of silently choosing or fusing.
- OpenBodies (part_id + solids by name) and loose_solids() are the value a preview draws in that case.
- project_product rebuilds the implicit Part with make_part_rpart(body=...) (it used the multi-body rule).
- runtime/live.py: preview_product catches SeveralOpenBodiesError and returns OpenBodies, so the live
preview of an unfinished notebook (a tool next to its target) keeps working, including when a later cell
failed (the partial-model report names the shown solids as before).
- scene/preview.py
- compile_open_bodies(part_id, solids): one node per solid, `node/<notebook id>/body:<name>`, entity
namespace `entity/<notebook id>/body:<name>/...` (two solids of the same shape stay two). This is the
same wire shape the multi-body preview produced, deliberately: editor-web, element references
(live_refs) and entity_info need no change.
- compile_preview draws a Part as one node; its multi-body branch is removed.
WHY NOT choose one open solid automatically (e.g. the last): the tip is a guess, and exporting a Part that
silently left a solid out is worse than an error; the author knows which solid is the part.
Checked by running: one open body -> Part; two open bodies -> SeveralOpenBodiesError with the guidance;
compile_open_bodies header / entity_info for two same-shape solids; a one-body Part previews as before.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…e body (no per-body branches) DECISION: every consumer of a Part reads `part.body` again. The code that treated a multi-body Part as a set of child solid nodes, a Compound view or a list of named / coloured STEP solids is removed. WHY - THE DISCUSSION (owner and assistant, 2026-11-10; full account in design-docs/part-holds-one-solid.md) The multi-body Part was judged a wrong design (it was made so the editor could hold several solids with no assembly constraint, and it makes assembly hard); the agreed fix is Part = one solid, and the editor's several solids become loose preview values (commit "runtime + preview"). The audit of 2.2.8 showed what a second code path per consumer costs: each consumer had to branch on `multi_body`, and some did not (explode, the legacy scene compiler), which is how the release came to have crashes. With the invariant enforced in the Part constructor these branches can never run, so they are deleted rather than kept as dead code. WHAT CHANGES - scene/product_scene.py: the package scene has one GLB and one entity document per Part again; _body_geometries and the `node/<part>/body:<id>` child solid nodes are removed; _part_geometry no longer refuses; _appearance_properties reads the part's one appearance row. (The package-scene node tree of a one-body part is what it always was.) - exporter/step.py: the STEP product's shape is `part.body`; the body colour overrides the material colour as before; the `bodies` list of named / coloured solids is no longer built for a Part. The STEP writer's generic support for named solids (io::StepBody in C++) is left in place: it is the writer's ability, it has its own tests, and removing it would be a separate decision. - exporter/mjcf.py: the collision / visual mesh of a Part is `part.body`. - scene/prewarm.py, exporter/_surface_mesh.py, build/definition.py: only the comments that said "every body of a multi-body part" (the loops over `part.bodies` stay: it has one element). Checked: test_product_exporter, test_product_packages, test_scene_preview, test_scene_compiler and test_notebook_runtime pass. test_body_appearance fails on its multi-body fixtures; the tests are updated in a later commit of the series. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
DECISION: delete inspect_body_overlaps_rlist, BodyOverlap, BodyOverlapWarning, part_shape and part_from_bodies from the SDK, and narrow the text of measure_part_mass_rmassreport to one body. WHY - THE DISCUSSION (owner and assistant, 2026-11-10; full account in design-docs/part-holds-one-solid.md) A Part is one solid again, because the several solids the editor needs side by side are notebook values, not product items, and the multi-body Part made assembly hard. Each of these helpers answered a question that only exists when a Part holds several bodies: - inspect_body_overlaps_rlist / BodyOverlap / BodyOverlapWarning: "do two bodies of one Part share volume?" (a Part of one solid has none; checking loose solids against each other is a different, not requested, feature); - part_shape: "the Part as one shape, a Compound for several bodies" (it is `part.body`); - part_from_bodies: "the Part a notebook's named solids make" (the new rule is in runtime/projection.py: one open solid is the Part, several are an error that says what to do). They were public (`scad.*`) only since the multi-body work (after 2.2.7), and were first shipped by the withdrawn 2.2.8 release, so removing them breaks no supported release. WHAT CHANGES - product/bodies.py keeps body_display_color, body_appearance_fields, BodyMass, MassReport and measure_part_mass_rmassreport (the report has one row: the Part's body; `.bodies` is still a tuple so a caller that read `report.body(id)` or `report.bodies[0]` keeps working). - scad.__init__, operators/*, _glue/modules.json, _glue/docstrings.json (one entry removed, the mass entry re-derived from the new docstring) and the replay table of recording/serializer.py (the diagnostic entry of the removed operation) no longer list them. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…e (use_as_product), and say so
DECISION: the editor keeps letting solids exist side by side (a tool next to its body), but they are no longer
the bodies of one implicit Part. With exactly one open solid the Part is implicit as before; with several the
notebook has no product yet and the editor asks the author which solid is the part.
WHY - THE DISCUSSION (owner and assistant, 2026-11-10; full account in design-docs/part-holds-one-solid.md)
This is the editor need that started the whole multi-body Part: several solids in one notebook with no
assembly constraint (pattern a tool, then cut). The owner's judgement was that solving it inside the Part
makes assembly hard and was wrong; the agreed answer was "a Part is one solid, and the editor supports the
several solids some other way". This commit is that other way at the editor: the solids stay visible and
editable (the preview draws them, commit "runtime + preview"), and the product is chosen, never guessed. A
guess would silently leave a solid out of the exported Part; the author knows which solid is the part.
WHAT CHANGES
- editor/gui_features.py
- snapshot(): the implicit product exists only for exactly one open solid (and its dict loses `bodies`);
several open solids give `product: null` while `open` still lists them.
- use_as_product(): when the notebook has no product, a part notebook and a body that is open, it writes
the product cell `<stem> = scad.make_part_rpart(part_id=scad.notebook_id(), body=<chosen>)`. Before, it
refused ("no product cell"), which would have left the author without a way out.
- editor-web
- FeatureTree: a notice with one "use <solid>" button per open solid while there is no product and
several solids are open ("N solids are open, and a Part holds exactly one solid. Choose the part:").
The product badge reads the one product body.
- features.ts: FeatureState.product loses `bodies`.
- preview.ts previewSummary: nodes `node/<id>/body:<name>` are now the notebook's open solids, not the
bodies of a part, so the header says "N open solids" (zh-CN strings added) instead of "1 part · N bodies".
- tests: test_editor / test_editor_features follow the new product dict and the required `body` argument
of Part; a new test drives the editor flow end to end (two open solids -> no product, a headless run
raises SeveralOpenBodiesError naming them -> use_as_product -> the Part exists and the notebook runs).
Checked: editor-web vitest (331 tests) and tsc --noEmit pass; the editor test files pass except for the
multi-body fixtures of other files, which are updated in the tests commit of the series.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ids are not parts)
DECISION: the log line written when a preview arrives reuses previewSummary(), so it counts parts and open
solids the same way as the header.
WHY: found by driving the real editor in a browser (ego-browser) after the editor commit of this series.
A part notebook with two open solids (a body and a tool, no Part yet) showed "2 open solids" in the header
but "model shown: 2 parts" in the log: the log still counted every node as a part, which was true while a
Part could hold several bodies and is false now that a Part holds exactly one solid (see
design-docs/part-holds-one-solid.md; the owner's point was that the editor's side-by-side solids are
notebook values, not parts, so the editor must not call them parts).
- App.tsx: log.push("ok", ...) uses t("model shown: {{summary}}", { summary: previewSummary(nodes) }).
- zh-CN.json: the two plural strings it replaces are one string with the summary.
Checked in the browser: a part notebook with two open solids -> header "2 open solids", log "model shown: 2
open solids"; the same notebook with a make_part_rpart cell -> header "1 part". editor-web vitest (331) and
tsc --noEmit pass.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ous commands fail
DECISION: write the build experience of this series into AGENT.md, as that file asks ("if you have any
experience during working, write it down here, with detailed reasons").
WHY: the owner asked for a one-solid Part to be implemented urgently (see design-docs/part-holds-one-solid.md),
which needed a rebuilt native module, and the first three build attempts failed for reasons the errors do not
show: a CMakeCache pointing into a deleted temporary build environment, the NMake generator being chosen when
none is configured, a stale OpenCASCADE found in %TEMP%, and the installed .pyd being locked by the owner's
running editor. The next person (or agent) to change src/cpp on this machine should not rediscover them. The
same note records the web UI rebuild, the `npx tsc` trap, `git add -f` for design-docs, and that
docstrings.json shadows source docstrings.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…le (and of what the audit found broken) DECISION: test_multi_body_part.py becomes test_one_solid_part.py (git rename), and test_body_appearance.py is rewritten for the single body of a Part. The tests now state the rule of this series instead of the withdrawn design. WHY - THE DISCUSSION (owner and assistant, 2026-11-10; full account in design-docs/part-holds-one-solid.md) The owner's need was several solids side by side in one notebook with no assembly constraint (pattern a tool and cut); the multi-body Part was a wrong way to give it, because it made assembly hard; the agreed rule is Part = one solid, the editor shows loose solids, an Assembly delivers several. A rule that is not tested comes back: the audit of 2.2.8 found a multi-body Part refusing connectors and crashing explode_rassembly, and nothing had said so. So the tests include those two cases, now for every Part. test_one_solid_part.py - a Part has one body; several solids / `bodies=` are refused with the guidance; one solid in a list or a Body is accepted; pickle keeps body and tags; the mass report has one row; the removed helpers are gone; - every Part takes connectors, and an assembly of such Parts flattens, explodes (the audit's crash) and previews; several solids are delivered as an Assembly of Parts; - replay: a recorded Part replays, and a model recorded with several solids in one Part is refused; - preview: compile_open_bodies draws each open solid as its own node and entity namespace (two same-shape solids stay two), a Part is one node, nothing to draw is an error; - notebook: one open solid is the implicit Part; several raise SeveralOpenBodiesError naming them and saying what to do; the tool-cut-into-body example of the design leaves one open solid; a Part made of the chosen solid is the product, its package holds one solid (profile 2, no solid_cache.bodies) and exports one STEP solid, and the cell cache restores it; the live preview shows the open solids (OpenBodies); - split cuts still give Pieces (they are notebook values): the old Pieces tests are kept, and a Part takes one piece, not several. test_body_appearance.py: the appearance tests of a single body (colour, opacity, name, reset, material kept, pickle, replay, preview, legacy scene compiler, package scene with ONE node and no child solid nodes, STEP colour). The multi-body definition test is gone with profile 3. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
DECISION: only wording. ops_projection.cpp's loop over Part::bodies() stays (it has one element and the loop shared the code with the appearance carrier); its comment said "every body of a multi-body part". The on_split comment in the Python boolean test said the pieces of a split cut are "multi-body parts". WHY: a Part holds exactly one solid since "part: a Part holds exactly one solid" in this series (the owner's decision after the discussion recorded in design-docs/part-holds-one-solid.md: several solids side by side are notebook values, not a Part). A comment that says otherwise sends the next reader to the withdrawn design. No code changes, so the module that was built and verified is unaffected. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…that still said "multi-body part" DECISION: delete the one piece of dead code the one-solid change left behind, and correct comments that point at the withdrawn design. WHY: a Part holds exactly one solid (owner's decision after the discussion recorded in design-docs/part-holds-one-solid.md). The part-definition no longer has `bodies`, so product/packages.py's loop `for body in getattr(definition, "bodies", ()) or ()` (checking each body's BRep and snapshot path prefix) could never run; it only hid, by returning (), that the attribute is gone. Removing it with _checked_body_refs makes the package's blob check say exactly what a definition has. The comments in scene/preview.py (entity ids `entity/<id>/body:<name>/...`) and _glue/session.py described a "body of a multi-body part"; `body:<name>` entities now belong to the open solids of a notebook (compile_open_bodies), and the walk of product values lists a Part's children in _children. No behaviour change. Checked: test_product_packages, test_scene_preview and test_one_solid_part pass (59 tests). Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ies=`, multi-body wording and inspect_body_overlaps_rlist) DECISION: the generated-style API reference pages follow the code decision of the commit "part: a Part holds exactly one solid (the constructor refuses several; make_part_rpart and Part lose `bodies=`)". WHY - THE DISCUSSION (owner and assistant, 2026-11-10; full account in design-docs/part-holds-one-solid.md) The editor must let several solids exist in one notebook with no assembly constraint (pattern a tool, then cut: for a moment there are several tools and the body). The multi-body Part was made to hold them; the owner judged that a wrong decision because it makes assembly hard. The agreed analysis: those solids are intermediate values of a notebook, not product items, so a Part is one solid and the editor shows loose solids. These pages are what a model author (and the skill) reads, so they must not keep promising `bodies=`, a multi-body Part, per-body overlap checks or "connectors are not supported on multi-body parts yet". WHAT CHANGES - make_part_rpart.md, Part.md: signature `make_part_rpart(part_id, body, name=None)` / `Part(part_id, body, name, material, connectors)`; body is a Solid, a Body, or a list of exactly one piece; several solids raise ValueError that says what to do (one Part per solid in an Assembly, or union / cut to one solid); Part.bodies is a one-element tuple (Body carries display name, colour, opacity). - set_body_appearance_rpart.md: it paints the Part's one body (body id "body"); the read-back is part.bodies[0]. - cut_rsolid.md: pieces of a splitting cut are a notebook value; more than one piece cannot be one Part. - ref.face.md: "a multi-body result has one per body" -> an operation that makes several solids. - core/compound.md: a Part is one Solid; part_shape is gone; a Compound is not a way to hold several solids as one Part. - core/part_assembly_development_plan.md: "multi-body part authoring" removed from the out-of-scope list (it is not a planned lake any more). - README.md (API index) and inspect_body_overlaps_rlist.md: the operation was removed (commit "api: remove the helpers that only existed for a Part of several bodies"), so its page and index entry are deleted. NOT REGENERATED: tools/auto_docs_gen.py builds these pages from the docstrings in src/. Those docstrings (the C++ bindings' make_part_rpart / Part text and the cut_rsolid, ref and set_body_appearance_rpart docstrings) are owned by the code side of the series; until they say the same, a regeneration would bring the old text back, so the pages were edited by hand to the same words. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ne-solid Part (and the open-solids rule)
DECISION: the skill pages that tell a model author how to structure a part, a notebook and an editor session say
what the SDK now does: a Part holds exactly one solid; several solids that exist together while building are
notebook values; a part notebook with one open solid has an implicit Part, with several it has no product.
WHY - THE DISCUSSION (owner and assistant, 2026-11-10; full account in design-docs/part-holds-one-solid.md)
The multi-body Part was made so that the editor could hold several solids in one notebook with no assembly
constraint (pattern a tool, then cut). The owner judged it a wrong decision because it makes assembly hard; the
agreed analysis was that those solids are intermediate values of a notebook, not product items, so a Part is one
solid and the editor shows loose solids. The skill is what an agent reads before it writes a model: if it kept
recommending `make_part_rpart(bodies={...})` or "the runtime makes the Part from all open bodies", agents would
write code that now fails (or, worse, rely on a rule that no longer exists). The pages also have to tell the
author what to do instead, because the old advice was "keep them as bodies of the part".
WHAT CHANGES
- domains/part-modeling.md: "Multi-body parts" becomes "One Part, one solid" (refusal and its remedy: one Part per
solid in an Assembly, or union / cut; part.body / part.bodies; the open-solids rule and the editor's "N open
solids" / use_as_product; mass). The split-cut paragraph says the pieces are notebook values and several pieces
cannot be one Part. The colour section uses set_body_appearance_rpart(part, color=...) (body id "body") and
part.bodies[0] for the read-back.
- domains/assembly-and-product.md: a Part wraps one Solid; every Part can be placed, mated and given connectors;
the "several bodies of one part" entry in "Do not use" is removed (there is no such thing).
- discipline/notebook-cell-design.md C9: one open solid -> implicit Part; several -> SeveralOpenBodiesError, choose
the solid that is the part.
- discipline/mechanical-modeling.md, discipline/failure-and-repair.md ("Union returns multiple bodies"): the answer
for "several bodies" is an Assembly of Parts, not a multi-body Part.
- docs/guides/notebook-runtime.md: the implicit-Part rule and the "A Part holds exactly one solid" bullet; the part
definition is always profile simplecad-part-definition-2 with one solid.
- domains/editor-gui-features.md: "New body" leaves a loose solid beside the first ("N open solids"); use_as_product
makes the Part from the chosen one.
- workflows/single-part-modeling/role-4-build.md: a split cut's pieces are one Part each in an Assembly.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… solid_cache.bodies, no profile 3)
DECISION: the package-format reference states the part definition as it is again: profile
simplecad-part-definition-2, one solid, at most one appearance row.
WHY - THE DISCUSSION (owner and assistant, 2026-11-10; full account in design-docs/part-holds-one-solid.md)
The multi-body Part existed so the editor could keep several solids side by side with no assembly constraint
(pattern a tool, then cut); the owner judged it a wrong decision that makes assembly hard, and the agreed
analysis was that those solids are notebook intermediates, not product items, so a Part is one solid. The commit
"artifacts: a part definition holds one solid (drop BodyRef, `solid_cache.bodies` and profile 3)" removed the
durable format that described several bodies. This page is what a reader of a package follows, so it must not
tell them to expect `solid_cache.bodies` or a "multi-body part names its bodies" rule.
- docs/skill/references/scadpkg-format.md ("Definitions"): names the profile, the one-row appearance block
(colour and opacity, name), and says explicitly that `solid_cache.bodies` and profile 3 do not exist (they
only appeared in the withdrawn 2.2.8 release), so a reader who met them there knows they are gone.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…d what the multi-body notes now mean)
DECISION: say the change where a release reader and a maintainer look: the Unreleased changelog entry, and the
status lines of the defect log entries that were written for the multi-body research.
WHY - THE DISCUSSION (owner and assistant, 2026-11-10; full account in design-docs/part-holds-one-solid.md)
The editor needs several solids in one notebook with no assembly constraint (pattern a tool, then cut). The
multi-body Part was made for that and the owner judged it a wrong decision because it makes assembly hard; the
agreed analysis: those solids are notebook intermediates, not product items, so a Part is one solid and the editor
shows loose solids. The 2.2.8 release carrying the multi-body Part was withdrawn on 2026-11-10 after an audit
(connectors refused, explode_rassembly raised, the legacy scene compiler read part.body), so the next release's
notes must say that the feature is gone and what replaces it, and the defect log must not keep saying "worked
around by bodies".
- CHANGELOG.md, Unreleased / Changed: removed API (bodies=, Part.multi_body, part_shape,
inspect_body_overlaps_rlist, BodyOverlapWarning, profile 3 / solid_cache.bodies), the reason, the open-solids
rule (one open solid -> implicit Part; several -> SeveralOpenBodiesError; editor "N open solids" and
use_as_product) and the remedy for deliverables of several solids (an Assembly of Parts).
- examples/SDK_DEFECTS.md entries 27-30 (marked [multi-solid research]): status lines updated: 27 (Compound drops
tags) is still open but no product value is affected; 28 (pickling) names the new Part reader; 29 ("one solid per
definition") is resolved, the `body:` id segment now only names the open solids of a preview; 30 (error guidance
frozen by goldens): the guidance sentence is right again.
README.md and README.zh-CN.md were searched and say nothing about multi-body Parts, so they are unchanged.
sandbox/arm6 is untracked scratch work outside this worktree; it calls no `bodies=` (one comment mentions a
BodyOverlapWarning observation) and was left alone.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…nd the pages are regenerated from them)
DECISION: update the docstrings that are the source of the generated API reference (and of help() in a
notebook), then regenerate the pages with tools/auto_docs_gen.py, instead of leaving hand-edited pages that the
next regeneration would overwrite with the old multi-body text.
WHY: the docs commits of this series (cherry-picked from a parallel docs pass) were edited by hand because the
generator reads src/. They were right, but the sources they derive from still said "several named bodies",
`bodies={...}`, "connectors are not supported on multi-body parts yet" and "paints the lid body of a multi-body
part". The owner's decision (design-docs/part-holds-one-solid.md: the editor's several solids are notebook
values, a Part is one solid, several solids are an Assembly of Parts) has to hold in the text a user reads at
help(scad.make_part_rpart) too, and the generated pages must equal their sources or the next doc run undoes the
change.
- _glue/docstrings.json (read back over the Python / binding docstrings at import): make_part_rpart, Part, Body,
set_body_appearance_rpart, cut_rsolid (pieces are notebook values; one Part per piece) and apply_tag_rselection
(the body of a Part). measure_part_mass_rmassreport's source was already updated by "api: remove the helpers
that only existed for a Part of several bodies"; its page is regenerated here.
- ref.py: ref.face's `slot` text said "a multi-body result has one per body"; it is "an operation that makes
several solids has one per solid" (what the docs pass wrote by hand), so the source and the page agree.
- docs/skill/references/docs/api: Body, set_body_appearance_rpart, cut_rsolid, apply_tag_rselection and
measure_part_mass_rmassreport regenerated (LF endings; the generator writes CRLF on Windows and the committed
pages are LF). Checked: tools/auto_docs_gen.py's whole output (415 pages) equals the committed directory.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… piece DECISION: the notebook in test_cell_cache_store.py::TestPiecesComeBackAsPieces made `make_part_rpart(body=moved)` from the pieces of a split cut, which was a multi-body Part with bodies body_1 / body_2. A Part holds exactly one solid now (design-docs/part-holds-one-solid.md), so the last cell makes the Part from one piece. WHY: the test is about the cell cache restoring `Pieces` as `Pieces` (marimo stores a list as a plain list), not about multi-body Parts, so what it protects is kept: the restored split still feeds the next cell as pieces (kind == "Pieces", the translate runs per piece). Only the product cell and the final assertion changed: the Part is `moved[0]`, and the assertion compares the Part's body (id and the moved height: +1 mm between the first and the second run) instead of the number of `definition.bodies`, which no longer exists. Found by running the test files that mention bodies / Pieces after the one-solid change (178 other tests in those files passed). Checked: test_cell_cache_store.py passes (9 tests). Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… `bodies`
DECISION: update test_editor.py::ApiIndexTest::test_classes_list_their_constructor_without_private_fields to the
one-solid constructor: `part_id` and `body` are required, `bodies` is not a parameter.
WHY: the test said "a Part takes one body or several bodies (multi-body parts), so only part_id is required". A
Part holds exactly one solid since "part: a Part holds exactly one solid" in this series (owner's decision, see
design-docs/part-holds-one-solid.md), so `body` is required and `bodies` must not appear in the editor's index of
the SDK constructor signatures (the index is what the editor's forms and completion read). An earlier commit of
the series ("editor: several open solids have no product ...") fixed the first assertion of this test but left
this one, which a full run of the suite caught (the earlier run of the editor test files had passed it only
because the test then still matched the stale index).
Checked: test_editor.py::ApiIndexTest passes (6 tests).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…it, with the reasons from the discussion) DECISION: record the owner's commit rule in AGENT.md, so that it applies to every later session and not only to the series that introduced it. WHY: while the one-solid Part was being implemented (design-docs/part-holds-one-solid.md) the owner asked for every decision to be its own commit, split as finely as possible, with a message that states the chain of the decision and its reason, and said that the reason is essentially the discussion between the owner and the assistant, because people who take over or trace back later must be able to see from the commit message why the decision exists. The owner then asked for this to be written into AGENT.md, which is the file that tells an agent how to work in this repository. The section states the rule and its purpose, what counts as a decision, the message layout that the series used (DECISION / WHY / ALTERNATIVE REJECTED / WHAT CHANGES / Checked), the decision record in design-docs (with `git add -f`), the practice of naming other commits by subject because hashes changed once when the series' messages were reworded, the duty to run a check before claiming it, and the commit mechanics that worked in this environment. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
editor: sca cell must not kill the editor it talks to (Windows pid probe)
….1.8 (CI's clang-format job failed on them)
DECISION: only formatting, no behaviour change: re-wrap the header comment of part.hpp and the `bodies` property
definition in bind_product_values.cpp the way clang-format 18.1.8 writes them.
WHY: the CI job "clang-format" of the pull request for this series ("A Part holds exactly one solid") listed
exactly these two files ("needs formatting"). They were written by hand in the commit "part: a Part holds exactly
one solid" and the comment commit that followed, and the local build does not run clang-format (the project pins
18.1.8, installed here with `uv tool run --from clang-format==18.1.8 clang-format`). The reason for those edits is
unchanged (design-docs/part-holds-one-solid.md: a Part holds exactly one solid); this commit only makes them pass
the repository's format gate.
Checked: the eight C++ files the series touched are identical to clang-format 18.1.8's output (compared with
the CR characters removed). Note: running src/cpp/tools/check_format.sh with CLANG_FORMAT="uv tool run ..." reports
every file, because the script quotes the variable as one command name; use an installed clang-format instead.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…(Python 3.12 CI failed on it)
DECISION: the open solids a notebook leaves are compared as a set of names in the tests, and the docstring of
SeveralOpenBodiesError says the order is the cell order only across cells.
WHY: CI (Python 3.12) failed test_one_solid_part.py::TestNotebookProduct::test_several_open_solids_have_no_product
with `('tool', 'base') != ('base', 'tool')`. Both solids are defined by one cell, and the names a cell defines
come from a set, so their order is not fixed (it differed on 3.12; it can differ on any run). The behaviour that
matters (design-docs/part-holds-one-solid.md: several open solids give no product, and the error names them and
says what to do) does not depend on that order, and the editor's own list (`snapshot()["open"]`) is built from the
cell order and stays deterministic. So the test asserted more than the rule: it now asserts the set of names, in
this test and in the same assertion of test_editor_features.py (which passed locally by luck).
- runtime/projection.py: SeveralOpenBodiesError's docstring no longer says the names come "in cell order"
without qualification.
Checked: test_one_solid_part.py passes (28 tests). I did not make the order deterministic in the code: nothing
needs it, and the owner's rule is to do only what is necessary.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A Part holds exactly one solid (withdraw the multi-body Part); open solids are shown, not a Part
DECISION: release the one-solid Part and the fixes on dev as 2.2.9. The version number is 2.2.9, not 2.2.8 again and not 2.2.8.post1. WHY - THE DISCUSSION (owner and assistant, 2026-11-10) - 2.2.8 shipped a Part that could hold several bodies. The audit found such a Part could not take part in an assembly (no connectors, explode_rassembly raised); the owner judged the multi-body Part a wrong decision made for the editor's convenience, and the agreed answer was Part = one solid, the editor shows loose solids (design-docs/part-holds-one-solid.md, merged in the pull request "A Part holds exactly one solid"). - The owner deleted the GitHub release of 2.2.8 and yanked 2.2.8 on PyPI. The owner asked whether 2.2.8 could be published again: it cannot. PyPI keeps a version and its file names for ever, even when yanked or deleted, and does not accept another upload under them. So the fixed code needs a new number. - 2.2.9 rather than 2.2.8.post1: the change removes interface that 2.2.8 had (`bodies=`, `inspect_body_overlaps_rlist`, `part_shape`, profile 3), which a patch number says more plainly than a post-release of a withdrawn version; this repository's `.post1` / `.post2` releases (2.2.6) were for packaging fixes. WHAT CHANGES (the same files as "release: 2.2.8" touched) - pyproject.toml, uv.lock (the simplecadapi package entry), docs/skill/SKILL.md (version, package-version) and docs/skill/references/SDK_OVERVIEW.md: 2.2.8 -> 2.2.9. - CHANGELOG.md: the Unreleased entry of the one-solid change becomes 2.2.9 (2026-11-10), with a Fixed section for what else is on dev since the 2.2.8 tag: the Windows editor pid probe, GCC 14 and MinGW source builds, macOS wheels on macos-15. - docs/updates/2.2.9.md and 2.2.9.zh-CN.md: the update notes, which the release workflow turns into the release page. They say that 2.2.8 is withdrawn, why a Part holds one solid, what changes for users (the argument and helpers removed, SeveralOpenBodiesError, the editor's open solids) and what was fixed. Checked: nothing in the tests reads these files; the release workflow checks that the tag equals the pyproject version (v2.2.9). The wheels and the PyPI upload are built by the workflow when the tag is pushed. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
release: 2.2.9 (replaces the withdrawn 2.2.8)
…d OCCT detection, rebuild only when src/cpp changes) On a Windows machine without Visual Studio, scikit-build-core left the generator to CMake's default (NMake), the build failed with "'nmake' -? failed", and every contributor had to hand-set CMAKE_GENERATOR / CC / CXX / CMAKE_ARGS. uv also rebuilt the editable install on any pyproject.toml change (a version bump), which is how the failure showed up after the 2.2.9 release. - tools/build_backend/scad_build.py: a PEP 517 backend wrapping scikit-build-core. On Windows it uses Ninja + the gcc on PATH when Visual Studio is absent, finds OCCT (SCAD_OCCT_PREFIX, C:/occt, a per-user cache, or builds it once with tools/build_occt.py) and limits jobs by RAM (g++ ran out of memory under ninja's default). Anything the user or CI sets wins. - [tool.uv] cache-keys: rebuild only when src/cpp changes. Build tooling only: nothing in the published package changes, so no version bump. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…t sits on a boundary (#87) DECISION: snap the ratio value/tolerance to a binary grid before rounding it to an integer. Do not pre-round to a fixed number of decimals, do not make the snapshot validator tolerant of off-by-one integers. PROBLEM A body's geometry fingerprint is a hash of every measured value divided by 1e-7 mm and rounded to an integer (`_q`, artifacts/geometry_interface.py). A design value such as -3.34690815 mm is exactly a boundary of that rounding, (k + 0.5) * 1e-7. The value computed one way is a unit in the last place under the boundary, the same value after a BREP round trip is one over it (-3.3469081499999995 against -3.34690815), and `int(round(...))` sends the two to neighbouring integers. The reporter's body had two such edge centers: its fingerprint changed across the readback and `restore_topology_snapshot` raised `geometry_mismatch`. REPRODUCED - On the attachment of the issue (R02 BREP + recorded snapshot, read in this checkout, without running the attachment's own script): the fingerprint after the readback is exactly the issue's B (sha256:d9620b...), and the two edge centers quantize to -33469082 and -66530918, the integers the issue lists after the readback. - A whole solid that changes its fingerprint across a BREP round trip could not be built synthetically: the doubles survive the binary BREP unchanged, the one-ulp difference of the issue comes from how the readback shape is measured (centers are integrated). So the regression test is at the quantizer, with the issue's two value pairs and 20000 random values on a boundary with up to three ulps of noise on each side: before this change 6482 of them split, and both issue pairs failed. WHY THIS FIX - Pre-rounding to a fixed number of decimals either leaves large coordinates (a 1 m part is 1e10 quanta, one ulp is 2e-6 of a quantum) unprotected or moves 1% of all values to a different integer. The grid follows the ratio's magnitude (2**-46 of it, at least 2**-24): a half-integer is exactly on it, noise is well under it, and the window of values that change integer is about 1e-5 of a quantum (test: at most 5 of 50000 ordinary values move). - A tolerant validator (accept an off-by-one integer) would have to be taught every place a label is compared (body, face, edge and vertex labels, the topology match keys, the connector hashes); one deterministic quantizer fixes them all at the source. WHAT THIS DOES NOT FIX - Fingerprints are stored in artifacts. A value that was on a boundary now quantizes (half to even) where it used to fall on either side, so such a stored fingerprint changes once; every other value keeps its integer. The reporter's recorded snapshot (made on the other side of the boundary) still does not restore: it has to be captured again. It could not have been restored before either. - `exporter/_surface_mesh.py` and `inspect/brep/compare.py` round the same way for mesh welding and BREP comparison; they were not part of the report and are left alone. Checked: test_artifact_*, test_product_*, test_brep_region_snapshot, test_topology_getter_contract, test_part_interface, test_assembly_builder, test_native_canonical pass. test_scene_contract (generated types) and test_native_without_ocp fail on the base commit as well (this machine lacks OCP and the pinned generator). Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…only, unattended run (#89) DECISION: three changes for the three obstacles of the issue, each the smallest that removes the obstacle. No new API name, no relocatable state directory, no rewrite of rule R3. 1. A run without the cache touches nothing next to the source (the crash). PROBLEM: `run_notebook(path, cache=False)` failed on a read-only source directory (`Errno 30` on Linux, `WinError 5` here): `_run` ended with `write_dependencies`, which writes `__marimo__/.../deps.json` beside the notebook, and `exclusive()` made the state directory for its lock. REPRODUCED on Windows with an ACL that denies creating entries in the directory, on the issue's own notebook: PermissionError at runner.py `write_dependencies`. After the change the same run prints `ok` and creates nothing. WHY THIS FIX: the dependency record exists for the cache (cache entries are keyed on the files it lists); a run that keeps no entries has no reason to move it, and moving it left the record and the entries out of step. The lock exists to protect pruning and that record, neither of which a run without the cache does. A run WITH the cache still writes its state and still fails loudly when it cannot: asking for persistence and silently not getting it would be worse. REJECTED: an environment variable or argument that moves the state directory (more surface, and the problem is that there should be no state, not that it is in the wrong place); swallowing the OSError (hides real cache failures). 2. The exporters take a build result (one step, no `.scadpkg` on disk). PROBLEM: STEP from a freshly built Part needed `capture(result, path)` and then the exporter on that path, and the default capture tessellates a scene nobody reads: 1.85 s against 0.26 s for the issue's 36-hole plate. WHY THIS FIX: `build_product_package` already accepts a build result or a definition and can leave the scene out; `read_product_package_translation_units`, which every exporter and translator goes through, now builds that package in memory when it is given neither a package nor bytes nor a path. So STEP, STL, OBJ, MJCF and FreeCAD all gain the one-step form with the same validation, under the existing names. A value that is no product still raises TypeError. REJECTED: `exporter.step.export_part(part_or_solid, path)` as the issue suggests: a bare Part or solid has no definition (feature graph, topology snapshot, hashes), those come from `@scad.part`; and a second family of names beside `export_product_package_to_*` (the tests pin that these live only in `scad.exporter`). 3. The skill lists the plain script as an entry for a single-script task. PROBLEM: R3 says a model is a marimo notebook; an agent given "one script, `--output part.step`" read the skill and chose another library. WHY THIS FIX: R3 keeps its force for everything a person opens or reuses; it gains one exception, a task that itself asks for a single script, and the template (which I ran in a read-only directory: STEP written, source untouched) lives in the export domain page next to the rule it relaxes. THIS IS A POLICY DECISION OF THE OWNER, made here provisionally: R3 was a deliberate rule, and the PR says so. Not done: the issue's timings were not rerun on Linux, only on this Windows machine (same direction). Checked: test_notebook_runtime and the cache tests (123 passed), test_product_exporter/packages/capture/assembly pass with the three new exporter tests, test_skill_lint passes. test_skill_build::test_files_without_directives_copied_verbatim fails on the base commit as well. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…nd never returns an invalid solid (#88) DECISION: three changes, in this order of importance, each independent of the others. (1) a fillet or chamfer whose result OCCT calls invalid is an error; (2) cut_rsolid, cut_rsolid_list and intersect_rsolid unify the same-domain faces and edges of their result by default, with `clean=False` to keep the raw result, as union already does; (3) the diagnosis names split faces instead of blaming the radius. No compatibility shim for records made before `clean` existed: replay requires the parameter, like every other recorded one. PROBLEM (reproduced on the issue's two cases, current dev build on Windows, OCCT 7.9.3) A cut through the side of a box with a cylinder splits the cylinder wall along the cylinder's parametric seam into two faces, and the top edge of the notch into a long and a short arc. union unified (ShapeUpgrade_UnifySameDomain) and cut did not. OCCT then fillets that top edge wrongly: - case A: BRepCheck-invalid solid, volume 28305.59 (should be 29604.46), no error at all: `fillet_rsolid` checked only IsDone and volume > 0; - case B: refusal, and the diagnosis said "the radius exceeds the 2.5 mm of face room" - the half of the wall the seam cut off - so the advised radius was not the limit. Same geometry, other libraries: CadQuery and build123d clean after a boolean by default and give 29604.46 / 29811.31. (1) THE EXIT CHECK IS THE MOST IMPORTANT `assert_blend_valid`, next to the volume check in `finish_blend` (fillet and chamfer): BRepCheck_Analyzer on the result; invalid while the input was valid is a BlendValidityViolation, which leads the error message ("the kernel returned broken geometry"), like the existing volume violation. Not raised when the input was already invalid (an imported solid is not the blend's fault). It turns "wrong volume found three operations later" into an error at the operation that caused it, whatever else is or is not done about cleaning. (2) CLEAN BY DEFAULT, WITH THE HISTORY FOLLOWING SDK booleans go through `tracked_cut` / `tracked_intersect`, which ask the kernel's history what became of every face (that is how tags and lineage survive), so cleaning only the shape would break tracking. `BooleanOptions::clean` makes `run_boolean` unify the result, merge the unifier's history into the boolean's, and follow the section edges through it; the section-edge following was lifted out of `fuse_shapes_with_history` into one helper both use (fuse behaves as before). The unified shape replaces the result only when it has the same number of solids and is valid whenever the raw result was; otherwise the raw result stands. FreeCAD and build123d fall back the same way, and replicad's unconditional SimplifyResult has been seen to invalidate results (BarbourSmith/Abundance#1934), so the fallback is not decoration. `clean` is a keyword of the three functions (default True), recorded as params.clean like union's. C++ `cut_shapes`, `common_shapes` and `tracked_*` keep `clean=false` as their default so other callers (diagnosis, set booleans, std parts) are unchanged. Not done on purpose: OCCT's own SimplifyResult(true, true) that the issue suggests: the explicit unifier already gives the same merge and history, with a fallback we control. REPLAY: NO LEGACY BRANCH (owner's rule: do not keep old formats alive) `flag(p, "clean")` and `ctx.require_params(..., "clean")` fail on a graph recorded before this change, as they do for any missing parameter. Such a graph has to be recorded again. (3) THE DIAGNOSIS `classify_split_faces`, before the face-room probe: unify the solid, carry the selected edges over with the unifier's history, blend again with the same size; if that works the split faces are the cause, and the message says so and how to fix it (clean=True, or union_rsolid(clean=True)). The room probe stays for a radius that really is too large (test: radius 50 on the cleaned case is still reported as too large). EVIDENCE - Both cases now give valid fillets with volumes 29604.46 and 29811.31, equal to CadQuery's. - clean=False: A raises "not a valid solid" (it was returned), B raises "faces ... split on a surface ... clean=True". - test/test_boolean_clean.py (10 tests): face counts 8 against 9, volume unchanged by cleaning, cut_list and intersect, both cases, both clean=False diagnoses, a really-too-large radius, `clean` recorded, replay giving 8 / 9 faces. - A random sample, 120 cases of the issue's recipe (box minus 2-4 vertical boxes/cylinders on a grid, top edges, r 1-3), both modes: 240 results, none invalid and returned (clean=True 84 valid / 36 errors, clean=False 83 / 37). - Full Python suite on this machine: 2674 passed; 22 failed and 9 errors, none touching booleans (Windows paths, encodings, file locks, addon and shell integration, missing OCP, a math-library last digit in test_tolerance). NOT DONE / NOT KNOWN - The C++ unit tests (ctest) were not built or run here (SCAD_BUILD_TESTS is off for the wheel build). CutOptions::clean defaults to true, so a C++ test that compares a cut's topology may need its expectation updated: CI will say. - The issue's 16000-case run was not repeated, only the small sample above; it shows that no invalid result is returned, not the 0.4% rate of the issue (there is no old binary here to compare with). - A stored artifact or fingerprint of a model with a cut that left split faces changes: the faces are fewer. That is the point of the change; rebuild such models. - Unifying can still make a later blend worse on rare inputs (the issue saw 6 segfaults after unify in other libraries too); here the unify is inside the boolean, so a crash would show in the boolean. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… result, ProductInput) The committed API pages must equal what tools/auto_docs_gen.py writes (test_auto_docs_gen); the new parameter type of export_product_package_to_step/stl/obj/mjcf was not in them, which failed Python 3.10-3.13 in CI. The cut and intersect pages the generator also touched belong to #92 (they come from the native module installed here, which has that branch's signatures) and are left out. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ilders, format (CI of #88) DECISION: the roller-chain sprocket goldens and the C++ expectation move to the cleaned topology; they are not held at the old counts. The port is now deliberately different from the 2.2.6 reference on this point. WHAT CI SAID, AND WHY EACH ITEM IS RIGHT TO CHANGE - C++ doctest `std` (test_chain_bearing.cpp): the 12B sprocket has 39 faces, not 54. The sprocket is a body cut by tooth and roller profiles; cut now unifies the faces a cylinder seam had split. Volume, area, bounding box, tags are equal to the golden to 1e-6, so the geometry is the same and only the split faces are gone. - src/cpp/tests/golden/std_sprocket.json (faces 54 -> 39, edges 156 -> 111) and std_sprocket_unbored.json (35 -> 26 faces, 99 -> 72 edges): these files are recorded from the 2.2.6 Python reference SDK, which does not clean after a cut, and the suite refuses SCAD_REGOLDEN in CI, so they cannot be re-recorded there. I edited the four numbers by hand after comparing the port's whole output with the golden field by field: nothing else differs. Anyone who re-records from a reference that cleans will get the same numbers. - test_signatures_match_python: cut_rsolid has the keyword `clean` now. - OCCT guard audit: the trial blend of the new split-faces probe built BRepFilletAPI_Make* outside occt_guard. It runs under occt_guard now (a crash inside is a KernelError, which the diagnosis already swallows). - clang-format 18.1.8 (installed from pip into a scratch venv): blends.cpp and blend_diagnosis.cpp. CHECKED HERE: src/cpp/tests/python 556 passed (2 failures, test_locator_checks and test_failures, are Windows-only and did not fail in CI), check_occt_guard --strict 0, check_format.sh clean. The doctest executables were not built here; the CI run of this commit is the check for them. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ndary fix: fingerprint quantization no longer splits a value on a boundary (#87)
fix: model and export STEP from a script in a read-only, unattended run (#89)
fix: cut/intersect unify their result, and a blend never returns an invalid solid (#88)
build: a plain uv sync builds the C++ module on Windows
DECISION: release the one-solid Part, the Windows build fix and the three issue fixes (#87, #88, #89) as 2.3.0. Not 2.2.10: the owner chose 2.3.0. 2.2.9 is skipped; it was prepared on dev but never tagged or published (no v2.2.9 tag, nothing on PyPI, the dry run of its workflow published nothing), so there is nothing to withdraw. WHY A MINOR VERSION - It removes interface of the withdrawn 2.2.8 (`bodies=`, `Part.multi_body`, profile 3), adds keywords (`clean=` on three booleans), and widens the exporters' input. - It changes a default: `cut_rsolid`, `cut_rsolid_list` and `intersect_rsolid` unify their result, so the topology and the fingerprint of a body that used to keep a split face change. - It breaks a recorded format on purpose: `clean` is a required parameter of the three recorded operations, a graph recorded before 2.3.0 is rejected on replay and has to be recorded again (the owner's rule: no branches for old formats). WHAT CHANGES - pyproject.toml, uv.lock (the simplecadapi package entry), docs/skill/SKILL.md and docs/skill/references/SDK_OVERVIEW.md: 2.2.9 -> 2.3.0. - CHANGELOG.md: the 2.2.9 entry becomes 2.3.0 (2026-11-12) and gains the four changes; "Fixed" gains the blend validity check. - docs/updates/2.3.0.md and 2.3.0.zh-CN.md replace the 2.2.9 notes (deleted): everything the 2.2.9 notes said, then the booleans, the exporters and read-only runs, the fingerprints, and the Windows build. The release workflow turns them into the release page. Checked: test_skill_lint and test_auto_docs_gen pass; the release workflow checks that the tag equals the pyproject version (v2.3.0). test_skill_build::test_files_without_directives_copied_verbatim fails on the base commit as well (a Windows encoding problem of this machine). The code of dev was green in CI at 8c48236 before this commit; this commit changes no code. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
release: 2.3.0 (includes the never-published 2.2.9)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings master up to date with dev for the 2.3.0 release. (This PR was opened for 2.2.9; 2.2.9 was never published, and the version that went out is 2.3.0, so title and text now say 2.3.0.)
uv syncbuilds the package on Windows (build: a plain uv sync builds the C++ module on Windows #86).cut_rsolid/intersect_rsolidunify their result and a blend never returns an invalid solid (cut_rsolid/intersect_rsolid之后不做 clean,后续fillet_rsolid失败(诊断有误导)或返回无效实体且不报错 #88, fix: cut/intersect unify their result, and a blend never returns an invalid solid (#88) #92); runs without the cache work in a read-only directory and the exporters take a build result (支持在只读目录、无人值守的场景下用单个 Python 脚本建模并导出 STEP #89, fix: model and export STEP from a script in a read-only, unattended run (#89) #91).2.2.8 is withdrawn (yanked on PyPI). The tag
v2.3.0on the merge commit started the release workflow, which published 2.3.0 to PyPI and GitHub.🤖 Generated with Claude Code