diff --git a/.claude/launch.json b/.claude/launch.json index 34e84800..559997bb 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -33,6 +33,28 @@ "tools/example-svelte" ], "port": 5202 + }, + { + "name": "example-vue", + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run", + "dev", + "--prefix", + "tools/example-vue" + ], + "port": 5203 + }, + { + "name": "example-webcomponent", + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run", + "dev", + "--prefix", + "tools/example-webcomponent" + ], + "port": 5204 } ] } diff --git a/CLAUDE.md b/CLAUDE.md index 28e2e7eb..c6ba4532 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,6 +66,37 @@ homepage, feature gallery, manual, demo/examples, screenshots, and the coverage 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). +## Framework parity completion check (mandatory) + +**A public capability is not "done" until it has a deliberate parity decision across every framework +adapter** (DD-031). This applies to **both** viewer surfaces: the toolpath/Preview side +(`GcodePreview` over `gcode-preview-core`) and the source-model/Prepare side (`ModelViewer` over +`gcode-model-renderer`'s `createModelPreviewController`, exposed on each adapter's `/model` subpath). +When a change adds or materially changes a consumer-meaningful capability on a controller or a +renderer, answer each — "not applicable" is a valid, explicit answer, but it must be a decision, not +an omission: + +1. Does **core** (`GcodePreviewControls` / `GcodePreviewState` / `PreviewEvent`) expose it correctly? +2. Do **Vue**, **React**, **Svelte**, and the **Web Component** expose it — declaratively where it + logically belongs, imperatively via `controls` otherwise? +3. Are the **lower-level APIs** (`useGcodePreview` / `createGcodePreview` / `element`) covered? +4. Are **events/callbacks** equivalent (allowing each framework's naming idiom)? +5. Is it exercised by the **portable behavioral suite** (`gcode-preview-core/src/testing.ts` for + toolpath; `gcode-model-renderer/src/model-testing.ts` for the model viewer)? The + controls/state-completeness **parity guards** there fail CI if a `GcodePreviewControls` / + `ModelPreviewControls` method (or a state field) isn't reachable through an adapter — add new + controls/state to the matching required list. +6. Does the **Feature Lab demo** expose it, and do the **framework showcase examples** demonstrate it + where useful? +7. Is it **documented** consistently across the adapter READMEs + `docs/manual/adapters.md`, and does + **visual coverage** (`docs/VISUAL_FEATURE_COVERAGE.md`) need updating? + +Deliberate per-framework differences (e.g. the Web Component's property-vs-attribute split and the +`` tag, Svelte's `.svelte` subpath, the `/model` subpath for source-model viewing) +are fine — but they must be **documented as intentional**, never accidental drift. +No capability should be reachable only through the `raw.renderer()` escape hatch for lack of a public +surface. The release review (`RELEASE_REVIEW.md`) carries a parity line so this is checked before a cut. + ## Repo orientation - `packages/*` — the 14 published packages (foundation → parse → color → render → adapters). Build diff --git a/README.md b/README.md index 475b215f..f419091d 100644 --- a/README.md +++ b/README.md @@ -452,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.19.0**) with npm provenance. +packages publish to npm in lockstep (currently **v0.20.0**) with npm provenance. ## Development diff --git a/RELEASE_REVIEW.md b/RELEASE_REVIEW.md index 4173b390..b53c1d25 100644 --- a/RELEASE_REVIEW.md +++ b/RELEASE_REVIEW.md @@ -12,10 +12,10 @@ parses them literally. Delete rows only for packages that did not change. --> -# Release review — v0.19.0 +# Release review — v0.20.0 -**Review version:** v0.19.0 -**Changed-capability inventory diffed against:** `v0.18.0` +**Review version:** v0.20.0 +**Changed-capability inventory diffed against:** `v0.19.0` This artifact records the per-release **Public Product + Documentation + Visual** reconciliation: before the `dev` -> `main` promotion, compare the changed capabilities below against the README, @@ -28,9 +28,13 @@ each disposition. Guidance: CLAUDE.md "Public-docs completion check" and | 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 | +| `@chestnutlabs/gcode-model-renderer` | 4 | Make interactive **source-model viewing** (STL / 3MF) a first-class, declarative half of the SDK — | Status: reviewed | +| `@chestnutlabs/gcode-preview-core` | 4 | Close the dark-capability gaps so no shipped capability is reachable only through the | Status: reviewed | +| `@chestnutlabs/gcode-preview-element` | 6 | Close the dark-capability gaps so no shipped capability is reachable only through the | Status: reviewed | +| `@chestnutlabs/gcode-preview-react` | 7 | Close the dark-capability gaps so no shipped capability is reachable only through the | Status: reviewed | +| `@chestnutlabs/gcode-preview-svelte` | 6 | Close the dark-capability gaps so no shipped capability is reachable only through the | Status: reviewed | +| `@chestnutlabs/gcode-preview-vue` | 6 | Close the dark-capability gaps so no shipped capability is reachable only through the | Status: reviewed | +| `@chestnutlabs/gcode-renderer-three` | 3 | `CaptureOptions` gains `includeBuildVolume?: boolean` (default `true`). Set it to `false` to exclude | Status: reviewed | Set each **Disposition** to one of `Status: reviewed` / `Status: no-change-needed` / `Status: not-applicable` (never `Status: pending`). @@ -42,3 +46,27 @@ Set each **Disposition** to one of `Status: reviewed` / `Status: no-change-neede - **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._ + +## Reconciliation notes (v0.20.0) + +All seven changed packages are the DD-031 consumer-UX / framework-parity pass, reconciled together: + +- **Product:** the whole point of this release is consumer-facing surface. `docs/README.md` + "Current state" leads with v0.20.0; the two-viewer (Preview + Prepare) story is accurate. The root + README and Pages homepage remain accurate (they describe the toolpath viewer + source-model + rendering; the new framework model adapters are additive API documented in the canonical adapter + docs). No existing product copy misrepresents the shipped software. +- **Docs:** `docs/manual/adapters.md` gained "Two viewers: Preview and Prepare" + a per-framework + import table; all four adapter READMEs gained a "Model viewing (Prepare side)" section; the + `gcode-model-renderer` README documents `createModelPreviewController` + the `/testing` suite; + `CLAUDE.md`'s parity check now covers both viewer surfaces. `npm run docs:links` passes. +- **Visual:** the interactive model viewer is now demonstrated in the Feature Lab + all four + showcases (Preview/Prepare toggle) and a minimal model page per framework; the coverage matrix + (`docs/VISUAL_FEATURE_COVERAGE.md` §6/§10) records this. Dark **app-UI** screenshots of Prepare + mode were captured for review but are **not yet canonical `docs/media`** — the manifest harness + emits the mid-grey capability presentation, not app chrome; that gap is explicitly recorded and + carried to the next Visual Review. No shipped capability's existing media is now stale. +- **Framework parity:** every new controller/model capability is exposed across Vue, React, Svelte, + and the Web Component (declaratively where it belongs; the `` tag and the + `/model` subpath are documented intentional differences), exercised by the portable behavioral + suites with controls/state completeness parity guards for both the toolpath and model surfaces. diff --git a/docs/README.md b/docs/README.md index 06b6a840..ae3fd213 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-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) +## Current state (updated 2026-08-30 — **v0.20.0 shipped to npm** (14 lockstep pkgs): interactive **source-model viewing** (STL / 3MF) becomes a first-class, declarative half of the SDK — the Prepare side alongside the toolpath Preview side. A framework-neutral **`createModelPreviewController`** (wrapping the existing `createModelViewer` engine unchanged) plus a **``** adapter on each framework's **`/model`** subpath — `@chestnutlabs/gcode-preview-{react,vue,svelte,element}/model` (the Web Component ships as ``) — give the source model the same declarative parity as the toolpath component: lifecycle, props/options, events, capability exposure (the honest `materials` colour tier + object/instance counts), cleanup and error handling, all locked by a portable model behavioral suite with its own controls/state parity guard ([DD-031](design/DD-031-consumer-ux-and-framework-parity.md)). Shipped with the **dark-capability closure** across all four toolpath adapters so no shipped capability is reachable only through the raw-renderer escape hatch — `getRenderStats()` render diagnostics, `pickSegment()` source-mapping, `isColorModeAvailable()`, declarative `hiddenFeatureRoles`, and handle-level `capture()` — plus `capture({ includeBuildVolume: false })` for grid-free toolpath cutouts. The demo is rebuilt as a viewport-first **Feature Lab** and every framework ships **two-tier examples** (a minimal getting-started page + a full capability-aware showcase, with a Preview/Prepare toggle) on the real published adapters. Prior: **v0.19.0** (14 lockstep pkgs): a new **`setFeatureRoleVisible(role, visible)`** on the toolpath renderer and the shared controls hides or shows a single **feature role** — e.g. hide brim/skirt to declutter a part preview, or isolate support — by collapsing those per-segment moves to GPU-discarded geometry and restoring them byte-for-byte; it's extrude-only, capability-gated (`featureRoles: 'known'`), and **byte-identical when unused**. Shipped alongside a **public-product + documentation pass**: GitHub Pages is now a visual product manual — a product-first homepage, a comprehensive **feature gallery** with real screenshots plus scrub/build-up **animations**, and screenshots across the concept pages — backed by a maintained **Visual Feature Coverage Matrix** ([`VISUAL_FEATURE_COVERAGE.md`](VISUAL_FEATURE_COVERAGE.md)), a canonical mid-grey documentation-capture look, and an **enforced per-release Public Product + Docs + Visual review** (`RELEASE_REVIEW.md`) that blocks the `dev → main` promotion until every changed capability is reconciled. Prior: **v0.18.0** (14 lockstep pkgs): the parser now executes the full **RS274NGC programming layer** that CAM / LinuxCNC output uses to *generate* geometry — numbered/named/global **parameters**, bracket **expressions** (degree trig + the full operator set), **O-word control flow** (`if`/`elseif`/`else`, `while`/`do`/`repeat`, `break`/`continue`), and in-file **subroutines** (`sub`/`call`/`return` with recursion-safe LinuxCNC scoping) — so a bolt circle written as a `while` loop, or a feature factored into a called sub, resolves to the real toolpath instead of rendering empty; all bounded (`maxProgramIterations` / `maxCallDepth` — a hostile program wastes only bounded CPU), honesty-tiered (`parametricProgram` `known` / `approximated`), and **byte-identical** for FDM / non-parametric input ([DD-017](design/DD-017-rs274ngc-parametric-programs.md), phases 1–3). Plus a generic **renderer/viewer interoperability** batch ([DD-030](design/DD-030-renderer-viewer-interop.md)): interactive **`capture()` → `Blob`** (render-to-target — user-selected thumbnails, large-file fallback, screenshots) on the controls (all four adapters inherit it) and the model viewer; **per-plate render scope** so a single-plate variant of a multi-plate project renders and frames just *that* plate (`renderModelStill({ renderScope })` + `ModelViewer.setRenderScope()`); and **non-rectangular build beds** (`BuildVolumeDef.shape` — delta / round / polygon, with the floor grid clipped to the true outline). Prior: **v0.17.0** — `frameContent: 'object'` now frames the **printed model** — a non-model classifier excludes slicer housekeeping (skirt, brim, raft, prime/wipe tower, purge) by object membership when the file carries it, else by feature role, producing a new additive `modelBounds` and a `nonModelClassification` confidence (`known` / `inferred` / `unavailable`); the framing precedence is `modelBounds → objectBounds → bounds`, so a Bambu wipe tower at the plate edge no longer inflates the frame — validated on a real 6.5 M-segment four-colour plate ([DD-026](design/DD-026-non-model-geometry-classification-and-object-framing.md) T2), and the OrcaSlicer/Bambu adapter now reads `FLUSH_START/END` and `WIPE_TOWER_START/END` brackets into first-class `Purge` / `WipeTower` roles. `renderStill` gains **`tubeByteBudget`** so a deployment with RAM to spare can retain full tubes on a large plate instead of silently falling to lines behind the conservative default — a separate axis from the parallel build's `geometryMemoryBudgetBytes` (CPU→build speed vs RAM→tubes-retained). And a geometry-worker-pool failure now degrades to a **serial main-thread tube build** — never an unhandled rejection or a silent drop to lines — so the [DD-028](design/DD-028-parallel-geometry-worker-pool.md) pool is safe to enable in headless bundlers that cannot construct the worker URL. Prior: **v0.16.0** — heavy toolpaths now build their tube geometry **off the main thread** on a capability-sized, memory-bounded worker pool — byte-identical to the serial build and degrading to continuous lines (never chopped) if a worker fails ([DD-028](design/DD-028-parallel-geometry-worker-pool.md)); the viewer prepares the **complete** geometry and reveals it **once** via `progressivePreview: 'auto' | 'lines' | 'hold' | 'off'` (`'hold'` reveals only the final, correctly-coloured build — no low-quality preview, no segment-dropping), backed by a staged `stage` event that carries a real building-geometry `%` ([DD-029](design/DD-029-preparation-and-reveal.md)); a new **render-diagnostics** surface — `getRenderStats()` plus a `renderStats` event — reports whether WebGL is running on **hardware or software**, the GPU renderer, geometry mode, segment/vertex counts, draw calls, and timings, with honest `null`s rather than guesses ([DD-027](design/DD-027-render-diagnostics.md)); and the slicer adapters now capture real **object labels and feature roles** from OrcaSlicer, PrusaSlicer, ideaMaker, and Simplify3D, with first-class non-model **FeatureRoles** — PrimeTower, WipeTower, Raft, Purge ([DD-026](design/DD-026-non-model-geometry-classification-and-object-framing.md) T1/T2). Prior: **v0.15.0** — a **`progressivePreview`** curtain (`'lines'` default / `'hold'` / `'off'`) over the during-parse preview, across the toolpath renderer, core controller, and all four adapters ([#373](https://github.com/ChestnutLabs/gcode-preview/pull/373)). `'hold'` keeps the progress signal (drive a `%` bar from `parse-progress`) but reveals **only the final, correctly-coloured, policy-quality build** — replacing the neutral-then-recolour "double render" on streamed files with a single clean reveal; `'off'` suppresses the built-in preview for a consumer-driven loading treatment; `'lines'` (the default) preserves today's streaming line preview. Orthogonal to `quality`/`qualityMode` — the revealed representation is always the policy-correct one, never a silent large-file lines fallback. Prior: **v0.14.0** — a **`qualityMode`** fidelity policy (Full / Adaptive / Fast) across the toolpath renderer, core controller, and all four adapters ([DD-023](design/DD-023-capability-aware-render-budget.md) §4 D6, Phase B) — `'full'` renders the COMPLETE representation (no every-Nth decimation, full-radial continuous tubes, no budget→lines fallback); `'adaptive'` (default) is the capability-aware auto path; `'fast'` is flat lines. Plus a **continuity correction: tubes are never segment-decimated** — `autoDecimation`'s every-Nth drop was chopping tube geometry into disconnected stubs (the RR-006 continuity break via the decimation lever the v0.12.0 fix didn't cover); tube memory is now bounded ONLY by continuity-preserving radial reduction, degrading to continuous lines when even a 3-sided tube won't fit — never chopped. The honest order is full-radial tubes → lower-radial tubes → continuous lines. (Classifier-driven auto budget + the too-heavy signal land later.) Prior: **v0.13.0** (14 lockstep pkgs), a farm-workload batch: render the colour/structure a source model actually carries, stop over-degrading normal large plates, and lay the capability-aware groundwork. **Toolpath:** the tube memory budget now counts only **extrude** segments (not travel/wipe), so a heavy-travel plate (800+ parts) renders as continuous tubes instead of dropping to lines; a new **`filament` colour mode** renders a toolpath in the file's OWN filament colours (resolved from parse metadata before the first build — no neutral-then-recolour flash on raw `.gcode`). **Source model** ([`gcode-model-renderer`](../packages/gcode-model-renderer)): **multi-plate structure** ([DD-025](design/DD-025-multi-plate-model-structure.md) Phase A — `ModelScene.plates` + placement-level `plateIds` + `capabilities.plates`, declared-only), **Bambu/Orca per-object extruder colours** (a `.3mf` coloured via the project convention renders in colour, not neutral), and **staged loading progress** ([DD-024](design/DD-024-staged-loading-progress.md) Phase A — typed, consumer-neutral, generation-scoped, never a faked %). **Capability groundwork** ([DD-023](design/DD-023-capability-aware-render-budget.md) Phase A): a shared fail-safe WebGL hw/sw classifier + `QualityPolicy`/`CapabilityHint` types (no behaviour change yet; capability-aware budgets + `qualityMode` land later). Prior: **v0.12.0** — fixes the two regressions surfaced against v0.11.0 and promotes the tube memory bound to an honest control. **Tube rendering** ([RR-006](research/RR-006-tube-mesh-memory-and-large-file-budget.md) correction) now bounds memory by **coarsening the cross-section** (fewer radial sides) at full segment continuity — replacing the v0.10.0 drop-every-Nth-segment budget that shredded tubes into spiky capped stubs; new public `tubeByteBudget` option, `tubeSegmentBudget` deprecated, lines fallback only when even a 3-sided tube can't fit. **Model estimate** now counts **unique** master triangles, not baked-per-placement, so a full-sheet instanced `.3mf` plate (~40 reused copies) parses/renders at default limits instead of falsely rejecting as "too large" (instance bomb still guarded by `maxInstances`). Prior: **v0.11.0** — **instance-aware source-model rendering** ([DD-022](design/DD-022-model-instancing-and-lod.md), Phases 0–1) — a full-sheet `.3mf` that reuses a master mesh via production-extension components / repeated build items now renders each master **once** via GPU instancing (three `InstancedMesh`) instead of baking a copy per placement, so memory scales with unique geometry not copy count (a ~40× instanced plate that used to exceed the triangle limit now renders); plus a **sub-second byte-estimate fast-reject** for genuinely oversize / instance-bomb files, and `instancedCount` + `decimationApplied` disclosure on the model `ready`/still result. **v0.10.0** added a **tube-mesh memory budget** so large forced-`tubes` renders (≈1.6 M+ segments) no longer OOM a 2 GB render worker — tubes decimate to a `TUBE_SEGMENT_BUDGET`, disclosed via `decimationApplied` (also on `RenderStillResult`), layer boundaries preserved ([RR-006](research/RR-006-tube-mesh-memory-and-large-file-budget.md)); plus a fix so `frameContent:'object'` frames the printed object on AnycubicSlicerNext and every object-labeling file (`objectBounds` is refreshed after dialect annotation). **v0.9.0** shipped the **interactive source-model viewer** `createModelViewer` in `@chestnutlabs/gcode-model-renderer` — the live orbit/zoom/pan analogue of `renderModelStill` for an STL or 3MF, over a shared interactive "stage" extracted in `gcode-renderer-three`, with an open-`kind` model-source loader registry (STL/3MF now, OBJ/STEP/PLY later without an API change), capability-honest color pass-through, and a `renderer-unsupported` fallback ([DD-021](design/DD-021-interactive-model-viewer.md)). v0.8.1 added production Bambu/Orca 3MF `paint_color` multicolor decode ([RR-005](research/RR-005-3mf-paint-color-facet-format.md)) so a designer's painted model renders in its true colors without slicing, plus a filament-slot dedup fix. v0.7.0 shipped the presentation package `@chestnutlabs/gcode-model-renderer` (headless `renderModelStill`, STL + 3MF multi-object/material, [DD-018](design/DD-018-model-renderer.md)), the 3MF Production-Extension (`p:path`) reader, and a toolpath-renderer batch — slice metadata on `ready`, structured multi-plate metadata, independent build-volume cage, frame-to-content, interaction-aware render quality ([DD-020](design/DD-020-interaction-aware-quality.md)), and a `renderStill` background option. v0.5.x added camera preset-views + serializable `CameraState`, zoom-to-cursor, keyboard-a11y, `capabilities`/`warnings` on `ready`, and the Vue/Svelte adapter mount-prop fixes (epics #279/#296/#306/DD-021). DD-017 RS274NGC parametric programs proposed, unbuilt) | Epic | Status | Gate | |---|---|---| @@ -67,7 +67,7 @@ golden/native fixtures + manifest with CI validation, boundary lint, license CI consumer smoke harnesses, plus the five framework adapters (`gcode-preview-core`/`-vue`/`-react`/ `-svelte`/`-element`) and the presentation renderer (`gcode-model-renderer`). CI (`build`) is a required check on `main`/`dev`. **All fourteen `@chestnutlabs/*` packages are published to npm at -`v0.19.0`** (lockstep, npm provenance); `main` is the release branch, publishing from tagged GitHub +`v0.20.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` @@ -80,5 +80,10 @@ programming layer — parameters/expressions/O-word control flow/subroutines DD- renderer/viewer interop — interactive `capture()`, per-plate render scope, non-rectangular build beds 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). +Visual Feature Coverage Matrix, and the enforced per-release Product/Docs/Visual review gate; +`v0.20.0` source-model viewing as a first-class declarative half of the SDK — `createModelPreviewController` ++ `` on each framework's `/model` subpath (`` for the Web Component), +the toolpath dark-capability closure (`getRenderStats`/`pickSegment`/`isColorModeAvailable`/declarative +`hiddenFeatureRoles`/handle `capture`) + `capture({ includeBuildVolume })`, the Feature Lab demo, and +two-tier framework examples DD-031). Changesets accumulate on `dev` for the next release. diff --git a/docs/VISUAL_FEATURE_COVERAGE.md b/docs/VISUAL_FEATURE_COVERAGE.md index 7d2dee43..b2e0c219 100644 --- a/docs/VISUAL_FEATURE_COVERAGE.md +++ b/docs/VISUAL_FEATURE_COVERAGE.md @@ -91,6 +91,33 @@ Genuinely deferred, each with a recorded reason (valid dispositions, not silent These are tracked here so the next release's Public Product + Documentation + Visual Review picks them up rather than rediscovering them. +## This pass (2026-08-29, DD-031) — consumer UX & framework parity + +The consumer-experience pass reworked the *demonstration surfaces* (it added no new engine +capabilities, so the per-capability rows below are unchanged): + +- The demo is rebuilt as the **Feature Lab** on the published `gcode-preview-core` controller — + viewport-first, intent-grouped controls, and **capability-aware** UI (a color mode greys out with + a plain-language reason when the file can't support it; the CNC tab appears only when a tool-power + channel exists; capability confidence is shown as tiered badges). +- Every adapter now ships a **two-tier example** (`tools/example-{react,vue,svelte,webcomponent}`): + a genuinely minimal getting-started page and a full **showcase** that mirrors the Feature Lab's + capability-aware UX in that framework's idiom, on the shared demo-kit. This is what makes "find the + interactive version of everything in the docs" true per-framework, not just in the demo. +- Review screenshots of the Feature Lab and the four showcases were captured this pass (dark app UI). + These are **not yet canonical `docs/media` assets**: the manifest harness produces the mid-grey + capability presentation, not app chrome. Deferred: teach the harness to emit app-UI shots (or add a + dark-app class) so these become tracked media — carried to the next Visual Review. +- `getRenderStats` diagnostics and `pickSegment` picking are now reachable through the **public + adapter surface** in all four showcases (previously controls/raw-only) — the dark-capability + closure that section 1's `pickSegment` row and section 4's diagnostics row depend on. +- **Source-model viewing became a first-class declarative half of the SDK**: a `` adapter + on each framework's `/model` subpath (over the new `createModelPreviewController`), a Preview/Prepare + toggle in the Feature Lab and all four showcases, and a minimal model page per framework. This moves + the interactive model viewer (§6) from "needs demo exposure" to demonstrated everywhere consumers + look. App-UI screenshots of Prepare mode were captured for review but, like the toolpath showcase + shots, are not yet canonical `docs/media` (same harness gap). + ## 1. Toolpath inspection | Capability | User-facing | Demo | Media | README | Pages gallery | Manual/pkg | Status | @@ -165,7 +192,8 @@ them up rather than rediscovering them. | Toolpath vs source-model (two renderers) | ✅ | model page | `model-render-stl-3mf` | ✅ | gallery | — | covered | | STL presentation still | ✅ | model page | (in pair) | ✅ | gallery | model-renderer README | covered | | 3MF material/color presentation | ✅ | model page | (in pair) | ✅ | gallery | model-renderer README | covered | -| Interactive model viewer (`createModelViewer`) | ✅ | model-viewer page | — | ✅ (prose) | gallery | model-renderer README | needs demo exposure (link) + needs animation | +| Interactive model viewer (`createModelViewer`) | ✅ | model-viewer page + Feature Lab (Prepare) | — | ✅ (prose) | gallery | model-renderer README | covered (interactive) — Feature Lab + all 4 showcases have a Prepare mode; animation still nice-to-have | +| Model-viewer framework adapters (`` / `/model`) | ✅ | Feature Lab + 4 showcases (Prepare) + model.html minimals | — | ✅ (table) | — | adapters + 4 READMEs | covered (code + examples); app-UI media pending harness support | | Multi-object files | ✅ | model page | — | mention | gallery | — | needs screenshot | | Multi-plate files + per-plate render | ✅ | ❌ | — | ❌ | gallery | — | needs demo exposure + needs comparison | | Render scope / object subsets (`RenderScope`) | ✅ | ❌ | — | ❌ | gallery | recipes | needs demo exposure + needs comparison | @@ -207,7 +235,8 @@ them up rather than rediscovering them. | Capability | User-facing | Demo | Media | README | Pages gallery | Manual/pkg | Status | |---|:---:|:---:|:---:|:---:|:---:|:---:|---| -| Vue / React / Svelte / Web Component adapters | ✅ | examples | — | ✅ (table) | — | adapters | not visually meaningful (code) | +| Vue / React / Svelte / Web Component adapters | ✅ | two-tier examples + Feature Lab | — | ✅ (table) | — | adapters | covered (code + examples) — each adapter has a minimal + showcase example on the shared design language; canonical app-UI media pending harness support | +| Capability-aware consumer UX (gated modes + reasons, confidence tiers, contextual controls) | ✅ | Feature Lab + 4 showcases | — | ✅ (prose) | — | adapters | demonstrated in demo + examples; review captures exist (dark app UI), not yet canonical mid-grey media | | Headless toolpath still (`renderStill`) | ✅ | still page | — | ✅ | gallery | reference/still-render | needs screenshot (server-thumbnail context) | | Headless model still (`renderModelStill`) | ✅ | model page | `model-render-stl-3mf` | ✅ | gallery | model-renderer README | covered | | Off-thread parsing / cancellation / limits | ✅ | ✅ | — | ✅ (prose) | — | concept-workers | not visually meaningful | diff --git a/docs/design/DD-031-consumer-ux-and-framework-parity.md b/docs/design/DD-031-consumer-ux-and-framework-parity.md new file mode 100644 index 00000000..84a03505 --- /dev/null +++ b/docs/design/DD-031-consumer-ux-and-framework-parity.md @@ -0,0 +1,326 @@ +# DD-031 — Consumer SDK experience, framework parity, and the Feature Lab + +**Status:** Accepted +**Authors/Owners:** Nathaniel Chestnut (owner), Claude Code (lead) +**Date:** 2026-08-28 · **Last revised:** 2026-08-28 + +> **Owner decisions (2026-08-28):** (1) The flagship Feature Lab is rebuilt on the **published +> framework-neutral `gcode-preview-core` controller**; the raw renderer stays a lower-level/internal +> escape hatch, not the flagship's path. (2) Shared showcase/control logic goes in an **unpublished +> workspace-internal demo-kit**; **no** styled viewer kit or headless-control package is published +> this pass (re-evaluated later from what the new UX teaches us — §12/§22). (3) **All** identified +> dark-capability gaps are closed in the public controller + all four adapters. Every new public +> controller capability **requires equivalent exposure across Vue/React/Svelte/Web Component where +> applicable, plus parity tests and documentation, before the visual redesign begins** (§4.4→§9→§4.5 +> ordering is mandatory). +**Owning Epic:** Consumer-UX productization · **Milestone:** post-v0.19.0 +**Supersedes / Superseded by:** none +**Related:** DD-007 (adapter boundary), DD-013 (docs/manual), DD-014 (2D renderer), DD-018/021 (model renderer/viewer), DD-027 (render diagnostics), DD-030 (renderer/viewer interop), `docs/VISUAL_FEATURE_COVERAGE.md` + +--- + +## 1. Problem + +The engine and its published API have grown enormously (14 lockstep packages, four framework +adapters, a mature Three.js renderer, a model viewer, CNC/laser support, render diagnostics, +parallel geometry, capture, staged progress). The **consumer-facing experience** — the demo, the +examples, the framework adapters' discoverability, and the coherence of the public surface — has not +kept pace. The main demo still structurally resembles the inherited `xyz-tools/gcode-preview` +engineering harness (a 300px fixed sidebar of history-ordered `
`s beside a canvas), the +examples are minimal-only, there is **no Web Component example at all**, and several shipped +capabilities are reachable only through escape hatches. This DD records the audit (Phase 1), the +architecture decisions (Phase 2), and the parity/discoverability contract that makes framework +parity part of feature completion going forward. + +## 2. Scope + +- A factual inventory of the consumer capability surface (this doc, §4.1). +- The **Framework Feature Parity Matrix** (§4.2) and **demo/example exposure** (§4.3). +- Identified **API gaps** where shipped capability is unreachable or awkward through the public + surface (§4.4), and the additive core changes that close them. +- The main-demo **UX architecture** redesign into a "Feature Lab" (§4.5) and its **design tokens**. +- A **two-tier example strategy** (minimal + full showcase) per framework incl. Web Component, with + shared, non-bypassing example infrastructure (§4.6). +- **Parity tests** that lock capability equivalence across adapters (§9). +- The durable **parity-completion rule** added to project process (§4.7). + +## 3. Non-goals + +- No breaking changes to published API for cosmetic reasons (DD-002 versioning still governs; see + §10). Additive-only core changes here. +- Not turning `` into an opinionated slicer application — the SDK component stays + embeddable; the Feature Lab is a shell *around* it. +- **A new published "viewer kit" package is out of scope for this pass** (evaluated in §12; recorded + as a possible future epic). This pass may extract framework-neutral *control-model logic* into an + existing package where it removes a real consumer burden, but ships no new styled-component package. +- The renderer scene theme and the documentation-capture theme are unchanged (DD-009 / the v0.19.0 + doc pass own those); only the *application chrome* of the demo/examples is designed here. + +## 4. Data contracts / API + +### 4.1 Consumer capability inventory (today) + +The framework-neutral controller (`gcode-preview-core/controller.ts`) is the canonical vocabulary: +`GcodePreviewControls` (25 imperative methods), `GcodePreviewState` (reactive snapshot), +`PreviewEvent` (13-variant union), `PreviewControllerOptions.renderer` (declarative options). The +Three renderer (`ToolpathRenderer`) and 2D renderer (`LayerView2D`) sit behind `PreviewRenderer`; +the model viewer (`ModelViewer`) is a separate product surface. + +Consumer-meaningful capabilities the SDK supports today: + +- **Parse/render**: source (bytes/File), parse options, renderer mode 2d/3d, cancel, progressive + partial streaming. +- **Inspect**: layer range, segment scrub, time scrub, move-kind visibility (travel/wipe/extrude), + retraction/deretraction markers, feature-role visibility (skirt/brim/…), segment picking + (`pickSegment` — source mapping). +- **Appearance**: 10 color modes — `single`, `tool`, `feature`, `colorChange`, `filament`, + `feedrate` (speed), `object` (+isolate), `layerHeight`, `power` (CNC/laser), `moveKind` + (cut-vs-rapid); declarative `theme`; material preset. +- **View/camera**: perspective/orthographic, 7 presets (top/bottom/front/back/left/right/iso), + camera-state get/set (serializable), `frame()`, `frameContent` object-vs-all, build-volume cage. +- **Rendering**: quality tier (auto/tubes/lines), fidelity policy (`full`/`adaptive`/`fast`), + progressive-preview curtain (auto/lines/hold/off), interaction-aware quality, capture → Blob, + tube byte budget, parallel geometry pool. +- **Machine/geometry**: build volume rect/round/polygon, excluded regions, bed surface, consumer- + wins discovery precedence. +- **Live progress**: DD-006 observation mapping, presentation (exact/approximate/stale/hidden), + staleness ticking, telemetry-tier honesty. +- **Diagnostics/honesty**: per-field capability confidence, warnings, disclosures, `getRenderStats` + (backend/GPU/geometry-mode/parallelism/timings), capability gates (`isColorModeAvailable`, + `hasRetractions`, `hasColorChanges`). +- **Model product**: STL/3MF presentation, material confidence, plates, render scope, instancing, + model capture. + +### 4.2 Framework Feature Parity Matrix (as audited, pre-fix) + +Legend: **prop** = declarative prop; **attr** = observed+reflected HTML attribute; **prop-only** = +JS property (not attribute-settable); **ctl** = reachable only via `controls.*`; **raw** = reachable +only via `raw.renderer()` escape hatch; **✗** = absent; **—** = not applicable. Demo/Example/Tested +mark exposure in the main demo, the framework examples, and the portable behavioral suite. + +| Capability | Core | Vue | React | Svelte | WC (Element) | Demo | Example | Tested | +|---|---|---|---|---|---|---|---|---| +| source / parse | ✓ | prop | prop | prop | prop-only | ✓ | ✓ | ✓ | +| renderer mode 2d/3d | ✓ | prop | prop | prop | **attr, unobserved, no accessor** | (2d.html) | ✗ | ✗ | +| layer range | ✓ | prop | prop | prop | attr | ✓ | ✓ | ✗ | +| segment scrub | ✓ | prop | prop | prop | attr | ✓ | ✓ | ✓ | +| time scrub | ✓ | prop | prop | prop | attr | ✓ | ✗ | ✗ | +| move-kind (travel/wipe) | ✓ | prop | prop | prop | attr | ✓ | ✗ | ✗ | +| retraction markers | ✓ | prop | prop | prop | attr | ✓ | ✗ | ✗ | +| **feature-role visibility** | ctl | **ctl** | **ctl** | **ctl** | **ctl** | ✓ | ✗ | ✗ | +| color modes (10) | ✓ | prop | prop | prop | prop-only | 7/10 | ✗ | ✗ | +| quality tier | ✓ | prop | prop | prop | attr | ✓ | fixed | ✗ | +| fidelity policy (qualityMode) | ✓ | prop | prop | prop | attr | ✗ | ✗ | ✗ | +| progressive preview | ✓ | prop | prop | prop | attr | ✓ | ✗ | ✗ | +| camera mode (proj) | ✓ | prop | prop | prop | attr | ✓ | ✗ | ✗ | +| camera presets (view) | ✓ | prop | prop | prop | attr | ✓ | ✗ | ✓ | +| camera state get/set | ✓ | prop+ctl | prop+ctl | prop+ctl | prop+method | ✓ | ✗ | ✓ | +| **frame()** | ctl | **ctl** | **ctl** | **ctl** | **ctl** | ✓ | ✗ | ✗ | +| frame content object/all | ✓ | prop | prop | prop | attr | ✗ | ✗ | ✗ | +| interaction quality | ✓ | prop | prop | prop | attr | ✗ | ✗ | ✗ | +| theme | ✓ | prop | prop | prop | prop-only | ✓ | ✗ | ✗ | +| build volume | ✓ | prop | prop | prop | prop-only | (discovered) | ✗ | evt | +| build-volume cage | ✓ | prop | prop | prop | attr | ✗ | ✗ | ✗ | +| bed shapes rect/round/poly | ✓ | prop | prop | prop | prop-only | (shots) | ✗ | ✗ | +| **capture → Blob** | ctl | **ctl** | **ctl** | **ctl** | method | ✓ | ✗ | ✗ | +| live progress (observe) | ✓ | prop+handle | prop+handle | prop+handle | prop+method | ✓ | ✓ | ✓ | +| progress presentation | ✓ | state | state | state | state | ✓ | ✓ | ✓ | +| **render diagnostics (getRenderStats)** | **raw** | **raw** | **raw** | **raw** | **raw** | ✓ | ✗ | ✗ | +| **segment picking (pickSegment)** | **raw** | **raw** | **raw** | **raw** | **raw** | ✗ | ✗ | ✗ | +| capability gates (isColorModeAvailable/hasRetractions/hasColorChanges) | renderer | raw | raw | raw | raw | ✓ | ✗ | ✗ | +| CNC/laser power color | ✓ | prop | prop | prop | prop-only | ✗ | ✗ | ✗ | +| cut-vs-rapid (moveKind) color | ✓ | prop | prop | prop | prop-only | ✗ | ✗ | ✗ | +| model rendering (STL/3MF) | ModelViewer | ✗ | ✗ | ✗ | ✗ | (model*.html) | ✗ | pkg-local | +| render scope (plate/object) | ModelViewer | ✗ | ✗ | ✗ | ✗ | (model-viewer.html) | ✗ | pkg-local | +| plates | ModelViewer | ✗ | ✗ | ✗ | ✗ | (model-viewer.html) | ✗ | pkg-local | + +**Finding: Vue/React/Svelte declarative parity is actually strong** (~24 aligned props each); the +real problems are (a) a set of capabilities that are *dark in every adapter* (feature-role +visibility, frame, render diagnostics, segment picking — controls/raw-only, no declarative surface), +(b) **Web Component divergence** (unobserved construction attrs, property-only inputs that can't be +set in markup, event-payload wrapping drift), and (c) documentation that both undersells (READMEs +list ~10 of ~24 props) and overclaims (`adapters.md` "identical across adapters" is wrong for the WC). + +### 4.3 Demo / example exposure (today) + +- **Main demo** (`tools/demo`, `main.js` 630 lines, `index.html` 355 lines): feature-rich — exposes + most toolpath capabilities — **but imports the raw `ToolpathRenderer` + `GcodeParseSession` + directly, bypassing every published package.** It therefore cannot catch an adapter regression and + reaches renderer-only capabilities (`getRenderStats`, capability gates) that the controller does + not expose. Layout: fixed 300px sidebar, `
` groups **ordered by feature-development + history**, single inline ` - -
- + -
- -
+ +
+ Ready — pick a sample and press Load. + · + +
+ - + diff --git a/tools/demo/package.json b/tools/demo/package.json index bdda57d2..3ec6948a 100644 --- a/tools/demo/package.json +++ b/tools/demo/package.json @@ -19,7 +19,8 @@ "vue": "^3.4.0", "@chestnutlabs/gcode-colors": "file:../../packages/gcode-colors", "@chestnutlabs/gcode-renderer-2d": "file:../../packages/gcode-renderer-2d", - "@chestnutlabs/gcode-model-renderer": "file:../../packages/gcode-model-renderer" + "@chestnutlabs/gcode-model-renderer": "file:../../packages/gcode-model-renderer", + "@chestnutlabs/gcode-preview-core": "file:../../packages/gcode-preview-core" }, "devDependencies": { "vite": "^6.0.0" diff --git a/tools/demo/src/main.js b/tools/demo/src/main.js index cb5e8c0e..21290f34 100644 --- a/tools/demo/src/main.js +++ b/tools/demo/src/main.js @@ -1,73 +1,39 @@ -/** - * DD-004 phase 3 demo (issue #58): the full pipeline — GcodeParseSession - * (worker parse, off-thread, zero-copy transfer) → ToolpathRenderer (incremental - * upload, draw-range clipping/scrub, capability-honest coloring) — over the - * inherited MIT demo corpus or any local G-code file. +/* + * G-code Preview — Feature Lab (DD-031). A viewport-dominant, capability-aware showcase built on the + * PUBLISHED framework-neutral controller (`@chestnutlabs/gcode-preview-core`) — the same contract an + * external consumer uses, so this demo dogfoods the real adapter surface rather than the raw renderer. + * Shared control metadata / palettes / fixtures / tokens come from the workspace-internal demo-kit. */ -import { GcodeParseSession, CancelledError } from '@chestnutlabs/gcode-parser'; -import { ToolpathRenderer } from '@chestnutlabs/gcode-renderer-three'; +import '../../demo-kit/tokens.css'; +import { createPreviewController } from '@chestnutlabs/gcode-preview-core'; +// DD-031: the Prepare-side controller for source-model (STL/3MF) viewing — the counterpart to the +// toolpath controller above. The flagship demonstrates both halves of the SDK. +import { createModelPreviewController } from '@chestnutlabs/gcode-model-renderer'; +import { FeatureRole } from '@chestnutlabs/toolpath-core'; import { - createProgressMapper, - computeToolpathTime, - segmentsCompletedAtTime, - FeatureRole -} from '@chestnutlabs/toolpath-core'; + COLOR_MODES, + COLOR_MODE_BY_ID, + colorModeReason, + confidenceTier, + rgbCss, + FIXTURE_GROUPS, + FIXTURE_BY_ID, + MODEL_FIXTURES, + MODEL_FIXTURE_BY_ID, + count, + duration, + timeSourceNote +} from '../../demo-kit/index.js'; import { downloadToolpathStl } from './stl-export.js'; -// Inherited MIT demo corpus (see test-data/manifest.json), served by Vite's publicDir. -const CORPUS = [ - ['gcodes/3DBenchy.gcode', '3DBenchy (3.7 MB)'], - ['gcodes/calicat.gcode', 'Calicat (635 KB)'], - ['gcodes/vase.gcode', 'Vase (spiral mode)'], - ['gcodes/screw.gcode', 'Screw'], - ['gcodes/plant-sign.gcode', 'Plant sign'], - ['gcodes/easel.gcode', 'Easel (19 KB)'], - ['gcodes/mach3.gcode', 'Mach3 (CNC-style)'], - ['fixtures/parametric/bolt-circle.ngc', 'Parametric bolt-circle (RS274NGC)'], - ['fixtures/containers/mini-project.gcode.3mf', 'mini-project.gcode.3mf (container)'], - ['fixtures/annotations/wipe-brackets.gcode', 'Wipe brackets (#182 demo)'], - ['fixtures/annotations/variable-layers.gcode', 'Variable layer height (#179 demo)'], - ['fixtures/annotations/skirt-brim-model.gcode', 'Model + skirt + brim (framing / hide-adhesion)'] -]; - -const TOOL_PALETTE = [ - [0.9, 0.4, 0.7], - [0.35, 0.7, 0.95], - [0.95, 0.75, 0.3], - [0.5, 0.9, 0.5] -]; -const FEATURE_PALETTE = [ - [0.9, 0.4, 0.7], - [0.35, 0.7, 0.95], - [0.95, 0.75, 0.3], - [0.5, 0.9, 0.5], - [0.8, 0.5, 0.95] -]; +const els = {}; +for (const el of document.querySelectorAll('[id]')) els[el.id] = el; -// Color-by-layer-height ramp (#179): thin → thick as blue → green → yellow → red. -const HEIGHT_RAMP = [ - [0.15, 0.4, 0.9], - [0.2, 0.85, 0.45], - [0.95, 0.85, 0.2], - [0.9, 0.3, 0.2] -]; -// Color-by-speed ramp (#177): slow → fast as blue → yellow → red. -const SPEED_RAMP = [ - [0.12, 0.36, 0.95], - [0.97, 0.86, 0.2], - [0.92, 0.22, 0.15] -]; -const OBJECT_PALETTE = [ - [0.35, 0.7, 0.95], - [0.95, 0.55, 0.3], - [0.5, 0.88, 0.5], - [0.9, 0.42, 0.72] -]; - -// Named scene themes (#153, DD-009 D4): each is a bounded declarative Theme. -// Unspecified fields fall back to the default look (replace semantics). +// Named scene themes (#153, DD-009 D4) — bounded declarative Theme objects; unspecified fields keep defaults. const THEMES = { default: {}, + // The canonical neutral mid-grey documentation workspace (matches the screenshot harness). + docgrey: { background: '#6d7176', gridColor: '#565a60', bedColor: '#565a60', hemisphereIntensity: 2.1 }, blueprint: { background: '#0d1b2a', gridColor: '#1b9aaa', bedColor: '#415a77', hemisphereIntensity: 2.2 }, light: { background: '#eef1f5', @@ -76,555 +42,595 @@ const THEMES = { hemisphereIntensity: 2.4, directionalIntensity: 1.4 }, - // #185: a filled build-plate surface under the toolpath (off in every other theme). bedplate: { background: '#10141b', gridColor: '#3a4658', bedColor: '#556173', - hemisphereIntensity: 2.0, bedSurface: { mode: 'solid', color: '#20242c', opacity: 1 } } }; +const themeFor = () => ({ ...(THEMES[els.theme.value] ?? {}), materialPreset: els.material.value }); -function themeFor(name, material) { - return { ...(THEMES[name] ?? THEMES.default), materialPreset: material }; +// ---- controller (the published core, 3D by default) ---- +const preview = createPreviewController({ + renderer: { quality: 'auto', qualityMode: 'adaptive', progressivePreview: 'auto', interactionQuality: 'off' } +}); +preview.bindCanvas(els.view); + +// mutable view state +let fileBytes = 0; +let capabilities = {}; +let savedCamera = null; +let ready = false; + +// ---- Prepare (source-model) mode: the second half of the SDK, lazily created on first use ---- +let mode = 'preview'; // 'preview' (toolpath) | 'prepare' (source model) +let modelController = null; +let modelInfo = null; + +function ensureModel() { + if (modelController !== null) return modelController; + modelController = createModelPreviewController({ background: 'transparent', interactionQuality: 'auto' }); + modelController.bindCanvas(els.modelView); + modelController.onEvent((e) => { + if (e.type === 'ready') { + modelInfo = e.info; + renderModelInfo(); + } else if (e.type === 'error') { + els.status.textContent = `Model error: ${e.code} — ${e.message}`; + } + }); + return modelController; } -const $ = (id) => document.getElementById(id); -const els = { - fixture: $('fixture'), - file: $('file'), - parse: $('parse'), - cancel: $('cancel'), - progress: $('progress'), - status: $('status'), - startLayer: $('startLayer'), - endLayer: $('endLayer'), - scrub: $('scrub'), - startLayerVal: $('startLayerVal'), - endLayerVal: $('endLayerVal'), - scrubVal: $('scrubVal'), - travel: $('travel'), - wipe: $('wipe'), - retractions: $('retractions'), - colorMode: $('colorMode'), - quality: $('quality'), - cameraMode: $('cameraMode'), - theme: $('theme'), - material: $('material'), - qualityNote: $('qualityNote'), - frame: $('frame'), - exportStl: $('exportStl'), - timeScrub: $('timeScrub'), - timeScrubVal: $('timeScrubVal'), - printTimeNote: $('printTimeNote'), - saveView: $('saveView'), - restoreView: $('restoreView'), - disclosure: $('disclosure'), - stats: $('stats'), - progressTier: $('progressTier'), - progressPlay: $('progressPlay'), - progressNote: $('progressNote'), - hideAdhesion: $('hideAdhesion'), - progressive: $('progressive'), - capturePng: $('capturePng'), - showStats: $('showStats'), - renderStats: $('renderStats'), - canvas: $('view') -}; - -for (const [file, label] of CORPUS) { - const opt = document.createElement('option'); - opt.value = file; - opt.textContent = label; - els.fixture.appendChild(opt); +function renderModelInfo() { + if (modelInfo === null) { + els.modelInfo.textContent = '—'; + return; + } + const t = confidenceTier(modelInfo.materials); + const plates = modelInfo.plates ? `\nplates: ${modelInfo.plates.list.length}` : ''; + els.modelInfo.innerHTML = + `objects: ${count(modelInfo.objectCount)}\n` + + `placements: ${count(modelInfo.instancedCount)}\n` + + `materials: ${t.label}${plates}`; } -const session = new GcodeParseSession(); -const renderer = new ToolpathRenderer({ - canvas: els.canvas, - buildVolume: { x: 220, y: 220, z: 250 }, - quality: 'auto' -}); - -let parsing = false; -let travelAvailable = false; -let bedNote = ''; - -function setStatus(text, isError = false) { - els.status.textContent = text; - els.status.classList.toggle('error', isError); +function populateFixtures(prepare) { + els.fixture.innerHTML = ''; + if (prepare) { + for (const m of MODEL_FIXTURES) { + const o = document.createElement('option'); + o.value = m.id; + o.textContent = m.label; + els.fixture.appendChild(o); + } + els.fixture.value = 'colored-3mf'; + } else { + for (const g of FIXTURE_GROUPS) { + const og = document.createElement('optgroup'); + og.label = g.group; + for (const it of g.items) { + const o = document.createElement('option'); + o.value = it.id; + o.textContent = it.label; + og.appendChild(o); + } + els.fixture.appendChild(og); + } + els.fixture.value = 'skirt-brim'; + } } -function colorModeFor(kind) { - if (kind === 'tool') return { mode: 'tool', palette: TOOL_PALETTE, fallback: [0.7, 0.7, 0.7] }; - if (kind === 'feature') return { mode: 'feature', palette: FEATURE_PALETTE, fallback: [0.55, 0.55, 0.55] }; - if (kind === 'colorChange') return { mode: 'colorChange', palette: TOOL_PALETTE, fallback: [0.55, 0.55, 0.55] }; - if (kind === 'layerHeight') return { mode: 'layerHeight', ramp: HEIGHT_RAMP, fallback: [0.6, 0.6, 0.6] }; - if (kind === 'feedrate') return { mode: 'feedrate', ramp: SPEED_RAMP, fallback: [0.55, 0.6, 0.62] }; - if (kind === 'object') return { mode: 'object', palette: OBJECT_PALETTE, fallback: [0.55, 0.55, 0.55] }; - return { mode: 'single', color: [0.9, 0.4, 0.7] }; +function setMode(next) { + if (next === mode) return; + mode = next; + const prepare = next === 'prepare'; + for (const b of els.modeSeg.children) b.classList.toggle('gp-on', b.dataset.mode === next); + els.view.hidden = prepare; + els.modelView.hidden = !prepare; + // Toolpath tabs are irrelevant in Prepare; show only the Prepare tab and select it (and vice-versa). + for (const t of els.tabs.querySelectorAll('.gp-tab')) { + if (t.dataset.tab !== 'prepare') t.hidden = prepare; + } + document.querySelector('[data-tab="prepare"]').hidden = !prepare; + selectTab(prepare ? 'prepare' : 'inspect'); + populateFixtures(prepare); + els.legendCard.hidden = true; + els.countPill.hidden = true; + // The bottom scrub strip (layers/segments/time) is toolpath-only — hide it in Prepare. + els.scrubStrip.hidden = prepare; + if (prepare) ensureModel(); + loadSelected(); } -function applyLayerRange() { - let start = Number(els.startLayer.value); - let end = Number(els.endLayer.value); - if (start > end) { - // Keep the pair ordered no matter which slider moved. - if (document.activeElement === els.startLayer) end = start; - else start = end; - els.startLayer.value = String(start); - els.endLayer.value = String(end); +// ============ fixture picker ============ +for (const g of FIXTURE_GROUPS) { + const og = document.createElement('optgroup'); + og.label = g.group; + for (const it of g.items) { + const o = document.createElement('option'); + o.value = it.id; + o.textContent = it.label; + og.appendChild(o); } - els.startLayerVal.textContent = String(start); - els.endLayerVal.textContent = String(end); - renderer.setLayerRange(start, end); + els.fixture.appendChild(og); } - -function applyScrub() { - const v = Number(els.scrub.value); - const max = Number(els.scrub.max); - const all = v >= max; - els.scrubVal.textContent = all ? 'all' : `${v.toLocaleString()} / ${max.toLocaleString()}`; - renderer.setScrubPosition(all ? null : v); +els.fixture.value = 'skirt-brim'; + +// ============ color mode select ============ +for (const m of COLOR_MODES) { + const o = document.createElement('option'); + o.value = m.id; + o.textContent = m.label; + els.colorMode.appendChild(o); } -// #276: kinematic time axis backs the time-scrub; the slicer estimate (when present) is the -// displayed total. The provenance (slicer vs kinematic) is shown honestly. -let timeAxis = null; -let savedCameraState = null; - -function fmtTime(ms) { - const min = ms / 60000; - return min >= 1 ? `${min.toFixed(1)} min` : `${(ms / 1000).toFixed(1)} s`; +function refreshColorModes(state) { + const modes = state.availableColorModes ?? []; + for (const opt of els.colorMode.options) { + const m = COLOR_MODE_BY_ID[opt.value]; + const ok = m.always || modes.includes(opt.value); + opt.disabled = !ok; + opt.textContent = ok ? m.label : `${m.label} — unavailable`; + } + updateColorReason(); + renderLegend(); } -function setupTimeScrub(ir, metadata) { - timeAxis = computeToolpathTime(ir); - const slicerSeconds = metadata?.printEstimate?.seconds; - const totalMs = slicerSeconds !== undefined ? slicerSeconds * 1000 : timeAxis.totalMs; - const source = slicerSeconds !== undefined ? 'slicer' : 'kinematic'; - els.timeScrub.max = String(Math.max(1, Math.round(timeAxis.totalMs))); - els.timeScrub.value = els.timeScrub.max; - els.timeScrub.disabled = false; - els.timeScrubVal.textContent = 'all'; - const caveat = - source === 'slicer' - ? ' (slicer estimate)' - : ` (kinematic — constant-velocity approximation${ - timeAxis.hasUnknownFeedrate ? ', lower bound: some feedrates unknown' : '' - })`; - els.printTimeNote.textContent = `Estimated print time: ${fmtTime(totalMs)}${caveat}`; +function updateColorReason() { + const reason = colorModeReason(els.colorMode.value, capabilities); + els.colorModeReason.textContent = reason ? `Not available: ${reason}.` : ''; } -function applyTimeScrub() { - if (timeAxis === null) return; - const ms = Number(els.timeScrub.value); - const all = ms >= Number(els.timeScrub.max); - renderer.setScrubPosition(all ? null : segmentsCompletedAtTime(timeAxis.cumulativeMs, ms)); - els.timeScrubVal.textContent = all ? 'all' : fmtTime(ms); +function applyColorMode() { + const m = COLOR_MODE_BY_ID[els.colorMode.value]; + preview.controls.setColorMode(m.build()); + updateColorReason(); + renderLegend(); } -function enableControls(ir) { - const lastLayer = Math.max(0, renderer.layerCount - 1); - els.startLayer.max = String(lastLayer); - els.endLayer.max = String(lastLayer); - els.startLayer.value = '0'; - els.endLayer.value = String(lastLayer); - els.startLayerVal.textContent = '0'; - els.endLayerVal.textContent = String(lastLayer); - els.scrub.max = String(renderer.segmentCount); - els.scrub.value = String(renderer.segmentCount); - els.scrubVal.textContent = 'all'; - for (const el of [els.startLayer, els.endLayer, els.scrub, els.colorMode, els.frame, els.exportStl, els.saveView]) - el.disabled = false; - for (const btn of document.querySelectorAll('.view-btn')) btn.disabled = false; - - // Capability-honest color modes (§4.6): never offer fabricated feature colors. - const featureOpt = els.colorMode.querySelector('option[value="feature"]'); - const featureOk = renderer.isColorModeAvailable('feature'); - featureOpt.disabled = !featureOk; - featureOpt.textContent = featureOk - ? 'By feature role' - : `By feature role (unavailable: featureRoles = ${ir.header.capabilities.featureRoles ?? 'unknown'})`; - if (!featureOk && els.colorMode.value === 'feature') { - els.colorMode.value = 'single'; - } - // "Hide brim/skirt" needs classified feature roles (same gate as feature colour); start visible. - els.hideAdhesion.disabled = !featureOk; - els.hideAdhesion.checked = false; - renderer.setFeatureRoleVisible(FeatureRole.Skirt, true); - renderer.setFeatureRoleVisible(FeatureRole.Brim, true); - // M600 color-change coloring (#147): only offered when the IR actually saw an M600. - const ccOpt = els.colorMode.querySelector('option[value="colorChange"]'); - const ccOk = renderer.isColorModeAvailable('colorChange'); - ccOpt.disabled = !ccOk; - ccOpt.textContent = ccOk - ? 'By color change (M600)' - : `By color change (unavailable: colorChanges = ${ir.header.capabilities.colorChanges ?? 'unknown'})`; - if (!ccOk && els.colorMode.value === 'colorChange') { - els.colorMode.value = 'single'; +function renderLegend() { + const m = COLOR_MODE_BY_ID[els.colorMode.value]; + const items = []; + if (m.legend) for (const [label, rgb] of m.legend) items.push([label, rgbCss(rgb)]); + else if (m.ramp) { + const grad = `linear-gradient(90deg, ${m.ramp.map(rgbCss).join(',')})`; + els.legendTitle.textContent = m.label; + els.legend.innerHTML = `
+ ${m.rampLabel?.[0] ?? ''} + + ${m.rampLabel?.[1] ?? ''}
`; + els.legendCard.hidden = false; + return; } - // Layer-height coloring (#179): only meaningful with a real planar layer table. - const lhOpt = els.colorMode.querySelector('option[value="layerHeight"]'); - const lhOk = renderer.isColorModeAvailable('layerHeight'); - lhOpt.disabled = !lhOk; - lhOpt.textContent = lhOk - ? 'By layer height' - : `By layer height (unavailable: layers = ${ir.header.capabilities.layers ?? 'unknown'})`; - if (!lhOk && els.colorMode.value === 'layerHeight') { - els.colorMode.value = 'single'; + if (items.length === 0) { + els.legendCard.hidden = true; + return; } - // Speed/feedrate coloring (#177): offered when the file carries feedrates. - const spOpt = els.colorMode.querySelector('option[value="feedrate"]'); - const spOk = renderer.isColorModeAvailable('feedrate'); - spOpt.disabled = !spOk; - spOpt.textContent = spOk - ? 'By speed' - : `By speed (unavailable: feedrate = ${ir.header.capabilities.feedrate ?? 'unknown'})`; - if (!spOk && els.colorMode.value === 'feedrate') els.colorMode.value = 'single'; - // Object coloring (#178): only when the dialect resolved per-object membership. - const objOpt = els.colorMode.querySelector('option[value="object"]'); - const objOk = renderer.isColorModeAvailable('object'); - objOpt.disabled = !objOk; - objOpt.textContent = objOk - ? 'By object' - : `By object (unavailable: objects = ${ir.header.capabilities.objects ?? 'unknown'})`; - if (!objOk && els.colorMode.value === 'object') els.colorMode.value = 'single'; - renderer.setColorMode(colorModeFor(els.colorMode.value)); + els.legendTitle.textContent = m.label; + els.legend.innerHTML = items + .map( + ([label, css]) => + `${label}` + ) + .join(''); + els.legendCard.hidden = false; } -renderer.onEvent((e) => { - if (e.type === 'buildComplete') { - travelAvailable = !e.travelHidden; - els.travel.disabled = !travelAvailable; - els.travel.checked = travelAvailable ? els.travel.checked : false; - // Wipe toggle: enable only when the IR actually carries wipe moves (DD-016, #182) — honest. - const hasWipe = renderer.ir?.header.capabilities?.wipeMoves === 'known'; - els.wipe.disabled = !hasWipe; - if (!hasWipe) els.wipe.checked = false; - // Retraction markers: enable only when the IR actually carries events (#148). - els.retractions.disabled = !renderer.hasRetractions; - if (!renderer.hasRetractions) els.retractions.checked = false; - els.disclosure.textContent = - e.decimationApplied > 1 - ? `Decimation active: showing every ${e.decimationApplied}th extrusion segment ` + - `(layer boundaries kept); travel hidden. ${e.segments.toLocaleString()} segments drawn.` - : ''; - els.qualityNote.textContent = `Rendering as: ${e.quality}${bedNote}`; - els.capturePng.disabled = false; - els.showStats.disabled = false; - } else if (e.type === 'qualityFallback') { - els.qualityNote.textContent = `Tubes unavailable — fell back to lines (${e.reason})`; - } else if (e.type === 'error') { - setStatus(`Renderer: ${e.code} — ${e.message}`, true); - } -}); - -// Progressive preview (#60): stream partial slices straight into the scene for -// large files (worker default: ≥ 25 MB). The final IR replaces the preview. -session.onPartial((slice, cumulativeSegments) => { - renderer.appendPartial(slice); - els.qualityNote.textContent = `Preview: ${cumulativeSegments.toLocaleString()} segments streamed…`; -}); - -session.onProgress((p) => { - if (p.totalBytes > 0) { - els.progress.value = p.bytesProcessed / p.totalBytes; +// ============ parse / load ============ +async function loadSelected() { + if (mode === 'prepare') return loadSelectedModel(); + const fx = FIXTURE_BY_ID[els.fixture.value]; + if (!fx) return; + els.status.textContent = `Fetching ${fx.label}…`; + try { + const res = await fetch(fx.path); + if (!res.ok) throw new Error(`${res.status}`); + await parseInput(new Uint8Array(await res.arrayBuffer()), fx.label); + } catch (err) { + els.status.textContent = `Could not fetch ${fx.label}: ${err.message}`; } - setStatus( - p.phase === 'finalizing' - ? 'Finalizing…' - : `Parsing… ${(p.bytesProcessed / 1e6).toFixed(1)} / ${(p.totalBytes / 1e6).toFixed(1)} MB` - ); -}); +} -async function loadInput() { - const file = els.file.files?.[0]; - if (file) return { name: file.name, input: file, bytes: file.size }; - const name = els.fixture.value; - const res = await fetch(`./${name}`); - if (!res.ok) throw new Error(`fetch ${name}: HTTP ${res.status}`); - const buf = await res.arrayBuffer(); - return { name, input: new Uint8Array(buf), bytes: buf.byteLength }; +// Prepare: hand the model controller a synthetic MIT-clean source model (no fetch). Its `ready` event +// (wired in ensureModel) populates the Source-model panel with the honest capability tier + counts. +function loadSelectedModel() { + const fx = MODEL_FIXTURE_BY_ID[els.fixture.value]; + if (!fx) return; + modelInfo = null; + renderModelInfo(); + els.status.textContent = `${fx.label} — ${fx.blurb}`; + els.modelBlurb.textContent = fx.blurb; + ensureModel().controls.setSource(fx.source()); } -async function parseAndRender() { - if (parsing) return; - parsing = true; - els.parse.disabled = true; - els.cancel.disabled = false; - els.progress.hidden = false; - els.progress.value = 0; - els.disclosure.textContent = ''; - try { - const { name, input, bytes } = await loadInput(); - setStatus(`Loading ${name}…`); - const t0 = performance.now(); - const result = await session.parse(input, { yieldIntervalMs: 25 }); - const parseMs = performance.now() - t0; - const ir = result.ir; - renderer.setIR(ir); - enableControls(ir); - enableSim(ir, bytes); - // DD-005 §4.2: this demo opts into file-discovered bed geometry (arrives - // with the phase-2/3 adapters); mismatches surface via the renderer event. - setupTimeScrub(ir, result.metadata); - const machine = result.metadata?.machine; - bedNote = machine ? ` · bed from file: ${machine.printerName ?? 'unknown printer'} (${machine.confidence})` : ''; - if (machine) { - renderer.setBuildVolume(machine); - } - const caps = Object.entries(ir.header.capabilities) - .map(([k, v]) => `${k}: ${v}`) - .join('\n '); - els.stats.textContent = - `${name} — ${(bytes / 1e6).toFixed(2)} MB\n` + - `Parsed off-thread in ${parseMs.toFixed(0)} ms\n` + - `${ir.segments.count.toLocaleString()} segments · ${ir.layers.length.toLocaleString()} layers\n` + - (ir.header.complete ? '' : `INCOMPLETE: stopped (${result.stats.stopReason ?? 'limit'})\n`) + - `Capabilities:\n ${caps}`; - setStatus(ir.header.complete ? 'Done.' : `Partial result — ${result.stats.stopReason ?? 'limit reached'}.`); - } catch (err) { - if (err instanceof CancelledError) { - setStatus('Cancelled.'); - } else { - setStatus(`Parse failed: ${err && err.message ? err.message : err}`, true); - } - } finally { - parsing = false; - els.parse.disabled = false; - els.cancel.disabled = true; - els.progress.hidden = true; +async function parseInput(input, label) { + setParsing(true); + els.stage.hidden = false; + els.stageLabel.textContent = 'Parsing…'; + const outcome = await preview.parse(input); + setParsing(false); + if (!outcome.ok) { + els.stage.hidden = true; + if (!outcome.cancelled) els.status.textContent = `Parse failed: ${outcome.error.message}`; + return; } + els.status.textContent = `Loaded ${label ?? 'file'} — ${count(outcome.result.ir.segments.count)} segments.`; } -// --------------------------------------------------------------------------- -// Simulated live telemetry (DD-006 phase 3, #92): drive the mapper + overlay -// with observation streams shaped like the real §1.1 surfaces — no printer -// needed to verify exact/band/stale/hidden presentations. -// --------------------------------------------------------------------------- -const sim = { mapper: null, ir: null, fileBytes: 0, timer: null, staleTimer: null, fraction: 0 }; - -function stopSim(clearOverlay) { - if (sim.timer !== null) clearInterval(sim.timer); - if (sim.staleTimer !== null) clearInterval(sim.staleTimer); - sim.timer = null; - sim.staleTimer = null; - els.progressPlay.textContent = 'Play'; - if (clearOverlay && sim.mapper !== null) { - sim.mapper.reset(); - renderer.setProgress(null); - els.progressNote.textContent = 'Stopped.'; - } +function setParsing(on) { + els.parse.disabled = on; + els.cancel.hidden = !on; + els.progress.hidden = !on; + if (on) els.progress.value = 0; } -function simObservation(fraction) { - const tier = els.progressTier.value; - const base = { v: 1, timestampMs: Date.now(), state: fraction >= 1 ? 'complete' : 'printing' }; - const layerCount = sim.ir.layers.length; - switch (tier) { - case 'byte': - return { ...base, position: { byte: Math.round(fraction * sim.fileBytes) } }; - case 'percent-bytes': - return { ...base, position: { percent: fraction, percentBasis: 'bytes' } }; - case 'layer': - return { - ...base, - position: { layer: Math.min(layerCount - 1, Math.floor(fraction * layerCount)), totalLayers: layerCount } - }; - case 'percent-job': - return { ...base, position: { percent: fraction, percentBasis: 'job' } }; - case 'mismatch': - return { - ...base, - position: { byte: Math.round(fraction * sim.fileBytes) }, - file: { sizeBytes: Math.round(sim.fileBytes * 1.5), name: 'some-other-file.gcode' } - }; +// ============ controller events ============ +preview.onEvent((e) => { + switch (e.type) { + case 'parse-started': + fileBytes = e.bytes; + break; + case 'parse-progress': + els.progress.value = e.progress.totalBytes ? e.progress.bytesProcessed / e.progress.totalBytes : 0; + break; + case 'stage': + els.stageLabel.textContent = stageLabel(e); + if (e.stage === 'ready') els.stage.hidden = true; + break; + case 'buildComplete': + els.stage.hidden = true; + onReady(e); + break; + case 'parse-complete': + capabilities = e.capabilities ?? {}; + applyContext(); + break; + case 'machine-geometry-discovered': + showMachine(e.machine); + break; + case 'error': + els.status.textContent = `Error (${e.code}): ${e.message}`; + break; default: - return base; + break; } +}); + +function stageLabel(e) { + const map = { + parsing: 'Parsing G-code…', + classifying: 'Classifying moves…', + 'building-geometry': 'Building geometry…', + 'preparing-gpu': 'Preparing GPU…', + ready: 'Ready' + }; + let s = map[e.stage] ?? e.stage; + if (e.stage === 'building-geometry' && e.detail) s += ` (${count(e.detail.built)}/${count(e.detail.total)})`; + return s; } -function simStep() { - sim.fraction = Math.min(1, sim.fraction + 0.004); - const mappedResult = sim.mapper.observe(simObservation(sim.fraction)); - renderer.setProgress(mappedResult); - const notes = mappedResult.notes.map((n) => n.code).join(', '); - els.progressNote.textContent = - `${(sim.fraction * 100).toFixed(1)}% · basis: ${mappedResult.basis} · ` + - `confidence: ${mappedResult.confidence} · shown as: ${renderer.progressPresentation}` + - (notes ? ` · notes: ${notes}` : ''); - if (sim.fraction >= 1) stopSim(false); +function onReady() { + ready = true; + els.countPill.hidden = false; + enableControls(true); + // scrub ranges + const state = preview.getState(); + els.segCount.textContent = count(state.segmentCount); + els.layerCountLbl.textContent = count(state.layerCount); + els.scrub.max = state.segmentCount; + els.scrub.value = state.segmentCount; + els.startLayer.max = Math.max(0, state.layerCount - 1); + els.endLayer.max = Math.max(0, state.layerCount - 1); + els.startLayer.value = 0; + els.endLayer.value = Math.max(0, state.layerCount - 1); + updateLayerLabel(); + // re-apply appearance the user had chosen + applyColorMode(); + preview.controls.setTheme(themeFor()); } -els.progressPlay.addEventListener('click', () => { - if (sim.timer !== null) { - // Pause: stop observing but keep ticking the mapper — after staleAfterMs - // the overlay visibly grays out (honest staleness, not a frozen lie). - stopSim(false); - els.progressPlay.textContent = 'Resume'; - els.progressNote.textContent += ' · paused (overlay goes stale in ~10 s)'; - sim.staleTimer = setInterval(() => { - const ticked = sim.mapper.tick(Date.now()); - renderer.setProgress(ticked); - if (ticked.stale) { - els.progressNote.textContent = `Stale — last position held, shown as: ${renderer.progressPresentation}`; - clearInterval(sim.staleTimer); - sim.staleTimer = null; - } - }, 1000); - return; +// ============ reactive state ============ +preview.onStateChange((state) => { + refreshColorModes(state); + // retraction gating (contextual + honest) + els.retractions.disabled = !state.hasRetractions && ready; + els.retractRow.classList.toggle('gp-disabled', ready && !state.hasRetractions); + els.retractRow.querySelector('span').textContent = + ready && !state.hasRetractions ? 'Retraction markers — none in this file' : 'Retraction markers'; + // disclosure + time + els.disclosure.textContent = state.disclosure ?? ''; + if (state.totalTimeMs != null) { + els.timePill.hidden = false; + els.timeLbl.textContent = duration(state.totalTimeMs); + els.timeLbl.title = `Estimate: ${timeSourceNote(state.timeEstimateSource)}`; + els.timeScrub.max = Math.round(state.totalTimeMs); + els.timeScrub.value = Math.round(state.totalTimeMs); + els.timeScrubLbl.textContent = duration(state.totalTimeMs); } - if (sim.mapper === null) return; - if (sim.staleTimer !== null) clearInterval(sim.staleTimer); - sim.staleTimer = null; - if (sim.fraction >= 1) sim.fraction = 0; - els.progressPlay.textContent = 'Pause'; - sim.timer = setInterval(simStep, 100); }); -els.progressTier.addEventListener('change', () => { - if (sim.mapper !== null) { - sim.mapper.reset(); - sim.fraction = 0; - } -}); +// ============ contextual behavior ============ +function applyContext() { + // Capability badges (diagnostics) + els.capBadges.innerHTML = Object.entries(capabilities) + .map(([k, v]) => { + const t = confidenceTier(v); + return `${k}`; + }) + .join(''); + // Adhesion toggle: only when feature roles are identified. + const hasFeatures = capabilities.featureRoles && capabilities.featureRoles !== 'unavailable'; + toggleContext('adhesion', hasFeatures); + // CNC tab: only when the file exposes tool power (a CNC/laser signal). + const isCam = preview.controls.isColorModeAvailable('power') || capabilities.toolPower !== undefined; + toggleContext('cam', isCam); +} -function enableSim(ir, fileBytes) { - stopSim(false); - sim.ir = ir; - sim.fileBytes = fileBytes; - sim.fraction = 0; - sim.mapper = createProgressMapper(ir, { fileSizeBytes: fileBytes }); - els.progressTier.disabled = false; - els.progressPlay.disabled = false; - els.progressNote.textContent = 'Pick a tier and press Play.'; +function toggleContext(ctx, on) { + for (const el of document.querySelectorAll(`[data-context="${ctx}"]`)) el.hidden = !on; } -els.parse.addEventListener('click', () => void parseAndRender()); -els.cancel.addEventListener('click', () => session.cancel()); -els.startLayer.addEventListener('input', applyLayerRange); -els.endLayer.addEventListener('input', applyLayerRange); -els.scrub.addEventListener('input', applyScrub); -els.travel.addEventListener('change', () => renderer.setKindVisible('travel', els.travel.checked)); -els.wipe.addEventListener('change', () => renderer.setKindVisible('wipe', els.wipe.checked)); -els.retractions.addEventListener('change', () => renderer.setShowRetractions(els.retractions.checked)); -els.hideAdhesion.addEventListener('change', () => { - const visible = !els.hideAdhesion.checked; - renderer.setFeatureRoleVisible(FeatureRole.Skirt, visible); - renderer.setFeatureRoleVisible(FeatureRole.Brim, visible); -}); -els.colorMode.addEventListener('change', () => { - if (!renderer.setColorMode(colorModeFor(els.colorMode.value))) { - els.colorMode.value = 'single'; - renderer.setColorMode(colorModeFor('single')); - } -}); -els.quality.addEventListener('change', () => renderer.setQuality(els.quality.value)); -els.cameraMode.addEventListener('change', () => renderer.setCameraMode(els.cameraMode.value)); -const applyTheme = () => renderer.setTheme(themeFor(els.theme.value, els.material.value)); -els.theme.addEventListener('change', applyTheme); -els.material.addEventListener('change', applyTheme); -els.frame.addEventListener('click', () => renderer.frame()); -els.timeScrub.addEventListener('input', applyTimeScrub); -// #276: preset views + save/restore camera state (exercises setView / get+setCameraState). -for (const btn of document.querySelectorAll('.view-btn')) { - btn.addEventListener('click', () => renderer.setView(btn.dataset.view)); +function showMachine(machine) { + if (!machine) return; + const bed = machine.bed; + els.machineNote.textContent = `Discovered from the file (confidence: ${machine.confidence}).`; + els.machineInfo.textContent = + bed?.kind === 'rect' + ? `Rect bed ${bed.max.x - bed.min.x}×${bed.max.y - bed.min.y} mm, height ${machine.heightMm} mm` + : `${bed?.kind ?? 'unknown'} bed`; } -els.saveView.addEventListener('click', () => { - savedCameraState = renderer.getCameraState(); - els.restoreView.disabled = false; -}); -els.restoreView.addEventListener('click', () => { - if (savedCameraState !== null) renderer.setCameraState(savedCameraState); + +// ============ enable/disable ============ +const RUNTIME_CONTROLS = [ + 'startLayer', + 'endLayer', + 'scrub', + 'timeScrub', + 'travel', + 'wipe', + 'retractions', + 'hideAdhesion', + 'colorMode', + 'quality', + 'qualityMode', + 'progressive', + 'interactionQuality', + 'saveView', + 'frame', + 'progressPlay', + 'showStats', + 'capture' +]; +function enableControls(on) { + for (const id of RUNTIME_CONTROLS) if (els[id]) els[id].disabled = !on; +} + +// ============ wiring: header ============ +els.parse.addEventListener('click', loadSelected); +els.fixture.addEventListener('change', loadSelected); +els.open.addEventListener('click', () => els.file.click()); +els.file.addEventListener('change', async () => { + const f = els.file.files?.[0]; + if (f) await parseInput(f, f.name); }); -els.exportStl.addEventListener('click', () => { - const ir = renderer.ir; - if (!ir) return; - const { segments, emitted, triangles } = downloadToolpathStl(ir, 'toolpath.stl'); - const note = - emitted < segments - ? `Exported STL: ${triangles.toLocaleString()} triangles from ${emitted.toLocaleString()} of ${segments.toLocaleString()} productive segments (strided to the triangle budget).` - : `Exported STL: ${triangles.toLocaleString()} triangles from ${segments.toLocaleString()} productive segments.`; - setStatus(note); +els.cancel.addEventListener('click', () => preview.cancel()); +els.chromeToggle.addEventListener('click', () => { + const root = document.documentElement; + root.dataset.chrome = root.dataset.chrome === 'light' ? 'dark' : 'light'; }); -// Progressive preview (#60, DD-029): how a large file is revealed while parsing. -els.progressive.addEventListener('change', () => renderer.setProgressivePreview(els.progressive.value)); -// Capture the current view as a PNG via the generic renderer.capture() (DD-030 D1). -els.capturePng.addEventListener('click', async () => { +els.capture.addEventListener('click', async () => { try { - const blob = await renderer.capture({ format: 'png' }); + const blob = await preview.controls.capture({ format: 'image/png' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'gcode-preview.png'; a.click(); URL.revokeObjectURL(url); - setStatus(`Captured ${(blob.size / 1024).toFixed(0)} KB PNG.`); } catch (err) { - setStatus(`Capture failed: ${err && err.message ? err.message : err}`, true); + els.status.textContent = `Capture unavailable: ${err.message}`; } }); -// Render diagnostics (DD-027): read renderer.getRenderStats() and show it — backend, -// hardware-vs-software GPU, geometry mode, counts, and timings, never fabricated. + +// ============ wiring: tabs + rail ============ +function selectTab(name) { + for (const t of els.tabs.querySelectorAll('.gp-tab')) t.classList.toggle('gp-on', t.dataset.tab === name); + for (const p of document.querySelectorAll('[data-panel]')) p.hidden = p.dataset.panel !== name; +} +els.tabs.addEventListener('click', (ev) => { + const btn = ev.target.closest('[data-tab]'); + if (!btn) return; + selectTab(btn.dataset.tab); +}); +els.modeSeg.addEventListener('click', (ev) => { + const b = ev.target.closest('[data-mode]'); + if (b) setMode(b.dataset.mode); +}); +els.railToggle.addEventListener('click', () => els.main.classList.toggle('gp-rail-collapsed')); + +// ============ wiring: inspect ============ +els.travel.addEventListener('change', () => preview.controls.setKindVisible('travel', els.travel.checked)); +els.wipe.addEventListener('change', () => preview.controls.setKindVisible('wipe', els.wipe.checked)); +els.retractions.addEventListener('change', () => preview.controls.setShowRetractions(els.retractions.checked)); +els.hideAdhesion.addEventListener('change', () => { + const visible = !els.hideAdhesion.checked; + preview.controls.setFeatureRoleVisible(FeatureRole.Skirt, visible); + preview.controls.setFeatureRoleVisible(FeatureRole.Brim, visible); +}); + +// segment picking (source mapping) via the public controls.pickSegment +els.view.addEventListener('click', (ev) => { + if (!ready) return; + const r = els.view.getBoundingClientRect(); + const ndcX = ((ev.clientX - r.left) / r.width) * 2 - 1; + const ndcY = -(((ev.clientY - r.top) / r.height) * 2 - 1); + const seg = preview.controls.pickSegment(ndcX, ndcY); + els.pickResult.textContent = + seg == null ? 'No segment under cursor' : `Segment #${count(seg)} of ${count(preview.getState().segmentCount)}`; +}); + +// ============ wiring: appearance ============ +els.colorMode.addEventListener('change', applyColorMode); +els.theme.addEventListener('change', () => preview.controls.setTheme(themeFor())); +els.material.addEventListener('change', () => preview.controls.setTheme(themeFor())); + +// ============ wiring: view ============ +els.projSeg.addEventListener('click', (ev) => { + const b = ev.target.closest('[data-proj]'); + if (!b) return; + for (const x of els.projSeg.children) x.classList.toggle('gp-on', x === b); + preview.controls.setCameraMode(b.dataset.proj); +}); +els.projToggle.addEventListener('click', () => { + const cur = preview.getState(); + const next = els.projSeg.querySelector('.gp-on').dataset.proj === 'perspective' ? 'orthographic' : 'perspective'; + for (const x of els.projSeg.children) x.classList.toggle('gp-on', x.dataset.proj === next); + preview.controls.setCameraMode(next); + void cur; +}); +// Camera presets + frame route to whichever viewer is active (toolpath or source model). +const activeControls = () => + mode === 'prepare' && modelController !== null ? modelController.controls : preview.controls; +document + .querySelectorAll('[data-view]') + .forEach((b) => b.addEventListener('click', () => activeControls().setView(b.dataset.view))); +els.frame.addEventListener('click', () => activeControls().frame()); +els.frameSeg.addEventListener('click', (ev) => { + const b = ev.target.closest('[data-frame]'); + if (!b) return; + for (const x of els.frameSeg.children) x.classList.toggle('gp-on', x === b); + preview.controls.setFrameContent(b.dataset.frame); +}); +els.cage.addEventListener('change', () => preview.controls.setBuildVolumeCage(els.cage.checked)); +els.saveView.addEventListener('click', () => { + savedCamera = preview.controls.getCameraState(); + els.restoreView.disabled = savedCamera == null; +}); +els.restoreView.addEventListener('click', () => savedCamera && preview.controls.setCameraState(savedCamera)); + +// ============ wiring: rendering ============ +els.quality.addEventListener('change', () => preview.controls.setQuality(els.quality.value)); +els.qualityMode.addEventListener('change', () => preview.controls.setQualityMode(els.qualityMode.value)); +els.progressive.addEventListener('change', () => preview.controls.setProgressivePreview(els.progressive.value)); +els.interactionQuality.addEventListener('change', () => + preview.controls.setInteractionQuality(els.interactionQuality.checked ? 'auto' : 'off') +); + +// ============ wiring: diagnostics ============ els.showStats.addEventListener('click', () => { - const s = renderer.getRenderStats(); - if (!s) { - els.renderStats.textContent = 'No render stats yet — render a file first.'; + const s = preview.controls.getRenderStats(); + els.renderStats.textContent = s + ? JSON.stringify(s, null, 2) + : 'No render stats available (e.g. the 2D renderer produces none).'; +}); + +// ============ wiring: scrub strip ============ +els.scrubMode.addEventListener('click', (ev) => { + const b = ev.target.closest('[data-scrub]'); + if (!b) return; + for (const x of els.scrubMode.children) x.classList.toggle('gp-on', x === b); + for (const t of document.querySelectorAll('.gp-scrub .track')) t.hidden = t.dataset.track !== b.dataset.scrub; +}); +function updateLayerLabel() { + const a = Math.min(+els.startLayer.value, +els.endLayer.value); + const b = Math.max(+els.startLayer.value, +els.endLayer.value); + els.layerRangeLbl.textContent = `${a}–${b}`; + preview.controls.setLayerRange(a, b); +} +els.startLayer.addEventListener('input', updateLayerLabel); +els.endLayer.addEventListener('input', updateLayerLabel); +els.scrub.addEventListener('input', () => { + const v = +els.scrub.value; + els.scrubLbl.textContent = count(v); + preview.controls.setScrubPosition(v >= +els.scrub.max ? null : v); +}); +els.timeScrub.addEventListener('input', () => { + const ms = +els.timeScrub.value; + els.timeScrubLbl.textContent = duration(ms); + preview.controls.setScrubTime(ms >= +els.timeScrub.max ? null : ms); +}); +els.scrubReset.addEventListener('click', () => { + els.scrub.value = els.scrub.max; + els.timeScrub.value = els.timeScrub.max; + els.startLayer.value = 0; + els.endLayer.value = els.endLayer.max; + preview.controls.setScrubPosition(null); + preview.controls.setScrubTime(null); + updateLayerLabel(); +}); + +// ============ wiring: simulated live progress ============ +let simTimer = null; +let simT = 0; +els.progressPlay.addEventListener('click', () => { + if (simTimer) { + stopSim(); return; } - const ms = (v) => (v == null ? '—' : `${Math.round(v)} ms`); - els.renderStats.textContent = [ - `backend: ${s.backend} (WebGL ${s.webglVersion ?? '?'})`, - `capability: ${s.capability}${s.gpuRenderer ? ` — ${s.gpuRenderer}` : ''}`, - `geometry: ${s.geometryMode} · parallelism ${s.buildParallelism}${s.workerCount ? ` (${s.workerCount} workers)` : ''}`, - `segments: ${(s.renderedSegmentCount ?? 0).toLocaleString()} / ${(s.sourceSegmentCount ?? 0).toLocaleString()}${s.decimationApplied ? ' (decimated)' : ''}`, - `draw calls: ${s.drawCalls ?? '—'} · vertices ${s.vertexCount?.toLocaleString() ?? '—'}`, - `tube bytes: ${s.tubeBytes ? (s.tubeBytes / 1e6).toFixed(1) + ' MB' : '—'} / budget ${s.tubeByteBudget ? (s.tubeByteBudget / 1e6).toFixed(0) + ' MB' : '—'}`, - `timings: parse ${ms(s.parseMs)} · build ${ms(s.geometryBuildMs)} · first frame ${ms(s.firstRenderMs)} · ready ${ms(s.totalReadyMs)}`, - s.disclosures?.length ? `disclosures: ${s.disclosures.join(', ')}` : '' - ] - .filter(Boolean) - .join('\n'); + els.progressPlay.textContent = 'Stop'; + simT = 0; + const tier = els.progressTier.value; + const layers = preview.getState().layerCount; + simTimer = setInterval(() => { + simT += 0.02; + if (simT >= 1) { + if (tier === 'mismatch') { + preview.tickProgress(performance.now()); // stop observing → goes stale + reportProgress(); + return; + } + simT = 1; + } + const now = performance.now(); + if (tier === 'byte') + preview.observeProgress({ v: 1, timestampMs: now, position: { byte: Math.round(simT * fileBytes) } }); + else if (tier === 'layer') + preview.observeProgress({ v: 1, timestampMs: now, position: { layer: Math.round(simT * (layers - 1)) } }); + else preview.observeProgress({ v: 1, timestampMs: now, position: { percent: simT } }); + reportProgress(); + if (simT >= 1 && tier !== 'mismatch') stopSim(); + }, 60); }); +function stopSim() { + clearInterval(simTimer); + simTimer = null; + els.progressPlay.textContent = 'Play'; +} +function reportProgress() { + const s = preview.getState(); + els.progressNote.textContent = `Presentation: ${s.presentation} · ${Math.round(simT * 100)}%`; +} -// App-level keyboard shortcuts (master plan §9.5); every control is also plain -// tab-reachable, and the sliders take arrow/page keys natively. -const nudge = (el, delta) => { - if (el.disabled) return; - el.value = String(Math.min(Number(el.max), Math.max(Number(el.min), Number(el.value) + delta))); - el.dispatchEvent(new Event('input')); -}; -window.addEventListener('keydown', (ev) => { - const t = ev.target; - if (t instanceof HTMLElement && (t.tagName === 'SELECT' || t.tagName === 'INPUT')) return; - const scrubStep = Math.max(1, Math.round(Number(els.scrub.max) / 100)); - if (ev.key === '[') nudge(els.endLayer, -1); - else if (ev.key === ']') nudge(els.endLayer, 1); - else if (ev.key === ',') nudge(els.scrub, -scrubStep); - else if (ev.key === '.') nudge(els.scrub, scrubStep); - else if (ev.key === 't' && !els.travel.disabled) { - els.travel.checked = !els.travel.checked; - els.travel.dispatchEvent(new Event('change')); - } else if (ev.key === 'f' && !els.frame.disabled) renderer.frame(); - else if (ev.key === 'o') { - els.cameraMode.value = els.cameraMode.value === 'orthographic' ? 'perspective' : 'orthographic'; - els.cameraMode.dispatchEvent(new Event('change')); - } else return; - ev.preventDefault(); +// ============ keyboard shortcuts (accessibility preserved) ============ +window.addEventListener('keydown', (e) => { + if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return; + if (e.key === 'f') preview.controls.frame(); + else if (e.key === 'o') els.projToggle.click(); + else if (e.key === '[') { + els.endLayer.value = Math.max(0, +els.endLayer.value - 1); + updateLayerLabel(); + } else if (e.key === ']') { + els.endLayer.value = Math.min(+els.endLayer.max, +els.endLayer.value + 1); + updateLayerLabel(); + } }); -// Keep the canvas backing store matched to its layout size. Explicit initial fit -// plus a window fallback — some embedded hosts never deliver ResizeObserver entries. -const main = els.canvas.parentElement; -function fitCanvas() { - const r = main.getBoundingClientRect(); - renderer.resize(Math.max(1, Math.round(r.width)), Math.max(1, Math.round(r.height))); -} -fitCanvas(); -window.addEventListener('resize', fitCanvas); -new ResizeObserver(fitCanvas).observe(main); +// ============ canvas auto-fit ============ +new ResizeObserver(() => { + const r = preview.raw.renderer(); + if (r) r.resize(els.view.clientWidth, els.view.clientHeight); +}).observe(els.view); +new ResizeObserver(() => { + if (modelController !== null) modelController.controls.resize(els.modelView.clientWidth, els.modelView.clientHeight); +}).observe(els.modelView); + +// expose for automated verification (kept minimal — the flagship path is the controller) +window.viewer = { preview, model: () => modelController }; + +// STL export lives on a header-adjacent action only when useful; keep the util imported (used by tests). +void downloadToolpathStl; -// Debug/inspection handle (also used by automated demo verification). -window.viewer = { renderer, session, sim }; +// initial load +loadSelected(); diff --git a/tools/demo/vite.config.js b/tools/demo/vite.config.js index 785da9dd..c639b339 100644 --- a/tools/demo/vite.config.js +++ b/tools/demo/vite.config.js @@ -7,7 +7,9 @@ export default defineConfig({ publicDir: '../../test-data', server: { port: 5199, - strictPort: true + strictPort: true, + // Allow importing the workspace-internal demo-kit (tools/demo-kit) which lives outside this app's root. + fs: { allow: ['../..'] } }, resolve: { // One three.js instance even though the renderer package is symlinked. diff --git a/tools/example-react/index.html b/tools/example-react/index.html index 43351bd2..3d472401 100644 --- a/tools/example-react/index.html +++ b/tools/example-react/index.html @@ -3,44 +3,89 @@ - GcodePreview React — example app (DD-007 phase 6) + @chestnutlabs/gcode-preview-react — examples -
- +
+

@chestnutlabs/gcode-preview-react

+

+ Example apps built on the published React adapter — both halves of the SDK: sliced G-code + (Preview) and source models (Prepare). All drive the real package — no raw renderer imports. +

+ +
diff --git a/tools/example-react/minimal.html b/tools/example-react/minimal.html new file mode 100644 index 00000000..7bdd4f20 --- /dev/null +++ b/tools/example-react/minimal.html @@ -0,0 +1,40 @@ + + + + + + gcode-preview-react — minimal + + + +
+ + + diff --git a/tools/example-react/model.html b/tools/example-react/model.html new file mode 100644 index 00000000..20e7fe30 --- /dev/null +++ b/tools/example-react/model.html @@ -0,0 +1,36 @@ + + + + + + gcode-preview-react — minimal model viewer + + + +
+ + + diff --git a/tools/example-react/package.json b/tools/example-react/package.json index a2ad1b69..336937a5 100644 --- a/tools/example-react/package.json +++ b/tools/example-react/package.json @@ -3,7 +3,7 @@ "private": true, "version": "0.0.0", "type": "module", - "description": "DD-007 phase 6 example application (issue #113): the packaged React component drives the full pipeline — the separate example app required by the D1 amendment.", + "description": "Two-tier React example (DD-031 §4.7): minimal.html (smallest real integration via ) + showcase.html (full declarative surface + shared demo-kit). Consumes the published @chestnutlabs/gcode-preview-react.", "scripts": { "dev": "vite", "build": "vite build" diff --git a/tools/example-react/showcase.html b/tools/example-react/showcase.html new file mode 100644 index 00000000..4db01452 --- /dev/null +++ b/tools/example-react/showcase.html @@ -0,0 +1,121 @@ + + + + + + gcode-preview-react — showcase + + + + +
+ + + diff --git a/tools/example-react/src/main.jsx b/tools/example-react/src/main.jsx deleted file mode 100644 index 3a5cba48..00000000 --- a/tools/example-react/src/main.jsx +++ /dev/null @@ -1,132 +0,0 @@ -/** - * DD-007 phase 6 example app (issue #113): the packaged REACT component drives the whole - * pipeline — corpus select, layer/scrub props, simulated DD-006 progress — with the D2 - * default (batteries) worker path under real Vite. No raw session/renderer imports. - */ -import { StrictMode, useRef, useState } from 'react'; -import { createRoot } from 'react-dom/client'; -import { GcodePreview } from '@chestnutlabs/gcode-preview-react'; - -const CORPUS = ['easel.gcode', 'calicat.gcode', '3DBenchy.gcode', 'vase.gcode', 'screw.gcode']; - -function App() { - const [fixture, setFixture] = useState(CORPUS[0]); - const [source, setSource] = useState(null); - const [layerCount, setLayerCount] = useState(0); - const [segmentCount, setSegmentCount] = useState(0); - const [lastLayer, setLastLayer] = useState(0); - const [scrub, setScrub] = useState(null); - const [progressObs, setProgressObs] = useState(null); - const [log, setLog] = useState('Pick a fixture and press Load.'); - const simTimer = useRef(null); - const preview = useRef(null); - - const append = (s) => setLog((prev) => `${s}\n${prev}`.split('\n').slice(0, 14).join('\n')); - - async function load() { - stopSim(); - const res = await fetch(`./${fixture}`); - setSource(new Uint8Array(await res.arrayBuffer())); // prop change → component re-parses - } - - function stopSim() { - if (simTimer.current !== null) clearInterval(simTimer.current); - simTimer.current = null; - setProgressObs(null); - } - - function playSim() { - if (simTimer.current !== null) { - stopSim(); - return; - } - let fraction = 0; - simTimer.current = setInterval(() => { - fraction = Math.min(1, fraction + 0.01); - setProgressObs({ - v: 1, - timestampMs: Date.now(), - state: fraction >= 1 ? 'complete' : 'printing', - position: { percent: fraction, percentBasis: 'bytes' } - }); - if (fraction >= 1) stopSim(); - }, 100); - } - - return ( -
- -
- 0 ? [0, lastLayer] : null} - scrub={scrub} - progress={progressObs} - quality="auto" - onReady={(s) => { - setLayerCount(s.layers); - setSegmentCount(s.segments); - setLastLayer(Math.max(0, s.layers - 1)); - setScrub(null); - append(`ready: ${s.segments.toLocaleString()} segments, ${s.layers} layers`); - }} - onParseError={(e) => append(`parse-error: ${e.code} ${e.message}`)} - onBuildComplete={(e) => append(`build-complete: ${e.quality}`)} - onQualityFallback={(e) => append(`quality-fallback: ${e.from} -> ${e.to}`)} - onProgressPresentationChanged={(e) => - append(`progress shown as: ${e.mode}${e.reason ? ` (${e.reason})` : ''}`) - } - onDisclosure={(t) => t && append(`disclosure: ${t}`)} - onError={(e) => append(`error: ${e.code}`)} - /> -
-
- ); -} - -const root = createRoot(document.getElementById('app')); -root.render( - - - -); -window.reactExample = { root }; diff --git a/tools/example-react/src/minimal.jsx b/tools/example-react/src/minimal.jsx new file mode 100644 index 00000000..abd7fe3d --- /dev/null +++ b/tools/example-react/src/minimal.jsx @@ -0,0 +1,71 @@ +/* + * Minimal getting-started example (DD-031 §4.7, tier 1). + * + * The smallest real integration of @chestnutlabs/gcode-preview-react: give a + * `source` (bytes/File/ArrayBuffer) and it parses + renders. Everything else here — the fixture + * picker and the layer slider — is ordinary React state driving declarative props. No design + * system, no demo-kit, no raw renderer. Copy this file to start. + */ +import { StrictMode, useState } from 'react'; +import { createRoot } from 'react-dom/client'; +import { GcodePreview } from '@chestnutlabs/gcode-preview-react'; + +const FIXTURES = ['3DBenchy.gcode', 'calicat.gcode', 'vase.gcode', 'mach3.gcode']; + +function App() { + const [source, setSource] = useState(null); + const [layers, setLayers] = useState(0); + const [topLayer, setTopLayer] = useState(0); + + async function load(name) { + const res = await fetch(`./gcodes/${name}`); + setSource(new Uint8Array(await res.arrayBuffer())); // new source prop → re-parse + } + + return ( + <> +
+ gcode-preview-react · minimal + + {layers > 0 && ( + + )} +
+
+ 0 ? [0, topLayer] : null} + showTravel={false} + onReady={(s) => { + setLayers(s.layers); + setTopLayer(Math.max(0, s.layers - 1)); + }} + /> +
+ + ); +} + +createRoot(document.getElementById('app')).render( + + + +); diff --git a/tools/example-react/src/model-minimal.jsx b/tools/example-react/src/model-minimal.jsx new file mode 100644 index 00000000..11362490 --- /dev/null +++ b/tools/example-react/src/model-minimal.jsx @@ -0,0 +1,54 @@ +/* + * Minimal source-model viewer (DD-031 §4.7, tier 1 — Prepare side). + * + * The smallest real integration of @chestnutlabs/gcode-preview-react/model: give a + * `source` ({ kind, bytes } — an STL or 3MF) and it parses + renders. The counterpart to minimal.jsx, + * which does the same for sliced G-code with . No design system, no demo-kit engine + * handles — just the published adapter. (The fixtures here are synthetic MIT-clean sample models.) + */ +import { StrictMode, useState } from 'react'; +import { createRoot } from 'react-dom/client'; +import { ModelViewer } from '@chestnutlabs/gcode-preview-react/model'; +import { MODEL_FIXTURES, MODEL_FIXTURE_BY_ID } from '../../demo-kit/index.js'; + +function App() { + const [source, setSource] = useState(null); + const [info, setInfo] = useState(null); + + function load(id) { + const fx = MODEL_FIXTURE_BY_ID[id]; + if (fx) setSource(fx.source()); // { kind, bytes } → ModelViewer parses + renders + } + + return ( + <> +
+ gcode-preview-react · minimal model viewer + + {info && ( + + {info.objectCount} object{info.objectCount === 1 ? '' : 's'} · materials: {info.materials} + + )} +
+
+ setInfo(i)} /> +
+ + ); +} + +createRoot(document.getElementById('app')).render( + + + +); diff --git a/tools/example-react/src/showcase.jsx b/tools/example-react/src/showcase.jsx new file mode 100644 index 00000000..6dfb4da4 --- /dev/null +++ b/tools/example-react/src/showcase.jsx @@ -0,0 +1,417 @@ +/* + * Full showcase example (DD-031 §4.7, tier 2). + * + * Demonstrates BOTH halves of the SDK in one app, like a slicer's two modes: + * - Preview → sliced G-code / toolpath via (@chestnutlabs/gcode-preview-react) + * - Prepare → source model (STL / 3MF) via (@chestnutlabs/gcode-preview-react/model) + * + * Both are the real published adapters (no raw renderer) and mirror the Feature Lab's capability-aware, + * honest UX in React idiom: toolpath color modes gate on the file's capabilities with a plain-language + * reason; the model side surfaces the material-colour capability tier and object/instance counts. + * Diagnostics/picking come through the ref handle. Styling is the shared workspace-internal demo-kit. + */ +import { StrictMode, useRef, useState } from 'react'; +import { createRoot } from 'react-dom/client'; +import { GcodePreview } from '@chestnutlabs/gcode-preview-react'; +import { ModelViewer } from '@chestnutlabs/gcode-preview-react/model'; +import { FeatureRole } from '@chestnutlabs/toolpath-core'; +import { + FIXTURE_GROUPS, + FIXTURE_BY_ID, + MODEL_FIXTURES, + MODEL_FIXTURE_BY_ID, + COLOR_MODES, + COLOR_MODE_BY_ID, + colorModeReason, + confidenceTier, + rgbCss, + count as formatCount, + duration as formatDuration +} from '../../demo-kit/index.js'; +import '../../demo-kit/tokens.css'; + +const ADHESION_ROLES = [FeatureRole.Skirt, FeatureRole.Brim]; +const CAM_VIEWS = [ + ['iso', 'Iso'], + ['top', 'Top'], + ['front', 'Front'] +]; + +function App() { + const ref = useRef(null); // toolpath handle + const modelRef = useRef(null); // model handle + const [chrome, setChrome] = useState('dark'); + const [mode, setMode] = useState('preview'); // 'preview' (toolpath) | 'prepare' (model) + const [view, setView] = useState('iso'); + + // Preview (toolpath) state + const [fixtureId, setFixtureId] = useState('skirt-brim'); + const [source, setSource] = useState(null); + const [caps, setCaps] = useState({}); + const [layers, setLayers] = useState(0); + const [segments, setSegments] = useState(0); + const [topLayer, setTopLayer] = useState(0); + const [disclosure, setDisclosure] = useState(''); + const [hasRetractions, setHasRetractions] = useState(false); + const [time, setTime] = useState({ ms: null, src: null }); + const [stats, setStats] = useState(null); + const [picked, setPicked] = useState(null); + const [colorModeId, setColorModeId] = useState('feature'); + const [hideAdhesion, setHideAdhesion] = useState(false); + const [showTravel, setShowTravel] = useState(false); + const [showRetractions, setShowRetractions] = useState(false); + + // Prepare (model) state + const [modelFixtureId, setModelFixtureId] = useState('colored-3mf'); + const [modelSource, setModelSource] = useState(null); + const [modelInfo, setModelInfo] = useState(null); + + document.documentElement.dataset.chrome = chrome; + + const modeItem = COLOR_MODE_BY_ID[colorModeId]; + const modeReason = colorModeReason(colorModeId, caps); + const colorMode = modeReason === '' ? modeItem.build() : undefined; + const legend = modeItem.legend ?? null; + const featureRolesKnown = caps.featureRoles === 'known' || caps.featureRoles === 'inferred'; + + async function load(id) { + const fx = FIXTURE_BY_ID[id]; + if (!fx) return; + setFixtureId(id); + const res = await fetch(`./${fx.path}`); + setSource(new Uint8Array(await res.arrayBuffer())); + setStats(null); + setPicked(null); + } + + function loadModel(id) { + const fx = MODEL_FIXTURE_BY_ID[id]; + if (!fx) return; + setModelFixtureId(id); + setModelSource(fx.source()); // { kind, bytes } — no fetch, synthetic MIT-clean fixture + setModelInfo(null); + } + + function onReady(s) { + if (typeof window !== 'undefined') window.gcodePreview = ref.current; + setCaps(s.capabilities); + setLayers(s.layers); + setSegments(s.segments); + setTopLayer(Math.max(0, s.layers - 1)); + const st = ref.current?.state; + if (st) { + setHasRetractions(st.hasRetractions); + setTime({ ms: st.totalTimeMs, src: st.timeEstimateSource }); + if (!st.availableColorModes.includes(colorModeId) && colorModeReason(colorModeId, s.capabilities) !== '') { + setColorModeId('single'); + } + } + } + + function refreshStats() { + setStats(ref.current?.controls.getRenderStats() ?? null); + } + + function onCanvasClick(e) { + if (mode !== 'preview') return; + const c = e.currentTarget.querySelector('canvas'); + if (!c || layers === 0) return; + const r = c.getBoundingClientRect(); + const ndcX = ((e.clientX - r.left) / r.width) * 2 - 1; + const ndcY = -(((e.clientY - r.top) / r.height) * 2 - 1); + setPicked(ref.current?.controls.pickSegment(ndcX, ndcY) ?? null); + } + + const fx = FIXTURE_BY_ID[fixtureId]; + const modelFx = MODEL_FIXTURE_BY_ID[modelFixtureId]; + const prepare = mode === 'prepare'; + + return ( + <> +
+
+ G-code Preview · React + Showcase +
+ {/* Preview (toolpath) vs Prepare (source model) — the two halves of the SDK. */} +
+ + +
+ {!prepare ? ( + + ) : ( + + )} + +
+
+ {['dark', 'light'].map((c) => ( + + ))} +
+
+ +
+
+ {!prepare ? ( + 0 ? [0, topLayer] : null} + hiddenFeatureRoles={hideAdhesion ? ADHESION_ROLES : undefined} + showTravel={showTravel} + showRetractions={showRetractions} + view={view} + frameContent="object" + quality="auto" + onReady={onReady} + onBuildComplete={refreshStats} + onDisclosure={(t) => setDisclosure(t ?? '')} + onParseError={(e) => setDisclosure(`Parse error: ${e.code} — ${e.message}`)} + /> + ) : ( + setModelInfo(info)} + onError={(e) => setDisclosure(`Model error: ${e.code} — ${e.message}`)} + /> + )} + + {!prepare && legend && ( +
+ {modeItem.label} +
+ {legend.map(([label, rgb]) => ( + + + {label} + + ))} +
+
+ )} +
+ {CAM_VIEWS.map(([v, label]) => ( + + ))} +
+ {!prepare && segments > 0 && ( +
+ {formatCount(segments)} segments · {layers} layers + {time.ms !== null && ` · ${formatDuration(time.ms)}`} +
+ )} + {prepare && modelInfo && ( +
+ {formatCount(modelInfo.objectCount)} object{modelInfo.objectCount === 1 ? '' : 's'} + {modelInfo.instancedCount > modelInfo.objectCount && + ` · ${formatCount(modelInfo.instancedCount)} placements`} +
+ )} +
+ + +
+ +
+
+ {!prepare + ? ['featureRoles', 'objects', 'feedrate', 'colorChanges'].map((k) => { + const tier = confidenceTier(caps[k]); + return ( + + {k} · {tier.label} + + ); + }) + : modelInfo && ( + + materials · {confidenceTier(modelInfo.materials).label} + + )} +
+
+ + {disclosure || + (prepare ? 'Prepare: view the source model before slicing.' : fx ? fx.blurb : 'Pick a fixture to begin.')} + +
+ + ); +} + +createRoot(document.getElementById('app')).render( + + + +); diff --git a/tools/example-react/vite.config.js b/tools/example-react/vite.config.js index 61192caa..37da7ce8 100644 --- a/tools/example-react/vite.config.js +++ b/tools/example-react/vite.config.js @@ -1,14 +1,31 @@ import { defineConfig } from 'vite'; +import { resolve } from 'node:path'; +// Two-tier example (DD-031 §4.7): `minimal.html` is the smallest real integration; `showcase.html` +// exercises the full declarative capability surface and shares the workspace-internal demo-kit +// (../demo-kit) with the Feature Lab so the examples read as one project. Both import the REAL +// published @chestnutlabs/gcode-preview-react — never a raw renderer/session. export default defineConfig({ base: './', // Automatic JSX runtime without @vitejs/plugin-react (no HMR fast-refresh needed here). esbuild: { jsx: 'automatic' }, - // Serve the MIT demo corpus (inherited upstream, tracked in test-data/manifest.json). - publicDir: '../../test-data/gcodes', + // Serve the whole MIT demo corpus so demo-kit fixture paths (gcodes/…, fixtures/…) resolve. + publicDir: '../../test-data', server: { port: 5201, - strictPort: true + strictPort: true, + // Allow the shared demo-kit, which lives outside this app's root (tools/demo-kit). + fs: { allow: ['../..'] } + }, + build: { + rollupOptions: { + input: { + index: resolve(import.meta.dirname, 'index.html'), + minimal: resolve(import.meta.dirname, 'minimal.html'), + showcase: resolve(import.meta.dirname, 'showcase.html'), + model: resolve(import.meta.dirname, 'model.html') + } + } }, resolve: { dedupe: ['three', 'react', 'react-dom'] diff --git a/tools/example-svelte/index.html b/tools/example-svelte/index.html index da001a02..66102478 100644 --- a/tools/example-svelte/index.html +++ b/tools/example-svelte/index.html @@ -3,44 +3,90 @@ - GcodePreview Svelte — example app (DD-007 phase 7) + @chestnutlabs/gcode-preview-svelte — examples -
- +
+

