Skip to content

fix(geometry): the print check is right about size, contact, walls, bridges and open bodies - #353

Merged
ManuelStaggl merged 12 commits into
mainfrom
fix/print-check-audit
Nov 12, 2026
Merged

ManuelStaggl merged 12 commits into
mainfrom
fix/print-check-audit

Conversation

@ManuelStaggl

@ManuelStaggl ManuelStaggl commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes the severity-A findings of the full audit (2026-11-12) on the ready-to-print check, one commit per point. Values were measured with the audit harness (tools/audit, cases druckpruefung-befunde and druckpruefung-gegenprobe, mechanik-others, normteile-policy-raw) on SolidCraft 0.40.1, profile PLA, 256 mm plate, 0.4 mm nozzle, 0.2 mm layer, before and after.

Findings

Finding Input Before After
D2 bar 300 × 10 × 5 block, size 300 × 10 × 5 ready (370 × 10 × 5 still block)
MG-02 bar 300 × 20 × 10 block ready
D1 two 20 mm cubes 300 mm apart block, size 320 × 20 × 20 ready
MG-03 two plates 200 × 200 × 3, 100 mm apart block, size 500 × 200 × 3 ready
D3 cube + sphere Ø 20 no contact contact 0 mm², body "Kugel"
D3 cube + cone on its tip ready warn, contact 1 mm², body "Kegel"
D4 plate 30 × 30 × 0.3 flat block, "thinner than the nozzle" warn, floor 0.3 under 0.8 mm
D4 box, floor 0.3 and wall 0.3 one block finding over 6481 mm² wall block 4635 mm², floor warn 1729 mm²
N-druckpruefung-1 box, floor 0.8, wall 1.35 warn, thinWall 0.8 ready
D6 wall tapering 2.5 → 0.2 mm (three variants) ready (one with contact only) block, thinWall 0.2 mm, 364 mm² (exact: 365.2)
MG-04 plate and wall of 0.9 mm warn, thinWall 0.9 under 0.9 no thinWall (0.89 still found)
MG-01 box ceiling 40 × 40, bridge 40, ceiling 100 × 100 ready warn, overhang 1600 / 800 / 10 000 mm²
N-mechanik-1 guide groove ceiling 101.9 / 27.9 / 13.9 mm wide 0 mm² as checked 3057.96 / 837.96 / 417.96 mm²; 5.97 mm wide stays 0
N-druckpruefung-2 SolidCraft surface bodies, open mesh warn (contact 0 / separate), ready block, open
G1 lid plate designed 27 mm above the box's floor ready unchanged, see "Open"
N-normteile-policy-2 Ø 3.4 hole 1 mm from the outer wall ready unchanged, see "Open"

Controls unchanged: cantilever 40 mm warn, bridge 3 mm ready, uniform 0.2 mm wall block; the five good bodies of the counter-check (cube, box, bracket with 45° chamfer, cylinder, plate with hole) stay ready; no mechanism or hinge body reads as open.

What changed

  • Plate size: every body is held against the plate by itself, as it lies, and tried turned about Z in steps of 0.1° (convex outline). The size finding names its body.
  • Contact: the area on the bed is compared per body; a body with no face on the bed is still named.
  • Walls: PrintCheckProfile carries the layer height. What lies within 45° of flat (as the body lies) is a floor: warned under minimumFloor = max(0.8 mm, 4 layers), blocked under one layer. Walls keep two lines / nozzle. Rays are spread a nozzle width apart over each triangle and set at its corners; the block counts the area under the limit; a wall of exactly the minimum is not counted. thinWall findings carry floor, walls and floors are separate findings. The design rule text takes minimumFloor from the same place.
  • Bridges: a small-overhang patch must hold at points spread over its triangles, not only at their corners. A flat ceiling between two supported edges up to BRIDGE_SPAN = 10 mm apart is a bridge and stays out; the design rule text uses the same constant.
  • Open bodies: the render mesh keeps the count of open mesh edges; a body with any is reported as open (block) and left out of the other checks.

Each point has a unit test in packages/geometry/src holding the measured error and a counter-case; the wording for the assistant and the chip texts (de, en) follow.

Open

  • G1 (bodies printed together share a bed): not built. The check has no reliable sign that two bodies are printed in place: a joint also connects parts that are printed apart, structure.joints names only the bodies a joint moves, and the audit itself leaves open whether the export keeps jointed bodies together.
  • N-normteile-policy-2: the breakout leaves 54° wedges, which the wall check leaves out on purpose (dovetail, thread run-out). It needs a check where the hole is made.
  • The wall check now takes about three times as long (cable clip 63 → 200 ms, measured in vitest); it still runs on the UI thread.
  • tools/audit on the audit branch needs layer in its profiles (harness.ts, mechanik-helpers.ts) and body / floor in the hand-written findings of the passung-texte cases.

Checked

pnpm --filter @partbuddy/geometry test (66 after the review, 72 after the second), pnpm exec vitest run in apps/desktop (716, unchanged after the second review), pnpm run typecheck over the workspace, eslint on both packages. Not run: Playwright E2E (one expected text in e2e/app.spec.ts was adapted by hand), the SolidCraft contract tests, the app itself.

After the review

Rebased onto main; three more commits. Each point has a unit test with the example of the review.

