diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index ed232d0b..3127b494 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -22,10 +22,12 @@ ## Documentation - [ ] User- and maintainer-facing docs updated in this PR (docs are part of the feature) - [ ] Support matrix / migration notes updated, or N/A -- [ ] **Release promotion (`dev` → `main`) only:** `npm run docs:release-check` is green, the - `docs/README` current-state narrative + history name this version, `RELEASE_NOTES_DRAFT.md` - is folded in and deleted, and screenshots/guides were reviewed (CLAUDE.md public-docs - completion check) — or N/A for a normal `dev` PR. +- [ ] **Release promotion (`dev` → `main`) only:** `npm run docs:release-check` is green — the + `docs/README` current-state narrative + history name this version, `RELEASE_NOTES_DRAFT.md` is + folded in and deleted, and the enforced **`RELEASE_REVIEW.md`** (Public Product + Docs + Visual + review) is present for this version with every disposition resolved (each changed package + `reviewed` / `no-change-needed` / `not-applicable`; Product / Docs / Visual markers `resolved`). + The gate blocks promotion until it is — or N/A for a normal `dev` PR. ## Impact - Security: diff --git a/.github/workflows/docs-pages.yml b/.github/workflows/docs-pages.yml index c866ccc4..f9ba207d 100644 --- a/.github/workflows/docs-pages.yml +++ b/.github/workflows/docs-pages.yml @@ -13,6 +13,8 @@ on: - 'packages/**' - 'typedoc.json' - 'tools/docs/**' + - 'docs/manual/**' + - 'docs/media/**' - '.github/workflows/docs-pages.yml' workflow_dispatch: diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 82c3af2c..a35432fa 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -1,6 +1,6 @@ # DD-008 D3 (#130): the ONLY path that publishes @chestnutlabs/* — a GitHub Release # (tag vX.Y.Z) cut from `main`. Fresh install, every repository gate, then a -# dependency-ordered `npm publish` ×9 with npm provenance. Trusted publishing (OIDC) +# dependency-ordered `npm publish` ×14 with npm provenance. Trusted publishing (OIDC) # is the intended auth; a granular NPM_TOKEN secret is the recorded fallback # (DD-008 §4.3 — the @chestnutlabs scope is user-owned; org conversion deferred). # No workstation publishing, ever. diff --git a/.prettierignore b/.prettierignore index 3092074f..80dfe5f5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,3 +5,4 @@ dist *.min.css *.svelte +docs-site diff --git a/CLAUDE.md b/CLAUDE.md index 230d5b93..28e2e7eb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ Project instructions for Claude Code and other agents working in this repository A browser G-code / toolpath toolkit: parse `.gcode`, `.gcode.3mf`, and Prusa `.bgcode` off the main thread into a neutral intermediate representation (`ToolpathIR`) and render it with Three.js or a -Canvas 2D fallback. It ships as 13 lockstep-versioned `@chestnutlabs/*` packages plus Vue, React, +Canvas 2D fallback. It ships as 14 lockstep-versioned `@chestnutlabs/*` packages plus Vue, React, Svelte, and Web Component adapters over one shared engine. It began as a fork of `xyz-tools/gcode-preview` — attribution and provenance are load-bearing (see `NOTICE.md`). @@ -48,18 +48,27 @@ deliberate consideration is): If visible behavior changed, regenerate the affected media with [`tools/screenshots/`](tools/screenshots/README.md) rather than leaving a stale image in place. +Every user-facing feature is **visually documented somewhere** unless the visual coverage matrix +([`docs/VISUAL_FEATURE_COVERAGE.md`](docs/VISUAL_FEATURE_COVERAGE.md)) records why not — see the +visual standard in [`docs/USER_FACING_DOCS_STYLE.md`](docs/USER_FACING_DOCS_STYLE.md) §6/§8. **Docs freshness is enforced at release time, not left as post-release cleanup.** The `version` -script auto-stamps the deterministic "vX.Y.Z is on npm" strings into the generated Version PR and +script auto-stamps the deterministic "vX.Y.Z is on npm" strings into the generated Version PR, drops a `RELEASE_NOTES_DRAFT.md` to seed the `docs/README` "Current state" narrative + history -(fold it in, then delete it). The `Docs release gate` (`npm run docs:release-check`) then **blocks -the `dev` → `main` promotion** until every version surface, the current-state lead, and the -history list name the version being cut. Surfaces live in `tools/release/doc-surfaces.mjs`; the -flow is documented in [`docs/reference/release-process.md`](docs/reference/release-process.md). +(fold it in, then delete it), and generates `RELEASE_REVIEW.md` — a **per-release Public Product + +Documentation + Visual review** seeded from the changed-capability inventory (every package whose +`src/` changed since the previous tag). The `Docs release gate` (`npm run docs:release-check`) then +**blocks the `dev` → `main` promotion** until every version surface names the version being cut, +`RELEASE_NOTES_DRAFT.md` is gone, **and** `RELEASE_REVIEW.md` is present with every disposition +resolved (each changed package `reviewed` / `no-change-needed` / `not-applicable`, and the Product / +Docs / Visual markers `resolved`). This is enforcement, not a checkbox: reconcile the README, Pages +homepage, feature gallery, manual, demo/examples, screenshots, and the coverage matrix against the +inventory, then resolve the review. Surfaces live in `tools/release/doc-surfaces.mjs`; the flow is +documented in [`docs/reference/release-process.md`](docs/reference/release-process.md). ## Repo orientation -- `packages/*` — the 13 published packages (foundation → parse → color → render → adapters). Build +- `packages/*` — the 14 published packages (foundation → parse → color → render → adapters). Build in dependency order; each package builds itself (`npm run build -w @chestnutlabs/`). - `docs/` — user manual (`manual/`), consumer references (`reference/`, `compatibility/`), and the maintainer planning/design/research set. diff --git a/README.md b/README.md index 09965583..475b215f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ live printer progress. Parsing runs in a Web Worker, so a 250 MB file never free [![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 dark build plate](docs/media/viewer-benchy-tubes.png) +![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) --- @@ -66,7 +66,8 @@ Every one of those maps to something the library actually does, below. move, with a keyboard-operable slider. - **Time scrub & print-time estimate** — scrub along the *time* axis and read an estimated print time, labeled by provenance (the slicer's own estimate, or a kinematic approximation). -- **Feature inspection** — toggle travel, wipe, seam, and retraction/de-retraction markers. +- **Feature inspection** — toggle travel, wipe, seam, and retraction/de-retraction markers, or + **hide a whole feature role** (`setFeatureRoleVisible` — e.g. hide brim/skirt to declutter a part). - **Source-line ↔ segment mapping** — go from a byte in the file to the segment it drew, and back. ### Live job progress @@ -100,6 +101,18 @@ signal deserves**: giving up final-frame fidelity. The hard GPU/vertex-budget fallback still applies underneath. - **Degradation is disclosed** — over a large-file threshold the renderer decimates and *tells you* the exact reduction factor rather than dropping detail silently. +- **Any build surface, not just a rectangle** — rectangular, **circular / delta**, and **polygonal** + beds render with the outline and grid following the real shape (`buildVolume.shape`). +- **Capture / export** — `capture()` returns the current view as a `Blob` (PNG/JPEG/WebP) from the + interactive viewer *or* the headless still, including a **transparent background** for compositing + onto cards — the same primitive behind server thumbnails and "save image". + + + + + + +
A print rendered on a round, delta-style build plate — the bed outline and grid are circular, with the model centered on itNon-rectangular beds. A circular (delta) or polygonal build surface draws its true outline and clips the grid to the shape, instead of forcing a rectangle. Pass a shape on the build volume; rectangular stays the default.
@@ -132,6 +145,31 @@ a controller's semantic claims are reported `inferred` until confirmed on real h — today **GRBL/LightBurn laser is hardware-validated**; GRBL-mill and LinuxCNC are experimental. Geometry always parses regardless of tier. +#### Parametric programs (RS274NGC) + +Some G-code doesn't spell out its geometry move by move — it **computes** it. A LinuxCNC / CAM +program can set variables, evaluate expressions as coordinates, loop, branch, and call subroutines, +so a bolt circle is a `while` loop over an angle instead of twelve explicit moves. The parser runs +that programming layer, so these files resolve to the **real toolpath** instead of rendering empty. + +![A CNC toolpath of eight drilled holes on a circle plus a centre hole and a square frame, all computed by a parametric program, cut moves in pink and rapid moves in blue](docs/media/parametric-bolt-circle.png) + +```gcode +o200 while [# LT #] + # = [# * 360 / #] + G0 X[# + # * COS[#]] Y[# + # * SIN[#]] ; computed hole position + o100 call ; drill (subroutine) + # = [# + 1] +o200 endwhile +``` + +Parameters, expressions, `if` / `while` / `do` / `repeat` control flow, and in-file subroutines all +run — **bounded** (a hostile file can't hang the parser: `maxProgramIterations` / `maxCallDepth`) and +**honest** (a clean run reports `parametricProgram: 'known'`; a limit or unsupported construct drops +to `approximated` with a specific warning). It engages only on files that use `#` / `[` / O-words; +every FDM file is byte-for-byte unchanged. See +[Parametric programs](docs/manual/concept-parametric-programs.md). + ### Framework integration Four adapters over one engine, with matching options, events, and TypeScript types — enforced by a @@ -148,6 +186,17 @@ shared behavioral test suite that runs against all four in CI: - **Off-thread parsing** — a `GcodeParseSession` runs the parser in a Web Worker with streaming input, progressive previews for large files, resource limits, and cancellation. +- **Parallel geometry build** — a geometry worker pool builds extrusion tubes off the main thread + (byte-identical to serial), bounded by a memory budget and degrading gracefully (pool → serial → + lines) with every step disclosed. Big plates stay responsive. +- **Preparation is staged, the reveal is clean** — a `stage` event walks + `parsing → classifying → building-geometry → preparing-gpu → ready` with a real build percentage, + and `progressivePreview: 'hold'` suppresses the half-built scene so the model appears in one clean + reveal instead of visibly growing. +- **Render diagnostics** — `getRenderStats()` reports what actually happened: backend and WebGL + version, hardware-vs-software GPU (unmasked renderer/vendor), geometry mode, rendered segment and + draw-call counts, whether decimation applied, worker count, and a timing breakdown — a ready-made + "what am I running on / why is this slow" panel, never fabricated. - **Batteries-included worker** — the adapters wire up a worker with every dialect and `.gcode.3mf` support via the bundler-native `new Worker(new URL(...))` pattern (Vite works out of the box); a `createWorker` hook is the escape hatch for slim builds, custom dialects, or strict-CSP hosts. @@ -201,6 +250,12 @@ multicolor) with camera presets and the same serializable camera state, over the orbit controls. See the [model-renderer README](packages/gcode-model-renderer/README.md#interactive-viewer). +**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** +(`{ plateId }`, `{ objectIds }`, or an instance filter) narrows a still or the viewer to exactly the +plate or objects you want — a single plate's thumbnail, or one part isolated from the rest — with a +stable cache key per scope. + ## Quick start Install an adapter **plus `three`** (the renderer declares `three` as a peer dependency, range @@ -319,7 +374,9 @@ contain. Details: [ToolpathIR & the capability model](docs/manual/concept-ir-cap ## Documentation -- **[Manual & getting started](https://chestnutlabs.github.io/gcode-preview/)** and the +- **[Manual & getting started](https://chestnutlabs.github.io/gcode-preview/)**, the + **[visual feature gallery](https://chestnutlabs.github.io/gcode-preview/documents/Feature_gallery.html)** + (what it does, shown rather than described), and the **[API reference](https://chestnutlabs.github.io/gcode-preview/api/)** (generated from source). - Concepts: [workers & performance](docs/manual/concept-workers.md) · [IR & capabilities](docs/manual/concept-ir-capabilities.md) · @@ -395,7 +452,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.18.0**) with npm provenance. +packages publish to npm in lockstep (currently **v0.19.0**) with npm provenance. ## Development diff --git a/RELEASE_REVIEW.md b/RELEASE_REVIEW.md new file mode 100644 index 00000000..4173b390 --- /dev/null +++ b/RELEASE_REVIEW.md @@ -0,0 +1,44 @@ + + +# Release review — v0.19.0 + +**Review version:** v0.19.0 +**Changed-capability inventory diffed against:** `v0.18.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, +the Pages homepage, the feature gallery, the manual, the demo and examples, the screenshots, and +the coverage matrix ([`docs/VISUAL_FEATURE_COVERAGE.md`](docs/VISUAL_FEATURE_COVERAGE.md)); then set +each disposition. Guidance: CLAUDE.md "Public-docs completion check" and +[`docs/reference/release-process.md`](docs/reference/release-process.md). + +## Changed packages (`src/` since the previous release) + +| Package | Changed src files | Changelog summary | Disposition | +|---|--:|---|---| +| `@chestnutlabs/gcode-preview-core` | 2 | feat(renderer): `setFeatureRoleVisible(role, visible)` — show/hide a single feature role | Status: reviewed | +| `@chestnutlabs/gcode-renderer-three` | 2 | feat(renderer): `setFeatureRoleVisible(role, visible)` — show/hide a single feature role | Status: reviewed | +| `@chestnutlabs/toolpath-core` | 1 | feat(renderer): `setFeatureRoleVisible(role, visible)` — show/hide a single feature role | 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. + +_Resolve each marker above by replacing `pending` with `resolved` once reconciled._ diff --git a/docs/README.md b/docs/README.md index cdf5cc66..06b6a840 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-27 — **v0.18.0 shipped to npm** (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-08-28 — **v0.19.0 shipped to npm** (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 | |---|---|---| @@ -57,7 +57,7 @@ tracked in [`UPSTREAM_PROVENANCE.md`](UPSTREAM_PROVENANCE.md). | **E11** Documentation, SDK Reference & Published Manual (#197) | **Closed/Accepted** (2026-07-25) | [DD-013](design/DD-013-documentation-sdk-reference-and-published-manual.md) **Accepted** (D1–D7 as recommended). Completed E7/DD-008's docs ambit. **All four phases shipped:** typedoc API reference over the ten packages (#198), GitHub Pages + Chestnut Labs theme (#199), SDK manual — getting-started/guides/concepts (#200), accuracy gate + `eslint-plugin-tsdoc` + version stamp (#201). **Live: [chestnutlabs.github.io/gcode-preview](https://chestnutlabs.github.io/gcode-preview/)** (manual at `/`, API at `/api/`), auto-redeployed on merge/release. No package/IR/renderer change. Deferred: flip the `notDocumented` doc-coverage check to error once the 55 TSDoc-syntax gaps are filled | | **E8** Low-Resource Layer Mode (#9) | **Closed/Accepted** (2026-07-26) | [DD-014](design/DD-014-low-resource-layer-renderer.md) **Accepted** (D1–D5 as recommended; D6 build-now on the standing AnyBridge evidence). **All four §14 phases shipped** — #212 two new lockstep pkgs (`gcode-colors` shared ColorMode subsystem + `gcode-renderer-2d` Canvas 2D `LayerView2D`, no `three`/framework; renderer-three refactored onto the shared colorer) → **12 packages**; #213 adjacent "ghost" layers over a stable model frame; #214 **`renderer: '2d' \| '3d'` prop** through core + all 4 adapters (the 3D renderer is **loaded on demand** so a 2D-only bundle never ships Three.js) + 2D live-progress "cut"; #215 capability-honesty disclosures (non-planar/CNC via `renderer-unsupported`) + **§8 low-resource budget verified on a real device** (redraw ~0.2 ms median, ≤ 3.6 ms @ 6× CPU throttle vs the 16 ms budget; ~0 MB heap growth over 870 renders — no per-layer geometry) ([report](../tools/benchmark/results/e8-2d-lowresource-benchmark-2026-07-26.md)). §15 acceptance criteria met. Roadmap note (DD-008 §4.8): pure-Node GPU-less still rendering is a related deferred capability | | **Binary G-code (`.bgcode`) decode** (#188) | **Closed/Accepted** (2026-07-28) | [DD-011](design/DD-011-binary-gcode-decode-adapter.md) **Accepted**, preceded by [RR-003](research/RR-003-bgcode-licensing-and-format-audit.md). New 13th lockstep pkg `@chestnutlabs/gcode-bgcode` decodes Prusa `.bgcode` → plain G-code (None / DEFLATE-zlib / heatshrink w11+w12 / MeatPack; MeatPack MIT + heatshrink ISC clean-room, no AGPL); validated against real Prusa files (21 MB XL 856/856 CRC; cube golden byte-identical); [§7 security review](design/SECURITY-REVIEW-DD-011-bgcode.md) **signed off**. **Shipped in `v0.3.0`.** | -| **Non-extrusion toolpath — CNC / laser / plotter** (#189) | **Phases 1–5 built + merged; documented; experimental tier** | [DD-012](design/DD-012-non-extrusion-toolpath-and-modal-tool-state.md) **Accepted** (D1–D8), preceded by [RR-004](research/RR-004-non-extrusion-toolpath-coverage.md). **Phases 1–5 (#248–#255):** `MoveKind.Cut` + opt-in `ModalChannel`/`toolPower`; modal-motion + canned cycles (`G81`/`G82`/`G83`); GRBL-laser/GRBL-mill/LinuxCNC dialects + **validation tiers** (experimental → claims `inferred`); color-by-power + cut-vs-rapid; [compatibility matrix](compatibility/dialects-and-containers.md#dialects-non-extrusion--cnc--laser--plotter-dd-012-189) + support docs. **Real-file work:** lexer handles multi-command lines / `N`-numbers / bare-`S` (#256 — real LinuxCNC/TinyG now parse); evidence-based detection (#258, 0/6→3/6 real samples); [STL export](../tools/demo) (#152, #257). All FDM-byte-identical. **Remaining:** phase 6 — hardware validation → promote tiers to `validated`; larger gaps (LinuxCNC O-word subroutines/params/expressions). Rides the next lockstep release (`v0.4.0`). | +| **Non-extrusion toolpath — CNC / laser / plotter** (#189) | **Phases 1–5 built + merged; documented; experimental tier** | [DD-012](design/DD-012-non-extrusion-toolpath-and-modal-tool-state.md) **Accepted** (D1–D8), preceded by [RR-004](research/RR-004-non-extrusion-toolpath-coverage.md). **Phases 1–5 (#248–#255):** `MoveKind.Cut` + opt-in `ModalChannel`/`toolPower`; modal-motion + canned cycles (`G81`/`G82`/`G83`); GRBL-laser/GRBL-mill/LinuxCNC dialects + **validation tiers** (experimental → claims `inferred`); color-by-power + cut-vs-rapid; [compatibility matrix](compatibility/dialects-and-containers.md#dialects-non-extrusion--cnc--laser--plotter-dd-012-189) + support docs. **Real-file work:** lexer handles multi-command lines / `N`-numbers / bare-`S` (#256 — real LinuxCNC/TinyG now parse); evidence-based detection (#258, 0/6→3/6 real samples); [STL export](../tools/demo) (#152, #257). All FDM-byte-identical. **Phase 7 (RS274NGC programming layer) shipped `v0.18.0`:** parameters + expressions + O-word control flow + in-file subroutines ([DD-017](design/DD-017-rs274ngc-parametric-programs.md); [coverage](compatibility/gcode-motion-coverage.md#rs274ngc-parametric-programs-dd-017), [guide](manual/concept-parametric-programs.md)). **Remaining:** phase 6 — hardware validation → promote grbl-mill/linuxcnc tiers to `validated` (laser already validated). | | #152 STL export | Open, gated | Follows the **#118** chrome/showcase product decision, not an architecture DD (DD-009 §3) | Shipped so far: `packages/toolpath-core` (SoA `ToolpathIR`, capability model), `packages/gcode-parser` @@ -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.18.0`** (lockstep, npm provenance); `main` is the release branch, publishing from tagged GitHub +`v0.19.0`** (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` @@ -78,5 +78,7 @@ budget + capability classifier DD-023, all Phase A; `v0.14.0` qualityMode Full/A Phase B + tubes-never-chopped continuity fix; `v0.15.0` `progressivePreview` curtain (lines/hold/off) #373; `v0.16.0` off-main-thread geometry worker pool + single-clean-reveal + render diagnostics + slicer object/feature capture DD-026/027/028/029; `v0.17.0` non-model classifier + `modelBounds` object framing + FLUSH/WIPE_TOWER brackets DD-026 T2, `tubeByteBudget` on `renderStill`, geometry-pool→serial-tubes fallback DD-028; `v0.18.0` RS274NGC programming layer — parameters/expressions/O-word control flow/subroutines DD-017 (phases 1–3) + renderer/viewer interop — interactive `capture()`, per-plate render scope, non-rectangular build beds -DD-030). +DD-030; `v0.19.0` `setFeatureRoleVisible` per-feature-role hide/show (hide brim/skirt) + the +public-product/docs pass — visual Pages homepage + feature gallery + scrub/build-up animations, +Visual Feature Coverage Matrix, and the enforced per-release Product/Docs/Visual review gate). Changesets accumulate on `dev` for the next release. diff --git a/docs/USER_FACING_DOCS_STYLE.md b/docs/USER_FACING_DOCS_STYLE.md index 7ff99648..726ad33e 100644 --- a/docs/USER_FACING_DOCS_STYLE.md +++ b/docs/USER_FACING_DOCS_STYLE.md @@ -86,11 +86,35 @@ confident, visual, technically credible, and human**. ## 6. Visuals -Screenshots carry the product. Rules: +Screenshots carry the product. + +**Default rule:** every user-facing feature is **visually documented somewhere** — unless the +coverage matrix ([`docs/VISUAL_FEATURE_COVERAGE.md`](VISUAL_FEATURE_COVERAGE.md)) records *why not* +(the feature is non-visual, is redundant with an existing shot, or is inherently temporal and +covered by a clip). "We didn't get to it" is not a recorded reason; an explicit matrix entry is. +The per-release Public Product + Docs + **Visual** review (`RELEASE_REVIEW.md`, enforced by +`npm run docs:release-check`) is where this is reconciled before promotion. + +**Two visual classes** — know which one you are producing: + +- **Showcase** — the hero images that sell the product (README top, Pages homepage). They pick the + most compelling model + framing and demonstrate the product as a whole. +- **Feature-proof** — the minimum image that *proves a specific capability exists and works* + (a color mode, a non-rectangular bed, a clip plane, a capability disclosure). It is judged by the + §8 recognition test, not by how pretty it is. Every feature the matrix tracks needs one, even when + it will never be a hero shot. + +Rules: - **Real software only.** Every image is a render from the current build (see [`tools/screenshots/`](../tools/screenshots/README.md)). Never mock, composite, or hand-paint a screenshot, and never show UI the software doesn't have. +- **Documentation capture background.** Feature-proof and grid captures use the canonical + **mid-grey, slicer/CAD-style documentation background** — a named shared capture setting in + [`tools/screenshots/`](../tools/screenshots/README.md), *not* a library default. The neutral grey + keeps toolpath colors, feature coloring, and geometry legible and consistent across a set. It is a + documentation convention only: consumers keep full control of the rendered background, and nothing + here changes the library's own defaults. - **Put a strong visual near the top** of the root README, above the fold. It should demonstrate the product, not decorate the page. - **Consistency:** hold viewport size, framing, crop, sample models, filenames, and UI state steady @@ -133,6 +157,16 @@ consideration is): If a visible behavior changed, regenerate the affected media with the capture harness rather than leaving a stale image in place. +**The recognition test (for feature-proof visuals).** A feature-proof image passes only if: + +> Would a developer who knows the product recognize *this specific feature* from the image alone, +> with the caption removed? + +If the capability is not legible without the words underneath it, the shot is not proving anything — +reframe it, pick a model that exercises the feature, or add the on-screen state that makes it +visible. Track the outcome in the coverage matrix ([`docs/VISUAL_FEATURE_COVERAGE.md`](VISUAL_FEATURE_COVERAGE.md)); +a feature with no passing image needs either a new capture or a recorded reason it has none. + ## 9. Links and legal - Link to deeper docs instead of inlining every detail: compatibility matrix, progress contract, diff --git a/docs/VISUAL_FEATURE_COVERAGE.md b/docs/VISUAL_FEATURE_COVERAGE.md new file mode 100644 index 00000000..7d2dee43 --- /dev/null +++ b/docs/VISUAL_FEATURE_COVERAGE.md @@ -0,0 +1,246 @@ +# Visual Feature Coverage Matrix + +**Purpose.** A persistent, maintained inventory of every user-facing capability G-code Preview +ships, and how well each is *communicated* — demonstrated in the demo, shown in media, and +documented across the README, the GitHub Pages feature gallery, and the manual/package docs. + +This file is a **release artifact**, not a one-time audit. The release process +([`docs/reference/release-process.md`](reference/release-process.md)) requires the Public Product + +Documentation + Visual Review to reconcile this matrix against what actually shipped, so no +capability silently loses coverage. See +[Maintaining this matrix](#maintaining-this-matrix-release-integration) at the bottom. + +> **Scope.** "User-facing" = something a consumer of the packages can see, control, inspect, +> configure, or must understand to use the library. Internal architecture, build tooling, and test +> harnesses are out of scope except where they surface a consumer-visible behavior. + +## Status vocabulary + +| Status | Meaning | +|---|---| +| `covered` | Demonstrated and documented adequately; a current visual explains it where visual. | +| `needs screenshot` | Real capability, no image yet — a static shot would materially help. | +| `needs better coverage` | Has an image, but it under-explains the feature (see the §8 test below). | +| `needs comparison` | Best shown as before/after or A-vs-B, not a single frame. | +| `needs animation` | The interaction *is* the feature; a static frame can't carry it. | +| `needs demo exposure` | Not reachable/observable in the shipped showcase demo. | +| `not visually meaningful` | Genuinely non-visual; documented by code/table/prose instead. | +| `internal-only` | Not consumer-facing; here only to record the decision to exclude it. | + +The **§8 test** (from [`USER_FACING_DOCS_STYLE.md`](USER_FACING_DOCS_STYLE.md)): *if a developer saw +this image with no caption, would they have a reasonable chance of noticing which feature it +demonstrates?* If not, the visual is `needs better coverage` no matter how pretty it is. + +## Two visual classes + +- **Showcase** — clean, immediately legible, carefully framed. Used in the README, the Pages + homepage, and major landing sections. +- **Feature-proof** — intentionally shows controls, toggles, legends, values, selected states, + diagnostics, warnings, confidence tiers, and before/after comparisons. Used in the Pages feature + gallery, manual concept pages, and recipes. Completeness over beauty. + +Both are captured on the shared **mid-grey documentation presentation** +(`tools/screenshots/`) — a neutral slicer/CAD-style viewport, not the demo's incidental dark UI. +Intentional exceptions (transparency, dark/light themes, custom backgrounds) are noted per-row. + +--- + +## This pass (2026-08-27) — coverage delta + +What moved to **covered** in the second public-product pass: + +- All 14 existing images **regenerated on the mid-grey documentation presentation**; the harness is + now manifest-driven (`shots.manifest.json`) with a shared `lib/presentation.mjs` look. +- **New media**: `parametric-bolt-circle`, `bed-circular`, `bed-polygon`, `render-tubes`, + `render-lines` — each on the mid-grey look. +- **Parametric RS274NGC**, **non-rectangular beds**, **capture/export**, **render diagnostics**, + **progressive/held reveal**, **parallel geometry pool**, **render scope / per-plate**: now in the + **README**, the **feature gallery**, and (where visual) concept pages with real screenshots. +- **Demo now exposes**: `capture()` (Capture PNG), `getRenderStats()` (diagnostics panel), + `progressivePreview` selector, **By speed** + **By object** color modes, the parametric fixture, + and nav links to the model / model-viewer / 2D / CNC-validation / headless-still pages. + +**Animations wired up** (WebM, via the harness `encodeWebm` → `image2pipe`/mjpeg → VP8; embedded as +` = [# * 360 / #] + G0 X[# + # * COS[#]] Y[# + # * SIN[#]] + # = [# + 1] +o100 endwhile +``` + +The rapids land on the circle: the loop runs and the degree-trig expressions evaluate, so the +preview shows the pattern the machine would follow — not an empty plate. + +![A CNC toolpath of holes on a circle plus a centre hole and a square frame, all computed by a parametric program, cut moves in pink and rapid moves in blue](../media/parametric-bolt-circle.png) + +*A slightly larger version of the same idea — eight holes, a centre hole, and a computed frame — +rendered from [`test-data/fixtures/parametric/bolt-circle.ngc`](https://github.com/ChestnutLabs/gcode-preview/blob/dev/test-data/fixtures/parametric/bolt-circle.ngc). +Nothing in that file lists a hole position; every one is computed.* + +## It costs nothing on ordinary files + +The interpreter engages **only** when a program actually uses `#`, `[`, or an O-word. Every FDM and +plain-CNC file takes the ordinary parse path and produces byte-for-byte identical output — enabling +this changed nothing for files that don't use it. + +## Honesty and safety + +Computed geometry is real geometry. When a program runs to completion, the `parametricProgram` +capability is **`known`**: the coordinates are a deterministic function of the program, so the +interpreter computes them — it does not guess. If something is off — a resource limit was reached, an +O-word was unbalanced or unsupported, a condition was malformed — the capability drops to +**`approximated`** and a specific warning names exactly what was lost. A non-parametric file reports +`unavailable`. It never silently renders the wrong thing. + +A parametric program is a small program, so it is bounded like one. Two limits (part of the parser's +[`ParseLimits`](concept-workers.md)) keep a hostile or buggy file from hanging the parser: + +- **`maxProgramIterations`** (default 1,000,000) bounds total loop work — an `o while [1]` stops with a + partial result and a `rs274-iteration-limit` disclosure. +- **`maxCallDepth`** (default 50) bounds subroutine recursion — runaway or mutual recursion is + disclosed (`rs274-call-depth`) and skipped, never a stack overflow. + +There is no `eval` and no file I/O: the evaluator is a fixed arithmetic grammar over a fixed set of +functions, so an adversarial program can waste only bounded CPU. + +## Limits + +- **In-file subroutines only.** External subprogram files — Fanuc `M98 P…`, or an `o call` that + resolves to a separate `.ngc` — are out of scope; the parser does no file I/O. +- **Return values aren't propagated to the caller** yet; `return` acts as an early exit from the + subroutine. +- **Persistent parameters** (LinuxCNC's `.var` file) aren't read — a program is interpreted in + isolation. + +For the construct-by-construct coverage table and every warning code, see +[G-code motion & position coverage](../compatibility/gcode-motion-coverage.md#rs274ngc-parametric-programs-dd-017); +for the design and its bounded-execution model, see +[DD-017](../design/DD-017-rs274ngc-parametric-programs.md). diff --git a/docs/manual/concept-progress-motion.md b/docs/manual/concept-progress-motion.md index 4aab4817..e8bc5748 100644 --- a/docs/manual/concept-progress-motion.md +++ b/docs/manual/concept-progress-motion.md @@ -20,6 +20,11 @@ and the stack maps it onto the toolpath with **tiered confidence**: - **stale** signal → handled explicitly, not silently frozen; - **user scrub always wins** over incoming telemetry. +| Known position | Approximated position | +|---|---| +| ![Live progress with a byte-exact position marker and a ghosted remaining path](../media/progress-known.png) | ![Live progress from a coarse signal shown as an emphasis band over the current layer](../media/progress-approximated.png) | +| Byte-exact telemetry → a precise cut and an exact marker. | A layer index or bare percentage → an uncertainty band, not a fake dot. | + The normalized contract and consumer guidance are in the [progress signal contract](https://github.com/ChestnutLabs/gcode-preview/blob/dev/docs/reference/progress-signal-contract.md) and [consumer notes](https://github.com/ChestnutLabs/gcode-preview/blob/dev/docs/reference/progress-consumer-notes.md). diff --git a/docs/manual/concept-workers.md b/docs/manual/concept-workers.md index 331b21d0..0082767e 100644 --- a/docs/manual/concept-workers.md +++ b/docs/manual/concept-workers.md @@ -18,6 +18,8 @@ Input is **streamed** into the parser, and the renderer can append geometry as i large print shows a growing preview (time-to-first-paint) rather than a spinner until the whole file is parsed. The session reports progress as it goes. +

+ The **`progressivePreview`** prop chooses how that stream is presented (3D): - **`'lines'`** (default) — show the growing line preview as it parses, then swap in the final build. @@ -54,3 +56,13 @@ The Three.js renderer keeps interactions cheap: **layer chunks** with decimation clip and segment scrub as **draw-range** updates (no geometry rebuilds), tube-or-line geometry with automatic quality fallback, per-file build plates, and WebGL context-loss recovery. Pair a bounded parse with draw-range navigation and even very large toolpaths stay responsive. + +| Tubes | Lines | +|---|---| +| ![The calicat as lit 3D extrusion tubes](../media/render-tubes.png) | ![The calicat as flat single-pixel toolpath lines](../media/render-lines.png) | +| Lit 3D cross-sections — the default for models that fit the geometry budget. | Flat one-pixel paths — lighter, for very large files or low-GPU devices. | + +A **parallel geometry worker pool** builds the tubes off the main thread (byte-identical to serial, +degrading pool → serial → lines under a memory budget, all disclosed), and **`getRenderStats()`** +reports exactly what happened — backend, hardware-vs-software GPU, draw calls, and timings — so "why +is this slow / what am I running on" is answerable, never guessed. diff --git a/docs/manual/feature-gallery.md b/docs/manual/feature-gallery.md new file mode 100644 index 00000000..2a14230c --- /dev/null +++ b/docs/manual/feature-gallery.md @@ -0,0 +1,167 @@ +--- +title: Feature gallery +group: Guide +category: Guide +--- + +# Feature gallery + +What G-code Preview can actually do, shown rather than described. Every image is a **real render of a +real file** from the tracked demo corpus, captured by [`tools/screenshots/`](https://github.com/ChestnutLabs/gcode-preview/tree/dev/tools/screenshots) +on a neutral slicer/CAD viewport — nothing mocked. This page is the visual companion to the +[capability model](concept-ir-capabilities.md) and the per-feature +[coverage matrix](https://github.com/ChestnutLabs/gcode-preview/blob/dev/docs/VISUAL_FEATURE_COVERAGE.md). + +> New here? Start with the [getting-started guide](index.md), then come back to browse the breadth. + +## Toolpath inspection + +The core job: turn a sliced file into an interactive picture of the actual moves the machine makes. + +| | | +|---|---| +| ![3DBenchy as extrusion tubes, colored by feature role](../media/viewer-benchy-tubes.png) | ![3DBenchy clipped to a mid-height layer band, exposing perimeters and infill](../media/layer-clip-benchy.png) | +| **Tube geometry, feature coloring.** Lit 3D extrusion cross-sections, colored by what each move *is*. | **Layer-range clipping.** Isolate any band of layers by index — a draw-range trim, no geometry rebuild. | +| ![Calicat lower layers with retraction and de-retraction markers pinned on the path](../media/retraction-markers.png) | ![The calicat as flat single-pixel toolpath lines](../media/render-lines.png) | +| **Retraction & de-retraction markers.** Toggle markers for retractions, wipes, travel, and seams. | **Line geometry.** Flat one-pixel paths — lighter for very large files or low-GPU devices. | + +

+ +*Segment scrub sweeping the draw-range through 3DBenchy — the toolpath drawing itself in exact print +order. Scrubbing is a draw-range trim, so it stays smooth even on large models.* + +Also here: **segment scrub** (step move-by-move), **time scrub** with an honest print-time estimate +(labeled *slicer estimate* vs *kinematic approximation*), and **source-line ↔ segment mapping** — +click a move to find its byte in the file, and back. See [recipes](recipes.md). + +**Hide by feature role** (`setFeatureRoleVisible`) — e.g. hide brim/skirt to declutter a part +preview, at the same framing: + +| Brim + skirt shown | Adhesion hidden | +|---|---| +| ![A model with its skirt ring and brim shown](../media/frame-all.png) | ![The same view with the skirt and brim hidden, leaving just the model](../media/adhesion-hidden.png) | +| The pink skirt ring and brim frame the part. | `setFeatureRoleVisible(Skirt/Brim, false)` leaves only the model. | + +## Coloring & analysis + +Color is analysis. Every mode is capability-gated — it colors from real data or explains why it can't. + +| | | +|---|---| +| ![The calicat colored by feedrate, blue slow to red fast](../media/color-speed-calicat.png) | ![A variable-layer-height print colored blue for thin layers to red for thick](../media/color-layerheight.png) | +| **By speed / feedrate.** An auto-ranged ramp reveals slow corners and fast infill. | **By layer height.** Spot variable-layer-height regions at a glance. | + +Also: **feature role**, **object**, **tool**, **M600 color change**, the file's **own filament +colours**, **tool power** (laser/spindle), and **cut-vs-rapid** for CNC. All share one +renderer-agnostic [`ColorMode`](concept-ir-capabilities.md) model across the 3D and 2D renderers. + +## Live job progress + +Feed the viewer your printer's telemetry and it maps the signal onto the toolpath **at the confidence +the signal deserves** — the library's honesty rule made visible. + +| Known position | Approximated position | +|---|---| +| ![Live progress with a byte-exact position marker and a ghosted remaining path](../media/progress-known.png) | ![Live progress from a coarse signal shown as an emphasis band over the current layer](../media/progress-approximated.png) | +| Byte-exact telemetry → a precise cut and an exact marker. | A layer index or bare percentage → an uncertainty **band**, not a fake dot. | + +A **stale** signal greys the overlay instead of freezing a lie; **user scrub always wins** over +incoming telemetry; file-identity mismatches are detected and disclosed. See +[live progress & motion model](concept-progress-motion.md). + +## Rendering & quality + +| | | +|---|---| +| ![The calicat as lit 3D tubes](../media/render-tubes.png) | ![A single layer drawn by the Canvas 2D renderer, no WebGL](../media/canvas-2d-fallback.png) | +| **Tubes vs lines.** Automatic quality selection, or force either. | **Canvas 2D fallback.** A flat layer view with **no WebGL and no Three.js** — the 2D bundle never ships Three. | + +

+ +*Raising the top layer builds the model bottom to top — the same draw-range mechanism, and the visual +of a progressive reveal.* + +Underneath: **quality modes** (full / adaptive / fast), **interaction-aware quality** (drop detail +while orbiting, restore on settle), a **parallel geometry worker pool** (byte-identical tubes off the +main thread, degrading pool → serial → lines, all disclosed), **progressive preview** with a **single +clean `hold` reveal**, staged preparation progress, and **`getRenderStats()`** diagnostics (backend, +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) + +*`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 +build/first-frame timings.* + +## Cameras, framing & capture + +| | | | +|---|---|---| +| ![3DBenchy from the front, orthographic](../media/camera-front.png) | ![3DBenchy from the top, orthographic](../media/camera-top.png) | ![3DBenchy isometric, perspective](../media/camera-iso.png) | +| **Front** (ortho) | **Top** (ortho) | **Iso** (perspective) | + +Seven **camera presets**, orthographic/perspective, and a serializable **camera state** you can +persist and restore. + +**Object-aware framing** (`frameContent: 'object'`) fits the printed object — excluding skirt, prime +line, and purge — instead of the whole machine volume: + +| Whole-job framing | Object-aware framing | +|---|---| +| ![A small model framed together with its wide skirt ring, so the model sits small](../media/frame-all.png) | ![The same model framed tightly on the part, with the skirt excluded](../media/frame-object.png) | +| `'all'` fits everything, so the model is small inside its skirt. | `'object'` fits the part — the skirt/brim fall outside the frame. | + +**`capture()`** returns the current view as a `Blob` (PNG/JPEG/WebP), including a **transparent +background** for compositing onto cards, from the interactive viewer *or* the headless still. + +## Models & plates — the other renderer + +Sometimes you don't want the toolpath at all — you want a clean picture of *what the object is*. +That's a separate presentation renderer over the **source model** (STL / 3MF). + +![Two presentation stills: a neutral STL labeled materials unavailable beside a colored 3MF labeled materials known](../media/model-render-stl-3mf.png) + +STL is a single neutral object; **3MF** brings multi-object structure and per-object / per-triangle +**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 +[model-renderer README](https://github.com/ChestnutLabs/gcode-preview/blob/dev/packages/gcode-model-renderer/README.md). + +## Machine geometry + +| | | +|---|---| +| ![A print on a round, delta-style build plate with a circular grid](../media/bed-circular.png) | ![A print on a hexagonal build plate with the grid clipped to the polygon](../media/bed-polygon.png) | +| **Circular / delta beds.** The outline and grid follow the round shape. | **Polygonal beds.** An arbitrary polygon outline, grid clipped to the shape. | + +Rectangular stays the default and byte-identical; a `shape` on the build volume opts into round or +polygonal. Bed geometry discovered from the file (its printer profile) is offered to the consumer, +never force-applied over a bed you set. + +## CNC, laser & parametric + +| | | +|---|---| +| ![An Easel CNC carve colored by move kind, cut moves pink and rapid moves blue](../media/cnc-cut-vs-rapid.png) | ![A CNC bolt circle plus frame computed by a parametric program](../media/parametric-bolt-circle.png) | +| **Cut vs rapid.** Non-extrusion toolpaths classify tool-engaged moves as `Cut`; color by cut-vs-rapid or tool power. | **Parametric programs (RS274NGC).** Geometry the machine *computes* — a `while`-loop bolt circle + subroutine — resolved to the real toolpath. | + +Canned drilling cycles (`G81`/`G82`/`G83`) expand; controller support is **honesty-tiered** +(experimental until hardware-validated) but geometry always parses. Parametric execution is +**bounded** and reports `parametricProgram: 'known'` only on a clean run. See +[parametric programs](concept-parametric-programs.md) and the +[motion & position coverage](../compatibility/gcode-motion-coverage.md). + +## 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). + +--- + +*Missing a capability here? The [coverage matrix](https://github.com/ChestnutLabs/gcode-preview/blob/dev/docs/VISUAL_FEATURE_COVERAGE.md) +tracks every user-facing feature and its documentation state — including the shots still on the +to-capture list.* diff --git a/docs/manual/index.md b/docs/manual/index.md index 56d339cc..df3ec600 100644 --- a/docs/manual/index.md +++ b/docs/manual/index.md @@ -1,11 +1,50 @@ -A worker-based, cross-vendor **G-code toolpath stack** for the browser: parse `.gcode` and -`.gcode.3mf` off the main thread, normalize them into a versioned intermediate representation -(**ToolpathIR**), and render an interactive Three.js preview — with first-class **Vue, React, -Svelte**, and **Web Component** adapters over one shared, framework-neutral engine. +
+ +
    +
  • + +

    Toolpath inspection

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

    +
  • +
  • + +

    Coloring & analysis

    Color by speed, feature, object, tool, layer height, color change, or tool power.

    +
  • +
  • + +

    Live job progress

    Map printer telemetry onto the path at the confidence it deserves — exact, band, or stale.

    +
  • +
  • + +

    Models & plates

    Present the source STL / 3MF — multi-object, per-plate, colored materials — a second renderer.

    +
  • +
  • + +

    CNC, laser & parametric

    Cut vs rapid, tool power, canned cycles, and RS274NGC programs the machine computes.

    +
  • +
  • + +

    Any build surface

    Rectangular, circular / delta, and polygonal beds — outline and grid follow the real shape.

    +
  • +
-This is the **SDK manual** (these Guide and Concept pages). The **generated -API reference** — every public export of all fourteen `@chestnutlabs/*` packages — is a companion site -linked from the **API reference** item in the top nav. +These Guide and Concept pages are the **SDK manual**. The generated +**[API reference](https://chestnutlabs.github.io/gcode-preview/api/)** — every public export of all +fourteen `@chestnutlabs/*` packages — is a companion site linked from the top nav. New here? Jump to +the **[feature gallery](feature-gallery.md)** to browse the breadth, or start below. ## Install @@ -64,9 +103,10 @@ viewer is reachable without switching APIs. - **Concepts** — [ToolpathIR & the capability model](concept-ir-capabilities.md), [Workers, streaming & performance](concept-workers.md), [Dialects & containers](concept-dialects-containers.md), - [Live progress & motion model](concept-progress-motion.md). + [Live progress & motion model](concept-progress-motion.md), + [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.18.0`, +> **Status:** published — all fourteen `@chestnutlabs/*` packages are on npm (latest `v0.19.0`, > 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/adhesion-hidden.png b/docs/media/adhesion-hidden.png new file mode 100644 index 00000000..1c042069 Binary files /dev/null and b/docs/media/adhesion-hidden.png differ diff --git a/docs/media/app-control-panel.png b/docs/media/app-control-panel.png index d1d01306..e466eb72 100644 Binary files a/docs/media/app-control-panel.png and b/docs/media/app-control-panel.png differ diff --git a/docs/media/bed-circular.png b/docs/media/bed-circular.png new file mode 100644 index 00000000..6acf4ebd Binary files /dev/null and b/docs/media/bed-circular.png differ diff --git a/docs/media/bed-polygon.png b/docs/media/bed-polygon.png new file mode 100644 index 00000000..44cd0767 Binary files /dev/null and b/docs/media/bed-polygon.png differ diff --git a/docs/media/camera-front.png b/docs/media/camera-front.png index 8acccbed..2d88ba56 100644 Binary files a/docs/media/camera-front.png and b/docs/media/camera-front.png differ diff --git a/docs/media/camera-iso.png b/docs/media/camera-iso.png index f7060951..061a9274 100644 Binary files a/docs/media/camera-iso.png and b/docs/media/camera-iso.png differ diff --git a/docs/media/camera-top.png b/docs/media/camera-top.png index 6ece7f6a..4122b430 100644 Binary files a/docs/media/camera-top.png and b/docs/media/camera-top.png differ diff --git a/docs/media/canvas-2d-fallback.png b/docs/media/canvas-2d-fallback.png index 1c6ca251..478d9660 100644 Binary files a/docs/media/canvas-2d-fallback.png and b/docs/media/canvas-2d-fallback.png differ diff --git a/docs/media/cnc-cut-vs-rapid.png b/docs/media/cnc-cut-vs-rapid.png index 05f4efa0..bb0d2883 100644 Binary files a/docs/media/cnc-cut-vs-rapid.png and b/docs/media/cnc-cut-vs-rapid.png differ diff --git a/docs/media/color-layerheight.png b/docs/media/color-layerheight.png index 5a407775..5b2eb53f 100644 Binary files a/docs/media/color-layerheight.png and b/docs/media/color-layerheight.png differ diff --git a/docs/media/color-speed-calicat.png b/docs/media/color-speed-calicat.png index c3a210f8..63b10bab 100644 Binary files a/docs/media/color-speed-calicat.png and b/docs/media/color-speed-calicat.png differ diff --git a/docs/media/diagnostics-panel.png b/docs/media/diagnostics-panel.png new file mode 100644 index 00000000..2d97556d Binary files /dev/null and b/docs/media/diagnostics-panel.png differ diff --git a/docs/media/frame-all.png b/docs/media/frame-all.png new file mode 100644 index 00000000..e170b6a6 Binary files /dev/null and b/docs/media/frame-all.png differ diff --git a/docs/media/frame-object.png b/docs/media/frame-object.png new file mode 100644 index 00000000..c494e169 Binary files /dev/null and b/docs/media/frame-object.png differ diff --git a/docs/media/layer-buildup.webm b/docs/media/layer-buildup.webm new file mode 100644 index 00000000..4f1ad4d4 Binary files /dev/null and b/docs/media/layer-buildup.webm differ diff --git a/docs/media/layer-clip-benchy.png b/docs/media/layer-clip-benchy.png index 503d8de8..f3ff6570 100644 Binary files a/docs/media/layer-clip-benchy.png and b/docs/media/layer-clip-benchy.png differ diff --git a/docs/media/model-render-stl-3mf.png b/docs/media/model-render-stl-3mf.png index 6eba0963..e6d9f731 100644 Binary files a/docs/media/model-render-stl-3mf.png and b/docs/media/model-render-stl-3mf.png differ diff --git a/docs/media/parametric-bolt-circle.png b/docs/media/parametric-bolt-circle.png new file mode 100644 index 00000000..5ea787e7 Binary files /dev/null and b/docs/media/parametric-bolt-circle.png differ diff --git a/docs/media/progress-approximated.png b/docs/media/progress-approximated.png index 606cb5aa..3fc9261b 100644 Binary files a/docs/media/progress-approximated.png and b/docs/media/progress-approximated.png differ diff --git a/docs/media/progress-known.png b/docs/media/progress-known.png index 4f253903..f296dcf1 100644 Binary files a/docs/media/progress-known.png and b/docs/media/progress-known.png differ diff --git a/docs/media/render-lines.png b/docs/media/render-lines.png new file mode 100644 index 00000000..244ba63a Binary files /dev/null and b/docs/media/render-lines.png differ diff --git a/docs/media/render-tubes.png b/docs/media/render-tubes.png new file mode 100644 index 00000000..b945b982 Binary files /dev/null and b/docs/media/render-tubes.png differ diff --git a/docs/media/retraction-markers.png b/docs/media/retraction-markers.png index 3781e5af..4c4dc6ec 100644 Binary files a/docs/media/retraction-markers.png and b/docs/media/retraction-markers.png differ diff --git a/docs/media/scrub-sweep.webm b/docs/media/scrub-sweep.webm new file mode 100644 index 00000000..139039f5 Binary files /dev/null and b/docs/media/scrub-sweep.webm differ diff --git a/docs/media/viewer-benchy-tubes.png b/docs/media/viewer-benchy-tubes.png index 75638a53..a8abef74 100644 Binary files a/docs/media/viewer-benchy-tubes.png and b/docs/media/viewer-benchy-tubes.png differ diff --git a/docs/reference/release-process.md b/docs/reference/release-process.md index 86823721..f2ddd7d6 100644 --- a/docs/reference/release-process.md +++ b/docs/reference/release-process.md @@ -26,13 +26,31 @@ from the protected `Release / publish` workflow on a tag from `main` — never f stamper drops a `RELEASE_NOTES_DRAFT.md` (proposed lead + history line + deduped changelog points) into the PR for the author to fold in, then delete. The single source of truth for these surfaces is `tools/release/doc-surfaces.mjs`. + - **The Public Product + Docs + Visual review is seeded here too.** The same stamper generates + `RELEASE_REVIEW.md` from a **changed-capability inventory** — every package whose `src/` changed + since the previous `vX.Y.Z` tag (`changedCapabilityInventory()`), with its changed files and + changelog summary — each disposition pre-filled `Status: pending`, plus three global markers + (Product / Docs / Visual) pre-filled `pending`. It is the *inverse* of the notes draft: the + draft must be **absent** to promote, this review must be **present and fully resolved**. See the + dedicated review step below. +2a. **Public Product + Documentation + Visual review (required, before promotion):** open + `RELEASE_REVIEW.md` and reconcile the changed-capability inventory against the whole public + surface — the root `README`, the Pages homepage, the feature gallery, the manual (`docs/manual/`), + the demo and `tools/example-*` apps, the screenshots, and the visual coverage matrix + (`docs/VISUAL_FEATURE_COVERAGE.md`). For each changed package set its disposition to + `Status: reviewed` / `no-change-needed` / `not-applicable` (never `pending`), regenerating any + media the change invalidated (`tools/screenshots/`). Then resolve the three global markers by + replacing `pending` with `resolved`. `npm run docs:release-check` (the `Docs release gate`) reads + this file and **blocks the promotion PR** until the version matches and every disposition is + resolved — so the review is enforced, not a reminder. 3. **Promotion PR (`dev` → `main`):** a deliberate PR carrying the versioned state to `main`, merged only with the required checks green — including the **`Docs release gate`** (`npm run docs:release-check`), which fails the promotion if any version surface, the `docs/README` current-state lead, or the release-history list disagrees with the version being - cut, or if `RELEASE_NOTES_DRAFT.md` is still present. It also prints a screenshots/guides review - reminder (the judgment call from CLAUDE.md's *Public-docs completion check*; confirmed via the - promotion PR-template checkbox, not hard-failed). The first promotion (`v0.1.0`) ended the + cut, if `RELEASE_NOTES_DRAFT.md` is still present, or if `RELEASE_REVIEW.md` is missing, names a + different version, or still carries an unresolved disposition (a `Status: pending` row or a + Product/Docs/Visual marker not `resolved`). The Public Product + Docs + Visual review (step 2a) is + thus **enforced by the gate**, not left to a checkbox. The first promotion (`v0.1.0`) ended the founding-baseline freeze on `main`. 4. **Tag + GitHub Release:** tag `vX.Y.Z` on `main` and publish a GitHub Release for it. This — and nothing else — triggers publication. @@ -70,7 +88,7 @@ from the protected `Release / publish` workflow on a tag from `main` — never f | Branch | Protection | |---|---| | `dev` | required check `build` (existing); the *Changeset presence* check runs on every PR | -| `main` | required check `build` (existing, kept name-stable); **add `node-24` as required** at the rehearsal; no direct pushes; release PRs (promotion) only | +| `main` | required check `build` (existing, kept name-stable); **add `node-24` as required** at the rehearsal; the **`Docs release gate`** (`docs-release-gate.yml` → `npm run docs:release-check`) runs on every PR to `main` and — since promotion PRs are the only PRs targeting `main` — is the mechanism that enforces both the version surfaces and the resolved `RELEASE_REVIEW.md`; no direct pushes; release PRs (promotion) only | | tags `v*` | created only on `main` (the publish workflow independently verifies tag ∈ `main` and refuses otherwise) | ## Failure behavior (DD-008 §6) diff --git a/package-lock.json b/package-lock.json index ad9c304f..0b076d7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7389,11 +7389,11 @@ }, "packages/gcode-bgcode": { "name": "@chestnutlabs/gcode-bgcode", - "version": "0.18.0", + "version": "0.19.0", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-containers": "0.18.0", - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/gcode-containers": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0" }, "engines": { "node": ">=22" @@ -7401,10 +7401,10 @@ }, "packages/gcode-colors": { "name": "@chestnutlabs/gcode-colors", - "version": "0.18.0", + "version": "0.19.0", "license": "MIT", "dependencies": { - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/toolpath-core": "0.19.0" }, "engines": { "node": ">=22" @@ -7412,10 +7412,10 @@ }, "packages/gcode-containers": { "name": "@chestnutlabs/gcode-containers", - "version": "0.18.0", + "version": "0.19.0", "license": "MIT", "dependencies": { - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/toolpath-core": "0.19.0" }, "engines": { "node": ">=22" @@ -7423,10 +7423,10 @@ }, "packages/gcode-dialects": { "name": "@chestnutlabs/gcode-dialects", - "version": "0.18.0", + "version": "0.19.0", "license": "MIT", "dependencies": { - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/toolpath-core": "0.19.0" }, "engines": { "node": ">=22" @@ -7434,12 +7434,12 @@ }, "packages/gcode-model-renderer": { "name": "@chestnutlabs/gcode-model-renderer", - "version": "0.18.0", + "version": "0.19.0", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-containers": "0.18.0", - "@chestnutlabs/gcode-renderer-three": "0.18.0", - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/gcode-containers": "0.19.0", + "@chestnutlabs/gcode-renderer-three": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0" }, "devDependencies": { "@types/three": "0.178.0" @@ -7453,13 +7453,13 @@ }, "packages/gcode-parser": { "name": "@chestnutlabs/gcode-parser", - "version": "0.18.0", + "version": "0.19.0", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-bgcode": "0.18.0", - "@chestnutlabs/gcode-containers": "0.18.0", - "@chestnutlabs/gcode-dialects": "0.18.0", - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/gcode-bgcode": "0.19.0", + "@chestnutlabs/gcode-containers": "0.19.0", + "@chestnutlabs/gcode-dialects": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0" }, "engines": { "node": ">=22" @@ -7467,13 +7467,13 @@ }, "packages/gcode-preview-core": { "name": "@chestnutlabs/gcode-preview-core", - "version": "0.18.0", + "version": "0.19.0", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-parser": "0.18.0", - "@chestnutlabs/gcode-renderer-2d": "0.18.0", - "@chestnutlabs/gcode-renderer-three": "0.18.0", - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/gcode-parser": "0.19.0", + "@chestnutlabs/gcode-renderer-2d": "0.19.0", + "@chestnutlabs/gcode-renderer-three": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0" }, "engines": { "node": ">=22" @@ -7481,13 +7481,13 @@ }, "packages/gcode-preview-element": { "name": "@chestnutlabs/gcode-preview-element", - "version": "0.18.0", + "version": "0.19.0", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-parser": "0.18.0", - "@chestnutlabs/gcode-preview-core": "0.18.0", - "@chestnutlabs/gcode-renderer-three": "0.18.0", - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/gcode-parser": "0.19.0", + "@chestnutlabs/gcode-preview-core": "0.19.0", + "@chestnutlabs/gcode-renderer-three": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0" }, "engines": { "node": ">=22" @@ -7495,13 +7495,13 @@ }, "packages/gcode-preview-react": { "name": "@chestnutlabs/gcode-preview-react", - "version": "0.18.0", + "version": "0.19.0", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-parser": "0.18.0", - "@chestnutlabs/gcode-preview-core": "0.18.0", - "@chestnutlabs/gcode-renderer-three": "0.18.0", - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/gcode-parser": "0.19.0", + "@chestnutlabs/gcode-preview-core": "0.19.0", + "@chestnutlabs/gcode-renderer-three": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0" }, "devDependencies": { "@types/react": "^18.3.0", @@ -7517,13 +7517,13 @@ }, "packages/gcode-preview-svelte": { "name": "@chestnutlabs/gcode-preview-svelte", - "version": "0.18.0", + "version": "0.19.0", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-parser": "0.18.0", - "@chestnutlabs/gcode-preview-core": "0.18.0", - "@chestnutlabs/gcode-renderer-three": "0.18.0", - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/gcode-parser": "0.19.0", + "@chestnutlabs/gcode-preview-core": "0.19.0", + "@chestnutlabs/gcode-renderer-three": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0" }, "devDependencies": { "svelte": "^4.2.0" @@ -7537,13 +7537,13 @@ }, "packages/gcode-preview-vue": { "name": "@chestnutlabs/gcode-preview-vue", - "version": "0.18.0", + "version": "0.19.0", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-parser": "0.18.0", - "@chestnutlabs/gcode-preview-core": "0.18.0", - "@chestnutlabs/gcode-renderer-three": "0.18.0", - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/gcode-parser": "0.19.0", + "@chestnutlabs/gcode-preview-core": "0.19.0", + "@chestnutlabs/gcode-renderer-three": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0" }, "devDependencies": { "vue": "^3.4.0" @@ -7557,11 +7557,11 @@ }, "packages/gcode-renderer-2d": { "name": "@chestnutlabs/gcode-renderer-2d", - "version": "0.18.0", + "version": "0.19.0", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-colors": "0.18.0", - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/gcode-colors": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0" }, "engines": { "node": ">=22" @@ -7569,11 +7569,11 @@ }, "packages/gcode-renderer-three": { "name": "@chestnutlabs/gcode-renderer-three", - "version": "0.18.0", + "version": "0.19.0", "license": "MIT", "dependencies": { - "@chestnutlabs/gcode-colors": "0.18.0", - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/gcode-colors": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0" }, "devDependencies": { "@types/three": "0.178.0" @@ -7587,7 +7587,7 @@ }, "packages/toolpath-core": { "name": "@chestnutlabs/toolpath-core", - "version": "0.18.0", + "version": "0.19.0", "license": "MIT", "engines": { "node": ">=22" diff --git a/packages/gcode-bgcode/CHANGELOG.md b/packages/gcode-bgcode/CHANGELOG.md index af1c11f5..1b52b049 100644 --- a/packages/gcode-bgcode/CHANGELOG.md +++ b/packages/gcode-bgcode/CHANGELOG.md @@ -1,5 +1,13 @@ # @chestnutlabs/gcode-bgcode +## 0.19.0 + +### Patch Changes + +- Updated dependencies [[`81690dc`](https://github.com/ChestnutLabs/gcode-preview/commit/81690dcfece21d6fd11074ad7a264bcfc9edf455)]: + - @chestnutlabs/toolpath-core@0.19.0 + - @chestnutlabs/gcode-containers@0.19.0 + ## 0.18.0 ### Patch Changes diff --git a/packages/gcode-bgcode/package.json b/packages/gcode-bgcode/package.json index 2b6dd74c..f70e47d9 100644 --- a/packages/gcode-bgcode/package.json +++ b/packages/gcode-bgcode/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/gcode-bgcode", - "version": "0.18.0", + "version": "0.19.0", "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.18.0", - "@chestnutlabs/gcode-containers": "0.18.0" + "@chestnutlabs/toolpath-core": "0.19.0", + "@chestnutlabs/gcode-containers": "0.19.0" } } diff --git a/packages/gcode-colors/CHANGELOG.md b/packages/gcode-colors/CHANGELOG.md index fe63fb2e..00030f07 100644 --- a/packages/gcode-colors/CHANGELOG.md +++ b/packages/gcode-colors/CHANGELOG.md @@ -1,5 +1,12 @@ # @chestnutlabs/gcode-colors +## 0.19.0 + +### Patch Changes + +- Updated dependencies [[`81690dc`](https://github.com/ChestnutLabs/gcode-preview/commit/81690dcfece21d6fd11074ad7a264bcfc9edf455)]: + - @chestnutlabs/toolpath-core@0.19.0 + ## 0.18.0 ### Patch Changes diff --git a/packages/gcode-colors/package.json b/packages/gcode-colors/package.json index aa9e8c07..7b9ad81c 100644 --- a/packages/gcode-colors/package.json +++ b/packages/gcode-colors/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/gcode-colors", - "version": "0.18.0", + "version": "0.19.0", "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.18.0" + "@chestnutlabs/toolpath-core": "0.19.0" } } diff --git a/packages/gcode-containers/CHANGELOG.md b/packages/gcode-containers/CHANGELOG.md index 2c63739e..e09799dd 100644 --- a/packages/gcode-containers/CHANGELOG.md +++ b/packages/gcode-containers/CHANGELOG.md @@ -1,5 +1,12 @@ # @chestnutlabs/gcode-containers +## 0.19.0 + +### Patch Changes + +- Updated dependencies [[`81690dc`](https://github.com/ChestnutLabs/gcode-preview/commit/81690dcfece21d6fd11074ad7a264bcfc9edf455)]: + - @chestnutlabs/toolpath-core@0.19.0 + ## 0.18.0 ### Patch Changes diff --git a/packages/gcode-containers/package.json b/packages/gcode-containers/package.json index 9bc84ffe..354e24f3 100644 --- a/packages/gcode-containers/package.json +++ b/packages/gcode-containers/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/gcode-containers", - "version": "0.18.0", + "version": "0.19.0", "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.18.0" + "@chestnutlabs/toolpath-core": "0.19.0" } } diff --git a/packages/gcode-dialects/CHANGELOG.md b/packages/gcode-dialects/CHANGELOG.md index 1f808aba..52da7064 100644 --- a/packages/gcode-dialects/CHANGELOG.md +++ b/packages/gcode-dialects/CHANGELOG.md @@ -1,5 +1,12 @@ # @chestnutlabs/gcode-dialects +## 0.19.0 + +### Patch Changes + +- Updated dependencies [[`81690dc`](https://github.com/ChestnutLabs/gcode-preview/commit/81690dcfece21d6fd11074ad7a264bcfc9edf455)]: + - @chestnutlabs/toolpath-core@0.19.0 + ## 0.18.0 ### Patch Changes diff --git a/packages/gcode-dialects/package.json b/packages/gcode-dialects/package.json index 1244a8c1..76c4ca94 100644 --- a/packages/gcode-dialects/package.json +++ b/packages/gcode-dialects/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/gcode-dialects", - "version": "0.18.0", + "version": "0.19.0", "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.18.0" + "@chestnutlabs/toolpath-core": "0.19.0" } } diff --git a/packages/gcode-model-renderer/CHANGELOG.md b/packages/gcode-model-renderer/CHANGELOG.md index 653fd651..11a68be4 100644 --- a/packages/gcode-model-renderer/CHANGELOG.md +++ b/packages/gcode-model-renderer/CHANGELOG.md @@ -1,5 +1,14 @@ # @chestnutlabs/gcode-model-renderer +## 0.19.0 + +### Patch Changes + +- Updated dependencies [[`81690dc`](https://github.com/ChestnutLabs/gcode-preview/commit/81690dcfece21d6fd11074ad7a264bcfc9edf455)]: + - @chestnutlabs/gcode-renderer-three@0.19.0 + - @chestnutlabs/toolpath-core@0.19.0 + - @chestnutlabs/gcode-containers@0.19.0 + ## 0.18.0 ### Minor Changes diff --git a/packages/gcode-model-renderer/package.json b/packages/gcode-model-renderer/package.json index 29911c54..5db79329 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.18.0", + "version": "0.19.0", "description": "Three.js presentation renderer for source models (STL/3MF) — clean thumbnails/cards, distinct from toolpath inspection (DD-018).", "keywords": [ "3d-printing", @@ -51,9 +51,9 @@ "test": "vitest run" }, "dependencies": { - "@chestnutlabs/gcode-containers": "0.18.0", - "@chestnutlabs/gcode-renderer-three": "0.18.0", - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/gcode-containers": "0.19.0", + "@chestnutlabs/gcode-renderer-three": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0" }, "devDependencies": { "@types/three": "0.178.0" diff --git a/packages/gcode-parser/CHANGELOG.md b/packages/gcode-parser/CHANGELOG.md index fa0a7bb3..8ee8372f 100644 --- a/packages/gcode-parser/CHANGELOG.md +++ b/packages/gcode-parser/CHANGELOG.md @@ -1,5 +1,15 @@ # @chestnutlabs/gcode-parser +## 0.19.0 + +### Patch Changes + +- Updated dependencies [[`81690dc`](https://github.com/ChestnutLabs/gcode-preview/commit/81690dcfece21d6fd11074ad7a264bcfc9edf455)]: + - @chestnutlabs/toolpath-core@0.19.0 + - @chestnutlabs/gcode-bgcode@0.19.0 + - @chestnutlabs/gcode-containers@0.19.0 + - @chestnutlabs/gcode-dialects@0.19.0 + ## 0.18.0 ### Minor Changes diff --git a/packages/gcode-parser/package.json b/packages/gcode-parser/package.json index a4c7a7ec..43b09e3e 100644 --- a/packages/gcode-parser/package.json +++ b/packages/gcode-parser/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/gcode-parser", - "version": "0.18.0", + "version": "0.19.0", "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.18.0", - "@chestnutlabs/gcode-dialects": "0.18.0", - "@chestnutlabs/gcode-containers": "0.18.0", - "@chestnutlabs/gcode-bgcode": "0.18.0" + "@chestnutlabs/toolpath-core": "0.19.0", + "@chestnutlabs/gcode-dialects": "0.19.0", + "@chestnutlabs/gcode-containers": "0.19.0", + "@chestnutlabs/gcode-bgcode": "0.19.0" } } diff --git a/packages/gcode-preview-core/CHANGELOG.md b/packages/gcode-preview-core/CHANGELOG.md index 59cddf00..d9cc6702 100644 --- a/packages/gcode-preview-core/CHANGELOG.md +++ b/packages/gcode-preview-core/CHANGELOG.md @@ -1,5 +1,27 @@ # @chestnutlabs/gcode-preview-core +## 0.19.0 + +### Minor Changes + +- [#423](https://github.com/ChestnutLabs/gcode-preview/pull/423) [`81690dc`](https://github.com/ChestnutLabs/gcode-preview/commit/81690dcfece21d6fd11074ad7a264bcfc9edf455) Thanks [@sobechestnut-dev](https://github.com/sobechestnut-dev)! - feat(renderer): `setFeatureRoleVisible(role, visible)` — show/hide a single feature role + + Toggle a `FeatureRole` on or off — e.g. hide `Skirt`/`Brim` to declutter a part preview, or isolate + `Support`. Feature roles live per-segment inside the extrusion geometry (unlike the whole-chunk move-kind + toggle), so hidden segments are collapsed to NaN positions and the GPU discards them; showing the role + again restores the geometry byte-for-byte. Available on `ToolpathRenderer` and via + `GcodePreviewControls.setFeatureRoleVisible` (a no-op on the 2D renderer). Additive and capability-gated: + gate the UI on `capabilities.featureRoles === 'known'`, like the feature colour mode; untouched geometry + (no role hidden) is byte-identical. Also exports `FeatureRoleValue` from `@chestnutlabs/toolpath-core`. + +### Patch Changes + +- Updated dependencies [[`81690dc`](https://github.com/ChestnutLabs/gcode-preview/commit/81690dcfece21d6fd11074ad7a264bcfc9edf455)]: + - @chestnutlabs/gcode-renderer-three@0.19.0 + - @chestnutlabs/toolpath-core@0.19.0 + - @chestnutlabs/gcode-parser@0.19.0 + - @chestnutlabs/gcode-renderer-2d@0.19.0 + ## 0.18.0 ### Minor Changes diff --git a/packages/gcode-preview-core/package.json b/packages/gcode-preview-core/package.json index 79f867b0..a11e7c62 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.18.0", + "version": "0.19.0", "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.18.0", - "@chestnutlabs/gcode-renderer-2d": "0.18.0", - "@chestnutlabs/gcode-renderer-three": "0.18.0", - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/gcode-parser": "0.19.0", + "@chestnutlabs/gcode-renderer-2d": "0.19.0", + "@chestnutlabs/gcode-renderer-three": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0" } } diff --git a/packages/gcode-preview-core/src/controller.ts b/packages/gcode-preview-core/src/controller.ts index e6ec5034..5aeda5b9 100644 --- a/packages/gcode-preview-core/src/controller.ts +++ b/packages/gcode-preview-core/src/controller.ts @@ -43,6 +43,7 @@ import { segmentsCompletedAtTime, type Confidence, type DialectMetadata, + type FeatureRoleValue, type MachineGeometry, type MappedProgress, type ProgressMapper, @@ -181,6 +182,12 @@ export interface GcodePreviewControls { */ setScrubTime(ms: number | null): void; setKindVisible(kind: MoveKindToggle, visible: boolean): void; + /** + * Show/hide a single feature role — e.g. hide `Skirt`/`Brim` to declutter a part preview. Gate the + * UI on `capabilities.featureRoles === 'known'` (like the feature color mode); a no-op on the 2D + * renderer and on files without feature annotations. Import `FeatureRole` from `@chestnutlabs/toolpath-core`. + */ + setFeatureRoleVisible(role: FeatureRoleValue, visible: boolean): void; /** Opt-in retraction/deretraction markers (DD-009 D1, #148). Off by default. */ setShowRetractions(visible: boolean): void; setColorMode(mode: ColorMode): boolean; @@ -545,6 +552,7 @@ export function createPreviewController(options: PreviewControllerOptions = {}): withRenderer((r) => r.setScrubPosition(seg)); }, setKindVisible: (k, v) => withRenderer((r) => r.setKindVisible(k, v)), + setFeatureRoleVisible: (role, v) => withRenderer((r) => r.setFeatureRoleVisible?.(role, v)), setShowRetractions: (v) => withRenderer((r) => r.setShowRetractions(v)), // Availability is IR/renderer-dependent; before the renderer is ready we optimistically queue // and report true (the mode applies once bound). Callers can re-check after `parse-complete`. diff --git a/packages/gcode-preview-core/src/renderer-interface.ts b/packages/gcode-preview-core/src/renderer-interface.ts index 4d6a7555..e30bae9e 100644 --- a/packages/gcode-preview-core/src/renderer-interface.ts +++ b/packages/gcode-preview-core/src/renderer-interface.ts @@ -23,7 +23,7 @@ import type { } from '@chestnutlabs/gcode-renderer-three'; export type { CameraState, CameraView, CaptureOptions } from '@chestnutlabs/gcode-renderer-three'; -import type { MachineGeometry, MappedProgress, ToolpathIR } from '@chestnutlabs/toolpath-core'; +import type { FeatureRoleValue, MachineGeometry, MappedProgress, ToolpathIR } from '@chestnutlabs/toolpath-core'; /** Which renderer implementation backs the preview. `'3d'` is the default (Three.js). */ export type RendererMode = '2d' | '3d'; @@ -63,6 +63,11 @@ export interface PreviewRenderer { setLayerRange(startLayer: number, endLayer: number): void; setScrubPosition(segIndex: number | null): void; setKindVisible(kind: MoveKindToggle, visible: boolean): void; + /** + * Show/hide a single feature role (e.g. hide `Skirt`/`Brim`). 3D only — the flat 2D view treats it + * as a no-op. Optional so a renderer need not implement it; gate the UI on `featureRoles: 'known'`. + */ + setFeatureRoleVisible?(role: FeatureRoleValue, visible: boolean): void; setShowRetractions(visible: boolean): void; setColorMode(mode: ColorMode): boolean; setQuality(quality: QualityMode | 'auto'): void; diff --git a/packages/gcode-preview-element/CHANGELOG.md b/packages/gcode-preview-element/CHANGELOG.md index 33ea7409..a8bc573d 100644 --- a/packages/gcode-preview-element/CHANGELOG.md +++ b/packages/gcode-preview-element/CHANGELOG.md @@ -1,5 +1,15 @@ # @chestnutlabs/gcode-preview-element +## 0.19.0 + +### Patch Changes + +- Updated dependencies [[`81690dc`](https://github.com/ChestnutLabs/gcode-preview/commit/81690dcfece21d6fd11074ad7a264bcfc9edf455)]: + - @chestnutlabs/gcode-renderer-three@0.19.0 + - @chestnutlabs/gcode-preview-core@0.19.0 + - @chestnutlabs/toolpath-core@0.19.0 + - @chestnutlabs/gcode-parser@0.19.0 + ## 0.18.0 ### Minor Changes diff --git a/packages/gcode-preview-element/package.json b/packages/gcode-preview-element/package.json index 5b1af563..205991c8 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.18.0", + "version": "0.19.0", "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", @@ -52,9 +52,9 @@ "test": "vitest run" }, "dependencies": { - "@chestnutlabs/gcode-parser": "0.18.0", - "@chestnutlabs/gcode-preview-core": "0.18.0", - "@chestnutlabs/gcode-renderer-three": "0.18.0", - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/gcode-parser": "0.19.0", + "@chestnutlabs/gcode-preview-core": "0.19.0", + "@chestnutlabs/gcode-renderer-three": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0" } } diff --git a/packages/gcode-preview-react/CHANGELOG.md b/packages/gcode-preview-react/CHANGELOG.md index ad5cc244..7b0c6ee8 100644 --- a/packages/gcode-preview-react/CHANGELOG.md +++ b/packages/gcode-preview-react/CHANGELOG.md @@ -1,5 +1,15 @@ # @chestnutlabs/gcode-preview-react +## 0.19.0 + +### Patch Changes + +- Updated dependencies [[`81690dc`](https://github.com/ChestnutLabs/gcode-preview/commit/81690dcfece21d6fd11074ad7a264bcfc9edf455)]: + - @chestnutlabs/gcode-renderer-three@0.19.0 + - @chestnutlabs/gcode-preview-core@0.19.0 + - @chestnutlabs/toolpath-core@0.19.0 + - @chestnutlabs/gcode-parser@0.19.0 + ## 0.18.0 ### Patch Changes diff --git a/packages/gcode-preview-react/package.json b/packages/gcode-preview-react/package.json index e94aa10d..dc9e5a67 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.18.0", + "version": "0.19.0", "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", @@ -49,10 +49,10 @@ "test": "vitest run" }, "dependencies": { - "@chestnutlabs/gcode-parser": "0.18.0", - "@chestnutlabs/gcode-preview-core": "0.18.0", - "@chestnutlabs/gcode-renderer-three": "0.18.0", - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/gcode-parser": "0.19.0", + "@chestnutlabs/gcode-preview-core": "0.19.0", + "@chestnutlabs/gcode-renderer-three": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" diff --git a/packages/gcode-preview-svelte/CHANGELOG.md b/packages/gcode-preview-svelte/CHANGELOG.md index 100b9a6c..8f741945 100644 --- a/packages/gcode-preview-svelte/CHANGELOG.md +++ b/packages/gcode-preview-svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # @chestnutlabs/gcode-preview-svelte +## 0.19.0 + +### Patch Changes + +- Updated dependencies [[`81690dc`](https://github.com/ChestnutLabs/gcode-preview/commit/81690dcfece21d6fd11074ad7a264bcfc9edf455)]: + - @chestnutlabs/gcode-renderer-three@0.19.0 + - @chestnutlabs/gcode-preview-core@0.19.0 + - @chestnutlabs/toolpath-core@0.19.0 + - @chestnutlabs/gcode-parser@0.19.0 + ## 0.18.0 ### Patch Changes diff --git a/packages/gcode-preview-svelte/package.json b/packages/gcode-preview-svelte/package.json index e7e45b18..08ebc3cd 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.18.0", + "version": "0.19.0", "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", @@ -55,10 +55,10 @@ "test": "vitest run" }, "dependencies": { - "@chestnutlabs/gcode-parser": "0.18.0", - "@chestnutlabs/gcode-preview-core": "0.18.0", - "@chestnutlabs/gcode-renderer-three": "0.18.0", - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/gcode-parser": "0.19.0", + "@chestnutlabs/gcode-preview-core": "0.19.0", + "@chestnutlabs/gcode-renderer-three": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0" }, "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 e7dfa91c..6b4bb5c7 100644 --- a/packages/gcode-preview-vue/CHANGELOG.md +++ b/packages/gcode-preview-vue/CHANGELOG.md @@ -1,5 +1,15 @@ # @chestnutlabs/gcode-preview-vue +## 0.19.0 + +### Patch Changes + +- Updated dependencies [[`81690dc`](https://github.com/ChestnutLabs/gcode-preview/commit/81690dcfece21d6fd11074ad7a264bcfc9edf455)]: + - @chestnutlabs/gcode-renderer-three@0.19.0 + - @chestnutlabs/gcode-preview-core@0.19.0 + - @chestnutlabs/toolpath-core@0.19.0 + - @chestnutlabs/gcode-parser@0.19.0 + ## 0.18.0 ### Patch Changes diff --git a/packages/gcode-preview-vue/package.json b/packages/gcode-preview-vue/package.json index fe5475a4..380bcae2 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.18.0", + "version": "0.19.0", "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", @@ -50,10 +50,10 @@ "test": "vitest run" }, "dependencies": { - "@chestnutlabs/gcode-parser": "0.18.0", - "@chestnutlabs/gcode-renderer-three": "0.18.0", - "@chestnutlabs/toolpath-core": "0.18.0", - "@chestnutlabs/gcode-preview-core": "0.18.0" + "@chestnutlabs/gcode-parser": "0.19.0", + "@chestnutlabs/gcode-renderer-three": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0", + "@chestnutlabs/gcode-preview-core": "0.19.0" }, "peerDependencies": { "vue": "^3.4.0" diff --git a/packages/gcode-renderer-2d/CHANGELOG.md b/packages/gcode-renderer-2d/CHANGELOG.md index afd44607..a7e04b99 100644 --- a/packages/gcode-renderer-2d/CHANGELOG.md +++ b/packages/gcode-renderer-2d/CHANGELOG.md @@ -1,5 +1,13 @@ # @chestnutlabs/gcode-renderer-2d +## 0.19.0 + +### Patch Changes + +- Updated dependencies [[`81690dc`](https://github.com/ChestnutLabs/gcode-preview/commit/81690dcfece21d6fd11074ad7a264bcfc9edf455)]: + - @chestnutlabs/toolpath-core@0.19.0 + - @chestnutlabs/gcode-colors@0.19.0 + ## 0.18.0 ### Patch Changes diff --git a/packages/gcode-renderer-2d/package.json b/packages/gcode-renderer-2d/package.json index aff7863b..68f71504 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.18.0", + "version": "0.19.0", "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.18.0", - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/gcode-colors": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0" } } diff --git a/packages/gcode-renderer-three/CHANGELOG.md b/packages/gcode-renderer-three/CHANGELOG.md index 8d4c1d38..0ab97efb 100644 --- a/packages/gcode-renderer-three/CHANGELOG.md +++ b/packages/gcode-renderer-three/CHANGELOG.md @@ -1,5 +1,25 @@ # @chestnutlabs/gcode-renderer-three +## 0.19.0 + +### Minor Changes + +- [#423](https://github.com/ChestnutLabs/gcode-preview/pull/423) [`81690dc`](https://github.com/ChestnutLabs/gcode-preview/commit/81690dcfece21d6fd11074ad7a264bcfc9edf455) Thanks [@sobechestnut-dev](https://github.com/sobechestnut-dev)! - feat(renderer): `setFeatureRoleVisible(role, visible)` — show/hide a single feature role + + Toggle a `FeatureRole` on or off — e.g. hide `Skirt`/`Brim` to declutter a part preview, or isolate + `Support`. Feature roles live per-segment inside the extrusion geometry (unlike the whole-chunk move-kind + toggle), so hidden segments are collapsed to NaN positions and the GPU discards them; showing the role + again restores the geometry byte-for-byte. Available on `ToolpathRenderer` and via + `GcodePreviewControls.setFeatureRoleVisible` (a no-op on the 2D renderer). Additive and capability-gated: + gate the UI on `capabilities.featureRoles === 'known'`, like the feature colour mode; untouched geometry + (no role hidden) is byte-identical. Also exports `FeatureRoleValue` from `@chestnutlabs/toolpath-core`. + +### Patch Changes + +- Updated dependencies [[`81690dc`](https://github.com/ChestnutLabs/gcode-preview/commit/81690dcfece21d6fd11074ad7a264bcfc9edf455)]: + - @chestnutlabs/toolpath-core@0.19.0 + - @chestnutlabs/gcode-colors@0.19.0 + ## 0.18.0 ### Minor Changes diff --git a/packages/gcode-renderer-three/package.json b/packages/gcode-renderer-three/package.json index 90b6a740..fb82ae54 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.18.0", + "version": "0.19.0", "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.18.0", - "@chestnutlabs/toolpath-core": "0.18.0" + "@chestnutlabs/gcode-colors": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0" }, "devDependencies": { "@types/three": "0.178.0" diff --git a/packages/gcode-renderer-three/src/__tests__/feature-visibility.test.ts b/packages/gcode-renderer-three/src/__tests__/feature-visibility.test.ts new file mode 100644 index 00000000..ac7cc413 --- /dev/null +++ b/packages/gcode-renderer-three/src/__tests__/feature-visibility.test.ts @@ -0,0 +1,142 @@ +// @vitest-environment happy-dom +/** + * setFeatureRoleVisible (DD-009 extension, AnyBridge "hide adhesion" request): show/hide a single + * FeatureRole by collapsing its segments' vertices to NaN (GPU-discarded), reversibly. Unlike the + * whole-chunk move-kind toggle, feature roles live per-segment inside the extrusion geometry. + * + * Guarantees under test: hidden segments' positions go NaN; other segments untouched; showing again + * restores byte-for-byte; never called → geometry byte-identical (no base copy saved); travel geometry + * is unaffected (feature roles are an extrusion channel). + */ +import { describe, expect, it } from 'vitest'; +import { FeatureRole, MoveKind, ToolpathIRBuilder, type ToolpathIR } from '@chestnutlabs/toolpath-core'; +import { ToolpathRenderer, type GLRendererLike } from '../index.js'; + +/** One extrude segment per `roles[i]`, plus `travel` travel segments (feature roles don't apply). */ +function makeIR(roles: number[], travel = 0): ToolpathIR { + const b = new ToolpathIRBuilder({ parserVersion: 'test', units: 'mm', unitsSource: 'known' }); + b.setCapability('featureRoles', 'known'); + let src = 0; + for (let t = 0; t < travel; t++) { + b.addSegment({ + x0: 90 + t, + y0: 90, + z0: 0.2, + x1: 91 + t, + y1: 90, + z1: 0.2, + e: 0, + kind: MoveKind.Travel, + layer: 0, + srcByte: src++ * 10 + }); + } + for (let s = 0; s < roles.length; s++) { + b.addSegment({ + x0: 100 + s, + y0: 100, + z0: 0.2, + x1: 101 + s, + y1: 100, + z1: 0.2, + e: 1, + kind: MoveKind.Extrude, + layer: 0, + srcByte: src++ * 10, + feature: roles[s] + }); + } + return b.finalize(); +} + +function makeRenderer(): { renderer: ToolpathRenderer; run: () => void } { + const canvas = document.createElement('canvas'); + const stub: GLRendererLike = { + render: () => undefined, + setSize: () => undefined, + setPixelRatio: () => undefined, + dispose: () => undefined, + domElement: canvas + }; + const ticks: (() => void)[] = []; + const renderer = new ToolpathRenderer({ + canvas, + buildVolume: { x: 220, y: 220, z: 250 }, + chunksPerTick: 1, + quality: 'lines', // GL_LINES: 6 floats (2 vertices) per segment, in chunk order + createRenderer: () => stub, + scheduleFrame: (cb) => ticks.push(cb) + }); + const run = (): void => { + while (ticks.length > 0) ticks.shift()?.(); + }; + return { renderer, run }; +} + +/** The single extrude chunk mesh (lines quality → one LineSegments over all extrude segments). */ +function extrudeMesh(renderer: ToolpathRenderer) { + return renderer.chunkMeshes.find((m) => (m.userData.chunk as { kind: string } | undefined)?.kind === 'extrude')!; +} +const positions = (mesh: { geometry: { getAttribute: (n: string) => { array: ArrayLike } } }): Float32Array => + mesh.geometry.getAttribute('position').array as Float32Array; + +describe('setFeatureRoleVisible', () => { + it('hides one role (its segments → NaN) and leaves others intact; showing restores byte-for-byte', () => { + const { renderer, run } = makeRenderer(); + renderer.setIR(makeIR([FeatureRole.Skirt, FeatureRole.Perimeter, FeatureRole.Skirt, FeatureRole.Perimeter])); + run(); + const mesh = extrudeMesh(renderer); + const baseline = positions(mesh).slice(); // 4 segments × 6 floats = 24 + + renderer.setFeatureRoleVisible(FeatureRole.Skirt, false); + const hidden = positions(mesh); + // segments 0 and 2 (Skirt) → all 6 floats NaN; 1 and 3 (Perimeter) → unchanged + for (const seg of [0, 2]) for (let f = 0; f < 6; f++) expect(Number.isNaN(hidden[seg * 6 + f])).toBe(true); + for (const seg of [1, 3]) for (let f = 0; f < 6; f++) expect(hidden[seg * 6 + f]).toBe(baseline[seg * 6 + f]); + expect(renderer.getHiddenFeatureRoles()).toEqual([FeatureRole.Skirt]); + + renderer.setFeatureRoleVisible(FeatureRole.Skirt, true); + expect(Array.from(positions(mesh))).toEqual(Array.from(baseline)); // restored exactly + expect(renderer.getHiddenFeatureRoles()).toEqual([]); + }); + + it('hides multiple roles at once and un-hides them independently', () => { + const { renderer, run } = makeRenderer(); + renderer.setIR(makeIR([FeatureRole.Skirt, FeatureRole.Brim, FeatureRole.Perimeter])); + run(); + const mesh = extrudeMesh(renderer); + renderer.setFeatureRoleVisible(FeatureRole.Skirt, false); + renderer.setFeatureRoleVisible(FeatureRole.Brim, false); + const p = positions(mesh); + expect(Number.isNaN(p[0])).toBe(true); // skirt seg 0 + expect(Number.isNaN(p[6])).toBe(true); // brim seg 1 + expect(Number.isNaN(p[12])).toBe(false); // perimeter seg 2 visible + // un-hide brim only: seg 1 returns, seg 0 stays hidden + renderer.setFeatureRoleVisible(FeatureRole.Brim, true); + const p2 = positions(mesh); + expect(Number.isNaN(p2[0])).toBe(true); + expect(Number.isNaN(p2[6])).toBe(false); + }); + + it('never called → geometry is byte-identical (no NaN, no base copy retained)', () => { + const { renderer, run } = makeRenderer(); + renderer.setIR(makeIR([FeatureRole.Skirt, FeatureRole.Perimeter])); + run(); + const mesh = extrudeMesh(renderer); + expect(Array.from(positions(mesh)).some(Number.isNaN)).toBe(false); + expect(mesh.userData.baseFeaturePositions).toBeUndefined(); + }); + + it('does not touch travel geometry (feature roles are extrusion-only)', () => { + const { renderer, run } = makeRenderer(); + renderer.setIR(makeIR([FeatureRole.Skirt], 2)); + run(); + renderer.setFeatureRoleVisible(FeatureRole.Skirt, false); + const travel = renderer.chunkMeshes.find( + (m) => (m.userData.chunk as { kind: string } | undefined)?.kind === 'travel' + ); + expect(travel).toBeDefined(); + expect(Array.from(positions(travel!)).some(Number.isNaN)).toBe(false); + expect(travel!.userData.baseFeaturePositions).toBeUndefined(); + }); +}); diff --git a/packages/gcode-renderer-three/src/scene.ts b/packages/gcode-renderer-three/src/scene.ts index e46402dc..c463b671 100644 --- a/packages/gcode-renderer-three/src/scene.ts +++ b/packages/gcode-renderer-three/src/scene.ts @@ -42,7 +42,7 @@ import { Vector2, Vector3 } from 'three'; -import type { MachineGeometry, MappedProgress, ToolpathIR } from '@chestnutlabs/toolpath-core'; +import type { FeatureRoleValue, MachineGeometry, MappedProgress, ToolpathIR } from '@chestnutlabs/toolpath-core'; import { autoDecimation, buildChunks, type ChunkBuildResult, type GeometryChunk } from './chunks.js'; import { buildChunkColors, type ColorMode } from './colors.js'; import { computeDrawState, computeOverlayDrawStates } from './ranges.js'; @@ -358,6 +358,9 @@ export class ToolpathRenderer { private endLayer = Infinity; private scrubSegIndex: number | null = null; private kindVisible: Record = { extrude: true, travel: true, wipe: true }; + // Per-feature-role visibility (DD-009 extension): hidden roles are collapsed to NaN positions so the + // GPU discards them, reversibly. Empty by default → no work, geometry byte-identical when unused. + private hiddenFeatureRoles = new Set(); // Live-progress overlay state (DD-006 §4.5, phase 3): completed cut + ghost + marker/band. private progress: MappedProgress | null = null; private presentationMode: ProgressPresentationMode = 'hidden'; @@ -780,6 +783,7 @@ export class ToolpathRenderer { mesh.name = `chunk:${c.kind}:${c.layerStart}-${c.layerEnd}`; mesh.userData.chunk = c; this.applyDrawStateToMesh(mesh, c); + this.applyFeatureVisibilityToMesh(mesh, c); this.toolpathGroup.add(mesh); nonExtrude++; } @@ -860,6 +864,7 @@ export class ToolpathRenderer { mesh.userData.drawUnitsPerSegment = response.indicesPerSegment; mesh.userData.vertexSegment = vertexSegment; this.applyDrawStateToMesh(mesh, chunk); + this.applyFeatureVisibilityToMesh(mesh, chunk); return mesh; } @@ -993,6 +998,7 @@ export class ToolpathRenderer { mesh.name = `chunk:${chunk.kind}:${chunk.layerStart}-${chunk.layerEnd}`; mesh.userData.chunk = chunk; this.applyDrawStateToMesh(mesh, chunk); // honor clipping set during an in-flight build + this.applyFeatureVisibilityToMesh(mesh, chunk); // honor a hidden feature role, too this.toolpathGroup.add(mesh); this.builtCount++; built++; @@ -1197,6 +1203,78 @@ export class ToolpathRenderer { this.render(); } + /** + * Show or hide a single `FeatureRole` — e.g. hide `Skirt`/`Brim` to declutter a part preview, + * or isolate `Support`. Feature roles live per-segment inside the extrusion geometry (unlike move + * kinds, which are whole chunks), so hidden segments are collapsed to NaN positions and the GPU + * discards them; the original positions are restored when the role is shown again. + * + * Only meaningful when the file carried feature annotations (`capabilities.featureRoles === 'known'` + * — gate the UI on it, like the feature color mode); on an unannotated file every segment is + * `Unknown` and hiding a specific role is a no-op. Untouched geometry (no role hidden) is + * byte-identical to before this call existed. Interacts cleanly with layer clip and scrub. + */ + setFeatureRoleVisible(role: FeatureRoleValue, visible: boolean): void { + if (this.disposed) return; + if (visible) this.hiddenFeatureRoles.delete(role); + else this.hiddenFeatureRoles.add(role); + this.applyFeatureVisibility(); + this.render(); + } + + /** The feature roles currently hidden via {@link setFeatureRoleVisible} (for state persistence). */ + getHiddenFeatureRoles(): FeatureRoleValue[] { + return [...this.hiddenFeatureRoles] as FeatureRoleValue[]; + } + + private applyFeatureVisibility(): void { + for (const mesh of this.chunkMeshes) { + const chunk = mesh.userData.chunk as GeometryChunk | undefined; + if (chunk) this.applyFeatureVisibilityToMesh(mesh, chunk); + } + } + + /** + * Collapse hidden-feature-role segments in one extrude mesh to NaN (GPU-discarded), restoring from a + * lazily-saved base copy when nothing is hidden. No-op for travel/wipe chunks (feature roles are an + * extrusion channel) and when no role is hidden and none ever was on this mesh — so unused = free. + */ + private applyFeatureVisibilityToMesh(mesh: LineSegments | Mesh, chunk: GeometryChunk): void { + if (this.ir === null || chunk.kind !== 'extrude') return; + const posAttr = (mesh.geometry as BufferGeometry).getAttribute('position') as BufferAttribute; + const arr = posAttr.array as Float32Array; + let base = mesh.userData.baseFeaturePositions as Float32Array | undefined; + if (this.hiddenFeatureRoles.size === 0) { + if (base !== undefined) { + arr.set(base); // restore, then release the base copy + posAttr.needsUpdate = true; + mesh.userData.baseFeaturePositions = undefined; + } + return; + } + if (base === undefined) { + base = arr.slice(); + mesh.userData.baseFeaturePositions = base; + } + const feature = this.ir.segments.feature; + const vertexSegment = mesh.userData.vertexSegment as Uint32Array | undefined; + const vcount = arr.length / 3; + for (let v = 0; v < vcount; v++) { + const inChunk = vertexSegment !== undefined ? vertexSegment[v] : v >> 1; + const globalSeg = chunk.segIndices[inChunk]; + if (this.hiddenFeatureRoles.has(feature[globalSeg])) { + arr[v * 3] = NaN; + arr[v * 3 + 1] = NaN; + arr[v * 3 + 2] = NaN; + } else { + arr[v * 3] = base[v * 3]; + arr[v * 3 + 1] = base[v * 3 + 1]; + arr[v * 3 + 2] = base[v * 3 + 2]; + } + } + posAttr.needsUpdate = true; + } + /** * Capability gate (§4.6): feature coloring needs the IR to actually carry * feature roles — `featureRoles: 'unavailable'` means the UI must disable it, diff --git a/packages/toolpath-core/CHANGELOG.md b/packages/toolpath-core/CHANGELOG.md index ebc482a1..96d2df6e 100644 --- a/packages/toolpath-core/CHANGELOG.md +++ b/packages/toolpath-core/CHANGELOG.md @@ -1,5 +1,19 @@ # @chestnutlabs/toolpath-core +## 0.19.0 + +### Minor Changes + +- [#423](https://github.com/ChestnutLabs/gcode-preview/pull/423) [`81690dc`](https://github.com/ChestnutLabs/gcode-preview/commit/81690dcfece21d6fd11074ad7a264bcfc9edf455) Thanks [@sobechestnut-dev](https://github.com/sobechestnut-dev)! - feat(renderer): `setFeatureRoleVisible(role, visible)` — show/hide a single feature role + + Toggle a `FeatureRole` on or off — e.g. hide `Skirt`/`Brim` to declutter a part preview, or isolate + `Support`. Feature roles live per-segment inside the extrusion geometry (unlike the whole-chunk move-kind + toggle), so hidden segments are collapsed to NaN positions and the GPU discards them; showing the role + again restores the geometry byte-for-byte. Available on `ToolpathRenderer` and via + `GcodePreviewControls.setFeatureRoleVisible` (a no-op on the 2D renderer). Additive and capability-gated: + gate the UI on `capabilities.featureRoles === 'known'`, like the feature colour mode; untouched geometry + (no role hidden) is byte-identical. Also exports `FeatureRoleValue` from `@chestnutlabs/toolpath-core`. + ## 0.18.0 ## 0.17.0 diff --git a/packages/toolpath-core/package.json b/packages/toolpath-core/package.json index 7e8414ee..f36b06ae 100644 --- a/packages/toolpath-core/package.json +++ b/packages/toolpath-core/package.json @@ -1,6 +1,6 @@ { "name": "@chestnutlabs/toolpath-core", - "version": "0.18.0", + "version": "0.19.0", "description": "Neutral ToolpathIR and capability model for the Chestnut Labs G-code Preview toolpath stack.", "keywords": [ "gcode", diff --git a/packages/toolpath-core/src/ir.ts b/packages/toolpath-core/src/ir.ts index c42f746c..dd54ae87 100644 --- a/packages/toolpath-core/src/ir.ts +++ b/packages/toolpath-core/src/ir.ts @@ -72,6 +72,8 @@ export const FeatureRole = { Purge: 14 } as const; export type FeatureRoleName = keyof typeof FeatureRole; +/** A {@link FeatureRole} value (the numeric code stored per segment in `segments.feature`). */ +export type FeatureRoleValue = (typeof FeatureRole)[FeatureRoleName]; export interface Warning { code: string; diff --git a/test-data/fixtures/annotations/skirt-brim-model.gcode b/test-data/fixtures/annotations/skirt-brim-model.gcode new file mode 100644 index 00000000..746be6b8 --- /dev/null +++ b/test-data/fixtures/annotations/skirt-brim-model.gcode @@ -0,0 +1,521 @@ +; generated by PrusaSlicer 2.7.0+win64 +; MIT-clean documentation fixture: model + skirt + brim (Chestnut Labs) +; bed_shape = 0x0,250x0,250x250,0x250 +; nozzle_diameter = 0.4 +M83 ; relative extrusion +G21 +G90 +;LAYER_CHANGE +G1 Z0.200 F1200 +;TYPE:Skirt +G1 X86.000 Y86.000 F9000 +G1 X134.000 Y86.000 E2.16000 F1200 +G1 X134.000 Y134.000 E2.16000 F1200 +G1 X86.000 Y134.000 E2.16000 F1200 +G1 X86.000 Y86.000 E2.16000 F1200 +;TYPE:Brim +G1 X95.000 Y95.000 F9000 +G1 X125.000 Y95.000 E1.35000 F1200 +G1 X125.000 Y125.000 E1.35000 F1200 +G1 X95.000 Y125.000 E1.35000 F1200 +G1 X95.000 Y95.000 E1.35000 F1200 +G1 X96.000 Y96.000 F9000 +G1 X124.000 Y96.000 E1.26000 F1200 +G1 X124.000 Y124.000 E1.26000 F1200 +G1 X96.000 Y124.000 E1.26000 F1200 +G1 X96.000 Y96.000 E1.26000 F1200 +G1 X97.000 Y97.000 F9000 +G1 X123.000 Y97.000 E1.17000 F1200 +G1 X123.000 Y123.000 E1.17000 F1200 +G1 X97.000 Y123.000 E1.17000 F1200 +G1 X97.000 Y97.000 E1.17000 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z0.400 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z0.600 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z0.800 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z1.000 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z1.200 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z1.400 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z1.600 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z1.800 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z2.000 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z2.200 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z2.400 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z2.600 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z2.800 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +M400 +; end diff --git a/test-data/fixtures/parametric/bolt-circle.ngc b/test-data/fixtures/parametric/bolt-circle.ngc new file mode 100644 index 00000000..fa423763 --- /dev/null +++ b/test-data/fixtures/parametric/bolt-circle.ngc @@ -0,0 +1,51 @@ +; RS274NGC parametric program (LinuxCNC dialect) — computed geometry demo. +; +; Nothing here spells out a hole position move-by-move: a bolt circle of holes and +; a surrounding frame are COMPUTED from a handful of parameters using expressions, +; a while-loop, and a subroutine. The parser runs this programming layer, so the +; preview shows the real toolpath the machine would follow — not an empty plate. +; +; MIT-clean, hand-authored for documentation (Chestnut Labs). + +G21 G17 G90 ; mm, XY plane, absolute +S12000 M3 ; spindle on -> productive G1 moves classify as Cut + +; --- parameters ----------------------------------------------------------- +# = 60 ; bolt-circle centre X +# = 60 ; bolt-circle centre Y +# = 34 ; bolt-circle radius +# = 8 ; number of holes +# = 46 ; frame half-width +#<_zsafe> = 3 ; retract height (global: readable inside the subroutine) +#<_zcut> = -4 ; hole depth (global: named locals are per-call-frame) + +; --- subroutine: peck-drill a hole at the current X/Y ---------------------- +o100 sub + G1 Z#<_zcut> F120 ; plunge (cut) + G0 Z#<_zsafe> ; retract (rapid) +o100 endsub + +; --- bolt circle: one hole per step, position computed with degree trig ----- +# = 0 +o200 while [# LT #] + # = [# * 360 / #] + G0 X[# + # * COS[#]] Y[# + # * SIN[#]] + o100 call + # = [# + 1] +o200 endwhile + +; --- a centre hole --------------------------------------------------------- +G0 X# Y# +o100 call + +; --- computed square frame around the pattern ------------------------------ +G0 X[# - #] Y[# - #] +G1 Z#<_zcut> F160 +G1 X[# + #] Y[# - #] +G1 X[# + #] Y[# + #] +G1 X[# - #] Y[# + #] +G1 X[# - #] Y[# - #] +G0 Z#<_zsafe> + +M5 ; spindle off +M2 ; end diff --git a/tools/demo/index.html b/tools/demo/index.html index 7e2ffd8f..014e72ad 100644 --- a/tools/demo/index.html +++ b/tools/demo/index.html @@ -217,12 +217,18 @@

Show retractions +