Skip to content

release: promote v0.18.0 to main - #419

Merged
sobechestnut-dev merged 10 commits into
mainfrom
dev
Aug 27, 2026
Merged

release: promote v0.18.0 to main#419
sobechestnut-dev merged 10 commits into
mainfrom
dev

Conversation

@sobechestnut-dev

Copy link
Copy Markdown
Contributor

Promote v0.18.0 → main

Cuts v0.18.0 (14 lockstep @chestnutlabs/* packages). Bundles two completed workstreams:

Version PR #412 merged (c5d0bd4); release notes folded into docs/README.md current-state + history (07fde26); docs:release-check PASS.

Public-docs completion check (CLAUDE.md)

  • Root/docs README updated for the new user-visible capabilities (current-state narrative + history rewritten for v0.18.0).
  • Package-level guidance: new APIs documented in changesets/CHANGELOGs; DD-017 & DD-030 design docs on record.
  • No existing screenshot misrepresents the software — the new capabilities (capture/per-plate/non-rect bed/RS274NGC) are net-new; no prior screenshot shows them.
  • Quick-start / example code still shows the recommended API (all changes additive; no API removed or changed).

Merge-commit (not squash), per the release process; then gh release create v0.18.0 --target main publishes.

sobechestnut-dev and others added 10 commits August 27, 2026 09:00
…phase 7) (#410)

New pure `rs274.ts` interpreter for the RS274NGC programming layer: numbered/
named/global parameters (#100, #<name>, #<_global>), assignment (#n = <expr>),
and bracket expressions ([...]) resolved wherever a numeric word value is
expected (X[#1+2], Z#<depth>). Previously dropped → parametric programs rendered
honest-but-empty; they now resolve to real geometry.

Recursive-descent evaluator: RS274NGC operator/function set with correct
precedence (** > */MOD > +- > compare > logical), degree trig, LinuxCNC MOD/EQ
semantics, FIX/FUP/ROUND, indirect ##n and computed #[expr], read-only
system-param allow-list (#5420-#5422 = position). Bounded + safe: a depth guard
on ALL recursive paths (brackets, unary chains, indirect refs), no eval/Function,
capped stores — a hostile program wastes only bounded CPU. Malformed/non-finite
values drop the word with a specific disclosure, never throwing out of the parse.

Additive: a `parametricProgram` capability (known/unavailable). Engaged only on
lines using #/[ via a per-line scan — every FDM/simple-CNC line takes the
untouched lexer, byte-identical (native goldens regenerated for the additive key
only; ~2.7% parse-gate overhead measured on 3DBenchy, geometry unchanged).

Scope: Phase 1 only. O-word control flow (if/while/sub/call) is a later phase.
Reviewed (sub-agent) — fixed a stack-overflow DoS on unbounded recursion, a
computed-NaN/uninitialized ambiguity, and silent non-finite drops before landing.

Co-authored-by: Nathaniel Chestnut <sobechestnut-dev@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…e families (scope only) (#411)

Scopes (does NOT build) a future "provenance detection" epic: moving dialect
detection from single-winner-per-kind to a multi-candidate, confidence-ranked
model with an explicit ambiguity signal, validated against a labelled provenance
corpus of the derivative slicer families (Orca/Bambu/Anycubic/QIDI lineage,
Prusa/Super lineage) + the orthogonal controller axis. Recommendation: keep
scoped until the corpus has real coverage and a concrete consumer need appears;
reject speculative build + premature public candidates[] API. Non-G-code formats
(Heidenhain .h, Siemens .mpf) out of scope.

Co-authored-by: Nathaniel Chestnut <sobechestnut-dev@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…e 7) (#413)

Execute the RS274NGC O-word control structures CAM/LinuxCNC programs use to
generate geometry: if/elseif/else/endif, while/endwhile, do/while,
repeat/endrepeat, break/continue. A bolt circle written as a while loop, a
pocket cut by a repeat, an if that selects a path — these previously ran once
(or not at all) on the forward-only engine; they now resolve to the full,
correct toolpath.

New module rs274-flow.ts is the program-buffered interpreter of DD-017 D1:
engaged only when programUsesOWords(text) (a cheap char-scan; FDM never enters
it, byte-identical), it buffers the program, builds a block tree, and executes
it — re-feeding each plain line through the same processLine (one geometry
path) and evaluating conditions/counts against the shared Phase-1 parameter
store (Rs274Context.evalExpression).

Bounded and safe (this is now a small interpreter): new public
maxProgramIterations limit (default 1_000_000) bounds TOTAL loop work — charged
per loop pass AND per statement executed inside a loop body, so a large body
cannot multiply work past the cap — stopping a runaway `o while [1]` with a
partial IR and a rs274-iteration-limit disclosure. Structural nesting is capped;
loops are iteration not recursion, so only bounded JS stack is used. No eval,
no I/O.

Honest and additive: clean flow keeps parametricProgram 'known'; a degraded run
(hit cap / unbalanced / unsupported / malformed condition / geometry-limit
truncation) reports 'approximated' with a specific disclosure. Subroutines
(sub/call/return) are Phase 3 — disclosed unsupported and NOT executed inline.
Non-parametric input never enters the interpreter and stays byte-identical (both
golden suites unchanged). Streaming, which can't re-run a line range from a
partial stream, discloses and runs linearly.

Validated on a real LinuxCNC-idiom while-loop bolt circle (params + degree trig
+ flow). 31 new flow tests; detection pre-filter proven a strict superset of the
classifier (no false-negative silent-wrong-geometry). maxCallDepth (subroutine
recursion) deferred to Phase 3.

Co-authored-by: Nathaniel Chestnut <sobechestnut-dev@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(parser): RS274NGC subroutines (DD-017 Phase 3, #189 phase 7)

Execute in-file O-word subroutines: o<id> sub/endsub, o<id> call [args],
o<id> return. A call binds args to #1..#30 in a fresh call frame, runs the
named sub's body, and returns; return exits early. Subs may be
forward-referenced and may recurse.

Correct, recursion-safe scoping (LinuxCNC): #1..#30 and #<local> are LOCAL to
each call frame; #31+ and #<_global> are shared globals. A recursive sub's #1
cannot clobber its caller's; locals do not leak. The main program runs in the
base scope, so a no-subroutine program is byte-identical (both golden suites
unchanged).

Bounded and safe: new maxCallDepth limit (default 50) caps recursion — an
unbounded/mutually-recursive call is disclosed (rs274-call-depth) and skipped,
never a stack overflow; a runtime exec-depth backstop bounds combined
nesting+call-depth JS recursion. A sub called inside a loop still charges its
body work against maxProgramIterations. break/continue/return are frame-local
(never cross a call boundary).

Honest: clean subroutine execution keeps parametricProgram 'known'; a degraded
run (unknown sub, recursion-limit, duplicate definition, misplaced return)
reports 'approximated' with a specific disclosure. Return VALUES are not yet
propagated (documented Phase-3 limitation; return = early exit). External sub
files (M98, o<name> call to a separate .ngc) remain an explicit non-goal.

44 subroutine + safety tests; full parser suite (243) + both goldens green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(parser): charge subroutine call-tree work; disclose endsub/arg mismatches (DD-017 Phase 3 review)

Adversarial review found a bounded-execution hole: the iteration budget was
charged only inside loops, so a loop-free recursive subroutine fan-out
(`o100 sub` calling `o100` twice) spawned an exponential call-tree that
maxCallDepth bounded only in DEPTH — total work was uncharged and, emitting no
geometry, tripped no segment limit either → an effectively-infinite,
uncancellable hang. Now every statement executed inside a loop OR a subroutine
is charged, so total call-tree size is bounded by maxProgramIterations
(disclosed rs274-iteration-limit). Verified with a fan-out regression test.

Also (honesty nits from the review): an endsub whose id does not match its sub
is now disclosed (rs274-unbalanced-oword), and a malformed trailing call argument
is disclosed and dropped rather than silently truncated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(parser): update rs274-flow header — subroutines are supported (Phase 3)

---------

Co-authored-by: Nathaniel Chestnut <sobechestnut-dev@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…cope, custom bed (#414)

* docs(design): DD-030 renderer/viewer interop — capture(), per-plate render scope, custom bed geometry

Proposed design for the three product-approved generic renderer/viewer
primitives, brought for public-surface sign-off before implementation:

- capture() -> Blob on GcodePreviewControls (+ ModelViewer), render-to-target
  mechanism (no change to the interactive preserveDrawingBuffer default),
  inherited by all 4 adapters.
- Per-plate render scope: a generic RenderScope on renderModelStill/ModelViewer
  with {plateId} sugar over an object-subset filter (model side only; toolpath
  plate isolation stays parse-time). Gated on capabilities.plates==='known'.
- Custom bed geometry: additive BuildVolumeDef.shape (polygon/circular primitive,
  mesh reserved) + machineToVolume upgrade so discovered non-rect beds stop
  collapsing to a bounding rect.

All additive -> one lockstep minor. Overhang/support-need viz explicitly
deferred (no analysis substrate; own future DD). Sliced-gcode variant thumbnails
+ rectangular bed/texture confirmed already-shipped and out of scope. Consumer
use-cases (AnyBridge) recorded as evidence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(design): DD-030 accepted — resolve §7 open questions (Blob-only, bbox cage, default non-rect bed, omit mesh)

---------

Co-authored-by: Nathaniel Chestnut <sobechestnut-dev@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Add BuildVolumeDef.shape?: BedShape (rect|circular|polygon) so the build volume
can draw an honest delta/round/irregular bed: a circle is polygonized, the
outline is filled and drawn, and the floor grid is clipped to the outline
(a round bed is no longer a square with a circle over it). machineToVolume()
now maps a discovered circular/polygon MachineGeometry.bed onto shape, so
discovered non-rect beds render as their true outline instead of collapsing to a
bounding rectangle. The mesh escape hatch is reserved.

Additive and byte-identical for rectangular beds (no shape / kind:'rect' takes
the original path; grid/plate/cage/origin unchanged). Caller supplies the shape;
the renderer just draws the polygon — no profile parsing, no vendor semantics.
First increment of the DD-030 renderer/viewer interop batch.

Co-authored-by: Nathaniel Chestnut <sobechestnut-dev@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…D2) (#417)

Add a generic RenderScope ({plateId} | {objectIds} | {instanceFilter}) and a
pure, three-free applyRenderScope(scene, scope) that returns a filtered
ModelScene (dropping non-matching objects/placements) with bounds recomputed
from the kept placements, so framing follows the subset. {plateId} is sugar over
the placement-level plateIds (DD-025).

Wired into renderModelStill (renderScope option + folded into the still cacheKey
so plate-1/plate-2 thumbnails key distinctly) and createModelViewer (initial
renderScope + setRenderScope(scope|null) handle that rebuilds/reframes).

Additive & honest: no renderScope renders the whole project unchanged; a
{plateId} that matches nothing (no declared plate structure) renders empty by
design (gate on capabilities.plates==='known'); declared plates/capabilities are
preserved. Second increment of the DD-030 renderer/viewer interop batch.

Co-authored-by: Nathaniel Chestnut <sobechestnut-dev@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Add capture(opts?) that returns the currently displayed view as an image Blob,
for user-selected thumbnails, large-file fallback, and screenshots. Available on
GcodePreviewControls.capture (all 4 adapters inherit it; the Web Component adds
an imperative method) and ModelViewer.capture; implemented on InteractiveStage
and delegated by ToolpathRenderer.

Mechanism = render-to-target: render the current scene + active camera into an
off-screen WebGLRenderTarget at the requested size and read it back, so it never
flips the interactive preserveDrawingBuffer default nor disturbs the live view,
and supports an arbitrary output size + independent/transparent background. The
library returns the Blob and never triggers a download (caller owns the pixels,
same contract as renderStill).

Honest: capture() rejects with a typed CaptureUnsupportedError
(E_CAPTURE_UNSUPPORTED) when the renderer cannot render-to-target (2D renderer,
stub GL / no WebGL) or the stage is disposed/context-lost. Purely additive (new
optional method on the renderer contract). Final increment of the DD-030 batch.

Co-authored-by: Nathaniel Chestnut <sobechestnut-dev@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@sobechestnut-dev
sobechestnut-dev merged commit 7fdc834 into main Aug 27, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant