@chestnutlabs/gcode-model-renderer 0.11.0 → 0.13.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/dist/index.d.ts CHANGED
@@ -21,5 +21,5 @@ export { computeCacheKey, defaultEnvId } from './cache-key.js';
21
21
  export { DEFAULT_LIMITS, resolveLimits, ModelParseError } from './limits.js';
22
22
  export type { ModelLimits, ResolvedLimits } from './limits.js';
23
23
  export { IDENTITY_MAT4 } from './scene-model.js';
24
- export type { ModelScene, ModelObject, ModelMaterial, MeshGeometry, ModelBounds, RGB, Mat4 } from './scene-model.js';
24
+ export type { ModelScene, ModelObject, ModelMaterial, ModelPlateSummary, MeshGeometry, ModelBounds, RGB, Mat4 } from './scene-model.js';
25
25
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC5E,YAAY,EAAE,oBAAoB,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACnG,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,YAAY,EAAE,WAAW,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAC5G,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAC3G,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,qBAAqB,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAChH,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACpF,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC7E,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC5E,YAAY,EAAE,oBAAoB,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACnG,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,YAAY,EAAE,WAAW,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAC5G,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAC3G,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,qBAAqB,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAChH,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACpF,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC7E,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,YAAY,EACV,UAAU,EACV,WAAW,EACX,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,WAAW,EACX,GAAG,EACH,IAAI,EACL,MAAM,kBAAkB,CAAC"}
@@ -28,8 +28,12 @@ export declare class ModelContent {
28
28
  private readonly disposables;
29
29
  private framedModel;
30
30
  constructor(scene: Scene);
31
- /** Build meshes for the scene and record its framing. Replaces (and disposes) any previous meshes. */
32
- setScene(scene: ModelScene): void;
31
+ /**
32
+ * Build meshes for the scene and record its framing. Replaces (and disposes) any previous meshes.
33
+ * `onObject(done)` (DD-024) is invoked after each object's mesh is built, with the running count, for
34
+ * real `building-geometry` progress.
35
+ */
36
+ setScene(scene: ModelScene, onObject?: (done: number) => void): void;
33
37
  /** The framing of the last-set scene (`null` before the first {@link setScene}). */
34
38
  get framing(): ModelFraming | null;
35
39
  private buildMesh;
@@ -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,sGAAsG;IACtG,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAWjC,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;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"}
@@ -37,11 +37,17 @@ export class ModelContent {
37
37
  fill.position.set(-0.7, 0.3, -0.5);
38
38
  this.scene.add(fill);
39
39
  }
40
- /** Build meshes for the scene and record its framing. Replaces (and disposes) any previous meshes. */
41
- setScene(scene) {
40
+ /**
41
+ * Build meshes for the scene and record its framing. Replaces (and disposes) any previous meshes.
42
+ * `onObject(done)` (DD-024) is invoked after each object's mesh is built, with the running count, for
43
+ * real `building-geometry` progress.
44
+ */
45
+ setScene(scene, onObject) {
42
46
  this.clearMeshes();
43
- for (const obj of scene.objects)
44
- this.root.add(this.buildMesh(obj));
47
+ for (let i = 0; i < scene.objects.length; i++) {
48
+ this.root.add(this.buildMesh(scene.objects[i]));
49
+ onObject?.(i + 1);
50
+ }
45
51
  const [minX, minY, minZ] = scene.bounds.min;
46
52
  const [maxX, maxY, maxZ] = scene.bounds.max;
47
53
  const center = new Vector3((minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2);
@@ -1,9 +1,9 @@
1
- import { type CameraMode, type CameraState, type CameraView, type GLRendererLike, type InteractiveStageOptions, type RenderTargetCanvas } from '@chestnutlabs/gcode-renderer-three';
1
+ import { type CameraMode, type CameraState, type CameraView, type GLRendererLike, type InteractiveStageOptions, type LoadProgress, type RenderTargetCanvas } from '@chestnutlabs/gcode-renderer-three';
2
2
  import type { Confidence } from '@chestnutlabs/toolpath-core';
3
3
  import { type ModelLoader, type ModelSourceInput } from './loaders.js';
4
4
  import { type ModelLimits } from './limits.js';
5
5
  import type { ModelBackground, PresentationView } from './model-renderer.js';
6
- import { type ModelBounds } from './scene-model.js';
6
+ import { type ModelBounds, type ModelPlateSummary } from './scene-model.js';
7
7
  export interface ModelViewerOptions {
8
8
  /** Loader registry (open `kind`). Default: `[stlLoader, threeMfLoader]`. */
9
9
  loaders?: readonly ModelLoader[];
@@ -21,6 +21,12 @@ export interface ModelViewerOptions {
21
21
  createRenderer?: (canvas: RenderTargetCanvas) => GLRendererLike;
22
22
  /** Injected orbit controls (tests). Default: three `OrbitControls` via the stage. */
23
23
  createControls?: NonNullable<InteractiveStageOptions['createControls']>;
24
+ /**
25
+ * Staged loading-progress callback (DD-024): typed `stage`/`done`/`total`/`unit` events (no human copy),
26
+ * each tagged with the load `generation` so a consumer drops events from a superseded `setSource`. The
27
+ * consumer owns all wording/i18n. Events from a superseded load are not emitted.
28
+ */
29
+ onProgress?: (progress: LoadProgress) => void;
24
30
  }
25
31
  /** What the source actually carried, surfaced on `ready` (never fabricated — DD-001). */
26
32
  export interface ModelReadyInfo {
@@ -38,6 +44,15 @@ export interface ModelReadyInfo {
38
44
  * (DD-022 Phase 2); reserved so the field is stable.
39
45
  */
40
46
  decimationApplied: number;
47
+ /**
48
+ * Declared plate structure (DD-025), present only when the source explicitly declares plates
49
+ * (`capabilities.plates === 'known'`). A consumer builds a plate selector from `plates.list`; absent for a
50
+ * single/plate-less source. `active` is the source's declared active plate id when present.
51
+ */
52
+ plates?: {
53
+ list: ModelPlateSummary[];
54
+ active?: number;
55
+ };
41
56
  }
42
57
  export type ModelViewerEvent = {
43
58
  type: 'ready';
@@ -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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"}
@@ -84,12 +84,23 @@ export function createModelViewer(canvas, options = {}) {
84
84
  }
85
85
  async function setSource(input) {
86
86
  const my = ++sourceToken;
87
+ // Progress is generation-scoped (DD-024 §4 D5): only emit while THIS load is current, so a superseded
88
+ // load can never advance the next load's progress. Typed fields only — no human copy.
89
+ const onProgress = options.onProgress;
90
+ const progress = (p) => {
91
+ if (onProgress !== undefined && my === sourceToken && !disposed)
92
+ onProgress({ ...p, generation: my });
93
+ };
87
94
  if (stage === null || disposed) {
88
95
  const message = disposed ? 'model viewer is disposed' : 'WebGL renderer unavailable';
89
96
  emit({ type: 'error', code: 'E_MODEL_RENDERER_UNAVAILABLE', message });
90
97
  throw new ModelParseError('E_MODEL_RENDERER_UNAVAILABLE', message);
91
98
  }
92
99
  try {
100
+ // Parse/resolve is a single opaque async step here (unzip + XML parse); its total is unknown ⇒ honest
101
+ // indeterminate, not a fabricated bar. Finer parse-stage counts are a later phase (threading into the
102
+ // loader/parser); Phase A closes the biggest gap — the model renderer had NO progress at all.
103
+ progress({ stage: 'parsing', indeterminate: true });
93
104
  const parsed = await resolveModelScene(input, loaders, limits, filamentPalette ? { filamentPalette } : undefined);
94
105
  const info = {
95
106
  objectCount: parsed.objects.length,
@@ -98,14 +109,19 @@ export function createModelViewer(canvas, options = {}) {
98
109
  instancedCount: sceneInstanceCount(parsed),
99
110
  decimationApplied: 1
100
111
  };
112
+ if (parsed.plates !== undefined)
113
+ info.plates = parsed.plates;
101
114
  // Last-wins: a newer setSource (or a dispose) superseded this one — discard without mutating.
102
115
  if (my !== sourceToken || disposed || stage === null)
103
116
  return info;
104
- content.setScene(parsed);
117
+ const total = parsed.objects.length;
118
+ progress({ stage: 'building-geometry', done: 0, total, unit: 'objects' });
119
+ content.setScene(parsed, (done) => progress({ stage: 'building-geometry', done, total, unit: 'objects' }));
105
120
  currentScene = parsed;
106
121
  const f = content.framing;
107
122
  if (f !== null)
108
123
  stage.frameTo(f.center, f.radius);
124
+ progress({ stage: 'ready' });
109
125
  emit({ type: 'ready', info });
110
126
  return info;
111
127
  }
@@ -13,7 +13,7 @@
13
13
  import type { Confidence } from '@chestnutlabs/toolpath-core';
14
14
  import type { GLRendererLike, RenderTargetCanvas } from '@chestnutlabs/gcode-renderer-three';
15
15
  import { type ModelBackground, type PresentationView } from './model-renderer.js';
16
- import { type ModelScene } from './scene-model.js';
16
+ import { type ModelPlateSummary, type ModelScene } from './scene-model.js';
17
17
  import type { ModelLimits } from './limits.js';
18
18
  /**
19
19
  * A source model: STL bytes, 3MF bytes, or a pre-built `ModelScene`. This concrete union is the
@@ -64,6 +64,15 @@ export interface RenderModelStillResult {
64
64
  * size" the same way. Always 1 until model LOD lands (DD-022 Phase 2); reserved so the field is stable.
65
65
  */
66
66
  decimationApplied: number;
67
+ /**
68
+ * Declared plate structure (DD-025), present only when the source explicitly declares plates
69
+ * (`capabilities.plates === 'known'`); absent for a single/plate-less source. `active` is the source's
70
+ * declared active plate id when present.
71
+ */
72
+ plates?: {
73
+ list: ModelPlateSummary[];
74
+ active?: number;
75
+ };
67
76
  /** Stable identity for caching: `hash(source) + options + envId`. */
68
77
  cacheKey: string;
69
78
  }
@@ -1 +1 @@
1
- {"version":3,"file":"render-model-still.d.ts","sourceRoot":"","sources":["../src/render-model-still.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAC7F,OAAO,EAAiB,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEjG,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;;GAIG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,UAAU,GAAG,WAAW,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,UAAU,GAAG,WAAW,CAAA;CAAE,GAChD,UAAU,CAAC;AAEf,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mGAAmG;IACnG,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,qCAAqC;IACrC,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,cAAc,CAAC;IAChE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;OAIG;IACH,eAAe,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;IAClD,6FAA6F;IAC7F,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,sGAAsG;IACtG,SAAS,EAAE,UAAU,CAAC;IACtB;;;OAGG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAkBD;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,sBAAsB,CAAC,CAmCjC"}
1
+ {"version":3,"file":"render-model-still.d.ts","sourceRoot":"","sources":["../src/render-model-still.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAC7F,OAAO,EAAiB,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEjG,OAAO,EAAsB,KAAK,iBAAiB,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE/F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;;GAIG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,UAAU,GAAG,WAAW,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,UAAU,GAAG,WAAW,CAAA;CAAE,GAChD,UAAU,CAAC;AAEf,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mGAAmG;IACnG,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,qCAAqC;IACrC,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,cAAc,CAAC;IAChE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;OAIG;IACH,eAAe,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;IAClD,6FAA6F;IAC7F,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,sGAAsG;IACtG,SAAS,EAAE,UAAU,CAAC;IACtB;;;OAGG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,iBAAiB,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxD,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAkBD;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,sBAAsB,CAAC,CAoCjC"}
@@ -45,6 +45,7 @@ export async function renderModelStill(source, options) {
45
45
  materials: scene.capabilities.materials,
46
46
  instancedCount: sceneInstanceCount(scene),
47
47
  decimationApplied: 1,
48
+ ...(scene.plates !== undefined ? { plates: scene.plates } : {}),
48
49
  cacheKey
49
50
  };
50
51
  }
@@ -60,13 +60,48 @@ export interface ModelObject {
60
60
  * transforms (GPU instancing), so memory scales with unique geometry, not copy count.
61
61
  */
62
62
  instances?: Mat4[];
63
+ /**
64
+ * Per-**placement** plate membership (DD-025): `plateIds[i]` is the {@link ModelPlateSummary.id} (plater
65
+ * id) of placement `i` — aligned with {@link instances} when present, else index 0 is the single placement at
66
+ * {@link transform}. Plate membership lives on the placement, not the master, because the same reused
67
+ * master can be instantiated on more than one plate. Absent ⇒ the source declared no plate structure
68
+ * (`capabilities.plates: 'unavailable'`), never fabricated.
69
+ */
70
+ plateIds?: number[];
63
71
  /** Omitted ⇒ no source material ⇒ neutral default render + `capabilities.materials: 'unavailable'`. */
64
72
  material?: ModelMaterial;
65
73
  }
74
+ /**
75
+ * One declared plate in a multi-plate source (DD-025), paralleling the toolpath-side `PlateSummary`.
76
+ * Present only when the source **explicitly** declares plate structure (Bambu/Orca
77
+ * `Metadata/model_settings.config`); an undeclared/implicit single plate is not summarized here.
78
+ */
79
+ export interface ModelPlateSummary {
80
+ /** The source's plater id (`plater_id`), stable within the scene. */
81
+ id: number;
82
+ /** Source-declared plate name when present and non-empty. */
83
+ name?: string;
84
+ /** Distinct master objects referenced by this plate's placements (a master shared across plates counts in each). */
85
+ objectCount: number;
86
+ /** Placements assigned to this plate. */
87
+ instanceCount: number;
88
+ /** Axis-aligned bounds of this plate's placements, in scene units. */
89
+ bounds: ModelBounds;
90
+ }
66
91
  /** A presentation scene: one or more objects plus capability honesty about what the source carried. */
67
92
  export interface ModelScene {
68
93
  objects: ModelObject[];
69
94
  bounds: ModelBounds;
95
+ /**
96
+ * Declared plate structure (DD-025). Present only when the source **explicitly** declares plates; a
97
+ * consumer shows a plate selector only when `capabilities.plates === 'known'`. `active` is the source's
98
+ * active/default plate id when declared (a consumer's natural initial selection — the renderer never
99
+ * forces all-plates).
100
+ */
101
+ plates?: {
102
+ list: ModelPlateSummary[];
103
+ active?: number;
104
+ };
70
105
  /** What the source actually carried (DD-001 ethos — never fabricated). */
71
106
  capabilities: {
72
107
  /** `'known'` when the source assigned colors/materials, else `'unavailable'`. */
@@ -78,6 +113,9 @@ export interface ModelScene {
78
113
  /** `'known'` when the source reused geometry via instances (more placements than unique masters), so
79
114
  * the scene preserved it as GPU instancing (DD-022); `'unavailable'` otherwise. */
80
115
  instanced: Confidence;
116
+ /** `'known'` when the source **explicitly** declares plate structure (incl. an explicit single plate);
117
+ * `'unavailable'` for undeclared/implicit plate structure (DD-025). */
118
+ plates: Confidence;
81
119
  };
82
120
  }
83
121
  /** The identity transform (column-major), for single-object sources. */
@@ -1 +1 @@
1
- {"version":3,"file":"scene-model.d.ts","sourceRoot":"","sources":["../src/scene-model.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,0BAA0B;AAC1B,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAE3C,iGAAiG;AACjG,MAAM,MAAM,IAAI,GAAG,SAAS,MAAM,EAAE,CAAC;AAErC,4FAA4F;AAC5F,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,GAAG,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED,wGAAwG;AACxG,MAAM,WAAW,YAAY;IAC3B,kDAAkD;IAClD,SAAS,EAAE,YAAY,CAAC;IACxB,kFAAkF;IAClF,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,iEAAiE;IACjE,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB;;;;OAIG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED,gGAAgG;AAChG,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,KAAK,CAAC,EAAE,GAAG,CAAC;CACb;AAED,wCAAwC;AACxC,MAAM,WAAW,WAAW;IAC1B,qEAAqE;IACrE,EAAE,EAAE,MAAM,CAAC;IACX,gDAAgD;IAChD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,QAAQ,EAAE,YAAY,CAAC;IACvB,iGAAiG;IACjG,SAAS,EAAE,IAAI,CAAC;IAChB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,IAAI,EAAE,CAAC;IACnB,uGAAuG;IACvG,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B;AAED,uGAAuG;AACvG,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,MAAM,EAAE,WAAW,CAAC;IACpB,0EAA0E;IAC1E,YAAY,EAAE;QACZ,iFAAiF;QACjF,SAAS,EAAE,UAAU,CAAC;QACtB,sFAAsF;QACtF,UAAU,EAAE,UAAU,CAAC;QACvB,+DAA+D;QAC/D,WAAW,EAAE,UAAU,CAAC;QACxB;4FACoF;QACpF,SAAS,EAAE,UAAU,CAAC;KACvB,CAAC;CACH;AAED,wEAAwE;AACxE,eAAO,MAAM,aAAa,EAAE,IAAuD,CAAC;AAEpF;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAI5D"}
1
+ {"version":3,"file":"scene-model.d.ts","sourceRoot":"","sources":["../src/scene-model.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,0BAA0B;AAC1B,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAE3C,iGAAiG;AACjG,MAAM,MAAM,IAAI,GAAG,SAAS,MAAM,EAAE,CAAC;AAErC,4FAA4F;AAC5F,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,GAAG,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED,wGAAwG;AACxG,MAAM,WAAW,YAAY;IAC3B,kDAAkD;IAClD,SAAS,EAAE,YAAY,CAAC;IACxB,kFAAkF;IAClF,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,iEAAiE;IACjE,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB;;;;OAIG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED,gGAAgG;AAChG,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,KAAK,CAAC,EAAE,GAAG,CAAC;CACb;AAED,wCAAwC;AACxC,MAAM,WAAW,WAAW;IAC1B,qEAAqE;IACrE,EAAE,EAAE,MAAM,CAAC;IACX,gDAAgD;IAChD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,QAAQ,EAAE,YAAY,CAAC;IACvB,iGAAiG;IACjG,SAAS,EAAE,IAAI,CAAC;IAChB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,IAAI,EAAE,CAAC;IACnB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,uGAAuG;IACvG,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,qEAAqE;IACrE,EAAE,EAAE,MAAM,CAAC;IACX,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oHAAoH;IACpH,WAAW,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,uGAAuG;AACvG,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,MAAM,EAAE,WAAW,CAAC;IACpB;;;;;OAKG;IACH,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxD,0EAA0E;IAC1E,YAAY,EAAE;QACZ,iFAAiF;QACjF,SAAS,EAAE,UAAU,CAAC;QACtB,sFAAsF;QACtF,UAAU,EAAE,UAAU,CAAC;QACvB,+DAA+D;QAC/D,WAAW,EAAE,UAAU,CAAC;QACxB;4FACoF;QACpF,SAAS,EAAE,UAAU,CAAC;QACtB;gFACwE;QACxE,MAAM,EAAE,UAAU,CAAC;KACpB,CAAC;CACH;AAED,wEAAwE;AACxE,eAAO,MAAM,aAAa,EAAE,IAAuD,CAAC;AAEpF;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAI5D"}
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,CAwD3F"}
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"}
package/dist/stl.js CHANGED
@@ -65,7 +65,8 @@ export function parseStl(source, limits) {
65
65
  materials: 'unavailable',
66
66
  transforms: 'unavailable',
67
67
  multiObject: 'unavailable',
68
- instanced: 'unavailable'
68
+ instanced: 'unavailable',
69
+ plates: 'unavailable'
69
70
  }
70
71
  };
71
72
  }
@@ -9,5 +9,34 @@ export interface Parse3mfOptions {
9
9
  */
10
10
  filamentPalette?: readonly (string | undefined)[];
11
11
  }
12
+ /** Declared plate structure parsed from `Metadata/model_settings.config` (Bambu/Orca) — DD-025. */
13
+ interface PlateConfig {
14
+ /** `${object_id}#${instance_id}` → plater_id, mapping each declared model-instance to its plate. */
15
+ membership: Map<string, number>;
16
+ /** plater_id → declared plate name (only non-empty names). */
17
+ names: Map<number, string>;
18
+ /** Declared plater_ids in source order. */
19
+ order: number[];
20
+ }
21
+ /**
22
+ * Parse the `<plate>` elements of a Bambu/Orca `Metadata/model_settings.config` (DD-025). Each `<plate>`
23
+ * carries a `plater_id` and a list of `<model_instance>` (`object_id` + `instance_id`) assigning the Nth
24
+ * placement of a build-item object to that plate. Clean-room from the observed format (parity with the
25
+ * RR-005 `paint_color` approach); tolerant scan — a malformed/absent config yields an empty result so the
26
+ * scene honestly reports `capabilities.plates: 'unavailable'` rather than a fabricated split.
27
+ *
28
+ * Returns `null` when no `<plate>` is declared (⇒ implicit single plate, `'unavailable'`).
29
+ */
30
+ export declare function parsePlateConfig(xml: string): PlateConfig | null;
31
+ /**
32
+ * Parse per-object / per-part extruder assignments from `Metadata/model_settings.config` (Bambu/Orca) — a
33
+ * source-model colour convention where each object/part is solid-coloured by its assigned filament
34
+ * (`<object id="X">…<metadata key="extruder" value="N"/>`, and the same on nested `<part id="Y">`),
35
+ * 1-based into the `filament_colour` palette. Returns a map of id → 1-based extruder. Each object/part's own
36
+ * extruder is the FIRST `key="extruder"` in the window from its header to the next object/part header (so a
37
+ * part's extruder overrides its parent object's default). Empty map ⇒ no assignments declared.
38
+ */
39
+ export declare function parseObjectExtruders(xml: string): Map<string, number>;
12
40
  export declare function parse3mf(source: Uint8Array | ArrayBuffer, limits?: ModelLimits, opts?: Parse3mfOptions): Promise<ModelScene>;
41
+ export {};
13
42
  //# sourceMappingURL=three-mf.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"three-mf.d.ts","sourceRoot":"","sources":["../src/three-mf.ts"],"names":[],"mappings":"AAqBA,OAAO,EAML,KAAK,UAAU,EAEhB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAkC,KAAK,WAAW,EAAuB,MAAM,aAAa,CAAC;AAkbpG,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,eAAe,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;CACnD;AA0FD,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,UAAU,GAAG,WAAW,EAChC,MAAM,CAAC,EAAE,WAAW,EACpB,IAAI,CAAC,EAAE,eAAe,GACrB,OAAO,CAAC,UAAU,CAAC,CAyKrB"}
1
+ {"version":3,"file":"three-mf.d.ts","sourceRoot":"","sources":["../src/three-mf.ts"],"names":[],"mappings":"AAqBA,OAAO,EAOL,KAAK,UAAU,EAEhB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAkC,KAAK,WAAW,EAAuB,MAAM,aAAa,CAAC;AAofpG,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,eAAe,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;CACnD;AAKD,mGAAmG;AACnG,UAAU,WAAW;IACnB,oGAAoG;IACpG,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,8DAA8D;IAC9D,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B,2CAA2C;IAC3C,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAuBhE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAarE;AAyGD,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,UAAU,GAAG,WAAW,EAChC,MAAM,CAAC,EAAE,WAAW,EACpB,IAAI,CAAC,EAAE,eAAe,GACrB,OAAO,CAAC,UAAU,CAAC,CAsNrB"}
package/dist/three-mf.js CHANGED
@@ -315,6 +315,29 @@ function parseModelPart(xml, lim) {
315
315
  }
316
316
  return { objects, palettes, unitScale, items };
317
317
  }
318
+ /**
319
+ * Record a placement's plate membership (DD-025): bump the plate's instance count, note the master it
320
+ * references (distinct-object count), and expand the plate's bounds by this placement's transformed AABB.
321
+ * A no-op when no plate is active (source declares no plates).
322
+ */
323
+ function recordPlacementPlate(st, masterKey, localMin, localMax, affine) {
324
+ const p = st.currentPlateId;
325
+ if (p === undefined)
326
+ return;
327
+ st.plateInstanceCount.set(p, (st.plateInstanceCount.get(p) ?? 0) + 1);
328
+ let objs = st.plateObjects.get(p);
329
+ if (objs === undefined) {
330
+ objs = new Set();
331
+ st.plateObjects.set(p, objs);
332
+ }
333
+ objs.add(masterKey);
334
+ let b = st.plateBounds.get(p);
335
+ if (b === undefined) {
336
+ b = { min: [Infinity, Infinity, Infinity], max: [-Infinity, -Infinity, -Infinity] };
337
+ st.plateBounds.set(p, b);
338
+ }
339
+ expandBoundsByBox(b, localMin, localMax, affine);
340
+ }
318
341
  /**
319
342
  * Place one mesh object (DD-022 instancing). The FIRST time a master `${partKey}#${objectid}` is reached
320
343
  * its geometry is built once in LOCAL space (unit-scaled, un-baked); every reach — first and repeats —
@@ -327,8 +350,11 @@ function placeMaster(obj, part, matrix, key, st, lim) {
327
350
  const existing = st.masters.get(key);
328
351
  if (existing !== undefined) {
329
352
  existing.instances.push(mat4);
353
+ if (st.currentPlateId !== undefined)
354
+ existing.plates.push(st.currentPlateId);
330
355
  st.totalInstances++;
331
356
  expandBoundsByBox(st, existing.localMin, existing.localMax, affine);
357
+ recordPlacementPlate(st, key, existing.localMin, existing.localMax, affine);
332
358
  return;
333
359
  }
334
360
  const triCount = obj.tris.length / 3;
@@ -403,12 +429,96 @@ function placeMaster(obj, part, matrix, key, st, lim) {
403
429
  const object = { id: key, geometry, transform: mat4 };
404
430
  if (obj.name !== undefined)
405
431
  object.name = obj.name;
406
- st.masters.set(key, { object, instances: [mat4], localMin, localMax });
432
+ // Bambu/Orca per-object/part extruder solid colour (maintainer-approved): when the object declares no
433
+ // basematerials/colorgroup/paint colour, but the source assigns it an extruder and the project's
434
+ // `filament_colour` palette has that slot, colour the whole object with `palette[extruder - 1]` (a solid
435
+ // material colour). Honesty preserved: only when the source actually declares the mapping AND the palette
436
+ // resolves — otherwise the object stays neutral (`materials: 'unavailable'`). Never guessed.
437
+ if (!objHasColor && st.paintPalette.length > 0) {
438
+ const oid = key.substring(key.lastIndexOf('#') + 1);
439
+ const extruder = st.objectExtruders.get(oid);
440
+ if (extruder !== undefined) {
441
+ const col = st.paintPalette[extruder - 1];
442
+ if (col !== undefined) {
443
+ object.material = { color: col };
444
+ st.anyColor = true;
445
+ }
446
+ }
447
+ }
448
+ st.masters.set(key, {
449
+ object,
450
+ instances: [mat4],
451
+ plates: st.currentPlateId !== undefined ? [st.currentPlateId] : [],
452
+ localMin,
453
+ localMax
454
+ });
407
455
  st.built.push(object);
408
456
  st.totalInstances++;
409
457
  expandBoundsByBox(st, localMin, localMax, affine);
458
+ recordPlacementPlate(st, key, localMin, localMax, affine);
410
459
  }
411
460
  const hexPaletteToLinear = (hexes) => hexes.map((hex) => (hex !== undefined ? (srgbHexToLinear(hex) ?? [0.8, 0.8, 0.82]) : [0.8, 0.8, 0.82]));
461
+ /**
462
+ * Parse the `<plate>` elements of a Bambu/Orca `Metadata/model_settings.config` (DD-025). Each `<plate>`
463
+ * carries a `plater_id` and a list of `<model_instance>` (`object_id` + `instance_id`) assigning the Nth
464
+ * placement of a build-item object to that plate. Clean-room from the observed format (parity with the
465
+ * RR-005 `paint_color` approach); tolerant scan — a malformed/absent config yields an empty result so the
466
+ * scene honestly reports `capabilities.plates: 'unavailable'` rather than a fabricated split.
467
+ *
468
+ * Returns `null` when no `<plate>` is declared (⇒ implicit single plate, `'unavailable'`).
469
+ */
470
+ export function parsePlateConfig(xml) {
471
+ const plateBlocks = xml.match(/<plate\b[\s\S]*?<\/plate>/gi);
472
+ if (plateBlocks === null || plateBlocks.length === 0)
473
+ return null;
474
+ const membership = new Map();
475
+ const names = new Map();
476
+ const order = [];
477
+ for (const block of plateBlocks) {
478
+ const idM = /key="plater_id"\s+value="(\d+)"/i.exec(block);
479
+ if (idM === null)
480
+ continue;
481
+ const plateId = parseInt(idM[1], 10);
482
+ if (!Number.isFinite(plateId))
483
+ continue;
484
+ order.push(plateId);
485
+ const nameM = /key="plater_name"\s+value="([^"]*)"/i.exec(block);
486
+ if (nameM !== null && nameM[1].length > 0)
487
+ names.set(plateId, nameM[1]);
488
+ // Each <model_instance> declares object_id + instance_id (both required to key a placement).
489
+ for (const inst of block.match(/<model_instance\b[\s\S]*?<\/model_instance>/gi) ?? []) {
490
+ const objM = /key="object_id"\s+value="(\d+)"/i.exec(inst);
491
+ const insM = /key="instance_id"\s+value="(\d+)"/i.exec(inst);
492
+ if (objM === null || insM === null)
493
+ continue;
494
+ membership.set(`${objM[1]}#${insM[1]}`, plateId);
495
+ }
496
+ }
497
+ return order.length > 0 ? { membership, names, order } : null;
498
+ }
499
+ /**
500
+ * Parse per-object / per-part extruder assignments from `Metadata/model_settings.config` (Bambu/Orca) — a
501
+ * source-model colour convention where each object/part is solid-coloured by its assigned filament
502
+ * (`<object id="X">…<metadata key="extruder" value="N"/>`, and the same on nested `<part id="Y">`),
503
+ * 1-based into the `filament_colour` palette. Returns a map of id → 1-based extruder. Each object/part's own
504
+ * extruder is the FIRST `key="extruder"` in the window from its header to the next object/part header (so a
505
+ * part's extruder overrides its parent object's default). Empty map ⇒ no assignments declared.
506
+ */
507
+ export function parseObjectExtruders(xml) {
508
+ const map = new Map();
509
+ const headerRe = /<(?:object|part)\s+id="(\d+)"[^>]*>([\s\S]*?)(?=<(?:object|part)\s+id="|<\/config>|$)/g;
510
+ let m;
511
+ while ((m = headerRe.exec(xml)) !== null) {
512
+ const id = m[1];
513
+ const ext = /key="extruder"\s+value="(\d+)"/i.exec(m[2]);
514
+ if (ext !== null) {
515
+ const n = parseInt(ext[1], 10);
516
+ if (Number.isFinite(n) && n >= 1)
517
+ map.set(id, n);
518
+ }
519
+ }
520
+ return map;
521
+ }
412
522
  /**
413
523
  * Conservative UPPER bound on the XML bytes a single 3MF triangle (plus its amortized share of a shared
414
524
  * vertex) occupies — used to turn an external part's uncompressed byte size into a LOWER bound on its
@@ -428,17 +538,27 @@ const EST_REJECT_MARGIN = 2;
428
538
  /**
429
539
  * Fast structural cost estimate (DD-022 Phase 0), run after the (tiny, for production-extension files)
430
540
  * main part is parsed but BEFORE any large external geometry part is decompressed. Walks the build-item /
431
- * component tree counting instance placements and a lower-bound triangle estimate:
432
- * - a mesh object's own triangles are counted exactly (already parsed);
433
- * - an external `p:path` component is estimated from its ZIP-directory uncompressed size / the
434
- * conservative bytes-per-triangle bound, and treated as a leaf (not recursed) — a safe under-count.
435
- * Throws {@link ModelParseError} for a clear instance-bomb / oversize file so it rejects in sub-second
436
- * time instead of ~10 s of decompress-and-bake. Files near the budget do not reject here — the exact
437
- * per-triangle limit in the real parse still applies.
541
+ * component tree counting **placements** (all of them) and a lower-bound **unique**-triangle estimate:
542
+ * - a mesh master's triangles are counted **once per unique master** (already parsed), NOT per placement;
543
+ * - an external `p:path` part is estimated from its ZIP-directory uncompressed size / the conservative
544
+ * bytes-per-triangle bound, counted **once per unique path** (a safe under-count), treated as a leaf.
545
+ * Because instancing renders each master once (DD-022 Phase 1), the triangle estimate must be UNIQUE — a
546
+ * full-sheet plate of ~40 copies costs its ~1 master, not ~40× baked, so it is NOT rejected here. The
547
+ * placement count still guards an instance bomb (`maxInstances`). Throws {@link ModelParseError} for a
548
+ * clear instance-bomb / oversize file so it rejects in sub-second time instead of ~10 s of decompress-and-
549
+ * bake; files near the budget fall through to the exact per-triangle parse.
438
550
  */
439
551
  function estimateSceneCost(mainPart, entryByName, lim) {
440
552
  let instanceCount = 0;
441
553
  let estTriangles = 0;
554
+ // Unique masters already counted toward the triangle estimate (keyed by objectid / external path), so a
555
+ // reused master is counted once — matching the instanced render, not the old per-placement bake.
556
+ const countedMasters = new Set();
557
+ const checkInstances = () => {
558
+ if (instanceCount > lim.maxInstances) {
559
+ throw new ModelParseError('E_MODEL_TOO_MANY_INSTANCES', `3MF declares more than ${lim.maxInstances} instance placements`);
560
+ }
561
+ };
442
562
  const visit = (part, objectid, depth) => {
443
563
  if (depth > MAX_DEPTH)
444
564
  return;
@@ -447,23 +567,30 @@ function estimateSceneCost(mainPart, entryByName, lim) {
447
567
  return;
448
568
  if (obj.tris.length > 0) {
449
569
  instanceCount++;
450
- estTriangles += obj.tris.length / 3;
570
+ const key = `local:${objectid}`;
571
+ if (!countedMasters.has(key)) {
572
+ countedMasters.add(key);
573
+ estTriangles += obj.tris.length / 3;
574
+ }
575
+ checkInstances();
451
576
  }
452
577
  for (const comp of obj.components) {
453
578
  if (comp.path !== undefined) {
454
579
  // External production-extension part: estimate from its directory size WITHOUT decompressing it.
455
- const entry = entryByName(comp.path.replace(/^\//, '').toLowerCase());
580
+ const norm = comp.path.replace(/^\//, '').toLowerCase();
581
+ const entry = entryByName(norm);
456
582
  if (entry !== undefined) {
457
583
  instanceCount++;
458
- estTriangles += entry.uncompressedSize / EST_MAX_BYTES_PER_TRIANGLE;
584
+ if (!countedMasters.has(norm)) {
585
+ countedMasters.add(norm);
586
+ estTriangles += entry.uncompressedSize / EST_MAX_BYTES_PER_TRIANGLE;
587
+ }
588
+ checkInstances();
459
589
  }
460
590
  }
461
591
  else {
462
592
  visit(part, comp.objectid, depth + 1); // local sub-object (already-parsed geometry)
463
593
  }
464
- if (instanceCount > lim.maxInstances) {
465
- throw new ModelParseError('E_MODEL_TOO_MANY_INSTANCES', `3MF declares more than ${lim.maxInstances} instance placements`);
466
- }
467
594
  }
468
595
  };
469
596
  const roots = mainPart.items.length > 0
@@ -562,8 +689,11 @@ export async function parse3mf(source, limits, opts) {
562
689
  }
563
690
  }
564
691
  }
565
- // Default extruder (1-based) for a painted mesh's unpainted facets; from `model_settings.config`, else 1.
692
+ // `model_settings.config` (Bambu/Orca) carries both the default extruder for unpainted facets AND the
693
+ // declared plate structure (DD-025) — read once.
566
694
  let defaultExtruderIdx = 0;
695
+ let plateConfig = null;
696
+ let objectExtruders = new Map();
567
697
  const modelSettingsEntry = entryByName('metadata/model_settings.config');
568
698
  if (modelSettingsEntry !== undefined) {
569
699
  try {
@@ -572,9 +702,13 @@ export async function parse3mf(source, limits, opts) {
572
702
  const n = m !== null ? parseInt(m[1], 10) : NaN;
573
703
  if (Number.isFinite(n) && n >= 1)
574
704
  defaultExtruderIdx = n - 1;
705
+ plateConfig = parsePlateConfig(xml);
706
+ objectExtruders = parseObjectExtruders(xml);
575
707
  }
576
708
  catch {
577
709
  defaultExtruderIdx = 0;
710
+ plateConfig = null;
711
+ objectExtruders = new Map();
578
712
  }
579
713
  }
580
714
  const st = {
@@ -588,7 +722,13 @@ export async function parse3mf(source, limits, opts) {
588
722
  min: [Infinity, Infinity, Infinity],
589
723
  max: [-Infinity, -Infinity, -Infinity],
590
724
  totalTris: 0,
591
- totalInstances: 0
725
+ totalInstances: 0,
726
+ plateConfig,
727
+ objectExtruders,
728
+ currentPlateId: undefined,
729
+ plateBounds: new Map(),
730
+ plateInstanceCount: new Map(),
731
+ plateObjects: new Map()
592
732
  };
593
733
  // Resolve an object (mesh and/or components) within `part`, folding component transforms into the
594
734
  // running placement `matrix` (DD-022) instead of baking vertices. `partKey` identifies the part so the
@@ -618,21 +758,33 @@ export async function parse3mf(source, limits, opts) {
618
758
  const roots = mainPart.items.length > 0
619
759
  ? mainPart.items
620
760
  : [...mainPart.objects.keys()].map((objectid) => ({ objectid, transform: null }));
761
+ // Per-object-id ordinal so the Nth build item of object X maps to plate `X#N` (DD-025).
762
+ const rootOrdinals = new Map();
621
763
  for (const inst of roots) {
622
764
  if (inst.transform !== null)
623
765
  st.anyTransform = true;
766
+ if (st.plateConfig !== null) {
767
+ const ord = rootOrdinals.get(inst.objectid) ?? 0;
768
+ rootOrdinals.set(inst.objectid, ord + 1);
769
+ // Unmapped placement (declared plates but this one not listed) → the first declared plate, so
770
+ // plateIds stay complete and aligned; never fabricated silently beyond that fallback.
771
+ st.currentPlateId = st.plateConfig.membership.get(`${inst.objectid}#${ord}`) ?? st.plateConfig.order[0];
772
+ }
624
773
  await resolve(mainPart, inst.objectid, inst.transform, mainKey, 0);
625
774
  }
626
775
  if (st.built.length === 0)
627
776
  throw new ModelParseError('E_MODEL_EMPTY', '3MF contains no renderable geometry');
628
777
  // Attach the instance list to any master reached more than once (a single placement keeps its
629
- // `transform` and omits `instances`, per the contract).
778
+ // `transform` and omits `instances`, per the contract). Plate ids (DD-025) attach per placement,
779
+ // aligned with `instances` when present, else the single placement.
630
780
  for (const rec of st.masters.values()) {
631
781
  if (rec.instances.length > 1)
632
782
  rec.object.instances = rec.instances;
783
+ if (rec.plates.length > 0)
784
+ rec.object.plateIds = rec.plates;
633
785
  }
634
786
  const bounds = { min: st.min, max: st.max };
635
- return {
787
+ const scene = {
636
788
  objects: st.built,
637
789
  bounds,
638
790
  capabilities: {
@@ -641,7 +793,27 @@ export async function parse3mf(source, limits, opts) {
641
793
  transforms: st.anyTransform ? 'known' : 'unavailable',
642
794
  multiObject: st.built.length > 1 ? 'known' : 'unavailable',
643
795
  // Instancing preserved when there are more placements than unique masters.
644
- instanced: st.totalInstances > st.masters.size ? 'known' : 'unavailable'
796
+ instanced: st.totalInstances > st.masters.size ? 'known' : 'unavailable',
797
+ // Plate structure is `known` only when the source explicitly declared plates (DD-025).
798
+ plates: st.plateConfig !== null ? 'known' : 'unavailable'
645
799
  }
646
800
  };
801
+ const pc = st.plateConfig;
802
+ if (pc !== null) {
803
+ const list = pc.order.map((id) => {
804
+ const b = st.plateBounds.get(id);
805
+ const summary = {
806
+ id,
807
+ objectCount: st.plateObjects.get(id)?.size ?? 0,
808
+ instanceCount: st.plateInstanceCount.get(id) ?? 0,
809
+ bounds: b !== undefined ? { min: b.min, max: b.max } : { min: [0, 0, 0], max: [0, 0, 0] }
810
+ };
811
+ const name = pc.names.get(id);
812
+ if (name !== undefined)
813
+ summary.name = name;
814
+ return summary;
815
+ });
816
+ scene.plates = { list };
817
+ }
818
+ return scene;
647
819
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chestnutlabs/gcode-model-renderer",
3
- "version": "0.11.0",
3
+ "version": "0.13.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",
@@ -51,9 +51,9 @@
51
51
  "test": "vitest run"
52
52
  },
53
53
  "dependencies": {
54
- "@chestnutlabs/gcode-containers": "0.11.0",
55
- "@chestnutlabs/gcode-renderer-three": "0.11.0",
56
- "@chestnutlabs/toolpath-core": "0.11.0"
54
+ "@chestnutlabs/gcode-containers": "0.13.0",
55
+ "@chestnutlabs/gcode-renderer-three": "0.13.0",
56
+ "@chestnutlabs/toolpath-core": "0.13.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/three": "0.178.0"