diff --git a/.claude/launch.json b/.claude/launch.json index 559997b..5e70f71 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -55,6 +55,14 @@ "tools/example-webcomponent" ], "port": 5204 + }, + { + "name": "docs-site", + "runtimeExecutable": "node", + "runtimeArgs": [ + "tools/docs/serve-site.mjs" + ], + "port": 5098 } ] } diff --git a/.github/workflows/docs-pages.yml b/.github/workflows/docs-pages.yml index f9ba207..ae62cd7 100644 --- a/.github/workflows/docs-pages.yml +++ b/.github/workflows/docs-pages.yml @@ -13,6 +13,12 @@ on: - 'packages/**' - 'typedoc.json' - 'tools/docs/**' + - 'tools/demo/**' + - 'tools/demo-kit/**' + - 'tools/example-react/**' + - 'tools/example-vue/**' + - 'tools/example-svelte/**' + - 'tools/example-webcomponent/**' - 'docs/manual/**' - 'docs/media/**' - '.github/workflows/docs-pages.yml' @@ -39,6 +45,8 @@ jobs: cache: 'npm' - run: npm ci - run: npm run docs:build + # Build the live, interactive demos into the Pages artifact (docs-site/demos/*). + - run: npm run docs:demos - uses: actions/configure-pages@v5 - uses: actions/upload-pages-artifact@v3 with: diff --git a/README.md b/README.md index f419091..824547a 100644 --- a/README.md +++ b/README.md @@ -3,24 +3,28 @@ **Show a 3D print or CNC/laser job in the browser — before, during, or after it runs.** Drop `.gcode`, `.gcode.3mf`, or Prusa `.bgcode` into a web page and get an interactive toolpath view: orbit it, clip it to a layer, scrub through it, color it by feature or speed, and overlay -live printer progress. Parsing runs in a Web Worker, so a 250 MB file never freezes your UI. +live printer progress. Or view the **source model** (STL / 3MF) before it's sliced. Parsing runs in +a Web Worker, so a 250 MB file never freezes your UI. [![npm](https://img.shields.io/npm/v/@chestnutlabs/gcode-preview-react?label=npm&color=cb3837)](https://www.npmjs.com/package/@chestnutlabs/gcode-preview-react) [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE) [![docs](https://img.shields.io/badge/docs-manual%20%2B%20API-2ea44f)](https://chestnutlabs.github.io/gcode-preview/) ![node](https://img.shields.io/badge/node-%E2%89%A522-339933) -![3DBenchy rendered as extrusion tubes with per-feature coloring — perimeters, infill, and skirt in distinct colors — on a neutral grey build plate](docs/media/viewer-benchy-tubes.png) +![The G-code Preview Feature Lab — a 3DBenchy colored by feature role in the viewport, with the inspector rail (move-visibility toggles, feature-role hide, segment inspection), a color-mode legend, camera controls, a layers/segments/time scrub strip, and honest capability badges](docs/media/app-control-panel.png) + +**▶ [Try the live demo](https://chestnutlabs.github.io/gcode-preview/demos/)** — open the interactive **Feature Lab**, or the **React / Vue / Svelte / Web Component** examples, right in your browser. Nothing is uploaded; parsing runs in a Web Worker. --- ## What it is -G-code Preview turns a sliced G-code file into an interactive picture of the actual toolpath. It -reads the file the way the printer will — every move, layer, feature, and tool change — and draws -it with Three.js (or a Canvas 2D fallback for low-GPU devices). It ships as a set of -`@chestnutlabs/*` npm packages with ready-made **Vue, React, Svelte, and Web Component** components, -all built on one shared engine. +G-code Preview covers **both halves** of looking at a print job. **Preview** turns a sliced G-code +file into an interactive picture of the actual toolpath — reading the file the way the printer will, +every move, layer, feature, and tool change, drawn with Three.js (or a Canvas 2D fallback for +low-GPU devices). **Prepare** shows the **source model** (STL / 3MF) before slicing — the object, +its parts, and its material colors. It ships as a set of `@chestnutlabs/*` npm packages with +ready-made **Vue, React, Svelte, and Web Component** components, all built on one shared engine. One rule runs through the whole thing: **it shows what it can prove and refuses to fake the rest.** If a slicer didn't record feature types, the viewer says so instead of inventing colors. If live @@ -30,7 +34,22 @@ telemetry only reports a percentage, it draws an uncertainty band, not a false-p npm install @chestnutlabs/gcode-preview-react three # or -vue / -svelte / -element ``` -![The demo application: a control panel — corpus picker, layer and time scrub, an honest "54.2 min (slicer estimate)" print time, display toggles — beside a fully rendered 3DBenchy with feature coloring](docs/media/app-control-panel.png) +### Two modes, one SDK — Prepare & Preview + +| Prepare — the source model | Preview — the sliced toolpath | +|---|---| +| ![The Feature Lab in Prepare mode: a colored 3MF source model (red, amber, green stacked blocks) with a Source Model panel reporting 3 objects, 3 placements, and materials: known](docs/media/app-prepare.png) | ![The Feature Lab in Preview mode: a 3DBenchy colored by feature role with the inspector rail, feature legend, camera controls, and layer/segment scrub](docs/media/app-control-panel.png) | +| **STL / 3MF** solid model — objects, materials, model controls, honest material-color tier. | **Sliced G-code** — layers, features, speed, travel, retractions, and toolpath controls. | + +The controls and capabilities are front-and-centre, not hidden behind a render: + +| Color modes, capability-gated | Render diagnostics & capabilities | +|---|---| +| ![The Appearance tab: a color-by selector (single / tool / feature / speed / object / layer height) with the active feature-role legend; modes the file can't support are greyed out](docs/media/app-appearance.png) | ![The Diagnostics tab: capability confidence badges and getRenderStats() output — backend, GPU, geometry mode, segment and draw-call counts, worker pool](docs/media/app-diagnostics.png) | + +You can drive all of this yourself in the **[live demos](https://chestnutlabs.github.io/gcode-preview/demos/)**. A cropped beauty render — the same engine, no UI: + +![3DBenchy rendered as extrusion tubes with per-feature coloring — perimeters, infill, and skirt in distinct colors — on a neutral grey build plate](docs/media/viewer-benchy-tubes.png) ## Why you'd use it @@ -173,14 +192,16 @@ every FDM file is byte-for-byte unchanged. See ### Framework integration Four adapters over one engine, with matching options, events, and TypeScript types — enforced by a -shared behavioral test suite that runs against all four in CI: +shared behavioral test suite (with a controls/state parity guard) that runs against all four in CI. +Each adapter exposes **both** viewers: the toolpath `` from the package root, and the +source-model `` from the package's **`/model`** subpath (the Prepare side). -| Adapter | Package | Component + lower-level API | -|---|---|---| -| **Vue 3** | [`@chestnutlabs/gcode-preview-vue`](packages/gcode-preview-vue) | `` + `useGcodePreview()` composable | -| **React** | [`@chestnutlabs/gcode-preview-react`](packages/gcode-preview-react) | `` + `useGcodePreview()` hook (StrictMode-safe) | -| **Svelte** | [`@chestnutlabs/gcode-preview-svelte`](packages/gcode-preview-svelte) | `` + `createGcodePreview()` store/action | -| **Web Component** | [`@chestnutlabs/gcode-preview-element`](packages/gcode-preview-element) | `` custom element, no framework peer | +| Adapter | Package | Preview (toolpath) | Prepare (source model, `/model`) | +|---|---|---|---| +| **Vue 3** | [`@chestnutlabs/gcode-preview-vue`](packages/gcode-preview-vue) | `` + `useGcodePreview()` | `` + `useModelViewer()` | +| **React** | [`@chestnutlabs/gcode-preview-react`](packages/gcode-preview-react) | `` + `useGcodePreview()` (StrictMode-safe) | `` + `useModelViewer()` | +| **Svelte** | [`@chestnutlabs/gcode-preview-svelte`](packages/gcode-preview-svelte) | `` + `createGcodePreview()` | `` + `createModelViewer()` | +| **Web Component** | [`@chestnutlabs/gcode-preview-element`](packages/gcode-preview-element) | `` (no framework peer) | `` | ### Browser & performance architecture @@ -244,11 +265,15 @@ const { canvas, materials, cacheKey } = await renderModelStill( // materials: 'known' when the source carried colors, 'unavailable' when it didn't. ``` -Need it live rather than as a thumbnail — a "View in 3D" for the part? `createModelViewer` is the -interactive analogue of the still: it orbits, zooms, and pans the same STL / 3MF (including production -multicolor) with camera presets and the same serializable camera state, over the shared camera and -orbit controls. See the -[model-renderer README](packages/gcode-model-renderer/README.md#interactive-viewer). +Need it live rather than as a thumbnail — a "View in 3D" for the part? Interactive source-model +viewing is a **first-class, declarative half of the SDK**: drop `` from any +framework's **`/model`** subpath (`@chestnutlabs/gcode-preview-{react,vue,svelte}/model`, or +`` from the Web Component) and orbit/zoom/pan the same STL / 3MF (including +production multicolor) with camera presets, serializable camera state, per-plate render scope, and the +honest `materials` capability tier — the same parity (props, events, lifecycle, cleanup) as the +toolpath component. It wraps the framework-neutral `createModelPreviewController` over the shared +`createModelViewer` engine. See the [adapters guide](docs/manual/adapters.md) ("Two viewers: Preview +and Prepare") and the [model-renderer README](packages/gcode-model-renderer/README.md). **Multi-plate, multi-object, or just a subset.** A 3MF project can hold several plates and many objects. Parse or render **one plate at a time** (`parseOptions.plate`), and a **render scope** @@ -452,7 +477,7 @@ row inspects the *toolpath*, the bottom row presents the *source model* (see Design rationale, boundaries, and the accepted architecture live in [`docs/design/`](docs/design); the [docs index](docs/README.md) maps the whole set. Fourteen -packages publish to npm in lockstep (currently **v0.20.0**) with npm provenance. +packages publish to npm in lockstep (currently **v0.20.1**) with npm provenance. ## Development diff --git a/RELEASE_REVIEW.md b/RELEASE_REVIEW.md index b53c1d2..520f21a 100644 --- a/RELEASE_REVIEW.md +++ b/RELEASE_REVIEW.md @@ -12,10 +12,10 @@ parses them literally. Delete rows only for packages that did not change. --> -# Release review — v0.20.0 +# Release review — v0.20.1 -**Review version:** v0.20.0 -**Changed-capability inventory diffed against:** `v0.19.0` +**Review version:** v0.20.1 +**Changed-capability inventory diffed against:** `v0.20.0` This artifact records the per-release **Public Product + Documentation + Visual** reconciliation: before the `dev` -> `main` promotion, compare the changed capabilities below against the README, @@ -28,45 +28,18 @@ each disposition. Guidance: CLAUDE.md "Public-docs completion check" and | Package | Changed src files | Changelog summary | Disposition | |---|--:|---|---| -| `@chestnutlabs/gcode-model-renderer` | 4 | Make interactive **source-model viewing** (STL / 3MF) a first-class, declarative half of the SDK — | Status: reviewed | -| `@chestnutlabs/gcode-preview-core` | 4 | Close the dark-capability gaps so no shipped capability is reachable only through the | Status: reviewed | -| `@chestnutlabs/gcode-preview-element` | 6 | Close the dark-capability gaps so no shipped capability is reachable only through the | Status: reviewed | -| `@chestnutlabs/gcode-preview-react` | 7 | Close the dark-capability gaps so no shipped capability is reachable only through the | Status: reviewed | -| `@chestnutlabs/gcode-preview-svelte` | 6 | Close the dark-capability gaps so no shipped capability is reachable only through the | Status: reviewed | -| `@chestnutlabs/gcode-preview-vue` | 6 | Close the dark-capability gaps so no shipped capability is reachable only through the | Status: reviewed | -| `@chestnutlabs/gcode-renderer-three` | 3 | `CaptureOptions` gains `includeBuildVolume?: boolean` (default `true`). Set it to `false` to exclude | Status: reviewed | +| `@chestnutlabs/gcode-parser` | 2 | Fix a parse-session hang on a double `cancel()`. A second `cancel()` while a cancel was already pending (e.g. the controller cancelling befo | Status: reviewed | +| `@chestnutlabs/gcode-preview-element` | 2 | Fix `` connect/disconnect defects: the initial `view` attribute (and a `cameraState` set before connect) are now applied on c | Status: reviewed | +| `@chestnutlabs/gcode-preview-react` | 2 | Fix `` / `useModelViewer` (React) leaking its controller on unmount. The hook now disposes the model-preview controller when th | Status: reviewed | +| `@chestnutlabs/gcode-renderer-three` | 4 | Fix `capture()` returning too-dark (linear colour-space) pixels. The interactive capture path rendered into a render target without sRGB out | Status: reviewed | Set each **Disposition** to one of `Status: reviewed` / `Status: no-change-needed` / `Status: not-applicable` (never `Status: pending`). ## Global dispositions -- **Product review:** resolved — README / Pages homepage / feature gallery still describe the product accurately for this release. -- **Docs review:** resolved — manual, package READMEs, examples, and quick-start match the shipped API. -- **Visual review:** resolved — every changed user-facing capability is visually documented, or the coverage matrix records why not. +- **Product review:** resolved — v0.20.1 is a correctness patch with **no API or product-surface change**. README / Pages homepage / feature gallery remain accurate; the `docs/README.md` "Current state" now leads with the v0.20.1 patch and its five fixes. +- **Docs review:** resolved — no public API changed, so the manual, package READMEs, examples, and quick-start still match the shipped API. Each fix carries a changeset → per-package CHANGELOG entry, and the deferred/related findings are tracked in issues #430–#435. +- **Visual review:** resolved — no UI/visual change. The app-UI screenshots and capability renders still show the current UI (the demo screenshot harness copies the live sRGB-correct canvas, so it was never affected by the `capture()` colour-space bug this patch fixes), and the live demos build/run on the published SDK. The `capture()` fix is verified in-browser (a `#6d7176` scene round-trips through `capture()` as `#6d7176`, not the previous linear `#272a2e`). -_Resolve each marker above by replacing `pending` with `resolved` once reconciled._ - -## Reconciliation notes (v0.20.0) - -All seven changed packages are the DD-031 consumer-UX / framework-parity pass, reconciled together: - -- **Product:** the whole point of this release is consumer-facing surface. `docs/README.md` - "Current state" leads with v0.20.0; the two-viewer (Preview + Prepare) story is accurate. The root - README and Pages homepage remain accurate (they describe the toolpath viewer + source-model - rendering; the new framework model adapters are additive API documented in the canonical adapter - docs). No existing product copy misrepresents the shipped software. -- **Docs:** `docs/manual/adapters.md` gained "Two viewers: Preview and Prepare" + a per-framework - import table; all four adapter READMEs gained a "Model viewing (Prepare side)" section; the - `gcode-model-renderer` README documents `createModelPreviewController` + the `/testing` suite; - `CLAUDE.md`'s parity check now covers both viewer surfaces. `npm run docs:links` passes. -- **Visual:** the interactive model viewer is now demonstrated in the Feature Lab + all four - showcases (Preview/Prepare toggle) and a minimal model page per framework; the coverage matrix - (`docs/VISUAL_FEATURE_COVERAGE.md` §6/§10) records this. Dark **app-UI** screenshots of Prepare - mode were captured for review but are **not yet canonical `docs/media`** — the manifest harness - emits the mid-grey capability presentation, not app chrome; that gap is explicitly recorded and - carried to the next Visual Review. No shipped capability's existing media is now stale. -- **Framework parity:** every new controller/model capability is exposed across Vue, React, Svelte, - and the Web Component (declaratively where it belongs; the `` tag and the - `/model` subpath are documented intentional differences), exercised by the portable behavioral - suites with controls/state completeness parity guards for both the toolpath and model surfaces. +_All markers resolved: this patch changes behaviour (bug fixes) with no product/docs/visual surface change; the capture colour fix was validated in a real browser._ diff --git a/docs/README.md b/docs/README.md index ae3fd21..cbb428e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -39,7 +39,7 @@ See also [`../PROJECT_SETUP.md`](../PROJECT_SETUP.md) for the local environment Provenance of inherited vs. Chestnut code, the exact founding baseline, and the branch mapping are tracked in [`UPSTREAM_PROVENANCE.md`](UPSTREAM_PROVENANCE.md). -## Current state (updated 2026-08-30 — **v0.20.0 shipped to npm** (14 lockstep pkgs): interactive **source-model viewing** (STL / 3MF) becomes a first-class, declarative half of the SDK — the Prepare side alongside the toolpath Preview side. A framework-neutral **`createModelPreviewController`** (wrapping the existing `createModelViewer` engine unchanged) plus a **``** adapter on each framework's **`/model`** subpath — `@chestnutlabs/gcode-preview-{react,vue,svelte,element}/model` (the Web Component ships as ``) — give the source model the same declarative parity as the toolpath component: lifecycle, props/options, events, capability exposure (the honest `materials` colour tier + object/instance counts), cleanup and error handling, all locked by a portable model behavioral suite with its own controls/state parity guard ([DD-031](design/DD-031-consumer-ux-and-framework-parity.md)). Shipped with the **dark-capability closure** across all four toolpath adapters so no shipped capability is reachable only through the raw-renderer escape hatch — `getRenderStats()` render diagnostics, `pickSegment()` source-mapping, `isColorModeAvailable()`, declarative `hiddenFeatureRoles`, and handle-level `capture()` — plus `capture({ includeBuildVolume: false })` for grid-free toolpath cutouts. The demo is rebuilt as a viewport-first **Feature Lab** and every framework ships **two-tier examples** (a minimal getting-started page + a full capability-aware showcase, with a Preview/Prepare toggle) on the real published adapters. Prior: **v0.19.0** (14 lockstep pkgs): a new **`setFeatureRoleVisible(role, visible)`** on the toolpath renderer and the shared controls hides or shows a single **feature role** — e.g. hide brim/skirt to declutter a part preview, or isolate support — by collapsing those per-segment moves to GPU-discarded geometry and restoring them byte-for-byte; it's extrude-only, capability-gated (`featureRoles: 'known'`), and **byte-identical when unused**. Shipped alongside a **public-product + documentation pass**: GitHub Pages is now a visual product manual — a product-first homepage, a comprehensive **feature gallery** with real screenshots plus scrub/build-up **animations**, and screenshots across the concept pages — backed by a maintained **Visual Feature Coverage Matrix** ([`VISUAL_FEATURE_COVERAGE.md`](VISUAL_FEATURE_COVERAGE.md)), a canonical mid-grey documentation-capture look, and an **enforced per-release Public Product + Docs + Visual review** (`RELEASE_REVIEW.md`) that blocks the `dev → main` promotion until every changed capability is reconciled. Prior: **v0.18.0** (14 lockstep pkgs): the parser now executes the full **RS274NGC programming layer** that CAM / LinuxCNC output uses to *generate* geometry — numbered/named/global **parameters**, bracket **expressions** (degree trig + the full operator set), **O-word control flow** (`if`/`elseif`/`else`, `while`/`do`/`repeat`, `break`/`continue`), and in-file **subroutines** (`sub`/`call`/`return` with recursion-safe LinuxCNC scoping) — so a bolt circle written as a `while` loop, or a feature factored into a called sub, resolves to the real toolpath instead of rendering empty; all bounded (`maxProgramIterations` / `maxCallDepth` — a hostile program wastes only bounded CPU), honesty-tiered (`parametricProgram` `known` / `approximated`), and **byte-identical** for FDM / non-parametric input ([DD-017](design/DD-017-rs274ngc-parametric-programs.md), phases 1–3). Plus a generic **renderer/viewer interoperability** batch ([DD-030](design/DD-030-renderer-viewer-interop.md)): interactive **`capture()` → `Blob`** (render-to-target — user-selected thumbnails, large-file fallback, screenshots) on the controls (all four adapters inherit it) and the model viewer; **per-plate render scope** so a single-plate variant of a multi-plate project renders and frames just *that* plate (`renderModelStill({ renderScope })` + `ModelViewer.setRenderScope()`); and **non-rectangular build beds** (`BuildVolumeDef.shape` — delta / round / polygon, with the floor grid clipped to the true outline). Prior: **v0.17.0** — `frameContent: 'object'` now frames the **printed model** — a non-model classifier excludes slicer housekeeping (skirt, brim, raft, prime/wipe tower, purge) by object membership when the file carries it, else by feature role, producing a new additive `modelBounds` and a `nonModelClassification` confidence (`known` / `inferred` / `unavailable`); the framing precedence is `modelBounds → objectBounds → bounds`, so a Bambu wipe tower at the plate edge no longer inflates the frame — validated on a real 6.5 M-segment four-colour plate ([DD-026](design/DD-026-non-model-geometry-classification-and-object-framing.md) T2), and the OrcaSlicer/Bambu adapter now reads `FLUSH_START/END` and `WIPE_TOWER_START/END` brackets into first-class `Purge` / `WipeTower` roles. `renderStill` gains **`tubeByteBudget`** so a deployment with RAM to spare can retain full tubes on a large plate instead of silently falling to lines behind the conservative default — a separate axis from the parallel build's `geometryMemoryBudgetBytes` (CPU→build speed vs RAM→tubes-retained). And a geometry-worker-pool failure now degrades to a **serial main-thread tube build** — never an unhandled rejection or a silent drop to lines — so the [DD-028](design/DD-028-parallel-geometry-worker-pool.md) pool is safe to enable in headless bundlers that cannot construct the worker URL. Prior: **v0.16.0** — heavy toolpaths now build their tube geometry **off the main thread** on a capability-sized, memory-bounded worker pool — byte-identical to the serial build and degrading to continuous lines (never chopped) if a worker fails ([DD-028](design/DD-028-parallel-geometry-worker-pool.md)); the viewer prepares the **complete** geometry and reveals it **once** via `progressivePreview: 'auto' | 'lines' | 'hold' | 'off'` (`'hold'` reveals only the final, correctly-coloured build — no low-quality preview, no segment-dropping), backed by a staged `stage` event that carries a real building-geometry `%` ([DD-029](design/DD-029-preparation-and-reveal.md)); a new **render-diagnostics** surface — `getRenderStats()` plus a `renderStats` event — reports whether WebGL is running on **hardware or software**, the GPU renderer, geometry mode, segment/vertex counts, draw calls, and timings, with honest `null`s rather than guesses ([DD-027](design/DD-027-render-diagnostics.md)); and the slicer adapters now capture real **object labels and feature roles** from OrcaSlicer, PrusaSlicer, ideaMaker, and Simplify3D, with first-class non-model **FeatureRoles** — PrimeTower, WipeTower, Raft, Purge ([DD-026](design/DD-026-non-model-geometry-classification-and-object-framing.md) T1/T2). Prior: **v0.15.0** — a **`progressivePreview`** curtain (`'lines'` default / `'hold'` / `'off'`) over the during-parse preview, across the toolpath renderer, core controller, and all four adapters ([#373](https://github.com/ChestnutLabs/gcode-preview/pull/373)). `'hold'` keeps the progress signal (drive a `%` bar from `parse-progress`) but reveals **only the final, correctly-coloured, policy-quality build** — replacing the neutral-then-recolour "double render" on streamed files with a single clean reveal; `'off'` suppresses the built-in preview for a consumer-driven loading treatment; `'lines'` (the default) preserves today's streaming line preview. Orthogonal to `quality`/`qualityMode` — the revealed representation is always the policy-correct one, never a silent large-file lines fallback. Prior: **v0.14.0** — a **`qualityMode`** fidelity policy (Full / Adaptive / Fast) across the toolpath renderer, core controller, and all four adapters ([DD-023](design/DD-023-capability-aware-render-budget.md) §4 D6, Phase B) — `'full'` renders the COMPLETE representation (no every-Nth decimation, full-radial continuous tubes, no budget→lines fallback); `'adaptive'` (default) is the capability-aware auto path; `'fast'` is flat lines. Plus a **continuity correction: tubes are never segment-decimated** — `autoDecimation`'s every-Nth drop was chopping tube geometry into disconnected stubs (the RR-006 continuity break via the decimation lever the v0.12.0 fix didn't cover); tube memory is now bounded ONLY by continuity-preserving radial reduction, degrading to continuous lines when even a 3-sided tube won't fit — never chopped. The honest order is full-radial tubes → lower-radial tubes → continuous lines. (Classifier-driven auto budget + the too-heavy signal land later.) Prior: **v0.13.0** (14 lockstep pkgs), a farm-workload batch: render the colour/structure a source model actually carries, stop over-degrading normal large plates, and lay the capability-aware groundwork. **Toolpath:** the tube memory budget now counts only **extrude** segments (not travel/wipe), so a heavy-travel plate (800+ parts) renders as continuous tubes instead of dropping to lines; a new **`filament` colour mode** renders a toolpath in the file's OWN filament colours (resolved from parse metadata before the first build — no neutral-then-recolour flash on raw `.gcode`). **Source model** ([`gcode-model-renderer`](../packages/gcode-model-renderer)): **multi-plate structure** ([DD-025](design/DD-025-multi-plate-model-structure.md) Phase A — `ModelScene.plates` + placement-level `plateIds` + `capabilities.plates`, declared-only), **Bambu/Orca per-object extruder colours** (a `.3mf` coloured via the project convention renders in colour, not neutral), and **staged loading progress** ([DD-024](design/DD-024-staged-loading-progress.md) Phase A — typed, consumer-neutral, generation-scoped, never a faked %). **Capability groundwork** ([DD-023](design/DD-023-capability-aware-render-budget.md) Phase A): a shared fail-safe WebGL hw/sw classifier + `QualityPolicy`/`CapabilityHint` types (no behaviour change yet; capability-aware budgets + `qualityMode` land later). Prior: **v0.12.0** — fixes the two regressions surfaced against v0.11.0 and promotes the tube memory bound to an honest control. **Tube rendering** ([RR-006](research/RR-006-tube-mesh-memory-and-large-file-budget.md) correction) now bounds memory by **coarsening the cross-section** (fewer radial sides) at full segment continuity — replacing the v0.10.0 drop-every-Nth-segment budget that shredded tubes into spiky capped stubs; new public `tubeByteBudget` option, `tubeSegmentBudget` deprecated, lines fallback only when even a 3-sided tube can't fit. **Model estimate** now counts **unique** master triangles, not baked-per-placement, so a full-sheet instanced `.3mf` plate (~40 reused copies) parses/renders at default limits instead of falsely rejecting as "too large" (instance bomb still guarded by `maxInstances`). Prior: **v0.11.0** — **instance-aware source-model rendering** ([DD-022](design/DD-022-model-instancing-and-lod.md), Phases 0–1) — a full-sheet `.3mf` that reuses a master mesh via production-extension components / repeated build items now renders each master **once** via GPU instancing (three `InstancedMesh`) instead of baking a copy per placement, so memory scales with unique geometry not copy count (a ~40× instanced plate that used to exceed the triangle limit now renders); plus a **sub-second byte-estimate fast-reject** for genuinely oversize / instance-bomb files, and `instancedCount` + `decimationApplied` disclosure on the model `ready`/still result. **v0.10.0** added a **tube-mesh memory budget** so large forced-`tubes` renders (≈1.6 M+ segments) no longer OOM a 2 GB render worker — tubes decimate to a `TUBE_SEGMENT_BUDGET`, disclosed via `decimationApplied` (also on `RenderStillResult`), layer boundaries preserved ([RR-006](research/RR-006-tube-mesh-memory-and-large-file-budget.md)); plus a fix so `frameContent:'object'` frames the printed object on AnycubicSlicerNext and every object-labeling file (`objectBounds` is refreshed after dialect annotation). **v0.9.0** shipped the **interactive source-model viewer** `createModelViewer` in `@chestnutlabs/gcode-model-renderer` — the live orbit/zoom/pan analogue of `renderModelStill` for an STL or 3MF, over a shared interactive "stage" extracted in `gcode-renderer-three`, with an open-`kind` model-source loader registry (STL/3MF now, OBJ/STEP/PLY later without an API change), capability-honest color pass-through, and a `renderer-unsupported` fallback ([DD-021](design/DD-021-interactive-model-viewer.md)). v0.8.1 added production Bambu/Orca 3MF `paint_color` multicolor decode ([RR-005](research/RR-005-3mf-paint-color-facet-format.md)) so a designer's painted model renders in its true colors without slicing, plus a filament-slot dedup fix. v0.7.0 shipped the presentation package `@chestnutlabs/gcode-model-renderer` (headless `renderModelStill`, STL + 3MF multi-object/material, [DD-018](design/DD-018-model-renderer.md)), the 3MF Production-Extension (`p:path`) reader, and a toolpath-renderer batch — slice metadata on `ready`, structured multi-plate metadata, independent build-volume cage, frame-to-content, interaction-aware render quality ([DD-020](design/DD-020-interaction-aware-quality.md)), and a `renderStill` background option. v0.5.x added camera preset-views + serializable `CameraState`, zoom-to-cursor, keyboard-a11y, `capabilities`/`warnings` on `ready`, and the Vue/Svelte adapter mount-prop fixes (epics #279/#296/#306/DD-021). DD-017 RS274NGC parametric programs proposed, unbuilt) +## Current state (updated 2026-09-01 — **v0.20.1 shipped to npm** (14 lockstep pkgs): a focused **correctness patch** fixing five confirmed bugs from an internal audit, no API changes. Interactive **`capture()`** / `ModelViewer.capture()` now return **sRGB-correct colours** instead of too-dark linear pixels (a `#6d7176` background had been captured as `#272a2e`), matching the on-screen view and `renderStill`; a double **`cancel()`** no longer orphans the parse-session terminate backstop (which could wedge a subsequent re-parse at `parsing: true`); **`GeometryWorkerPool.dispose()`** no longer hangs an in-flight build; the React **`` / `useModelViewer`** now disposes its controller on unmount (parity with the toolpath hook and the other adapters); and **``** applies its initial `view` (and a pre-connect `cameraState`) on connect and re-applies `hidden-feature-roles` after a DOM reconnect. Prior: **v0.20.0** (14 lockstep pkgs): interactive **source-model viewing** (STL / 3MF) becomes a first-class, declarative half of the SDK — the Prepare side alongside the toolpath Preview side. A framework-neutral **`createModelPreviewController`** (wrapping the existing `createModelViewer` engine unchanged) plus a **``** adapter on each framework's **`/model`** subpath — `@chestnutlabs/gcode-preview-{react,vue,svelte,element}/model` (the Web Component ships as ``) — give the source model the same declarative parity as the toolpath component: lifecycle, props/options, events, capability exposure (the honest `materials` colour tier + object/instance counts), cleanup and error handling, all locked by a portable model behavioral suite with its own controls/state parity guard ([DD-031](design/DD-031-consumer-ux-and-framework-parity.md)). Shipped with the **dark-capability closure** across all four toolpath adapters so no shipped capability is reachable only through the raw-renderer escape hatch — `getRenderStats()` render diagnostics, `pickSegment()` source-mapping, `isColorModeAvailable()`, declarative `hiddenFeatureRoles`, and handle-level `capture()` — plus `capture({ includeBuildVolume: false })` for grid-free toolpath cutouts. The demo is rebuilt as a viewport-first **Feature Lab** and every framework ships **two-tier examples** (a minimal getting-started page + a full capability-aware showcase, with a Preview/Prepare toggle) on the real published adapters. Prior: **v0.19.0** (14 lockstep pkgs): a new **`setFeatureRoleVisible(role, visible)`** on the toolpath renderer and the shared controls hides or shows a single **feature role** — e.g. hide brim/skirt to declutter a part preview, or isolate support — by collapsing those per-segment moves to GPU-discarded geometry and restoring them byte-for-byte; it's extrude-only, capability-gated (`featureRoles: 'known'`), and **byte-identical when unused**. Shipped alongside a **public-product + documentation pass**: GitHub Pages is now a visual product manual — a product-first homepage, a comprehensive **feature gallery** with real screenshots plus scrub/build-up **animations**, and screenshots across the concept pages — backed by a maintained **Visual Feature Coverage Matrix** ([`VISUAL_FEATURE_COVERAGE.md`](VISUAL_FEATURE_COVERAGE.md)), a canonical mid-grey documentation-capture look, and an **enforced per-release Public Product + Docs + Visual review** (`RELEASE_REVIEW.md`) that blocks the `dev → main` promotion until every changed capability is reconciled. Prior: **v0.18.0** (14 lockstep pkgs): the parser now executes the full **RS274NGC programming layer** that CAM / LinuxCNC output uses to *generate* geometry — numbered/named/global **parameters**, bracket **expressions** (degree trig + the full operator set), **O-word control flow** (`if`/`elseif`/`else`, `while`/`do`/`repeat`, `break`/`continue`), and in-file **subroutines** (`sub`/`call`/`return` with recursion-safe LinuxCNC scoping) — so a bolt circle written as a `while` loop, or a feature factored into a called sub, resolves to the real toolpath instead of rendering empty; all bounded (`maxProgramIterations` / `maxCallDepth` — a hostile program wastes only bounded CPU), honesty-tiered (`parametricProgram` `known` / `approximated`), and **byte-identical** for FDM / non-parametric input ([DD-017](design/DD-017-rs274ngc-parametric-programs.md), phases 1–3). Plus a generic **renderer/viewer interoperability** batch ([DD-030](design/DD-030-renderer-viewer-interop.md)): interactive **`capture()` → `Blob`** (render-to-target — user-selected thumbnails, large-file fallback, screenshots) on the controls (all four adapters inherit it) and the model viewer; **per-plate render scope** so a single-plate variant of a multi-plate project renders and frames just *that* plate (`renderModelStill({ renderScope })` + `ModelViewer.setRenderScope()`); and **non-rectangular build beds** (`BuildVolumeDef.shape` — delta / round / polygon, with the floor grid clipped to the true outline). Prior: **v0.17.0** — `frameContent: 'object'` now frames the **printed model** — a non-model classifier excludes slicer housekeeping (skirt, brim, raft, prime/wipe tower, purge) by object membership when the file carries it, else by feature role, producing a new additive `modelBounds` and a `nonModelClassification` confidence (`known` / `inferred` / `unavailable`); the framing precedence is `modelBounds → objectBounds → bounds`, so a Bambu wipe tower at the plate edge no longer inflates the frame — validated on a real 6.5 M-segment four-colour plate ([DD-026](design/DD-026-non-model-geometry-classification-and-object-framing.md) T2), and the OrcaSlicer/Bambu adapter now reads `FLUSH_START/END` and `WIPE_TOWER_START/END` brackets into first-class `Purge` / `WipeTower` roles. `renderStill` gains **`tubeByteBudget`** so a deployment with RAM to spare can retain full tubes on a large plate instead of silently falling to lines behind the conservative default — a separate axis from the parallel build's `geometryMemoryBudgetBytes` (CPU→build speed vs RAM→tubes-retained). And a geometry-worker-pool failure now degrades to a **serial main-thread tube build** — never an unhandled rejection or a silent drop to lines — so the [DD-028](design/DD-028-parallel-geometry-worker-pool.md) pool is safe to enable in headless bundlers that cannot construct the worker URL. Prior: **v0.16.0** — heavy toolpaths now build their tube geometry **off the main thread** on a capability-sized, memory-bounded worker pool — byte-identical to the serial build and degrading to continuous lines (never chopped) if a worker fails ([DD-028](design/DD-028-parallel-geometry-worker-pool.md)); the viewer prepares the **complete** geometry and reveals it **once** via `progressivePreview: 'auto' | 'lines' | 'hold' | 'off'` (`'hold'` reveals only the final, correctly-coloured build — no low-quality preview, no segment-dropping), backed by a staged `stage` event that carries a real building-geometry `%` ([DD-029](design/DD-029-preparation-and-reveal.md)); a new **render-diagnostics** surface — `getRenderStats()` plus a `renderStats` event — reports whether WebGL is running on **hardware or software**, the GPU renderer, geometry mode, segment/vertex counts, draw calls, and timings, with honest `null`s rather than guesses ([DD-027](design/DD-027-render-diagnostics.md)); and the slicer adapters now capture real **object labels and feature roles** from OrcaSlicer, PrusaSlicer, ideaMaker, and Simplify3D, with first-class non-model **FeatureRoles** — PrimeTower, WipeTower, Raft, Purge ([DD-026](design/DD-026-non-model-geometry-classification-and-object-framing.md) T1/T2). Prior: **v0.15.0** — a **`progressivePreview`** curtain (`'lines'` default / `'hold'` / `'off'`) over the during-parse preview, across the toolpath renderer, core controller, and all four adapters ([#373](https://github.com/ChestnutLabs/gcode-preview/pull/373)). `'hold'` keeps the progress signal (drive a `%` bar from `parse-progress`) but reveals **only the final, correctly-coloured, policy-quality build** — replacing the neutral-then-recolour "double render" on streamed files with a single clean reveal; `'off'` suppresses the built-in preview for a consumer-driven loading treatment; `'lines'` (the default) preserves today's streaming line preview. Orthogonal to `quality`/`qualityMode` — the revealed representation is always the policy-correct one, never a silent large-file lines fallback. Prior: **v0.14.0** — a **`qualityMode`** fidelity policy (Full / Adaptive / Fast) across the toolpath renderer, core controller, and all four adapters ([DD-023](design/DD-023-capability-aware-render-budget.md) §4 D6, Phase B) — `'full'` renders the COMPLETE representation (no every-Nth decimation, full-radial continuous tubes, no budget→lines fallback); `'adaptive'` (default) is the capability-aware auto path; `'fast'` is flat lines. Plus a **continuity correction: tubes are never segment-decimated** — `autoDecimation`'s every-Nth drop was chopping tube geometry into disconnected stubs (the RR-006 continuity break via the decimation lever the v0.12.0 fix didn't cover); tube memory is now bounded ONLY by continuity-preserving radial reduction, degrading to continuous lines when even a 3-sided tube won't fit — never chopped. The honest order is full-radial tubes → lower-radial tubes → continuous lines. (Classifier-driven auto budget + the too-heavy signal land later.) Prior: **v0.13.0** (14 lockstep pkgs), a farm-workload batch: render the colour/structure a source model actually carries, stop over-degrading normal large plates, and lay the capability-aware groundwork. **Toolpath:** the tube memory budget now counts only **extrude** segments (not travel/wipe), so a heavy-travel plate (800+ parts) renders as continuous tubes instead of dropping to lines; a new **`filament` colour mode** renders a toolpath in the file's OWN filament colours (resolved from parse metadata before the first build — no neutral-then-recolour flash on raw `.gcode`). **Source model** ([`gcode-model-renderer`](../packages/gcode-model-renderer)): **multi-plate structure** ([DD-025](design/DD-025-multi-plate-model-structure.md) Phase A — `ModelScene.plates` + placement-level `plateIds` + `capabilities.plates`, declared-only), **Bambu/Orca per-object extruder colours** (a `.3mf` coloured via the project convention renders in colour, not neutral), and **staged loading progress** ([DD-024](design/DD-024-staged-loading-progress.md) Phase A — typed, consumer-neutral, generation-scoped, never a faked %). **Capability groundwork** ([DD-023](design/DD-023-capability-aware-render-budget.md) Phase A): a shared fail-safe WebGL hw/sw classifier + `QualityPolicy`/`CapabilityHint` types (no behaviour change yet; capability-aware budgets + `qualityMode` land later). Prior: **v0.12.0** — fixes the two regressions surfaced against v0.11.0 and promotes the tube memory bound to an honest control. **Tube rendering** ([RR-006](research/RR-006-tube-mesh-memory-and-large-file-budget.md) correction) now bounds memory by **coarsening the cross-section** (fewer radial sides) at full segment continuity — replacing the v0.10.0 drop-every-Nth-segment budget that shredded tubes into spiky capped stubs; new public `tubeByteBudget` option, `tubeSegmentBudget` deprecated, lines fallback only when even a 3-sided tube can't fit. **Model estimate** now counts **unique** master triangles, not baked-per-placement, so a full-sheet instanced `.3mf` plate (~40 reused copies) parses/renders at default limits instead of falsely rejecting as "too large" (instance bomb still guarded by `maxInstances`). Prior: **v0.11.0** — **instance-aware source-model rendering** ([DD-022](design/DD-022-model-instancing-and-lod.md), Phases 0–1) — a full-sheet `.3mf` that reuses a master mesh via production-extension components / repeated build items now renders each master **once** via GPU instancing (three `InstancedMesh`) instead of baking a copy per placement, so memory scales with unique geometry not copy count (a ~40× instanced plate that used to exceed the triangle limit now renders); plus a **sub-second byte-estimate fast-reject** for genuinely oversize / instance-bomb files, and `instancedCount` + `decimationApplied` disclosure on the model `ready`/still result. **v0.10.0** added a **tube-mesh memory budget** so large forced-`tubes` renders (≈1.6 M+ segments) no longer OOM a 2 GB render worker — tubes decimate to a `TUBE_SEGMENT_BUDGET`, disclosed via `decimationApplied` (also on `RenderStillResult`), layer boundaries preserved ([RR-006](research/RR-006-tube-mesh-memory-and-large-file-budget.md)); plus a fix so `frameContent:'object'` frames the printed object on AnycubicSlicerNext and every object-labeling file (`objectBounds` is refreshed after dialect annotation). **v0.9.0** shipped the **interactive source-model viewer** `createModelViewer` in `@chestnutlabs/gcode-model-renderer` — the live orbit/zoom/pan analogue of `renderModelStill` for an STL or 3MF, over a shared interactive "stage" extracted in `gcode-renderer-three`, with an open-`kind` model-source loader registry (STL/3MF now, OBJ/STEP/PLY later without an API change), capability-honest color pass-through, and a `renderer-unsupported` fallback ([DD-021](design/DD-021-interactive-model-viewer.md)). v0.8.1 added production Bambu/Orca 3MF `paint_color` multicolor decode ([RR-005](research/RR-005-3mf-paint-color-facet-format.md)) so a designer's painted model renders in its true colors without slicing, plus a filament-slot dedup fix. v0.7.0 shipped the presentation package `@chestnutlabs/gcode-model-renderer` (headless `renderModelStill`, STL + 3MF multi-object/material, [DD-018](design/DD-018-model-renderer.md)), the 3MF Production-Extension (`p:path`) reader, and a toolpath-renderer batch — slice metadata on `ready`, structured multi-plate metadata, independent build-volume cage, frame-to-content, interaction-aware render quality ([DD-020](design/DD-020-interaction-aware-quality.md)), and a `renderStill` background option. v0.5.x added camera preset-views + serializable `CameraState`, zoom-to-cursor, keyboard-a11y, `capabilities`/`warnings` on `ready`, and the Vue/Svelte adapter mount-prop fixes (epics #279/#296/#306/DD-021). DD-017 RS274NGC parametric programs proposed, unbuilt) | Epic | Status | Gate | |---|---|---| @@ -67,7 +67,7 @@ golden/native fixtures + manifest with CI validation, boundary lint, license CI consumer smoke harnesses, plus the five framework adapters (`gcode-preview-core`/`-vue`/`-react`/ `-svelte`/`-element`) and the presentation renderer (`gcode-model-renderer`). CI (`build`) is a required check on `main`/`dev`. **All fourteen `@chestnutlabs/*` packages are published to npm at -`v0.20.0`** (lockstep, npm provenance); `main` is the release branch, publishing from tagged GitHub +`v0.20.1`** (lockstep, npm provenance); `main` is the release branch, publishing from tagged GitHub Releases (`v0.1.0` E7/DD-008, `v0.2.0` E9/DD-009, `v0.3.0` E8/#188, `v0.4.0` #189 non-extrusion, `v0.5.x` hardening #279, `v0.6.0`/`v0.7.0` ModelRenderer + toolpath batch #296/#306, `v0.8.1` 3MF `paint_color` multicolor decode, `v0.9.0` interactive `createModelViewer` #296/DD-021, `v0.10.0` @@ -85,5 +85,7 @@ Visual Feature Coverage Matrix, and the enforced per-release Product/Docs/Visual + `` on each framework's `/model` subpath (`` for the Web Component), the toolpath dark-capability closure (`getRenderStats`/`pickSegment`/`isColorModeAvailable`/declarative `hiddenFeatureRoles`/handle `capture`) + `capture({ includeBuildVolume })`, the Feature Lab demo, and -two-tier framework examples DD-031). +two-tier framework examples DD-031; `v0.20.1` correctness patch — sRGB `capture()` colour-space fix, +session double-`cancel()` idempotency, geometry-pool dispose-mid-build, React `` unmount +disposal, and `` connect/reconnect lifecycle). Changesets accumulate on `dev` for the next release. diff --git a/docs/VISUAL_FEATURE_COVERAGE.md b/docs/VISUAL_FEATURE_COVERAGE.md index b2e0c21..3f2ee80 100644 --- a/docs/VISUAL_FEATURE_COVERAGE.md +++ b/docs/VISUAL_FEATURE_COVERAGE.md @@ -118,6 +118,32 @@ capabilities, so the per-capability rows below are unchanged): look. App-UI screenshots of Prepare mode were captured for review but, like the toolpath showcase shots, are not yet canonical `docs/media` (same harness gap). +## This pass (2026-08-30, v0.20.0 docs/demo parity) — RESOLVES the app-UI + demo gaps above + +The two deferrals from the previous pass (app-UI media not canonical; interactive demos not published) +are now closed: + +- **App-UI screenshots are now canonical + reproducible.** A new manifest-driven harness + (`tools/screenshots/capture-app.mjs` + `app-shots.manifest.json`, run via `npm run docs:shots:app`) + captures the actual Feature Lab application surface — studio-dark chrome — by compositing the real + render into the DOM. Committed shots: `app-control-panel` (the lead controls shot, replaces the old + mid-grey `app-control-panel`), `app-appearance` (color modes), `app-diagnostics` (capabilities + + `getRenderStats`, replaces the stale pre-Feature-Lab `diagnostics-panel`), `app-prepare` (Prepare / + source model). These are the *application* look (dark), deliberately distinct from the mid-grey + capability renders — both are now first-class classes in the media set. +- **Live interactive demos are published to GitHub Pages** (`tools/docs/build-demos.mjs`, `npm run + docs:demos`, wired into `docs-pages.yml`): the Feature Lab and all four framework showcases + minimal + examples under `/demos/*`, on the published SDK, with a product-facing landing at `/demos/`. The + README, Pages homepage, and feature gallery now lead with "Try the live demo" and the interactive + application surface, not only renders. +- **README/Pages refreshed against v0.20.0:** the README hero is now the Feature Lab UI; a Prepare + + Preview pairing and a controls/capabilities pairing appear early; the homepage hero + a demos + banner + a Prepare/Preview card were added; the gallery gained a "Try it live" section and swapped + the stale `diagnostics-panel` for `app-diagnostics`. +- **Reproducibility / release gate:** `docs:shots` (capability renders) and `docs:shots:app` (app-UI) + regenerate the media; the RELEASE_REVIEW Visual disposition now explicitly covers app-UI + live-demo + parity so a future cut can't advertise a stale UI (see the release template). + ## 1. Toolpath inspection | Capability | User-facing | Demo | Media | README | Pages gallery | Manual/pkg | Status | @@ -169,7 +195,7 @@ capabilities, so the per-capability rows below are unchanged): | Renderer build stages (`stage` event) | ✅ | ❌ | — | ✅ (prose) | gallery | concept-workers | needs animation | | Interaction-aware quality | ✅ | ❌ | — | ✅ (prose) | gallery | concept-workers | needs animation | | Disclosed decimation / degradation | ✅ | ✅ (disclosure) | — | ✅ (prose) | gallery | concept-workers | needs screenshot (disclosure text) | -| Render diagnostics (`getRenderStats`) | ✅ | ✅ (panel) | `diagnostics-panel` | ✅ | gallery | concept-workers | covered — panel-in-context screenshot in the gallery | +| Render diagnostics (`getRenderStats`) | ✅ | ✅ (panel) | `app-diagnostics` | ✅ | gallery | concept-workers | covered — current Feature Lab Diagnostics tab (capability badges + stats) in the gallery | | Geometry worker pool (parallel tubes) | ✅ | ❌ | — | mention | gallery | concept-workers | not visually meaningful (diagnostics panel proxies it) | | WebGL context-loss recovery | ✅ | ❌ | — | mention | — | — | not visually meaningful | @@ -193,7 +219,7 @@ capabilities, so the per-capability rows below are unchanged): | STL presentation still | ✅ | model page | (in pair) | ✅ | gallery | model-renderer README | covered | | 3MF material/color presentation | ✅ | model page | (in pair) | ✅ | gallery | model-renderer README | covered | | Interactive model viewer (`createModelViewer`) | ✅ | model-viewer page + Feature Lab (Prepare) | — | ✅ (prose) | gallery | model-renderer README | covered (interactive) — Feature Lab + all 4 showcases have a Prepare mode; animation still nice-to-have | -| Model-viewer framework adapters (`` / `/model`) | ✅ | Feature Lab + 4 showcases (Prepare) + model.html minimals | — | ✅ (table) | — | adapters + 4 READMEs | covered (code + examples); app-UI media pending harness support | +| Model-viewer framework adapters (`` / `/model`) | ✅ | Feature Lab + 4 showcases (Prepare) + model.html minimals + **live demos on Pages** | `app-prepare` | ✅ (table + README pairing) | homepage + gallery | adapters + 4 READMEs | covered — app-UI shot + running demos | | Multi-object files | ✅ | model page | — | mention | gallery | — | needs screenshot | | Multi-plate files + per-plate render | ✅ | ❌ | — | ❌ | gallery | — | needs demo exposure + needs comparison | | Render scope / object subsets (`RenderScope`) | ✅ | ❌ | — | ❌ | gallery | recipes | needs demo exposure + needs comparison | @@ -235,8 +261,8 @@ capabilities, so the per-capability rows below are unchanged): | Capability | User-facing | Demo | Media | README | Pages gallery | Manual/pkg | Status | |---|:---:|:---:|:---:|:---:|:---:|:---:|---| -| Vue / React / Svelte / Web Component adapters | ✅ | two-tier examples + Feature Lab | — | ✅ (table) | — | adapters | covered (code + examples) — each adapter has a minimal + showcase example on the shared design language; canonical app-UI media pending harness support | -| Capability-aware consumer UX (gated modes + reasons, confidence tiers, contextual controls) | ✅ | Feature Lab + 4 showcases | — | ✅ (prose) | — | adapters | demonstrated in demo + examples; review captures exist (dark app UI), not yet canonical mid-grey media | +| Vue / React / Svelte / Web Component adapters | ✅ | two-tier examples + Feature Lab + **live demos on Pages** | `app-control-panel` | ✅ (table) | homepage + gallery | adapters | covered — each adapter has a minimal + showcase example, all runnable on the Pages demos site | +| Capability-aware consumer UX (gated modes + reasons, confidence tiers, contextual controls) | ✅ | Feature Lab + 4 showcases | `app-appearance`, `app-diagnostics` | ✅ | gallery + README | adapters | covered — app-UI shots show gated modes + capability badges (captured by capture-app.mjs) | | Headless toolpath still (`renderStill`) | ✅ | still page | — | ✅ | gallery | reference/still-render | needs screenshot (server-thumbnail context) | | Headless model still (`renderModelStill`) | ✅ | model page | `model-render-stl-3mf` | ✅ | gallery | model-renderer README | covered | | Off-thread parsing / cancellation / limits | ✅ | ✅ | — | ✅ (prose) | — | concept-workers | not visually meaningful | diff --git a/docs/manual/feature-gallery.md b/docs/manual/feature-gallery.md index 2a14230..ca7f6d4 100644 --- a/docs/manual/feature-gallery.md +++ b/docs/manual/feature-gallery.md @@ -14,6 +14,32 @@ on a neutral slicer/CAD viewport — nothing mocked. This page is the visual com > New here? Start with the [getting-started guide](index.md), then come back to browse the breadth. +> The capability shots below are **real renders** on a neutral mid-grey documentation viewport. The +> application shots in this first section show the **live Feature Lab** in its studio-dark chrome — +> the actual interactive product. + +## Try it live — Prepare & Preview + +**▶ [Open the live demos](https://chestnutlabs.github.io/gcode-preview/demos/)** — the interactive +[Feature Lab](https://chestnutlabs.github.io/gcode-preview/demos/feature-lab/index.html) and the +[React](https://chestnutlabs.github.io/gcode-preview/demos/react/showcase.html) / +[Vue](https://chestnutlabs.github.io/gcode-preview/demos/vue/showcase.html) / +[Svelte](https://chestnutlabs.github.io/gcode-preview/demos/svelte/showcase.html) / +[Web Component](https://chestnutlabs.github.io/gcode-preview/demos/webcomponent/showcase.html) +examples, running in your browser on the published `@chestnutlabs/*` packages. + +The SDK covers **both halves** of looking at a print job — the sliced toolpath (Preview) and the +source model (Prepare), each with real controls: + +| Preview — the sliced toolpath | Prepare — the source model | +|---|---| +| ![The Feature Lab in Preview mode: a 3DBenchy colored by feature role with the inspector rail, feature legend, camera controls, and layer/segment scrub](../media/app-control-panel.png) | ![The Feature Lab in Prepare mode: a colored 3MF source model with a Source Model panel reporting objects, placements, and materials: known](../media/app-prepare.png) | +| Layers, features, speed, travel, retractions, source-line picking — the toolpath controls. | STL / 3MF solid model — objects, materials, model controls, honest material-color tier. | + +| Color modes, capability-gated | Render diagnostics & capabilities | +|---|---| +| ![The Feature Lab Appearance tab: a color-by selector with the active feature-role legend; unsupported modes greyed out](../media/app-appearance.png) | ![The Feature Lab Diagnostics tab: capability badges and getRenderStats output](../media/app-diagnostics.png) | + ## Toolpath inspection The core job: turn a sliced file into an interactive picture of the actual moves the machine makes. @@ -88,7 +114,7 @@ clean `hold` reveal**, staged preparation progress, and **`getRenderStats()`** d hardware-vs-software GPU, draw calls, timings — never fabricated). See [workers, streaming & performance](concept-workers.md). -![The demo's Render diagnostics panel populated beside the render — backend 3d-webgl (WebGL 2), software ANGLE/SwiftShader GPU, geometry tubes, segment and draw counts, tube bytes, and timings](../media/diagnostics-panel.png) +![The Feature Lab Diagnostics tab: capability confidence badges and getRenderStats() output beside the render — backend 3d-webgl (WebGL 2), software ANGLE/SwiftShader GPU, geometry tubes, segment and draw-call counts, vertex count, tube bytes, worker pool, and timings](../media/app-diagnostics.png) *`getRenderStats()` in context — every value read from the actual render, never fabricated; here reporting a software (SwiftShader) backend, `108,725 / 108,729` segments, 92 draw calls, and the @@ -126,8 +152,11 @@ STL is a single neutral object; **3MF** brings multi-object structure and per-ob **material colors** — and when the source *doesn't* declare colors, the render says `materials: 'unavailable'` rather than inventing one. A 3MF project can hold several **plates** and many **objects**: render **one plate at a time** and narrow to a **render scope** (a plate, or a -subset of objects) for a single thumbnail. Headless `renderModelStill` and interactive -`createModelViewer` share the look. See the +subset of objects) for a single thumbnail. Headless `renderModelStill` and the interactive viewer +share the look — and as of v0.20.0 the interactive viewer is a **first-class framework adapter**: +`` on every framework's `/model` subpath (`` for the Web Component), +the Prepare side shown at the top of this page. See the +[adapters guide](adapters.md) and the [model-renderer README](https://github.com/ChestnutLabs/gcode-preview/blob/dev/packages/gcode-model-renderer/README.md). ## Machine geometry @@ -156,9 +185,11 @@ Canned drilling cycles (`G81`/`G82`/`G83`) expand; controller support is **hones ## Framework integration -The whole viewer ships as drop-in **Vue, React, Svelte, and Web Component** components over one shared -engine, plus a lower-level API (composable / hook / store / action) for building your own controls, -and a headless `renderStill` for server thumbnails. See [framework adapters](adapters.md). +Both viewers ship as drop-in **Vue, React, Svelte, and Web Component** components over one shared +engine — the toolpath `` from the package root and the source-model `` from +the `/model` subpath — plus a lower-level API (composable / hook / store / action) for building your +own controls, and a headless `renderStill` for server thumbnails. Try them in the +[live demos](https://chestnutlabs.github.io/gcode-preview/demos/); see [framework adapters](adapters.md). --- diff --git a/docs/manual/index.md b/docs/manual/index.md index 73107ad..bfc90c0 100644 --- a/docs/manual/index.md +++ b/docs/manual/index.md @@ -3,18 +3,25 @@