Point Example Before (branch) After Test
1 bridge across a free edge roof of a blind T-slot, 9 mm wide with a 3 mm slot 0 mm² (main: 189) 189 mm² takes no bridge across a slot or a hole in the ceiling…
1 ring under a counterbore, 9 mm with a 4 mm hole 0 mm² 65 mm² same
2 anchor on hanging material shelf 8 mm out with a drip edge ready overhang 240 mm² (the shelf; without the lip 300 mm² of a 10 mm shelf) hangs nothing from material that itself ends in the air
3 degenerate triangle closed cube plus triangle (a, a, b) 1 open edge, blocked 0 open edges does not take a triangle drawn to a line for a hole in the shell
4 no count of open edges 10 × 10 × 300 mm, openEdges undefined ready size, block checks a body whose model carries no count of open edges…
5 no layer height plate 30 × 30 × 0.3, layer undefined ready warn, floor under 0.8 mm measures what lies flat in layers…
6 "(four layers)" layer 0.12 mm, minimum 0.8 mm "(four layers)" "(6.7 layers)"; 4 at 0.2 mm same, and tells a thin floor from a thin wall…
7 lying holes rule "about 10 mm", check from Ø 8 (as the review found) contradiction rule and check take LYING_HOLE = 8 mm: Ø 8 not reported, Ø 9 reported lets a hole lying across the layers print round up to 8 mm…
8 fifty pins 4 × 4 mm 50 bodies with 16 mm² each 50 findings, report cut at 40, note at 12 one finding, bodies: 50, all 50 named; note says "and 46 more bodies" gathers the bodies with little on the bed into one finding…, says how much it leaves out…

Still right: groove ceilings 101.9 / 27.9 / 13.9 mm reported (3057 / 837 / 417 mm² on closed test bodies; harness 3057.96 / 837.96 / 417.96), bridges of 5.97 and 3 mm free, arm of 40 mm reported. The harness cases druckpruefung-befunde and -gegenprobe give the same statuses and areas as before the review fixes; only the new fields (bodies, layers) are added.

Open bodies on SolidCraft 0.40.1 (point 3): sphere Ø 20 (25 504 triangles), cone with a tip (5 242) and a cube with a modelled M5 thread (8 918) each have 0 degenerate triangles and 0 open edges; none reads as open.