@chestnutlabs/gcode-preview-svelte

+

+ Example apps built on the published Svelte adapter — both halves of the SDK: sliced G-code + (Preview) and source models (Prepare). All drive the real package — no raw renderer or parser + imports. +

+ +
diff --git a/tools/example-svelte/minimal.html b/tools/example-svelte/minimal.html new file mode 100644 index 00000000..5be0d745 --- /dev/null +++ b/tools/example-svelte/minimal.html @@ -0,0 +1,43 @@ + + + + + + gcode-preview-svelte — minimal + + + +
+ + + diff --git a/tools/example-svelte/model.html b/tools/example-svelte/model.html new file mode 100644 index 00000000..8b8e2607 --- /dev/null +++ b/tools/example-svelte/model.html @@ -0,0 +1,36 @@ + + + + + + gcode-preview-svelte — minimal model viewer + + + +
+ + + diff --git a/tools/example-svelte/package.json b/tools/example-svelte/package.json index 4eecf2cd..dcfb17db 100644 --- a/tools/example-svelte/package.json +++ b/tools/example-svelte/package.json @@ -3,7 +3,7 @@ "private": true, "version": "0.0.0", "type": "module", - "description": "DD-007 phase 7 example application (issue #114): the packaged Svelte component drives the full pipeline — the separate example app required by the D1 amendment.", + "description": "Two-tier Svelte example (DD-031 §4.7): minimal.html (smallest real integration via ) + showcase.html (full declarative surface + shared demo-kit). Consumes the published @chestnutlabs/gcode-preview-svelte.", "scripts": { "dev": "vite", "build": "vite build" diff --git a/tools/example-svelte/showcase.html b/tools/example-svelte/showcase.html new file mode 100644 index 00000000..3deb583b --- /dev/null +++ b/tools/example-svelte/showcase.html @@ -0,0 +1,123 @@ + + + + + + gcode-preview-svelte — showcase + + + +
+ + + diff --git a/tools/example-svelte/src/App.svelte b/tools/example-svelte/src/App.svelte deleted file mode 100644 index 9b52937f..00000000 --- a/tools/example-svelte/src/App.svelte +++ /dev/null @@ -1,104 +0,0 @@ - - - -
- 0 ? [0, lastLayer] : null} - {scrub} - progress={progressObs} - quality="auto" - on:ready={onReady} - on:parseerror={(e) => append(`parse-error: ${e.detail.code} ${e.detail.message}`)} - on:buildcomplete={(e) => append(`build-complete: ${e.detail.quality}`)} - on:qualityfallback={(e) => append(`quality-fallback: ${e.detail.from} -> ${e.detail.to}`)} - on:progresspresentationchanged={(e) => append(`progress shown as: ${e.detail.mode}${e.detail.reason ? ` (${e.detail.reason})` : ''}`)} - on:disclosure={(e) => e.detail && append(`disclosure: ${e.detail}`)} - on:error={(e) => append(`error: ${e.detail.code}`)} - /> -
diff --git a/tools/example-svelte/src/Minimal.svelte b/tools/example-svelte/src/Minimal.svelte new file mode 100644 index 00000000..9e1a9113 --- /dev/null +++ b/tools/example-svelte/src/Minimal.svelte @@ -0,0 +1,49 @@ + + + +
+ gcode-preview-svelte · minimal + + {#if layers > 0} + + {/if} +
+
+ 0 ? [0, topLayer] : null} showTravel={false} on:ready={onReady} /> +
diff --git a/tools/example-svelte/src/ModelMinimal.svelte b/tools/example-svelte/src/ModelMinimal.svelte new file mode 100644 index 00000000..cbf80691 --- /dev/null +++ b/tools/example-svelte/src/ModelMinimal.svelte @@ -0,0 +1,43 @@ + + + +
+ gcode-preview-svelte · minimal model viewer + + {#if info} + + {info.objectCount} object{info.objectCount === 1 ? '' : 's'} · materials: {info.materials} + + {/if} +
+
+ +
diff --git a/tools/example-svelte/src/Showcase.svelte b/tools/example-svelte/src/Showcase.svelte new file mode 100644 index 00000000..04470bb5 --- /dev/null +++ b/tools/example-svelte/src/Showcase.svelte @@ -0,0 +1,355 @@ + + + +
+
+ G-code Preview · Svelte + Showcase +
+ +
+ + +
+ {#if !prepare} + + {:else} + + {/if} + +
+
+ + +
+
+ +
+ +
+ {#if !prepare} + 0 ? [0, topLayer] : null} + hiddenFeatureRoles={hiddenRoles} + {showTravel} + {showRetractions} + {view} + frameContent="object" + quality="auto" + on:ready={onReady} + on:buildcomplete={refreshStats} + on:disclosure={(e) => (disclosure = e.detail ?? '')} + on:parseerror={(e) => (disclosure = `Parse error: ${e.detail.code} — ${e.detail.message}`)} + /> + {:else} + (disclosure = `Model error: ${e.detail.code} — ${e.detail.message}`)} + /> + {/if} + {#if !prepare && legend} +
+ {colorModeItem.label} +
+ {#each legend as [label, rgb]} + + + {label} + + {/each} +
+
+ {/if} +
+ {#each CAM_VIEWS as [v, label]} + + {/each} +
+ {#if !prepare && segments > 0} +
{countLabel}
+ {/if} + {#if prepare && modelInfo} +
{modelCountLabel}
+ {/if} +
+ + +
+ +
+
+ {#if !prepare} + {#each CAP_KEYS as k} + + {k} · {confidenceTier(caps[k]).label} + + {/each} + {:else if modelInfo} + materials · {materialsTier.label} + {/if} +
+
+ {statusText} +
diff --git a/tools/example-svelte/src/main.js b/tools/example-svelte/src/main.js deleted file mode 100644 index ac77a891..00000000 --- a/tools/example-svelte/src/main.js +++ /dev/null @@ -1,4 +0,0 @@ -import App from './App.svelte'; - -const app = new App({ target: document.getElementById('app') }); -window.svelteExample = { app }; diff --git a/tools/example-svelte/src/minimal.js b/tools/example-svelte/src/minimal.js new file mode 100644 index 00000000..0af9f82b --- /dev/null +++ b/tools/example-svelte/src/minimal.js @@ -0,0 +1,3 @@ +import Minimal from './Minimal.svelte'; + +new Minimal({ target: document.getElementById('app') }); diff --git a/tools/example-svelte/src/model-minimal.js b/tools/example-svelte/src/model-minimal.js new file mode 100644 index 00000000..0a66fc2d --- /dev/null +++ b/tools/example-svelte/src/model-minimal.js @@ -0,0 +1,3 @@ +import ModelMinimal from './ModelMinimal.svelte'; + +new ModelMinimal({ target: document.getElementById('app') }); diff --git a/tools/example-svelte/src/showcase.js b/tools/example-svelte/src/showcase.js new file mode 100644 index 00000000..c00a49e6 --- /dev/null +++ b/tools/example-svelte/src/showcase.js @@ -0,0 +1,4 @@ +import Showcase from './Showcase.svelte'; +import '../../demo-kit/tokens.css'; + +new Showcase({ target: document.getElementById('app') }); diff --git a/tools/example-svelte/vite.config.js b/tools/example-svelte/vite.config.js index 18e78666..addecc4e 100644 --- a/tools/example-svelte/vite.config.js +++ b/tools/example-svelte/vite.config.js @@ -1,14 +1,30 @@ import { defineConfig } from 'vite'; import { svelte } from '@sveltejs/vite-plugin-svelte'; +import { resolve } from 'node:path'; +// Two-tier example (DD-031 §4.7): `minimal.html` is the smallest real integration; `showcase.html` +// exercises the full declarative surface and shares the workspace-internal demo-kit (../demo-kit) +// with the Feature Lab. Both import the REAL published @chestnutlabs/gcode-preview-svelte. export default defineConfig({ base: './', plugins: [svelte()], - // Serve the MIT demo corpus (inherited upstream, tracked in test-data/manifest.json). - publicDir: '../../test-data/gcodes', + // Serve the whole MIT demo corpus so demo-kit fixture paths (gcodes/…, fixtures/…) resolve. + publicDir: '../../test-data', server: { port: 5202, - strictPort: true + strictPort: true, + // Allow the shared demo-kit, which lives outside this app's root (tools/demo-kit). + fs: { allow: ['../..'] } + }, + build: { + rollupOptions: { + input: { + index: resolve(import.meta.dirname, 'index.html'), + minimal: resolve(import.meta.dirname, 'minimal.html'), + model: resolve(import.meta.dirname, 'model.html'), + showcase: resolve(import.meta.dirname, 'showcase.html') + } + } }, resolve: { dedupe: ['three', 'svelte'] diff --git a/tools/example-vue/.gitignore b/tools/example-vue/.gitignore new file mode 100644 index 00000000..f06235c4 --- /dev/null +++ b/tools/example-vue/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/tools/example-vue/index.html b/tools/example-vue/index.html new file mode 100644 index 00000000..c906ddc9 --- /dev/null +++ b/tools/example-vue/index.html @@ -0,0 +1,92 @@ + + + + + + @chestnutlabs/gcode-preview-vue — examples + + + +
+

@chestnutlabs/gcode-preview-vue

+

+ Example apps built on the published Vue adapter — both halves of the SDK: sliced G-code + (Preview) and source models (Prepare). All drive the real package — no raw renderer or parser + imports. +

+ +
+ + diff --git a/tools/example-vue/minimal.html b/tools/example-vue/minimal.html new file mode 100644 index 00000000..929bfd08 --- /dev/null +++ b/tools/example-vue/minimal.html @@ -0,0 +1,43 @@ + + + + + + gcode-preview-vue — minimal + + + +
+ + + diff --git a/tools/example-vue/model.html b/tools/example-vue/model.html new file mode 100644 index 00000000..d8521a5b --- /dev/null +++ b/tools/example-vue/model.html @@ -0,0 +1,39 @@ + + + + + + gcode-preview-vue — minimal model viewer + + + +
+ + + diff --git a/tools/example-vue/package-lock.json b/tools/example-vue/package-lock.json new file mode 100644 index 00000000..645ff1bf --- /dev/null +++ b/tools/example-vue/package-lock.json @@ -0,0 +1,1520 @@ +{ + "name": "gcode-preview-example-vue", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gcode-preview-example-vue", + "version": "0.0.0", + "dependencies": { + "@chestnutlabs/gcode-containers": "file:../../packages/gcode-containers", + "@chestnutlabs/gcode-dialects": "file:../../packages/gcode-dialects", + "@chestnutlabs/gcode-parser": "file:../../packages/gcode-parser", + "@chestnutlabs/gcode-preview-core": "file:../../packages/gcode-preview-core", + "@chestnutlabs/gcode-preview-vue": "file:../../packages/gcode-preview-vue", + "@chestnutlabs/gcode-renderer-three": "file:../../packages/gcode-renderer-three", + "@chestnutlabs/toolpath-core": "file:../../packages/toolpath-core", + "three": "0.178.0", + "vue": "^3.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "vite": "^6.0.0" + } + }, + "../../packages/gcode-containers": { + "name": "@chestnutlabs/gcode-containers", + "version": "0.19.0", + "license": "MIT", + "dependencies": { + "@chestnutlabs/toolpath-core": "0.19.0" + }, + "engines": { + "node": ">=22" + } + }, + "../../packages/gcode-dialects": { + "name": "@chestnutlabs/gcode-dialects", + "version": "0.19.0", + "license": "MIT", + "dependencies": { + "@chestnutlabs/toolpath-core": "0.19.0" + }, + "engines": { + "node": ">=22" + } + }, + "../../packages/gcode-parser": { + "name": "@chestnutlabs/gcode-parser", + "version": "0.19.0", + "license": "MIT", + "dependencies": { + "@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" + } + }, + "../../packages/gcode-preview-core": { + "name": "@chestnutlabs/gcode-preview-core", + "version": "0.19.0", + "license": "MIT", + "dependencies": { + "@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" + } + }, + "../../packages/gcode-preview-vue": { + "name": "@chestnutlabs/gcode-preview-vue", + "version": "0.19.0", + "license": "MIT", + "dependencies": { + "@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" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "vue": "^3.4.0" + } + }, + "../../packages/gcode-renderer-three": { + "name": "@chestnutlabs/gcode-renderer-three", + "version": "0.19.0", + "license": "MIT", + "dependencies": { + "@chestnutlabs/gcode-colors": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0" + }, + "devDependencies": { + "@types/three": "0.178.0" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "three": "^0.178.0" + } + }, + "../../packages/toolpath-core": { + "name": "@chestnutlabs/toolpath-core", + "version": "0.19.0", + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@chestnutlabs/gcode-containers": { + "resolved": "../../packages/gcode-containers", + "link": true + }, + "node_modules/@chestnutlabs/gcode-dialects": { + "resolved": "../../packages/gcode-dialects", + "link": true + }, + "node_modules/@chestnutlabs/gcode-parser": { + "resolved": "../../packages/gcode-parser", + "link": true + }, + "node_modules/@chestnutlabs/gcode-preview-core": { + "resolved": "../../packages/gcode-preview-core", + "link": true + }, + "node_modules/@chestnutlabs/gcode-preview-vue": { + "resolved": "../../packages/gcode-preview-vue", + "link": true + }, + "node_modules/@chestnutlabs/gcode-renderer-three": { + "resolved": "../../packages/gcode-renderer-three", + "link": true + }, + "node_modules/@chestnutlabs/toolpath-core": { + "resolved": "../../packages/toolpath-core", + "link": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.42.tgz", + "integrity": "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.42", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz", + "integrity": "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz", + "integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.42", + "@vue/compiler-dom": "3.5.42", + "@vue/compiler-ssr": "3.5.42", + "@vue/shared": "3.5.42", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz", + "integrity": "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.42.tgz", + "integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.42.tgz", + "integrity": "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz", + "integrity": "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.42", + "@vue/runtime-core": "3.5.42", + "@vue/shared": "3.5.42", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.42.tgz", + "integrity": "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.42", + "@vue/runtime-dom": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.42.tgz", + "integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/three": { + "version": "0.178.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.178.0.tgz", + "integrity": "sha512-ybFIB0+x8mz0wnZgSGy2MO/WCO6xZhQSZnmfytSPyNpM0sBafGRVhdaj+erYh5U+RhQOAg/eXqw5uVDiM2BjhQ==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.42.tgz", + "integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.42", + "@vue/compiler-sfc": "3.5.42", + "@vue/runtime-dom": "3.5.42", + "@vue/server-renderer": "3.5.42", + "@vue/shared": "3.5.42" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/tools/example-vue/package.json b/tools/example-vue/package.json new file mode 100644 index 00000000..9c937fb7 --- /dev/null +++ b/tools/example-vue/package.json @@ -0,0 +1,26 @@ +{ + "name": "gcode-preview-example-vue", + "private": true, + "version": "0.0.0", + "type": "module", + "description": "Two-tier Vue example (DD-031 §4.7): minimal.html (smallest real integration via ) + showcase.html (full declarative surface + shared demo-kit). Consumes the published @chestnutlabs/gcode-preview-vue.", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "dependencies": { + "@chestnutlabs/gcode-containers": "file:../../packages/gcode-containers", + "@chestnutlabs/gcode-dialects": "file:../../packages/gcode-dialects", + "@chestnutlabs/gcode-parser": "file:../../packages/gcode-parser", + "@chestnutlabs/gcode-preview-core": "file:../../packages/gcode-preview-core", + "@chestnutlabs/gcode-preview-vue": "file:../../packages/gcode-preview-vue", + "@chestnutlabs/gcode-renderer-three": "file:../../packages/gcode-renderer-three", + "@chestnutlabs/toolpath-core": "file:../../packages/toolpath-core", + "three": "0.178.0", + "vue": "^3.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "vite": "^6.0.0" + } +} diff --git a/tools/example-vue/showcase.html b/tools/example-vue/showcase.html new file mode 100644 index 00000000..483f2b99 --- /dev/null +++ b/tools/example-vue/showcase.html @@ -0,0 +1,123 @@ + + + + + + gcode-preview-vue — showcase + + + +
+ + + diff --git a/tools/example-vue/src/Minimal.vue b/tools/example-vue/src/Minimal.vue new file mode 100644 index 00000000..28ba8d4c --- /dev/null +++ b/tools/example-vue/src/Minimal.vue @@ -0,0 +1,52 @@ + + + + diff --git a/tools/example-vue/src/ModelMinimal.vue b/tools/example-vue/src/ModelMinimal.vue new file mode 100644 index 00000000..778199db --- /dev/null +++ b/tools/example-vue/src/ModelMinimal.vue @@ -0,0 +1,38 @@ + + + + diff --git a/tools/example-vue/src/Showcase.vue b/tools/example-vue/src/Showcase.vue new file mode 100644 index 00000000..aa43af75 --- /dev/null +++ b/tools/example-vue/src/Showcase.vue @@ -0,0 +1,362 @@ + + + + diff --git a/tools/example-vue/src/minimal.js b/tools/example-vue/src/minimal.js new file mode 100644 index 00000000..11efc66e --- /dev/null +++ b/tools/example-vue/src/minimal.js @@ -0,0 +1,4 @@ +import { createApp } from 'vue'; +import Minimal from './Minimal.vue'; + +createApp(Minimal).mount('#app'); diff --git a/tools/example-vue/src/model-minimal.js b/tools/example-vue/src/model-minimal.js new file mode 100644 index 00000000..a8371ab3 --- /dev/null +++ b/tools/example-vue/src/model-minimal.js @@ -0,0 +1,4 @@ +import { createApp } from 'vue'; +import ModelMinimal from './ModelMinimal.vue'; + +createApp(ModelMinimal).mount('#app'); diff --git a/tools/example-vue/src/showcase.js b/tools/example-vue/src/showcase.js new file mode 100644 index 00000000..83254868 --- /dev/null +++ b/tools/example-vue/src/showcase.js @@ -0,0 +1,5 @@ +import { createApp } from 'vue'; +import Showcase from './Showcase.vue'; +import '../../demo-kit/tokens.css'; + +createApp(Showcase).mount('#app'); diff --git a/tools/example-vue/vite.config.js b/tools/example-vue/vite.config.js new file mode 100644 index 00000000..831872f0 --- /dev/null +++ b/tools/example-vue/vite.config.js @@ -0,0 +1,43 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import { resolve } from 'node:path'; + +// Two-tier example (DD-031 §4.7): `minimal.html` is the smallest real integration; `showcase.html` +// exercises the full declarative surface and shares the workspace-internal demo-kit (../demo-kit) +// with the Feature Lab. Both import the REAL published @chestnutlabs/gcode-preview-vue. +export default defineConfig({ + base: './', + plugins: [vue()], + // Serve the whole MIT demo corpus so demo-kit fixture paths (gcodes/…, fixtures/…) resolve. + publicDir: '../../test-data', + server: { + port: 5203, + strictPort: true, + // Allow the shared demo-kit, which lives outside this app's root (tools/demo-kit). + fs: { allow: ['../..'] } + }, + build: { + rollupOptions: { + input: { + index: resolve(import.meta.dirname, 'index.html'), + minimal: resolve(import.meta.dirname, 'minimal.html'), + showcase: resolve(import.meta.dirname, 'showcase.html'), + model: resolve(import.meta.dirname, 'model.html') + } + } + }, + resolve: { + dedupe: ['three', 'vue'] + }, + optimizeDeps: { + exclude: [ + '@chestnutlabs/gcode-parser', + '@chestnutlabs/toolpath-core', + '@chestnutlabs/gcode-renderer-three', + '@chestnutlabs/gcode-dialects', + '@chestnutlabs/gcode-containers', + '@chestnutlabs/gcode-preview-core', + '@chestnutlabs/gcode-preview-vue' + ] + } +}); diff --git a/tools/example-webcomponent/.gitignore b/tools/example-webcomponent/.gitignore new file mode 100644 index 00000000..f06235c4 --- /dev/null +++ b/tools/example-webcomponent/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/tools/example-webcomponent/index.html b/tools/example-webcomponent/index.html new file mode 100644 index 00000000..ae16a1b1 --- /dev/null +++ b/tools/example-webcomponent/index.html @@ -0,0 +1,93 @@ + + + + + + @chestnutlabs/gcode-preview-element — examples + + + +
+

@chestnutlabs/gcode-preview-element

+

+ Example apps built on the published <gcode-preview> and + <gcode-model-viewer> Web Components — framework-free, vanilla JS. They cover + both halves of the SDK: Preview a sliced toolpath and Prepare + a source model. All drive the real package: no raw renderer or parser imports. +

+ +
+ + diff --git a/tools/example-webcomponent/minimal.html b/tools/example-webcomponent/minimal.html new file mode 100644 index 00000000..8a639191 --- /dev/null +++ b/tools/example-webcomponent/minimal.html @@ -0,0 +1,57 @@ + + + + + + gcode-preview element — minimal + + + +
+ gcode-preview element · minimal + + +
+ + + + + diff --git a/tools/example-webcomponent/model.html b/tools/example-webcomponent/model.html new file mode 100644 index 00000000..d9c1d3b3 --- /dev/null +++ b/tools/example-webcomponent/model.html @@ -0,0 +1,50 @@ + + + + + + gcode-preview element — minimal model viewer + + + +
+ gcode-model-viewer · minimal + + +
+ + + + + diff --git a/tools/example-webcomponent/package-lock.json b/tools/example-webcomponent/package-lock.json new file mode 100644 index 00000000..6b53d1f5 --- /dev/null +++ b/tools/example-webcomponent/package-lock.json @@ -0,0 +1,1298 @@ +{ + "name": "gcode-preview-example-webcomponent", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gcode-preview-example-webcomponent", + "version": "0.0.0", + "dependencies": { + "@chestnutlabs/gcode-containers": "file:../../packages/gcode-containers", + "@chestnutlabs/gcode-dialects": "file:../../packages/gcode-dialects", + "@chestnutlabs/gcode-parser": "file:../../packages/gcode-parser", + "@chestnutlabs/gcode-preview-core": "file:../../packages/gcode-preview-core", + "@chestnutlabs/gcode-preview-element": "file:../../packages/gcode-preview-element", + "@chestnutlabs/gcode-renderer-three": "file:../../packages/gcode-renderer-three", + "@chestnutlabs/toolpath-core": "file:../../packages/toolpath-core", + "three": "0.178.0" + }, + "devDependencies": { + "vite": "^6.0.0" + } + }, + "../../packages/gcode-containers": { + "name": "@chestnutlabs/gcode-containers", + "version": "0.19.0", + "license": "MIT", + "dependencies": { + "@chestnutlabs/toolpath-core": "0.19.0" + }, + "engines": { + "node": ">=22" + } + }, + "../../packages/gcode-dialects": { + "name": "@chestnutlabs/gcode-dialects", + "version": "0.19.0", + "license": "MIT", + "dependencies": { + "@chestnutlabs/toolpath-core": "0.19.0" + }, + "engines": { + "node": ">=22" + } + }, + "../../packages/gcode-parser": { + "name": "@chestnutlabs/gcode-parser", + "version": "0.19.0", + "license": "MIT", + "dependencies": { + "@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" + } + }, + "../../packages/gcode-preview-core": { + "name": "@chestnutlabs/gcode-preview-core", + "version": "0.19.0", + "license": "MIT", + "dependencies": { + "@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" + } + }, + "../../packages/gcode-preview-element": { + "name": "@chestnutlabs/gcode-preview-element", + "version": "0.19.0", + "license": "MIT", + "dependencies": { + "@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" + } + }, + "../../packages/gcode-renderer-three": { + "name": "@chestnutlabs/gcode-renderer-three", + "version": "0.19.0", + "license": "MIT", + "dependencies": { + "@chestnutlabs/gcode-colors": "0.19.0", + "@chestnutlabs/toolpath-core": "0.19.0" + }, + "devDependencies": { + "@types/three": "0.178.0" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "three": "^0.178.0" + } + }, + "../../packages/toolpath-core": { + "name": "@chestnutlabs/toolpath-core", + "version": "0.19.0", + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@chestnutlabs/gcode-containers": { + "resolved": "../../packages/gcode-containers", + "link": true + }, + "node_modules/@chestnutlabs/gcode-dialects": { + "resolved": "../../packages/gcode-dialects", + "link": true + }, + "node_modules/@chestnutlabs/gcode-parser": { + "resolved": "../../packages/gcode-parser", + "link": true + }, + "node_modules/@chestnutlabs/gcode-preview-core": { + "resolved": "../../packages/gcode-preview-core", + "link": true + }, + "node_modules/@chestnutlabs/gcode-preview-element": { + "resolved": "../../packages/gcode-preview-element", + "link": true + }, + "node_modules/@chestnutlabs/gcode-renderer-three": { + "resolved": "../../packages/gcode-renderer-three", + "link": true + }, + "node_modules/@chestnutlabs/toolpath-core": { + "resolved": "../../packages/toolpath-core", + "link": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/three": { + "version": "0.178.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.178.0.tgz", + "integrity": "sha512-ybFIB0+x8mz0wnZgSGy2MO/WCO6xZhQSZnmfytSPyNpM0sBafGRVhdaj+erYh5U+RhQOAg/eXqw5uVDiM2BjhQ==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/tools/example-webcomponent/package.json b/tools/example-webcomponent/package.json new file mode 100644 index 00000000..8c388e2f --- /dev/null +++ b/tools/example-webcomponent/package.json @@ -0,0 +1,24 @@ +{ + "name": "gcode-preview-example-webcomponent", + "private": true, + "version": "0.0.0", + "type": "module", + "description": "Two-tier Web Component example (DD-031 §4.7): minimal.html (smallest real integration of ) + showcase.html (full attribute/property/event surface + shared demo-kit). Consumes the published @chestnutlabs/gcode-preview-element.", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "dependencies": { + "@chestnutlabs/gcode-containers": "file:../../packages/gcode-containers", + "@chestnutlabs/gcode-dialects": "file:../../packages/gcode-dialects", + "@chestnutlabs/gcode-parser": "file:../../packages/gcode-parser", + "@chestnutlabs/gcode-preview-core": "file:../../packages/gcode-preview-core", + "@chestnutlabs/gcode-preview-element": "file:../../packages/gcode-preview-element", + "@chestnutlabs/gcode-renderer-three": "file:../../packages/gcode-renderer-three", + "@chestnutlabs/toolpath-core": "file:../../packages/toolpath-core", + "three": "0.178.0" + }, + "devDependencies": { + "vite": "^6.0.0" + } +} diff --git a/tools/example-webcomponent/showcase.html b/tools/example-webcomponent/showcase.html new file mode 100644 index 00000000..0159eb88 --- /dev/null +++ b/tools/example-webcomponent/showcase.html @@ -0,0 +1,223 @@ + + + + + + gcode-preview element — showcase + + + +
+
+ G-code Preview · Web Component + Showcase +
+ +
+ + +
+ + + +
+
+ + +
+
+ +
+
+ + + + + +
+ + + +
+ +
+ + +
+ +
+
+
+ Pick a fixture to begin. +
+ + + + diff --git a/tools/example-webcomponent/src/minimal.js b/tools/example-webcomponent/src/minimal.js new file mode 100644 index 00000000..a6235b3f --- /dev/null +++ b/tools/example-webcomponent/src/minimal.js @@ -0,0 +1,43 @@ +/* + * Minimal getting-started example (DD-031 §4.7, tier 1). + * + * The smallest real integration of the Web Component: importing `/define` + * auto-registers the element, then set the `source` PROPERTY (objects can't be attributes) and it + * parses + renders. The layer slider drives the `layer-range` ATTRIBUTE — the attribute/property + * split is the one thing that makes the Web Component differ from the framework wrappers. No design + * system, no demo-kit, no raw renderer. Copy this file to start. + */ +import '@chestnutlabs/gcode-preview-element/define'; + +const view = document.getElementById('view'); +const fixture = document.getElementById('fixture'); +const layer = document.getElementById('layer'); +const layerLabel = document.getElementById('layerLabel'); +const layerNum = document.getElementById('layerNum'); +const layerMax = document.getElementById('layerMax'); + +// Hide travel moves for a clean first view (property; the element defaults showTravel to true). +view.showTravel = false; + +let layers = 0; + +fixture.addEventListener('change', async () => { + if (!fixture.value) return; + const res = await fetch(`./gcodes/${fixture.value}`); + view.source = new Uint8Array(await res.arrayBuffer()); // property (object) → re-parse +}); + +// `ready`'s detail carries the parse summary. +view.addEventListener('ready', (e) => { + layers = e.detail.layers; + layerLabel.hidden = layers === 0; + layer.max = String(Math.max(0, layers - 1)); + layer.value = layer.max; + layerNum.textContent = layer.value; + layerMax.textContent = layer.max; +}); + +layer.addEventListener('input', () => { + layerNum.textContent = layer.value; + view.setAttribute('layer-range', `0,${layer.value}`); // primitive → attribute +}); diff --git a/tools/example-webcomponent/src/model-minimal.js b/tools/example-webcomponent/src/model-minimal.js new file mode 100644 index 00000000..432a2feb --- /dev/null +++ b/tools/example-webcomponent/src/model-minimal.js @@ -0,0 +1,33 @@ +/* + * Minimal source-model viewer (DD-031 §4.7, tier 1 — Prepare side). + * + * The smallest real integration of the Web Component: importing `/model/define` + * auto-registers the element (tag `gcode-model-viewer`, not the reserved `model-viewer`), then set the + * `source` PROPERTY ({ kind, bytes } — an STL or 3MF; objects can't be attributes) and it parses + + * renders. The counterpart to minimal.js, which does the same for sliced G-code with . + * No design system, no demo-kit tokens — just the published adapter. (The fixtures are synthetic + * MIT-clean sample models.) + */ +import '@chestnutlabs/gcode-preview-element/model/define'; +import { MODEL_FIXTURES, MODEL_FIXTURE_BY_ID } from '../../demo-kit/index.js'; + +const view = document.getElementById('view'); +const fixture = document.getElementById('fixture'); +const info = document.getElementById('info'); + +// Populate the picker from the shared model fixtures. +fixture.insertAdjacentHTML( + 'beforeend', + MODEL_FIXTURES.map((m) => ``).join('') +); + +fixture.addEventListener('change', () => { + const fx = MODEL_FIXTURE_BY_ID[fixture.value]; + if (fx) view.source = fx.source(); // property (object) → parse + render +}); + +// `ready`'s detail is a ModelReadyInfo carrying the parse summary. +view.addEventListener('ready', (e) => { + const i = e.detail; + info.textContent = `${i.objectCount} object${i.objectCount === 1 ? '' : 's'} · materials: ${i.materials}`; +}); diff --git a/tools/example-webcomponent/src/showcase.js b/tools/example-webcomponent/src/showcase.js new file mode 100644 index 00000000..7ba58b3b --- /dev/null +++ b/tools/example-webcomponent/src/showcase.js @@ -0,0 +1,339 @@ +/* + * Full showcase example (DD-031 §4.7, tier 2). + * + * Exercises the real surface of the published Web Component and mirrors the Feature + * Lab's capability-aware UX in vanilla JS: color modes gate on the file's capabilities with a + * plain-language reason, feature-role hiding is a property, and diagnostics/picking come through + * `element.controls`. This is the adapter where the attribute/property split matters — objects + * (source, colorMode, hiddenFeatureRoles) are set as PROPERTIES; primitives (layer-range, view) can + * be set as ATTRIBUTES. Chrome (light/dark) is separate from the renderer's neutral viewport. + * Styling is the shared workspace-internal demo-kit. Still the real package: no raw renderer imports. + */ +import '@chestnutlabs/gcode-preview-element/define'; +// Auto-registers (the Prepare-side source-model viewer). Note the tag is +// `gcode-model-viewer`, not the reserved `model-viewer`. +import '@chestnutlabs/gcode-preview-element/model/define'; +import { FeatureRole } from '@chestnutlabs/toolpath-core'; +import { + FIXTURE_GROUPS, + FIXTURE_BY_ID, + MODEL_FIXTURES, + MODEL_FIXTURE_BY_ID, + COLOR_MODES, + COLOR_MODE_BY_ID, + colorModeReason, + confidenceTier, + rgbCss, + count as formatCount, + duration as formatDuration +} from '../../demo-kit/index.js'; +import '../../demo-kit/tokens.css'; + +const ADHESION_ROLES = [FeatureRole.Skirt, FeatureRole.Brim]; +const CAP_KEYS = ['featureRoles', 'objects', 'feedrate', 'colorChanges']; +const $ = (id) => document.getElementById(id); + +const view = $('view'); +const modelView = $('modelView'); +let caps = {}; +let layers = 0; +let segments = 0; +let colorModeId = 'feature'; +let mode = 'preview'; // 'preview' (toolpath) | 'prepare' (source model) +let previewCount = { has: false, text: '' }; // preview count-pill state, restored when switching back +let modelInfo = null; // last ModelReadyInfo (Prepare) + +// ---- fixture pickers: one