@chestnutlabs/gcode-model-renderer 0.20.1 → 0.21.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017-2025 Remco Veldkamp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE.md ADDED
@@ -0,0 +1,46 @@
1
+ # NOTICE — Attribution & Provenance
2
+
3
+ This file summarizes attribution and provenance for **ChestnutLabs/gcode-preview**. It complements,
4
+ and does not replace, the [`LICENSE`](LICENSE) file and the full
5
+ [Upstream, Fork, License & Contribution Policy](docs/03_UPSTREAM_FORK_LICENSE_AND_CONTRIBUTION_POLICY.md).
6
+
7
+ ## Upstream origin
8
+
9
+ - **This repository is a public GitHub fork of** [`xyz-tools/gcode-preview`](https://github.com/xyz-tools/gcode-preview)
10
+ (project identity `remcoder/gcode-preview`; npm package `gcode-preview`).
11
+ - **Chestnut founding baseline:** branch `develop` at commit
12
+ `15375e56129f5a57aebc099ed7bfb5a092dcfb9e` (2026-06-08).
13
+ - The inherited Git history and the upstream fork relationship are preserved. Chestnut Labs maintains
14
+ its own roadmap, governance, release cadence, and package APIs.
15
+
16
+ ## Copyright
17
+
18
+ - Inherited code: **Copyright (c) 2017–2025 Remco Veldkamp** and the `xyz-tools/gcode-preview`
19
+ contributors, licensed under the MIT License (see [`LICENSE`](LICENSE)).
20
+ - Chestnut Labs additions and modifications: **Copyright (c) 2026 Chestnut Labs**, also under the MIT
21
+ License.
22
+
23
+ The upstream copyright notice and MIT permission notice are retained in [`LICENSE`](LICENSE) and must
24
+ not be removed. Chestnut notices are added **alongside** — never in place of — upstream notices.
25
+
26
+ ## License
27
+
28
+ MIT for both the inherited fork and Chestnut Labs additions, unless a later, explicit
29
+ legal/governance decision changes the license for an independently separable new package. See the
30
+ fork/license policy for the full rationale, including how an MIT package may be consumed by a
31
+ GPL-3.0 application (e.g., AnyBridge) without altering the package's MIT notice obligations.
32
+
33
+ ## Derived vs. independently replaced code
34
+
35
+ Chestnut Labs does **not** claim a clean-room implementation. Subsystem provenance (inherited /
36
+ modified / replaced / original) is tracked in
37
+ [`docs/UPSTREAM_PROVENANCE.md`](docs/UPSTREAM_PROVENANCE.md). Replacing every line of an inherited
38
+ file does not by itself establish clean-room status; the ledger exists for honesty, maintenance, and
39
+ license review.
40
+
41
+ ## Third-party code
42
+
43
+ Bundled and dependency notices are tracked in [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).
44
+ Reference implementations studied for behavior (Sindarius/Mainsail, Fluidd, PrusaSlicer/libvgcode,
45
+ BambuBuddy, and others) are **not** incorporated as source unless their license is compatible and the
46
+ incorporation is recorded per policy.
package/README.md CHANGED
@@ -88,6 +88,13 @@ restore a pose (the same serializable `CameraState` as the toolpath renderer), `
88
88
  source formats become viewable by registering a `ModelLoader` for a new `kind`, with no change to the
89
89
  viewer's API (design: [DD-021](../../docs/design/DD-021-interactive-model-viewer.md)).
90
90
 
91
+ The first frame and `frame()` (a "Reset view" button) fit the model's bounding box to the canvas: a
92
+ wide, flat part fills a wide canvas and still fits a narrow 375 px panel, centered with a small margin.
93
+ Until the user orbits, zooms, or pans, each `resize(w, h)` re-fits the model, so a sidebar collapse
94
+ or a window resize never leaves it cropped. After the user moves the camera, `resize` keeps their
95
+ view, and `frame()` hands framing back to the viewer. Pass `autoFrameOnResize: false` to keep the
96
+ first framing across resizes. The `camera-changed` event fires only when a user gesture settles.
97
+
91
98
  Prefer to build your own scene? `parseStl` / `parse3mf` return a three-free `ModelScene`, and both the
92
99
  still and the viewer accept a pre-built `ModelScene` directly.
93
100
 
@@ -15,6 +15,9 @@ import type { ModelScene, RGB } from './scene-model.js';
15
15
  export interface ModelFraming {
16
16
  center: Vector3;
17
17
  radius: number;
18
+ /** The model bounds as a scene-space (Y-up) box, for an aspect-aware fit. */
19
+ sceneMin: Vector3;
20
+ sceneMax: Vector3;
18
21
  }
19
22
  /**
20
23
  * Adds the model root + studio rig to a provided `Scene` and builds meshes into it. The root carries the
@@ -1 +1 @@
1
- {"version":3,"file":"model-content.d.ts","sourceRoot":"","sources":["../src/model-content.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAWL,KAAK,EACL,OAAO,EACR,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAe,UAAU,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAKrE,yGAAyG;AACzG,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,qBAAa,YAAY;IAKX,OAAO,CAAC,QAAQ,CAAC,KAAK;IAJlC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD,OAAO,CAAC,WAAW,CAA6B;gBAEnB,KAAK,EAAE,KAAK;IAezC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAcpE,oFAAoF;IACpF,IAAI,OAAO,IAAI,YAAY,GAAG,IAAI,CAEjC;IAED,OAAO,CAAC,SAAS;IAoCjB,OAAO,CAAC,WAAW;IAMnB,kGAAkG;IAClG,OAAO,IAAI,IAAI;CAGhB;AAED,2FAA2F;AAC3F,eAAO,MAAM,sBAAsB,EAAE,GAAmB,CAAC"}
1
+ {"version":3,"file":"model-content.d.ts","sourceRoot":"","sources":["../src/model-content.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAWL,KAAK,EACL,OAAO,EACR,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAe,UAAU,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAKrE,yGAAyG;AACzG,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;GAKG;AACH,qBAAa,YAAY;IAKX,OAAO,CAAC,QAAQ,CAAC,KAAK;IAJlC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD,OAAO,CAAC,WAAW,CAA6B;gBAEnB,KAAK,EAAE,KAAK;IAezC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAiBpE,oFAAoF;IACpF,IAAI,OAAO,IAAI,YAAY,GAAG,IAAI,CAEjC;IAED,OAAO,CAAC,SAAS;IAoCjB,OAAO,CAAC,WAAW;IAMnB,kGAAkG;IAClG,OAAO,IAAI,IAAI;CAGhB;AAED,2FAA2F;AAC3F,eAAO,MAAM,sBAAsB,EAAE,GAAmB,CAAC"}
@@ -52,7 +52,10 @@ export class ModelContent {
52
52
  const [maxX, maxY, maxZ] = scene.bounds.max;
53
53
  const center = new Vector3((minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2);
54
54
  const radius = Math.max(1e-3, center.distanceTo(new Vector3(minX, minY, minZ)));
55
- this.framedModel = { center, radius };
55
+ // Model Z-up → scene Y-up: (x, y, z) → (x, z, −y), matching the root rotation.
56
+ const sceneMin = new Vector3(minX, minZ, -maxY);
57
+ const sceneMax = new Vector3(maxX, maxZ, -minY);
58
+ this.framedModel = { center, radius, sceneMin, sceneMax };
56
59
  }
57
60
  /** The framing of the last-set scene (`null` before the first {@link setScene}). */
58
61
  get framing() {
@@ -46,6 +46,8 @@ export interface ModelPreviewControllerOptions {
46
46
  createRenderer?: (canvas: RenderTargetCanvas) => GLRendererLike;
47
47
  /** Injected orbit controls (tests). */
48
48
  createControls?: NonNullable<InteractiveStageOptions['createControls']>;
49
+ /** Re-fit the model on canvas resize until the user moves the camera (default `true`). */
50
+ autoFrameOnResize?: boolean;
49
51
  }
50
52
  /** Reactive snapshot of the model viewer, derived from engine events. Immutable — every change replaces it. */
51
53
  export interface ModelPreviewState {
@@ -1 +1 @@
1
- {"version":3,"file":"model-preview.d.ts","sourceRoot":"","sources":["../src/model-preview.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACV,cAAc,EACd,cAAc,EACd,uBAAuB,EACvB,YAAY,EACZ,kBAAkB,EACnB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAqB,KAAK,WAAW,EAAE,KAAK,gBAAgB,EAAE,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACpH,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC7E,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;GAGG;AACH,MAAM,WAAW,6BAA6B;IAC5C,OAAO,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IACjC,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,kBAAkB,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACpC,UAAU,CAAC,EAAE,uBAAuB,CAAC,YAAY,CAAC,CAAC;IACnD,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;IAClD,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,6EAA6E;IAC7E,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;IAC9C,0CAA0C;IAC1C,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,cAAc,CAAC;IAChE,uCAAuC;IACvC,cAAc,CAAC,EAAE,WAAW,CAAC,uBAAuB,CAAC,gBAAgB,CAAC,CAAC,CAAC;CACzE;AAED,+GAA+G;AAC/G,MAAM,WAAW,iBAAiB;IAChC,kDAAkD;IAClD,OAAO,EAAE,OAAO,CAAC;IACjB,6EAA6E;IAC7E,KAAK,EAAE,OAAO,CAAC;IACf,sGAAsG;IACtG,iBAAiB,EAAE,OAAO,CAAC;IAC3B,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,SAAS,EAAE,UAAU,CAAC;IACtB,yFAAyF;IACzF,cAAc,EAAE,MAAM,CAAC;IACvB,8EAA8E;IAC9E,iBAAiB,EAAE,MAAM,CAAC;IAC1B,0CAA0C;IAC1C,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3B,0GAA0G;IAC1G,MAAM,EAAE;QAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC9D,yFAAyF;IACzF,SAAS,EAAE,OAAO,CAAC;IACnB,uEAAuE;IACvE,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,wDAAwD;IACxD,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IAC9B,yDAAyD;IACzD,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACjD;AAED,yFAAyF;AACzF,MAAM,WAAW,oBAAoB;IACnC,4FAA4F;IAC5F,SAAS,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IAC5D,OAAO,CAAC,IAAI,EAAE,gBAAgB,GAAG,UAAU,GAAG,IAAI,CAAC;IACnD,cAAc,IAAI,WAAW,GAAG,IAAI,CAAC;IACrC,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IACzC,aAAa,CAAC,EAAE,EAAE,eAAe,GAAG,IAAI,CAAC;IACzC,qBAAqB,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,IAAI,CAAC;IAClD,wHAAwH;IACxH,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAAC;IAChD,qDAAqD;IACrD,KAAK,IAAI,IAAI,CAAC;IACd,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,0GAA0G;IAC1G,OAAO,CAAC,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,sBAAsB;IACrC,2GAA2G;IAC3G,UAAU,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,GAAG,IAAI,CAAC;IACpD,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,QAAQ,IAAI,iBAAiB,CAAC;IAC9B,aAAa,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAClE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACvD,+EAA+E;IAC/E,GAAG,EAAE;QAAE,MAAM,EAAE,MAAM,WAAW,GAAG,IAAI,CAAA;KAAE,CAAC;IAC1C,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,eAAO,MAAM,mBAAmB,EAAE,iBAcjC,CAAC;AAEF,wBAAgB,4BAA4B,CAAC,OAAO,GAAE,6BAAkC,GAAG,sBAAsB,CA6LhH"}
1
+ {"version":3,"file":"model-preview.d.ts","sourceRoot":"","sources":["../src/model-preview.ts"],"names":[],"mappings":"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"}
@@ -47,6 +47,7 @@ export function createModelPreviewController(options = {}) {
47
47
  ...(options.renderScope ? { renderScope: options.renderScope } : {}),
48
48
  ...(options.createRenderer ? { createRenderer: options.createRenderer } : {}),
49
49
  ...(options.createControls ? { createControls: options.createControls } : {}),
50
+ ...(options.autoFrameOnResize !== undefined ? { autoFrameOnResize: options.autoFrameOnResize } : {}),
50
51
  // Fold progress into state, then hand it to the consumer's callback.
51
52
  onProgress: (p) => {
52
53
  mutate({ progress: p, loading: p.stage !== 'ready' });
@@ -88,9 +89,43 @@ export function createModelPreviewController(options = {}) {
88
89
  emit(e);
89
90
  });
90
91
  };
92
+ /** Detaches the bound canvas's size tracking (ResizeObserver + window-resize fallback). */
93
+ let unsizeCanvas = null;
94
+ /**
95
+ * Keep the engine sized to a DOM canvas's CSS box, mirroring the toolpath controller: an initial fit,
96
+ * a ResizeObserver, and a window-resize fallback (observers may never fire in embedded panes). A
97
+ * zero-sized (hidden) canvas is skipped. OffscreenCanvas hosts size themselves via `controls.resize`.
98
+ */
99
+ const trackCanvasSize = (canvas, v) => {
100
+ const el = canvas;
101
+ if (typeof el.getBoundingClientRect !== 'function')
102
+ return;
103
+ const fit = () => {
104
+ const w = el.clientWidth ?? 0;
105
+ const h = el.clientHeight ?? 0;
106
+ if (w > 0 && h > 0)
107
+ v.resize(w, h);
108
+ };
109
+ fit();
110
+ let ro = null;
111
+ if (typeof ResizeObserver !== 'undefined') {
112
+ ro = new ResizeObserver(fit);
113
+ ro.observe(canvas);
114
+ }
115
+ const hasWindow = typeof window !== 'undefined';
116
+ if (hasWindow)
117
+ window.addEventListener('resize', fit);
118
+ unsizeCanvas = () => {
119
+ ro?.disconnect();
120
+ if (hasWindow)
121
+ window.removeEventListener('resize', fit);
122
+ };
123
+ };
91
124
  function bindCanvas(canvas) {
92
125
  if (disposed || canvas === boundCanvas)
93
126
  return;
127
+ unsizeCanvas?.();
128
+ unsizeCanvas = null;
94
129
  // Tear down the engine bound to the previous canvas.
95
130
  if (engine !== null) {
96
131
  unsubEngine?.();
@@ -104,6 +139,7 @@ export function createModelPreviewController(options = {}) {
104
139
  const v = createModelViewer(canvas, engineOptions);
105
140
  engine = v;
106
141
  wire(v);
142
+ trackCanvasSize(canvas, v);
107
143
  for (const op of pendingOps)
108
144
  op(v);
109
145
  pendingOps.length = 0;
@@ -187,6 +223,8 @@ export function createModelPreviewController(options = {}) {
187
223
  if (disposed)
188
224
  return;
189
225
  disposed = true;
226
+ unsizeCanvas?.();
227
+ unsizeCanvas = null;
190
228
  unsubEngine?.();
191
229
  unsubEngine = null;
192
230
  engine?.dispose();
@@ -29,6 +29,12 @@ export interface ModelViewerOptions {
29
29
  * consumer owns all wording/i18n. Events from a superseded load are not emitted.
30
30
  */
31
31
  onProgress?: (progress: LoadProgress) => void;
32
+ /**
33
+ * Re-fit the camera to the model when the canvas is resized (`resize()`), until the user moves the
34
+ * camera (orbit/zoom/pan or `setCameraState`); after that their view is kept. `frame()` (Reset view),
35
+ * a new source, or a render-scope change resumes auto-framing. Default `true`.
36
+ */
37
+ autoFrameOnResize?: boolean;
32
38
  }
33
39
  /** What the source actually carried, surfaced on `ready` (never fabricated — DD-001). */
34
40
  export interface ModelReadyInfo {
@@ -59,7 +65,9 @@ export interface ModelReadyInfo {
59
65
  export type ModelViewerEvent = {
60
66
  type: 'ready';
61
67
  info: ModelReadyInfo;
62
- } | {
68
+ }
69
+ /** Fires once per user camera gesture (orbit/zoom/pan) after it settles — never for programmatic moves. */
70
+ | {
63
71
  type: 'camera-changed';
64
72
  state: CameraState;
65
73
  } | {
@@ -86,7 +94,10 @@ export interface ModelViewer {
86
94
  setBackground(bg: ModelBackground): void;
87
95
  setInteractionQuality(mode: 'off' | 'auto'): void;
88
96
  resize(width: number, height: number): void;
89
- /** Re-fit the camera to the current model bounds. */
97
+ /**
98
+ * Re-fit the camera to the current model bounds (the default 3/4 view), fitting the bounding box to
99
+ * the canvas aspect and field of view. Also resumes auto-framing on resize.
100
+ */
90
101
  frame(): void;
91
102
  /**
92
103
  * Render only a subset of the current source (DD-030 D2): `{ plateId }`, `{ objectIds }`, or an
@@ -1 +1 @@
1
- {"version":3,"file":"model-viewer.d.ts","sourceRoot":"","sources":["../src/model-viewer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"model-viewer.d.ts","sourceRoot":"","sources":["../src/model-viewer.ts"],"names":[],"mappings":"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"}
@@ -55,7 +55,17 @@ export function createModelViewer(canvas, options = {}) {
55
55
  const scopedScene = () => currentScene === null ? null : currentScope ? applyRenderScope(currentScene, currentScope) : currentScene;
56
56
  // Monotonic token for last-wins overlapping setSource (stale async results are discarded).
57
57
  let sourceToken = 0;
58
+ const autoFrameOnResize = options.autoFrameOnResize ?? true;
59
+ // True while the camera is library-framed; a user gesture (or setCameraState) hands it to the user.
60
+ let autoFraming = true;
58
61
  let stage = null;
62
+ /** Fit the current model's box to the viewport (aspect + FOV aware); `keepDirection` keeps the view angle. */
63
+ const fit = (keepDirection = false) => {
64
+ const f = content.framing;
65
+ if (f === null || stage === null)
66
+ return;
67
+ stage.fitBox(f.sceneMin, f.sceneMax, keepDirection ? { direction: stage.viewDirection() } : {});
68
+ };
59
69
  try {
60
70
  stage = new InteractiveStage({
61
71
  canvas,
@@ -65,6 +75,9 @@ export function createModelViewer(canvas, options = {}) {
65
75
  createRenderer: options.createRenderer ?? ((c) => createDefaultGLRenderer(c, { alpha: true })),
66
76
  ...(options.createControls ? { createControls: options.createControls } : {}),
67
77
  onCameraChanged: (state) => emit({ type: 'camera-changed', state }),
78
+ onInteractionStart: () => {
79
+ autoFraming = false;
80
+ },
68
81
  onContextLost: () => emit({ type: 'context-lost' }),
69
82
  onContextRestored: () => {
70
83
  // GPU resources are gone; rebuild meshes from the retained scene and reframe, mirroring the
@@ -72,9 +85,8 @@ export function createModelViewer(canvas, options = {}) {
72
85
  const shown = scopedScene();
73
86
  if (shown !== null && stage !== null) {
74
87
  content.setScene(shown);
75
- const f = content.framing;
76
- if (f !== null)
77
- stage.frameTo(f.center, f.radius);
88
+ if (autoFraming)
89
+ fit(true);
78
90
  }
79
91
  emit({ type: 'context-restored' });
80
92
  }
@@ -124,9 +136,8 @@ export function createModelViewer(canvas, options = {}) {
124
136
  const total = shown.objects.length;
125
137
  progress({ stage: 'building-geometry', done: 0, total, unit: 'objects' });
126
138
  content.setScene(shown, (done) => progress({ stage: 'building-geometry', done, total, unit: 'objects' }));
127
- const f = content.framing;
128
- if (f !== null)
129
- stage.frameTo(f.center, f.radius);
139
+ autoFraming = true;
140
+ fit();
130
141
  progress({ stage: 'ready' });
131
142
  emit({ type: 'ready', info });
132
143
  return info;
@@ -144,11 +155,15 @@ export function createModelViewer(canvas, options = {}) {
144
155
  setSource,
145
156
  setView(view) {
146
157
  stage?.setView(view);
158
+ // While library-framed, a preset view is fitted too (and stays fitted across resizes).
159
+ if (autoFraming)
160
+ fit(true);
147
161
  },
148
162
  getCameraState() {
149
163
  return stage ? stage.getCameraState() : null;
150
164
  },
151
165
  setCameraState(state) {
166
+ autoFraming = false;
152
167
  stage?.setCameraState(state);
153
168
  },
154
169
  setBackground(bg) {
@@ -160,11 +175,13 @@ export function createModelViewer(canvas, options = {}) {
160
175
  },
161
176
  resize(width, height) {
162
177
  stage?.resize(width, height);
178
+ // A hidden (0×0) canvas has no aspect to fit; keep the pose and re-fit on the next real size.
179
+ if (autoFrameOnResize && autoFraming && width > 0 && height > 0)
180
+ fit(true);
163
181
  },
164
182
  frame() {
165
- const f = content.framing;
166
- if (f !== null)
167
- stage?.frameTo(f.center, f.radius);
183
+ autoFraming = true;
184
+ fit();
168
185
  },
169
186
  setRenderScope(scope) {
170
187
  currentScope = scope;
@@ -172,10 +189,8 @@ export function createModelViewer(canvas, options = {}) {
172
189
  if (shown === null || stage === null)
173
190
  return; // no source yet — applied on the next setSource
174
191
  content.setScene(shown);
175
- const f = content.framing;
176
- if (f !== null)
177
- stage.frameTo(f.center, f.radius);
178
- stage.render();
192
+ autoFraming = true;
193
+ fit();
179
194
  },
180
195
  capture(opts) {
181
196
  if (stage === null) {
package/dist/stl.d.ts CHANGED
@@ -2,4 +2,10 @@ import { type ModelScene } from './scene-model.js';
2
2
  import { type ModelLimits } from './limits.js';
3
3
  /** Parse STL bytes into a single-object {@link ModelScene}. Throws {@link ModelParseError} on failure. */
4
4
  export declare function parseStl(source: Uint8Array | ArrayBuffer, limits?: ModelLimits): ModelScene;
5
+ /**
6
+ * STL facet normals are advisory and many exporters write `0 0 0` (or garbage). A zero/non-finite normal
7
+ * lights the facet black, so replace each degenerate one with the face normal from its winding; valid
8
+ * file normals are kept. A triangle with no area keeps a zero normal (it covers no pixels).
9
+ */
10
+ export declare function repairFacetNormals(positions: Float32Array, normals: Float32Array): Float32Array;
5
11
  //# sourceMappingURL=stl.d.ts.map
package/dist/stl.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"stl.d.ts","sourceRoot":"","sources":["../src/stl.ts"],"names":[],"mappings":"AAQA,OAAO,EAAsD,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACvG,OAAO,EAAkC,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAgB/E,0GAA0G;AAC1G,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,WAAW,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,UAAU,CAyD3F"}
1
+ {"version":3,"file":"stl.d.ts","sourceRoot":"","sources":["../src/stl.ts"],"names":[],"mappings":"AAQA,OAAO,EAAsD,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACvG,OAAO,EAAkC,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAgB/E,0GAA0G;AAC1G,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,WAAW,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,UAAU,CA4D3F;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,GAAG,YAAY,CA+B/F"}
package/dist/stl.js CHANGED
@@ -54,7 +54,9 @@ export function parseStl(source, limits) {
54
54
  }
55
55
  const positions = new Float32Array(posAttr.array);
56
56
  const normAttr = geom.getAttribute('normal');
57
- const normals = normAttr !== undefined ? new Float32Array(normAttr.array) : undefined;
57
+ const normals = normAttr !== undefined
58
+ ? repairFacetNormals(positions, new Float32Array(normAttr.array))
59
+ : undefined;
58
60
  const bounds = boundsOf(positions);
59
61
  geom.dispose();
60
62
  const geometry = normals !== undefined ? { positions, normals } : { positions };
@@ -70,6 +72,45 @@ export function parseStl(source, limits) {
70
72
  }
71
73
  };
72
74
  }
75
+ /**
76
+ * STL facet normals are advisory and many exporters write `0 0 0` (or garbage). A zero/non-finite normal
77
+ * lights the facet black, so replace each degenerate one with the face normal from its winding; valid
78
+ * file normals are kept. A triangle with no area keeps a zero normal (it covers no pixels).
79
+ */
80
+ export function repairFacetNormals(positions, normals) {
81
+ for (let t = 0; t + 8 < normals.length && t + 8 < positions.length; t += 9) {
82
+ const nx = normals[t];
83
+ const ny = normals[t + 1];
84
+ const nz = normals[t + 2];
85
+ const len = Math.hypot(nx, ny, nz);
86
+ if (Number.isFinite(len) && len > 1e-6)
87
+ continue;
88
+ const ax = positions[t + 3] - positions[t];
89
+ const ay = positions[t + 4] - positions[t + 1];
90
+ const az = positions[t + 5] - positions[t + 2];
91
+ const bx = positions[t + 6] - positions[t];
92
+ const by = positions[t + 7] - positions[t + 1];
93
+ const bz = positions[t + 8] - positions[t + 2];
94
+ let cx = ay * bz - az * by;
95
+ let cy = az * bx - ax * bz;
96
+ let cz = ax * by - ay * bx;
97
+ const cl = Math.hypot(cx, cy, cz);
98
+ if (cl > 0) {
99
+ cx /= cl;
100
+ cy /= cl;
101
+ cz /= cl;
102
+ }
103
+ else {
104
+ cx = cy = cz = 0;
105
+ }
106
+ for (let v = 0; v < 9; v += 3) {
107
+ normals[t + v] = cx;
108
+ normals[t + v + 1] = cy;
109
+ normals[t + v + 2] = cz;
110
+ }
111
+ }
112
+ return normals;
113
+ }
73
114
  function boundsOf(positions) {
74
115
  let minX = Infinity;
75
116
  let minY = Infinity;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chestnutlabs/gcode-model-renderer",
3
- "version": "0.20.1",
3
+ "version": "0.21.0",
4
4
  "description": "Three.js presentation renderer for source models (STL/3MF) — clean thumbnails/cards, distinct from toolpath inspection (DD-018).",
5
5
  "keywords": [
6
6
  "3d-printing",
@@ -44,7 +44,9 @@
44
44
  }
45
45
  },
46
46
  "files": [
47
- "dist"
47
+ "dist",
48
+ "LICENSE",
49
+ "NOTICE.md"
48
50
  ],
49
51
  "publishConfig": {
50
52
  "access": "public"
@@ -55,9 +57,9 @@
55
57
  "test": "vitest run"
56
58
  },
57
59
  "dependencies": {
58
- "@chestnutlabs/gcode-containers": "0.20.1",
59
- "@chestnutlabs/gcode-renderer-three": "0.20.1",
60
- "@chestnutlabs/toolpath-core": "0.20.1"
60
+ "@chestnutlabs/gcode-containers": "0.21.0",
61
+ "@chestnutlabs/gcode-renderer-three": "0.21.0",
62
+ "@chestnutlabs/toolpath-core": "0.21.0"
61
63
  },
62
64
  "devDependencies": {
63
65
  "@types/three": "0.178.0"