@chestnutlabs/gcode-colors 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 ADDED
@@ -0,0 +1,43 @@
1
+ # @chestnutlabs/gcode-colors
2
+
3
+ Renderer-agnostic **per-segment color model** for the [Chestnut Labs G-code Preview](https://github.com/ChestnutLabs/gcode-preview) toolpath stack (DD-014 D3).
4
+
5
+ This package is the single home for the toolpath **color subsystem**: the `ColorMode` union and an
6
+ honest, capability-gated mapping from a `ToolpathIR` segment to an `RGB` triple. Both the Three.js
7
+ renderer (`@chestnutlabs/gcode-renderer-three`) and the low-resource 2D renderer
8
+ (`@chestnutlabs/gcode-renderer-2d`) depend on it, so color and move-kind semantics never drift
9
+ between them.
10
+
11
+ It depends only on `@chestnutlabs/toolpath-core` — **no `three`, no canvas, no framework**.
12
+
13
+ ## Color modes
14
+
15
+ | Mode | Channel | Honest fallback |
16
+ |---|---|---|
17
+ | `single` | — | n/a (one color) |
18
+ | `tool` | `segments.tool` | `fallback` when no palette |
19
+ | `feature` | `segments.feature` (0 = unknown) | `fallback` for unknown / no palette |
20
+ | `colorChange` | `colorChanges[]` swap slot (M600, #147) | `fallback` when no changes / no palette |
21
+ | `feedrate` | `segments.feedrate` (mm/min; NaN = unknown, #177) | `fallback` for NaN |
22
+ | `object` | `segments.object` (1-based; 0 = none, #178) | `fallback` for none / non-`only` |
23
+
24
+ Every mode degrades to its `fallback` for unknown channel values — it never fabricates a
25
+ role/speed/object color. Modes that read an optional channel (`feature`, `object`, `feedrate`) must
26
+ be **capability-gated by the caller** before use.
27
+
28
+ ## Usage
29
+
30
+ ```ts
31
+ import { createSegmentColorer } from '@chestnutlabs/gcode-colors';
32
+
33
+ // Build a colorer once (feedrate auto-range is resolved here), then O(1) per segment.
34
+ const colorOf = createSegmentColorer(ir, { mode: 'feedrate', ramp: [[0, 0, 1], [1, 0, 0]], fallback: [0.7, 0.7, 0.7] });
35
+ for (let i = 0; i < ir.segments.count; i++) {
36
+ const [r, g, b] = colorOf(i);
37
+ // …map onto your surface (WebGL vertex buffer, Canvas 2D strokeStyle, …)
38
+ }
39
+ ```
40
+
41
+ ## License
42
+
43
+ MIT © Chestnut Labs. See the [repository](https://github.com/ChestnutLabs/gcode-preview) for provenance and the full support policy.
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Renderer-agnostic per-segment color model (DD-014 D3, extracted from
3
+ * `gcode-renderer-three` so the 3D and 2D renderers single-source it).
4
+ *
5
+ * A color is a pure function of the IR's channels (`tool`/`feature`/`object`/
6
+ * `feedrate`/`colorChanges`) and a consumer palette — no `three`, no canvas, no
7
+ * geometry. Each renderer maps the returned {@link RGB} onto its own surface
8
+ * (a vertex-color buffer for WebGL, a `strokeStyle` for Canvas 2D).
9
+ *
10
+ * Honesty is built in: unknown channel values degrade to the mode's `fallback`
11
+ * (or the neutral default), never a fabricated role/speed/object color. Modes
12
+ * that read an optional channel (`feature`, `object`, `feedrate`) must be
13
+ * capability-gated by the caller before use — the IR is told, not shown nonsense.
14
+ */
15
+ import type { ToolpathIR } from '@chestnutlabs/toolpath-core';
16
+ export type RGB = [number, number, number];
17
+ export type ColorMode = {
18
+ mode: 'single';
19
+ color: RGB;
20
+ } | {
21
+ mode: 'tool';
22
+ palette: RGB[];
23
+ fallback?: RGB;
24
+ } | {
25
+ mode: 'feature';
26
+ palette: RGB[];
27
+ fallback: RGB;
28
+ } | {
29
+ mode: 'colorChange';
30
+ palette: RGB[];
31
+ fallback: RGB;
32
+ } | {
33
+ mode: 'feedrate';
34
+ ramp: RGB[];
35
+ range?: [number, number];
36
+ fallback: RGB;
37
+ } | {
38
+ mode: 'object';
39
+ palette: RGB[];
40
+ fallback: RGB;
41
+ only?: number;
42
+ } | {
43
+ mode: 'layerHeight';
44
+ ramp: RGB[];
45
+ range?: [number, number];
46
+ fallback: RGB;
47
+ };
48
+ /** Neutral color used when a mode has no palette / the channel value is unknown. */
49
+ export declare const DEFAULT_FALLBACK: RGB;
50
+ /** Min/max of the IR's defined (non-NaN) feedrates — the auto-range for color-by-speed (#177). */
51
+ export declare function feedrateRange(ir: ToolpathIR): [number, number];
52
+ /**
53
+ * Per-layer height (mm): the Z-delta from the previous layer, indexed by layer. Layer 0's height is
54
+ * its absolute Z (its thickness from the bed). Negative deltas (non-monotonic Z, e.g. spiral/vase
55
+ * synthetic layers) clamp to 0 rather than fabricate a negative height. Derived only from `ir.layers`.
56
+ */
57
+ export declare function layerHeights(ir: ToolpathIR): Float32Array;
58
+ /** Min/max per-layer height — the auto-range for color-by-layer-height (#179). */
59
+ export declare function layerHeightRange(ir: ToolpathIR): [number, number];
60
+ /** Sample a color ramp at t∈[0,1] with linear interpolation between stops. */
61
+ export declare function rampColor(ramp: RGB[], t: number): RGB;
62
+ /** Per-segment RGB accessor — the loop-friendly form both renderers use. */
63
+ export type SegmentColorer = (segIndex: number) => RGB;
64
+ /**
65
+ * Build a per-segment color accessor for `ir` under `mode`. Feedrate auto-range (or the
66
+ * explicit `range`) is resolved once here, so the returned function is O(1) per segment.
67
+ * The returned RGB is honest: unknown channel values yield the mode's fallback.
68
+ */
69
+ export declare function createSegmentColorer(ir: ToolpathIR, mode: ColorMode): SegmentColorer;
70
+ /**
71
+ * The color of a single segment under `mode`. Convenience over {@link createSegmentColorer}
72
+ * for one-off lookups; for a loop, build a colorer once (feedrate auto-range is otherwise
73
+ * rescanned per call).
74
+ */
75
+ export declare function segmentColor(ir: ToolpathIR, mode: ColorMode, segIndex: number): RGB;
76
+ //# sourceMappingURL=colors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"colors.d.ts","sourceRoot":"","sources":["../src/colors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAE3C,MAAM,MAAM,SAAS,GACjB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,GAAG,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,GAAG,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,GAAG,EAAE,CAAC;IAAC,QAAQ,EAAE,GAAG,CAAA;CAAE,GAClD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,GAAG,EAAE,CAAC;IAAC,QAAQ,EAAE,GAAG,CAAA;CAAE,GAItD;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,GAAG,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,QAAQ,EAAE,GAAG,CAAA;CAAE,GAG1E;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,GAAG,EAAE,CAAC;IAAC,QAAQ,EAAE,GAAG,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAKhE;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,GAAG,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,QAAQ,EAAE,GAAG,CAAA;CAAE,CAAC;AAElF,oFAAoF;AACpF,eAAO,MAAM,gBAAgB,EAAE,GAAqB,CAAC;AAErD,kGAAkG;AAClG,wBAAgB,aAAa,CAAC,EAAE,EAAE,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAW9D;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,UAAU,GAAG,YAAY,CAOzD;AAED,kFAAkF;AAClF,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CASjE;AAED,8EAA8E;AAC9E,wBAAgB,SAAS,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,GAAG,CAWrD;AAkBD,4EAA4E;AAC5E,MAAM,MAAM,cAAc,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,GAAG,CAAC;AAEvD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,GAAG,cAAc,CAwDpF;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,GAAG,CAEnF"}
package/dist/colors.js ADDED
@@ -0,0 +1,146 @@
1
+ /** Neutral color used when a mode has no palette / the channel value is unknown. */
2
+ export const DEFAULT_FALLBACK = [0.7, 0.7, 0.7];
3
+ /** Min/max of the IR's defined (non-NaN) feedrates — the auto-range for color-by-speed (#177). */
4
+ export function feedrateRange(ir) {
5
+ const f = ir.segments.feedrate;
6
+ let min = Infinity;
7
+ let max = -Infinity;
8
+ for (let i = 0; i < f.length; i++) {
9
+ const v = f[i];
10
+ if (Number.isNaN(v))
11
+ continue;
12
+ if (v < min)
13
+ min = v;
14
+ if (v > max)
15
+ max = v;
16
+ }
17
+ return min <= max ? [min, max] : [0, 0];
18
+ }
19
+ /**
20
+ * Per-layer height (mm): the Z-delta from the previous layer, indexed by layer. Layer 0's height is
21
+ * its absolute Z (its thickness from the bed). Negative deltas (non-monotonic Z, e.g. spiral/vase
22
+ * synthetic layers) clamp to 0 rather than fabricate a negative height. Derived only from `ir.layers`.
23
+ */
24
+ export function layerHeights(ir) {
25
+ const layers = ir.layers;
26
+ const h = new Float32Array(layers.length);
27
+ for (let i = 0; i < layers.length; i++) {
28
+ h[i] = i === 0 ? Math.max(0, layers[0].z) : Math.max(0, layers[i].z - layers[i - 1].z);
29
+ }
30
+ return h;
31
+ }
32
+ /** Min/max per-layer height — the auto-range for color-by-layer-height (#179). */
33
+ export function layerHeightRange(ir) {
34
+ const h = layerHeights(ir);
35
+ let min = Infinity;
36
+ let max = -Infinity;
37
+ for (let i = 0; i < h.length; i++) {
38
+ if (h[i] < min)
39
+ min = h[i];
40
+ if (h[i] > max)
41
+ max = h[i];
42
+ }
43
+ return min <= max ? [min, max] : [0, 0];
44
+ }
45
+ /** Sample a color ramp at t∈[0,1] with linear interpolation between stops. */
46
+ export function rampColor(ramp, t) {
47
+ if (ramp.length === 0)
48
+ return DEFAULT_FALLBACK;
49
+ if (ramp.length === 1)
50
+ return ramp[0];
51
+ const clamped = t < 0 ? 0 : t > 1 ? 1 : t;
52
+ const p = clamped * (ramp.length - 1);
53
+ const lo = Math.floor(p);
54
+ const hi = Math.min(lo + 1, ramp.length - 1);
55
+ const frac = p - lo;
56
+ const a = ramp[lo];
57
+ const b = ramp[hi];
58
+ return [a[0] + (b[0] - a[0]) * frac, a[1] + (b[1] - a[1]) * frac, a[2] + (b[2] - a[2]) * frac];
59
+ }
60
+ /**
61
+ * Swap slot at a segment index (DD-009 D2, #147): the count of M600 color changes at
62
+ * or before it. `colorChanges` is ascending by `segIndex` (source order), so this is a
63
+ * binary upper-bound — decimation-safe (slots are keyed on the original segment index).
64
+ */
65
+ function swapSlotAt(colorChanges, segIndex) {
66
+ let lo = 0;
67
+ let hi = colorChanges.length;
68
+ while (lo < hi) {
69
+ const mid = (lo + hi) >>> 1;
70
+ if (colorChanges[mid].segIndex <= segIndex)
71
+ lo = mid + 1;
72
+ else
73
+ hi = mid;
74
+ }
75
+ return lo;
76
+ }
77
+ /**
78
+ * Build a per-segment color accessor for `ir` under `mode`. Feedrate auto-range (or the
79
+ * explicit `range`) is resolved once here, so the returned function is O(1) per segment.
80
+ * The returned RGB is honest: unknown channel values yield the mode's fallback.
81
+ */
82
+ export function createSegmentColorer(ir, mode) {
83
+ const seg = ir.segments;
84
+ if (mode.mode === 'single') {
85
+ const c = mode.color;
86
+ return () => c;
87
+ }
88
+ if (mode.mode === 'tool') {
89
+ const fallback = mode.fallback ?? DEFAULT_FALLBACK;
90
+ return (i) => (mode.palette.length > 0 ? mode.palette[seg.tool[i] % mode.palette.length] : fallback);
91
+ }
92
+ if (mode.mode === 'feature') {
93
+ // Feature 0 = unknown (DD-001): fallback color, never a fabricated role color.
94
+ return (i) => {
95
+ const feature = seg.feature[i];
96
+ return feature === 0 || mode.palette.length === 0
97
+ ? mode.fallback
98
+ : mode.palette[(feature - 1) % mode.palette.length];
99
+ };
100
+ }
101
+ if (mode.mode === 'colorChange') {
102
+ // Shade by swap slot, not by seg.tool — the "reuse by-tool coloring" of DD-009 D2, keyed on the
103
+ // M600 count. Slot 0 = base filament = palette[0]. With no color changes (or no palette) use the
104
+ // neutral fallback, never a fabricated slot color.
105
+ return (i) => ir.colorChanges.length === 0 || mode.palette.length === 0
106
+ ? mode.fallback
107
+ : mode.palette[swapSlotAt(ir.colorChanges, i) % mode.palette.length];
108
+ }
109
+ if (mode.mode === 'feedrate') {
110
+ const [lo, hi] = mode.range ?? feedrateRange(ir);
111
+ const span = hi - lo;
112
+ // NaN feedrate (before the first F) → fallback, never a fabricated speed color (#177).
113
+ return (i) => {
114
+ const v = seg.feedrate[i];
115
+ return Number.isNaN(v) ? mode.fallback : rampColor(mode.ramp, span > 0 ? (v - lo) / span : 0);
116
+ };
117
+ }
118
+ if (mode.mode === 'layerHeight') {
119
+ // Precompute per-layer heights once → O(1) per segment (#179). An out-of-range layer index
120
+ // (shouldn't happen; a hostile IR) → fallback, never a fabricated height color.
121
+ const heights = layerHeights(ir);
122
+ const [lo, hi] = mode.range ?? layerHeightRange(ir);
123
+ const span = hi - lo;
124
+ return (i) => {
125
+ const li = seg.layer[i];
126
+ if (li < 0 || li >= heights.length)
127
+ return mode.fallback;
128
+ return rampColor(mode.ramp, span > 0 ? (heights[li] - lo) / span : 0);
129
+ };
130
+ }
131
+ // object mode: object 0 = none/unknown → fallback; `only` isolates one object, dimming the rest (#178).
132
+ return (i) => {
133
+ const obj = seg.object[i];
134
+ return obj === 0 || (mode.only !== undefined && obj !== mode.only) || mode.palette.length === 0
135
+ ? mode.fallback
136
+ : mode.palette[(obj - 1) % mode.palette.length];
137
+ };
138
+ }
139
+ /**
140
+ * The color of a single segment under `mode`. Convenience over {@link createSegmentColorer}
141
+ * for one-off lookups; for a loop, build a colorer once (feedrate auto-range is otherwise
142
+ * rescanned per call).
143
+ */
144
+ export function segmentColor(ir, mode, segIndex) {
145
+ return createSegmentColorer(ir, mode)(segIndex);
146
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @chestnutlabs/gcode-colors — renderer-agnostic per-segment color model (DD-014 D3).
3
+ *
4
+ * The single home for the toolpath color subsystem: the {@link ColorMode} union and an
5
+ * honest, capability-gated mapping from a `ToolpathIR` segment to an {@link RGB}. Both the
6
+ * Three.js renderer and the low-resource 2D renderer depend on this package so color/kind
7
+ * semantics never drift between them. Depends only on `@chestnutlabs/toolpath-core` — no
8
+ * `three`, no canvas, no framework.
9
+ */
10
+ export type { RGB, ColorMode, SegmentColorer } from './colors.js';
11
+ export { DEFAULT_FALLBACK, feedrateRange, layerHeights, layerHeightRange, rampColor, createSegmentColorer, segmentColor } from './colors.js';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,YAAY,EAAE,GAAG,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,gBAAgB,EAChB,SAAS,EACT,oBAAoB,EACpB,YAAY,EACb,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { DEFAULT_FALLBACK, feedrateRange, layerHeights, layerHeightRange, rampColor, createSegmentColorer, segmentColor } from './colors.js';
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@chestnutlabs/gcode-colors",
3
+ "version": "0.3.0",
4
+ "description": "Renderer-agnostic per-segment color model for the Chestnut Labs G-code toolpath stack (DD-014 D3): the ColorMode union and honest, capability-gated segment coloring over ToolpathIR channels, shared by the 3D and 2D renderers.",
5
+ "keywords": [
6
+ "gcode",
7
+ "3d-printing",
8
+ "toolpath",
9
+ "cnc",
10
+ "chestnutlabs",
11
+ "color",
12
+ "colormap",
13
+ "visualization"
14
+ ],
15
+ "author": "Chestnut Labs",
16
+ "homepage": "https://github.com/ChestnutLabs/gcode-preview#readme",
17
+ "bugs": "https://github.com/ChestnutLabs/gcode-preview/issues",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/ChestnutLabs/gcode-preview.git",
21
+ "directory": "packages/gcode-colors"
22
+ },
23
+ "license": "MIT",
24
+ "engines": {
25
+ "node": ">=22"
26
+ },
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "./dist/index.js",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "import": "./dist/index.js"
36
+ }
37
+ },
38
+ "files": [
39
+ "dist"
40
+ ],
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "scripts": {
45
+ "build": "tsc -p tsconfig.json",
46
+ "typecheck": "tsc -p tsconfig.json --noEmit",
47
+ "test": "vitest run"
48
+ },
49
+ "dependencies": {
50
+ "@chestnutlabs/toolpath-core": "0.3.0"
51
+ }
52
+ }