@chestnutlabs/gcode-model-renderer 0.12.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;AA4GD,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
@@ -579,8 +689,11 @@ export async function parse3mf(source, limits, opts) {
579
689
  }
580
690
  }
581
691
  }
582
- // 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.
583
694
  let defaultExtruderIdx = 0;
695
+ let plateConfig = null;
696
+ let objectExtruders = new Map();
584
697
  const modelSettingsEntry = entryByName('metadata/model_settings.config');
585
698
  if (modelSettingsEntry !== undefined) {
586
699
  try {
@@ -589,9 +702,13 @@ export async function parse3mf(source, limits, opts) {
589
702
  const n = m !== null ? parseInt(m[1], 10) : NaN;
590
703
  if (Number.isFinite(n) && n >= 1)
591
704
  defaultExtruderIdx = n - 1;
705
+ plateConfig = parsePlateConfig(xml);
706
+ objectExtruders = parseObjectExtruders(xml);
592
707
  }
593
708
  catch {
594
709
  defaultExtruderIdx = 0;
710
+ plateConfig = null;
711
+ objectExtruders = new Map();
595
712
  }
596
713
  }
597
714
  const st = {
@@ -605,7 +722,13 @@ export async function parse3mf(source, limits, opts) {
605
722
  min: [Infinity, Infinity, Infinity],
606
723
  max: [-Infinity, -Infinity, -Infinity],
607
724
  totalTris: 0,
608
- totalInstances: 0
725
+ totalInstances: 0,
726
+ plateConfig,
727
+ objectExtruders,
728
+ currentPlateId: undefined,
729
+ plateBounds: new Map(),
730
+ plateInstanceCount: new Map(),
731
+ plateObjects: new Map()
609
732
  };
610
733
  // Resolve an object (mesh and/or components) within `part`, folding component transforms into the
611
734
  // running placement `matrix` (DD-022) instead of baking vertices. `partKey` identifies the part so the
@@ -635,21 +758,33 @@ export async function parse3mf(source, limits, opts) {
635
758
  const roots = mainPart.items.length > 0
636
759
  ? mainPart.items
637
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();
638
763
  for (const inst of roots) {
639
764
  if (inst.transform !== null)
640
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
+ }
641
773
  await resolve(mainPart, inst.objectid, inst.transform, mainKey, 0);
642
774
  }
643
775
  if (st.built.length === 0)
644
776
  throw new ModelParseError('E_MODEL_EMPTY', '3MF contains no renderable geometry');
645
777
  // Attach the instance list to any master reached more than once (a single placement keeps its
646
- // `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.
647
780
  for (const rec of st.masters.values()) {
648
781
  if (rec.instances.length > 1)
649
782
  rec.object.instances = rec.instances;
783
+ if (rec.plates.length > 0)
784
+ rec.object.plateIds = rec.plates;
650
785
  }
651
786
  const bounds = { min: st.min, max: st.max };
652
- return {
787
+ const scene = {
653
788
  objects: st.built,
654
789
  bounds,
655
790
  capabilities: {
@@ -658,7 +793,27 @@ export async function parse3mf(source, limits, opts) {
658
793
  transforms: st.anyTransform ? 'known' : 'unavailable',
659
794
  multiObject: st.built.length > 1 ? 'known' : 'unavailable',
660
795
  // Instancing preserved when there are more placements than unique masters.
661
- 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'
662
799
  }
663
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;
664
819
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chestnutlabs/gcode-model-renderer",
3
- "version": "0.12.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.12.0",
55
- "@chestnutlabs/gcode-renderer-three": "0.12.0",
56
- "@chestnutlabs/toolpath-core": "0.12.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"