@chestnutlabs/gcode-model-renderer 0.6.0 → 0.7.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/three-mf.d.ts +0 -1
- package/dist/three-mf.d.ts.map +1 -1
- package/dist/three-mf.js +202 -139
- package/package.json +4 -4
package/dist/three-mf.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type ModelScene } from './scene-model.js';
|
|
2
2
|
import { type ModelLimits } from './limits.js';
|
|
3
|
-
/** Parse 3MF bytes into a {@link ModelScene}. Rejects with {@link ModelParseError} on failure. */
|
|
4
3
|
export declare function parse3mf(source: Uint8Array | ArrayBuffer, limits?: ModelLimits): Promise<ModelScene>;
|
|
5
4
|
//# sourceMappingURL=three-mf.d.ts.map
|
package/dist/three-mf.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"three-mf.d.ts","sourceRoot":"","sources":["../src/three-mf.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"three-mf.d.ts","sourceRoot":"","sources":["../src/three-mf.ts"],"names":[],"mappings":"AAoBA,OAAO,EAKL,KAAK,UAAU,EAEhB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAkC,KAAK,WAAW,EAAuB,MAAM,aAAa,CAAC;AA8PpG,wBAAsB,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,WAAW,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAkH1G"}
|
package/dist/three-mf.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 3MF → {@link ModelScene} (DD-018 Phase 2). Multi-object,
|
|
3
|
-
* build-item transforms
|
|
2
|
+
* 3MF → {@link ModelScene} (DD-018 Phase 2, + production-extension follow-up). Multi-object,
|
|
3
|
+
* per-object/per-triangle **solid** colors, build-item transforms, and the **3MF Production Extension**
|
|
4
|
+
* (Bambu/MakerWorld default): `<components>` whose `<component p:path=…>` reference external
|
|
5
|
+
* `/3D/Objects/*.model` parts. A multicolor model becomes a multicolor thumbnail without slicing.
|
|
4
6
|
*
|
|
5
7
|
* 3MF is an OPC (ZIP) package; the ZIP is opened with the hardened, zero-dep reader from
|
|
6
|
-
* `@chestnutlabs/gcode-containers` (DD-005 §7 — zip-bomb / traversal / size caps reused verbatim).
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* `@chestnutlabs/gcode-containers` (DD-005 §7 — zip-bomb / traversal / size caps reused verbatim). Each
|
|
9
|
+
* model part is a small XML document parsed with a minimal, worker-safe scan (no `DOMParser`, which Web
|
|
10
|
+
* Workers lack; no dependency). Only the subset needed for a presentation render is read:
|
|
9
11
|
* `<vertices>/<triangles>`, object/triangle material refs, `<basematerials>`/`<m:colorgroup>` palettes,
|
|
10
|
-
* and `<build><item>` transforms. Textures and non-color
|
|
11
|
-
* capability honesty), never fetched (no network).
|
|
12
|
+
* `<components><component>` (with `p:path`), and `<build><item>` transforms. Textures and non-color
|
|
13
|
+
* material properties are ignored (disclosed via capability honesty), never fetched (no network).
|
|
12
14
|
*/
|
|
13
15
|
import { readDirectory, extractEntry, DEFAULT_CONTAINER_LIMITS } from '@chestnutlabs/gcode-containers';
|
|
14
16
|
import { IDENTITY_MAT4 } from './scene-model.js';
|
|
@@ -22,6 +24,8 @@ const UNIT_TO_MM = {
|
|
|
22
24
|
inch: 25.4,
|
|
23
25
|
foot: 304.8
|
|
24
26
|
};
|
|
27
|
+
/** Max component-resolution depth (guards cyclic / pathological assemblies). */
|
|
28
|
+
const MAX_DEPTH = 50;
|
|
25
29
|
const TAG_RE = /<(\/?)([\w:.-]+)((?:[^>"']|"[^"]*"|'[^']*')*?)(\/?)>/g;
|
|
26
30
|
const ATTR_RE = /([\w:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
27
31
|
function localName(tag) {
|
|
@@ -52,48 +56,31 @@ function srgbHexToLinear(hex) {
|
|
|
52
56
|
const b = to(4);
|
|
53
57
|
return Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b) ? null : [r, g, b];
|
|
54
58
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
let entry = byName((n) => n === '3d/3dmodel.model');
|
|
61
|
-
if (entry === undefined) {
|
|
62
|
-
const rels = byName((n) => n === '_rels/.rels');
|
|
63
|
-
if (rels !== undefined) {
|
|
64
|
-
const relXml = new TextDecoder().decode(await extractEntry(bytes, rels, limits.maxMetadataBytes));
|
|
65
|
-
const target = /Target="\/?([^"]+\.model)"/i.exec(relXml)?.[1];
|
|
66
|
-
if (target !== undefined)
|
|
67
|
-
entry = byName((n) => n === target.toLowerCase());
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
if (entry === undefined)
|
|
71
|
-
entry = byName((n) => n.endsWith('.model'));
|
|
72
|
-
if (entry === undefined)
|
|
73
|
-
throw new ModelParseError('E_MODEL_PARSE', '3MF contains no 3D model part');
|
|
74
|
-
return extractEntry(bytes, entry, limits.maxExpandedBytesPerEntry);
|
|
59
|
+
function parseTransform(s) {
|
|
60
|
+
if (s === undefined)
|
|
61
|
+
return null;
|
|
62
|
+
const v = s.trim().split(/\s+/).map(Number);
|
|
63
|
+
return v.length === 12 && v.every((n) => !Number.isNaN(n)) ? v : null;
|
|
75
64
|
}
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
// Single scan over the model XML.
|
|
65
|
+
/** Apply a 3MF row-vector affine (12 values) to a point. */
|
|
66
|
+
function applyTransform(v, x, y, z) {
|
|
67
|
+
return [
|
|
68
|
+
v[0] * x + v[3] * y + v[6] * z + v[9],
|
|
69
|
+
v[1] * x + v[4] * y + v[7] * z + v[10],
|
|
70
|
+
v[2] * x + v[5] * y + v[8] * z + v[11]
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
/** Apply a chain of transforms in order (innermost first): comp → … → build item. */
|
|
74
|
+
function applyChain(chain, x, y, z) {
|
|
75
|
+
let p = [x, y, z];
|
|
76
|
+
for (const t of chain)
|
|
77
|
+
p = applyTransform(t, p[0], p[1], p[2]);
|
|
78
|
+
return p;
|
|
79
|
+
}
|
|
80
|
+
/** Parse ONE model part's XML into objects/palettes/items (no cross-part resolution). */
|
|
81
|
+
function parseModelPart(xml, lim) {
|
|
95
82
|
let unitScale = 1;
|
|
96
|
-
const palettes = new Map();
|
|
83
|
+
const palettes = new Map();
|
|
97
84
|
const objects = new Map();
|
|
98
85
|
const items = [];
|
|
99
86
|
let curObj = null;
|
|
@@ -138,7 +125,7 @@ export async function parse3mf(source, limits) {
|
|
|
138
125
|
else {
|
|
139
126
|
const a = attrs(raw);
|
|
140
127
|
curObjId = a.id ?? null;
|
|
141
|
-
curObj = { vertices: [], tris: [], triColorIdx: [], triPid: [], name: a.name };
|
|
128
|
+
curObj = { vertices: [], tris: [], triColorIdx: [], triPid: [], components: [], name: a.name };
|
|
142
129
|
if (a.pid !== undefined)
|
|
143
130
|
curObj.objPid = a.pid;
|
|
144
131
|
if (a.pindex !== undefined)
|
|
@@ -149,6 +136,18 @@ export async function parse3mf(source, limits) {
|
|
|
149
136
|
}
|
|
150
137
|
}
|
|
151
138
|
}
|
|
139
|
+
else if (name === 'component' && curObj !== null) {
|
|
140
|
+
// Production extension: a sub-object, possibly in an external part via `p:path` (any prefix).
|
|
141
|
+
const a = attrs(raw);
|
|
142
|
+
const path = a['p:path'] ?? a.path;
|
|
143
|
+
if (a.objectid !== undefined) {
|
|
144
|
+
curObj.components.push({
|
|
145
|
+
...(path !== undefined ? { path } : {}),
|
|
146
|
+
objectid: a.objectid,
|
|
147
|
+
transform: parseTransform(a.transform)
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
152
151
|
else if (name === 'vertex' && curObj !== null) {
|
|
153
152
|
const a = attrs(raw);
|
|
154
153
|
curObj.vertices.push(Number(a.x) || 0, Number(a.y) || 0, Number(a.z) || 0);
|
|
@@ -161,7 +160,6 @@ export async function parse3mf(source, limits) {
|
|
|
161
160
|
}
|
|
162
161
|
const pid = a.pid ?? curObj.objPid ?? null;
|
|
163
162
|
curObj.triPid.push(pid);
|
|
164
|
-
// Per-vertex color indices p1/p2/p3 (fall back to p1 for all, then object pindex).
|
|
165
163
|
if (a.p1 !== undefined) {
|
|
166
164
|
const p1 = Number(a.p1);
|
|
167
165
|
const p2 = a.p2 !== undefined ? Number(a.p2) : p1;
|
|
@@ -182,107 +180,172 @@ export async function parse3mf(source, limits) {
|
|
|
182
180
|
else if (name === 'item' && inBuild && !closing) {
|
|
183
181
|
const a = attrs(raw);
|
|
184
182
|
if (a.objectid !== undefined)
|
|
185
|
-
items.push({ objectid: a.objectid, transform: a.transform });
|
|
183
|
+
items.push({ objectid: a.objectid, transform: parseTransform(a.transform) });
|
|
186
184
|
}
|
|
187
185
|
}
|
|
188
|
-
return
|
|
189
|
-
}
|
|
190
|
-
function parseTransform(s) {
|
|
191
|
-
if (s === undefined)
|
|
192
|
-
return null;
|
|
193
|
-
const v = s.trim().split(/\s+/).map(Number);
|
|
194
|
-
return v.length === 12 && v.every((n) => !Number.isNaN(n)) ? v : null;
|
|
186
|
+
return { objects, palettes, unitScale, items };
|
|
195
187
|
}
|
|
196
|
-
/**
|
|
197
|
-
function
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
[x, y, z] = applyTransform(xform, x, y, z);
|
|
239
|
-
const o = (ti * 3 + k) * 3;
|
|
240
|
-
positions[o] = x;
|
|
241
|
-
positions[o + 1] = y;
|
|
242
|
-
positions[o + 2] = z;
|
|
243
|
-
if (x < min[0])
|
|
244
|
-
min[0] = x;
|
|
245
|
-
if (y < min[1])
|
|
246
|
-
min[1] = y;
|
|
247
|
-
if (z < min[2])
|
|
248
|
-
min[2] = z;
|
|
249
|
-
if (x > max[0])
|
|
250
|
-
max[0] = x;
|
|
251
|
-
if (y > max[1])
|
|
252
|
-
max[1] = y;
|
|
253
|
-
if (z > max[2])
|
|
254
|
-
max[2] = z;
|
|
255
|
-
if (palette !== undefined && cidx !== null) {
|
|
256
|
-
const col = palette[cidx[k]] ?? palette[0];
|
|
257
|
-
if (col !== undefined) {
|
|
258
|
-
if (colors === null)
|
|
259
|
-
colors = new Float32Array(triCount * 9);
|
|
260
|
-
colors[o] = col[0];
|
|
261
|
-
colors[o + 1] = col[1];
|
|
262
|
-
colors[o + 2] = col[2];
|
|
263
|
-
objHasColor = true;
|
|
264
|
-
}
|
|
188
|
+
/** Bake one mesh object (its own triangles) into a `ModelObject`, applying the transform chain. */
|
|
189
|
+
function bakeMesh(obj, part, chain, id, st, lim) {
|
|
190
|
+
const triCount = obj.tris.length / 3;
|
|
191
|
+
st.totalTris += triCount;
|
|
192
|
+
if (st.totalTris > lim.maxTriangles) {
|
|
193
|
+
throw new ModelParseError('E_MODEL_TOO_MANY_TRIANGLES', `3MF exceeds triangle limit ${lim.maxTriangles}`);
|
|
194
|
+
}
|
|
195
|
+
const positions = new Float32Array(triCount * 9);
|
|
196
|
+
let colors = null;
|
|
197
|
+
let objHasColor = false;
|
|
198
|
+
for (let ti = 0; ti < triCount; ti++) {
|
|
199
|
+
const pid = obj.triPid[ti];
|
|
200
|
+
const palette = pid !== null ? part.palettes.get(pid) : undefined;
|
|
201
|
+
const cidx = obj.triColorIdx[ti];
|
|
202
|
+
for (let k = 0; k < 3; k++) {
|
|
203
|
+
const vi = obj.tris[ti * 3 + k] * 3;
|
|
204
|
+
const [x, y, z] = applyChain(chain, obj.vertices[vi] * part.unitScale, obj.vertices[vi + 1] * part.unitScale, obj.vertices[vi + 2] * part.unitScale);
|
|
205
|
+
const o = (ti * 3 + k) * 3;
|
|
206
|
+
positions[o] = x;
|
|
207
|
+
positions[o + 1] = y;
|
|
208
|
+
positions[o + 2] = z;
|
|
209
|
+
if (x < st.min[0])
|
|
210
|
+
st.min[0] = x;
|
|
211
|
+
if (y < st.min[1])
|
|
212
|
+
st.min[1] = y;
|
|
213
|
+
if (z < st.min[2])
|
|
214
|
+
st.min[2] = z;
|
|
215
|
+
if (x > st.max[0])
|
|
216
|
+
st.max[0] = x;
|
|
217
|
+
if (y > st.max[1])
|
|
218
|
+
st.max[1] = y;
|
|
219
|
+
if (z > st.max[2])
|
|
220
|
+
st.max[2] = z;
|
|
221
|
+
if (palette !== undefined && cidx !== null) {
|
|
222
|
+
const col = palette[cidx[k]] ?? palette[0];
|
|
223
|
+
if (col !== undefined) {
|
|
224
|
+
if (colors === null)
|
|
225
|
+
colors = new Float32Array(triCount * 9);
|
|
226
|
+
colors[o] = col[0];
|
|
227
|
+
colors[o + 1] = col[1];
|
|
228
|
+
colors[o + 2] = col[2];
|
|
229
|
+
objHasColor = true;
|
|
265
230
|
}
|
|
266
231
|
}
|
|
267
232
|
}
|
|
268
|
-
if (objHasColor)
|
|
269
|
-
anyColor = true;
|
|
270
|
-
const geometry = colors !== null ? { positions, colors } : { positions };
|
|
271
|
-
const built1 = { id: inst.objectid, geometry, transform: IDENTITY_MAT4 };
|
|
272
|
-
if (obj.name !== undefined)
|
|
273
|
-
built1.name = obj.name;
|
|
274
|
-
built.push(built1);
|
|
275
233
|
}
|
|
276
|
-
if (
|
|
234
|
+
if (objHasColor)
|
|
235
|
+
st.anyColor = true;
|
|
236
|
+
const geometry = colors !== null ? { positions, colors } : { positions };
|
|
237
|
+
const built = { id, geometry, transform: IDENTITY_MAT4 };
|
|
238
|
+
if (obj.name !== undefined)
|
|
239
|
+
built.name = obj.name;
|
|
240
|
+
st.built.push(built);
|
|
241
|
+
}
|
|
242
|
+
export async function parse3mf(source, limits) {
|
|
243
|
+
const lim = resolveLimits(limits);
|
|
244
|
+
const bytes = source instanceof Uint8Array ? source : new Uint8Array(source);
|
|
245
|
+
if (bytes.byteLength === 0)
|
|
246
|
+
throw new ModelParseError('E_MODEL_EMPTY', '3MF source is empty');
|
|
247
|
+
if (bytes.byteLength > lim.maxSourceBytes) {
|
|
248
|
+
throw new ModelParseError('E_MODEL_TOO_LARGE', `3MF source ${bytes.byteLength} bytes exceeds limit ${lim.maxSourceBytes}`);
|
|
249
|
+
}
|
|
250
|
+
const climits = DEFAULT_CONTAINER_LIMITS;
|
|
251
|
+
let dir;
|
|
252
|
+
try {
|
|
253
|
+
dir = readDirectory(bytes, climits);
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
throw new ModelParseError('E_MODEL_PARSE', `3MF unzip failed: ${e.message}`);
|
|
257
|
+
}
|
|
258
|
+
const entryByName = (nameLower) => dir.entries.find((e) => e.name.toLowerCase() === nameLower);
|
|
259
|
+
// Locate the primary model part: conventional path, then OPC StartPart, then any *.model.
|
|
260
|
+
let mainEntry = entryByName('3d/3dmodel.model');
|
|
261
|
+
if (mainEntry === undefined) {
|
|
262
|
+
const rels = entryByName('_rels/.rels');
|
|
263
|
+
if (rels !== undefined) {
|
|
264
|
+
const relXml = new TextDecoder().decode(await extractEntry(bytes, rels, climits.maxMetadataBytes));
|
|
265
|
+
const target = /Target="\/?([^"]+\.model)"/i.exec(relXml)?.[1];
|
|
266
|
+
if (target !== undefined)
|
|
267
|
+
mainEntry = entryByName(target.toLowerCase());
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (mainEntry === undefined)
|
|
271
|
+
mainEntry = dir.entries.find((e) => e.name.toLowerCase().endsWith('.model'));
|
|
272
|
+
if (mainEntry === undefined)
|
|
273
|
+
throw new ModelParseError('E_MODEL_PARSE', '3MF contains no 3D model part');
|
|
274
|
+
// Lazily parse model parts by absolute OPC path (production-extension components reference them).
|
|
275
|
+
const partCache = new Map();
|
|
276
|
+
const parseEntry = async (entry) => {
|
|
277
|
+
const key = entry.name.toLowerCase();
|
|
278
|
+
const cached = partCache.get(key);
|
|
279
|
+
if (cached !== undefined)
|
|
280
|
+
return cached;
|
|
281
|
+
const xml = new TextDecoder().decode(await extractEntry(bytes, entry, climits.maxExpandedBytesPerEntry));
|
|
282
|
+
const part = parseModelPart(xml, lim);
|
|
283
|
+
partCache.set(key, part);
|
|
284
|
+
return part;
|
|
285
|
+
};
|
|
286
|
+
const getPartByPath = async (path) => {
|
|
287
|
+
const norm = path.replace(/^\//, '').toLowerCase();
|
|
288
|
+
const entry = entryByName(norm);
|
|
289
|
+
return entry === undefined ? null : parseEntry(entry);
|
|
290
|
+
};
|
|
291
|
+
let mainPart;
|
|
292
|
+
try {
|
|
293
|
+
mainPart = await parseEntry(mainEntry);
|
|
294
|
+
}
|
|
295
|
+
catch (e) {
|
|
296
|
+
if (e instanceof ModelParseError)
|
|
297
|
+
throw e;
|
|
298
|
+
throw new ModelParseError('E_MODEL_PARSE', `3MF unzip failed: ${e.message}`);
|
|
299
|
+
}
|
|
300
|
+
const st = {
|
|
301
|
+
built: [],
|
|
302
|
+
anyColor: false,
|
|
303
|
+
anyTransform: false,
|
|
304
|
+
min: [Infinity, Infinity, Infinity],
|
|
305
|
+
max: [-Infinity, -Infinity, -Infinity],
|
|
306
|
+
totalTris: 0
|
|
307
|
+
};
|
|
308
|
+
// Resolve an object (mesh and/or components) within `part`, applying the transform chain.
|
|
309
|
+
const resolve = async (part, objectid, chain, id, depth) => {
|
|
310
|
+
if (depth > MAX_DEPTH)
|
|
311
|
+
return;
|
|
312
|
+
const obj = part.objects.get(objectid);
|
|
313
|
+
if (obj === undefined)
|
|
314
|
+
return;
|
|
315
|
+
if (obj.tris.length > 0)
|
|
316
|
+
bakeMesh(obj, part, chain, id, st, lim);
|
|
317
|
+
for (let ci = 0; ci < obj.components.length; ci++) {
|
|
318
|
+
const comp = obj.components[ci];
|
|
319
|
+
const targetPart = comp.path !== undefined ? await getPartByPath(comp.path) : part;
|
|
320
|
+
if (targetPart === null)
|
|
321
|
+
continue;
|
|
322
|
+
// Apply the component transform FIRST (child space → this object), then the existing chain.
|
|
323
|
+
const childChain = comp.transform !== null ? [comp.transform, ...chain] : chain;
|
|
324
|
+
if (comp.transform !== null)
|
|
325
|
+
st.anyTransform = true;
|
|
326
|
+
await resolve(targetPart, comp.objectid, childChain, `${id}.${ci}`, depth + 1);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
// With no <build>, resolve every declared object at identity (some minimal 3MFs omit build).
|
|
330
|
+
const instances = mainPart.items.length > 0
|
|
331
|
+
? mainPart.items
|
|
332
|
+
: [...mainPart.objects.keys()].map((objectid) => ({ objectid, transform: null }));
|
|
333
|
+
for (const inst of instances) {
|
|
334
|
+
const chain = inst.transform !== null ? [inst.transform] : [];
|
|
335
|
+
if (inst.transform !== null)
|
|
336
|
+
st.anyTransform = true;
|
|
337
|
+
await resolve(mainPart, inst.objectid, chain, inst.objectid, 0);
|
|
338
|
+
}
|
|
339
|
+
if (st.built.length === 0)
|
|
277
340
|
throw new ModelParseError('E_MODEL_EMPTY', '3MF contains no renderable geometry');
|
|
278
|
-
const bounds = { min, max };
|
|
341
|
+
const bounds = { min: st.min, max: st.max };
|
|
279
342
|
return {
|
|
280
|
-
objects: built,
|
|
343
|
+
objects: st.built,
|
|
281
344
|
bounds,
|
|
282
345
|
capabilities: {
|
|
283
|
-
materials: anyColor ? 'known' : 'unavailable',
|
|
284
|
-
transforms: anyTransform ? 'known' : 'unavailable',
|
|
285
|
-
multiObject: built.length > 1 ? 'known' : 'unavailable'
|
|
346
|
+
materials: st.anyColor ? 'known' : 'unavailable',
|
|
347
|
+
transforms: st.anyTransform ? 'known' : 'unavailable',
|
|
348
|
+
multiObject: st.built.length > 1 ? 'known' : 'unavailable'
|
|
286
349
|
}
|
|
287
350
|
};
|
|
288
351
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chestnutlabs/gcode-model-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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.
|
|
55
|
-
"@chestnutlabs/gcode-renderer-three": "0.
|
|
56
|
-
"@chestnutlabs/toolpath-core": "0.
|
|
54
|
+
"@chestnutlabs/gcode-containers": "0.7.0",
|
|
55
|
+
"@chestnutlabs/gcode-renderer-three": "0.7.0",
|
|
56
|
+
"@chestnutlabs/toolpath-core": "0.7.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/three": "0.178.0"
|