Strip past-development-thread artifacts; lead the README with the demos
Follows the design/ removal: nothing that only makes sense as a record of a finished development thread should stay on develop or reach main, where the plugin package is the repository root and everything ships into every install. - Remove the 25 design/*.md citations left dangling across 23 source files. Each kept its surrounding explanation and lost only the pointer, so the "why" a comment was carrying survives without naming a deleted file. - Delete BUGS.md, a 527-line running log of issues hit while building models/renders/moonwatch/, and the 18 `/BUGS.md` pointers in that model's sources. Same treatment: the failure each comment describes is still there, the reference to the log is not. - Delete scripts/viewer/capture-render-baselines.mjs, the phase gate for the renderer consolidation whose design doc went with the previous commit. The standing per-format and per-theme sweeps in viewer/scripts cover it. README: drop the Screenshots section and lead with its demo table instead, right under the description, so the GIFs are the first thing below the intro rather than the last thing before Contributing. The LFS hydration command that section carried moves to CONTRIBUTING's Git Hygiene section, which is who it was for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -59,7 +59,7 @@ models/**/*.mkv
|
||||
# Local printer credentials
|
||||
/bambu-printers.json
|
||||
|
||||
# Component-GLB render packages are regenerated on demand (per design/step-generation-rearchitecture.md §7); never committed.
|
||||
# Component-GLB render packages are regenerated on demand; never committed.
|
||||
.*.step.glb/
|
||||
.*.step.glb
|
||||
.*.stp.glb/
|
||||
|
||||
@@ -1,527 +0,0 @@
|
||||
# BUGS.md — text-to-cad repo issues hit during the chronograph build
|
||||
|
||||
Running log of repo bugs, unexpected behavior, and doc gaps found while
|
||||
building `models/renders/moonwatch/`. Watch-model problems do not belong
|
||||
here. Format per entry: what I was doing, exact command, exact error/wrong
|
||||
output, workaround, blocked?, fixed?
|
||||
|
||||
---
|
||||
|
||||
## 1. `packages/cadjs` ESM cannot be loaded standalone from a lightweight worktree
|
||||
|
||||
- **Doing:** extracting the `cinematic` theme preset JSON to author a
|
||||
presentation render theme (`node -e "import('./packages/cadjs/src/common/themeSettings.js')..."`).
|
||||
- **Error:** `Cannot find package 'implicitjs' imported from packages/cadjs/src/common/camera.js`,
|
||||
then `Cannot find package 'three' imported from packages/implicitjs/src/common/camera.js`.
|
||||
- **Cause:** worktrees are intentionally lightweight (no `node_modules`), and
|
||||
`cadjs` resolves `implicitjs`/`three` as bare specifiers, so even a module
|
||||
of pure data constants (`themeSettings.js`) cannot be imported without a
|
||||
full install.
|
||||
- **Workaround:** symlinked `packages/cadjs/node_modules/implicitjs -> ../../implicitjs`
|
||||
and `packages/{cadjs,implicitjs}/node_modules/three -> <main checkout>/packages/cadjs/node_modules/three`.
|
||||
- **Blocked:** no (workaround in minutes). **Fixed:** no (logged only —
|
||||
non-blocking; arguably by design).
|
||||
|
||||
## 2. `CADGEN_WARM=1`: killing the CLI client does not cancel the in-daemon job
|
||||
|
||||
- **Doing:** first build of `finishing_sampler.step.py` was slow (my own
|
||||
O(n^2) boolean accumulation); I killed the client
|
||||
(`pkill -f "scripts/gen finishing_sampler"`) and relaunched with fixed
|
||||
source.
|
||||
- **Wrong output:** the relaunched client sat at 0% CPU for minutes. The
|
||||
daemon (pid from `$TMPDIR/cadgen-daemon-*.log`) was still burning ~600%
|
||||
CPU on the *killed* client's job — requests are handled sequentially, so
|
||||
the new run silently queued behind a job whose requester was gone.
|
||||
- **Workaround:** `kill -9 <daemon pid>` (socket + staleness handling
|
||||
respawn a fresh daemon transparently on the next call).
|
||||
- **Suggestion:** the daemon should abort a job when its client disconnects.
|
||||
- **Blocked:** ~10 min lost. **Fixed:** no (workaround only).
|
||||
- **FIXED in root source (2026-08-07):** the daemon now runs a liveness
|
||||
watchdog thread per request. Clients half-close their write side after the
|
||||
request, so read-EOF is normal — the probe is an empty stdout chunk (a
|
||||
protocol no-op for every client) sent every 0.5 s under a shared send lock;
|
||||
a failed send means the requester is gone, and the daemon logs, unlinks its
|
||||
socket, and exits, so the orphaned job stops burning CPU and the next
|
||||
invocation spawns a fresh daemon (the client already handles a missing
|
||||
socket that way). Test: `test_d_client_disconnect_aborts_orphaned_job` in
|
||||
`tests/python/skills/cad/cadgen_daemon/test_daemon.py`.
|
||||
|
||||
## 3. Sub-mm finishing booleans: overlapping-tool networks are pathological (OCC, not a repo defect per se)
|
||||
|
||||
- **Doing:** perlage (overlapping 0.02 mm-deep spherical dimples) on a
|
||||
14×8 mm coupon for `models/renders/moonwatch/_finishing.py`.
|
||||
- **Wrong output:** no error — `scripts/gen` sat in "Building geometry"
|
||||
indefinitely (>40 CPU-minutes for ~200 stamps; even ~60 stamps took
|
||||
minutes). Two escalating causes, both silent: (a) pairwise `a + b`
|
||||
accumulation of boolean tools is O(n²); (b) even in ONE multi-tool op,
|
||||
dimple spheres have ~15 mm radii, so every tool overlaps every other
|
||||
deep below the surface and OCC builds one giant intersection network.
|
||||
- **Workaround (both applied):** batch all boolean tools into a single
|
||||
list-operand op, AND pre-clip each stamp to a small lens cap
|
||||
(`Sphere & Cylinder` prototype, translated copies) so tools are
|
||||
disjoint. 14×8 mm field: >40 CPU-min → 0.69 s.
|
||||
- **Suggestion:** `scripts/gen` progress JSON could surface elapsed time
|
||||
per phase (it reports `ratio: 0.0` forever); a doc note in
|
||||
`references/build123d-modeling.md` about multi-tool list booleans would
|
||||
save others this cliff.
|
||||
- **Blocked:** ~45 min lost. **Fixed:** in model helpers (no repo change).
|
||||
|
||||
## 4. `scripts/gen` prints nothing to stdout/stderr during long builds
|
||||
|
||||
- **Doing:** first `scripts/gen finishing_sampler.step.py` runs (issues 2/3).
|
||||
- **Wrong output:** zero output for the entire run — no phase logging, no
|
||||
heartbeat; the only liveness signal is a hidden
|
||||
`__cadgen__/models/.<name>.generation.progress.json` (whose `ratio`
|
||||
stays 0.0 in the generate phase) plus `ps`. Made the hang look like a
|
||||
crash and cost several kill/retry cycles.
|
||||
- **Workaround:** watch the progress JSON + process CPU by hand.
|
||||
- **Blocked:** contributed to the ~45 min above. **Fixed:** no (logged).
|
||||
|
||||
## 5. Near-tangent boolean tools are dropped SILENTLY (OCC kernel via build123d)
|
||||
|
||||
- **Doing:** case cluster — flat crystal/crown/pusher domes built by intersecting
|
||||
huge near-tangent spheres (R≈1700 mm) with small revolves; also a crystal
|
||||
multi-tool subtract.
|
||||
- **Wrong output:** no error, exit 0, `inspect validate` clean — but half a
|
||||
tool's material was silently not removed (pusher head half-vanished), and one
|
||||
subtract left a stray disjoint 21.6 mm³ solid floating inside the crystal.
|
||||
Classic silent-no-op/degenerate-geometry behavior at near-tangency; only
|
||||
visual snapshot review caught it.
|
||||
- **Workaround:** avoid near-tangent booleans entirely — build such domes as a
|
||||
single revolved profile (RadiusArc in the profile), which is also crisper.
|
||||
- **Blocked:** no (caught in builder self-review). **Fixed:** in model source.
|
||||
|
||||
## 6. Snapshot renderer shows transparent parts (alpha < 1 source colors) as milky-opaque
|
||||
|
||||
- **Doing:** case cluster snapshots; `crystal` has color alpha 0.16, sapphire
|
||||
0.14 (confirmed present in the artifact descriptor).
|
||||
- **Wrong output:** in `scripts/snapshot` renders the crystal reads as a milky
|
||||
solid dome rather than glass; unclear whether the GLB bakes alpha and the
|
||||
snapshot material ignores it, or alpha is dropped earlier.
|
||||
- **Workaround:** none yet; to be re-checked at whole-watch compose (may need
|
||||
`display.mode` tweaks or a transparent-materials fix).
|
||||
- **Blocked:** not yet (cosmetic until final renders). **Fixed:** no.
|
||||
- **Root cause (traced):** two independent alpha drops.
|
||||
1. `packages/cadgen/src/cadgen/_internal/glb.py add_material()` bakes the
|
||||
RGBA into `baseColorFactor` but never sets `alphaMode: "BLEND"`, and glTF
|
||||
defaults to OPAQUE → alpha ignored by conformant loaders. **Fixed in root
|
||||
source** (BLEND set when alpha < 1) — helps standalone GLB exports.
|
||||
2. The component-package compose path drops alpha entirely: descriptor
|
||||
occurrence override colors go through
|
||||
`packages/cadjs/src/lib/assembly/meshData.js linearRgbToHex()` (3
|
||||
channels only), and `lib/viewer/surfaceMaterials.js` derives opacity
|
||||
solely from theme/display-mode settings — there is no per-part opacity
|
||||
concept at all. A real fix means threading alpha through part records
|
||||
into per-material `transparent`/`opacity`; too invasive for this
|
||||
project's "minimal targeted fixes" rule.
|
||||
- **Adopted workaround:** snapshot renders `--hide` the glass occurrences
|
||||
(crystal, caseback sapphire); optically defensible for macro shots.
|
||||
|
||||
## 7. Snapshot JSON jobs silently ignore unknown top-level keys (`hide` vs `selection.hide`)
|
||||
|
||||
- **Doing:** hiding the crystal in a `--job` render; wrote top-level
|
||||
`"hide": ["#o1.5"]` by analogy with the `--hide` CLI flag.
|
||||
- **Wrong output:** no error, no warning — the job rendered normally with
|
||||
nothing hidden (two identical renders before the cause was found). The
|
||||
correct schema is `"selection": {"hide": [...]}`; the CLI flag maps to it
|
||||
internally (`merge_focus_hide_options`).
|
||||
- **Suggestion:** reject or warn on unrecognized top-level job keys; the help
|
||||
text describes `--focus`/`--hide` flags but not the job-JSON field shape.
|
||||
- **Blocked:** ~10 min. **Fixed:** no (workaround: use `selection.hide`).
|
||||
- **Same trap, per-output variant (case lug fix, 2026-08-06):** a
|
||||
`"selection": {"hide": [...]}` object nested inside an `outputs[]` entry is
|
||||
ALSO silently ignored — `selection` is read only at job level
|
||||
(`__main__.py` `job.get("selection")`), and unknown per-output keys are
|
||||
dropped without warning, so the render completes with nothing hidden. To
|
||||
hide parts in one view of a multi-view job, split it into separate jobs in
|
||||
a `{"jobs": [...]}` array.
|
||||
- **FIXED in root source (2026-08-07):** `resolve_render_job` and
|
||||
`normalize_common_job` now validate jobs and outputs against closed key
|
||||
schemas (`SUPPORTED_JOB_KEYS` / `SUPPORTED_OUTPUT_KEYS`). Top-level
|
||||
`hide`/`focus`/`refs` get a message naming the `selection` object shape; a
|
||||
per-output `selection` gets the split-into-jobs fix; any other unknown key
|
||||
is rejected with the supported set listed. Covered by four new tests in
|
||||
`tests/python/skills/cad/snapshot/test_cli.py`.
|
||||
|
||||
## OCC chamfer on dome/eye-cap tangent chains: silent fail, minutes-long churn, or segfault (bracelet)
|
||||
|
||||
- **Where found:** `models/renders/moonwatch/_bracelet.py` (flat three-link
|
||||
bracelet rows: gently domed top face tangent to knuckle-eye cap cylinders at
|
||||
both link ends).
|
||||
- **Symptom:** `chamfer()` on the link top/bottom perimeter edges behaved three
|
||||
different ways depending only on the exact link width (taper step): silent
|
||||
failure (safe_chamfer returns unchanged), ~90 s per attempt CPU churn (366 s
|
||||
through the retry ladder for ONE link — one row cost 265 s), or a hard
|
||||
uncatchable SIGSEGV inside OCC. First full gen took 11 min and standalone
|
||||
builds segfaulted at reproducible-but-width-dependent links.
|
||||
- **Amplifier:** `_finishing.safe_chamfer`'s 0.7x retry ladder multiplies the
|
||||
churn 4-5x before giving up, and gives no signal that it degraded/failed.
|
||||
- **Workaround (adopted):** never 3D-chamfer edges belonging to a tangent chain.
|
||||
The bracelet links now carry the side bevel in the extruded/lofted SECTION
|
||||
(octagonal profile with built-in 45-degree bevels, `_plan_prism`) and only
|
||||
chamfer isolated flank arc edges. Gen dropped 11 min -> ~28 s.
|
||||
- **Blocked:** no. **Fixed:** worked around in model source; the underlying
|
||||
fragility is OCC's; consider a max-attempt-time guard in `safe_chamfer`.
|
||||
|
||||
## step_export warns "Unknown Compound type, color not set" for uncolored group compounds
|
||||
|
||||
- **Where found:** every `scripts/gen` run of
|
||||
`models/renders/moonwatch/bracelet.step.py` (labeled assembly with
|
||||
`strap_12`/`strap_6`/`clasp` group compounds; colors on leaves only, per the
|
||||
documented rule that color on a group compound is ignored anyway).
|
||||
- **Symptom:** `packages/cadgen/src/cadgen/step_export.py:379` emits
|
||||
`UserWarning: Unknown Compound type, color not set` for each intentionally
|
||||
uncolored group node, so the recommended color-the-leaves pattern always
|
||||
builds with warning noise.
|
||||
- **Expected:** group compounds without colors are the documented normal case
|
||||
and should not warn.
|
||||
- **Blocked:** no (cosmetic/noise). **Fixed:** no.
|
||||
|
||||
## models/renders/moonwatch/_finishing.py: `align=(None,None,None)` is not "centered"
|
||||
|
||||
Found by the movement-base builder (2026-08-06). In build123d,
|
||||
`align=(None, None, None)` places primitives at their RAW OCC datum —
|
||||
`Cylinder`/`Cone` base at z=0 (XY centered), `Box` corner at the origin —
|
||||
while `_finishing.py` (and `finishing_sampler.step.py`) were written assuming
|
||||
it means centered. Verified empirically:
|
||||
|
||||
- `Cylinder(1, 2, align=(None,None,None))` -> z [0, 2] (not [-1, 1]).
|
||||
- `Box(2, 2, 2, align=(None,None,None))` -> [0,2]x[0,2]x[0,2].
|
||||
|
||||
Downstream effects in `_finishing.py` (all silently wrong, no errors):
|
||||
|
||||
- `slotted_screw`: the slot cut box is corner-origin, so the "slot" is an
|
||||
off-center notch buried at mid-head height (x [0, 1.2*d], y [0, w]); the
|
||||
head-top datum is +head_height/2, not 0; the shank is shifted up by
|
||||
head_height/2 and pokes ~0.13 through the dome as a stub; the rim chamfer
|
||||
edge selector never matches (selects at -head_height, actual -h/2).
|
||||
- `jewel_countersink_cut`: the cone is half above the surface and its flare
|
||||
is inverted (wider at depth -> undercut, not a polished countersink).
|
||||
- `jewel`: top at +thickness/2, not 0 (jeweled_bearing partly compensates).
|
||||
- `perlage_cutter`: the lens-cap prototype is clipped to z >= 0 by the raw
|
||||
cylinder, so at the documented "surface at z=0" datum the stamps remove
|
||||
NOTHING. (The sampler coupon only shows perlage because its plate is also
|
||||
built corner-origin with its top at +0.6.)
|
||||
- `geneva_stripes_cutter`: bands are corner-origin: the field is offset +y
|
||||
by span_y*0.65 and the cutting band sits ~+0.46..+0.53 above the
|
||||
documented z=0 surface (again accidentally compensated in the sampler).
|
||||
- `train_wheel`: the crossing-out ring/spoke cutters span z [0, web+0.02]
|
||||
against a web extruded both=True (z [-web/2, +web/2]), so spoke windows
|
||||
are only cut through the TOP HALF; a membrane floor remains in every
|
||||
window.
|
||||
- `pinion`: body spans z [0, length] (not mid-plane 0) and the leaf boxes
|
||||
are offset half a leaf-width tangentially.
|
||||
|
||||
`_mvt_base.py` works around all of these locally (centered primitives via
|
||||
default align, corrective cones/slots/window-cutters layered on top of the
|
||||
helper output) without editing `_finishing.py`. Proper fix: change
|
||||
`_finishing.py` to use default (centered) alignment and re-verify the
|
||||
sampler; other movement builders should audit any direct use of these
|
||||
helpers at documented datums.
|
||||
|
||||
## `_bracelet.py` end link hit the same `align=(None,None,None)` corner-origin footgun (silent, shipped)
|
||||
|
||||
- **Where found:** `models/renders/moonwatch/_bracelet.py` `make_end_link`
|
||||
(2026-08-06, while giving the bracelet links crowned sections). Same root
|
||||
cause as the `_finishing.py` entry above: `align=(None, None, None)` is the
|
||||
RAW OCC datum (Box corner at origin), not "centered".
|
||||
- **Symptom:** two silent geometry defects in the shipped bracelet model, both
|
||||
probe-confirmed on the pre-fix source:
|
||||
- the "hollow back" cutter `Pos(0, 23.05, 1.7) * Box(16.6, 3.7, 2.4,
|
||||
align=(None,None,None))` spanned x [0, 16.6], z [1.7, 4.1] — it hollowed
|
||||
ONLY the +X half and cut up through the top surface (material probe at
|
||||
(+4, 24.5, 3.0) = empty, (-4, 24.5, 3.0) = solid);
|
||||
- the groove-pair cutters sat with their corner ON the top surface and
|
||||
extended upward, so the three-link separation grooves removed nothing.
|
||||
- **Fix:** switched both cutters to default (centered) alignment in the same
|
||||
change that crowned the links. The class of bug is already documented above;
|
||||
this entry records a second independent module that shipped with it —
|
||||
auditing other `align=(None,None,None)` uses across `models/` is warranted.
|
||||
- **Blocked:** no. **Fixed:** in `_bracelet.py` (this entry's instance only).
|
||||
|
||||
## build123d 2D sketch algebra: pairwise `+` decays, CW polygons shatter the fuse, and `ShapeList & Sketch` is silently EMPTY (keyless builder)
|
||||
|
||||
- **Where found:** `models/renders/moonwatch/_mvt_keyless.py` lever/spring
|
||||
profiles (circle+quad capsule chains for the setting lever, yoke, setting
|
||||
lever spring).
|
||||
- **Symptoms (all silent, exit 0, `inspect validate` clean):** parts extruded
|
||||
to NOTHING (a lever reduced to its pin+boss debris), or to 5-8 disjoint
|
||||
solid piles, or extruded DOWNWARD from the sketch plane. Three stacked
|
||||
causes, verified empirically:
|
||||
1. Pairwise 2D algebra decays: `Circle + Circle` returns a fused `Face`
|
||||
(not `Sketch`), and the NEXT `Face + Polygon` falls into raw shape fuse
|
||||
that returns an unregularized face pile; once any step yields a
|
||||
`ShapeList`, later `+` is Python list concatenation, not geometry.
|
||||
2. A CLOCKWISE-wound `Polygon(..., align=None)` fuses as a reversed face:
|
||||
the union "succeeds" but shatters into +Z/-Z mixed-normal fragments,
|
||||
and `extrude()` of that runs along the reversed normals (solids appear
|
||||
mirrored below the plane) as disconnected pieces.
|
||||
3. `ShapeList & Circle` (intersection used as a regularizing clip) returns
|
||||
an EMPTY ShapeList with no error, so the following extrude quietly
|
||||
produces a zero-volume part.
|
||||
- **Workaround (adopted, same as `F.train_wheel`'s internal pattern):** build
|
||||
every 2D profile as ONE multi-operand list fuse `first + [rest...]` with all
|
||||
polygons wound CCW, and apply the `& Circle(clip)` regularizer exactly once,
|
||||
LAST. Never accumulate 2D unions pairwise, never `+` two clipped results.
|
||||
- **Suggestion:** a note in `references/build123d-modeling.md` next to the
|
||||
existing multi-tool boolean guidance; possibly a lint for `Polygon` winding
|
||||
in helpers.
|
||||
- **Blocked:** ~30 min across two debug rounds. **Fixed:** in model source.
|
||||
|
||||
## Per-component STEP/GLB export silently drops the color of bare-`Compound` leaves (cadgen)
|
||||
|
||||
- **Where found:** `models/renders/moonwatch/_bracelet.py` bracelet rebuild
|
||||
(2026-08-06). 25 of 57 leaf bodies (all boolean/chamfer chains that happened
|
||||
to return a bare `build123d.Compound` instead of `Part`) rendered without
|
||||
their assigned `.color` even though `part.color` was set and the assembly
|
||||
STEP looked correct.
|
||||
- **Mechanism:** `packages/cadgen/src/cadgen/step_export.py` has two color
|
||||
paths. The assembly-tree path (`is_assembly=True`) colors every child label
|
||||
and is fine. But the per-shape path used when a leaf is exported ALONE (the
|
||||
component-GLB cache builds one doc per component) only recognizes
|
||||
`Part`/`Sketch`/`Curve` when picking the sub-shape explorer; any other
|
||||
`Compound` subtype hits `warnings.warn("Unknown Compound type, color not
|
||||
set")` and exports uncolored geometry. The warning is easy to miss (it
|
||||
deduplicates per callsite and interleaves with gen output), so the model
|
||||
ships with silently washed-out parts — here it erased the brushed-outer vs
|
||||
polished-center bracelet contrast that the source colors specify.
|
||||
- **Workaround (adopted):** coerce every leaf to `Part` before assembling the
|
||||
labeled `Compound` (`Part(shape.wrapped)` + reattach `.color`), see
|
||||
`build_bracelet()`.
|
||||
- **Suggestion:** in `_create_bin_xcaf_doc`, treat an unknown one-solid
|
||||
`Compound` like a `Part` (explore `TopAbs_SOLID`) instead of warning, or
|
||||
raise loudly; silent color loss on valid colored input is a data bug.
|
||||
- **Blocked:** no; found while chasing weak finish contrast in renders.
|
||||
- **FIXED in root source (2026-08-07):** `_create_bin_xcaf_doc` now explores a
|
||||
bare `Compound` leaf for its actual content (solids, then faces, then
|
||||
edges) and colors it like the recognized types; the "Unknown Compound type"
|
||||
warning is gone. Regression test:
|
||||
`test_colored_bare_compound_leaf_keeps_color_and_does_not_warn` in
|
||||
`tests/python/packages/cadgen/test_compound_assembly_generation.py`. This
|
||||
also removes the constant warning noise from the related "uncolored group
|
||||
compounds" entry above (same warn site).
|
||||
|
||||
## Mirrored `Polygon` points flip the face normal, so `extrude()` runs the OTHER way (build123d, silent misplaced boolean)
|
||||
|
||||
- **Where found:** `models/renders/moonwatch/_bracelet.py`
|
||||
`_corner_relief()` (2026-08-06): corner-relief pockets built from a point
|
||||
list mirrored with `[(-y, z) for y, z in pts]` for the opposite link end.
|
||||
- **Symptom (silent, exit 0, `inspect validate` clean, 57 occurrences):**
|
||||
mirroring the 2D profile reverses its winding, which reverses the planar
|
||||
face normal, and `extrude(face, amount)` extrudes along the normal — so
|
||||
every mirrored-end pocket extruded in -X instead of +X. The cutter gouged a
|
||||
strip 0.7 mm AWAY from the intended corner (it ate the end link's left
|
||||
prong tail, and the far-recess corner pockets on center links landed inside
|
||||
the crown), while the intended fang was left uncut. Point-classifier
|
||||
probing (`BRepClass3d_SolidClassifier` on mirrored coordinates) was what
|
||||
exposed the asymmetry; renders alone were ambiguous.
|
||||
- **Workaround (adopted):** when mirroring a profile, also reverse the point
|
||||
order (`[(-y, z) for y, z in reversed(pts)]`) so the winding — and the face
|
||||
normal — is preserved.
|
||||
- **Related:** same root class as the CW-polygon entry above (winding decides
|
||||
normals decides extrude direction); this instance is about MIRRORED point
|
||||
lists specifically, which look innocent in review.
|
||||
- **Blocked:** ~20 min. **Fixed:** in model source.
|
||||
|
||||
## OCC `chamfer` on blob-outline top rings: whole-ring fails, singles refuse concave junctions, grouped-after-neighbors SEGFAULTS
|
||||
|
||||
- **Where found:** `models/renders/moonwatch/_mvt_base.py` bridge anglage
|
||||
(2026-08-06, movement-base finishing pass). The bridges are extrusions of
|
||||
multi-circle union ("blob") profiles clipped to a disk.
|
||||
- **Symptoms (probe-measured on plain extrusions, BEFORE any boolean):**
|
||||
- `chamfer(all_top_edges, length)` fails at EVERY width on the barrel
|
||||
bridge outline and only succeeds at ~0.10 on the train bridge / balance
|
||||
cock, so `_finishing.anglage_top`'s whole-list retry ladder shipped
|
||||
bridges with zero or ~0.10 anglage (the blind critic's "plain vertical
|
||||
extruded walls").
|
||||
- Single-edge `chamfer` raises catchable ValueError on any arc bounded by
|
||||
a concave circle-circle junction (most of the visually large arcs).
|
||||
- Single- or multi-edge `chamfer` on a body that already carries bevels on
|
||||
NEIGHBORING arcs can SEGFAULT the process (exit 139, uncatchable, killed
|
||||
`scripts/gen`). Reproduced on the train-bridge wheel-reveal cutout rim
|
||||
after 3 neighboring arcs were beveled.
|
||||
- **Workaround (adopted):** never chamfer these rings; BAKE the 45-degree
|
||||
anglage into construction — extrude to `z_top - w`, then a tapered cap via
|
||||
`extrude(..., taper=45)`; when the draft prism itself fails (barrel
|
||||
outline: "BRepFill_TrimSurfaceTool ... incoherent intersection", and loft
|
||||
to `offset(prof, -w)` also fails), union per-circle `Cone(r, r-w, w)` caps
|
||||
clipped by the rim cone plus the inward-offset profile extruded through
|
||||
the band. Baked bevels also survive later booleans.
|
||||
- **Suggestion:** extend the "no 3D fillet after big booleans" guidance: on
|
||||
multi-arc blob outlines, OCC chamfer is unreliable even BEFORE booleans,
|
||||
and sequential chamfering can hard-crash; prefer constructive bevels.
|
||||
- **Blocked:** ~45 min. **Fixed:** in model source (`_bevel_extrude`).
|
||||
|
||||
## build123d 0.10: `Part(solid.wrapped)` reports `volume == 0`
|
||||
|
||||
- **Where found:** `models/renders/moonwatch/_mvt_base.py` stripe-shadow
|
||||
overlays (2026-08-06), splitting a multi-solid boolean result into one
|
||||
part per connected solid.
|
||||
- **Symptom:** re-wrapping a `Solid`'s `TopoDS_Solid` as `Part(sol.wrapped)`
|
||||
yields a shape whose `.volume` is 0 (probe: `Box(1,1,1)` solid -> Part
|
||||
wrap -> volume 0). Any volume-based guard then silently discards real
|
||||
geometry — a `shadow.volume < 1e-6: shadow = None` check threw away the
|
||||
balance-cock overlay bands with no error.
|
||||
- **Workaround:** use the `Solid` objects directly as labeled/colored
|
||||
compound children (Shape carries `label`/`color` fine), or fuse before
|
||||
measuring. Do not `Part(x.wrapped)` a bare solid.
|
||||
- **Blocked:** ~15 min (bands present in `inspect validate` count yet
|
||||
invisible; traced via descriptor + volume probes). **Fixed:** in model
|
||||
source.
|
||||
|
||||
## OCC multi-tool subtract with overlapping bore + ring tools leaves junk solids
|
||||
|
||||
- **Where found:** `models/renders/moonwatch/_case.py` case-middle
|
||||
finishing (2026-08-06): one batched `body - (functional_cuts +
|
||||
flank_ring_cutters + grain_tools)` where the crown-tube seat bore
|
||||
cylinder overlaps a stack of thin circumferential V-ring cutters.
|
||||
- **Symptom:** the single multi-tool `BRepAlgoAPI_Cut` returns a ShapeList
|
||||
of 5 solids instead of one: the main body, the crown-seat bore PLUG
|
||||
(volume ~18 mm^3 — material the bore tool should have removed, kept as a
|
||||
detached solid), and knife-edge slivers of the guard wall between
|
||||
adjacent ring cutters (~0.01 mm^3, detached skins). Every individual
|
||||
tool is a valid positive-volume solid; subtracting the same tools in two
|
||||
stages (functional cuts, then ring/grain cuts) yields one clean solid.
|
||||
- **Workaround (adopted):** split heavily overlapping tool families into
|
||||
separate batched subtracts — still list-based multi-tool booleans, never
|
||||
pairwise accumulation. Downstream `Compound(children=...)` also fails
|
||||
loudly on the ShapeList ("not a subclass of NodeMixin"), which is how it
|
||||
surfaced.
|
||||
- **Suggestion:** the "booleans over many tools: ONE operation" guidance
|
||||
needs a caveat: when tools overlap each other (a bore crossing a stack of
|
||||
near-tangent finishing cutters), OCC's multi-tool cut can emit wrong
|
||||
results; group tools so each batch is internally disjoint-ish.
|
||||
- **Blocked:** ~20 min (typed probes per builder, then staged-subtract
|
||||
bisection). **Fixed:** in model source (two-stage subtract).
|
||||
|
||||
## 9. No per-part material properties in the component-GLB render path (missing feature; the project's aesthetic ceiling)
|
||||
|
||||
- **Doing:** whole-watch and movement blind A/B comparisons against professional
|
||||
macro photography (the moonwatch project's core acceptance loop).
|
||||
- **Limitation:** the presentation theme applies ONE global
|
||||
roughness/metalness/clearcoat to every part; descriptor occurrence colors
|
||||
carry only RGB (see entry 6). There is no way to render brushed steel next
|
||||
to mirror-polished steel next to matte lacquer as *different material
|
||||
responses* — only albedo differs. Across ~20 fresh-context critic rounds,
|
||||
after all geometric finishing (anglage ribbons, V-groove striping, satin
|
||||
grain, snailing, perlage) was modeled and visible, every remaining loss
|
||||
verdict converged on the same sentence: "the metal has only one finish /
|
||||
uniform material response / reads as primed plastic."
|
||||
- **Workarounds used:** geometric micro-texture (V-grooves at 0.14–0.2 mm
|
||||
pitch) + albedo deltas + bright overlay "ribbon" bodies for polished zones +
|
||||
raking key light + strong neutral HDRI. These moved every render
|
||||
substantially but cannot produce anisotropic specular contrast.
|
||||
- **Suggestion (follow-up feature):** thread optional per-occurrence
|
||||
`roughness`/`metalness` (and finish the alpha channel from entry 6) from
|
||||
cadgen's descriptor into `packages/cadjs` part records and per-part
|
||||
materials; the snapshot runtime picks it up on rebundle. This is the single
|
||||
highest-leverage renderer change for photoreal CAD presentation.
|
||||
- **Blocked:** the blind-A/B win condition, not the modeling. **Fixed:** no
|
||||
(out of minimal-fix scope; documented).
|
||||
|
||||
## `safe_chamfer`'s volume-only gate accepts BOP-self-intersecting chamfers that `inspect validate` then rejects
|
||||
|
||||
- **Where found:** `models/renders/moonwatch/_mvt_chrono.py` lever anglage
|
||||
(2026-08-07, chronograph-works cluster). `F.anglage_top` /
|
||||
`F.safe_chamfer` accept a chamfer result when `result.volume > 0`.
|
||||
- **Symptom:** on some capsule-chain lever perimeters (reset lever, reset
|
||||
spring, brake lever) OCC `chamfer` at 0.14 returned a positive-volume solid
|
||||
whose skinny chamfer faces are BOP-faulty — `BRepAlgoAPI_Check` reports
|
||||
`BOPAlgo_SelfIntersect` + `BOPAlgo_TooSmallEdge` — so the retry ladder
|
||||
"succeeded" and shipped parts that `inspect validate` flags as
|
||||
`selfIntersecting` (3 failures). `BRepCheck_Analyzer.IsValid()` on the same
|
||||
solids is True, so simple topology validity checks do not catch it either;
|
||||
only the BOP check used by `cadgen.validity` does.
|
||||
- **Workaround (adopted):** `_mvt_chrono._lever` wraps the ladder with its own
|
||||
`BRepAlgoAPI_Check` gate and steps the width down (0.7x) until the chamfer
|
||||
passes, else skips the anglage.
|
||||
- **Suggestion:** `F.safe_chamfer`/`safe_fillet` should gate on the same BOP
|
||||
check `inspect validate` uses, not just `volume > 0` — silent acceptance
|
||||
here surfaces only at validation time with no pointer to the causing op.
|
||||
- **Blocked:** ~15 min. **Fixed:** locally in `_mvt_chrono.py` (helper
|
||||
unchanged; other clusters using `anglage_top` on wavy outlines can hit it).
|
||||
|
||||
## `snailing_cutter` V-groove cuts can leave BOP-self-intersecting results when a groove wall runs tangent to the target's profile wall
|
||||
|
||||
- **Doing:** cutting circular Geneva striping (the `_mvt_base._bridge`
|
||||
pattern: `F.snailing_cutter(...)` rings about the movement center,
|
||||
intersected with a stripe band, subtracted from a blob-outline bridge) into
|
||||
the new chronograph coupling cock in
|
||||
`models/renders/moonwatch/_mvt_chrono.py` (2026-08-07).
|
||||
- **Symptom:** the cut "succeeds" — one positive-volume solid,
|
||||
`BRepCheck_Analyzer.IsValid()` True — but `BRepAlgoAPI_Check` reports
|
||||
self-intersection, so `inspect validate` flags the part
|
||||
(`selfIntersecting`, 1 failure) with no pointer to the causing operation.
|
||||
Probe-bisected: the plain bevel extrude, jewel countersink and screw sink
|
||||
were all clean; adding the stripe cut alone flipped the part to
|
||||
BOP-faulty. The cock's outline circles sat at radii where a groove's wall
|
||||
cone ran tangent to the stripe-band inset wall (outline centers ~10.2–11.8
|
||||
from origin vs groove edge circles at 10.7 +/- 0.925, 12.6 +/- 0.925).
|
||||
- **Workaround:** nudge the outline centers/radii by ~0.03 until the
|
||||
tangency breaks (`COUPLING_COCK_OUTLINE` comment records the tuned
|
||||
values); verify with `BRepAlgoAPI_Check` per part before shipping.
|
||||
- **Suggestion:** same class as the `safe_chamfer` entry above — cutters
|
||||
built from `F.snailing_cutter` (and other tangency-prone V-groove tools)
|
||||
should be BOP-checked after the boolean by the shared vocabulary, or
|
||||
`inspect validate` should name the last boolean when a part fails.
|
||||
- **Blocked:** ~20 min (bisecting which cut was faulty). **Fixed:** locally
|
||||
(geometry nudged; helper unchanged).
|
||||
|
||||
## OCC kernel operations are broadly fragile on dense periodic B-spline profile faces (taper extrude, wire offset, coincident-face fuse, ruled loft at sharp corners)
|
||||
|
||||
- **Doing:** replacing the moonwatch bridge outlines (blobbed circle chains)
|
||||
with smooth closed silhouettes — ONE periodic `Spline` fit through ~250
|
||||
Catmull-Rom samples per bridge, `make_face`, then the `_bridge` factory's
|
||||
bevel/stripe/ribbon machinery — in
|
||||
`models/renders/moonwatch/_mvt_base.py` / `_mvt_chrono.py` (2026-08-07).
|
||||
- **Symptoms** (each probe-verified in isolation on build123d 0.10 / OCP
|
||||
7.9):
|
||||
1. `extrude(face, amount, taper=45)` (`LocOpe_DPrism`) throws on EVERY one
|
||||
of the six spline profiles — `BRepFill_TrimSurfaceTool::IntersectWith:
|
||||
incoherent intersection`, `NCollection_DataMap::Find`,
|
||||
`BRepFill_MultiLine: ValueOnFace` — although the same call succeeds on
|
||||
small hand-sampled spline faces (~40 pts).
|
||||
2. `offset(face, -d, kind=Kind.ARC)` (`BRepOffsetAPI_MakeOffset` via
|
||||
`Wire.offset_2d`) returns Null (`ValueError: Null TopoDS_Shape
|
||||
object`) for EVERY inward delta on the balance-cock profile while
|
||||
outward deltas work; barrel/train/pallet profiles offset fine.
|
||||
3. Fusing two individually valid solids that share a coincident
|
||||
spline-bounded planar face (straight-wall extrude + its 45-deg bevel
|
||||
cap, common face = the same spline profile) returns an EMPTY result —
|
||||
`+` succeeds, `volume == 0`, zero solids — on the barrel profile only;
|
||||
the same construction fused clean on the other five.
|
||||
4. `BRepOffsetAPI_ThruSections` (build123d `loft(..., ruled=True)`)
|
||||
between a profile and its inward offset builds but is
|
||||
`BRepCheck_Analyzer`-INVALID when the outer wire has sharp corners
|
||||
(the ruled surface folds where outer corner radius < offset delta).
|
||||
- **Workarounds (adopted, in `_mvt_base.py`):** compute ALL profile offsets
|
||||
numerically on the dense sample loop (normal offset + drop points closer
|
||||
than |delta| to the source polyline — the classic offset-validity prune —
|
||||
+ resample + light smoothing), never calling kernel offset; build the
|
||||
beveled body as ONE 3-section ruled loft (wall, wall, inward offset) so no
|
||||
coincident-face fuse exists; give outline traces explicit corner-rounding
|
||||
points at rim junctions; derive the polished-ribbon shell from the body's
|
||||
own cap translated vertically (a 45-deg cone translated up by d equals the
|
||||
cone grown horizontally by d) instead of lofting a second grown pair.
|
||||
- **Suggestion:** treat `extrude(taper=)`, kernel wire offset and
|
||||
coincident-face fuses as unavailable for interpolated many-point spline
|
||||
profiles; prefer ruled lofts between numerically offset sections. (This
|
||||
also supersedes the previous entry's `COUPLING_COCK_OUTLINE` 0.03-nudge
|
||||
note — the blob outlines it tuned no longer exist.)
|
||||
- **Blocked:** ~1.5 h across the four failures. **Fixed:** locally
|
||||
(helpers in `_mvt_base.py`; kernel behavior unchanged).
|
||||
|
||||
### Entry 9 addendum (final, 2026-08-07)
|
||||
|
||||
Per-part PBR materials + opacity WERE subsequently implemented (root sources,
|
||||
entry 6/9 suggestion; commit "render pipeline: per-occurrence PBR material
|
||||
overrides + opacity"). They transformed the renders — real glass, lacquer,
|
||||
mirror-vs-brushed steel — and moved blind-critic verdicts from "clay toy" to
|
||||
component-level finishing quibbles. Nine further whole-watch rounds still
|
||||
lost against professional photographs of the actual reference watch; late
|
||||
verdicts repeatedly named features that demonstrably exist in the geometry
|
||||
(striping, anglage ribbons, domed jewels, slotted screws), indicating the
|
||||
remaining delta is renderer-class: no anisotropic BSDFs, no path-traced
|
||||
bounce/DOF/grain. A photoreal path-tracing backend (or export to one) is the
|
||||
next real step for the blind-A/B-vs-photograph bar; the modeling side is no
|
||||
longer the limiting factor.
|
||||
@@ -417,3 +417,11 @@ manual edits inside generated runtime folders.
|
||||
CAD exchange files, generated render/topology assets, and `assets/**` may be
|
||||
LFS-tracked. Never disable LFS filters for `git add`, commits, or other
|
||||
object-writing operations.
|
||||
|
||||
`assets/**` holds heavyweight demo GIFs and is excluded from default LFS pulls,
|
||||
so lightweight clones do not fetch it. Hydrate it only when you need the demo
|
||||
assets locally:
|
||||
|
||||
```bash
|
||||
git lfs pull --include="assets/**"
|
||||
```
|
||||
|
||||
@@ -38,6 +38,29 @@ text-to-cad is a library of agent skills for generating, inspecting, sourcing,
|
||||
slicing, and handing off CAD and robot-description artifacts from local project
|
||||
files.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="33%">
|
||||
<a href="./assets/text-to-cad-demo.gif">
|
||||
<img src="./assets/text-to-cad-demo.gif" alt="CAD skill demo showing generated geometry in CAD Viewer" width="100%">
|
||||
</a>
|
||||
<a href="./skills/cad/SKILL.md"><strong>CAD</strong></a>
|
||||
</td>
|
||||
<td width="33%">
|
||||
<a href="./assets/urdf-demo.gif">
|
||||
<img src="./assets/urdf-demo.gif" alt="URDF skill demo showing robot description output in CAD Viewer" width="100%">
|
||||
</a>
|
||||
<a href="./skills/urdf/SKILL.md"><strong>URDF</strong></a>
|
||||
</td>
|
||||
<td width="33%">
|
||||
<a href="./assets/srdf-moveit2-demo.gif">
|
||||
<img src="./assets/srdf-moveit2-demo.gif" alt="SRDF MoveIt2 skill demo showing inverse kinematics in CAD Viewer" width="100%">
|
||||
</a>
|
||||
<a href="./skills/srdf/SKILL.md"><strong>SRDF / MoveIt2</strong></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 🧰 Skills
|
||||
|
||||
Install the library to give agents focused workflows for CAD, fabrication,
|
||||
@@ -97,39 +120,6 @@ Restart your agent if newly installed skills do not appear. For local
|
||||
development, branch from `develop`, open PRs against `develop`, and use the symlink
|
||||
workflow in [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
The repo stores heavyweight GIF assets in `assets/**` through Git LFS and
|
||||
excludes that tree from default LFS pulls, so lightweight clones do not fetch
|
||||
them. To hydrate the demo assets locally, run:
|
||||
|
||||
```bash
|
||||
git lfs pull --include="assets/**"
|
||||
```
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="33%">
|
||||
<a href="./assets/text-to-cad-demo.gif">
|
||||
<img src="./assets/text-to-cad-demo.gif" alt="CAD skill demo showing generated geometry in CAD Viewer" width="100%">
|
||||
</a>
|
||||
<a href="./skills/cad/SKILL.md"><strong>CAD</strong></a>
|
||||
</td>
|
||||
<td width="33%">
|
||||
<a href="./assets/urdf-demo.gif">
|
||||
<img src="./assets/urdf-demo.gif" alt="URDF skill demo showing robot description output in CAD Viewer" width="100%">
|
||||
</a>
|
||||
<a href="./skills/urdf/SKILL.md"><strong>URDF</strong></a>
|
||||
</td>
|
||||
<td width="33%">
|
||||
<a href="./assets/srdf-moveit2-demo.gif">
|
||||
<img src="./assets/srdf-moveit2-demo.gif" alt="SRDF MoveIt2 skill demo showing inverse kinematics in CAD Viewer" width="100%">
|
||||
</a>
|
||||
<a href="./skills/srdf/SKILL.md"><strong>SRDF / MoveIt2</strong></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 🛠️ Contributing
|
||||
|
||||
Development happens from the `develop` branch; open PRs against `develop`, not `main`.
|
||||
|
||||
@@ -198,8 +198,7 @@ def _crowned_face(xa, xb, z_top, z_bot, s_top, bev, s_bot=0.0, rnd=(False, False
|
||||
45-degree bevels (wrist side). Baking crown, bevels, and shoulder
|
||||
rounds into the SECTION replaces 3D edge chamfers/fillets entirely
|
||||
— OCC's chamfer on link perimeters that touch dome/eye-cap tangent
|
||||
chains silently fails, churns for minutes, or segfaults (see
|
||||
/BUGS.md).
|
||||
chains silently fails, churns for minutes, or segfaults.
|
||||
"""
|
||||
ba, bb = bev if isinstance(bev, tuple) else (bev, bev)
|
||||
ra, rb = rnd
|
||||
@@ -667,7 +666,7 @@ def make_end_link():
|
||||
# lines (along Y at the outer widths, near the top), the joint-1
|
||||
# rolled shoulder (chamfering its loft-station seams would flatten
|
||||
# the roll — and OCC chamfers on such tangent chains fail or
|
||||
# segfault, see /BUGS.md), and the groove shoulders crisp
|
||||
# segfault), and the groove shoulders crisp
|
||||
def _keep(e):
|
||||
bb = e.bounding_box()
|
||||
on_bevel_band = (
|
||||
|
||||
@@ -19,7 +19,7 @@ the train `JEWEL_POSITIONS_UPPER` entries) rise a further 0.06 above
|
||||
`RATCHET_SCREW_TOP_Z` over `BARREL_POS`.
|
||||
|
||||
NOTE: `_finishing.py` helpers assume `align=(None,None,None)` centers
|
||||
primitives; it actually leaves the raw OCC datum (see /BUGS.md). This module
|
||||
primitives; it actually leaves the raw OCC datum. This module
|
||||
compensates with corrective cuts (countersink cones, full-depth wheel
|
||||
windows, perlage z-shift) or constructive local replacements (`_screw`:
|
||||
flat slotted heads — F.slotted_screw's domed cap read as a featureless blob
|
||||
@@ -390,8 +390,8 @@ def _blob(circles, clip_r, cutouts=()):
|
||||
ONE multi-operand fuse, clip LAST: pairwise 2D `+` decays once operands
|
||||
stop overlapping (the balance-cock STRIPE INSET went disjoint at the stud
|
||||
lobe, its `& Circle` clip returned silently EMPTY, and the cock shipped
|
||||
with no striping cut at all — same failure class as /BUGS.md's keyless
|
||||
entry)."""
|
||||
with no striping cut at all — same failure class as the keyless-entry
|
||||
defect)."""
|
||||
discs = [Pos(x, y) * Circle(r) for x, y, r in circles]
|
||||
prof = discs[0] + discs[1:] if len(discs) > 1 else discs[0]
|
||||
prof = prof & Circle(clip_r)
|
||||
@@ -450,7 +450,7 @@ def _add_anglage(parts, ribbon, label):
|
||||
"""Append a bridge's polished-bevel ribbon overlay (list of connected
|
||||
solids), one part per solid — same pattern/rationale as `_add_shadow`
|
||||
(disjoint arcs in one part trip validation; `Part(sol.wrapped)` loses
|
||||
`.volume` on build123d 0.10, see /BUGS.md)."""
|
||||
`.volume` on build123d 0.10)."""
|
||||
if not ribbon:
|
||||
return
|
||||
if len(ribbon) == 1:
|
||||
@@ -500,7 +500,7 @@ def _sink_tool(cone_r=0.97, wall_r=0.72, cone_z=0.20, seat_z=0.60, bore_r=0.45):
|
||||
at depth cone_z — the bright polished ring of a real sink), a cylindrical
|
||||
seat counterbore holding chaton + jewel, then the through bore. Built
|
||||
locally: F.jewel_countersink_cut's cone is half above the surface AND
|
||||
inverted (raw-OCC-datum bug, /BUGS.md), so at any parameterization its
|
||||
inverted (raw-OCC-datum bug), so at any parameterization its
|
||||
opening bulges widest just under the rim — it cannot produce the
|
||||
countersink-ring read of a real jewel setting."""
|
||||
tool = (Pos(0, 0, -cone_z / 2 + 0.001) * Cone(wall_r, cone_r, cone_z)
|
||||
@@ -548,7 +548,7 @@ def _grain(span_l, span_w, cx, cy, z_top, angle, clip=None):
|
||||
|
||||
def _window_cutter(od, frac):
|
||||
"""Full-depth replica of F.train_wheel's crossing-out cutter (the helper
|
||||
only cuts the top half of every spoke window; see /BUGS.md)."""
|
||||
only cuts the top half of every spoke window)."""
|
||||
r_o = od / 2.0
|
||||
r_root = r_o - r_o * frac
|
||||
rim_inner = r_root - od * 0.10
|
||||
@@ -597,7 +597,7 @@ def _bevel_extrude(outline, z0, z1, w):
|
||||
tapered cap. OCC `chamfer` cannot cut this ring — F.anglage_top's
|
||||
whole-list retry shrank or dropped bevels, single-edge chamfers refuse
|
||||
concave junctions, and grouped chamfers after neighbors exist SEGFAULT
|
||||
OCC (see /BUGS.md). Baking the bevel also means every later boolean
|
||||
OCC. Baking the bevel also means every later boolean
|
||||
(stripes, sinks) cannot erase it. Built as ONE 3-section ruled loft
|
||||
(wall, wall, inward offset): fusing a straight extrude with a separate
|
||||
cap returned EMPTY on the barrel outline — OCC BOP glue failure on the
|
||||
@@ -1352,7 +1352,7 @@ def _balance_parts():
|
||||
# lyre spring: crisp wire form — thin outer arc ring, three radial arms
|
||||
# into a small hub ring circling the cap jewel dome, two feet flanking the
|
||||
# opening (the old 0.20-wide band + fat feet fused into one white blob at
|
||||
# macro). One multi-operand 2D fuse, gap wedge subtracted LAST (/BUGS.md).
|
||||
# macro). One multi-operand 2D fuse, gap wedge subtracted LAST.
|
||||
# z 3.20..3.29 stays inside COCK_SHOCK_TOP_Z = 3.4.
|
||||
gap_a = -40.0
|
||||
outer_ring = Circle(1.06) - Circle(0.95)
|
||||
|
||||
@@ -29,7 +29,7 @@ over bridge screw heads (top 2.91) at z >= 2.96, over the ratchet wheel
|
||||
stud drops only onto open plate (z=0) or a bridge top (2.85) that is clear
|
||||
below (verified against the base's train-wheel plan in `_mvt_base`).
|
||||
|
||||
`_finishing.py`'s align=(None,None,None) datum quirks (see /BUGS.md) are
|
||||
`_finishing.py`'s align=(None,None,None) datum quirks are
|
||||
compensated locally exactly as `_mvt_base.py` does (centered primitives via
|
||||
default align; `F.train_wheel` spoke windows re-cut full depth).
|
||||
"""
|
||||
@@ -211,7 +211,7 @@ def _cyl(r, h, z0):
|
||||
|
||||
|
||||
def _fuse2d(pieces, clip_r=13.4):
|
||||
"""ONE multi-operand sketch fuse + regularizing clip (see /BUGS.md)."""
|
||||
"""ONE multi-operand sketch fuse + regularizing clip."""
|
||||
prof = pieces[0] + pieces[1:] if len(pieces) > 1 else pieces[0]
|
||||
return prof & Circle(clip_r)
|
||||
|
||||
@@ -275,7 +275,7 @@ def _bop_clean(shape):
|
||||
def _lever(profile, z_band, label, anglage=S.ANGLAGE_WIDTH_SMALL,
|
||||
color=S.STEEL_LEVER, cuts=()):
|
||||
"""Extrude a lever plate, apply top anglage BEFORE the bore cuts
|
||||
(chamfer-after-boolean is the OCC segfault class, /BUGS.md).
|
||||
(chamfer-after-boolean is the OCC segfault class).
|
||||
|
||||
F.safe_chamfer only checks volume > 0, but on some capsule-chain
|
||||
perimeters OCC returns a positive-volume chamfer whose skinny faces are
|
||||
@@ -297,7 +297,7 @@ def _lever(profile, z_band, label, anglage=S.ANGLAGE_WIDTH_SMALL,
|
||||
def _screw_head(head_d=1.1, hh=0.24, slot_angle=0.0, color=S.BLUED):
|
||||
"""Flat mirror-top slotted screw head, top at z = 0, hanging below —
|
||||
the same constructive read as `_mvt_base._screw` (domed F.slotted_screw
|
||||
caps render as featureless blobs at macro; see /BUGS.md)."""
|
||||
caps render as featureless blobs at macro)."""
|
||||
r = head_d / 2.0
|
||||
ch = min(0.07, 0.30 * hh)
|
||||
depth = min(0.16, hh - 0.06)
|
||||
@@ -329,7 +329,7 @@ def _stud_and_screw(parts, x, y, base_z, lever_z0, lever_z1, label,
|
||||
|
||||
def _window_cutter(od, frac):
|
||||
"""Full-depth replica of F.train_wheel's crossing-out cutter (the helper
|
||||
only cuts the top half of every spoke window; see /BUGS.md)."""
|
||||
only cuts the top half of every spoke window)."""
|
||||
r_o = od / 2.0
|
||||
r_root = r_o - r_o * frac
|
||||
rim_inner = r_root - od * 0.10
|
||||
@@ -882,8 +882,8 @@ def build_chrono():
|
||||
parts += _brake_parts()
|
||||
parts += _chrono_bridge_parts()
|
||||
|
||||
# Per-component export drops the color of bare-`Compound` leaves (see
|
||||
# /BUGS.md) — coerce every leaf to Part and reattach label/color.
|
||||
# Per-component export drops the color of bare-`Compound` leaves --
|
||||
# coerce every leaf to Part and reattach label/color.
|
||||
from build123d import Compound, Part
|
||||
out = []
|
||||
for p in parts:
|
||||
|
||||
@@ -13,7 +13,7 @@ Everything here stays in the dial-side band z [-2.64, -1.5] EXCEPT:
|
||||
- the cannon / hour wheel pipes, which extend below -2.64 because they pass
|
||||
through the dial at assembly (pipe ends ~ -3.4 / -3.1).
|
||||
|
||||
`_finishing.py`'s align=(None,None,None) datum quirks (see /BUGS.md) are
|
||||
`_finishing.py`'s align=(None,None,None) datum quirks are
|
||||
compensated locally exactly as `_mvt_base.py` does: `F.pinion` spans
|
||||
z [0, L]; `F.slotted_screw` gets corrective shank/slot cuts via `_screw`.
|
||||
|
||||
@@ -176,7 +176,7 @@ def _edges_at_z(part, z, tol=1e-4):
|
||||
|
||||
def _screw(head_d=S.SCREW_HEAD_DIAMETER, hh=0.32, shank=0.9,
|
||||
slot_w=S.SCREW_SLOT_WIDTH, color=S.BLUED):
|
||||
"""F.slotted_screw + corrective cuts (see /BUGS.md: align datum): flatten
|
||||
"""F.slotted_screw + corrective cuts (align datum): flatten
|
||||
the shank stub poking through the dome, cut a proper centered slot.
|
||||
Returns (part, top): head apex ~ local z = top, shank hanging below."""
|
||||
s = F.slotted_screw(head_diameter=head_d, head_height=hh,
|
||||
@@ -396,7 +396,7 @@ def _motion_parts():
|
||||
|
||||
# cannon pinion at the center: 12 leaves, pipe down through the dial.
|
||||
# -3 deg seats the mesh at the measured minimum flank contact
|
||||
# (F.pinion leaves sit half a leaf-width off-axis; see /BUGS.md)
|
||||
# (F.pinion leaves sit half a leaf-width off-axis)
|
||||
rot_c = _ang(ctr, mw) + 180.0 / S.CANNON_PINION_LEAVES - 3.0
|
||||
leaves = Pos(0, 0, _CANNON_LEAF_Z[0]) * Rot(0, 0, rot_c) * F.pinion(
|
||||
S.CANNON_PINION_LEAVES, CANNON_TIP_D,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Component-GLB package emit (design/component-glb-artifacts.md).
|
||||
"""Component-GLB package emit.
|
||||
|
||||
Every model's render artifact is a package directory under the per-folder
|
||||
``__cadgen__`` home, holding one content-addressed component GLB per unique
|
||||
|
||||
@@ -12,8 +12,8 @@ the model folder's ``__cadgen__/models/``, keyed by the entry filename:
|
||||
**Two payloads with different jobs.** ``drawing.dxf`` is the exchange artifact (exported,
|
||||
downloaded, re-imported); ``preview.glb`` is what the viewport renders. The GLB is not an
|
||||
optimization: with the 2D SVG view deleted, the 3D mesh is the ONLY DXF view, and baking it
|
||||
here is what lets the browser stop carrying ~1,800 lines of DXF parsing and extrusion
|
||||
(design/unified-glb-render-artifacts.md §7.4.2). It is built by a Node child of whichever
|
||||
here is what lets the browser stop carrying ~1,800 lines of DXF parsing and extrusion.
|
||||
It is built by a Node child of whichever
|
||||
process holds this package's generation lock, because the mesher is JS and is reused verbatim
|
||||
rather than reimplemented.
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ viewer shares. The package makes it an ordinary baked mesh:
|
||||
model.glb # the baked mesh (quantized + meshopt-compressed)
|
||||
|
||||
**Baked, not live.** The mesh is sampled at the model's ``params.*.default`` values and one
|
||||
fixed resolution; live parameters and animations are gone, deliberately and with no opt-in
|
||||
(design/unified-glb-render-artifacts.md §0.1). Those settings are invisible to every other
|
||||
fixed resolution; live parameters and animations are gone, deliberately and with no
|
||||
opt-in. Those settings are invisible to every other
|
||||
freshness signal -- the source is unchanged, the payload is present, the closure still hashes
|
||||
-- so they are canonicalized into ``bakeHash`` and compared by BOTH freshness authorities.
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Descriptor-level freshness primitives shared by BOTH freshness authorities.
|
||||
|
||||
A render package's currency is decided twice, by design (see
|
||||
``design/unified-glb-render-artifacts.md`` §3.3/§5.3): the viewer's validator
|
||||
A render package's currency is decided twice, by design: the viewer's validator
|
||||
(``viewer/server_py/artifact.py``) decides what a status GET answers, and the producer's
|
||||
per-kind ``is_current`` callable — re-evaluated under the generation lock — decides
|
||||
whether a build no-ops. When the two disagree the failure is SILENT rather than loud: the
|
||||
|
||||
@@ -59,7 +59,7 @@ PHASE_WRITE = "write"
|
||||
# in the NODE child (parse the drawing, mesh the flat pattern, write the GLB) while this
|
||||
# process holds the lock, so they are declared here -- one run id and one status record span
|
||||
# both runtimes, and the bar has to be weighted over the phases it will actually be told
|
||||
# about (design/unified-glb-render-artifacts.md §7.4.2).
|
||||
# about.
|
||||
DRAWING_PACKAGE = ArtifactKind(
|
||||
name="drawing-package",
|
||||
phases=(PHASE_GENERATE, PHASE_PARSE, PHASE_MESH, PHASE_WRITE, PHASE_FINALIZE),
|
||||
|
||||
@@ -4,7 +4,7 @@ The implicit analogue of :mod:`cadgen.dxf_artifact`, and the SINGLE producer beh
|
||||
entrypoints that build an implicit package -- ``skills/implicit-cad/scripts/gen`` and the
|
||||
viewer's ``POST /__cad/artifact``. A second producer that assembled the lock, the status
|
||||
record and the currency gate by hand is exactly how the defects the coordination refactor
|
||||
fixed came to exist (design/unified-glb-render-artifacts.md §4.7).
|
||||
fixed came to exist.
|
||||
|
||||
The whole body runs inside ``artifact_build``: the lock is held across the freshness
|
||||
re-check, the Node mesher, and the descriptor write. The mesher is a CHILD of this process
|
||||
|
||||
@@ -5,8 +5,7 @@ Viewer's "Export model" action must not mesh geometry in the browser. It used to
|
||||
client loaded the ``.implicit.js`` module, meshed and serialized it in the tab, and POSTed
|
||||
the bytes for the server to write -- which made the viewer carry a second, live geometry
|
||||
runtime purely so a menu item could work. The mesher is JS either way; the difference is
|
||||
which process runs it, and running it here means the viewer keeps exactly one render path
|
||||
(design/unified-glb-render-artifacts.md §8, §0.1 decision 3).
|
||||
which process runs it, and running it here means the viewer keeps exactly one render path.
|
||||
|
||||
**Live-valued, unlike the render package.** ``model.glb`` is baked at ``params.*.default``
|
||||
and one fixed resolution, on purpose. An export is not: ``--params`` / ``--animation`` /
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Structural render-cost benchmark for a component-GLB package.
|
||||
//
|
||||
// Measures the metrics the viewer large-package rendering plan targets
|
||||
// (design/viewer-large-package-rendering.md), at the JS/meshData layer so it
|
||||
// runs deterministically in Node with no GPU/browser:
|
||||
// Measures the metrics that matter for large-package rendering, at the JS/meshData
|
||||
// layer so it runs deterministically in Node with no GPU/browser:
|
||||
//
|
||||
// - occurrences vs unique components (the dedup the render layer keeps)
|
||||
// - composed top-level vertices vs unique-component vertices. The shared-geometry
|
||||
|
||||
@@ -355,7 +355,7 @@ function meshPartNumericValue(part, key) {
|
||||
return Math.max(0, Math.floor(Number(part?.[key]) || 0));
|
||||
}
|
||||
|
||||
// --- Component-GLB package composition (design/component-glb-artifacts.md) -------
|
||||
// --- Component-GLB package composition ------------------------------------------
|
||||
//
|
||||
// A package's component GLBs are meshed once in their LOCAL frame and instanced N
|
||||
// times by the assembly descriptor. Composition keeps one component-local copy of
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
* This is the build-time half of the DXF render path: `buildDxfPreviewMeshData` is reused
|
||||
* VERBATIM as an input (it is ~1,300 lines of meshing nobody is rewriting), and its output is
|
||||
* handed to the shared `writeGlb`. Once the package carries `preview.glb` the browser stops
|
||||
* parsing and extruding DXF at open time -- the viewport is fed a GLB like every other entry
|
||||
* (design/unified-glb-render-artifacts.md §7.4.2).
|
||||
* parsing and extruding DXF at open time -- the viewport is fed a GLB like every other entry.
|
||||
*
|
||||
* It lives in `src/` rather than in `bin/dxf-artifact.mjs` so it is testable without spawning
|
||||
* a process: the builder script owns argv, file IO and the NDJSON protocol, and this owns the
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
*
|
||||
* It throws UNCONDITIONALLY -- there is no `CADGEN_STRICT_LOCKS` escape hatch like the Python
|
||||
* side's. The Python check is old enough to have callers whose environments must degrade
|
||||
* rather than fail; this one is new, and new code fails loud
|
||||
* (design/unified-glb-render-artifacts.md §0.2, §4.4).
|
||||
* rather than fail; this one is new, and new code fails loud.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
|
||||
@@ -797,9 +797,8 @@ export function implicitCadCameraState(model, camera = "iso", {
|
||||
//
|
||||
// Every function here also exists, hand-written a second time, as a JS entry in the BUILTINS
|
||||
// table of `sdfEvaluator.js`: the GPU runs these bodies, the baked render artifact runs those.
|
||||
// Two implementations of one contract is the divergence design/unified-glb-render-artifacts.md
|
||||
// section 7.2 flags, so `sdfField.test.js` parses THIS text and differentially evaluates it
|
||||
// against the JS table. It is interpolated verbatim into the shader below.
|
||||
// Two implementations of one contract is a standing divergence risk, so `sdfField.test.js`
|
||||
// parses THIS text and differentially evaluates it against the JS table. It is interpolated verbatim into the shader below.
|
||||
export const IMPLICIT_CAD_GLSL_LIBRARY = `float implicit_clamp01(float value) {
|
||||
return clamp(value, 0.0, 1.0);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* - CONSERVATIVE: call the interpreter's own helper/builtin function objects in the
|
||||
* interpreter's order -- bit-identical by construction. This is the fallback for any
|
||||
* expression whose type cannot be established.
|
||||
* - TYPED (phase 2, design/implicit-glsl-compiler.md §6): GLSL is statically typed, so
|
||||
* - TYPED: GLSL is statically typed, so
|
||||
* when an expression's SHAPE (scalar vs vec width) is certain, arithmetic inlines to raw
|
||||
* JS operators and componentwise array literals. Each inlined form performs the same
|
||||
* per-component IEEE-754 operations `mapBinary`/`mapUnary` would ("?? 0" fills are
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Focused tests for the GLSL->JS compiler's mirrored semantics -- each one targets a spot
|
||||
// where idiomatic JS codegen would silently diverge from the interpreter
|
||||
// (design/implicit-glsl-compiler.md §2.3). The corpus harness (sdfEquality.test.js) is the
|
||||
// where idiomatic JS codegen would silently diverge from the interpreter. The corpus
|
||||
// harness (sdfEquality.test.js) is the
|
||||
// broad gate; these pin the individual traps with hand-built programs so a regression names
|
||||
// the exact semantic it broke.
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ function evaluateModel(model) {
|
||||
|
||||
const sdf = createImplicitCadSdfEvaluator(model);
|
||||
const distances = points.map(([x, y, z]) => sdf(x, y, z));
|
||||
// COVERAGE GATE for the GLSL->JS compiler (design/implicit-glsl-compiler.md C4). The
|
||||
// COVERAGE GATE for the GLSL->JS compiler. The
|
||||
// compiler falls back to the interpreter silently on any problem -- correct for
|
||||
// production, but in the corpus it must be a loud failure: a codegen regression that
|
||||
// falls back everywhere keeps every digest green while the speedup silently vanishes.
|
||||
|
||||
@@ -297,7 +297,7 @@ const BUILTINS = {
|
||||
// fine and then failed the moment it was MESHED ("Unknown GLSL function: cosh", which is
|
||||
// exactly how `catenoid-ring-bridge` behaved). The baked render package makes the CPU
|
||||
// evaluator the thing users see, so a builtin the shader has and this does not is a
|
||||
// divergence, not a missing feature (design/unified-glb-render-artifacts.md §7.2).
|
||||
// divergence, not a missing feature.
|
||||
radians: (value) => mapUnary(value, (component) => (component * Math.PI) / 180),
|
||||
degrees: (value) => mapUnary(value, (component) => (component * 180) / Math.PI),
|
||||
asin: (value) => mapUnary(value, Math.asin),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// THE FIELD-AGREEMENT GATE (design/unified-glb-render-artifacts.md §9, phase 2 exit criteria;
|
||||
// a hard precondition on phase 3).
|
||||
// THE FIELD-AGREEMENT GATE.
|
||||
//
|
||||
// An implicit model is authored ONCE, in GLSL, and evaluated TWICE by unrelated code:
|
||||
//
|
||||
@@ -101,8 +100,7 @@ const MODELS_WITHOUT_A_SURFACE = new Map([
|
||||
[
|
||||
"menger-sponge.implicit.js",
|
||||
"known model defect: sdf >= 2.77 everywhere inside its own declared bounds (the `cross` "
|
||||
+ "term never goes negative), so it has no surface on the GPU either -- see "
|
||||
+ "design/unified-glb-render-artifacts.md §0.0",
|
||||
+ "term never goes negative), so it has no surface on the GPU either",
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
// `sdfField.test.js` is the gate, and it is browser-free by necessity -- CI never runs
|
||||
// `playwright install`, and a gate that skips is not a gate. That leaves it with one stated
|
||||
// blind spot it cannot close from Node: GLSL is typed and JavaScript is not, so a rewrite that
|
||||
// turns `i == 0` into `i == 0.0` -- one of the two normalizer bugs
|
||||
// design/unified-glb-render-artifacts.md §7.2 names -- changes NOTHING a JS evaluator can
|
||||
// turns `i == 0` into `i == 0.0` -- one of the two known normalizer bugs -- changes
|
||||
// NOTHING a JS evaluator can
|
||||
// observe (`0` and `0.0` are the same Number) while making the shader fail to compile:
|
||||
//
|
||||
// ERROR: '==' : wrong operand types - no operation '==' exists that takes a left-hand
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
// CPU meshing : `sdfEvaluator.js`'s BUILTINS table (the same ~60 helpers, hand-written a
|
||||
// second time in JS) + the RAW, un-normalized `model.glslSource`
|
||||
//
|
||||
// Neither input nor implementation is shared, and design/unified-glb-render-artifacts.md §7.2
|
||||
// promotes the CPU side from an export-only convenience to the geometry users actually see.
|
||||
// Neither input nor implementation is shared, and the CPU side is no longer an export-only
|
||||
// convenience -- it is the geometry users actually see.
|
||||
// So this module evaluates the GPU side's inputs -- the shader's own GLSL helper bodies, over
|
||||
// the shader's own normalized source -- and the test asserts the two agree numerically.
|
||||
//
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// Visual before/after baseline harness for renderer changes. Unit tests do not
|
||||
// exercise 3D rendering, so a risky render change is gated on screenshots:
|
||||
// capture a labeled baseline before the change, re-capture after, and eyeball
|
||||
// the diff. This drives an ALREADY-RUNNING dev viewer (start it via the
|
||||
// cad-viewer skill launcher) — it never starts or stops servers itself.
|
||||
//
|
||||
// For standing per-format and per-theme gates, prefer viewer/scripts/
|
||||
// e2e-format-sweep.mjs and e2e-theme-conformance.mjs; this one is for ad hoc
|
||||
// before/after comparisons.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/viewer/capture-render-baselines.mjs \
|
||||
// --base-url http://127.0.0.1:4178 --label pre-phase-1a \
|
||||
// [--models-dir <abs path>] [--fixture <relative-to-models path>]... \
|
||||
// [--theme light]... [--wait-ms 9000] [--out-dir tmp/render-baselines]
|
||||
//
|
||||
// Defaults capture one implicit and one mesh fixture in light + dark. The
|
||||
// mesh renderer has no preserveDrawingBuffer, so in-page canvas readback
|
||||
// reads blank — the composited page.screenshot below is the trustworthy
|
||||
// capture path.
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
|
||||
const DEFAULT_FIXTURES = [
|
||||
"implicits/parametric-pulse.implicit.js",
|
||||
"fun/miniature_spiral_staircase_highres.glb",
|
||||
];
|
||||
const DEFAULT_THEMES = ["light", "dark"];
|
||||
// Implicit models need shader compile + auto-fit before the first real frame
|
||||
// (~9s is the documented safe wait); meshes settle much faster but share the
|
||||
// same wait for simplicity unless overridden.
|
||||
const DEFAULT_WAIT_MS = 9000;
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
baseUrl: "",
|
||||
label: "baseline",
|
||||
modelsDir: path.join(repoRoot, "models"),
|
||||
fixtures: [],
|
||||
themes: [],
|
||||
waitMs: DEFAULT_WAIT_MS,
|
||||
outDir: path.join(repoRoot, "tmp", "render-baselines"),
|
||||
};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
const next = () => {
|
||||
index += 1;
|
||||
if (index >= argv.length) {
|
||||
throw new Error(`Missing value for ${arg}`);
|
||||
}
|
||||
return argv[index];
|
||||
};
|
||||
if (arg === "--base-url") options.baseUrl = next();
|
||||
else if (arg === "--label") options.label = next();
|
||||
else if (arg === "--models-dir") options.modelsDir = path.resolve(next());
|
||||
else if (arg === "--fixture") options.fixtures.push(next());
|
||||
else if (arg === "--theme") options.themes.push(next());
|
||||
else if (arg === "--wait-ms") options.waitMs = Number(next());
|
||||
else if (arg === "--out-dir") options.outDir = path.resolve(next());
|
||||
else throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
if (!options.baseUrl) {
|
||||
throw new Error(
|
||||
"--base-url is required (start the viewer with the cad-viewer skill launcher and pass the URL it prints)"
|
||||
);
|
||||
}
|
||||
if (!options.fixtures.length) options.fixtures = [...DEFAULT_FIXTURES];
|
||||
if (!options.themes.length) options.themes = [...DEFAULT_THEMES];
|
||||
if (!Number.isFinite(options.waitMs) || options.waitMs < 0) {
|
||||
throw new Error("--wait-ms must be a non-negative number");
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function resolvePlaywright() {
|
||||
// playwright is not a viewer dependency; it is vendored with implicitjs
|
||||
// (npm ci --prefix packages/implicitjs) for headless render tooling.
|
||||
const require = createRequire(import.meta.url);
|
||||
try {
|
||||
return require(path.join(repoRoot, "packages", "implicitjs", "node_modules", "playwright"));
|
||||
} catch {
|
||||
try {
|
||||
return require("playwright");
|
||||
} catch {
|
||||
throw new Error(
|
||||
"playwright not found. Run `npm ci --prefix packages/implicitjs` first (it vendors playwright)."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function safeName(value) {
|
||||
return value.replace(/[^a-zA-Z0-9._-]+/gu, "_");
|
||||
}
|
||||
|
||||
async function captureOne(browser, options, fixture, theme) {
|
||||
const url = new URL(options.baseUrl);
|
||||
url.searchParams.set("dir", options.modelsDir);
|
||||
url.searchParams.set("file", fixture);
|
||||
url.searchParams.set("theme", theme);
|
||||
|
||||
const page = await browser.newPage({ viewport: { width: 1280, height: 860 } });
|
||||
const consoleErrors = [];
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") {
|
||||
consoleErrors.push(message.text());
|
||||
}
|
||||
});
|
||||
try {
|
||||
await page.goto(url.toString(), { waitUntil: "domcontentloaded", timeout: 60000 });
|
||||
await page.waitForSelector("canvas", { timeout: 60000 });
|
||||
await page.waitForTimeout(options.waitMs);
|
||||
const outputPath = path.join(
|
||||
options.outDir,
|
||||
safeName(options.label),
|
||||
`${safeName(fixture)}__${safeName(theme)}.png`
|
||||
);
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||
await page.screenshot({ path: outputPath });
|
||||
return { outputPath, consoleErrors };
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const { chromium } = resolvePlaywright();
|
||||
const browser = await chromium.launch();
|
||||
const failures = [];
|
||||
try {
|
||||
for (const fixture of options.fixtures) {
|
||||
for (const theme of options.themes) {
|
||||
const target = `${fixture} [${theme}]`;
|
||||
try {
|
||||
const { outputPath, consoleErrors } = await captureOne(browser, options, fixture, theme);
|
||||
const errorNote = consoleErrors.length
|
||||
? ` (${consoleErrors.length} console error(s): ${consoleErrors[0]})`
|
||||
: "";
|
||||
process.stdout.write(`captured ${target} -> ${path.relative(repoRoot, outputPath)}${errorNote}\n`);
|
||||
if (consoleErrors.length) {
|
||||
failures.push(`${target}: console errors during render`);
|
||||
}
|
||||
} catch (error) {
|
||||
failures.push(`${target}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
process.stdout.write(`FAILED ${target}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
if (failures.length) {
|
||||
process.stderr.write(`\n${failures.length} capture(s) failed or logged console errors:\n`);
|
||||
for (const failure of failures) {
|
||||
process.stderr.write(` - ${failure}\n`);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -4,7 +4,7 @@ Mirrors `skills/cad/scripts/gen` point for point -- positional targets, `--force
|
||||
`--verbose`, one format-specific bake knob (`--resolution`), sequential per-target locks, a
|
||||
CliLogger on stderr and one self-erasing progress line -- because there is ONE producer
|
||||
(`cadgen.implicit_artifact`) behind both this and the viewer's POST, and the CLI's job is to
|
||||
present it, not to reimplement any part of it (design/unified-glb-render-artifacts.md §4.7).
|
||||
present it, not to reimplement any part of it.
|
||||
|
||||
Always builds the package. `--write` ALSO leaves the sibling `<name>.glb` beside the source
|
||||
(§0.1), in the export preset -- the package's own `model.glb` is quantized and
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
The build spans two runtimes -- Python runs the generator and holds the lock, a Node child
|
||||
bakes ``preview.glb`` inside it -- so the cross-runtime tests here spawn a REAL node child.
|
||||
They skip when node or the JS dependency graph is unavailable, per the plan's test split
|
||||
(design/unified-glb-render-artifacts.md §10.1): Python owns the package as a coordinated
|
||||
They skip when node or the JS dependency graph is unavailable. The test split: Python
|
||||
owns the package as a coordinated
|
||||
artifact, JS owns everything downstream of "here is geometry", and the geometry itself is
|
||||
pinned by ``packages/cadjs/src/lib/dxf/previewGlb.test.js``.
|
||||
"""
|
||||
|
||||
@@ -63,7 +63,7 @@ export function renderedFileSheetSectionIds(kind, options = {}) {
|
||||
// selectable in the viewport. The Tree panel is 556 lines inside StepFileSheet
|
||||
// reading 20 props and 33 derived locals; sharing it means extracting it, and a
|
||||
// second tree implementation for robots is exactly the parallel stack this effort
|
||||
// exists to remove. Tracked as R1b in design/viewer-robot-parity.md.
|
||||
// exists to remove.
|
||||
return [
|
||||
...status,
|
||||
...(isSdf ? [FILE_SHEET_SECTION_IDS.ROBOT_SDF] : []),
|
||||
|
||||
Reference in New Issue
Block a user