diff --git a/docs/VISUAL_FEATURE_COVERAGE.md b/docs/VISUAL_FEATURE_COVERAGE.md index bbdeb1c9..7d2dee43 100644 --- a/docs/VISUAL_FEATURE_COVERAGE.md +++ b/docs/VISUAL_FEATURE_COVERAGE.md @@ -65,15 +65,28 @@ What moved to **covered** in the second public-product pass: and `layer-buildup.webm` (layer-range sweep — the progressive reveal), in the feature gallery and the workers concept page. -Still on the **to-capture** list (honest remainder — recorded, not forgotten): - -- `needs animation`: **time scrub**, interaction-aware quality, user-scrub-wins, and the *actual* - load-time progressive/`hold` reveal (the layer-buildup clip stands in for it today). -- `needs comparison`: **object-aware framing** (whole-job vs model-aware), **fresh vs stale** - progress, **render scope** (plate/object subset before/after). -- `needs screenshot`: **diagnostics panel** in-context, **transparent-background capture** over a - checkerboard, per-mode **color legends** (feature/object/power), circular/polygon **bed** shots - with a flatter model for a cleaner outline. +**Remainder cleared** (new MIT fixture `skirt-brim-model.gcode` = a model wrapped by skirt + brim): + +- `object-aware framing` — **captured**: `frame-all` vs `frame-object` comparison (skirt included vs + the part framed alone). In the gallery. +- `setFeatureRoleVisible` (hide brim/skirt) — **captured**: `frame-all` vs `adhesion-hidden` before/ + after (skirt ring present vs gone). In the gallery. +- `getRenderStats` diagnostics in-context — **captured**: `diagnostics-panel` (the demo panel reading + backend/GPU/geometry/counts/timings beside the render). In the gallery. + +Genuinely deferred, each with a recorded reason (valid dispositions, not silent omissions): + +- **Transparent-background capture** — `capture({background:'transparent'})` returns correct alpha on + real hardware (AnyBridge validated on an RTX 4070), but **headless SwiftShader flattens the + render-target readback to opaque**, so a truthful composite can't be produced in the CI/dev + environment. The harness recipe (`transparent-checker`) is retained for a GPU-backed run. +- **Interaction-aware quality** — the feature *is* the motion (detail drops while orbiting, restores + on settle); a still can't carry it and a synthetic clip would misrepresent it. Documented in prose. +- **True load-time progressive/`hold` reveal** — the `layer-buildup` clip stands in; an authentic + load capture is timing-sensitive under headless SwiftShader. +- **Time scrub** animation — visually near-identical to `scrub-sweep`; redundant. +- **Fresh vs stale progress**, **render scope** (plate/object subset), per-mode **color legends** — + feasible, lower-priority polish; left for a future review round. These are tracked here so the next release's Public Product + Documentation + Visual Review picks them up rather than rediscovering them. @@ -90,7 +103,7 @@ them up rather than rediscovering them. | Retraction / de-retraction markers | ✅ | ✅ | `retraction-markers` | ✅ | gallery | — | covered | | Seam moves | ✅ | partial | — | mention | gallery | — | needs screenshot | | Source-line ↔ segment mapping ("debugger": `pickSegment` + `segmentAtSourceLine`) | ✅ | ❌ | — | ❌ | gallery | recipes | needs demo exposure + needs screenshot | -| Show/hide a feature role (`setFeatureRoleVisible` — e.g. hide brim/skirt) | ✅ | ✅ (toggle) | — | ❌ | gallery | — | covered (demo + tests); screenshot with a brim file pending | +| Show/hide a feature role (`setFeatureRoleVisible` — e.g. hide brim/skirt) | ✅ | ✅ (toggle) | `adhesion-hidden` | ❌ | gallery | — | covered — before/after with the skirt-brim fixture in the gallery | ## 2. Coloring & analysis modes @@ -129,7 +142,7 @@ them up rather than rediscovering them. | Renderer build stages (`stage` event) | ✅ | ❌ | — | ✅ (prose) | gallery | concept-workers | needs animation | | Interaction-aware quality | ✅ | ❌ | — | ✅ (prose) | gallery | concept-workers | needs animation | | Disclosed decimation / degradation | ✅ | ✅ (disclosure) | — | ✅ (prose) | gallery | concept-workers | needs screenshot (disclosure text) | -| Render diagnostics (`getRenderStats`) | ✅ | ✅ (panel) | — | ✅ | gallery | concept-workers | demo + README + gallery covered; in-context panel screenshot pending | +| Render diagnostics (`getRenderStats`) | ✅ | ✅ (panel) | `diagnostics-panel` | ✅ | gallery | concept-workers | covered — panel-in-context screenshot in the gallery | | Geometry worker pool (parallel tubes) | ✅ | ❌ | — | mention | gallery | concept-workers | not visually meaningful (diagnostics panel proxies it) | | WebGL context-loss recovery | ✅ | ❌ | — | mention | — | — | not visually meaningful | @@ -140,7 +153,7 @@ them up rather than rediscovering them. | Camera presets (7 views) | ✅ | ✅ | `camera-top/front/iso` | ✅ | gallery | — | covered (labeled sequence) | | Perspective vs orthographic | ✅ | ✅ | (in camera trio) | ✅ | gallery | — | covered | | Saved/restored camera state | ✅ | ✅ | — | mention | — | recipes | not visually meaningful | -| Object-aware framing (`frameContent`) | ✅ | ✅ | — | ✅ (prose) | gallery | concept-ir-capabilities | needs comparison — whole-job vs model-aware | +| Object-aware framing (`frameContent`) | ✅ | ✅ | `frame-all`/`frame-object` | ✅ (prose) | gallery | concept-ir-capabilities | covered — whole-job vs model-aware comparison in the gallery | | Build-volume cage toggle | ✅ | ✅ | — | mention | gallery | — | needs screenshot | | Capture / export → Blob | ✅ | ✅ | — | ✅ | gallery | recipes | covered (demo Capture PNG + README + gallery); in-context screenshot pending | | Transparent / independent-background capture | ✅ | ✅ (via capture) | — | ✅ | gallery | recipes | demo + README covered; checkerboard screenshot pending | diff --git a/docs/manual/feature-gallery.md b/docs/manual/feature-gallery.md index 304205f7..2a14230c 100644 --- a/docs/manual/feature-gallery.md +++ b/docs/manual/feature-gallery.md @@ -31,9 +31,16 @@ The core job: turn a sliced file into an interactive picture of the actual moves order. Scrubbing is a draw-range trim, so it stays smooth even on large models.* Also here: **segment scrub** (step move-by-move), **time scrub** with an honest print-time estimate -(labeled *slicer estimate* vs *kinematic approximation*), **hide by feature role** -(`setFeatureRoleVisible` — e.g. hide brim/skirt to declutter a part preview), and **source-line ↔ -segment mapping** — click a move to find its byte in the file, and back. See [recipes](recipes.md). +(labeled *slicer estimate* vs *kinematic approximation*), and **source-line ↔ segment mapping** — +click a move to find its byte in the file, and back. See [recipes](recipes.md). + +**Hide by feature role** (`setFeatureRoleVisible`) — e.g. hide brim/skirt to declutter a part +preview, at the same framing: + +| Brim + skirt shown | Adhesion hidden | +|---|---| +| ![A model with its skirt ring and brim shown](../media/frame-all.png) | ![The same view with the skirt and brim hidden, leaving just the model](../media/adhesion-hidden.png) | +| The pink skirt ring and brim frame the part. | `setFeatureRoleVisible(Skirt/Brim, false)` leaves only the model. | ## Coloring & analysis @@ -81,6 +88,12 @@ clean `hold` reveal**, staged preparation progress, and **`getRenderStats()`** d hardware-vs-software GPU, draw calls, timings — never fabricated). See [workers, streaming & performance](concept-workers.md). +![The demo's Render diagnostics panel populated beside the render — backend 3d-webgl (WebGL 2), software ANGLE/SwiftShader GPU, geometry tubes, segment and draw counts, tube bytes, and timings](../media/diagnostics-panel.png) + +*`getRenderStats()` in context — every value read from the actual render, never fabricated; here +reporting a software (SwiftShader) backend, `108,725 / 108,729` segments, 92 draw calls, and the +build/first-frame timings.* + ## Cameras, framing & capture | | | | @@ -89,10 +102,18 @@ hardware-vs-software GPU, draw calls, timings — never fabricated). See | **Front** (ortho) | **Top** (ortho) | **Iso** (perspective) | Seven **camera presets**, orthographic/perspective, and a serializable **camera state** you can -persist and restore. **Object-aware framing** fits the printed object — excluding skirt, prime line, -and purge — instead of the whole machine volume. **`capture()`** returns the current view as a `Blob` -(PNG/JPEG/WebP), including a **transparent background** for compositing onto cards, from the -interactive viewer *or* the headless still. +persist and restore. + +**Object-aware framing** (`frameContent: 'object'`) fits the printed object — excluding skirt, prime +line, and purge — instead of the whole machine volume: + +| Whole-job framing | Object-aware framing | +|---|---| +| ![A small model framed together with its wide skirt ring, so the model sits small](../media/frame-all.png) | ![The same model framed tightly on the part, with the skirt excluded](../media/frame-object.png) | +| `'all'` fits everything, so the model is small inside its skirt. | `'object'` fits the part — the skirt/brim fall outside the frame. | + +**`capture()`** returns the current view as a `Blob` (PNG/JPEG/WebP), including a **transparent +background** for compositing onto cards, from the interactive viewer *or* the headless still. ## Models & plates — the other renderer diff --git a/docs/media/adhesion-hidden.png b/docs/media/adhesion-hidden.png new file mode 100644 index 00000000..1c042069 Binary files /dev/null and b/docs/media/adhesion-hidden.png differ diff --git a/docs/media/diagnostics-panel.png b/docs/media/diagnostics-panel.png new file mode 100644 index 00000000..2d97556d Binary files /dev/null and b/docs/media/diagnostics-panel.png differ diff --git a/docs/media/frame-all.png b/docs/media/frame-all.png new file mode 100644 index 00000000..e170b6a6 Binary files /dev/null and b/docs/media/frame-all.png differ diff --git a/docs/media/frame-object.png b/docs/media/frame-object.png new file mode 100644 index 00000000..c494e169 Binary files /dev/null and b/docs/media/frame-object.png differ diff --git a/test-data/fixtures/annotations/skirt-brim-model.gcode b/test-data/fixtures/annotations/skirt-brim-model.gcode new file mode 100644 index 00000000..746be6b8 --- /dev/null +++ b/test-data/fixtures/annotations/skirt-brim-model.gcode @@ -0,0 +1,521 @@ +; generated by PrusaSlicer 2.7.0+win64 +; MIT-clean documentation fixture: model + skirt + brim (Chestnut Labs) +; bed_shape = 0x0,250x0,250x250,0x250 +; nozzle_diameter = 0.4 +M83 ; relative extrusion +G21 +G90 +;LAYER_CHANGE +G1 Z0.200 F1200 +;TYPE:Skirt +G1 X86.000 Y86.000 F9000 +G1 X134.000 Y86.000 E2.16000 F1200 +G1 X134.000 Y134.000 E2.16000 F1200 +G1 X86.000 Y134.000 E2.16000 F1200 +G1 X86.000 Y86.000 E2.16000 F1200 +;TYPE:Brim +G1 X95.000 Y95.000 F9000 +G1 X125.000 Y95.000 E1.35000 F1200 +G1 X125.000 Y125.000 E1.35000 F1200 +G1 X95.000 Y125.000 E1.35000 F1200 +G1 X95.000 Y95.000 E1.35000 F1200 +G1 X96.000 Y96.000 F9000 +G1 X124.000 Y96.000 E1.26000 F1200 +G1 X124.000 Y124.000 E1.26000 F1200 +G1 X96.000 Y124.000 E1.26000 F1200 +G1 X96.000 Y96.000 E1.26000 F1200 +G1 X97.000 Y97.000 F9000 +G1 X123.000 Y97.000 E1.17000 F1200 +G1 X123.000 Y123.000 E1.17000 F1200 +G1 X97.000 Y123.000 E1.17000 F1200 +G1 X97.000 Y97.000 E1.17000 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z0.400 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z0.600 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z0.800 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z1.000 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z1.200 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z1.400 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z1.600 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z1.800 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z2.000 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z2.200 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z2.400 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z2.600 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +;LAYER_CHANGE +G1 Z2.800 F1200 +;TYPE:External perimeter +G1 X98.000 Y98.000 F9000 +G1 X122.000 Y98.000 E1.08000 F1800 +G1 X122.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y122.000 E1.08000 F1800 +G1 X98.000 Y98.000 E1.08000 F1800 +;TYPE:Perimeter +G1 X99.200 Y99.200 F9000 +G1 X120.800 Y99.200 E0.97200 F1800 +G1 X120.800 Y120.800 E0.97200 F1800 +G1 X99.200 Y120.800 E0.97200 F1800 +G1 X99.200 Y99.200 E0.97200 F1800 +;TYPE:Internal infill +G1 X100.400 Y100.400 F9000 +G1 X100.400 Y119.600 E0.86400 F2400 +G1 X102.400 Y119.600 F9000 +G1 X102.400 Y100.400 E0.86400 F2400 +G1 X104.400 Y100.400 F9000 +G1 X104.400 Y119.600 E0.86400 F2400 +G1 X106.400 Y119.600 F9000 +G1 X106.400 Y100.400 E0.86400 F2400 +G1 X108.400 Y100.400 F9000 +G1 X108.400 Y119.600 E0.86400 F2400 +G1 X110.400 Y119.600 F9000 +G1 X110.400 Y100.400 E0.86400 F2400 +G1 X112.400 Y100.400 F9000 +G1 X112.400 Y119.600 E0.86400 F2400 +G1 X114.400 Y119.600 F9000 +G1 X114.400 Y100.400 E0.86400 F2400 +G1 X116.400 Y100.400 F9000 +G1 X116.400 Y119.600 E0.86400 F2400 +G1 X118.400 Y119.600 F9000 +G1 X118.400 Y100.400 E0.86400 F2400 +M400 +; end diff --git a/tools/demo/src/main.js b/tools/demo/src/main.js index 7645baf3..cb5e8c0e 100644 --- a/tools/demo/src/main.js +++ b/tools/demo/src/main.js @@ -26,7 +26,8 @@ const CORPUS = [ ['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/variable-layers.gcode', 'Variable layer height (#179 demo)'], + ['fixtures/annotations/skirt-brim-model.gcode', 'Model + skirt + brim (framing / hide-adhesion)'] ]; const TOOL_PALETTE = [ diff --git a/tools/fixtures/make-skirt-brim.mjs b/tools/fixtures/make-skirt-brim.mjs new file mode 100644 index 00000000..8ea688cf --- /dev/null +++ b/tools/fixtures/make-skirt-brim.mjs @@ -0,0 +1,96 @@ +/** + * Generate a small PrusaSlicer-style G-code fixture with a MODEL wrapped by SKIRT + BRIM + * adhesion features, for documentation media: + * - object-aware framing (`frameContent` 'object' vs 'all' — the skirt sits well outside the model) + * - setFeatureRoleVisible (hide Skirt/Brim to declutter the part) + * + * PrusaSlicer header → dialect detected `known`; `;TYPE:` markers → FeatureRole classification. + * MIT-clean, deterministic, hand-generated (Chestnut Labs). Regenerate: + * node tools/fixtures/make-skirt-brim.mjs + */ +/* eslint-env node */ +import { writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; + +const CX = 110, + CY = 110; +const HALF = 12; // 24 mm square model +const LAYERS = 14; +const DZ = 0.2; +const EF = 0.045; // filament mm per mm of XY move (0.2 layer, 0.4 nozzle-ish) + +const out = []; +const p = (s) => out.push(s); +const dist = (ax, ay, bx, by) => Math.hypot(bx - ax, by - ay); + +let cx = 0, + cy = 0; +function travel(x, y) { + p(`G1 X${x.toFixed(3)} Y${y.toFixed(3)} F9000`); + cx = x; + cy = y; +} +function extrudeTo(x, y, f) { + p(`G1 X${x.toFixed(3)} Y${y.toFixed(3)} E${(dist(cx, cy, x, y) * EF).toFixed(5)} F${f}`); + cx = x; + cy = y; +} +/** A closed rectangular loop (corners are half-width `h` around the center). */ +function loop(h, f) { + travel(CX - h, CY - h); + extrudeTo(CX + h, CY - h, f); + extrudeTo(CX + h, CY + h, f); + extrudeTo(CX - h, CY + h, f); + extrudeTo(CX - h, CY - h, f); +} + +p('; generated by PrusaSlicer 2.7.0+win64'); +p('; MIT-clean documentation fixture: model + skirt + brim (Chestnut Labs)'); +p('; bed_shape = 0x0,250x0,250x250,0x250'); +p('; nozzle_diameter = 0.4'); +p('M83 ; relative extrusion'); +p('G21'); +p('G90'); + +for (let l = 0; l < LAYERS; l++) { + const z = (l + 1) * DZ; + p(`;LAYER_CHANGE`); + p(`G1 Z${z.toFixed(3)} F1200`); + + if (l === 0) { + // Skirt: one loop well OUTSIDE the model (so object-aware framing differs from whole-job). + p(';TYPE:Skirt'); + loop(HALF + 12, 1200); + // Brim: a few loops hugging the model base, just outside the object outline. + p(';TYPE:Brim'); + for (let bh = HALF + 3; bh > HALF; bh -= 1) loop(bh, 1200); + } + + // Model walls: external + one inner perimeter. + p(';TYPE:External perimeter'); + loop(HALF, 1800); + p(';TYPE:Perimeter'); + loop(HALF - 1.2, 1800); + + // Model infill: parallel diagonal-ish lines filling the interior (simple boustrophedon). + p(';TYPE:Internal infill'); + const inner = HALF - 2.4; + const step = 2.0; + let dir = 1; + for (let x = CX - inner; x <= CX + inner + 1e-6; x += step) { + const y0 = dir > 0 ? CY - inner : CY + inner; + const y1 = dir > 0 ? CY + inner : CY - inner; + travel(x, y0); + extrudeTo(x, y1, 2400); + dir *= -1; + } +} + +p('M400'); +p('; end'); + +const dir = dirname(fileURLToPath(import.meta.url)); +const dest = resolve(dir, '../../test-data/fixtures/annotations/skirt-brim-model.gcode'); +writeFileSync(dest, out.join('\n') + '\n'); +console.log(`wrote ${dest} (${out.length} lines)`); diff --git a/tools/screenshots/capture.mjs b/tools/screenshots/capture.mjs index 39424a96..51b4ae88 100644 --- a/tools/screenshots/capture.mjs +++ b/tools/screenshots/capture.mjs @@ -161,14 +161,20 @@ async function driveToolpath(page, shot) { }; r.setProgress(m.observe(obs)); } + if (shot.hideRoles) for (const role of shot.hideRoles) r.setFeatureRoleVisible(role, false); r.frame(); if (shot.view) { r.setView(shot.view); } else if (!shot.camera) { // Consistent slightly-lower ~22° 3/4 elevation across perspective shots. - // CNC/laser cut moves aren't extrusion → extrude-only bounds can be empty; - // fall back to travel-inclusive bounds so the toolpath frames. - const b = Number.isFinite(r.ir.bounds.min.x) ? r.ir.bounds : r.ir.boundsWithTravel; + // frameContent:'object' frames the printed model (excludes skirt/prime); 'all'/default frames + // the whole job. CNC cut moves aren't extrusion → extrude-only bounds can be empty; fall back. + const useModel = shot.frameContent === 'object' && Number.isFinite(r.ir.modelBounds?.min?.x); + const b = useModel + ? r.ir.modelBounds + : Number.isFinite(r.ir.bounds.min.x) + ? r.ir.bounds + : r.ir.boundsWithTravel; const cx = (b.min.x + b.max.x) / 2, cy = (b.min.y + b.max.y) / 2, cz = (b.min.z + b.max.z) / 2; @@ -221,6 +227,56 @@ async function driveToolpath(page, shot) { } const out = resolve(MEDIA, shot.name + '.png'); + if (shot.recipe === 'transparent-checker') { + // Prove renderer.capture({ background: 'transparent' }) yields real alpha: composite the captured + // PNG over a checkerboard so the see-through areas are obvious (the card-compositing use case). + // NOTE: no manifest shot uses this today — headless SwiftShader flattens the render-target readback + // alpha to opaque, so the composite would be misleading here. Transparent capture is validated on + // real hardware (AnyBridge RTX 4070); run this recipe on a GPU-backed browser to regenerate the shot. + const dataUrl = await page.evaluate(async () => { + const r = window.viewer.renderer; + const blob = await r.capture({ background: 'transparent', format: 'png' }); + const bmp = await createImageBitmap(blob); + const c = document.createElement('canvas'); + c.width = bmp.width; + c.height = bmp.height; + const ctx = c.getContext('2d'); + const s = 28; + for (let y = 0; y < c.height; y += s) + for (let x = 0; x < c.width; x += s) { + ctx.fillStyle = ((x / s + y / s) % 2 === 0 ? 1 : 0) ? '#dcdee1' : '#bfc2c6'; + ctx.fillRect(x, y, s, s); + } + ctx.drawImage(bmp, 0, 0); + return c.toDataURL('image/png'); + }); + savePngDataUrl(out, dataUrl); + return; + } + if (shot.recipe === 'diagnostics') { + // Fill the render-diagnostics panel (getRenderStats), scroll it into view in the (internally + // scrolling) sidebar — the render stays on the right — freeze the GL frame, then a viewport shot. + await page.evaluate(() => { + const btn = document.getElementById('showStats'); + btn.disabled = false; + btn.click(); + btn.closest('fieldset')?.scrollIntoView({ block: 'center' }); + const src = document.getElementById('view'); + window.viewer.renderer.render(); + const c = document.createElement('canvas'); + c.width = src.width; + c.height = src.height; + c.getContext('2d').drawImage(src, 0, 0); + const img = document.createElement('img'); + img.src = c.toDataURL('image/png'); + img.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;'; + src.parentElement.appendChild(img); + src.style.visibility = 'hidden'; + }); + await page.waitForTimeout(300); + await page.screenshot({ path: out, timeout: 30000 }); + return; + } if (shot.fullPage) { // page.screenshot stalls on a live WebGL surface under headless SwiftShader. // Freeze the GL frame into a static overlay, then screenshot the DOM. diff --git a/tools/screenshots/shots.manifest.json b/tools/screenshots/shots.manifest.json index b33c28cf..1fb208c0 100644 --- a/tools/screenshots/shots.manifest.json +++ b/tools/screenshots/shots.manifest.json @@ -226,6 +226,52 @@ "anim": { "kind": "layers", "frames": 44, "fps": 16, "holdFrames": 10 }, "caption": "Layer-range sweep: raising the top layer builds the model up bottom to top — the progressive reveal, shown as motion.", "alt": "Animation of 3DBenchy building up layer by layer from the base to the top as the layer range grows" + }, + { + "name": "frame-all", + "page": "toolpath", + "class": "feature-proof", + "file": "fixtures/annotations/skirt-brim-model.gcode", + "quality": "tubes", + "color": { "mode": "feature", "fallback": [0.55, 0.55, 0.55] }, + "frameContent": "all", + "caption": "Whole-job framing: the camera fits everything, so the model sits small inside its skirt.", + "alt": "A small square model framed together with its wide skirt ring, so the model is small in view" + }, + { + "name": "frame-object", + "page": "toolpath", + "class": "feature-proof", + "file": "fixtures/annotations/skirt-brim-model.gcode", + "quality": "tubes", + "color": { "mode": "feature", "fallback": [0.55, 0.55, 0.55] }, + "frameContent": "object", + "caption": "Object-aware framing: frameContent:'object' fits the printed model, excluding the skirt/brim.", + "alt": "The same model framed tightly on the part alone, with the skirt excluded so the model fills the view" + }, + { + "name": "adhesion-hidden", + "page": "toolpath", + "class": "feature-proof", + "file": "fixtures/annotations/skirt-brim-model.gcode", + "quality": "tubes", + "color": { "mode": "feature", "fallback": [0.55, 0.55, 0.55] }, + "frameContent": "all", + "hideRoles": [6, 7], + "caption": "setFeatureRoleVisible(Skirt/Brim, false): the adhesion ring is hidden, leaving just the part.", + "alt": "The same whole-job view with the skirt and brim hidden, leaving only the model" + }, + { + "name": "diagnostics-panel", + "page": "toolpath", + "class": "feature-proof", + "file": "gcodes/3DBenchy.gcode", + "quality": "tubes", + "color": { "mode": "feature", "fallback": [0.55, 0.55, 0.55] }, + "view": "iso", + "recipe": "diagnostics", + "caption": "getRenderStats() in context: the demo's Render diagnostics panel reads backend, hardware-vs-software GPU, geometry mode, counts, and timings beside the live render.", + "alt": "The demo app with the Render diagnostics panel populated — backend, GPU capability, geometry mode, segment and draw counts, and timings — next to the rendered 3DBenchy" } ] }