@chestnutlabs/gcode-renderer-2d 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/layer-view.d.ts +198 -0
- package/dist/layer-view.d.ts.map +1 -0
- package/dist/layer-view.js +354 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @chestnutlabs/gcode-renderer-2d
|
|
2
|
+
|
|
3
|
+
Low-resource **Canvas 2D layer renderer** for the [Chestnut Labs G-code Preview](https://github.com/ChestnutLabs/gcode-preview) toolpath stack (DD-014 / Epic E8).
|
|
4
|
+
|
|
5
|
+
An **opt-in** current/adjacent-layer 2D view over the existing `ToolpathIR`, for contexts where the
|
|
6
|
+
default Three.js renderer is a poor fit: old / low-end mobile, embedded printer touchscreens
|
|
7
|
+
(Fluidd/Mainsail-class UIs), and locked-down or WebGL-blocked browsers. One parse, one IR, a second
|
|
8
|
+
lightweight view — **no second parser path, no `three`, no framework**. Per-segment color is
|
|
9
|
+
single-sourced with the 3D renderer via `@chestnutlabs/gcode-colors`, so the two never disagree.
|
|
10
|
+
|
|
11
|
+
The memory ceiling is the point: drawing is bounded to the active (± adjacent, later phases) layer,
|
|
12
|
+
never the whole model.
|
|
13
|
+
|
|
14
|
+
> **Status:** E8 phases 1–3 done. Phase 1 (#212): the Canvas 2D core — draw one layer from the IR,
|
|
15
|
+
> honest color, fit-to-canvas. Phase 2 (#213): current-layer **+ adjacent "ghost" layers** (dimmed
|
|
16
|
+
> preceding layers, `adjacentLayers` default 1 / floor 0) over one stable model frame. Phase 3 (#214):
|
|
17
|
+
> selectable from the viewer via **`<GcodePreview renderer="2d" />`** (the 3D renderer is loaded on
|
|
18
|
+
> demand, so a 2D-only bundle never ships Three.js), plus a **live-progress "completed cut"**
|
|
19
|
+
> (`setProgress`). Still ahead: the low-resource benchmark + capability-honesty UX (#215).
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { LayerView2D } from '@chestnutlabs/gcode-renderer-2d';
|
|
25
|
+
|
|
26
|
+
const view = new LayerView2D(canvas, { colorMode: { mode: 'feature', palette, fallback: [0.7, 0.7, 0.7] } });
|
|
27
|
+
view.setToolpath(ir); // a ToolpathIR from the worker parser
|
|
28
|
+
view.setLayer(12);
|
|
29
|
+
view.render(); // clears + redraws the active layer
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The drawing core is also exposed as pure functions — `drawLayer(ctx, ir, opts)`, `computeLayerFit`,
|
|
33
|
+
`layerBounds2D`, `rgbToCss` — which take a minimal Canvas-2D-context interface and are fully
|
|
34
|
+
testable in Node with a recording mock.
|
|
35
|
+
|
|
36
|
+
## Honesty
|
|
37
|
+
|
|
38
|
+
A flat per-layer 2D view cannot represent non-XY/CNC toolpaths or 3D-only options. A missing or
|
|
39
|
+
out-of-range layer draws nothing and reports `drawn: false` rather than fabricating geometry.
|
|
40
|
+
Color modes that read an optional IR channel (`feature`, `object`, `feedrate`) must be
|
|
41
|
+
capability-gated by the caller. The full capability-honesty UX for non-XY / `layers: unavailable`
|
|
42
|
+
inputs arrives with the phase-4 work (#215).
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT © Chestnut Labs. See the [repository](https://github.com/ChestnutLabs/gcode-preview) for provenance and the full support policy.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @chestnutlabs/gcode-renderer-2d — low-resource Canvas 2D layer renderer (DD-014 / E8).
|
|
3
|
+
*
|
|
4
|
+
* An opt-in current/adjacent-layer 2D view over the existing `ToolpathIR`, for low-GPU,
|
|
5
|
+
* low-memory, or WebGL-blocked devices (old mobile, embedded printer UIs, locked-down browsers).
|
|
6
|
+
* One parse, one IR, a second lightweight view — no second parser path, no `three`, no framework.
|
|
7
|
+
* Per-segment color is single-sourced with the 3D renderer via `@chestnutlabs/gcode-colors`.
|
|
8
|
+
*
|
|
9
|
+
* Re-exports the color model so 2D consumers get `ColorMode`/`RGB` from one import.
|
|
10
|
+
*/
|
|
11
|
+
export { LayerView2D, drawLayer, drawLayers, computeLayerFit, layerBounds2D, modelBounds2D, describe2DDisclosures, rgbToCss } from './layer-view.js';
|
|
12
|
+
export type { LayerView2DOptions, DrawLayerOptions, DrawLayerResult, DrawLayersOptions, DrawLayersResult, LayerProgress, LayerFit, LayerBounds2D, TravelStyle, CanvasContext2DLike } from './layer-view.js';
|
|
13
|
+
export type { ColorMode, RGB } from '@chestnutlabs/gcode-colors';
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EACL,WAAW,EACX,SAAS,EACT,UAAU,EACV,eAAe,EACf,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,QAAQ,EACT,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,QAAQ,EACR,aAAa,EACb,WAAW,EACX,mBAAmB,EACpB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,4BAA4B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @chestnutlabs/gcode-renderer-2d — low-resource Canvas 2D layer renderer (DD-014 / E8).
|
|
3
|
+
*
|
|
4
|
+
* An opt-in current/adjacent-layer 2D view over the existing `ToolpathIR`, for low-GPU,
|
|
5
|
+
* low-memory, or WebGL-blocked devices (old mobile, embedded printer UIs, locked-down browsers).
|
|
6
|
+
* One parse, one IR, a second lightweight view — no second parser path, no `three`, no framework.
|
|
7
|
+
* Per-segment color is single-sourced with the 3D renderer via `@chestnutlabs/gcode-colors`.
|
|
8
|
+
*
|
|
9
|
+
* Re-exports the color model so 2D consumers get `ColorMode`/`RGB` from one import.
|
|
10
|
+
*/
|
|
11
|
+
export { LayerView2D, drawLayer, drawLayers, computeLayerFit, layerBounds2D, modelBounds2D, describe2DDisclosures, rgbToCss } from './layer-view.js';
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-resource Canvas 2D layer view (DD-014 / E8, phase 1, #212).
|
|
3
|
+
*
|
|
4
|
+
* Draws a single layer of a `ToolpathIR` as flat, top-down polylines to a Canvas 2D
|
|
5
|
+
* surface — the memory ceiling is one layer's segments, not the whole model (DD-014 §6/§7).
|
|
6
|
+
* Geometry is read straight from the SoA in the IR's origin-relative frame; per-segment
|
|
7
|
+
* color comes from the shared, renderer-agnostic `@chestnutlabs/gcode-colors` so 2D and 3D
|
|
8
|
+
* never disagree. No `three`, no WebGL.
|
|
9
|
+
*
|
|
10
|
+
* The drawing core ({@link drawLayer}, {@link computeLayerFit}) is pure and takes a minimal
|
|
11
|
+
* {@link CanvasContext2DLike}, so it is fully testable in Node with a recording mock; the
|
|
12
|
+
* {@link LayerView2D} class is a thin lifecycle wrapper over a real `<canvas>`.
|
|
13
|
+
*/
|
|
14
|
+
import { type ToolpathIR } from '@chestnutlabs/toolpath-core';
|
|
15
|
+
import { type ColorMode, type RGB } from '@chestnutlabs/gcode-colors';
|
|
16
|
+
/** The subset of `CanvasRenderingContext2D` the layer view uses (a real context satisfies it). */
|
|
17
|
+
export type CanvasContext2DLike = Pick<CanvasRenderingContext2D, 'strokeStyle' | 'lineWidth' | 'lineCap' | 'lineJoin' | 'globalAlpha' | 'beginPath' | 'moveTo' | 'lineTo' | 'stroke' | 'clearRect'>;
|
|
18
|
+
/** XY extent of a set of segments, in the IR's origin-relative frame. */
|
|
19
|
+
export interface LayerBounds2D {
|
|
20
|
+
minX: number;
|
|
21
|
+
minY: number;
|
|
22
|
+
maxX: number;
|
|
23
|
+
maxY: number;
|
|
24
|
+
/** False when the layer has no segments (nothing to fit / draw). */
|
|
25
|
+
hasContent: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** Maps printer XY (origin-relative) to canvas pixels, with Y flipped (printer up → canvas down). */
|
|
28
|
+
export interface LayerFit {
|
|
29
|
+
/** World-units-to-pixels scale. */
|
|
30
|
+
scale: number;
|
|
31
|
+
/** Project a printer XY point to device pixels `[px, py]`. */
|
|
32
|
+
project(x: number, y: number): [number, number];
|
|
33
|
+
}
|
|
34
|
+
export interface DrawLayerOptions {
|
|
35
|
+
/** Index into `ir.layers`. Out-of-range or absent → nothing drawn (honest no-op). */
|
|
36
|
+
layer: number;
|
|
37
|
+
/** Per-segment color mode (shared with the 3D renderer). Applied to extrusion moves. */
|
|
38
|
+
colorMode: ColorMode;
|
|
39
|
+
/** World→pixel mapping. Usually from {@link computeLayerFit} for the same layer. */
|
|
40
|
+
fit: LayerFit;
|
|
41
|
+
/** Extrusion stroke width in device px (default 1). */
|
|
42
|
+
lineWidth?: number;
|
|
43
|
+
/** Draw travel moves too, in this style. Omitted/false → extrusion only (the default). */
|
|
44
|
+
travel?: TravelStyle | false;
|
|
45
|
+
/** Layer opacity in [0,1] via `globalAlpha` (default 1). Used to dim adjacent "ghost" layers. */
|
|
46
|
+
opacity?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Live-progress "completed cut" (DD-006): the last printed segment index in THIS layer. Extrusion
|
|
49
|
+
* segments at or before it draw at full opacity; those after it draw at {@link upcomingOpacity}
|
|
50
|
+
* (not yet printed). Omit for no progress split. Honest — nothing is hidden, only dimmed.
|
|
51
|
+
*/
|
|
52
|
+
progressCutSeg?: number;
|
|
53
|
+
/** Opacity of not-yet-printed extrusion when {@link progressCutSeg} is set (default 0.15). */
|
|
54
|
+
upcomingOpacity?: number;
|
|
55
|
+
}
|
|
56
|
+
export interface TravelStyle {
|
|
57
|
+
/** CSS color for travel moves (e.g. a muted gray). */
|
|
58
|
+
color: string;
|
|
59
|
+
/** Travel stroke width in device px (default 0.5). */
|
|
60
|
+
lineWidth?: number;
|
|
61
|
+
}
|
|
62
|
+
export interface DrawLayerResult {
|
|
63
|
+
layer: number;
|
|
64
|
+
layerCount: number;
|
|
65
|
+
extrudeDrawn: number;
|
|
66
|
+
travelDrawn: number;
|
|
67
|
+
/** False when the layer index is out of range or the IR has no layer table. */
|
|
68
|
+
drawn: boolean;
|
|
69
|
+
}
|
|
70
|
+
/** Convert a float {@link RGB} triple to a CSS `rgb(...)` string for `strokeStyle`. */
|
|
71
|
+
export declare function rgbToCss(rgb: RGB): string;
|
|
72
|
+
/** XY bounds of one layer's segments (both endpoints), in the origin-relative frame. */
|
|
73
|
+
export declare function layerBounds2D(ir: ToolpathIR, layer: number): LayerBounds2D;
|
|
74
|
+
/**
|
|
75
|
+
* Fit `bounds` into a `width`×`height` canvas with uniform scale, centered, `padding` px inset,
|
|
76
|
+
* and the Y axis flipped so higher printer-Y draws higher on screen. Degenerate (zero-span or
|
|
77
|
+
* empty) bounds fall back to scale 1 centered at the canvas middle.
|
|
78
|
+
*/
|
|
79
|
+
export declare function computeLayerFit(bounds: LayerBounds2D, width: number, height: number, padding?: number): LayerFit;
|
|
80
|
+
/**
|
|
81
|
+
* Draw one layer of `ir` to `ctx`. Extrusion moves are colored via {@link DrawLayerOptions.colorMode}
|
|
82
|
+
* (single-sourced with the 3D renderer); travel moves are drawn only when `travel` is given. The
|
|
83
|
+
* caller is responsible for clearing the canvas and for capability-gating the color mode. Returns an
|
|
84
|
+
* honest count; a missing/out-of-range layer draws nothing and reports `drawn: false`.
|
|
85
|
+
*/
|
|
86
|
+
export declare function drawLayer(ctx: CanvasContext2DLike, ir: ToolpathIR, opts: DrawLayerOptions): DrawLayerResult;
|
|
87
|
+
/** XY bounds over ALL of the IR's segments — a stable frame for the layer view (not per-layer). */
|
|
88
|
+
export declare function modelBounds2D(ir: ToolpathIR): LayerBounds2D;
|
|
89
|
+
/**
|
|
90
|
+
* Honest capability disclosures for rendering `ir` as a flat 2D layer view (DD-014 §6/§11). Returns
|
|
91
|
+
* human-readable notes a consumer UI can show; empty when the IR is faithfully representable (planar
|
|
92
|
+
* FDM). It never suppresses geometry — it explains what the flat top-down projection cannot convey.
|
|
93
|
+
*
|
|
94
|
+
* - `layers: 'unavailable'` (non-planar / CNC / unindexed): the parser put every move on layer 0, so
|
|
95
|
+
* the 2D view shows them all in one flat frame — Z variation and non-XY (e.g. `G18`/`G19`) motion
|
|
96
|
+
* are not represented.
|
|
97
|
+
* - `layers: 'inferred'`: layer boundaries are approximate.
|
|
98
|
+
*/
|
|
99
|
+
export declare function describe2DDisclosures(ir: ToolpathIR): string[];
|
|
100
|
+
export interface DrawLayersOptions {
|
|
101
|
+
/** Active (fully-drawn) layer index. */
|
|
102
|
+
layer: number;
|
|
103
|
+
/** Number of preceding layers drawn beneath the active one as dimmed "ghosts" (default 1, floor 0). */
|
|
104
|
+
adjacentLayers?: number;
|
|
105
|
+
/** Per-segment color mode for all drawn layers (shared with the 3D renderer). */
|
|
106
|
+
colorMode: ColorMode;
|
|
107
|
+
/** Shared world→pixel mapping — the SAME fit for every layer so they overlay in printer XY. */
|
|
108
|
+
fit: LayerFit;
|
|
109
|
+
/** Extrusion stroke width in device px (default 1). */
|
|
110
|
+
lineWidth?: number;
|
|
111
|
+
/** Draw travel moves too (default hidden). Applied to the active layer only. */
|
|
112
|
+
travel?: TravelStyle | false;
|
|
113
|
+
/** Opacity of each ghost layer in [0,1] (default 0.25). The active layer is always fully opaque. */
|
|
114
|
+
ghostOpacity?: number;
|
|
115
|
+
/** Live-progress cut (DD-006). Applied only when its `layerIndex` is the active layer. */
|
|
116
|
+
progress?: LayerProgress | null;
|
|
117
|
+
}
|
|
118
|
+
/** The minimal live-progress shape the 2D view needs (a projection of DD-006's `MappedProgress`). */
|
|
119
|
+
export interface LayerProgress {
|
|
120
|
+
/** Last printed segment index (global IR index); null when unknown. */
|
|
121
|
+
segIndex: number | null;
|
|
122
|
+
/** Layer currently printing; null when unknown. */
|
|
123
|
+
layerIndex: number | null;
|
|
124
|
+
}
|
|
125
|
+
export interface DrawLayersResult {
|
|
126
|
+
layer: number;
|
|
127
|
+
layerCount: number;
|
|
128
|
+
/** Ghost layer indices actually drawn (oldest→newest, beneath the active layer). */
|
|
129
|
+
ghostLayers: number[];
|
|
130
|
+
/** Extrusion segments drawn across the active layer + ghosts. */
|
|
131
|
+
extrudeDrawn: number;
|
|
132
|
+
travelDrawn: number;
|
|
133
|
+
drawn: boolean;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Draw the active layer over its preceding "ghost" layers (DD-014 D2, #213). Ghosts are the
|
|
137
|
+
* `adjacentLayers` layers immediately below the active one, drawn first (underneath) and dimmed to
|
|
138
|
+
* `ghostOpacity`; the active layer is drawn last at full opacity. All layers share ONE `fit`, so
|
|
139
|
+
* they overlay correctly in printer XY. `adjacentLayers: 0` draws the active layer only (the floor).
|
|
140
|
+
*/
|
|
141
|
+
export declare function drawLayers(ctx: CanvasContext2DLike, ir: ToolpathIR, opts: DrawLayersOptions): DrawLayersResult;
|
|
142
|
+
export interface LayerView2DOptions {
|
|
143
|
+
/** Initial layer index (default 0). */
|
|
144
|
+
layer?: number;
|
|
145
|
+
/** Initial color mode (default single mid-gray). */
|
|
146
|
+
colorMode?: ColorMode;
|
|
147
|
+
/** Extrusion stroke width in device px (default 1). */
|
|
148
|
+
lineWidth?: number;
|
|
149
|
+
/** Travel style, or false to hide travel (default: hidden). */
|
|
150
|
+
travel?: TravelStyle | false;
|
|
151
|
+
/** Padding inset in device px (default 8). */
|
|
152
|
+
padding?: number;
|
|
153
|
+
/** Preceding "ghost" layers drawn beneath the active one (default 1, floor 0). */
|
|
154
|
+
adjacentLayers?: number;
|
|
155
|
+
/** Ghost-layer opacity in [0,1] (default 0.25). */
|
|
156
|
+
ghostOpacity?: number;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* A thin lifecycle wrapper binding a `ToolpathIR` to a `<canvas>`: pick a layer, pick a color
|
|
160
|
+
* mode, `render()`. It owns no WebGL context and holds no per-layer geometry — each `render()`
|
|
161
|
+
* clears and redraws the active layer (plus its dimmed "ghost" layers), so peak memory stays near
|
|
162
|
+
* one layer (DD-014 §6). The view frame is the whole-model XY bounds (cached on `setToolpath`), so
|
|
163
|
+
* scrubbing layers doesn't jump or rescale and ghosts overlay the active layer exactly. Adapter
|
|
164
|
+
* wiring (the `renderer: '2d' | '3d'` prop) arrives in a later E8 phase.
|
|
165
|
+
*/
|
|
166
|
+
export declare class LayerView2D {
|
|
167
|
+
private readonly canvas;
|
|
168
|
+
private ir;
|
|
169
|
+
private bounds;
|
|
170
|
+
private layer;
|
|
171
|
+
private colorMode;
|
|
172
|
+
private lineWidth;
|
|
173
|
+
private travel;
|
|
174
|
+
private padding;
|
|
175
|
+
private adjacentLayers;
|
|
176
|
+
private ghostOpacity;
|
|
177
|
+
private progress;
|
|
178
|
+
constructor(canvas: HTMLCanvasElement, opts?: LayerView2DOptions);
|
|
179
|
+
setToolpath(ir: ToolpathIR | null): void;
|
|
180
|
+
setLayer(layer: number): void;
|
|
181
|
+
setColorMode(colorMode: ColorMode): void;
|
|
182
|
+
/** Number of preceding ghost layers drawn beneath the active one (clamped to ≥ 0). */
|
|
183
|
+
setAdjacentLayers(n: number): void;
|
|
184
|
+
/** Live-progress cut (DD-006); null clears it. Applied only when on the active layer. */
|
|
185
|
+
setProgress(progress: LayerProgress | null): void;
|
|
186
|
+
/** Show/hide travel moves on the active layer (`false` hides). */
|
|
187
|
+
setTravel(travel: TravelStyle | false): void;
|
|
188
|
+
/**
|
|
189
|
+
* Honest capability notes for the current IR (DD-014 §6/§11) — e.g. a non-planar/CNC toolpath the
|
|
190
|
+
* flat 2D view cannot fully represent. Empty for faithfully-representable (planar) input.
|
|
191
|
+
*/
|
|
192
|
+
getDisclosures(): string[];
|
|
193
|
+
/** Number of layers in the current IR (0 when none / no layer table). */
|
|
194
|
+
get layerCount(): number;
|
|
195
|
+
/** Clear and redraw the active layer + ghosts. Returns the draw result (drawn:false when empty). */
|
|
196
|
+
render(): DrawLayersResult;
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=layer-view.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layer-view.d.ts","sourceRoot":"","sources":["../src/layer-view.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-resource Canvas 2D layer view (DD-014 / E8, phase 1, #212).
|
|
3
|
+
*
|
|
4
|
+
* Draws a single layer of a `ToolpathIR` as flat, top-down polylines to a Canvas 2D
|
|
5
|
+
* surface — the memory ceiling is one layer's segments, not the whole model (DD-014 §6/§7).
|
|
6
|
+
* Geometry is read straight from the SoA in the IR's origin-relative frame; per-segment
|
|
7
|
+
* color comes from the shared, renderer-agnostic `@chestnutlabs/gcode-colors` so 2D and 3D
|
|
8
|
+
* never disagree. No `three`, no WebGL.
|
|
9
|
+
*
|
|
10
|
+
* The drawing core ({@link drawLayer}, {@link computeLayerFit}) is pure and takes a minimal
|
|
11
|
+
* {@link CanvasContext2DLike}, so it is fully testable in Node with a recording mock; the
|
|
12
|
+
* {@link LayerView2D} class is a thin lifecycle wrapper over a real `<canvas>`.
|
|
13
|
+
*/
|
|
14
|
+
import { MoveKind } from '@chestnutlabs/toolpath-core';
|
|
15
|
+
import { createSegmentColorer } from '@chestnutlabs/gcode-colors';
|
|
16
|
+
/** Clamp a 0..1 float channel and scale to an 8-bit int. */
|
|
17
|
+
function chan(v) {
|
|
18
|
+
const c = v < 0 ? 0 : v > 1 ? 1 : v;
|
|
19
|
+
return Math.round(c * 255);
|
|
20
|
+
}
|
|
21
|
+
/** Convert a float {@link RGB} triple to a CSS `rgb(...)` string for `strokeStyle`. */
|
|
22
|
+
export function rgbToCss(rgb) {
|
|
23
|
+
return `rgb(${chan(rgb[0])}, ${chan(rgb[1])}, ${chan(rgb[2])})`;
|
|
24
|
+
}
|
|
25
|
+
/** XY bounds of one layer's segments (both endpoints), in the origin-relative frame. */
|
|
26
|
+
export function layerBounds2D(ir, layer) {
|
|
27
|
+
const empty = { minX: 0, minY: 0, maxX: 0, maxY: 0, hasContent: false };
|
|
28
|
+
const l = ir.layers[layer];
|
|
29
|
+
if (!l)
|
|
30
|
+
return empty;
|
|
31
|
+
const seg = ir.segments;
|
|
32
|
+
let minX = Infinity;
|
|
33
|
+
let minY = Infinity;
|
|
34
|
+
let maxX = -Infinity;
|
|
35
|
+
let maxY = -Infinity;
|
|
36
|
+
for (let i = l.segStart; i <= l.segEnd && i < seg.count; i++) {
|
|
37
|
+
const xs = [seg.x0[i], seg.x1[i]];
|
|
38
|
+
const ys = [seg.y0[i], seg.y1[i]];
|
|
39
|
+
for (const x of xs) {
|
|
40
|
+
if (x < minX)
|
|
41
|
+
minX = x;
|
|
42
|
+
if (x > maxX)
|
|
43
|
+
maxX = x;
|
|
44
|
+
}
|
|
45
|
+
for (const y of ys) {
|
|
46
|
+
if (y < minY)
|
|
47
|
+
minY = y;
|
|
48
|
+
if (y > maxY)
|
|
49
|
+
maxY = y;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (minX > maxX)
|
|
53
|
+
return empty;
|
|
54
|
+
return { minX, minY, maxX, maxY, hasContent: true };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Fit `bounds` into a `width`×`height` canvas with uniform scale, centered, `padding` px inset,
|
|
58
|
+
* and the Y axis flipped so higher printer-Y draws higher on screen. Degenerate (zero-span or
|
|
59
|
+
* empty) bounds fall back to scale 1 centered at the canvas middle.
|
|
60
|
+
*/
|
|
61
|
+
export function computeLayerFit(bounds, width, height, padding = 8) {
|
|
62
|
+
const availW = Math.max(1, width - 2 * padding);
|
|
63
|
+
const availH = Math.max(1, height - 2 * padding);
|
|
64
|
+
const spanX = bounds.maxX - bounds.minX;
|
|
65
|
+
const spanY = bounds.maxY - bounds.minY;
|
|
66
|
+
const scale = !bounds.hasContent || (spanX <= 0 && spanY <= 0)
|
|
67
|
+
? 1
|
|
68
|
+
: Math.min(spanX > 0 ? availW / spanX : Infinity, spanY > 0 ? availH / spanY : Infinity);
|
|
69
|
+
const drawnW = spanX * scale;
|
|
70
|
+
const drawnH = spanY * scale;
|
|
71
|
+
const offsetX = padding + (availW - drawnW) / 2;
|
|
72
|
+
const offsetYTop = padding + (availH - drawnH) / 2;
|
|
73
|
+
const { minX, minY } = bounds;
|
|
74
|
+
return {
|
|
75
|
+
scale,
|
|
76
|
+
project(x, y) {
|
|
77
|
+
const px = offsetX + (x - minX) * scale;
|
|
78
|
+
// Flip Y: the top of the drawn box is offsetYTop; printer-Y grows upward.
|
|
79
|
+
const py = offsetYTop + (drawnH - (y - minY) * scale);
|
|
80
|
+
return [px, py];
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Draw one layer of `ir` to `ctx`. Extrusion moves are colored via {@link DrawLayerOptions.colorMode}
|
|
86
|
+
* (single-sourced with the 3D renderer); travel moves are drawn only when `travel` is given. The
|
|
87
|
+
* caller is responsible for clearing the canvas and for capability-gating the color mode. Returns an
|
|
88
|
+
* honest count; a missing/out-of-range layer draws nothing and reports `drawn: false`.
|
|
89
|
+
*/
|
|
90
|
+
export function drawLayer(ctx, ir, opts) {
|
|
91
|
+
const layerCount = ir.layers.length;
|
|
92
|
+
const l = ir.layers[opts.layer];
|
|
93
|
+
const base = {
|
|
94
|
+
layer: opts.layer,
|
|
95
|
+
layerCount,
|
|
96
|
+
extrudeDrawn: 0,
|
|
97
|
+
travelDrawn: 0,
|
|
98
|
+
drawn: false
|
|
99
|
+
};
|
|
100
|
+
if (!l)
|
|
101
|
+
return base;
|
|
102
|
+
const seg = ir.segments;
|
|
103
|
+
const colorOf = createSegmentColorer(ir, opts.colorMode);
|
|
104
|
+
const extrudeWidth = opts.lineWidth ?? 1;
|
|
105
|
+
const showTravel = opts.travel ? opts.travel : null;
|
|
106
|
+
ctx.lineCap = 'round';
|
|
107
|
+
ctx.lineJoin = 'round';
|
|
108
|
+
const prevAlpha = ctx.globalAlpha;
|
|
109
|
+
ctx.globalAlpha = opts.opacity ?? 1;
|
|
110
|
+
// Travel first (underneath), if requested.
|
|
111
|
+
let travelDrawn = 0;
|
|
112
|
+
if (showTravel) {
|
|
113
|
+
ctx.strokeStyle = showTravel.color;
|
|
114
|
+
ctx.lineWidth = showTravel.lineWidth ?? 0.5;
|
|
115
|
+
for (let i = l.segStart; i <= l.segEnd && i < seg.count; i++) {
|
|
116
|
+
if ((seg.kind[i] & MoveKind.Travel) === 0)
|
|
117
|
+
continue;
|
|
118
|
+
const [ax, ay] = opts.fit.project(seg.x0[i], seg.y0[i]);
|
|
119
|
+
const [bx, by] = opts.fit.project(seg.x1[i], seg.y1[i]);
|
|
120
|
+
ctx.beginPath();
|
|
121
|
+
ctx.moveTo(ax, ay);
|
|
122
|
+
ctx.lineTo(bx, by);
|
|
123
|
+
ctx.stroke();
|
|
124
|
+
travelDrawn++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Extrusion, colored per-segment. With a progress cut, segments after `progressCutSeg` in this
|
|
128
|
+
// layer are the not-yet-printed part — drawn dimmed (never hidden), the 2D "completed cut" (DD-006).
|
|
129
|
+
ctx.lineWidth = extrudeWidth;
|
|
130
|
+
const baseOpacity = opts.opacity ?? 1;
|
|
131
|
+
const upcomingOpacity = baseOpacity * (opts.upcomingOpacity ?? 0.15);
|
|
132
|
+
const cut = opts.progressCutSeg;
|
|
133
|
+
let extrudeDrawn = 0;
|
|
134
|
+
for (let i = l.segStart; i <= l.segEnd && i < seg.count; i++) {
|
|
135
|
+
if ((seg.kind[i] & MoveKind.Extrude) === 0)
|
|
136
|
+
continue;
|
|
137
|
+
ctx.globalAlpha = cut !== undefined && i > cut ? upcomingOpacity : baseOpacity;
|
|
138
|
+
ctx.strokeStyle = rgbToCss(colorOf(i));
|
|
139
|
+
const [ax, ay] = opts.fit.project(seg.x0[i], seg.y0[i]);
|
|
140
|
+
const [bx, by] = opts.fit.project(seg.x1[i], seg.y1[i]);
|
|
141
|
+
ctx.beginPath();
|
|
142
|
+
ctx.moveTo(ax, ay);
|
|
143
|
+
ctx.lineTo(bx, by);
|
|
144
|
+
ctx.stroke();
|
|
145
|
+
extrudeDrawn++;
|
|
146
|
+
}
|
|
147
|
+
ctx.globalAlpha = prevAlpha;
|
|
148
|
+
return { layer: opts.layer, layerCount, extrudeDrawn, travelDrawn, drawn: true };
|
|
149
|
+
}
|
|
150
|
+
/** XY bounds over ALL of the IR's segments — a stable frame for the layer view (not per-layer). */
|
|
151
|
+
export function modelBounds2D(ir) {
|
|
152
|
+
const seg = ir.segments;
|
|
153
|
+
let minX = Infinity;
|
|
154
|
+
let minY = Infinity;
|
|
155
|
+
let maxX = -Infinity;
|
|
156
|
+
let maxY = -Infinity;
|
|
157
|
+
for (let i = 0; i < seg.count; i++) {
|
|
158
|
+
const x0 = seg.x0[i];
|
|
159
|
+
const x1 = seg.x1[i];
|
|
160
|
+
const y0 = seg.y0[i];
|
|
161
|
+
const y1 = seg.y1[i];
|
|
162
|
+
if (x0 < minX)
|
|
163
|
+
minX = x0;
|
|
164
|
+
if (x1 < minX)
|
|
165
|
+
minX = x1;
|
|
166
|
+
if (x0 > maxX)
|
|
167
|
+
maxX = x0;
|
|
168
|
+
if (x1 > maxX)
|
|
169
|
+
maxX = x1;
|
|
170
|
+
if (y0 < minY)
|
|
171
|
+
minY = y0;
|
|
172
|
+
if (y1 < minY)
|
|
173
|
+
minY = y1;
|
|
174
|
+
if (y0 > maxY)
|
|
175
|
+
maxY = y0;
|
|
176
|
+
if (y1 > maxY)
|
|
177
|
+
maxY = y1;
|
|
178
|
+
}
|
|
179
|
+
if (minX > maxX)
|
|
180
|
+
return { minX: 0, minY: 0, maxX: 0, maxY: 0, hasContent: false };
|
|
181
|
+
return { minX, minY, maxX, maxY, hasContent: true };
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Honest capability disclosures for rendering `ir` as a flat 2D layer view (DD-014 §6/§11). Returns
|
|
185
|
+
* human-readable notes a consumer UI can show; empty when the IR is faithfully representable (planar
|
|
186
|
+
* FDM). It never suppresses geometry — it explains what the flat top-down projection cannot convey.
|
|
187
|
+
*
|
|
188
|
+
* - `layers: 'unavailable'` (non-planar / CNC / unindexed): the parser put every move on layer 0, so
|
|
189
|
+
* the 2D view shows them all in one flat frame — Z variation and non-XY (e.g. `G18`/`G19`) motion
|
|
190
|
+
* are not represented.
|
|
191
|
+
* - `layers: 'inferred'`: layer boundaries are approximate.
|
|
192
|
+
*/
|
|
193
|
+
export function describe2DDisclosures(ir) {
|
|
194
|
+
const notes = [];
|
|
195
|
+
const layers = ir.header.capabilities['layers'];
|
|
196
|
+
if (layers === 'unavailable') {
|
|
197
|
+
notes.push('No planar layer index (non-planar or CNC toolpath): showing all moves in one flat top-down ' +
|
|
198
|
+
'view. Z variation and non-XY (e.g. G18/G19 plane) motion are not represented in 2D.');
|
|
199
|
+
}
|
|
200
|
+
else if (layers === 'inferred') {
|
|
201
|
+
notes.push('Layer index is inferred; layer boundaries may be approximate.');
|
|
202
|
+
}
|
|
203
|
+
return notes;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Draw the active layer over its preceding "ghost" layers (DD-014 D2, #213). Ghosts are the
|
|
207
|
+
* `adjacentLayers` layers immediately below the active one, drawn first (underneath) and dimmed to
|
|
208
|
+
* `ghostOpacity`; the active layer is drawn last at full opacity. All layers share ONE `fit`, so
|
|
209
|
+
* they overlay correctly in printer XY. `adjacentLayers: 0` draws the active layer only (the floor).
|
|
210
|
+
*/
|
|
211
|
+
export function drawLayers(ctx, ir, opts) {
|
|
212
|
+
const layerCount = ir.layers.length;
|
|
213
|
+
const base = {
|
|
214
|
+
layer: opts.layer,
|
|
215
|
+
layerCount,
|
|
216
|
+
ghostLayers: [],
|
|
217
|
+
extrudeDrawn: 0,
|
|
218
|
+
travelDrawn: 0,
|
|
219
|
+
drawn: false
|
|
220
|
+
};
|
|
221
|
+
if (!ir.layers[opts.layer])
|
|
222
|
+
return base;
|
|
223
|
+
const adjacent = Math.max(0, Math.floor(opts.adjacentLayers ?? 1));
|
|
224
|
+
const ghostOpacity = opts.ghostOpacity ?? 0.25;
|
|
225
|
+
const firstGhost = Math.max(0, opts.layer - adjacent);
|
|
226
|
+
const ghostLayers = [];
|
|
227
|
+
let extrudeDrawn = 0;
|
|
228
|
+
let travelDrawn = 0;
|
|
229
|
+
// Ghosts first (oldest→newest), dimmed, extrusion only — a depth cue, not the focus.
|
|
230
|
+
for (let li = firstGhost; li < opts.layer; li++) {
|
|
231
|
+
const r = drawLayer(ctx, ir, {
|
|
232
|
+
layer: li,
|
|
233
|
+
colorMode: opts.colorMode,
|
|
234
|
+
fit: opts.fit,
|
|
235
|
+
lineWidth: opts.lineWidth,
|
|
236
|
+
opacity: ghostOpacity
|
|
237
|
+
});
|
|
238
|
+
if (r.drawn) {
|
|
239
|
+
ghostLayers.push(li);
|
|
240
|
+
extrudeDrawn += r.extrudeDrawn;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Active layer last, full opacity, with travel if requested. Apply the progress cut only when the
|
|
244
|
+
// live-progress layer IS the active layer — otherwise the marker would be misplaced (honest).
|
|
245
|
+
const p = opts.progress;
|
|
246
|
+
const progressCutSeg = p != null && p.segIndex != null && p.layerIndex === opts.layer ? p.segIndex : undefined;
|
|
247
|
+
const active = drawLayer(ctx, ir, {
|
|
248
|
+
layer: opts.layer,
|
|
249
|
+
colorMode: opts.colorMode,
|
|
250
|
+
fit: opts.fit,
|
|
251
|
+
lineWidth: opts.lineWidth,
|
|
252
|
+
travel: opts.travel,
|
|
253
|
+
progressCutSeg
|
|
254
|
+
});
|
|
255
|
+
extrudeDrawn += active.extrudeDrawn;
|
|
256
|
+
travelDrawn += active.travelDrawn;
|
|
257
|
+
return { layer: opts.layer, layerCount, ghostLayers, extrudeDrawn, travelDrawn, drawn: true };
|
|
258
|
+
}
|
|
259
|
+
const DEFAULT_COLOR_MODE = { mode: 'single', color: [0.55, 0.72, 0.55] };
|
|
260
|
+
/**
|
|
261
|
+
* A thin lifecycle wrapper binding a `ToolpathIR` to a `<canvas>`: pick a layer, pick a color
|
|
262
|
+
* mode, `render()`. It owns no WebGL context and holds no per-layer geometry — each `render()`
|
|
263
|
+
* clears and redraws the active layer (plus its dimmed "ghost" layers), so peak memory stays near
|
|
264
|
+
* one layer (DD-014 §6). The view frame is the whole-model XY bounds (cached on `setToolpath`), so
|
|
265
|
+
* scrubbing layers doesn't jump or rescale and ghosts overlay the active layer exactly. Adapter
|
|
266
|
+
* wiring (the `renderer: '2d' | '3d'` prop) arrives in a later E8 phase.
|
|
267
|
+
*/
|
|
268
|
+
export class LayerView2D {
|
|
269
|
+
canvas;
|
|
270
|
+
ir = null;
|
|
271
|
+
bounds = { minX: 0, minY: 0, maxX: 0, maxY: 0, hasContent: false };
|
|
272
|
+
layer;
|
|
273
|
+
colorMode;
|
|
274
|
+
lineWidth;
|
|
275
|
+
travel;
|
|
276
|
+
padding;
|
|
277
|
+
adjacentLayers;
|
|
278
|
+
ghostOpacity;
|
|
279
|
+
progress = null;
|
|
280
|
+
constructor(canvas, opts = {}) {
|
|
281
|
+
this.canvas = canvas;
|
|
282
|
+
this.layer = opts.layer ?? 0;
|
|
283
|
+
this.colorMode = opts.colorMode ?? DEFAULT_COLOR_MODE;
|
|
284
|
+
this.lineWidth = opts.lineWidth ?? 1;
|
|
285
|
+
this.travel = opts.travel ?? false;
|
|
286
|
+
this.padding = opts.padding ?? 8;
|
|
287
|
+
this.adjacentLayers = Math.max(0, Math.floor(opts.adjacentLayers ?? 1));
|
|
288
|
+
this.ghostOpacity = opts.ghostOpacity ?? 0.25;
|
|
289
|
+
}
|
|
290
|
+
setToolpath(ir) {
|
|
291
|
+
this.ir = ir;
|
|
292
|
+
// Cache the stable whole-model frame once (a scan is O(segments); render() stays layer-bounded).
|
|
293
|
+
this.bounds = ir ? modelBounds2D(ir) : { minX: 0, minY: 0, maxX: 0, maxY: 0, hasContent: false };
|
|
294
|
+
if (ir && this.layer > ir.layers.length - 1)
|
|
295
|
+
this.layer = Math.max(0, ir.layers.length - 1);
|
|
296
|
+
}
|
|
297
|
+
setLayer(layer) {
|
|
298
|
+
this.layer = layer < 0 ? 0 : layer;
|
|
299
|
+
}
|
|
300
|
+
setColorMode(colorMode) {
|
|
301
|
+
this.colorMode = colorMode;
|
|
302
|
+
}
|
|
303
|
+
/** Number of preceding ghost layers drawn beneath the active one (clamped to ≥ 0). */
|
|
304
|
+
setAdjacentLayers(n) {
|
|
305
|
+
this.adjacentLayers = Math.max(0, Math.floor(n));
|
|
306
|
+
}
|
|
307
|
+
/** Live-progress cut (DD-006); null clears it. Applied only when on the active layer. */
|
|
308
|
+
setProgress(progress) {
|
|
309
|
+
this.progress = progress;
|
|
310
|
+
}
|
|
311
|
+
/** Show/hide travel moves on the active layer (`false` hides). */
|
|
312
|
+
setTravel(travel) {
|
|
313
|
+
this.travel = travel;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Honest capability notes for the current IR (DD-014 §6/§11) — e.g. a non-planar/CNC toolpath the
|
|
317
|
+
* flat 2D view cannot fully represent. Empty for faithfully-representable (planar) input.
|
|
318
|
+
*/
|
|
319
|
+
getDisclosures() {
|
|
320
|
+
return this.ir ? describe2DDisclosures(this.ir) : [];
|
|
321
|
+
}
|
|
322
|
+
/** Number of layers in the current IR (0 when none / no layer table). */
|
|
323
|
+
get layerCount() {
|
|
324
|
+
return this.ir?.layers.length ?? 0;
|
|
325
|
+
}
|
|
326
|
+
/** Clear and redraw the active layer + ghosts. Returns the draw result (drawn:false when empty). */
|
|
327
|
+
render() {
|
|
328
|
+
const ctx = this.canvas.getContext('2d');
|
|
329
|
+
const empty = {
|
|
330
|
+
layer: this.layer,
|
|
331
|
+
layerCount: this.layerCount,
|
|
332
|
+
ghostLayers: [],
|
|
333
|
+
extrudeDrawn: 0,
|
|
334
|
+
travelDrawn: 0,
|
|
335
|
+
drawn: false
|
|
336
|
+
};
|
|
337
|
+
if (!ctx)
|
|
338
|
+
return empty;
|
|
339
|
+
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
340
|
+
if (!this.ir)
|
|
341
|
+
return empty;
|
|
342
|
+
const fit = computeLayerFit(this.bounds, this.canvas.width, this.canvas.height, this.padding);
|
|
343
|
+
return drawLayers(ctx, this.ir, {
|
|
344
|
+
layer: this.layer,
|
|
345
|
+
adjacentLayers: this.adjacentLayers,
|
|
346
|
+
colorMode: this.colorMode,
|
|
347
|
+
fit,
|
|
348
|
+
lineWidth: this.lineWidth,
|
|
349
|
+
travel: this.travel,
|
|
350
|
+
ghostOpacity: this.ghostOpacity,
|
|
351
|
+
progress: this.progress
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chestnutlabs/gcode-renderer-2d",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Low-resource Canvas 2D layer renderer for the Chestnut Labs G-code viewer (DD-014 / E8): an opt-in current/adjacent-layer 2D view over the existing ToolpathIR for low-GPU/low-memory/WebGL-blocked devices. No three, no framework.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"gcode",
|
|
7
|
+
"3d-printing",
|
|
8
|
+
"toolpath",
|
|
9
|
+
"cnc",
|
|
10
|
+
"chestnutlabs",
|
|
11
|
+
"canvas",
|
|
12
|
+
"canvas2d",
|
|
13
|
+
"low-resource",
|
|
14
|
+
"layer-view",
|
|
15
|
+
"renderer"
|
|
16
|
+
],
|
|
17
|
+
"author": "Chestnut Labs",
|
|
18
|
+
"homepage": "https://github.com/ChestnutLabs/gcode-preview#readme",
|
|
19
|
+
"bugs": "https://github.com/ChestnutLabs/gcode-preview/issues",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/ChestnutLabs/gcode-preview.git",
|
|
23
|
+
"directory": "packages/gcode-renderer-2d"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"module": "./dist/index.js",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"import": "./dist/index.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist"
|
|
42
|
+
],
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsc -p tsconfig.json",
|
|
48
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
49
|
+
"test": "vitest run"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@chestnutlabs/gcode-colors": "0.3.0",
|
|
53
|
+
"@chestnutlabs/toolpath-core": "0.3.0"
|
|
54
|
+
}
|
|
55
|
+
}
|