Show a 3D print or CNC/laser job in the browser — before, during, or after it runs.

Drop .gcode, .gcode.3mf, or Prusa .bgcode into a page and get an interactive toolpath: orbit it, clip to a layer, scrub it, color by feature or speed, overlay live progress. Parsing runs off the main thread, so a 250 MB file never freezes the UI — with Vue, React, Svelte, and Web Component adapters over one engine.

- 3DBenchy rendered as extrusion tubes with per-feature coloring on a neutral grey build plate + The G-code Preview Feature Lab application — a 3DBenchy colored by feature role with the inspector rail, color-mode legend, camera controls, layer/segment scrub, and capability badges
+

▶ Try the live demos — the interactive Feature Lab, and React / Vue / Svelte / Web Component examples, running in your browser on the published SDK.

+
    +
  • + +

    Prepare & Preview

    Two viewers, one SDK: the sliced-G-code toolpath and the source model (STL / 3MF) — a <ModelViewer> on every framework's /model subpath.

    +
  • Toolpath inspection

    Layer clip, segment & time scrub, travel / retraction / seam markers, source-line mapping.

    @@ -107,6 +114,6 @@ viewer is reachable without switching APIs. [Parametric programs (RS274NGC)](concept-parametric-programs.md). - **API reference** — pick a package in the sidebar (`toolpath-core`, `gcode-parser`, …). -> **Status:** published — all fourteen `@chestnutlabs/*` packages are on npm (latest `v0.20.0`, +> **Status:** published — all fourteen `@chestnutlabs/*` packages are on npm (latest `v0.20.1`, > lockstep-versioned with provenance). Governance and epic status live in the > [repository docs](https://github.com/ChestnutLabs/gcode-preview/tree/dev/docs). diff --git a/docs/media/app-appearance.png b/docs/media/app-appearance.png new file mode 100644 index 0000000..708ac81 Binary files /dev/null and b/docs/media/app-appearance.png differ diff --git a/docs/media/app-control-panel.png b/docs/media/app-control-panel.png index e466eb7..e5a14b7 100644 Binary files a/docs/media/app-control-panel.png and b/docs/media/app-control-panel.png differ diff --git a/docs/media/app-diagnostics.png b/docs/media/app-diagnostics.png new file mode 100644 index 0000000..08f0838 Binary files /dev/null and b/docs/media/app-diagnostics.png differ diff --git a/docs/media/app-prepare.png b/docs/media/app-prepare.png new file mode 100644 index 0000000..34cc0fc Binary files /dev/null and b/docs/media/app-prepare.png differ diff --git a/docs/reference/goldens/README.md b/docs/reference/goldens/README.md new file mode 100644 index 0000000..3e6eb4f --- /dev/null +++ b/docs/reference/goldens/README.md @@ -0,0 +1,51 @@ +# `capture()` colour-space golden — v0.20.1 + +Validation reference for the **v0.20.1** fix to `controls.capture()` / `ModelViewer.capture()` +(issue: interactive capture read back **linear** pixels instead of sRGB, so captured thumbnails were +too dark). This is the artifact AnyBridge (and any consumer using `capture()` for thumbnails) should +validate a regenerated thumbnail against. + +## What changed + +The interactive capture path rendered the scene into an off-screen `WebGLRenderTarget` whose texture +did not declare sRGB, so `readRenderTargetPixels` returned **linear-space** bytes — no sRGB output +encoding, unlike the canvas and unlike `renderStill`. v0.20.1 sets the capture target's texture to +`SRGBColorSpace`, so a captured image now matches the on-screen view. + +Fix: `packages/gcode-renderer-three/src/interactive-stage.ts` (`capture()` → `target.texture.colorSpace = SRGBColorSpace`). + +## Validation values (GPU-independent) + +Validate **by sampling a flat interior region and comparing the colour value** — not by hashing the +PNG. A pixel-exact hash is not portable: these reference images were rendered on SwiftShader (software +WebGL), so edge antialiasing differs from a hardware GPU (e.g. AnyBridge's RTX 4070). Flat interior +regions and the background are colour-stable across GPUs. + +| Region | Input (sRGB) | After fix (v0.20.1) | Before fix (buggy, linear) | +|---|---|---|---| +| Scene background | `#6d7176` | **`#6d7176`** ✅ (round-trips) | `#272a2e` ❌ (too dark) | + +General mapping (per 8-bit channel) for any solid colour `C` that was affected before the fix: + +``` +before = round( sRGB_to_linear(C/255) * 255 ) + where sRGB_to_linear(s) = s <= 0.04045 ? s/12.92 : ((s + 0.055)/1.055) ^ 2.4 +``` + +So after the fix, a flat region whose source colour is sRGB `C` reads back as `C` (was the darker +`before(C)`). AnyBridge's sidecar calls `capture({ background: 'transparent' })`: the background alpha +was always correct (a clean cutout); it was the **geometry/feature colours** that were darkened, and +those are now sRGB-correct. + +## The reference captures + +Both were produced through the real render-to-target `capture()` path via +[`tools/screenshots/capture-golden.mjs`](../../../tools/screenshots/capture-golden.mjs) (run against +the Feature Lab on `:5199`), on its default fixture in feature-role colour mode: + +- **`capture-golden-grey.png`** — `capture({ background: '#6d7176' })`. The background samples exactly + `#6d7176`; use it to confirm the background round-trips. +- **`capture-golden-transparent.png`** — `capture({ background: 'transparent' })`, AnyBridge's exact + call. Transparent background; the toolpath/feature colours are the sRGB-correct values. + +Regenerate with `node tools/screenshots/capture-golden.mjs` (Feature Lab running on `:5199`). diff --git a/docs/reference/goldens/capture-golden-grey.png b/docs/reference/goldens/capture-golden-grey.png new file mode 100644 index 0000000..32a2a72 Binary files /dev/null and b/docs/reference/goldens/capture-golden-grey.png differ diff --git a/docs/reference/goldens/capture-golden-transparent.png b/docs/reference/goldens/capture-golden-transparent.png new file mode 100644 index 0000000..96db187 Binary files /dev/null and b/docs/reference/goldens/capture-golden-transparent.png differ diff --git a/package-lock.json b/package-lock.json index b7fca06..9ab3ef9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7389,11 +7389,11 @@ }, "packages/gcode-bgcode": { "name": "@chestnutlabs/gcode-bgcode", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-containers": "0.20.0", - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/gcode-containers": "0.20.1", + "@chestnutlabs/toolpath-core": "0.20.1" }, "engines": { "node": ">=22" @@ -7401,10 +7401,10 @@ }, "packages/gcode-colors": { "name": "@chestnutlabs/gcode-colors", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/toolpath-core": "0.20.1" }, "engines": { "node": ">=22" @@ -7412,10 +7412,10 @@ }, "packages/gcode-containers": { "name": "@chestnutlabs/gcode-containers", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/toolpath-core": "0.20.1" }, "engines": { "node": ">=22" @@ -7423,10 +7423,10 @@ }, "packages/gcode-dialects": { "name": "@chestnutlabs/gcode-dialects", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/toolpath-core": "0.20.1" }, "engines": { "node": ">=22" @@ -7434,12 +7434,12 @@ }, "packages/gcode-model-renderer": { "name": "@chestnutlabs/gcode-model-renderer", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-containers": "0.20.0", - "@chestnutlabs/gcode-renderer-three": "0.20.0", - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/gcode-containers": "0.20.1", + "@chestnutlabs/gcode-renderer-three": "0.20.1", + "@chestnutlabs/toolpath-core": "0.20.1" }, "devDependencies": { "@types/three": "0.178.0" @@ -7453,13 +7453,13 @@ }, "packages/gcode-parser": { "name": "@chestnutlabs/gcode-parser", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-bgcode": "0.20.0", - "@chestnutlabs/gcode-containers": "0.20.0", - "@chestnutlabs/gcode-dialects": "0.20.0", - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/gcode-bgcode": "0.20.1", + "@chestnutlabs/gcode-containers": "0.20.1", + "@chestnutlabs/gcode-dialects": "0.20.1", + "@chestnutlabs/toolpath-core": "0.20.1" }, "engines": { "node": ">=22" @@ -7467,13 +7467,13 @@ }, "packages/gcode-preview-core": { "name": "@chestnutlabs/gcode-preview-core", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-parser": "0.20.0", - "@chestnutlabs/gcode-renderer-2d": "0.20.0", - "@chestnutlabs/gcode-renderer-three": "0.20.0", - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/gcode-parser": "0.20.1", + "@chestnutlabs/gcode-renderer-2d": "0.20.1", + "@chestnutlabs/gcode-renderer-three": "0.20.1", + "@chestnutlabs/toolpath-core": "0.20.1" }, "engines": { "node": ">=22" @@ -7481,14 +7481,14 @@ }, "packages/gcode-preview-element": { "name": "@chestnutlabs/gcode-preview-element", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-model-renderer": "0.20.0", - "@chestnutlabs/gcode-parser": "0.20.0", - "@chestnutlabs/gcode-preview-core": "0.20.0", - "@chestnutlabs/gcode-renderer-three": "0.20.0", - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/gcode-model-renderer": "0.20.1", + "@chestnutlabs/gcode-parser": "0.20.1", + "@chestnutlabs/gcode-preview-core": "0.20.1", + "@chestnutlabs/gcode-renderer-three": "0.20.1", + "@chestnutlabs/toolpath-core": "0.20.1" }, "engines": { "node": ">=22" @@ -7496,14 +7496,14 @@ }, "packages/gcode-preview-react": { "name": "@chestnutlabs/gcode-preview-react", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-model-renderer": "0.20.0", - "@chestnutlabs/gcode-parser": "0.20.0", - "@chestnutlabs/gcode-preview-core": "0.20.0", - "@chestnutlabs/gcode-renderer-three": "0.20.0", - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/gcode-model-renderer": "0.20.1", + "@chestnutlabs/gcode-parser": "0.20.1", + "@chestnutlabs/gcode-preview-core": "0.20.1", + "@chestnutlabs/gcode-renderer-three": "0.20.1", + "@chestnutlabs/toolpath-core": "0.20.1" }, "devDependencies": { "@types/react": "^18.3.0", @@ -7519,14 +7519,14 @@ }, "packages/gcode-preview-svelte": { "name": "@chestnutlabs/gcode-preview-svelte", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-model-renderer": "0.20.0", - "@chestnutlabs/gcode-parser": "0.20.0", - "@chestnutlabs/gcode-preview-core": "0.20.0", - "@chestnutlabs/gcode-renderer-three": "0.20.0", - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/gcode-model-renderer": "0.20.1", + "@chestnutlabs/gcode-parser": "0.20.1", + "@chestnutlabs/gcode-preview-core": "0.20.1", + "@chestnutlabs/gcode-renderer-three": "0.20.1", + "@chestnutlabs/toolpath-core": "0.20.1" }, "devDependencies": { "svelte": "^4.2.0" @@ -7540,14 +7540,14 @@ }, "packages/gcode-preview-vue": { "name": "@chestnutlabs/gcode-preview-vue", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-model-renderer": "0.20.0", - "@chestnutlabs/gcode-parser": "0.20.0", - "@chestnutlabs/gcode-preview-core": "0.20.0", - "@chestnutlabs/gcode-renderer-three": "0.20.0", - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/gcode-model-renderer": "0.20.1", + "@chestnutlabs/gcode-parser": "0.20.1", + "@chestnutlabs/gcode-preview-core": "0.20.1", + "@chestnutlabs/gcode-renderer-three": "0.20.1", + "@chestnutlabs/toolpath-core": "0.20.1" }, "devDependencies": { "vue": "^3.4.0" @@ -7561,11 +7561,11 @@ }, "packages/gcode-renderer-2d": { "name": "@chestnutlabs/gcode-renderer-2d", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-colors": "0.20.0", - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/gcode-colors": "0.20.1", + "@chestnutlabs/toolpath-core": "0.20.1" }, "engines": { "node": ">=22" @@ -7573,11 +7573,11 @@ }, "packages/gcode-renderer-three": { "name": "@chestnutlabs/gcode-renderer-three", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-colors": "0.20.0", - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/gcode-colors": "0.20.1", + "@chestnutlabs/toolpath-core": "0.20.1" }, "devDependencies": { "@types/three": "0.178.0" @@ -7591,7 +7591,7 @@ }, "packages/toolpath-core": { "name": "@chestnutlabs/toolpath-core", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "engines": { "node": ">=22" diff --git a/package.json b/package.json index 133b90f..1d53ba1 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,9 @@ "docs:api": "typedoc", "docs:api:watch": "typedoc --watch", "docs:build": "node tools/docs/build-api.mjs", + "docs:demos": "node tools/docs/build-demos.mjs", + "docs:shots": "node tools/screenshots/capture.mjs", + "docs:shots:app": "node tools/screenshots/capture-app.mjs", "changeset": "changeset", "version": "changeset version && node tools/release/sync-internal-ranges.mjs && node tools/release/stamp-release-docs.mjs && npm install --package-lock-only --no-audit --no-fund", "release:sync-check": "node tools/release/sync-internal-ranges.mjs --check", diff --git a/packages/gcode-bgcode/CHANGELOG.md b/packages/gcode-bgcode/CHANGELOG.md index 05afa4d..a4cd20c 100644 --- a/packages/gcode-bgcode/CHANGELOG.md +++ b/packages/gcode-bgcode/CHANGELOG.md @@ -1,5 +1,13 @@ # @chestnutlabs/gcode-bgcode +## 0.20.1 + +### Patch Changes + +- Updated dependencies []: + - @chestnutlabs/gcode-containers@0.20.1 + - @chestnutlabs/toolpath-core@0.20.1 + ## 0.20.0 ### Patch Changes diff --git a/packages/gcode-bgcode/package.json b/packages/gcode-bgcode/package.json index ee67352..f65befd 100644 --- a/packages/gcode-bgcode/package.json +++ b/packages/gcode-bgcode/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/gcode-bgcode", - "version": "0.20.0", + "version": "0.20.1", "description": "Binary G-code (.bgcode) decode adapter for the Chestnut Labs G-code toolpath stack (DD-011): a license-clean, in-memory block walker that decodes Prusa .bgcode to plain G-code for the existing parser/dialect/renderer pipeline. Decode-only.", "keywords": [ "gcode", @@ -47,7 +47,7 @@ "test": "vitest run" }, "dependencies": { - "@chestnutlabs/toolpath-core": "0.20.0", - "@chestnutlabs/gcode-containers": "0.20.0" + "@chestnutlabs/toolpath-core": "0.20.1", + "@chestnutlabs/gcode-containers": "0.20.1" } } diff --git a/packages/gcode-colors/CHANGELOG.md b/packages/gcode-colors/CHANGELOG.md index 60aa8c7..116dbc5 100644 --- a/packages/gcode-colors/CHANGELOG.md +++ b/packages/gcode-colors/CHANGELOG.md @@ -1,5 +1,12 @@ # @chestnutlabs/gcode-colors +## 0.20.1 + +### Patch Changes + +- Updated dependencies []: + - @chestnutlabs/toolpath-core@0.20.1 + ## 0.20.0 ### Patch Changes diff --git a/packages/gcode-colors/package.json b/packages/gcode-colors/package.json index 6f349fc..feee8c6 100644 --- a/packages/gcode-colors/package.json +++ b/packages/gcode-colors/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/gcode-colors", - "version": "0.20.0", + "version": "0.20.1", "description": "Renderer-agnostic per-segment color model for the Chestnut Labs G-code toolpath stack (DD-014 D3): the ColorMode union and honest, capability-gated segment coloring over ToolpathIR channels, shared by the 3D and 2D renderers.", "keywords": [ "gcode", @@ -47,6 +47,6 @@ "test": "vitest run" }, "dependencies": { - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/toolpath-core": "0.20.1" } } diff --git a/packages/gcode-containers/CHANGELOG.md b/packages/gcode-containers/CHANGELOG.md index 6ce5c0a..6d1e9c2 100644 --- a/packages/gcode-containers/CHANGELOG.md +++ b/packages/gcode-containers/CHANGELOG.md @@ -1,5 +1,12 @@ # @chestnutlabs/gcode-containers +## 0.20.1 + +### Patch Changes + +- Updated dependencies []: + - @chestnutlabs/toolpath-core@0.20.1 + ## 0.20.0 ### Patch Changes diff --git a/packages/gcode-containers/package.json b/packages/gcode-containers/package.json index 020d7a6..21728e5 100644 --- a/packages/gcode-containers/package.json +++ b/packages/gcode-containers/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/gcode-containers", - "version": "0.20.0", + "version": "0.20.1", "description": "Safe, bounded, in-memory container extraction for sliced G-code (.gcode.3mf) — DD-005 §4.4/§7. Never writes files, never fetches, zero dependencies.", "keywords": [ "gcode", @@ -42,6 +42,6 @@ "test": "vitest run" }, "dependencies": { - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/toolpath-core": "0.20.1" } } diff --git a/packages/gcode-dialects/CHANGELOG.md b/packages/gcode-dialects/CHANGELOG.md index bf7af41..d208f4f 100644 --- a/packages/gcode-dialects/CHANGELOG.md +++ b/packages/gcode-dialects/CHANGELOG.md @@ -1,5 +1,12 @@ # @chestnutlabs/gcode-dialects +## 0.20.1 + +### Patch Changes + +- Updated dependencies []: + - @chestnutlabs/toolpath-core@0.20.1 + ## 0.20.0 ### Patch Changes diff --git a/packages/gcode-dialects/package.json b/packages/gcode-dialects/package.json index c8973b3..1ce2598 100644 --- a/packages/gcode-dialects/package.json +++ b/packages/gcode-dialects/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/gcode-dialects", - "version": "0.20.0", + "version": "0.20.1", "description": "Slicer/firmware dialect adapters for ToolpathIR annotation (DD-005). Adapters annotate metadata and optional channels — they can never alter geometry.", "keywords": [ "gcode", @@ -45,6 +45,6 @@ "test": "vitest run" }, "dependencies": { - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/toolpath-core": "0.20.1" } } diff --git a/packages/gcode-model-renderer/CHANGELOG.md b/packages/gcode-model-renderer/CHANGELOG.md index 99a8a01..7d2eea2 100644 --- a/packages/gcode-model-renderer/CHANGELOG.md +++ b/packages/gcode-model-renderer/CHANGELOG.md @@ -1,5 +1,14 @@ # @chestnutlabs/gcode-model-renderer +## 0.20.1 + +### Patch Changes + +- Updated dependencies [[`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e), [`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e)]: + - @chestnutlabs/gcode-renderer-three@0.20.1 + - @chestnutlabs/gcode-containers@0.20.1 + - @chestnutlabs/toolpath-core@0.20.1 + ## 0.20.0 ### Minor Changes diff --git a/packages/gcode-model-renderer/package.json b/packages/gcode-model-renderer/package.json index b0aa9be..39d72db 100644 --- a/packages/gcode-model-renderer/package.json +++ b/packages/gcode-model-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/gcode-model-renderer", - "version": "0.20.0", + "version": "0.20.1", "description": "Three.js presentation renderer for source models (STL/3MF) — clean thumbnails/cards, distinct from toolpath inspection (DD-018).", "keywords": [ "3d-printing", @@ -55,9 +55,9 @@ "test": "vitest run" }, "dependencies": { - "@chestnutlabs/gcode-containers": "0.20.0", - "@chestnutlabs/gcode-renderer-three": "0.20.0", - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/gcode-containers": "0.20.1", + "@chestnutlabs/gcode-renderer-three": "0.20.1", + "@chestnutlabs/toolpath-core": "0.20.1" }, "devDependencies": { "@types/three": "0.178.0" diff --git a/packages/gcode-parser/CHANGELOG.md b/packages/gcode-parser/CHANGELOG.md index c331189..f19bf0c 100644 --- a/packages/gcode-parser/CHANGELOG.md +++ b/packages/gcode-parser/CHANGELOG.md @@ -1,5 +1,17 @@ # @chestnutlabs/gcode-parser +## 0.20.1 + +### Patch Changes + +- [`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e) Thanks [@sobechestnut-dev](https://github.com/sobechestnut-dev)! - Fix a parse-session hang on a double `cancel()`. A second `cancel()` while a cancel was already pending (e.g. the controller cancelling before a re-parse) overwrote the terminate backstop timer and orphaned the first — which could later terminate an unrelated in-flight parse and wedge the controller at `parsing: true`. `cancel()` is now idempotent while a cancel is already pending. + +- Updated dependencies []: + - @chestnutlabs/gcode-bgcode@0.20.1 + - @chestnutlabs/gcode-containers@0.20.1 + - @chestnutlabs/gcode-dialects@0.20.1 + - @chestnutlabs/toolpath-core@0.20.1 + ## 0.20.0 ### Patch Changes diff --git a/packages/gcode-parser/package.json b/packages/gcode-parser/package.json index 2bda8c7..334197b 100644 --- a/packages/gcode-parser/package.json +++ b/packages/gcode-parser/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/gcode-parser", - "version": "0.20.0", + "version": "0.20.1", "description": "Worker-safe G-code parse core producing ToolpathIR (DD-003).", "keywords": [ "gcode", @@ -49,9 +49,9 @@ "test": "vitest run" }, "dependencies": { - "@chestnutlabs/toolpath-core": "0.20.0", - "@chestnutlabs/gcode-dialects": "0.20.0", - "@chestnutlabs/gcode-containers": "0.20.0", - "@chestnutlabs/gcode-bgcode": "0.20.0" + "@chestnutlabs/toolpath-core": "0.20.1", + "@chestnutlabs/gcode-dialects": "0.20.1", + "@chestnutlabs/gcode-containers": "0.20.1", + "@chestnutlabs/gcode-bgcode": "0.20.1" } } diff --git a/packages/gcode-parser/src/__tests__/session-cancel.test.ts b/packages/gcode-parser/src/__tests__/session-cancel.test.ts new file mode 100644 index 0000000..8b595b1 --- /dev/null +++ b/packages/gcode-parser/src/__tests__/session-cancel.test.ts @@ -0,0 +1,68 @@ +/** + * Regression: GcodeParseSession.cancel() must be idempotent while a cancel is pending. A second + * cancel() (e.g. the core controller cancels before a re-parse) previously overwrote `cancelTimer`, + * orphaning the first backstop — which then fired later and terminated whatever parse was running by + * then, wedging the controller at parsing:true. cancel() now no-ops if a backstop is already pending. + */ +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; +import { GcodeParseSession, CancelledError, type WorkerLike } from '../session.js'; + +interface FakeWorker extends WorkerLike { + posted: unknown[]; + terminated: number; +} + +function makeFakeWorker(): FakeWorker { + const w: FakeWorker = { + posted: [], + terminated: 0, + onmessage: null, + onerror: null, + postMessage: (msg: unknown) => { + w.posted.push(msg); + }, + terminate: () => { + w.terminated++; + } + }; + return w; +} + +const cancelMsgs = (w: FakeWorker): unknown[] => + w.posted.filter( + (m): m is { type: string } => typeof m === 'object' && m !== null && (m as { type?: string }).type === 'cancel' + ); + +describe('GcodeParseSession — cancel idempotency (regression: orphaned cancelTimer)', () => { + beforeEach(() => vi.useFakeTimers()); + afterEach(() => vi.useRealTimers()); + + it('a second cancel() while a cancel is pending sends no second message and schedules no second backstop', async () => { + const worker = makeFakeWorker(); + const session = new GcodeParseSession({ worker, terminateFallbackMs: 2000 }); + // Worker never replies → the parse stays in-flight (active) so cancel() has something to cancel. + const settled = session.parse('G1 X1 E1').then( + () => 'resolved' as const, + (e: unknown) => e + ); + + session.cancel(); + session.cancel(); // must be a no-op — the first backstop is still pending + + expect(cancelMsgs(worker)).toHaveLength(1); // pre-fix this was 2 (and a second timer was orphaned) + + vi.advanceTimersByTime(2000); // fire the single backstop + expect(worker.terminated).toBe(1); + + const result = await settled; + expect(result).toBeInstanceOf(CancelledError); + expect((result as CancelledError).terminated).toBe(true); + }); + + it('cancel() with no active parse is a no-op', () => { + const worker = makeFakeWorker(); + const session = new GcodeParseSession({ worker, terminateFallbackMs: 2000 }); + expect(() => session.cancel()).not.toThrow(); + expect(worker.posted).toHaveLength(0); + }); +}); diff --git a/packages/gcode-parser/src/session.ts b/packages/gcode-parser/src/session.ts index c6424f6..a5fdfb8 100644 --- a/packages/gcode-parser/src/session.ts +++ b/packages/gcode-parser/src/session.ts @@ -123,6 +123,10 @@ export class GcodeParseSession { cancel(): void { const active = this.active; if (active === null || this.worker === null) return; + // Idempotent: a second cancel() on the same in-flight parse (e.g. the controller cancels before a + // re-parse) must not schedule a second backstop. Overwriting cancelTimer would orphan the first + // timer, which then fires later and terminates whatever parse is running by then — wedging it. + if (active.cancelTimer !== undefined) return; this.worker.postMessage({ v: PROTOCOL_VERSION, type: 'cancel', id: active.id }); active.cancelTimer = setTimeout(() => { // Last-resort backstop (§5.2): the cooperative path failed to acknowledge. diff --git a/packages/gcode-preview-core/CHANGELOG.md b/packages/gcode-preview-core/CHANGELOG.md index 259237d..6095e01 100644 --- a/packages/gcode-preview-core/CHANGELOG.md +++ b/packages/gcode-preview-core/CHANGELOG.md @@ -1,5 +1,15 @@ # @chestnutlabs/gcode-preview-core +## 0.20.1 + +### Patch Changes + +- Updated dependencies [[`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e), [`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e), [`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e)]: + - @chestnutlabs/gcode-renderer-three@0.20.1 + - @chestnutlabs/gcode-parser@0.20.1 + - @chestnutlabs/gcode-renderer-2d@0.20.1 + - @chestnutlabs/toolpath-core@0.20.1 + ## 0.20.0 ### Minor Changes diff --git a/packages/gcode-preview-core/package.json b/packages/gcode-preview-core/package.json index 25d148b..a9feb63 100644 --- a/packages/gcode-preview-core/package.json +++ b/packages/gcode-preview-core/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/gcode-preview-core", - "version": "0.20.0", + "version": "0.20.1", "description": "Framework-neutral preview controller for the Chestnut Labs G-code viewer (DD-007 §4.6): the shared engine glue, state model, and TypeScript contracts beneath the Vue/React/Svelte adapters.", "keywords": [ "gcode", @@ -52,9 +52,9 @@ "test": "vitest run" }, "dependencies": { - "@chestnutlabs/gcode-parser": "0.20.0", - "@chestnutlabs/gcode-renderer-2d": "0.20.0", - "@chestnutlabs/gcode-renderer-three": "0.20.0", - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/gcode-parser": "0.20.1", + "@chestnutlabs/gcode-renderer-2d": "0.20.1", + "@chestnutlabs/gcode-renderer-three": "0.20.1", + "@chestnutlabs/toolpath-core": "0.20.1" } } diff --git a/packages/gcode-preview-element/CHANGELOG.md b/packages/gcode-preview-element/CHANGELOG.md index 33b0121..da2adf4 100644 --- a/packages/gcode-preview-element/CHANGELOG.md +++ b/packages/gcode-preview-element/CHANGELOG.md @@ -1,5 +1,18 @@ # @chestnutlabs/gcode-preview-element +## 0.20.1 + +### Patch Changes + +- [`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e) Thanks [@sobechestnut-dev](https://github.com/sobechestnut-dev)! - Fix `` connect/disconnect defects: the initial `view` attribute (and a `cameraState` set before connect) are now applied on connect instead of being dropped, and `hidden-feature-roles` is re-applied after the element is moved/reconnected in the DOM (previously the hidden roles reappeared permanently on reconnect). + +- Updated dependencies [[`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e), [`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e), [`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e)]: + - @chestnutlabs/gcode-renderer-three@0.20.1 + - @chestnutlabs/gcode-parser@0.20.1 + - @chestnutlabs/gcode-model-renderer@0.20.1 + - @chestnutlabs/gcode-preview-core@0.20.1 + - @chestnutlabs/toolpath-core@0.20.1 + ## 0.20.0 ### Minor Changes diff --git a/packages/gcode-preview-element/package.json b/packages/gcode-preview-element/package.json index 088db29..747d287 100644 --- a/packages/gcode-preview-element/package.json +++ b/packages/gcode-preview-element/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/gcode-preview-element", - "version": "0.20.0", + "version": "0.20.1", "description": "Framework-free Web Component for the Chestnut Labs G-code viewer (DD-007 D1 / DD-009 D5): a custom element bridging @chestnutlabs/gcode-preview-core, no framework peer dependency.", "keywords": [ "gcode", @@ -60,10 +60,10 @@ "test": "vitest run" }, "dependencies": { - "@chestnutlabs/gcode-model-renderer": "0.20.0", - "@chestnutlabs/gcode-parser": "0.20.0", - "@chestnutlabs/gcode-preview-core": "0.20.0", - "@chestnutlabs/gcode-renderer-three": "0.20.0", - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/gcode-model-renderer": "0.20.1", + "@chestnutlabs/gcode-parser": "0.20.1", + "@chestnutlabs/gcode-preview-core": "0.20.1", + "@chestnutlabs/gcode-renderer-three": "0.20.1", + "@chestnutlabs/toolpath-core": "0.20.1" } } diff --git a/packages/gcode-preview-element/src/__tests__/element.test.ts b/packages/gcode-preview-element/src/__tests__/element.test.ts index 94fdf55..a956c8e 100644 --- a/packages/gcode-preview-element/src/__tests__/element.test.ts +++ b/packages/gcode-preview-element/src/__tests__/element.test.ts @@ -60,6 +60,44 @@ describe('gcode-preview custom element (#149)', () => { el.remove(); }); + it('applies the initial `view` attribute on connect (regression)', async () => { + defineGcodePreview(); + // `view` set as an attribute before connect: applyRuntimeState must apply it (attributeChangedCallback + // no-ops while controller === null). Before the fix it was dropped, so the camera stayed at the default. + const withTop = makeEl(document.createElement('canvas')); + withTop.setAttribute('view', 'top'); + document.body.appendChild(withTop); + await settle(); + const topCam = withTop.controls.getCameraState(); + + const dflt = makeEl(document.createElement('canvas')); + document.body.appendChild(dflt); + await settle(); + const defaultCam = dflt.controls.getCameraState(); + + expect(topCam).not.toEqual(defaultCam); // the top view actually reoriented the camera + withTop.remove(); + dflt.remove(); + }); + + it('re-applies hidden-feature-roles after a DOM reconnect (regression)', async () => { + defineGcodePreview(); + const el = makeEl(document.createElement('canvas')); + el.setAttribute('hidden-feature-roles', '6,7'); + el.source = new Uint8Array(1000); + document.body.appendChild(el); + await settle(); + expect([...el.raw.renderer()!.getHiddenFeatureRoles()].sort()).toEqual([6, 7]); + + el.remove(); + document.body.appendChild(el); // reconnect builds a FRESH (all-visible) controller + await settle(); + // Before the fix, prevHiddenRoles wasn't reset on disconnect, so the prev→next diff toggled nothing + // and skirt/brim reappeared permanently. It must be re-hidden on the fresh controller. + expect([...el.raw.renderer()!.getHiddenFeatureRoles()].sort()).toEqual([6, 7]); + el.remove(); + }); + it('applies a property set BEFORE connect, and leaks no workers across reconnect', async () => { defineGcodePreview(); const created0 = SuiteStubWorker.created; diff --git a/packages/gcode-preview-element/src/gcode-preview-element.ts b/packages/gcode-preview-element/src/gcode-preview-element.ts index a553f65..3a250fb 100644 --- a/packages/gcode-preview-element/src/gcode-preview-element.ts +++ b/packages/gcode-preview-element/src/gcode-preview-element.ts @@ -372,6 +372,10 @@ export class GcodePreviewElement extends HTMLElement { this.controller = null; this.canvasEl = null; this.shadowRoot?.replaceChildren(); + // A reconnect builds a FRESH controller with all roles visible. prevHiddenRoles tracks what the + // previous controller had hidden; if we don't reset it, the prev→next diff on re-apply sees no + // change and re-hides nothing, so hidden skirt/brim reappear permanently after a DOM move. + this.prevHiddenRoles = []; } attributeChangedCallback(name: string, _old: string | null, value: string | null): void { @@ -446,6 +450,13 @@ export class GcodePreviewElement extends HTMLElement { c.setScrubTime(this.scrubTime); this.applyHiddenRoles(this.hiddenFeatureRoles); this.applyProgress(); + // Camera framing set BEFORE connect: attributeChangedCallback/the cameraState setter both no-op + // while controller === null, so the initial `view` attribute and any pre-connect cameraState must + // be applied here or they're silently dropped (the element already does this). + // Apply the preset view first, then let an explicit cameraState override it. + const view = this.getAttribute('view'); + if (view !== null) c.setView(view as CameraView); + if (this._cameraState !== null) c.setCameraState(this._cameraState); } /** Feature-role visibility (DD-031 G3): diff prev→next so we only toggle roles this attribute owns. */ diff --git a/packages/gcode-preview-react/CHANGELOG.md b/packages/gcode-preview-react/CHANGELOG.md index 4dd3662..c112ce5 100644 --- a/packages/gcode-preview-react/CHANGELOG.md +++ b/packages/gcode-preview-react/CHANGELOG.md @@ -1,5 +1,18 @@ # @chestnutlabs/gcode-preview-react +## 0.20.1 + +### Patch Changes + +- [`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e) Thanks [@sobechestnut-dev](https://github.com/sobechestnut-dev)! - Fix `` / `useModelViewer` (React) leaking its controller on unmount. The hook now disposes the model-preview controller when the component unmounts — matching the toolpath hook and the Vue/Svelte/Web-Component adapters — so in-flight `setSource` operations settle and resources are released. + +- Updated dependencies [[`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e), [`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e), [`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e)]: + - @chestnutlabs/gcode-renderer-three@0.20.1 + - @chestnutlabs/gcode-parser@0.20.1 + - @chestnutlabs/gcode-model-renderer@0.20.1 + - @chestnutlabs/gcode-preview-core@0.20.1 + - @chestnutlabs/toolpath-core@0.20.1 + ## 0.20.0 ### Minor Changes diff --git a/packages/gcode-preview-react/package.json b/packages/gcode-preview-react/package.json index 404fb03..f7f956e 100644 --- a/packages/gcode-preview-react/package.json +++ b/packages/gcode-preview-react/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/gcode-preview-react", - "version": "0.20.0", + "version": "0.20.1", "description": "Thin React integration for the Chestnut Labs G-code viewer (DD-007 D1 amendment): useGcodePreview hook + GcodePreview component as a reactivity bridge over @chestnutlabs/gcode-preview-core.", "keywords": [ "gcode", @@ -53,11 +53,11 @@ "test": "vitest run" }, "dependencies": { - "@chestnutlabs/gcode-model-renderer": "0.20.0", - "@chestnutlabs/gcode-parser": "0.20.0", - "@chestnutlabs/gcode-preview-core": "0.20.0", - "@chestnutlabs/gcode-renderer-three": "0.20.0", - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/gcode-model-renderer": "0.20.1", + "@chestnutlabs/gcode-parser": "0.20.1", + "@chestnutlabs/gcode-preview-core": "0.20.1", + "@chestnutlabs/gcode-renderer-three": "0.20.1", + "@chestnutlabs/toolpath-core": "0.20.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" diff --git a/packages/gcode-preview-react/src/__tests__/model-dispose.test.ts b/packages/gcode-preview-react/src/__tests__/model-dispose.test.ts new file mode 100644 index 0000000..85452d8 --- /dev/null +++ b/packages/gcode-preview-react/src/__tests__/model-dispose.test.ts @@ -0,0 +1,61 @@ +// @vitest-environment happy-dom +/** + * Regression: the React / useModelViewer hook must DISPOSE its controller on unmount, + * matching the toolpath hook (and Vue/Svelte/Element). Previously only the GL engine was freed via the + * canvas-ref-null path; the controller was never disposed, so in-flight setSource promises never + * settled and the controller leaked. We spy on the controller factory and assert dispose() on unmount. + */ +import { describe, expect, it, vi } from 'vitest'; +import { act, createElement } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { MODEL_STUB_GL, MODEL_STUB_CONTROLS } from '@chestnutlabs/gcode-model-renderer/testing'; + +const spy = vi.hoisted(() => ({ disposes: [] as ReturnType[] })); + +vi.mock('@chestnutlabs/gcode-model-renderer', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createModelPreviewController: (opts?: Parameters[0]) => { + const controller = actual.createModelPreviewController(opts); + const disposeSpy = vi.fn(controller.dispose.bind(controller)); + controller.dispose = disposeSpy; + spy.disposes.push(disposeSpy); + return controller; + } + }; +}); + +// Import AFTER vi.mock so the hook binds to the spied factory. +const { useModelViewer } = await import('../model/index'); + +function Probe() { + const viewer = useModelViewer({ createRenderer: MODEL_STUB_GL, createControls: MODEL_STUB_CONTROLS }); + return createElement('canvas', { ref: viewer.canvasRef }); +} + +describe(' (react) — disposes its controller on unmount (regression)', () => { + it('calls controller.dispose() when the component unmounts', async () => { + (globalThis as Record).IS_REACT_ACT_ENVIRONMENT = true; + spy.disposes.length = 0; + const host = document.createElement('div'); + document.body.appendChild(host); + let root: Root | null = null; + await act(async () => { + root = createRoot(host); + root.render(createElement(Probe)); + }); + await act(async () => { + for (let i = 0; i < 4; i++) await new Promise((r) => setTimeout(r, 0)); + }); + expect(spy.disposes.length).toBeGreaterThan(0); // the hook created a controller through the factory + const disposedBefore = spy.disposes.some((d) => d.mock.calls.length > 0); + expect(disposedBefore).toBe(false); // still mounted → not disposed yet + + await act(async () => root?.unmount()); + host.remove(); + + const disposedAfter = spy.disposes.some((d) => d.mock.calls.length > 0); + expect(disposedAfter).toBe(true); // unmount must dispose the controller + }); +}); diff --git a/packages/gcode-preview-react/src/model/use-model-viewer.ts b/packages/gcode-preview-react/src/model/use-model-viewer.ts index 0200c48..bb09e41 100644 --- a/packages/gcode-preview-react/src/model/use-model-viewer.ts +++ b/packages/gcode-preview-react/src/model/use-model-viewer.ts @@ -5,7 +5,7 @@ * — never a separate viewer implementation (the D1 drift firewall). Shared contracts are re-exported * from the controller. */ -import { useCallback, useMemo, useRef, useSyncExternalStore } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'; import { createModelPreviewController, type ModelPreviewController, @@ -61,6 +61,23 @@ export function useModelViewer(options: UseModelViewerOptions = {}): ModelViewer const getSnapshot = useCallback(() => ensure().getState(), [ensure]); const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot); + // Own the controller's lifecycle: (re)create on mount (StrictMode's second setup lands here) and + // DISPOSE on unmount. Without this the GL engine was freed via the canvas-ref-null path but the + // controller was never disposed, so in-flight setSource promises never settled and the controller + // leaked — the toolpath hook already does this; the model hook must match. + useEffect(() => { + const c = ensure(); + // Refs attach BEFORE StrictMode's dispose/recreate cycle: rebind the delivered canvas to the fresh + // controller (refs never re-fire) so it renders again after a remount. + if (slot.current.canvas !== null && c.raw.viewer() === null) { + c.bindCanvas(slot.current.canvas); + } + return () => { + slot.current.controller?.dispose(); + slot.current.disposed = true; + }; + }, [ensure]); + const stateRef = useRef(state); stateRef.current = state; diff --git a/packages/gcode-preview-svelte/CHANGELOG.md b/packages/gcode-preview-svelte/CHANGELOG.md index 762f68e..c1c3b00 100644 --- a/packages/gcode-preview-svelte/CHANGELOG.md +++ b/packages/gcode-preview-svelte/CHANGELOG.md @@ -1,5 +1,16 @@ # @chestnutlabs/gcode-preview-svelte +## 0.20.1 + +### Patch Changes + +- Updated dependencies [[`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e), [`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e), [`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e)]: + - @chestnutlabs/gcode-renderer-three@0.20.1 + - @chestnutlabs/gcode-parser@0.20.1 + - @chestnutlabs/gcode-model-renderer@0.20.1 + - @chestnutlabs/gcode-preview-core@0.20.1 + - @chestnutlabs/toolpath-core@0.20.1 + ## 0.20.0 ### Minor Changes diff --git a/packages/gcode-preview-svelte/package.json b/packages/gcode-preview-svelte/package.json index 8ca0c71..a5f0b25 100644 --- a/packages/gcode-preview-svelte/package.json +++ b/packages/gcode-preview-svelte/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/gcode-preview-svelte", - "version": "0.20.0", + "version": "0.20.1", "description": "Thin Svelte integration for the Chestnut Labs G-code viewer (DD-007 D1 amendment): createGcodePreview store/action API + GcodePreview component as a reactivity bridge over @chestnutlabs/gcode-preview-core.", "keywords": [ "gcode", @@ -64,11 +64,11 @@ "test": "vitest run" }, "dependencies": { - "@chestnutlabs/gcode-model-renderer": "0.20.0", - "@chestnutlabs/gcode-parser": "0.20.0", - "@chestnutlabs/gcode-preview-core": "0.20.0", - "@chestnutlabs/gcode-renderer-three": "0.20.0", - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/gcode-model-renderer": "0.20.1", + "@chestnutlabs/gcode-parser": "0.20.1", + "@chestnutlabs/gcode-preview-core": "0.20.1", + "@chestnutlabs/gcode-renderer-three": "0.20.1", + "@chestnutlabs/toolpath-core": "0.20.1" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0" diff --git a/packages/gcode-preview-vue/CHANGELOG.md b/packages/gcode-preview-vue/CHANGELOG.md index a19fe5f..ab79ad1 100644 --- a/packages/gcode-preview-vue/CHANGELOG.md +++ b/packages/gcode-preview-vue/CHANGELOG.md @@ -1,5 +1,16 @@ # @chestnutlabs/gcode-preview-vue +## 0.20.1 + +### Patch Changes + +- Updated dependencies [[`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e), [`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e), [`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e)]: + - @chestnutlabs/gcode-renderer-three@0.20.1 + - @chestnutlabs/gcode-parser@0.20.1 + - @chestnutlabs/gcode-model-renderer@0.20.1 + - @chestnutlabs/gcode-preview-core@0.20.1 + - @chestnutlabs/toolpath-core@0.20.1 + ## 0.20.0 ### Minor Changes diff --git a/packages/gcode-preview-vue/package.json b/packages/gcode-preview-vue/package.json index db7c689..5900313 100644 --- a/packages/gcode-preview-vue/package.json +++ b/packages/gcode-preview-vue/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/gcode-preview-vue", - "version": "0.20.0", + "version": "0.20.1", "description": "Thin Vue 3 integration for the Chestnut Labs G-code viewer (DD-007): useGcodePreview composable + GcodePreview component over the framework-neutral parser/renderer/progress packages.", "keywords": [ "gcode", @@ -54,11 +54,11 @@ "test": "vitest run" }, "dependencies": { - "@chestnutlabs/gcode-model-renderer": "0.20.0", - "@chestnutlabs/gcode-parser": "0.20.0", - "@chestnutlabs/gcode-renderer-three": "0.20.0", - "@chestnutlabs/toolpath-core": "0.20.0", - "@chestnutlabs/gcode-preview-core": "0.20.0" + "@chestnutlabs/gcode-model-renderer": "0.20.1", + "@chestnutlabs/gcode-parser": "0.20.1", + "@chestnutlabs/gcode-renderer-three": "0.20.1", + "@chestnutlabs/toolpath-core": "0.20.1", + "@chestnutlabs/gcode-preview-core": "0.20.1" }, "peerDependencies": { "vue": "^3.4.0" diff --git a/packages/gcode-renderer-2d/CHANGELOG.md b/packages/gcode-renderer-2d/CHANGELOG.md index 8d41ec7..19a86be 100644 --- a/packages/gcode-renderer-2d/CHANGELOG.md +++ b/packages/gcode-renderer-2d/CHANGELOG.md @@ -1,5 +1,13 @@ # @chestnutlabs/gcode-renderer-2d +## 0.20.1 + +### Patch Changes + +- Updated dependencies []: + - @chestnutlabs/gcode-colors@0.20.1 + - @chestnutlabs/toolpath-core@0.20.1 + ## 0.20.0 ### Patch Changes diff --git a/packages/gcode-renderer-2d/package.json b/packages/gcode-renderer-2d/package.json index a46cd15..73ccf06 100644 --- a/packages/gcode-renderer-2d/package.json +++ b/packages/gcode-renderer-2d/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/gcode-renderer-2d", - "version": "0.20.0", + "version": "0.20.1", "description": "Low-resource Canvas 2D layer renderer for the Chestnut Labs G-code viewer (DD-014 / E8): an opt-in current/adjacent-layer 2D view over the existing ToolpathIR for low-GPU/low-memory/WebGL-blocked devices. No three, no framework.", "keywords": [ "gcode", @@ -49,7 +49,7 @@ "test": "vitest run" }, "dependencies": { - "@chestnutlabs/gcode-colors": "0.20.0", - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/gcode-colors": "0.20.1", + "@chestnutlabs/toolpath-core": "0.20.1" } } diff --git a/packages/gcode-renderer-three/CHANGELOG.md b/packages/gcode-renderer-three/CHANGELOG.md index 5130857..626c51a 100644 --- a/packages/gcode-renderer-three/CHANGELOG.md +++ b/packages/gcode-renderer-three/CHANGELOG.md @@ -1,5 +1,17 @@ # @chestnutlabs/gcode-renderer-three +## 0.20.1 + +### Patch Changes + +- [`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e) Thanks [@sobechestnut-dev](https://github.com/sobechestnut-dev)! - Fix `capture()` returning too-dark (linear colour-space) pixels. The interactive capture path rendered into a render target without sRGB output encoding, so `controls.capture()` and `ModelViewer.capture()` read back linear-space pixels — backgrounds and geometry colours came out darker than both the live canvas and `renderStill` (e.g. a `#6d7176` background captured as `#272a2e`). The capture render target now declares sRGB, so captured images match the on-screen view. + +- [`672fe42`](https://github.com/ChestnutLabs/gcode-preview/commit/672fe42c9391608adb3b816a6369d86b9f34307e) Thanks [@sobechestnut-dev](https://github.com/sobechestnut-dev)! - Fix `GeometryWorkerPool.dispose()` hanging an in-flight build. Disposing the pool mid-build (e.g. on canvas rebind or renderer teardown) dropped the parked build requests without settling them, so the streaming build's `Promise.all` never resolved and the build coroutine leaked. Dispose now rejects in-flight requests and the build stops cleanly. + +- Updated dependencies []: + - @chestnutlabs/gcode-colors@0.20.1 + - @chestnutlabs/toolpath-core@0.20.1 + ## 0.20.0 ### Minor Changes diff --git a/packages/gcode-renderer-three/package.json b/packages/gcode-renderer-three/package.json index 60fa339..8df5983 100644 --- a/packages/gcode-renderer-three/package.json +++ b/packages/gcode-renderer-three/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/gcode-renderer-three", - "version": "0.20.0", + "version": "0.20.1", "description": "Three.js toolpath renderer consuming ToolpathIR (DD-004). Phases 1-2: geometry builders + scene/lifecycle.", "keywords": [ "gcode", @@ -49,8 +49,8 @@ "test": "vitest run" }, "dependencies": { - "@chestnutlabs/gcode-colors": "0.20.0", - "@chestnutlabs/toolpath-core": "0.20.0" + "@chestnutlabs/gcode-colors": "0.20.1", + "@chestnutlabs/toolpath-core": "0.20.1" }, "devDependencies": { "@types/three": "0.178.0" diff --git a/packages/gcode-renderer-three/src/__tests__/capture.test.ts b/packages/gcode-renderer-three/src/__tests__/capture.test.ts index 153a18f..0ef526d 100644 --- a/packages/gcode-renderer-three/src/__tests__/capture.test.ts +++ b/packages/gcode-renderer-three/src/__tests__/capture.test.ts @@ -6,6 +6,7 @@ * validated in the browser, not in Node. */ import { describe, expect, it } from 'vitest'; +import { SRGBColorSpace } from 'three'; import { ToolpathRenderer, type GLRendererLike } from '../index.js'; import { flipRowsRGBA, resolveCaptureSize } from '../capture.js'; @@ -89,6 +90,38 @@ describe('capture — includeBuildVolume', () => { }); }); +describe('capture — colour space (regression: too-dark thumbnails)', () => { + it('renders the capture target as sRGB so readback matches the canvas / renderStill', async () => { + const canvas = document.createElement('canvas'); + let targetColorSpace: string | null = null; + const stub: GLRendererLike & { + setRenderTarget: (t: unknown) => void; + readRenderTargetPixels: () => void; + } = { + render: () => undefined, + setSize: () => undefined, + dispose: () => undefined, + domElement: canvas, + setRenderTarget: (t) => { + if (t !== null) targetColorSpace = (t as { texture: { colorSpace: string } }).texture.colorSpace; + }, + readRenderTargetPixels: () => undefined + }; + const renderer = new ToolpathRenderer({ + canvas, + quality: 'lines', + createRenderer: () => stub as unknown as GLRendererLike, + scheduleFrame: () => undefined + }); + // Encoding the pixels is browser-only and may reject after the render — but the off-screen render + // (and the target whose colour space we assert) happens first. A default (linear) target here would + // make readRenderTargetPixels return too-dark bytes (#6d7176 → ~#272a2e); sRGB matches the canvas. + await renderer.capture().catch(() => undefined); + expect(targetColorSpace).toBe(SRGBColorSpace); + renderer.dispose(); + }); +}); + describe('capture — unsupported path', () => { it('rejects with E_CAPTURE_UNSUPPORTED when the renderer cannot render-to-target', async () => { const canvas = document.createElement('canvas'); diff --git a/packages/gcode-renderer-three/src/__tests__/geometry-pool.test.ts b/packages/gcode-renderer-three/src/__tests__/geometry-pool.test.ts index 85c93c3..1a68941 100644 --- a/packages/gcode-renderer-three/src/__tests__/geometry-pool.test.ts +++ b/packages/gcode-renderer-three/src/__tests__/geometry-pool.test.ts @@ -97,6 +97,28 @@ describe('GeometryWorkerPool (DD-028)', () => { } }); + it('dispose() mid-build settles buildStreaming instead of hanging (regression)', async () => { + // Workers that never reply — every pump parks on its in-flight dispatch. Before the fix, dispose() + // dropped those parked promises and Promise.all never settled, hanging the build coroutine forever. + const stalling = (): GeometryWorkerLike => ({ onmessage: null, postMessage() {}, terminate() {} }); + const pool = new GeometryWorkerPool(2, stalling); + const ir = makeIR(4, 20); + const reqs = buildChunks(ir, { decimation: 1, targetSegmentsPerChunk: 20 }) + .chunks.filter((c) => c.kind === 'extrude') + .map((c, i) => reqFromChunk(c, i, 8)); + expect(reqs.length).toBeGreaterThan(0); + let settled = false; + const done = pool + .buildStreaming(reqs, () => undefined) + .then(() => { + settled = true; + }); + await Promise.resolve(); // let the pumps park on the never-answering workers + pool.dispose(); + await done; // must resolve, not hang (vitest would otherwise time out) + expect(settled).toBe(true); + }); + it('handles a single-worker pool and an empty request list', async () => { const pool = new GeometryWorkerPool(1, syncWorker); expect(await pool.buildAll([])).toEqual([]); diff --git a/packages/gcode-renderer-three/src/geometry-pool.ts b/packages/gcode-renderer-three/src/geometry-pool.ts index 9066942..a4ee7f9 100644 --- a/packages/gcode-renderer-three/src/geometry-pool.ts +++ b/packages/gcode-renderer-three/src/geometry-pool.ts @@ -93,7 +93,16 @@ export class GeometryWorkerPool { while (!this.disposed) { const i = next++; if (i >= requests.length) return; - const response = await this.dispatch(worker, requests[i]); + let response: GeometryBuildResponse; + try { + response = await this.dispatch(worker, requests[i]); + } catch (err) { + // dispose() rejects the in-flight request so this await unblocks (a terminated worker fires + // no message/error of its own). Treat a dispose-driven rejection as a clean stop; re-throw a + // genuine worker error so the batch still fails fast. + if (this.disposed) return; + throw err; + } if (this.disposed) return; onResult(i, response); } @@ -116,8 +125,12 @@ export class GeometryWorkerPool { dispose(): void { this.disposed = true; + // Reject every in-flight request BEFORE terminating: a terminated worker fires no message/error, so + // any `pump` parked at `await dispatch(...)` would otherwise never settle and `buildStreaming` would + // hang forever (leaking its coroutine + closures). pump swallows this rejection when disposed. + for (const p of this.current.values()) p.reject(new Error('E_POOL_DISPOSED: geometry pool disposed mid-build')); + this.current.clear(); for (const w of this.workers) w.terminate(); this.workers.length = 0; - this.current.clear(); } } diff --git a/packages/gcode-renderer-three/src/interactive-stage.ts b/packages/gcode-renderer-three/src/interactive-stage.ts index 825f92d..1064f04 100644 --- a/packages/gcode-renderer-three/src/interactive-stage.ts +++ b/packages/gcode-renderer-three/src/interactive-stage.ts @@ -9,7 +9,7 @@ * The camera types live here (not in the toolpath renderer) so the stage has no dependency on * toolpath concepts; the toolpath renderer re-exports them for import-path stability. */ -import { Color, OrthographicCamera, PerspectiveCamera, Scene, Vector3, WebGLRenderTarget } from 'three'; +import { Color, OrthographicCamera, PerspectiveCamera, Scene, SRGBColorSpace, Vector3, WebGLRenderTarget } from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import type { Vec3 } from '@chestnutlabs/toolpath-core'; import { @@ -360,7 +360,12 @@ export class InteractiveStage { } const gl = this.gl; const { w, h } = resolveCaptureSize(opts, this.canvas.width, this.canvas.height); + // The render target's texture MUST declare sRGB so three applies the same linear→sRGB output + // encoding it gives the canvas. A default (NoColorSpace/linear) target makes readRenderTargetPixels + // return LINEAR bytes — the whole capture (background AND geometry colours) comes back too dark + // (e.g. #6d7176 → ~#272a2e), diverging from the canvas view and from renderStill's toDataURL path. const target = new WebGLRenderTarget(w, h); + target.texture.colorSpace = SRGBColorSpace; // Frame the capture at its OWN aspect (not the viewport's) so a differently-shaped thumbnail is not // distorted; reuse the stage's projection machinery, then restore the live aspect. const prevAspect = this.aspect; diff --git a/packages/toolpath-core/CHANGELOG.md b/packages/toolpath-core/CHANGELOG.md index f95a4c9..7296c66 100644 --- a/packages/toolpath-core/CHANGELOG.md +++ b/packages/toolpath-core/CHANGELOG.md @@ -1,5 +1,7 @@ # @chestnutlabs/toolpath-core +## 0.20.1 + ## 0.20.0 ## 0.19.0 diff --git a/packages/toolpath-core/package.json b/packages/toolpath-core/package.json index 01d15f4..b9b4b1a 100644 --- a/packages/toolpath-core/package.json +++ b/packages/toolpath-core/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/toolpath-core", - "version": "0.20.0", + "version": "0.20.1", "description": "Neutral ToolpathIR and capability model for the Chestnut Labs G-code Preview toolpath stack.", "keywords": [ "gcode", diff --git a/tools/demo-kit/index.js b/tools/demo-kit/index.js index 442d128..c9092af 100644 --- a/tools/demo-kit/index.js +++ b/tools/demo-kit/index.js @@ -11,3 +11,4 @@ export * from './capabilities.js'; export * from './fixtures.js'; export * from './models.js'; export * from './format.js'; +export * from './theme.js'; diff --git a/tools/demo-kit/theme.js b/tools/demo-kit/theme.js new file mode 100644 index 0000000..cf285d7 --- /dev/null +++ b/tools/demo-kit/theme.js @@ -0,0 +1,21 @@ +/* + * demo-kit/theme — the one viewport look shared by every Chestnut Labs development surface. + * + * The Feature Lab and the four framework showcases all render into the canonical neutral **mid-grey + * documentation workspace** (#6d7176) — the same grey the screenshot harness uses for the capability + * renders in the README/docs — so a visitor moving between the demos, and the imagery in the docs, + * all read as one consistent product. This is a demo/documentation preference, NOT a library default: + * the renderer still ships a transparent/unset background and consumers keep full control of theming. + * + * `DOC_VIEWPORT_THEME` is a bounded declarative `Theme` for the toolpath viewer (``'s + * `theme` prop / `controls.setTheme`). `DOC_VIEWPORT_BG` is the matching flat background for the model + * viewer (``), which themes only its clear colour. + */ +export const DOC_VIEWPORT_BG = '#6d7176'; + +export const DOC_VIEWPORT_THEME = { + background: DOC_VIEWPORT_BG, + gridColor: '#565a60', + bedColor: '#565a60', + hemisphereIntensity: 2.1 +}; diff --git a/tools/demo/index.html b/tools/demo/index.html index 7fa7c3a..7a8f70f 100644 --- a/tools/demo/index.html +++ b/tools/demo/index.html @@ -454,8 +454,8 @@

    Scene