How:

  • A direction counts as a bridge only when the nearest rim of the patch ahead and behind is an anchor; a free edge or a hole on the way makes it an arm.
  • An edge is an anchor only when the material below it comes down along the mesh, never up, to the bed or to a face that looks up. Known limit, marked shortcut: in the code: a hook whose own back looks up still counts as standing.
  • The roof of a lying hole stays out up to LYING_HOLE = 8 mm (MakerVerse, the one source of the audit's scale with a size); the rule text takes the constant.
  • thinWall floors carry layers, contact is one finding with bodies, the report carries omitted; the note says "and N more" for bodies and findings.
  • The tests for bridges stand on closed bodies (gridBody, and prismBody with ear clipping instead of a fan).

After the second review

Two more commits; what counts as a small overhang (overhang.ts) is reworked. The reviewer's scripts (cases.mjs, more.mjs, perf.mjs, slot.mjs) were run against the result and their cases taken over as unit tests (bridges.test.ts, overhangSpeed.test.ts).

Point Example Before After Test
1 bridge askew in plan tunnel 8 × 30, roof meshed as a grid, turned 15° / 30° / 60° / 75° 240 mm² 0 at every angle gives the same answer however the part is turned on the bed
2 lying holes hole Ø 8 and Ø 7 at 1° … 75°; arch Ø 8 at 7.5° and 20° 220 mm² (157 is the outside alone); 188.5 157 at every angle; 0 same
3 open bridge off the grid tunnel 8 at 1° / 3° / 10°; tunnel 5 at 7.5° 240; 150 0; 0 same
4 hole in a bridge bridge 8 mm with a hole 3 × 3, 30 and 100 mm long 231; 791 mm² 15; 15 mm² (the two stubs beside the hole) takes no bridge across a slot or a hole in the ceiling…
5 run time stepped funnel 24 000 / 48 000 / 100 000 triangles; stem of 250 rings (100 400) 14 s / 56 s / –; 3.6–6.3 s 0.4 s / 0.3 s / 0.8 s; 0.3–0.5 s overhangSpeed.test.ts (limit 2 s at 100 000)
6 lip that looks up shelf with hook, with 0.5 mm step, with bead ready (all three) overhang 240 mm² (all three) hangs nothing from material that itself ends in the air
7 meshing hole Ø 9 with 20 segments free reported with 16 … 96 segments; Ø 8 free with all reads a hole the same however coarsely it is meshed
7 no angle overhang undefined or NaN ready checked at 45°, finding carries 45 checks overhangs at 45° where the profile names no angle
7 bodies count above 500 report fails its schema cap at 100 000 says how much it leaves out…

Each bridge, hole, T-slot, counterbore, shelf and box case of the tests is also run turned by 1°, 7.5°, 15°, 30°, 45°, 60°, 75° and 90°, with the same area within 1 %.

How it works now:

  • One direction per patch, from its anchors. The directions most anchor length lies across are tried (six at most), and the one that leaves the least hanging is taken, as a slicer lays a bridge in one direction. Points just inside the corners are tested, not points on the rim.
  • Only what does not hold is reported, with the arm back to its anchor (the strip near the wall that is no part of a bridge). The rest of the patch stays out.
  • What stands is found in one flood from the bed: upwards along the surface, never down, across overhang no further than a small overhang reaches, a rise on sound surface starting that count anew (a stepped funnel stands step on step). A face that looks up stands when the underside straight below it does (the floor of a box, also of a sealed one; not the back of a hook). The shortcut: about hooks is gone.
  • Curved roofs: the distance to a rim runs on to where the curve passes the limit angle, so the mesh does not decide. A patch that is curved nearly all the way is an arch throughout; one that is flat over a third or more is a ceiling with rounded sides.

Changed on purpose against the list of cases to hold, because of point 4:

  • T-slot roof: 162 mm², not 189. The two arms (3 × 27 mm each) are reported; the 9 × 3 mm behind the slot is a 9 mm bridge and stays out.
  • Counterbore 9 / 4: 20 mm², not 65. Laid across in one direction, two sides of the ring are bridges beside the hole, two are stubs 2.5 mm out; the stubs are reported.

Both still read warn. Holding: shelf with drip edge 240 mm², groove ceilings 101.9 / 27.9 / 13.9 mm reported (3057 / 837 / 417), 5.97 mm free, bridge of exactly 10 free and 10.001 reported (300), arm 40 mm reported (1200), hole Ø 8 free / Ø 9 reported, the harness cases druckpruefung-befunde and -gegenprobe unchanged (the five good bodies ready), and sphere, cone and M5 thread from SolidCraft 0.40.1 as before.

Known limits:

  • The thread of perf.mjs is a strip floating 5 mm over the bed with no core; nothing of it stands, so it is reported (4712 mm²) where the code before called it small. On a core that reaches the bed the same thread is small (test, 149 760 triangles in about 1.5 s).
  • Shapes between a flat ceiling and a round roof are a judgement: a tunnel 8 mm wide with R 3 corners is free, 9 or 10 mm wide with R 4 corners is reported (218 and 157 mm²). An arch of 9 mm meshed with only 12 segments on the half circle is reported in part (141 of 212 mm²).
  • By the one direction per patch, a cross of two channels under one ceiling is bridged along one leg only and the other is reported (follows from the rule, not measured).

Fourth round: the cases as tests, the check unchanged

Two commits, both tests only (ee59e9f, 1546201). overhang.ts is as after the second review. Not ready to merge: the false "ready" of the third review is still there.

  • overhangCases.test.ts: 178 cases from the three reviews, each as drawn, turned by 1°, 7.5°, 15°, 30°, 45°, 60°, 75° and 90°, and mirrored (as drawn and at 30°). On the check as committed 151 hold and 27 do not; those 27 run as expected failures (OPEN in the file), so the suite is green and a case that comes right says so.
  • overhangSpeed.test.ts: no limit in seconds any more. Each mesh is timed against one a quarter of its size, the fastest of three runs each; four times the triangles may take 12 times as long (one pass: 4, measured 4.2 to 7.3 under load; searching anew from every rim: 16).
Open Cases Committed check Should be
false "ready" shelf 8 with a pointed drip edge and a cheek at one end, 30 and 100 long 0 mm² 200–245, 750–805
false "ready" in part bridge 8 onto an apron hanging from the roof with a cheek at the back; apron 60 long between two cheeks 60, 120 mm² 270–305, 540–605
false "ready" stairs going out: 1.7 or 1.8 out per 1 up, 1.7 per 0.2, 1.7 per 0.05, 1 per 0.2, 1.7 with a riser leaning 44° 0 mm² reported
false "warn" L, T and cross of channels 8 wide; channel 3.5 to 8 wide round a post; ring channel 6 wide 38–416 mm² free
false "warn" tunnel 9 and 10 with corners rounded R 4; tunnel 8 and 10 with corners cut at 30° 139–218 mm² free
false "warn" lying nut pocket M4, M5, M6 with a corner up 243–346 mm² free

Why the check was not changed in this round. Two reworks of "what stands" were measured against the 178 cases, neither is committed:

  • A distance over the surface instead of a flood (how far material hangs out from what stands, less what it rises at the limit angle; asked through the material and along each underside). It answers every false "ready" above. As a recursion from each point it does not finish (the whole suite was stopped after 9 minutes; with 2 s per call 78 of 178 fail).
  • The same rule to a fixed depth over the vertices: 32 s, 46 of 178 fail. All stairs, cheeks and aprons are right; wrong are 44 printable shapes now reported (every tunnel with rounded or chamfered corners, arches and lying holes up to 8 mm, nut pockets with a flat up, the 45° stair), one flange 10 mm out reported with 356 of 628 mm², and a thread taking 21 s.

The cause is one: whether an edge stands has to be known along its whole length and across each face, and each facet of a rounding stands on the one below it. On the faces of the mesh that is either a chain without a bound or the surface sampled anew every millimetre, which is slicing by another road; and a body that rests on a reported overhang (a tube lying on the bed) needs the answer from the bottom up, which a surface gives in no order. The proposal is to cut the body into layers and measure per layer what rests on the one below and how far it spans (clipper2-ts is in the workspace). To be decided before any more work on overhang.ts.

Checked: pnpm --filter @partbuddy/geometry lint, typecheck and test (251: 224 pass, 27 expected failures, 11 s). Not run: the desktop tests, Playwright, the SolidCraft contract tests. The expected values of the cases were taken over as written in the interrupted round, not measured again against the reviewers' scripts.

🤖 Generated with Claude Code

@ManuelStaggl ManuelStaggl changed the title fix(geometry): the print check fixes the audit's findings on size, contact, walls, bridges and open bodies fix(geometry): the print check is right about size, contact, walls, bridges and open bodies Nov 12, 2026
ManuelStaggl and others added 8 commits November 12, 2026 13:45
…ong one diagonally

Audit D1/MG-03: width and depth were taken over all bodies together, so two 20 mm
cubes designed 300 mm apart blocked as 320 x 20 mm.
Audit D2/MG-02: only the axis-parallel box and its quarter turn were tried, so a
300 x 10 x 5 mm bar blocked on a 256 mm plate although it needs 219.2 mm a side
across the diagonal.

Every body is now measured as it lies and tried in steps of 0.1 degrees about Z; the
finding names its body.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…body

Audit D3: the area on the bed was added up over all bodies and compared once, so the
400 mm2 of a cube hid a sphere or a cone on its tip beside it (no contact finding).
Each body now gets its own finding, named even when no face of it lies on the bed.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…ross their whole face

Audit D4, N-druckpruefung-1: the wall check knew no direction and no layer height. A
0.3 mm plate flat on the bed blocked as "thinner than the nozzle", and the 0.8 mm
floor the design rule asks for warned as a wall under two lines. What lies within 45
degrees of flat, as the body lies, now stands against four layers (at least 0.8 mm)
and blocks under one layer; the profile carries the layer height.
Audit D6: only the middle of each triangle was measured, so a wall tapering from 2.5
to 0.2 mm passed. Rays are now spread a nozzle width apart and set at the corners,
and the block counts the area under the nozzle, not the faces that touch it.
Audit MG-04: a wall of exactly two lines (0.9 mm) was reported as under them.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…s corners rest on walls

Audit MG-01, N-mechanik-1: a patch counted as a small overhang when every corner of
its triangles lay within reach of an edge with material below. A flat face is two
triangles with all corners on its walls, so the ceiling of a box (1600 mm2), a 40 mm
bridge and the 102 mm ceiling of a guide's groove (3058 mm2) dropped out and the
check read ready.

Points spread over each triangle are tested now, not only its corners. A flat ceiling
between two such edges up to 10 mm apart is a bridge and stays out (UltiMaker; the
assistant's design rule takes the same constant); a wider one is reported.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Audit N-druckpruefung-2: SolidCraft's surface bodies (closed false, volume 0) ran
through the check as printable parts: four walls without a floor gave only "0 mm2
on the bed", a cube with its top face removed only "bodies touch", and a mesh of
five faces read ready.

The render mesh keeps the count of mesh edges with one triangle, which the edge
derivation already had. A body with any is reported as open, blocks, and is left out
of the other checks.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
… read as open

Review of the print check: a degenerate triangle (a, a, b) left an edge from a to
itself that no other triangle closes, so the count of open edges was above 0 and the
check blocked a closed solid as an open shell. Such an edge is skipped.

Measured on SolidCraft 0.40.1: a sphere (25504 triangles), a cone with a tip (5242)
and a cube with a modelled M5 thread (8918) carry no such triangle and no open edge.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
… no free edge between

Review of the print check, three faults in what counts as a small overhang:

A line through a point of a ceiling took the nearest anchor ahead and behind without
asking what lay between. The roof of a blind T-slot (9 mm between the walls, a 3 mm
slot down the middle) read as a 9 mm bridge: 0 mm2, where main had 189 mm2. A
direction is a bridge only when the nearest rim of the patch on both sides is an
anchor; the ring under a counterbore with a through hole is reported too (65 mm2).

Any edge whose neighbour goes on downwards was an anchor. A shelf 8 mm out of a wall
with a drip edge at its tip read ready, the lip counting as a second wall. An anchor
now needs its material to come down, never up, to the bed or to a face that looks
up; the shelf is reported with 240 mm2.

The assistant was told that lying holes up to about 10 mm are bridged while the
check warned from 8 mm. Both take LYING_HOLE = 8 mm now (MakerVerse, the one source
of the audit's scale with a size): the roof of a lying hole up to that stays out.

The cases stand on closed bodies (a grid of blocks, prisms with clipped ears).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
… without a word

Review of the print check:

A body whose model carries no count of open edges fell out of both the open and the
closed bodies: 10 x 10 x 300 mm read ready. It is checked as closed.

A profile without a layer height switched the floor check off (a 0.3 mm plate read
ready). It is checked as 0.2 mm.

"(four layers)" was wrong under a 0.2 mm layer, where the 0.8 mm minimum is more
(6.7 layers at 0.12 mm). The finding carries the number of layers and the texts name
it.

Fifty pins made fifty contact findings, of which the report kept 40 and the note 12.
The bodies with little on the bed are one finding with their count; the report says
how many findings it leaves out, and the note how many findings and bodies.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
ManuelStaggl and others added 4 commits November 12, 2026 15:34
…part lies, in one pass

Second review of the print check. What counts as a small overhang is reworked:

The bridge was tried in twelve fixed directions. An 8 mm tunnel turned by 1 degree
was reported whole (240 mm2), a lying hole of 8 mm was free at 0 and 90 degrees
only, and a roof meshed as a grid was reported at 15 degrees. The directions now
come from the anchors of the patch, and one is chosen for the patch as a slicer lays
a bridge. Points just inside the corners are tested, not points on the rim.

A hole in a bridge reported the whole ceiling (231 mm2 of 30 mm, 791 mm2 of 100 mm).
Only what does not hold is reported, with the arm back to its anchor: the two stubs
beside the hole, 15 mm2. By the same rule the roof of the blind T-slot reports its
two arms (162 mm2, not the 9 mm bridge behind the slot) and the ring under a
counterbore its two stubs (20 mm2).

Whether material stands was searched anew from every rim: 14 s on 24000 triangles of
a stepped funnel, 56 s on 48000. One flood from the bed finds it, upwards only and
across overhang no further than a small one reaches; 100000 triangles take under a
second. A face that looks up stands when the underside straight below it does, so a
lip with a hook, a step or a bead that looks up no longer carries a shelf.

A hole of 9 mm with 20 segments stayed out: the rim of its roof lies on a mesh edge.
The distance now runs to where the curve passes the limit angle.

A profile without an overhang angle is checked at 45 degrees.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
… an angle or with many bodies

Second review of the print check: the overhang finding carried the profile's angle
as it came, so one that is not a number failed the report's schema; it carries the
limit that was checked. The count of bodies with little on the bed was capped at 500
in the schema, where a larger design would have lost the whole report.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…rned and mirrored

178 cases, each in nine turns on the bed and mirrored. The 27 the check
still gets wrong run as expected failures (ten overhangs that pass as
printable: low stairs, a drip edge with a cheek; seventeen printable
shapes that are reported: channels that turn, tunnels with cut or
rounded corners, lying nut pockets). The check itself is unchanged.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…wth, not in seconds

The thread took 3.9 s against a limit of 3 s on a busy runner. Each
mesh is now timed against one a quarter of its size, the fastest of
three runs each: one pass takes 4 times as long (measured 4.2 to 7.3
under load), searching anew from every rim 16 times; the limit is 12.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@ManuelStaggl
ManuelStaggl merged commit 18820af into main Nov 12, 2026
7 checks passed
@ManuelStaggl
ManuelStaggl deleted the fix/print-check-audit branch November 12, 2026 16:22
ManuelStaggl added a commit that referenced this pull request Nov 12, 2026
…ks measure before they cut (#357)

A-findings of the full audit (2026-11-12) on the rules for the assistant
round the print check, on top of #353, plus the report of 2026-11-12
that the assistant does not optimise print time and material on its own,
and the points of the independent review of this PR. `origin/main` is
merged in (undo pattern of #348, block refusals of #355).

## R1 – the check reaches the assistant in the turn that changed the
design

**Before:** "Read them after your last change, before you call the part
done" was a request. The check reached the model with the next user
message; a turn could end on status `block` it never saw.

**After:** the work log (`workLog.ts`) reads SolidCraft's history before
and after every changing call. When the design changed, it hands the
tool's answer to `withCheckOfChange` (`printCheckNote.ts`), which waits
until the mirror has delivered that revision and the renderer has
reported its check, and puts the note behind the answer. It waits
outside the work log's queue.

**Why this and not a step at the end of the turn:** Claude Code (gate
proxy), ChatGPT and local models (`loop.ts`) all call tools through the
one chain `track(upstream)` in `index.ts`. One hook there covers all
three; a turn-end step would have needed three implementations and a
guard against loops. A tool answer cannot loop: no change, no note.

Details, after the review:
- **Two stages.** With two bodies or a joint the renderer sends the mesh
check at once (`measuring: true`) and the full check when SolidCraft has
measured clearance and joints. The answer waits up to 1.5 s for the full
check, then takes the mesh check marked as preliminary; after 3 s
without any check it says so. Timings at SolidCraft 0.40.1 without a
window (median of 5): mirror and mesh check 211 ms for a box with its
lid, 141 ms for a hinge; the hinge's two joint sweeps 498 ms;
`inspect.clearance` 1 ms for both; the renderer adds 250 ms of quiet
time. Not measured: the renderer's own check time in the app.
- **What counts as a change:** the undo depth moved, or a change
replaced what Redo held (`designChanged`). The revision alone does not:
trials, saves and a block that was taken back move it. At the cap of
SolidCraft's undo history (200; a step there leaves the depth and moves
the revision) a moved revision counts, at the price of a check too many
after a trial. Measured: a probe batch that is rolled back leaves
revision, undo and redo as they were; `body.print_face` moves revision
and undo depth, so it gets its check.
- **Told once.** `CheckTold` gives the full note the first time in a
turn and whenever status, any finding (all of them, with faces and
numbers, not only the lines shown), the preliminary mark or a failed
measure change; else one line plus the measures that changed. A new user
message starts over.
- **Changing tools** now include `partbuddy_place_drawing` and
`partbuddy_move_drawing` (check, queue, lock while a question is open, a
line in the work log). The other PartBuddy tools (`view`, `standard`,
`ask`, `plan`, `assembly`, `design_rules`) change no geometry.
- When the history cannot be read, the answer carries no check and the
log says so.

Texts to the assistant:
- VIEW_GUIDANCE, replacing the request: `PartBuddy checks the part for
printing after every change and shows the result to the user as a light;
the findings come behind the answer of each tool call that changed the
design, and partbuddy_view gives the latest ones.`
- Pending behind a tool answer: `[PartBuddy: the ready-to-print check of
the latest change is still running; call partbuddy_view for it before
you call the part done.]`
- Pending from `partbuddy_view` itself: `[PartBuddy: the ready-to-print
check of the latest change is still running; it will be here in a
moment.]`
- Preliminary: `Preliminary: clearance and joints still being measured
(bodies that overlap or come closer than a push fit, joints that stop
before a limit); call partbuddy_view for the full check before you call
the part done.` A clean preliminary check reads `nothing found so far
(build volume, wall and floor thickness, overhang angle, area on the
bed, open bodies, failed features, bodies that touch).`
- Preliminary from `partbuddy_view` itself: `Preliminary: clearance and
joints still being measured (…); the full check will be here in a
moment.`
- A measure that failed: `NOT measured, because SolidCraft's measure
failed: bodies that overlap or come closer than a push fit. Measure it
yourself (inspect.clearance) before you call the part done.` The clean
line then leaves that part out of what it checked.
- Unchanged while measuring: `[PartBuddy: ready-to-print check after
this change: unchanged, nothing found; clearance and joints still being
measured: call partbuddy_view for the full check before you call the
part done.]`
- Unchanged: `[PartBuddy: ready-to-print check after this change:
unchanged, still not ready to print, the same 1 finding as before.]` /
`… unchanged, nothing found.]`, followed by `Measures that changed:` and
those lines when there are any.

## R3 – a clean check says what it looked at

**Before:** `ready to print, nothing found.` / `The print check in
partbuddy_view still judges what you build.`

**After** (from the finding kinds of `printCheck.ts`, `withClearance`,
`withMotion`):
- clean: `… nothing found in what it checks (build volume, wall and
floor thickness, overhang angle, area on the bed, open bodies, failed
features, bodies that touch, bodies that overlap or come closer than a
push fit, joints that stop before a limit).` and at the end `Not
checked: strength and layer direction under load, the fits and hole
allowances you chose, details and text too fine to print, bed-edge
chamfers. Judge those yourself.`
- with findings the last line ends with the same `Not checked: …`
sentence.
- standard topics: `None of this has been printed by PartBuddy yet, and
the print check does not test these rules: measure what you built
against them.`

## R54 – blocks measure the plastic they need

One measure for all four: a probe cylinder is laid in as a body in a
batch that is rolled back, and `inspect.interference` says how much of
it lies in the part. The cylinder starts 0.5 mm under the point, so a
face that falls away that little (a curved one) is not read as a gap,
and an edge or a chamfer is.

| Block | Needs | Measured |
|---|---|---|
| keyhole | entry wide, lip + pocket + 1.2 mm deep, behind the entry and
behind the screw's rest | before anything is cut |
| magnet | pocket + 1.2 mm round it + 0.6 mm under it | before anything
is cut |
| insertSeat | the maker's wall round it (at least 2 mm, as a boss) +
0.6 mm under it; and the seat cuts its whole cylinder | after the cut,
each hole along the way it went in (read from what that hole took out of
the body: `solid.hole` names no direction); taken back when short |
| woodScrew | the screw's diameter of plastic round its clearance hole
(4 mm screw: 12.7 mm wide, so a 15 mm tab takes it) + 0.6 mm of that
hole under the countersink | the same, without the volume check (the
plate's thickness is not given) |
| any cutting block | the body stays one body | after the cut; taken
back |

`0.6 mm` under a seat and under a countersink are our own choice (three
layers), marked so in the code. "Round its hole" for the wood screw is
the reading of the block's own sentence ("between a hole and an edge");
measured from the countersink it refused a 15 mm tab.

Refusals, as the engine gave them:
- `Not built: at [220, 30, 0] P3 has 3 mm of plastic where 6.7 mm are
needed (the mean over a width of 9 mm; the far side, an edge or another
cut takes the rest); a keyhole needs the part 6.7 mm thick behind its
entry and its slot. Thicken the part there, move the keyhole away from
edges and other cuts, or use woodScrew.` (an entry over the edge names
the entry: `at [30, -3, 0] … 1.18 mm`)
- magnet: `… has 3 mm of plastic where 3.3 mm are needed (the mean over
a width of 8.7 mm; …); a pocket needs 1.2 mm of plastic round it and 0.6
mm under it. Thicken the part there, move the pocket away from edges and
other cuts, or take a thinner magnet.`
- insertSeat, the probe: `… has 6.52 mm of plastic where 7.3 mm are
needed (the mean over a width of 8 mm; …); the seat of an M3 insert
needs 2 mm of plastic round it and 0.6 mm under it. Thicken the part
there, move the seat away from edges and other cuts, or use a boss.`
- insertSeat, the volume: `Not built: at [4, 20, 20] the seat would cut
83.2 mm³ of plastic where its whole cylinder is 84.2 mm³: its rim breaks
out of the face, into an edge, a chamfer, the far side or another cut;
the seat of an M3 insert needs …`
- woodScrew: `… has 2.49 mm of plastic where 2.65 mm are needed (the
mean over a width of 12.7 mm; …); a countersunk hole for a 4 mm wood
screw needs 4 mm of plastic round its 4.7 mm hole and a plate at least
2.65 mm thick. Thicken the plate, or move the hole away from edges and
other cuts.`
- split: `Not built: it would leave 2 bodies where Strip was one (Strip
(1)): the cut goes right through the part there. Give a point with more
plastic round it, or a smaller size`
- not taken back: `…. What was built of it could NOT be taken back: undo
it before you call again` (and a line in the log)

Success answers end with `PartBuddy measured that plastic before
building, from 0.5 mm under the face on: it is there.` (keyhole,
magnet), `PartBuddy measured that plastic, from 0.5 mm under the face
on, and that each seat cut its whole cylinder: it is there.`
(insertSeat), `PartBuddy measured that plastic, from 0.5 mm under the
face on: it is there.` (woodScrew), or `PartBuddy could not measure that
plastic this time: check it yourself.` insertSeat adds `Each seat needs
2 mm of plastic round it and 0.6 mm under it.`

**Taking a block back.** SolidCraft 0.40.1 reads the depth of its undo
history as 200 at most (211 features: 200; one more: still 200, revision
and feature count move). A block that must not stay is therefore undone
one step at a time until the design measures as before (same bodies,
same volumes, same number of features and sketches, history no deeper),
with at most the steps it sent plus three; then the top component is
made active again.

**At the engine** (SolidCraft 0.40.1, audit harness):
- The shared volume is exact, not facetted: seven probe cylinders wholly
inside a block, a rod and a ball, along the axes and askew, each gave
1.0000000 of their volume; with 0.02 mm of 2.8 mm outside 0.99286; with
0.01 mm of the width over an edge 0.99993. The threshold stays at 0.999.
- The centre of mass comes with all the digits of a double: on a block
of 200 × 200 × 50 mm the axis of one M3 seat was 0.000007° off. On a 45°
face it read `[-0.707107, 0, -0.707107]`.
- Built, with the measured sentence: keyhole in 8 mm, magnet in 4 mm,
insert M3 in 10 mm, wood screw in 8 mm; magnet and two insert seats on a
45° face; magnet and insert seat on a rod of 20 mm radius (the seat took
0.03 % less than its cylinder there); two wood screws on a stepped plate
(8 and 4 mm); a 4 mm wood screw in a 15 mm tab.
- Refused, nothing left behind: keyhole in 3 mm and with its entry over
the edge; magnet with 0.3 mm under it and 1 mm from a wall; insert seat
in 3 mm, in a plate exactly as thick as the seat is deep, 2.5 mm from an
edge, on a 3 mm pilot hole through the part, 4 mm from an edge with a 3
mm chamfer, and one of two over a pocket from below; wood screw 5 in a 3
mm plate and 5 mm from an edge.
- A probe batch leaves revision, undo and redo untouched.
- The harness's `bloecke` cases (40) pass.

Limits: what is missing must be more than a thousandth of the stock's
volume to be seen (a seat 6 mm from the edge with the 3 mm chamfer is
built: its wall is 1 to 2 mm over the top millimetre only). The
uppermost 0.5 mm are not probed. Pockets of one call are measured
against the body before the cuts, not against each other. The same
conditions in `mechanismTool.ts` are untouched (others work there).

## Material and print time (reported 2026-11-12)

**Rule group `economy`** (fifth group, on by default; settings switch
"Design lean" / "Sparsam konstruieren", `partbuddy_design_rules {
economy }`). Text for a 0.4 mm nozzle and 0.2 mm layers:

> - Material and print time, from the first shape on and not as a
clean-up: build a shell, not a block. Slicers print 0.9, 1.35, 1.8 mm
(two to four lines) as wall and fill anything thicker with sparse
infill, so stiffen with ribs as thick as the wall, at least two lines
(0.9 mm), instead of thickening the wall. Keep the part low as it
prints: every 0.2 mm of height is one more layer, and print time grows
with the layers. Shape it so it needs no support, which costs more time
and material than any other choice. Leave out whole regions that do
nothing (an open frame instead of a plate, a shorter body); small
pockets and holes in a filled region add walls and save little or
nothing. Split a part only when that saves support or much height and a
joint is acceptable. Loaded regions, sizes the user names and what the
user wants solid stay as they are. Infill, speed and layer height are
set in the slicer: never promise grams or minutes. The print check lists
each body's solid volume, height, layers and support area: compare them
before and after a change.

The report line becomes `After each design step, name what you added on
your own, what you did for material and print time and the sizes you
snapped in one short line, e.g. "…, shell 1.35 mm with two ribs instead
of a solid block"`.

**Measures with every check** (tool answers, next message,
`partbuddy_view`), computed in the renderer from the mirrored mesh with
`meshMetrics` and `overhangOf`:

```
Per body as it prints. Solid volume overstates what thinning a thick region saves, because the slicer fills it sparsely anyway; the shell estimate (two wall lines on every surface, 15 % infill inside: an assumption, not the slicer's settings) is nearer to the filament. What counts for sure: fewer layers, less area to support, less surface. Promise neither grams nor minutes:
- "Box": 6 cm³ solid, about 2.58 cm³ as a shell, 10 mm high (50 layers), 2200 mm² of surface, 600 mm² on the bed, 0 mm² leaning past the overhang limit.
- "Pin": 90 mm³ solid, about 90 mm³ as a shell, 10 mm high (50 layers), …
- and 3 more bodies.
```

The shell estimate: surface × two lines, never more than the solid, plus
15 % of the rest. A block 40 × 40 × 20 mm reads 32 cm³ solid and about
9.7 cm³ as a shell; a wall of four lines reads the same in both. Six
bodies are listed, the rest counted; volumes under 1 cm³ are given in
mm³. No mass: the material profiles carry no density. A body that cannot
be measured is left out and named in the log; a report the main process
refuses is sent again without measures. `packages/geometry` is
unchanged; `laid` (print face down) and `smallOverhangReach` are not
exported there, so `bodyMeasures.ts` repeats the pose and the reach
(marked `shortcut:`).

### Yardstick

| Topic | Rule with number | Source 1 | Source 2 | Contradiction |
|---|---|---|---|---|
| Walls as lines | Whole multiples of the line width; 0.4 nozzle:
0.45/0.9/1.35/1.8 mm for 1–4 perimeters; strength mostly from
perimeters; shells typically 0.8 mm | Prusa,
https://help.prusa3d.com/en/article/modeling-with-3d-printing-in-mind_164135/
, https://help.prusa3d.com/en/article/layers-and-perimeters_1748 |
Protolabs Network,
https://www.hubs.com/knowledge-base/selecting-optimal-shell-and-infill-parameters-fdm-3d-printing/
| Minimum wall differs (Hubs 0.8 mm, UltiMaker 0.5 mm, Stratasys Direct
4 × layer). No source names a thickness above which more wall stops
paying off, so the rule names none. |
| Ribs instead of thick walls | Ribs stiffen thin walls and large flat
areas; in FDM a rib needs no thinning. Rule: as thick as the wall, at
least two lines | Stratasys Direct FDM guide (p. 4, 7),
https://www.stratasys.com/siteassets/sdm/content---website-storage/design-guides/dg_sdm_fdm_0725a.pdf
| Protolabs Network,
https://www.hubs.com/knowledge-base/dfm-tips-for-3d-printed-parts-with-thin-walls/
| Yes: Hubs gives 50–75 % of the wall, the rule of injection moulding
against sink marks; on a 0.9 mm wall that is one line, under the
thinnest wall that prints. Not taken over. Stratasys Direct also says
solid can build faster than hollow with ribs. |
| Height in Z | Print time follows the layer count: a 10 × 30 mm
cylinder is about 100 layers lying, 300 standing (0.1 mm) | Protolabs
Network,
https://www.hubs.com/knowledge-base/how-does-part-orientation-affect-3d-print/
| Xometry,
https://www.xometry.com/resources/3d-printing/5-easy-ways-to-reduce-3d-printing-costs/
(wording not verified) | Partly: Markforged names wall length, not
height, as the classic driver. |
| Support | Self-supporting to 45°; bridges 5 mm (Hubs) or 10 mm
(UltiMaker); support adds more time than any other factor | Protolabs
Network,
https://www.hubs.com/knowledge-base/supports-3d-printing-technology-overview/
| Markforged,
https://www.markforged.com/blog/design-for-3d-printing-part-3-decreasing-print-time
; UltiMaker,
https://ultimaker.com/learn/design-for-fff-3d-printing-maximize-your-success/
| Bridge 5 vs 10 mm; overhang 45° vs 45–60° (Prusa). |
| Pockets and holes | Holes may save material but not time: a ring took
10 % longer than the full disc; solid can build faster than hollowed |
Markforged, same article | Stratasys Direct, same PDF (p. 7) | Yes, by
process: Protolabs gives −36 % volume for hollowing, for SLS/MJF/metal,
not FDM. |
| Bed contact | Largest face on the bed; more contact, less warping |
Markforged,
https://markforged.com/learn/composites-3d-printing-design-tips |
UltiMaker, as above | None; Hubs: strength in XY is 4–5 × that in Z, so
load can outrank it. |
| Splitting | Fewer supports, fits the bed, shorter build; costs a seam,
pins and fit allowance | Prusa, as above | Markforged, Stratasys Direct,
as above | Clearance figures differ and are printer-specific. |
| Large flat faces | Prone to warping; break up with ribs, round corners
| Protolabs Network,
https://www.hubs.com/knowledge-base/key-design-considerations-3d-printing/
| Stratasys Direct, same PDF (p. 4) | Severity depends on the machine.
Pillowing of large tops: one source only (Polymaker), not in the rule. |
| The slicer's part | Infill 10–20 % usual, above 40 % rarely sensible;
layer height is the main factor of print time (0.1 → 0.2 mm halves it) |
Prusa,
https://blog.prusa3d.com/everything-you-need-to-know-about-infills_43579/
| Protolabs Network (infill article above); Markforged, as above | Hubs
contradicts itself on infill vs shells for strength; Prusa sides with
shells. |

## Test expectations changed

- `test/printCheckNote.test.ts`: `ready to print, nothing found` → the
new clean text and `Not checked: …`.
- `test/standardTool.test.ts`: closing sentence of a standard topic.
- `test/blockTool.test.ts`: the keyhole sends a trial batch
(`solid.cylinder inspect.interference … sketch.inspect`) before its
cuts; `MAGNET` has `around` and `under`; the R54 cases run on a plate
that answers probes and holes as the engine does.
- `test/designRules.test.ts`: rule list and defaults carry `economy`;
the report line names material and print time; ribs.
- `e2e/app.spec.ts`: the list of rule switches carries `economy`.
- `e2e/solidcraft.contract.spec.ts` (wall plate): the answers must carry
`PartBuddy measured that plastic before building, from 0.5 mm under the
face on: it is there.` after the keyholes and the magnets and `PartBuddy
measured that plastic, from 0.5 mm under the face on: it is there.`
after the wood screws.
- New: `test/checkOfChange.test.ts` (both routes, no change, bounded
wait, two stages, moved revision without change, a failing call that
changed the design, undo, two calls at once, unreadable history, drawing
tools, told once), `test/bodyMeasures.test.ts`.

## Checked

`pnpm run typecheck`, `eslint .` and `vitest run` in `apps/desktop`.
Audit harness against SolidCraft 0.40.1: `bloecke` (40 cases), the block
cases on flat, sloping and curved faces, the probe shares and the
timings above. Not run locally: the app, Playwright E2E and contract
tests (CI runs them), a real chat.

## Contract run 43261598187: eight red tests (fixed in e0f821e, tests
only)

- **Six tests** (standard screw, sketched blocks, snap fit, SVG logo,
colour logo, built text) failed with `the assistant is working on the
design`. Cause: the tool's answer now waits for the print check of its
change (`withCheckOfChange`), so the part shows before the assistant's
turn ends, and the tests called the user's action as soon as the mesh
showed. Log of the screw test: session started 16:50:13.770,
`design:apply-action failed` 16:50:14.710. The tests now wait for the
Stop button to go (`turnEnded`) after each first build; no fixed waits.
- **Wall plate and clipped cover**: nothing was refused. The page
snapshot of both shows the full answer with every expected sentence and
`it is there.`; the print check behind each block ends in a list of
measures, which the chat renders as a list, so the answer is several
elements and a `getByText` across two blocks no longer matches. The
tests read the whole answer (`assistant-text`) instead.
- **The lock**: bounded and always released. `withCheckOfChange` returns
after at most `CHECK_WAIT_MS` (3 s) per changing call, also when the
check never comes or the design changed (`test/checkOfChange.test.ts`:
bounded wait, check of another design). Measured locally on SolidCraft
0.40.1 with the fake assistant: the turn ended 150 to 350 ms after the
part showed in seven builds, 1.7 s in the first one after a cold start.
Before this PR the answer did not wait at all. No product change.
- All eight tests passed locally one by one against SolidCraft 0.40.1.
Not run: the other 49 contract tests, `contract.yml` in CI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: ManuelStaggl <17688572+ManuelStaggl@users.noreply.github.com>
Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant