diff --git a/.changeset/capture-exclude-build-volume.md b/.changeset/capture-exclude-build-volume.md new file mode 100644 index 00000000..414b71a2 --- /dev/null +++ b/.changeset/capture-exclude-build-volume.md @@ -0,0 +1,11 @@ +--- +"@chestnutlabs/gcode-renderer-three": minor +--- + +`CaptureOptions` gains `includeBuildVolume?: boolean` (default `true`). Set it to `false` to exclude +the whole build-volume group (grid + bed surface + origin + cage) from the off-screen capture render +only — so `capture({ background: 'transparent', includeBuildVolume: false })` produces a clean +toolpath-only cutout, matching `ModelRenderer` thumbnails. The live view is untouched (visibility is +restored synchronously, no intermediate frame is painted). The option flows through +`GcodePreviewControls.capture` to every adapter; it is a no-op where there is no build volume +(`ModelViewer`) or no capture (the 2D renderer). diff --git a/.changeset/consumer-ux-parity-dark-capabilities.md b/.changeset/consumer-ux-parity-dark-capabilities.md new file mode 100644 index 00000000..830a660c --- /dev/null +++ b/.changeset/consumer-ux-parity-dark-capabilities.md @@ -0,0 +1,37 @@ +--- +"@chestnutlabs/gcode-preview-core": minor +"@chestnutlabs/gcode-preview-vue": minor +"@chestnutlabs/gcode-preview-react": minor +"@chestnutlabs/gcode-preview-svelte": minor +"@chestnutlabs/gcode-preview-element": minor +--- + +Close the dark-capability gaps so no shipped capability is reachable only through the +`raw.renderer()` escape hatch, with full parity across all four framework adapters (DD-031). + +**New on the public controller (`GcodePreviewControls`), so it flows to every adapter's `controls`:** + +- `getRenderStats()` — the DD-027 render-diagnostics snapshot (backend, GPU, geometry mode, build + parallelism, timings), previously reachable only on the Three renderer. Returns `null` on the 2D + renderer (which produces no GPU/geometry diagnostics — never a fabricated stats object). +- `pickSegment(ndcX, ndcY, threshold?)` — source-mapping / segment picking, previously renderer-only. +- `isColorModeAvailable(mode)` — the honest per-file color-mode capability gate. + +**New capability-aware state (`GcodePreviewState`):** `availableColorModes`, `hasRetractions`, +`hasColorChanges` — refreshed after each parse, so a UI can offer and *explain* controls instead of +guessing (e.g. gray out feature coloring on a file with no feature roles, with a reason). + +**Adapter parity:** + +- `hiddenFeatureRoles` is now a declarative prop (Vue/React/Svelte) and a `hidden-feature-roles` + attribute (Web Component) — the declarative form of `setFeatureRoleVisible` (hide Skirt/Brim to + declutter a part). Gate it on `capabilities.featureRoles`. +- `capture()` is now a top-level method on the Vue/React/Svelte handles too, matching the Web + Component's `element.capture()`. +- Web Component: `renderer`/`adjacentLayers` gain property accessors; the package now also re-exports + `RendererMode` and `PreviewRenderer`. + +All additive — existing code keeps working unchanged; the 2D renderer honors the widened contract as +documented no-ops / honest `null`. The portable behavioral suite is extended (layer-range, color-mode +gating, diagnostics + picking reachability) and gains a controls-completeness parity guard so a +capability can no longer ship in core and silently vanish from an adapter. diff --git a/.changeset/model-viewer-adapters.md b/.changeset/model-viewer-adapters.md new file mode 100644 index 00000000..23ab1879 --- /dev/null +++ b/.changeset/model-viewer-adapters.md @@ -0,0 +1,39 @@ +--- +"@chestnutlabs/gcode-model-renderer": minor +"@chestnutlabs/gcode-preview-vue": minor +"@chestnutlabs/gcode-preview-react": minor +"@chestnutlabs/gcode-preview-svelte": minor +"@chestnutlabs/gcode-preview-element": minor +--- + +Make interactive **source-model viewing** (STL / 3MF) a first-class, declarative half of the SDK — +the Prepare side alongside the toolpath Preview side — so consumers no longer have to drop to the +imperative `createModelViewer` engine to show a model in a framework (DD-031). + +**New framework-neutral controller** (`@chestnutlabs/gcode-model-renderer`): + +- `createModelPreviewController(options)` — the Prepare-side analogue of `gcode-preview-core`'s toolpath + controller. It wraps the existing `createModelViewer` engine unchanged and adds what a framework + binding needs: canvas-deferred construction with `bindCanvas` (rebuilding on a canvas swap and + replaying the last source), a reactive `getState()`/`onStateChange()` snapshot derived from the + engine's events (`ModelPreviewState`: `loading`/`ready`/`rendererSupported`/`objectCount`/`materials`/ + `instancedCount`/`decimationApplied`/`bounds`/`plates`/`hasPlates`/`cameraState`/`progress`/`error`), + and an op queue for calls made before the canvas binds. `ModelPreviewControls` exposes `setSource`, + `setView`, get/`setCameraState`, `setBackground`, `setInteractionQuality`, `setRenderScope`, `frame`, + `resize`, and `capture`. +- A portable model behavioral suite ships via the `@chestnutlabs/gcode-model-renderer/testing` subpath + (`runModelBehavioralSuite`) with controls/state completeness parity guards. + +**New `/model` adapter subpath on every framework** — thin shells over the one controller, mirroring +each framework's toolpath idiom: + +- `@chestnutlabs/gcode-preview-react/model` — `useModelViewer` hook + `` component. +- `@chestnutlabs/gcode-preview-vue/model` — `useModelViewer` composable + `` component. +- `@chestnutlabs/gcode-preview-svelte/model` — `createModelViewer` store/action + the raw + `@chestnutlabs/gcode-preview-svelte/model/ModelViewer.svelte` component. +- `@chestnutlabs/gcode-preview-element/model` (+ `/model/define`) — the `` custom + element (tag chosen to avoid the reserved ``). + +All additive — the toolpath surface is unchanged, and toolpath-only consumers don't pull the model +renderer (it lives behind the opt-in `/model` subpath). Every adapter passes the portable model +behavioral suite; each package gains `@chestnutlabs/gcode-model-renderer` as a dependency. 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/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