Every commit. No marketing spin.
Pull the OCCT mesh/edge readback (extractMeshData + face/edge helpers + mesh/edge constants) out of evaluator-worker.js into a new pure module, mesh-extract.js, so it's importable by node/vitest and by the future native path. Worker now delegates to it. Readback micro-opts: skip the per-node gp_Pnt.Transformed() when the face location is identity (the ~100% common case), and always release the Node(i) wrapper (previously leaked -> per-vertex GC churn). Preallocated typed-array combine. Add dev-only _meshTrace ring (meshMs/readbackMs/combineMs/nodes/tris), exposed to the jc3-debug bridge as `meshTrace`, plus a golden-mesh vitest (box: 24 nodes/12 tris/bbox/ signed-volume winding; cylinder classification). Mesh verified byte-identical. Measurement finding (see local plans/occt-performance.md): native OCCT meshing dominates readback ~4.6:1 and is the hard ceiling; real parts are face-count-bound not node-bound, so readback micro-opts and deflection tuning are in the noise on prismatic parts. The real lever is parallel native meshing in a self-built OCCT WASM (scoped separately). Phase 1 ships for its leak fix + curved-part value + carry-forward to the native readback contract. Docs: evaluator.md documents the module split + perf finding. CLAUDE.md: make MCP-first (mcp__jc3-debug__*) a CRITICAL rule, CLI is fallback-only.
Modeled threads sat at "Computing…" forever: the old path fit the helix spine with GeomAPI_PointsToBSpline (wavy, inexact) and swept a sharp-V triangle, then booleaned that against the exact shaft — OCCT's boolean never converged, and since OCCT runs synchronously it wedged the whole worker. - Exact-geometry builder (FreeCAD parity) in new Drafting/thread-builder.js: buildExactHelixWire (edge on Geom_CylindricalSurface / Geom_ConicalSurface), buildISOThreadProfile (truncated ISO trapezoid, optional rounded crest), makeThreadSolid (PipeShell -> Simulate caps -> Sew -> MakeSolid -> orient). case 'thread' builds at origin, places onto the axis via gp_Trsf, one boolean. - Guards: pitch/turns clamp, degenerate bailouts throw -> surface a feature error. - Worker watchdog (evaluator.js): per-request timeout that terminates + respawns a wedged worker and rejects the request, so synchronous OCCT can never hang the UI. - Helix feature ported to the same exact spine (parity-verified, with fallback). - NPT tapered pipe threads (conical helix) + rounded-crest option + tool UI/data. - Fixes from live testing: Thread tool read the removed face.positions/indices, so hole depth defaulted to 1" (threads hung out the bottom) and internal-detection defaulted true — now read threeGeom position/normal. Internal threads now CUT the bore wall instead of fusing a floating ridge. - Tests: Drafting/thread-builder.test.js (10) — valid/closed/oriented solids, rounded/tapered, exact-helix parity, boolean terminates, internal cut removes material. Docs: docs/Drafting/threads.md. Release notes: v2.3.6. Note: internal-thread visual correctness is still WIP (tracked for follow-up).
Right-click a Sheet Metal Base → Flat Pattern → name the sketch, and the body (base + all flanges/bends) is developed into a static sketch: outline + holes on the default layer, fold centrelines on a "Bend Lines" layer. Implements unfoldShape() — the flatten/2D-extraction stage that was stubbed: rigidly places each reachable flat region into the root plane, replaces each bend with a bend-allowance-wide strip (inner radius from the coaxial cylinder pair, K-factor from the base feature), and extracts the outline (edges on exactly one skin face) + connecting strip sides + one bend line per fold. Also fixes findBends so a bend cylinder only binds to skin faces (normal ⟂ axis) — through-thickness end-caps and drilled-hole bores no longer bridge the two skins or read as bends. Wiring: 'unfold-body' worker RPC (evaluator-worker) + unfoldBody() + response dispatch (evaluator), Flat Pattern context-menu item (browser-panel), and _createFlatPattern() (Drafting/main) — prompts, runs the unfold, builds the sketch. unfold.test.js gains flat-pattern coverage (bracket develop length, two-bend U-channel, hole preservation, flat plate); docs updated.
Feature-edit parity: SheetMetalBase, Flange, and Revolve gain the standard
edit flow (_pendingEditSetup → _setupForEdit → in-place commit with cutoff),
joining the tools that already had it. The type→tool map moves to a shared
feature-edit-registry.js consulted by both onEditFeature and the browser
panel, which now hides Edit on unsupported feature types. Flange restores
its dialog via a pure flangeStateFromParams(); Base edits preserve the
feature id so body lineage and flange targets survive, and thickness edits
propagate to every bend; Revolve edits reuse stored profile/axis params
verbatim ("frozen" mode) so a hidden/deleted sketch can't block the edit.
Sheet metal interop: fix FlangeTool._context falling back to doc.root
(wrong default thickness for sheet bodies in nested components); add a
through-containment Cut inference (infer-op.js) so extrude/revolve
through-cuts on thin stock stop auto-suggesting Join; Revolve's
intersection scan now skips stale/hidden bodies like Extrude's.
Collateral: deleting a root feature with dependent features now confirms
and cascade-deletes them instead of leaving broken features; clearer error
when a target body's base feature is disabled.
Tests: context resolution invariance, flange params round-trip, infer-op
spans, and OCCT mappedName survival through base→cut→flange chains.
Docs: sheet-metal.md editing/interop sections, evaluator.md edit-cutoff
contract, release notes.The previous miter cut each wall at the corner's bisector plane, which bevels the edge THROUGH the sheet thickness — an edge a flat-cut-then-bent part cannot have — and welded/interfered geometry that no single sheet produces. New construction, faithful to the flat pattern: - Only the FLAT-WALL sub-prism extends past a shared corner; the bend zone and overlap end square at the edge span (a bend line only spans its physical edge), leaving the real corner notch between the two bend zones. - The miter cut plane CONTAINS the wall's own thickness direction (square edge) and follows the straight flat-pattern line where this wall's slab would first touch the partner wall's INNER skin, minus the gap: s(w) = −u_inner(w)·cot(Φ/2) + gap/(2·sin(Φ/2)) — linear in wall depth, so the locus is a plane spanned by t̂ and ŵ − (du_i/dw)·cot(Φ/2)·d̂. At 90°/90° it degenerates to the classic square wall end just short of the partner's inner face; at non-90° bends the two edges track each other down the lean and meet as closely as square edges physically can. - Provably zero wall↔wall interpenetration outside the sheet (each wall stays entirely out of the other's slab). The harness test's airCommon check shows un-mitered obtuse walls DO collide — the honest reason the cut exists — while mitered walls never do, at 90° or 120°. - _buildWallMiterCut replaces _buildMiterHalfSpace; miterExtension is no longer used by the worker (kept in flange-corners.js for its tests); keep-lines are emitted at the base-span bend caps for all ends again. Tests: square-through-thickness probes at both skins (the old bisector cut provably fails them), depth-independent 90° end, span-limited bend zone, airCommon interpenetration checks at 90°/120°, corner relief unchanged. 94/94 sheet-metal vitest, production build clean.
The miter was cutting both walls exactly AT the bisector plane, so they met coincident and the fuse joined them into one welded corner — zero clearance, which a single bent sheet can't produce and Unfold can't separate. Each wall's miter cut plane is now offset by half a clearance gap along the bisector normal, so the two cut faces run parallel with a uniform air gap between them. Because both planes are offsets of the same bisector, the clearance holds across every Bend Position, bend angle, and Height Datum. - evaluator-worker.js: _buildMiterHalfSpace takes gapHalf and shifts the cut plane by keep·gapHalf·m; the flange case resolves miterGap (param, null → 0.25·thickness — the usual press-brake gap-ratio rule) and threads it into each mitered end. - Flange tool: "Miter gap" input shown while Miter corners is checked (empty → the 0.25·t default). Param: miterGap (null → default). - flange-miter.test.js: harness cuts now model the offset planes; new clearance assertions — a probe ON the bisector is air at 90° and 120° while symmetric probes just outside the gap band are material (no V-gap, no weld), single valid solid throughout. 91/91 sheet-metal vitest, production build clean.
Adds the togglable arc relief at flange corners:
- Flange tool: "Corner relief" checkbox (default OFF, opt-in per feature) +
"Relief radius" input shown when enabled (empty → the feature's bend
radius). Params: cornerRelief, cornerReliefRadius (null → bend radius).
- flange-corners.js: cornerReliefCenter() — the relief axis point is the
intersection of the two walls' bend-START tangent lines (the corner point
itself at the Adjacent position; shifted bend positions move it inboard
with the bend), solved in the sheet plane.
- evaluator-worker.js: corners are now detected independent of the miter
toggle; per corner a cylinder along the sheet normal (through the full
sheet + bend + wall extent) is cut AFTER the wall fuses, before refine,
via _cutShapesEM's new opCode parameter — putting the long-reserved
OP.RELIEF ('RLF') element-map code to use. The plain-boolean fallback
applies the same cuts.
- Tests: cornerReliefCenter at Adjacent and shifted positions (pure);
node-harness relief on a mitered 90° two-wall corner — material at the
corner removed, walls elsewhere intact, single valid solid, and a
cylindrical face on the sheet-normal axis exists.
91/91 sheet-metal vitest, production build clean.Where two walls of one Flange share a reference-edge endpoint, each is now internally extended past the corner and cut back at the corner's bisector plane, so the walls meet exactly — at ANY bend angle and any corner angle. Previously the corner column was simply left open, and non-90° walls could never meet at all. - flange-corners.js (new, pure): findSharedCorners() — endpoint-coincidence corner detection with the bisector normal m = normalize(dA − dB) (contains the sheet normal by construction; m points to wall A's keep side); collinear continuations and >2-wall pairings are skipped. miterExtension() sizes the internal extension (outboard reach ÷ tan(φ/2), floored against near-collinear blowup; inboard-curling obtuse walls need only the margin — the cut eats inward instead). - evaluator-worker.js: the shared cross-section profile is now built once per feature (it is identical for every wall) with its max outboard reach; a mitered end replaces that end's user gap/extend with the miter extension and cuts the wall with a bisector half-space box (_buildMiterHalfSpace). The two walls meet exactly ON the plane, so the interface dissolves in the body fuse — mitered ends therefore emit no tangent keep-lines (their caps are gone). miterCorners defaults ON, including for old docs (absent → on): at 90° the miter strictly improves the old open-notch butt, everywhere else the old behavior was an unfillable gap; OFF reproduces the plain full-span square-ended walls (wanted when bending opposite directions off a shared corner). - Flange tool: "Miter corners" checkbox (default checked). - Tests: flange-corners.test.js (pairings incl. origin/far-end, collinear skip, 60° cosine, extension sizing); flange-miter.test.js node harness — 90°: miter fills the previously-open corner column (classifier probes both sides of the plane), single valid solid; 120°: walls meet at the bisector with no V-gap where un-mitered walls provably leave it open. 87/87 sheet-metal vitest, production build clean.
Fixes the documented known limitation (and the user-reported missing face split at the bend radius endpoints): a wall's side end-cap was a single prism face bounded by both bend arcs, so there was no flat sub-face to select for a flange folded around an already-bent corner. - flange-profile.js: buildFlangeProfile() additionally returns `sections` (the same profile as three closed loops — sheet-overlap / bend sector / straight wall — sharing the two bend-tangent segments) and `tangents`. - evaluator-worker.js: _buildFlangePrism builds the three sub-prisms and fuses them into one wall solid — the fuse dissolves the internal shared faces but leaves the side caps pre-split at the tangent lines; the bend faces remain the same true cylinders so Unfold detection is unaffected. The four tangent segments are lifted to 3D keep-lines, and the new _keepTangentSeams KeepShape()s any refine edge lying on one (endpoint + midpoint on-segment match, robust to the body fuse splitting an edge) — otherwise the ∥-sheet heal / band↔band sliver rules merge exactly the imprinted faces (they demonstrably do for r < ¾t). The band↔overlap-cap seam is intentionally not kept, so the fuse-overlap cap still heals into the parent perimeter face. - flange-frame.js: upHint sign flip now requires a clearly anti-parallel hint (dot < −0.1) — a near-perpendicular hint (picking a face on an already-folded wall) no longer flips the fold direction on tessellation noise. - Tests: profile section closure/shared-tangent/area-tiling; node-harness imprint coverage in flange-refine.test.js (exactly 3 cap-plane faces incl. the t×W wall face, ∥-sheet heals stay, small-radius dissolve documented without keep-lines, single valid solid); frame tests for the inclined 135° wall cap rectangle and the noise no-flip. 74/74 sheet-metal vitest, production build clean.
Replace the FreeCAD-style lengthSpec with Fusion 360's Height Datum — the flange Height is measured to Inner Faces (W = H − r·tan(θ/2)), Outer Faces (W = H − (r+t)·tan(θ/2)), or Tangent To Bend (W = H − (r+t), θ-independent): - flange-profile.js: wallLengthForDatum() replaces wallLengthForSpec(); a height the bend fully consumes (W ≤ 0) now throws a clear feature error instead of silently building a degenerate zero-length wall. - flange-params.js: HEIGHT_DATUMS enums + normalizeHeightDatum(); legacy lengthSpec maps at evaluate time (outer-sharp→outer, inner-sharp→inner — identical formulas — and leg/tangential→ evaluate-only 'leg' where H is the flat length itself), so saved docs keep their exact geometry. - Flange tool: "Height Datum" combo (Inner Faces / Outer Faces / Tangent To Bend); the length input is now labeled "Height". - Tests: modern↔legacy numeric equivalence, tangent θ-independence and tangent≡outer at 90°, too-small-height throw; node-harness extent checks (tangent datum at 90° drops exactly H below the sheet top; legacy leg pin). 65/65 sheet-metal vitest, production build clean.
Replace the FreeCAD-style bendType enum (only Material Outside worked) with Fusion 360's Bend Position — Adjacent / Inside / Outside / Tangent: - flange-params.js (new): canonical enums + evaluate-time normalization of legacy bendType/offset params, so saved docs load unchanged (material-outside→adjacent is geometry-identical; the other three were broken and now produce corrected geometry). - flange-profile.js: bendPositionShift() — adjacent 0, inside −(r+t)·tan(θ/2), outside −r·tan(θ/2), tangent −(r+t); Inside/Outside reject angles past 174° (setback → ∞); radius must be > 0; profile now returns uShift. - evaluator-worker.js: positions that shift the bend into the sheet now CUT a rectangular trim prism out of the parent (per wall, over the wall's span, overshooting both sheet faces) BEFORE any wall fuse — a fuse alone left the parent's square edge filling the inside of the bend, which is why every non-default bend type produced junk. Trims thread the element map via the existing OP.CUT path; the plain-boolean fallback applies them too. - Flange tool: "Bend position" combo (Adjacent/Inside/Outside/Tangent); the never-working Offset mode and its input are gone. - Tests: bendPositionShift formulas + legacy mapping (flange-profile.test.js); new flange-position.test.js builds all four positions as REAL solids in the OCCT node harness at 90° and 135° and probes wall placement with a solid classifier, including the trim-cut regression (the old fuse-only pipeline demonstrably leaves parent material inside the bend). Verified: 60/60 sheet-metal vitest, production build clean. Live visual A/B against Fusion pending a dev-server restart (Vite's ?worker_file transform is cached while JC3_NO_HMR=1, so the running app still executes the old worker).
The previous keep-seam heuristic (short + normal ⟂ sheet) couldn't tell the genuine sheet-perimeter↔wall-end-cap seam from the phantom sheet-perimeter↔bend-tangent-sliver seam — both are short and ⟂ the sheet — so it kept both, leaving a ~0.015" sliver that splits the face you click for the next flange. Replace the length+orientation test with the real distinguisher: each face's extent along the sheet normal. Keep a coplanar side seam only when the two faces' sheet-normal extents differ by > 3/4 thickness (thin sheet-band ↔ tall wall = real end-cap → keep, box stays finishable). Equal extents (band ↔ band) is the phantom sliver → heal. ∥-sheet strips still heal. Verified live via the jc3-debug worker eval (sliver = face area 0.0009) and reproduced in the node OCCT harness (raw fuse yields [0.0009, 0.2391] at min-X; fix heals to a single 0.24 face). flange-refine.test.js gains sliver coverage. Documents issue #2 (no tangent edge to fold a flange around an already-bent corner) as a known limitation needing tangent-line imprinting.
_keepPerimeterSeams protects short coplanar side seams (perimeter/end-cap joins, ⟂ the sheet normal) from UnifySameDomain before Build(), so folding a wall off one edge no longer dissolves the neighbouring perimeter faces a later flange needs. Bend-tangent/flat-split seams (∥ the sheet) still heal. Adds dev:stable (JC3_NO_HMR=1) so agents can edit source mid-session without HMR wiping live document state, plus flange-refine regression test and docs.
Flange now accepts multiple thin edge faces in one feature (params.walls[]), building a wall per face and fusing them into the body. A removable Wall list (click ✕, click a selected face again, or Clear faces) lets a mis-pick be undone before commit. Every wall's fold-direction axis (uy) is oriented to the body's sheet normal so all walls fold the same way. Booleans are refined with ShapeUpgrade_UnifySameDomain (_refineShapeEM, OP.REFINE) so flanges no longer leave phantom seam lines near the bend or across the parent face; results come back as clean single solids. Corners close by wall overlap + fuse (butt joint). Angled miters / clearance gap / round relief were prototyped and reverted pending a stable approach.
Adds a Sheet Metal toolset to the Drafting workspace (new toolbar group): - Sheet Metal Base: thicken a sketch into a sheet body carrying material + thickness + default bend radius + K-factor mode (single source of truth for all downstream bends; injected into feature params at evaluate time). - Flange / Add Wall: pick the thin perimeter FACE (only faces highlight) and grow a wall with a real radiused bend. Frame derived from the face via a minimum-area bounding box (robust to tessellation). Full param model: bend type, length spec, angle, invert, gaps, extends. - Evaluator: new sheet-metal-base + flange cases; flange registered as a direct modifier so it fuses into the body and threads the element map (OP.FLANGE). - K-factor engine: material presets + piecewise-interpolated bend allowance BA = (r + K*t)*angle, ANSI/DIN, engineering mode. - Unfold foundation (not yet user-facing): face classification, coplanar region grouping, tangent bend graph, spanning-tree plan. Flatten stage next. Fully unit-tested (kfactor, flange profile, flange->OCCT solid, face frame, unfold graph) plus contract tests; production build clean. Proven live: a 90-degree flange yields a valid solid with real cylindrical bend faces.
Refactor the shared Toolbar (src/renderer/src/toolbar.js) so all five CAD/CAM workspaces (Design, Drafting, Plasma, Laser, Router) plus the Drafting Sketch sub-toolbar inherit three capabilities at once: - Groups: bucket the left row by each descriptor's `group` into delimited `.toolbar-group` clusters with optional labels/order via a new `opts.groups`. - Multi-line wrap: opt-in `toolbar-wrap` mode (gated on `opts.persistKey`) so the tool row wraps onto extra rows instead of cutting tools off; groups never split mid-cluster. Right-slot overflow popover is untouched. - Drag-reorder: a four-dot handle per group (native HTML5 DnD) reorders groups; order persists per workspace via new toolbar-prefs.js (localStorage, frozen key registry, normalized reads) under jetcad3.toolbar.<workspace>.groupOrder. persistKey is an explicit literal per workspace, never workspaceName (which mangles under RELEASE_BUILD minification). Adds toolbar-prefs.test.js, docs/toolbar.md (linked from docs/README.md), and a release-notes entry.
The Extrude tool inferred Cut vs Join from axis-aligned bounding-box volume overlap, so a small outward extrude whose bbox landed inside a large/concave body's bbox falsely read as Cut — even when dragging away from material into empty space. Replace the bbox heuristic with the FreeCAD-parity material signal: the support face's orientation-aware outward normal. A new faceOutwardNormal() in plane-attach.js classifies a point +/-1e-4 off the face plane against the owning solid via BRepClass3d_SolidClassifier and returns the normal flipped to point away from material (orientation- agnostic, correct for concave bodies). resolvePlaneFrame returns it as frame.outwardNormal; the evaluator stores plane._outwardNormal; getPlaneById passes it through. Extrude's new _inferOperation dots the drag vector against it: into material -> Cut, away -> Join, with the bbox heuristic kept only as a fallback when no outward normal is available. Tests: 4 new faceOutwardNormal cases in plane-attach.test.js (top/bottom face, sign-flip robustness, through-plane -> null). Docs updated.
Sketching on a face produced by a downstream feature (e.g. a boss added by a joined union extrude) detached the plane on Finish — it snapped onto a same-normal base face. Both creation sites anchored the plane to the body's _sourceFeatureId (its create feature), so resolvePlaneFrame ran against a snapshot lacking the picked face; the mapped-name match missed and the geometric fallback landed on the wrong coplanar face. Anchor instead to the body's tip feature at creation (bodyTipFeatureId): the last enabled feature contributing to the body, which owns the picked face and is always upstream of any sketch/extrude later drawn on the plane — so no plane<->sketch<->extrude feedback loop returns. - plane-attach.js: add pure bodyTipFeatureId(component, bodyId). - Sketch/Plane tools: anchor _faceRef.featureId to the tip; Plane tool now also stores the face mappedName for the primary (mapped) resolve. - evaluator-worker.js: snapshot each union/cut extrude step when a plane anchors to it (was only fillet/chamfer/transform/thread) — otherwise the plane resolved to 'not-evaluated' and froze. - Tests: bodyTipFeatureId cases + end-to-end fused-boss face resolution. - docs/Drafting/face-attached-planes.md updated.
The agent's per-project auto-memory (fact-per-file .md notes + MEMORY.md index) is paramount to work here and should travel with the repo instead of living only in machine-local ~/.claude state. - Move the 33 memory files into tracked .claude/memory/. The live path ~/.claude/projects/<slug>/memory is now a symlink to it, so Claude keeps reading/writing there while the real files live in git. - scripts/link-claude-memory.sh recreates that symlink on any machine/clone (idempotent; computes the machine-specific slug from the repo path; imports and backs up any pre-existing memory before linking). - Gitignore machine-local Claude state (settings.local.json, scheduled_tasks.lock, *.bak.*). - docs/claude-memory.md + docs index entry explain the setup.
Add a `screenshot` command to the jc3dbg CLI + jc3-debug MCP server so an
agent can see the running dev renderer (not just eval numeric state).
- cdp.mjs: screenshot() core via CDP Page.captureScreenshot; full-frame by
default, optional clip {x,y,w,h} + scale to zoom into a region.
- CLI: `screenshot [--out P] [--clip x,y,w,h] [--scale N] [--format png|jpeg]`
writes a PNG (default temp file) and prints its path to Read back.
- MCP: `screenshot` tool returns an inline image content block so the capture
renders directly to the agent.
- Docs + CLAUDE.md updated.A construction plane placed on a body face, with a joined extrude built on it, entered an infinite plane->sketch->extrude jitter: _reparameterizePlanes re-derived the plane frame from the FINAL fused body's trimmed top-face mesh, but that face is reshaped by the very extrude drawn on the plane (which takes its position from the plane corners). Negative feedback with gain > 1 never settled inside the EPS guard. Adopt FreeCAD's attachment convention: a face-attached plane's placement is a pure function of its SOURCE feature's own output shape, read from the face's BRep Geom_Plane (trimming-independent), never the final tip or the mesh. That makes the dependency graph acyclic, so downstream features can't perturb the plane. - New plane-attach.js (pure, oc-injected, unit-tested): facePlaneFrame, frameToCorners (preserves authored ux/uy + d so no sketch shifts; re-aligns a sign-flipped BRep normal so sketches never mirror), resolvePlaneFrame (mapped-name match first, BRep normal+distance fallback). - Worker: snapshot per-source-feature shapes/maps before downstream features overwrite them (cached alongside _cacheMap for the coarse->fine fast path); return planePlacements on the result message. - Renderer: _send ships planes on full/non-cutoff/non-lightweight passes; _applyPlanePlacements replaces the mesh-based _reparameterizePlanes. - Converges in <=1 extra eval; editing a downstream consumer never moves the plane. - plane-attach.test.js: 9 tests incl. the invariance-under-fuse guarantee. - docs/Drafting/face-attached-planes.md + README + element-mapping cross-ref; release notes v2.3.5.
Dev-only CDP bridge so an agent can read/eval the running `npm run dev` app — including inside the OCCT WASM worker pool — instead of the log/teardown/repeat loop for 3D and feature-history bugs. Stripped from release builds. - Main: open CDP port 9222 on 127.0.0.1 when !app.isPackaged. - Worker: dev-only __jc3dbg-eval handler (oc + live shape caches + summarize/ explode/checkValid helpers in scope) plus occtTrace/featureTrace ring buffers capturing in/out of fuse/cut/fillet/chamfer/thread and every _evalFeature. - Evaluator: debugEvalAll() broadcasts eval to all pool workers, returns a per-worker result array (component->worker affinity). - Renderer: window.__jc3debug (status/listBodies/activeComponent/dumpShape/ occtLog/featureIO/evalInWorker), installed under import.meta.env.DEV. - Tooling: scripts/jc3dbg.mjs CLI + scripts/jc3dbg/cdp.mjs core + MCP server (mcp__jc3-debug__*). Renderer-context conditional breakpoints (`break`). - Docs + agent guidance (docs/live-debug-bridge.md, CLAUDE.md section). - Add chrome-remote-interface + @modelcontextprotocol/sdk as devDependencies.
Adds a FreeCAD-style persistent-naming subsystem for the OCCT evaluator so face/edge selections carry a stable, history-derived name instead of being re-matched by geometry each rebuild. New: - element-map.js — ElementMap, enumerateSubshapes (HashCode+IsSame; OCCT's IndexedMapOfShape isn't bound in opencascade.js), seedNames, mapHistory (Generated/Modified/IsDeleted + IsSame passthrough for unchanged subshapes), resolveMapped. Pure module (takes injected oc) → unit-testable. - occt-node-harness.js — load OCCT WASM under node/vitest. - element-map.test.js — 11 tests incl. coplanar disambiguation + topology-change robustness, run against real OCCT. Evaluator worker: - Element maps threaded through the rebuild pass loop and cached alongside shapes (_cacheEMMap, _evalCache[].em). Booleans (_fuseShapesEM/_cutShapesEM) and fillet/chamfer derive names from the OCCT builder history; base bodies fall back to deterministic index seeding. mappedName shipped on faces + per-face edges. - Phase 0a: thread tool now booleans the groove into the body (cut external / fuse internal) instead of the debug `return grooves` leak. - Phase 0b: _buildFace/_buildWireFromChain heal via ShapeFix_Wire and throw descriptive errors instead of returning null silently. Main thread + tools: - Face.mappedName; _reparameterizePlanes resolves by mapped name first (falls back to the legacy normal/distance search); Sketch _faceRef, Fillet/Chamfer edge refs store mappedName. Persisted via existing JSON (Option B — maps regenerate on load). All element-map paths are guarded (fall back to seeding / legacy matching), so geometry is never affected. Docs: docs/Drafting/element-mapping.md. KNOWN ISSUE (WIP): sketch-on-face still exhibits a plane feedback loop (planesChanged → markDirty → re-eval) on a body whose face is modified by the sketch's own downstream feature — suspected causes: (1) _reparameterizePlanes rebuilds the plane frame from the tracked face's mesh centroid/bbox, which drifts when that face's boundary depends on the plane; (2) _bodyEM seeds every body with a fixed op/tag, so seed names collide across bodies after a fuse. To be diagnosed with the incoming CDP live-debugging tool before fixing.
Undo/redo snapshots are captured synchronously on markDirty, but constraint applies solve asynchronously afterward, so a redo restored the constraint entity paired with pre-solve geometry (e.g. "equal" circles stayed different sizes) and nothing re-solved it. Sketch faces went stale the same way. - refreshAfterUndo now runs a full solveConstraints on the restored sketch so geometry satisfies its restored constraints (a consistent snapshot solves to itself; only violated constraints move) and the DOF readout refreshes. - It also calls _doLiveFaceRebuild so sketch.faces are recomputed — the live-face dirty listener is keyed on the old _selectedComponent at undo time and skipped the restored sketch, leaving stale face regions until the next edit. - runConstraintToggle now markDirty AFTER the solve so the undo snapshot captures solved geometry and dependent 3D features rebuild from the solved sketch. Release notes: v2.3.5.
Add a shared geometric-constraint engine (constraint-actions.js) driving three surfaces that stay in sync: - On-bar toggle group on the Sketch edit sub-tool bar showing only the constraints applicable to the current selection; pressed = already applied, click to remove (constraint-bar.js). - Shift-modified hotkeys for every constraint, registered in HOTKEY_DEFS and user-editable under Settings -> Keyboard (picking.js _tryConstraintHotkey). - Right-click Constrain menu rebuilt on the same engine: every relationship is now a true toggle with a check, incl. previously-hidden collinear / concentric center-align / fix-pin (constrain.js). Also add a selection-change notifier at the interactive selection choke points (click / box / chain / context-menu select) so the bar refreshes live. Icons: crisp line-art SVG set (parallel is two angled lines, distinct from equal). The same icons render on the toolbar buttons and the in-scene badges; dim-renderer draws a badge beside each participating entity's midpoint and uses the padlock icon for pinned geometry. Docs: docs/Drafting/sketch-constraints.md (+ README link). Tests: constraint-actions.test.js (19). Release notes: v2.3.5.
The previous fix only hooked Workspace.setActiveTool/deactivateActiveTool, but Sketch sub-tools (Line, Arc, Fillet, Rect, …) self-deactivate through the base Tool._finishSelf() and never touch the workspace method — so pressing Escape in a sub-tool still left the badge on screen until the next mousemove. Call refreshCursorToolBadge() from _finishSelf() (where the active sub-tool ref is cleared) so it clears immediately there too.
The cursor tool badge only re-resolved the active tool on mousemove, so pressing Escape to deactivate a tool left the badge on screen (with a stale icon) until the next cursor movement. Add CursorToolBadge.refresh() (exported as refreshCursorToolBadge) which re-resolves at once — hiding when no eligible tool is active, or re-showing at the last known cursor position otherwise — and call it from Workspace.setActiveTool / deactivateActiveTool so the badge tracks tool state instantly instead of lagging a mouse move.
Slot commits now wire the geometry that makes a slot rigid so a single driven dimension resizes it without distortion: equal-radius across the two caps and tangency between each cap and every side it meets. Applies to all modes (straight/curved/rail) via a `role` tag on slot-geometry primitives; mouse commits get constraints only, Tab commits keep the driven width/length dims. Inter-entity constraints are now right-click-deletable: glyphs carry their constraint id + 2D position, DimRenderer.hitTestConstraint picks them, and onContextMenu opens a Delete menu that splices the constraint and re-solves measure-only (frees DOF without moving geometry). This is the first direct way to remove a tangent — previously they could only be dropped by deleting an entity. Tangent glyphs now anchor at the true tangent point (the coincident rim junction between the two entities via new sharedEndpoint helper) instead of entity1's midpoint.
Adds a global cursor-following overlay that shows the active tool's toolbar icon as bare, semi-transparent SVG vectors up-and-right of the mouse, across all workspaces (Drafting main tools + Sketch sub-tools, Plasma, Laser, Router). - New utils/cursor-tool-badge.js singleton: polls getActiveTool() on mousemove over #viewport-wrap, falls through to the Sketch parent's _activeSubTool, and hides for the Select/Sketch mode tools. - Settings → Mouse gains a "Show active tool badge at cursor" toggle (settings.mouse.toolBadge, default on) using the shared makeToggle switch; the Mouse tab's other checkboxes were also converted to toggles. - main.js wires setEnabled() on startup and settings.onChanged. - Docs (workspace-system, mouse-navigation) and v2.3.5 release notes updated.
The True-Shape frame tessellated each contour's arcs into many tiny G53 G0 rapids; the short segments outran the FluidNC lookahead planner (jerky/ overshooting motion). G53 was the only reason arcs had to be linearized — G53 is illegal with G2/G3 (GRBL/FluidNC: "G53 only works with G0 and G1"). Since framing runs against a loaded program whose work zero is already set, drop G53 entirely and trace in the ACTIVE work coordinate system with plain G90 moves — straight segments as G1, arcs as native G2/G3. buildFrameShapes now returns per-shape bulge polylines instead of tessellated points (bulge kept; loopPoints/tessellateArc removed); the emitter converts bulge to I/J via bulgeToArc (reused from common/geometry/primitives.js). Moves run at the machine's max feed (_maxFeedrate); the operator throttles live with the FEED override (continuous, unlike G0 rapid-override's 25/50/100 steps). The bbox fallback is converted back to the work frame from getCutBoundsXY. Guard: refuse to frame if the active WCS isn't the program's (would trace off-part). Arc math verified against the circle fixture — emitted I/J round-trip exactly to the source G-code. frame-shapes.test.js updated for the bulge-polyline return shape.