@omrips/viewstl 2.0.0-rc.5 → 2.0.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/build/THIRD-PARTY-NOTICES.txt +1006 -1006
- package/build/cad/occt-import-js.source.txt +19 -19
- package/build/chunks/archive-QK7XZSVT.js +1 -0
- package/build/chunks/{cad-4JYHORYZ.js → cad-QGVO6UGR.js} +1 -1
- package/build/chunks/chunk-6AW5U52H.js +1 -0
- package/build/chunks/{chunk-Y6VY64LY.js → chunk-CVGQTUJ5.js} +2 -2
- package/build/chunks/chunk-FUXP2W57.js +1 -0
- package/build/chunks/chunk-LFTKNL2V.js +1 -0
- package/build/chunks/{chunk-PQYEV7WH.js → chunk-T6PPRDOE.js} +1 -1
- package/build/chunks/chunk-V6KYGP62.js +1 -0
- package/build/chunks/{export-VM6OQCQJ.js → export-2CFMTPJ3.js} +3 -3
- package/build/chunks/{export-cad-QXCB6UDU.js → export-cad-XOXIE726.js} +1 -1
- package/build/chunks/{export-zip-PVXETVLK.js → export-zip-I2ADLFJI.js} +1 -1
- package/build/chunks/measure-3LDALPCN.js +1 -0
- package/build/chunks/print-check-WWHTT5PW.js +1 -0
- package/build/chunks/{scene-export-ZIXMUNLI.js → scene-export-FPPL466I.js} +1 -1
- package/build/chunks/scene-reader-WXTRELY3.js +1 -0
- package/build/node/analysis-worker.js +2 -1
- package/build/stl_viewer.min.js +5 -5
- package/build/viewstl.cjs +1134 -392
- package/build/viewstl.d.cts +48 -0
- package/build/viewstl.d.ts +48 -0
- package/build/viewstl.js +1134 -392
- package/build/viewstl.min.js +5 -5
- package/build/viewstl.worker.js +1 -1
- package/docs/releases/2.0.0-rc.6.md +7 -0
- package/docs/v2-usage.md +24 -0
- package/docs/v2-verification.md +28 -0
- package/package.json +2 -2
- package/build/chunks/archive-JU75RPTU.js +0 -1
- package/build/chunks/chunk-IK3WL53C.js +0 -1
- package/build/chunks/chunk-OP2L7L4N.js +0 -1
- package/build/chunks/print-check-NJ4IONC5.js +0 -1
- package/build/chunks/scene-reader-JGHERXR2.js +0 -1
- package/docs/releases/2.0.0-rc.5.md +0 -46
package/build/viewstl.cjs
CHANGED
|
@@ -25,54 +25,6 @@ __export(commonjs_exports, {
|
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(commonjs_exports);
|
|
27
27
|
|
|
28
|
-
// src/appearance.js
|
|
29
|
-
var DISPLAYS = [
|
|
30
|
-
"flat",
|
|
31
|
-
"smooth",
|
|
32
|
-
"wireframe",
|
|
33
|
-
"edges",
|
|
34
|
-
"mesh",
|
|
35
|
-
"xray",
|
|
36
|
-
"studio",
|
|
37
|
-
"orientation"
|
|
38
|
-
];
|
|
39
|
-
function validateView(display, smooth) {
|
|
40
|
-
if (!DISPLAYS.includes(display)) throw new Error("Invalid display mode");
|
|
41
|
-
if (typeof smooth !== "boolean")
|
|
42
|
-
throw new Error("smooth_shading must be a boolean");
|
|
43
|
-
return { display, smooth_shading: display === "smooth" || smooth };
|
|
44
|
-
}
|
|
45
|
-
function viewFeatures(view) {
|
|
46
|
-
return {
|
|
47
|
-
smooth: (view.smooth_shading || view.display === "smooth") && view.display !== "wireframe",
|
|
48
|
-
edges: view.view_edges || view.display === "edges",
|
|
49
|
-
wire: view.display === "wireframe",
|
|
50
|
-
edgeAngle: view.display === "edges" ? 30 : 1
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
function validateSection(section) {
|
|
54
|
-
if (section == null) return null;
|
|
55
|
-
if (!["x", "y", "z"].includes(section.axis))
|
|
56
|
-
throw new Error("Section axis must be x, y or z");
|
|
57
|
-
if (!Number.isFinite(section.position) || section.position < 0 || section.position > 1)
|
|
58
|
-
throw new Error("Section position must be between 0 and 1");
|
|
59
|
-
if (section.flip !== void 0 && typeof section.flip !== "boolean")
|
|
60
|
-
throw new Error("Section flip must be a boolean");
|
|
61
|
-
return {
|
|
62
|
-
axis: section.axis,
|
|
63
|
-
position: section.position,
|
|
64
|
-
flip: section.flip ?? false
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
function sectionPlane(bounds, section) {
|
|
68
|
-
if (!section) return [0, 0, 0, 0];
|
|
69
|
-
const axis = ["x", "y", "z"].indexOf(section.axis), sign = section.flip ? -1 : 1;
|
|
70
|
-
const plane = [0, 0, 0, 0];
|
|
71
|
-
plane[axis] = sign;
|
|
72
|
-
plane[3] = -sign * (bounds.min[axis] + (bounds.max[axis] - bounds.min[axis]) * section.position);
|
|
73
|
-
return plane;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
28
|
// src/math.js
|
|
77
29
|
var add = (a, b) => a.map((v, i) => v + b[i]);
|
|
78
30
|
var sub = (a, b) => a.map((v, i) => v - b[i]);
|
|
@@ -205,41 +157,625 @@ function rayBox(origin, direction, min, max) {
|
|
|
205
157
|
if (origin[a] < min[a] || origin[a] > max[a]) return false;
|
|
206
158
|
continue;
|
|
207
159
|
}
|
|
208
|
-
let x = (min[a] - origin[a]) / direction[a], y = (max[a] - origin[a]) / direction[a];
|
|
209
|
-
if (x > y) [x, y] = [y, x];
|
|
210
|
-
near = Math.max(near, x);
|
|
211
|
-
far = Math.min(far, y);
|
|
212
|
-
if (far < near) return false;
|
|
160
|
+
let x = (min[a] - origin[a]) / direction[a], y = (max[a] - origin[a]) / direction[a];
|
|
161
|
+
if (x > y) [x, y] = [y, x];
|
|
162
|
+
near = Math.max(near, x);
|
|
163
|
+
far = Math.min(far, y);
|
|
164
|
+
if (far < near) return false;
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
function rayTriangle(o, d, p, i) {
|
|
169
|
+
const a = [p[i], p[i + 1], p[i + 2]], b = sub([p[i + 3], p[i + 4], p[i + 5]], a), c = sub([p[i + 6], p[i + 7], p[i + 8]], a);
|
|
170
|
+
const h = cross(d, c), det = dot(b, h);
|
|
171
|
+
if (Math.abs(det) < 1e-12) return Infinity;
|
|
172
|
+
const s = sub(o, a), u = dot(s, h) / det;
|
|
173
|
+
if (u < 0 || u > 1) return Infinity;
|
|
174
|
+
const q = cross(s, b), v = dot(d, q) / det;
|
|
175
|
+
if (v < 0 || u + v > 1) return Infinity;
|
|
176
|
+
const t = dot(c, q) / det;
|
|
177
|
+
return t >= 0 ? t : Infinity;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/animation.js
|
|
181
|
+
var animationKeys = [
|
|
182
|
+
"x",
|
|
183
|
+
"y",
|
|
184
|
+
"z",
|
|
185
|
+
"rotationx",
|
|
186
|
+
"rotationy",
|
|
187
|
+
"rotationz",
|
|
188
|
+
"scalex",
|
|
189
|
+
"scaley",
|
|
190
|
+
"scalez"
|
|
191
|
+
];
|
|
192
|
+
var isScale = (key) => key.startsWith("scale");
|
|
193
|
+
var number = (value, name, positive2 = false) => {
|
|
194
|
+
if (typeof value !== "number" || !Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER || positive2 && value <= 0)
|
|
195
|
+
throw new Error(`Invalid animation ${name}`);
|
|
196
|
+
return value;
|
|
197
|
+
};
|
|
198
|
+
var object = (value, name) => {
|
|
199
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
200
|
+
throw new Error(`Invalid animation ${name}`);
|
|
201
|
+
return value;
|
|
202
|
+
};
|
|
203
|
+
function animationPose(model) {
|
|
204
|
+
return Object.fromEntries(
|
|
205
|
+
animationKeys.map((key) => [
|
|
206
|
+
key,
|
|
207
|
+
number(
|
|
208
|
+
model[key] ?? (isScale(key) ? model.scale ?? 1 : 0),
|
|
209
|
+
key,
|
|
210
|
+
isScale(key)
|
|
211
|
+
)
|
|
212
|
+
])
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
function normalizeAnimation(input) {
|
|
216
|
+
object(input, "definition");
|
|
217
|
+
const result = {}, claimed = /* @__PURE__ */ new Set();
|
|
218
|
+
for (const mode of ["delta", "exact"]) {
|
|
219
|
+
if (input[mode] == null) continue;
|
|
220
|
+
const source = object(input[mode], mode), values = {};
|
|
221
|
+
const msec = number(source.msec ?? 300, "duration", true);
|
|
222
|
+
if (source.loop !== void 0 && typeof source.loop !== "boolean")
|
|
223
|
+
throw new Error("Animation loop must be a boolean");
|
|
224
|
+
const loop = mode === "delta" && !!source.loop;
|
|
225
|
+
for (const key of animationKeys) {
|
|
226
|
+
const value = source[key] ?? (isScale(key) ? source.scale : void 0);
|
|
227
|
+
if (value === void 0) continue;
|
|
228
|
+
if (claimed.has(key))
|
|
229
|
+
throw new Error(`Animation delta/exact conflict: ${key}`);
|
|
230
|
+
number(value, key, mode === "exact" && isScale(key));
|
|
231
|
+
if (loop && isScale(key) && value < 0)
|
|
232
|
+
throw new Error("Looping scale deltas cannot be negative");
|
|
233
|
+
values[key] = value;
|
|
234
|
+
claimed.add(key);
|
|
235
|
+
}
|
|
236
|
+
if (!Object.keys(values).length)
|
|
237
|
+
throw new Error("Animation track needs a transform");
|
|
238
|
+
result[mode] = { ...values, msec, ...loop ? { loop: true } : {} };
|
|
239
|
+
}
|
|
240
|
+
if (!Object.keys(result).length)
|
|
241
|
+
throw new Error("Animation needs delta or exact");
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
function normalizePlaybackState(input) {
|
|
245
|
+
object(input, "playback state");
|
|
246
|
+
if (input.version !== 1 || typeof input.paused !== "boolean")
|
|
247
|
+
throw new Error("Invalid animation playback state version or paused flag");
|
|
248
|
+
const elapsed_ms = number(input.elapsed_ms, "elapsed time");
|
|
249
|
+
if (elapsed_ms < 0) throw new Error("Invalid animation elapsed time");
|
|
250
|
+
object(input.origin, "origin");
|
|
251
|
+
for (const key of animationKeys) number(input.origin[key], key, isScale(key));
|
|
252
|
+
if (!Array.isArray(input.center) || input.center.length !== 3)
|
|
253
|
+
throw new Error("Invalid animation pivot");
|
|
254
|
+
return {
|
|
255
|
+
version: 1,
|
|
256
|
+
elapsed_ms,
|
|
257
|
+
paused: input.paused,
|
|
258
|
+
origin: animationPose(input.origin),
|
|
259
|
+
center: input.center.map((v) => number(v, "pivot"))
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function createPlayback(definition, model, saved) {
|
|
263
|
+
const animation = normalizeAnimation(definition);
|
|
264
|
+
const state = saved == null ? null : normalizePlaybackState(saved);
|
|
265
|
+
const origin = state?.origin ?? animationPose(model);
|
|
266
|
+
const tracks = Object.entries(animation).map(([mode, a]) => {
|
|
267
|
+
const values = Object.entries(a).filter(
|
|
268
|
+
([key]) => animationKeys.includes(key)
|
|
269
|
+
);
|
|
270
|
+
if (mode === "delta")
|
|
271
|
+
for (const [key, value] of values)
|
|
272
|
+
number(origin[key] + value, `${key} endpoint`, isScale(key));
|
|
273
|
+
return { mode, values, duration: a.msec, loop: !!a.loop, done: false };
|
|
274
|
+
});
|
|
275
|
+
const playback = {
|
|
276
|
+
animation,
|
|
277
|
+
origin,
|
|
278
|
+
tracks,
|
|
279
|
+
elapsed_ms: state?.elapsed_ms ?? 0,
|
|
280
|
+
paused: state?.paused ?? false,
|
|
281
|
+
last_time: null
|
|
282
|
+
};
|
|
283
|
+
evaluatePlayback({ ...playback, tracks: tracks.map((t) => ({ ...t })) }, {});
|
|
284
|
+
return playback;
|
|
285
|
+
}
|
|
286
|
+
function evaluatePlayback(playback, model) {
|
|
287
|
+
const updates = [];
|
|
288
|
+
for (const track of playback.tracks) {
|
|
289
|
+
if (track.done) continue;
|
|
290
|
+
const fraction = playback.elapsed_ms / track.duration;
|
|
291
|
+
const p = track.loop ? fraction : Math.min(fraction, 1);
|
|
292
|
+
for (const [key, value] of track.values) {
|
|
293
|
+
const from = playback.origin[key];
|
|
294
|
+
const to = track.mode === "delta" ? from + value * p : from * (1 - p) + value * p;
|
|
295
|
+
updates.push([key, number(to, `${key} result`, isScale(key))]);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
let scaleChanged = false;
|
|
299
|
+
for (const [key, value] of updates) {
|
|
300
|
+
scaleChanged ||= isScale(key) && model[key] !== value;
|
|
301
|
+
model[key] = value;
|
|
302
|
+
}
|
|
303
|
+
for (const track of playback.tracks)
|
|
304
|
+
if (!track.loop && playback.elapsed_ms >= track.duration) track.done = true;
|
|
305
|
+
if (scaleChanged) model._stats = null;
|
|
306
|
+
return playback.tracks.some((track) => !track.done);
|
|
307
|
+
}
|
|
308
|
+
function advancePlayback(playback, model, time) {
|
|
309
|
+
if (playback.paused) return true;
|
|
310
|
+
if (playback.last_time !== null)
|
|
311
|
+
playback.elapsed_ms = number(
|
|
312
|
+
playback.elapsed_ms + Math.max(0, time - playback.last_time),
|
|
313
|
+
"elapsed time"
|
|
314
|
+
);
|
|
315
|
+
playback.last_time = time;
|
|
316
|
+
return evaluatePlayback(playback, model);
|
|
317
|
+
}
|
|
318
|
+
function playbackState(playback, suspended = false) {
|
|
319
|
+
return {
|
|
320
|
+
status: playback.paused ? "paused" : "running",
|
|
321
|
+
elapsed_ms: playback.elapsed_ms,
|
|
322
|
+
suspended,
|
|
323
|
+
tracks: playback.tracks.filter((t) => !t.done).map((t) => ({
|
|
324
|
+
mode: t.mode,
|
|
325
|
+
msec: t.duration,
|
|
326
|
+
loop: t.loop,
|
|
327
|
+
progress: t.loop ? playback.elapsed_ms % t.duration / t.duration : Math.min(1, playback.elapsed_ms / t.duration)
|
|
328
|
+
}))
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function savePlayback(model) {
|
|
332
|
+
const p = model._animation;
|
|
333
|
+
if (!p) return null;
|
|
334
|
+
const factor = model.unitScale ?? 1;
|
|
335
|
+
const animation = structuredClone(p.animation), origin = { ...p.origin };
|
|
336
|
+
for (const key of animationKeys.filter(isScale)) {
|
|
337
|
+
origin[key] *= factor;
|
|
338
|
+
for (const track of Object.values(animation))
|
|
339
|
+
if (track[key] !== void 0) track[key] *= factor;
|
|
340
|
+
}
|
|
341
|
+
const animation_state = normalizePlaybackState({
|
|
342
|
+
version: 1,
|
|
343
|
+
origin,
|
|
344
|
+
elapsed_ms: p.elapsed_ms,
|
|
345
|
+
paused: p.paused,
|
|
346
|
+
center: [...model.center]
|
|
347
|
+
});
|
|
348
|
+
return { animation: normalizeAnimation(animation), animation_state };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/memory.js
|
|
352
|
+
var MEMORY_OVERHEAD = 64 * 1024 * 1024;
|
|
353
|
+
var TRIANGLE_MEMORY = 224;
|
|
354
|
+
function geometryMemory(g) {
|
|
355
|
+
if (!g) return 0;
|
|
356
|
+
return g.triangles * TRIANGLE_MEMORY + (g.smoothNormals?.byteLength || 0) * 4 + ((g.edges?.byteLength || 0) + (g.wire?.byteLength || 0)) * 2;
|
|
357
|
+
}
|
|
358
|
+
function assertMemory(limits, bytes, operation = "model") {
|
|
359
|
+
if (bytes > (limits.max_memory_bytes ?? Number.MAX_SAFE_INTEGER))
|
|
360
|
+
throw new Error(
|
|
361
|
+
`Estimated memory for this ${operation} exceeds the device safety budget. Use a smaller model or a device with more memory.`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
function derivedMemory(triangles, features) {
|
|
365
|
+
return MEMORY_OVERHEAD + triangles * (TRIANGLE_MEMORY + (features.smooth ? 1536 : 0) + (features.edges || features.wire ? 2048 : 0));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/print-options.js
|
|
369
|
+
function printOptions(input = {}) {
|
|
370
|
+
const level = input.level ?? "basic";
|
|
371
|
+
if (!["basic", "detailed"].includes(level))
|
|
372
|
+
throw new Error("Choose basic or detailed print checks");
|
|
373
|
+
const number2 = (v, name, max = 1e9) => {
|
|
374
|
+
if (!Number.isFinite(v) || v <= 0 || v > max)
|
|
375
|
+
throw new Error(`Invalid ${name}`);
|
|
376
|
+
return v;
|
|
377
|
+
};
|
|
378
|
+
let profile = null;
|
|
379
|
+
if (input.profile != null) {
|
|
380
|
+
if (input.profile.technology !== "fdm")
|
|
381
|
+
throw new Error("Only FDM profiles are supported");
|
|
382
|
+
profile = { technology: "fdm" };
|
|
383
|
+
for (const key of ["min_wall_mm", "line_width_mm"])
|
|
384
|
+
if (input.profile[key] != null)
|
|
385
|
+
profile[key] = number2(input.profile[key], key);
|
|
386
|
+
if (input.profile.overhang_degrees != null)
|
|
387
|
+
profile.overhang_degrees = number2(
|
|
388
|
+
input.profile.overhang_degrees,
|
|
389
|
+
"overhang angle",
|
|
390
|
+
89.9
|
|
391
|
+
);
|
|
392
|
+
if (input.profile.build_volume_mm != null) {
|
|
393
|
+
if (!Array.isArray(input.profile.build_volume_mm) || input.profile.build_volume_mm.length !== 3)
|
|
394
|
+
throw new Error("Build volume requires X, Y and Z");
|
|
395
|
+
profile.build_volume_mm = input.profile.build_volume_mm.map(
|
|
396
|
+
(v) => number2(v, "build volume")
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const time = number2(input.max_time_ms ?? 3e4, "check time limit", 12e4);
|
|
401
|
+
return {
|
|
402
|
+
level,
|
|
403
|
+
profile,
|
|
404
|
+
apply_transforms: input.apply_transforms !== false,
|
|
405
|
+
units_confirmed: input.units_confirmed === true,
|
|
406
|
+
max_time_ms: time
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
var printMemory = (triangles) => 64 * 1024 ** 2 + triangles * 2400;
|
|
410
|
+
var printSignature = (model) => JSON.stringify([
|
|
411
|
+
model.x,
|
|
412
|
+
model.y,
|
|
413
|
+
model.z,
|
|
414
|
+
model.rotationx,
|
|
415
|
+
model.rotationy,
|
|
416
|
+
model.rotationz,
|
|
417
|
+
model.scaleVector,
|
|
418
|
+
model.center,
|
|
419
|
+
model.units,
|
|
420
|
+
model.unitScale
|
|
421
|
+
]);
|
|
422
|
+
|
|
423
|
+
// src/model-tools.js
|
|
424
|
+
var axes = ["x", "y", "z"];
|
|
425
|
+
function validateUnits(units) {
|
|
426
|
+
if (!["mm", "inch"].includes(units)) throw new Error("Choose mm or inch");
|
|
427
|
+
return units;
|
|
428
|
+
}
|
|
429
|
+
function validateSizeState(value) {
|
|
430
|
+
if (!value || value.version !== 1 || !Array.isArray(value.baseline_mm) || value.baseline_mm.length !== 3 || value.baseline_mm.some((v) => !Number.isFinite(v) || v < 0 || v > 1e12) || typeof value.editable !== "boolean" || value.unit_factor !== void 0 && (!Number.isFinite(value.unit_factor) || value.unit_factor <= 0 || value.unit_factor > 1e12))
|
|
431
|
+
throw new Error("Invalid saved model size");
|
|
432
|
+
return {
|
|
433
|
+
version: 1,
|
|
434
|
+
baseline_mm: [...value.baseline_mm],
|
|
435
|
+
file_units: validateUnits(value.file_units),
|
|
436
|
+
editable: value.editable,
|
|
437
|
+
unit_factor: value.unit_factor ?? 1
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function sizeState(model) {
|
|
441
|
+
return model.size_state ??= {
|
|
442
|
+
version: 1,
|
|
443
|
+
baseline_mm: model.geometry.bounds.max.map(
|
|
444
|
+
(v, i) => (v - model.geometry.bounds.min[i]) * model.unitScale
|
|
445
|
+
),
|
|
446
|
+
file_units: "mm",
|
|
447
|
+
editable: !model.geometry.units,
|
|
448
|
+
unit_factor: 1
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function modelSize(model) {
|
|
452
|
+
if (!model.geometry) throw new Error("Wait for the model to finish opening");
|
|
453
|
+
const state = sizeState(model);
|
|
454
|
+
return {
|
|
455
|
+
...structuredClone(state),
|
|
456
|
+
dimensions_mm: model.geometry.bounds.max.map(
|
|
457
|
+
(v, i) => (v - model.geometry.bounds.min[i]) * model.scaleVector[i]
|
|
458
|
+
)
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function resizeModel(id, factors) {
|
|
462
|
+
const m = this._model(id);
|
|
463
|
+
modelSize(m);
|
|
464
|
+
if (!Array.isArray(factors) || factors.length !== 3 || factors.some((v) => !Number.isFinite(v) || v <= 0))
|
|
465
|
+
throw new Error("Scale must be a positive finite number");
|
|
466
|
+
const next = axes.map((a, i) => m["scale" + a] * factors[i]);
|
|
467
|
+
if (next.some((v) => !Number.isFinite(v) || v < 1e-12 || v > 1e12))
|
|
468
|
+
throw new Error("Scale is outside the supported range");
|
|
469
|
+
const center = m.geometry.bounds.min.map(
|
|
470
|
+
(v, i) => (v + m.geometry.bounds.max[i]) / 2
|
|
471
|
+
);
|
|
472
|
+
const before = transform(modelMatrix(m), center);
|
|
473
|
+
const cached = m._measurements;
|
|
474
|
+
const oldKey = m.scaleVector.join(",");
|
|
475
|
+
this._stopAnimation(m, "manual");
|
|
476
|
+
axes.forEach((a, i) => m["scale" + a] = next[i]);
|
|
477
|
+
const after = transform(modelMatrix(m), center);
|
|
478
|
+
axes.forEach((a, i) => m[a] += before[i] - after[i]);
|
|
479
|
+
m._stats = null;
|
|
480
|
+
if (cached?.key === oldKey && cached.positions === m.geometry.positions && factors.every((v) => v === factors[0])) {
|
|
481
|
+
m._measurements = {
|
|
482
|
+
...cached,
|
|
483
|
+
key: m.scaleVector.join(","),
|
|
484
|
+
result: {
|
|
485
|
+
...cached.result,
|
|
486
|
+
area_mm2: cached.result.area_mm2 * factors[0] ** 2,
|
|
487
|
+
volume_mm3: cached.result.volume_mm3 == null ? null : cached.result.volume_mm3 * factors[0] ** 3
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
this._invalidate();
|
|
492
|
+
return modelSize(m);
|
|
493
|
+
}
|
|
494
|
+
function setFileUnits(id, units) {
|
|
495
|
+
validateUnits(units);
|
|
496
|
+
const m = this._model(id), state = sizeState(m);
|
|
497
|
+
if (!state.editable)
|
|
498
|
+
throw new Error(
|
|
499
|
+
"This model has explicit file units, already converted to millimetres"
|
|
500
|
+
);
|
|
501
|
+
if (state.file_units === units) return modelSize(m);
|
|
502
|
+
const factor = units === "inch" ? 25.4 : 1 / 25.4;
|
|
503
|
+
const result = resizeModel.call(this, id, [factor, factor, factor]);
|
|
504
|
+
state.baseline_mm = state.baseline_mm.map((v) => v * factor);
|
|
505
|
+
state.file_units = units;
|
|
506
|
+
state.unit_factor = (state.unit_factor ?? 1) * factor;
|
|
507
|
+
return { ...result, ...structuredClone(state) };
|
|
508
|
+
}
|
|
509
|
+
function resetModelSize(id) {
|
|
510
|
+
const size = modelSize(this._model(id));
|
|
511
|
+
return resizeModel.call(
|
|
512
|
+
this,
|
|
513
|
+
id,
|
|
514
|
+
size.dimensions_mm.map((v, i) => v ? size.baseline_mm[i] / v : 1)
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
function duplicateModel(id) {
|
|
518
|
+
const source = this._model(id);
|
|
519
|
+
modelSize(source);
|
|
520
|
+
if (source._sceneImportPending)
|
|
521
|
+
throw new Error("Wait for the scene to finish opening");
|
|
522
|
+
if (this._models.size + 1 > this.limits.max_archive_entries || [...this._models.values()].reduce(
|
|
523
|
+
(n, m) => n + (m.geometry?.triangles || 0),
|
|
524
|
+
source.geometry.triangles
|
|
525
|
+
) > this.limits.max_triangles)
|
|
526
|
+
throw new Error("Scene model or triangle limit exceeded");
|
|
527
|
+
assertMemory(
|
|
528
|
+
this.limits,
|
|
529
|
+
this._occupiedMemory() + geometryMemory(source.geometry),
|
|
530
|
+
"duplicate"
|
|
531
|
+
);
|
|
532
|
+
const copy = {};
|
|
533
|
+
for (const key of [
|
|
534
|
+
"filename",
|
|
535
|
+
"orig_filename",
|
|
536
|
+
"local_file",
|
|
537
|
+
"x",
|
|
538
|
+
"y",
|
|
539
|
+
"z",
|
|
540
|
+
"rotationx",
|
|
541
|
+
"rotationy",
|
|
542
|
+
"rotationz",
|
|
543
|
+
"opacity",
|
|
544
|
+
"units",
|
|
545
|
+
"display",
|
|
546
|
+
"view_edges",
|
|
547
|
+
"smooth_shading",
|
|
548
|
+
"color"
|
|
549
|
+
])
|
|
550
|
+
copy[key] = source[key];
|
|
551
|
+
copy.id = this.max_model_id + 1;
|
|
552
|
+
if (!Number.isSafeInteger(copy.id))
|
|
553
|
+
throw new Error("Model ID limit exceeded");
|
|
554
|
+
copy.display_name = (source.display_name || source.orig_filename || source.filename || "Model").split(/[\\/]/).pop() + " \u2014 copy";
|
|
555
|
+
copy.center = [...source.center];
|
|
556
|
+
copy.unitScale = 1;
|
|
557
|
+
axes.forEach((a, i) => copy["scale" + a] = source.scaleVector[i]);
|
|
558
|
+
Object.defineProperty(copy, "scaleVector", {
|
|
559
|
+
get: () => axes.map((a) => copy["scale" + a] * copy.unitScale)
|
|
560
|
+
});
|
|
561
|
+
copy.geometry = { ...source.geometry };
|
|
562
|
+
copy.section = source.section ? { ...source.section } : null;
|
|
563
|
+
copy.size_state = structuredClone(sizeState(source));
|
|
564
|
+
copy.size_state.unit_factor = (copy.size_state.unit_factor ?? 1) * source.unitScale;
|
|
565
|
+
const saved = savePlayback(source);
|
|
566
|
+
this.max_model_id = copy.id;
|
|
567
|
+
this._models.set(copy.id, copy);
|
|
568
|
+
this.models_ref[copy.id] = this.models.length;
|
|
569
|
+
this.models.push(copy);
|
|
570
|
+
this.models_count = this._models.size;
|
|
571
|
+
if (saved)
|
|
572
|
+
this._startAnimation(copy, saved.animation, {
|
|
573
|
+
...saved.animation_state,
|
|
574
|
+
paused: true
|
|
575
|
+
});
|
|
576
|
+
this._invalidate();
|
|
577
|
+
return copy.id;
|
|
578
|
+
}
|
|
579
|
+
var cameraDirections = {
|
|
580
|
+
front: [
|
|
581
|
+
[0, -1, 0],
|
|
582
|
+
[0, 0, 1]
|
|
583
|
+
],
|
|
584
|
+
back: [
|
|
585
|
+
[0, 1, 0],
|
|
586
|
+
[0, 0, 1]
|
|
587
|
+
],
|
|
588
|
+
left: [
|
|
589
|
+
[-1, 0, 0],
|
|
590
|
+
[0, 0, 1]
|
|
591
|
+
],
|
|
592
|
+
right: [
|
|
593
|
+
[1, 0, 0],
|
|
594
|
+
[0, 0, 1]
|
|
595
|
+
],
|
|
596
|
+
top: [
|
|
597
|
+
[0, 0, 1],
|
|
598
|
+
[0, 1, 0]
|
|
599
|
+
],
|
|
600
|
+
bottom: [
|
|
601
|
+
[0, 0, -1],
|
|
602
|
+
[0, -1, 0]
|
|
603
|
+
],
|
|
604
|
+
isometric: [
|
|
605
|
+
[1, -1, 1],
|
|
606
|
+
[0, 0, 1]
|
|
607
|
+
]
|
|
608
|
+
};
|
|
609
|
+
function cameraView(name) {
|
|
610
|
+
const view = cameraDirections[name];
|
|
611
|
+
if (!view) throw new Error("Unknown camera view");
|
|
612
|
+
this.camera.position = this.camera.target.map((v, i) => v + view[0][i]);
|
|
613
|
+
this.camera.up = [...view[1]];
|
|
614
|
+
this.set_auto_zoom();
|
|
615
|
+
}
|
|
616
|
+
function validateBookmarks(bookmarks) {
|
|
617
|
+
if (!Array.isArray(bookmarks) || bookmarks.length > 32)
|
|
618
|
+
throw new Error("Save up to 32 camera views");
|
|
619
|
+
const names = /* @__PURE__ */ new Set();
|
|
620
|
+
return bookmarks.map((b) => {
|
|
621
|
+
const name = typeof b?.name === "string" ? b.name.trim() : "";
|
|
622
|
+
if (!name || name.length > 80 || /[\x00-\x1f]/.test(name) || names.has(name))
|
|
623
|
+
throw new Error("Use a unique view name of 1\u201380 characters");
|
|
624
|
+
names.add(name);
|
|
625
|
+
const state = {};
|
|
626
|
+
for (const key of ["position", "target", "up"]) {
|
|
627
|
+
if (!b.camera?.[key] || axes.some(
|
|
628
|
+
(a) => !Number.isFinite(b.camera[key][a]) || Math.abs(b.camera[key][a]) > 1e15
|
|
629
|
+
))
|
|
630
|
+
throw new Error("Invalid saved camera");
|
|
631
|
+
state[key] = Object.fromEntries(axes.map((a) => [a, b.camera[key][a]]));
|
|
632
|
+
}
|
|
633
|
+
const d = axes.map((a) => state.position[a] - state.target[a]), u = axes.map((a) => state.up[a]);
|
|
634
|
+
if (Math.hypot(
|
|
635
|
+
d[1] * u[2] - d[2] * u[1],
|
|
636
|
+
d[2] * u[0] - d[0] * u[2],
|
|
637
|
+
d[0] * u[1] - d[1] * u[0]
|
|
638
|
+
) < 1e-12)
|
|
639
|
+
throw new Error("Degenerate saved camera");
|
|
640
|
+
return { name, camera: state };
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
function mergeBookmarks(existing, incoming) {
|
|
644
|
+
const result = validateBookmarks(existing), names = new Set(result.map((b) => b.name));
|
|
645
|
+
for (const b of validateBookmarks(incoming)) {
|
|
646
|
+
let name = b.name, n = 2;
|
|
647
|
+
while (names.has(name)) name = b.name.slice(0, 70) + ` (${n++})`;
|
|
648
|
+
names.add(name);
|
|
649
|
+
result.push({ ...b, name });
|
|
650
|
+
}
|
|
651
|
+
return validateBookmarks(result);
|
|
652
|
+
}
|
|
653
|
+
async function measureModel(id, options = {}) {
|
|
654
|
+
await this.when_ready;
|
|
655
|
+
const m = this._model(id), g = m.geometry;
|
|
656
|
+
if (!g || m._sceneImportPending)
|
|
657
|
+
throw new Error("Wait for the model to finish opening");
|
|
658
|
+
const scale = m.scaleVector, key = scale.join(","), cached = m._measurements;
|
|
659
|
+
if (options.signal?.aborted)
|
|
660
|
+
throw new DOMException("Measurement cancelled", "AbortError");
|
|
661
|
+
if (cached?.positions === g.positions && cached.key === key)
|
|
662
|
+
return { ...cached.result };
|
|
663
|
+
const working = printMemory(g.triangles), occupied = this._occupiedMemory();
|
|
664
|
+
assertMemory(this.limits, occupied + working, "measurements");
|
|
665
|
+
const release = this._reserveMemory(
|
|
666
|
+
m,
|
|
667
|
+
working + geometryMemory(g),
|
|
668
|
+
"measurements"
|
|
669
|
+
);
|
|
670
|
+
const controller = new AbortController(), cancel = () => controller.abort();
|
|
671
|
+
this._aborters.add(controller);
|
|
672
|
+
(m._measureJobs ??= /* @__PURE__ */ new Set()).add(controller);
|
|
673
|
+
options.signal?.addEventListener("abort", cancel, { once: true });
|
|
674
|
+
let job, callbackError;
|
|
675
|
+
const check = () => {
|
|
676
|
+
if (controller.signal.aborted || this.killsign)
|
|
677
|
+
throw new DOMException("Measurement cancelled", "AbortError");
|
|
678
|
+
if (this._models.get(id) !== m || m.geometry.positions !== g.positions || m.scaleVector.join(",") !== key)
|
|
679
|
+
throw new Error("Model size changed. Calculate again.");
|
|
680
|
+
};
|
|
681
|
+
try {
|
|
682
|
+
const positions = new Float32Array(g.positions.length);
|
|
683
|
+
for (let start = 0; start < positions.length; start += 262144) {
|
|
684
|
+
check();
|
|
685
|
+
positions.set(g.positions.subarray(start, start + 262144), start);
|
|
686
|
+
if (start + 262144 < positions.length)
|
|
687
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
688
|
+
}
|
|
689
|
+
job = this._pool.run(
|
|
690
|
+
{
|
|
691
|
+
kind: "measure",
|
|
692
|
+
positions,
|
|
693
|
+
scale,
|
|
694
|
+
area: g.area,
|
|
695
|
+
max_time_ms: options.max_time_ms ?? 3e4,
|
|
696
|
+
limits: {
|
|
697
|
+
...this.limits,
|
|
698
|
+
max_memory_bytes: this.limits.max_memory_bytes - occupied
|
|
699
|
+
}
|
|
700
|
+
},
|
|
701
|
+
(progress) => {
|
|
702
|
+
try {
|
|
703
|
+
options.on_progress?.(progress);
|
|
704
|
+
} catch (error) {
|
|
705
|
+
callbackError = error;
|
|
706
|
+
controller.abort();
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
[positions.buffer]
|
|
710
|
+
);
|
|
711
|
+
controller.signal.addEventListener("abort", () => job.cancel(), {
|
|
712
|
+
once: true
|
|
713
|
+
});
|
|
714
|
+
check();
|
|
715
|
+
let result;
|
|
716
|
+
try {
|
|
717
|
+
result = await job;
|
|
718
|
+
} catch (error) {
|
|
719
|
+
throw callbackError || error;
|
|
720
|
+
}
|
|
721
|
+
check();
|
|
722
|
+
m._measurements = { positions: g.positions, key, result };
|
|
723
|
+
return { ...result };
|
|
724
|
+
} finally {
|
|
725
|
+
job?.cancel?.();
|
|
726
|
+
release();
|
|
727
|
+
this._aborters.delete(controller);
|
|
728
|
+
m._measureJobs.delete(controller);
|
|
729
|
+
options.signal?.removeEventListener("abort", cancel);
|
|
213
730
|
}
|
|
214
|
-
return true;
|
|
215
|
-
}
|
|
216
|
-
function rayTriangle(o, d, p, i) {
|
|
217
|
-
const a = [p[i], p[i + 1], p[i + 2]], b = sub([p[i + 3], p[i + 4], p[i + 5]], a), c = sub([p[i + 6], p[i + 7], p[i + 8]], a);
|
|
218
|
-
const h = cross(d, c), det = dot(b, h);
|
|
219
|
-
if (Math.abs(det) < 1e-12) return Infinity;
|
|
220
|
-
const s = sub(o, a), u = dot(s, h) / det;
|
|
221
|
-
if (u < 0 || u > 1) return Infinity;
|
|
222
|
-
const q = cross(s, b), v = dot(d, q) / det;
|
|
223
|
-
if (v < 0 || u + v > 1) return Infinity;
|
|
224
|
-
const t = dot(c, q) / det;
|
|
225
|
-
return t >= 0 ? t : Infinity;
|
|
226
731
|
}
|
|
227
732
|
|
|
228
|
-
// src/
|
|
229
|
-
var
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
733
|
+
// src/appearance.js
|
|
734
|
+
var DISPLAYS = [
|
|
735
|
+
"flat",
|
|
736
|
+
"smooth",
|
|
737
|
+
"wireframe",
|
|
738
|
+
"edges",
|
|
739
|
+
"mesh",
|
|
740
|
+
"xray",
|
|
741
|
+
"studio",
|
|
742
|
+
"orientation"
|
|
743
|
+
];
|
|
744
|
+
function validateView(display, smooth) {
|
|
745
|
+
if (!DISPLAYS.includes(display)) throw new Error("Invalid display mode");
|
|
746
|
+
if (typeof smooth !== "boolean")
|
|
747
|
+
throw new Error("smooth_shading must be a boolean");
|
|
748
|
+
return { display, smooth_shading: display === "smooth" || smooth };
|
|
234
749
|
}
|
|
235
|
-
function
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
750
|
+
function viewFeatures(view) {
|
|
751
|
+
return {
|
|
752
|
+
smooth: (view.smooth_shading || view.display === "smooth") && view.display !== "wireframe",
|
|
753
|
+
edges: view.view_edges || view.display === "edges",
|
|
754
|
+
wire: view.display === "wireframe",
|
|
755
|
+
edgeAngle: view.display === "edges" ? 30 : 1
|
|
756
|
+
};
|
|
240
757
|
}
|
|
241
|
-
function
|
|
242
|
-
|
|
758
|
+
function validateSection(section) {
|
|
759
|
+
if (section == null) return null;
|
|
760
|
+
if (!["x", "y", "z"].includes(section.axis))
|
|
761
|
+
throw new Error("Section axis must be x, y or z");
|
|
762
|
+
if (!Number.isFinite(section.position) || section.position < 0 || section.position > 1)
|
|
763
|
+
throw new Error("Section position must be between 0 and 1");
|
|
764
|
+
if (section.flip !== void 0 && typeof section.flip !== "boolean")
|
|
765
|
+
throw new Error("Section flip must be a boolean");
|
|
766
|
+
return {
|
|
767
|
+
axis: section.axis,
|
|
768
|
+
position: section.position,
|
|
769
|
+
flip: section.flip ?? false
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
function sectionPlane(bounds, section) {
|
|
773
|
+
if (!section) return [0, 0, 0, 0];
|
|
774
|
+
const axis = ["x", "y", "z"].indexOf(section.axis), sign = section.flip ? -1 : 1;
|
|
775
|
+
const plane = [0, 0, 0, 0];
|
|
776
|
+
plane[axis] = sign;
|
|
777
|
+
plane[3] = -sign * (bounds.min[axis] + (bounds.max[axis] - bounds.min[axis]) * section.position);
|
|
778
|
+
return plane;
|
|
243
779
|
}
|
|
244
780
|
|
|
245
781
|
// src/geometry.js
|
|
@@ -536,6 +1072,7 @@ var Renderer = class {
|
|
|
536
1072
|
this.canvas = canvas;
|
|
537
1073
|
this.domElement = canvas;
|
|
538
1074
|
this.resources = /* @__PURE__ */ new Map();
|
|
1075
|
+
this.sharedResources = /* @__PURE__ */ new WeakMap();
|
|
539
1076
|
this.frames = 0;
|
|
540
1077
|
this.lost = false;
|
|
541
1078
|
this.gl = canvas.getContext("webgl2", {
|
|
@@ -552,6 +1089,7 @@ var Renderer = class {
|
|
|
552
1089
|
e.preventDefault();
|
|
553
1090
|
this.lost = true;
|
|
554
1091
|
this.resources.clear();
|
|
1092
|
+
this.sharedResources = /* @__PURE__ */ new WeakMap();
|
|
555
1093
|
this.gridResource = null;
|
|
556
1094
|
this.infiniteGrid = null;
|
|
557
1095
|
this.selectionResource = null;
|
|
@@ -659,7 +1197,12 @@ var Renderer = class {
|
|
|
659
1197
|
if (this.diagnostic?.model === model) this.setDiagnostic(null);
|
|
660
1198
|
const resources = this.resources.get(model);
|
|
661
1199
|
if (resources)
|
|
662
|
-
for (const resource of Object.values(resources))
|
|
1200
|
+
for (const resource of Object.values(resources)) {
|
|
1201
|
+
if (--resource.users === 0) {
|
|
1202
|
+
this.sharedResources.get(resource.source)?.delete(resource);
|
|
1203
|
+
this.free(resource);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
663
1206
|
this.resources.delete(model);
|
|
664
1207
|
}
|
|
665
1208
|
clear() {
|
|
@@ -673,8 +1216,10 @@ var Renderer = class {
|
|
|
673
1216
|
}
|
|
674
1217
|
get liveBuffers() {
|
|
675
1218
|
let n = (this.gridResource?.buffers.length || 0) + (this.selectionResource?.buffers.length || 0) + (this.diagnosticResource?.buffers.length || 0);
|
|
1219
|
+
const unique = /* @__PURE__ */ new Set();
|
|
676
1220
|
for (const r of this.resources.values())
|
|
677
|
-
for (const b of Object.values(r))
|
|
1221
|
+
for (const b of Object.values(r)) unique.add(b);
|
|
1222
|
+
for (const b of unique) n += b.buffers.length;
|
|
678
1223
|
return n;
|
|
679
1224
|
}
|
|
680
1225
|
setDiagnostic(value) {
|
|
@@ -740,12 +1285,23 @@ var Renderer = class {
|
|
|
740
1285
|
this.resources.set(model, r);
|
|
741
1286
|
}
|
|
742
1287
|
if (!r[mode]) {
|
|
743
|
-
const g = model.geometry;
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
1288
|
+
const g = model.geometry, lines = mode === "edges" || mode === "wire", source = lines ? g[mode] : g.positions, normals = lines ? void 0 : mode === "smooth" ? g.smoothNormals : g.normals, colors = lines ? void 0 : g.colors;
|
|
1289
|
+
let entries = this.sharedResources.get(source);
|
|
1290
|
+
if (!entries) this.sharedResources.set(source, entries = /* @__PURE__ */ new Set());
|
|
1291
|
+
let resource = [...entries].find(
|
|
1292
|
+
(r2) => r2.normals === normals && r2.colors === colors
|
|
748
1293
|
);
|
|
1294
|
+
if (!resource) {
|
|
1295
|
+
resource = Object.assign(this.upload(source, normals, colors), {
|
|
1296
|
+
source,
|
|
1297
|
+
normals,
|
|
1298
|
+
colors,
|
|
1299
|
+
users: 0
|
|
1300
|
+
});
|
|
1301
|
+
entries.add(resource);
|
|
1302
|
+
}
|
|
1303
|
+
resource.users++;
|
|
1304
|
+
r[mode] = resource;
|
|
749
1305
|
}
|
|
750
1306
|
return r[mode];
|
|
751
1307
|
}
|
|
@@ -1088,205 +1644,34 @@ var WorkerPool = class {
|
|
|
1088
1644
|
return;
|
|
1089
1645
|
}
|
|
1090
1646
|
end(data.error ? new Error(data.error) : null, data.result);
|
|
1091
|
-
};
|
|
1092
|
-
job.worker.onerror = (e) => {
|
|
1093
|
-
e.preventDefault();
|
|
1094
|
-
end(
|
|
1095
|
-
new Error(
|
|
1096
|
-
e.message || "Worker failed; check worker URL and Content Security Policy"
|
|
1097
|
-
)
|
|
1098
|
-
);
|
|
1099
|
-
};
|
|
1100
|
-
job.worker.onmessageerror = () => end(new Error("Invalid worker response"));
|
|
1101
|
-
job.timer = setTimeout(
|
|
1102
|
-
() => end(new Error("File processing timed out")),
|
|
1103
|
-
this.timeout
|
|
1104
|
-
);
|
|
1105
|
-
job.worker.postMessage({ id: job.id, task: job.task }, job.transfer);
|
|
1106
|
-
} catch (error) {
|
|
1107
|
-
end(error);
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
clear() {
|
|
1112
|
-
for (const job of [...this.queue, ...this.active]) this.cancel(job);
|
|
1113
|
-
}
|
|
1114
|
-
dispose() {
|
|
1115
|
-
this.disposed = true;
|
|
1116
|
-
this.clear();
|
|
1117
|
-
}
|
|
1118
|
-
};
|
|
1119
|
-
|
|
1120
|
-
// src/animation.js
|
|
1121
|
-
var animationKeys = [
|
|
1122
|
-
"x",
|
|
1123
|
-
"y",
|
|
1124
|
-
"z",
|
|
1125
|
-
"rotationx",
|
|
1126
|
-
"rotationy",
|
|
1127
|
-
"rotationz",
|
|
1128
|
-
"scalex",
|
|
1129
|
-
"scaley",
|
|
1130
|
-
"scalez"
|
|
1131
|
-
];
|
|
1132
|
-
var isScale = (key) => key.startsWith("scale");
|
|
1133
|
-
var number = (value, name, positive2 = false) => {
|
|
1134
|
-
if (typeof value !== "number" || !Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER || positive2 && value <= 0)
|
|
1135
|
-
throw new Error(`Invalid animation ${name}`);
|
|
1136
|
-
return value;
|
|
1137
|
-
};
|
|
1138
|
-
var object = (value, name) => {
|
|
1139
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1140
|
-
throw new Error(`Invalid animation ${name}`);
|
|
1141
|
-
return value;
|
|
1142
|
-
};
|
|
1143
|
-
function animationPose(model) {
|
|
1144
|
-
return Object.fromEntries(
|
|
1145
|
-
animationKeys.map((key) => [
|
|
1146
|
-
key,
|
|
1147
|
-
number(
|
|
1148
|
-
model[key] ?? (isScale(key) ? model.scale ?? 1 : 0),
|
|
1149
|
-
key,
|
|
1150
|
-
isScale(key)
|
|
1151
|
-
)
|
|
1152
|
-
])
|
|
1153
|
-
);
|
|
1154
|
-
}
|
|
1155
|
-
function normalizeAnimation(input) {
|
|
1156
|
-
object(input, "definition");
|
|
1157
|
-
const result = {}, claimed = /* @__PURE__ */ new Set();
|
|
1158
|
-
for (const mode of ["delta", "exact"]) {
|
|
1159
|
-
if (input[mode] == null) continue;
|
|
1160
|
-
const source = object(input[mode], mode), values = {};
|
|
1161
|
-
const msec = number(source.msec ?? 300, "duration", true);
|
|
1162
|
-
if (source.loop !== void 0 && typeof source.loop !== "boolean")
|
|
1163
|
-
throw new Error("Animation loop must be a boolean");
|
|
1164
|
-
const loop = mode === "delta" && !!source.loop;
|
|
1165
|
-
for (const key of animationKeys) {
|
|
1166
|
-
const value = source[key] ?? (isScale(key) ? source.scale : void 0);
|
|
1167
|
-
if (value === void 0) continue;
|
|
1168
|
-
if (claimed.has(key))
|
|
1169
|
-
throw new Error(`Animation delta/exact conflict: ${key}`);
|
|
1170
|
-
number(value, key, mode === "exact" && isScale(key));
|
|
1171
|
-
if (loop && isScale(key) && value < 0)
|
|
1172
|
-
throw new Error("Looping scale deltas cannot be negative");
|
|
1173
|
-
values[key] = value;
|
|
1174
|
-
claimed.add(key);
|
|
1175
|
-
}
|
|
1176
|
-
if (!Object.keys(values).length)
|
|
1177
|
-
throw new Error("Animation track needs a transform");
|
|
1178
|
-
result[mode] = { ...values, msec, ...loop ? { loop: true } : {} };
|
|
1179
|
-
}
|
|
1180
|
-
if (!Object.keys(result).length)
|
|
1181
|
-
throw new Error("Animation needs delta or exact");
|
|
1182
|
-
return result;
|
|
1183
|
-
}
|
|
1184
|
-
function normalizePlaybackState(input) {
|
|
1185
|
-
object(input, "playback state");
|
|
1186
|
-
if (input.version !== 1 || typeof input.paused !== "boolean")
|
|
1187
|
-
throw new Error("Invalid animation playback state version or paused flag");
|
|
1188
|
-
const elapsed_ms = number(input.elapsed_ms, "elapsed time");
|
|
1189
|
-
if (elapsed_ms < 0) throw new Error("Invalid animation elapsed time");
|
|
1190
|
-
object(input.origin, "origin");
|
|
1191
|
-
for (const key of animationKeys) number(input.origin[key], key, isScale(key));
|
|
1192
|
-
if (!Array.isArray(input.center) || input.center.length !== 3)
|
|
1193
|
-
throw new Error("Invalid animation pivot");
|
|
1194
|
-
return {
|
|
1195
|
-
version: 1,
|
|
1196
|
-
elapsed_ms,
|
|
1197
|
-
paused: input.paused,
|
|
1198
|
-
origin: animationPose(input.origin),
|
|
1199
|
-
center: input.center.map((v) => number(v, "pivot"))
|
|
1200
|
-
};
|
|
1201
|
-
}
|
|
1202
|
-
function createPlayback(definition, model, saved) {
|
|
1203
|
-
const animation = normalizeAnimation(definition);
|
|
1204
|
-
const state = saved == null ? null : normalizePlaybackState(saved);
|
|
1205
|
-
const origin = state?.origin ?? animationPose(model);
|
|
1206
|
-
const tracks = Object.entries(animation).map(([mode, a]) => {
|
|
1207
|
-
const values = Object.entries(a).filter(
|
|
1208
|
-
([key]) => animationKeys.includes(key)
|
|
1209
|
-
);
|
|
1210
|
-
if (mode === "delta")
|
|
1211
|
-
for (const [key, value] of values)
|
|
1212
|
-
number(origin[key] + value, `${key} endpoint`, isScale(key));
|
|
1213
|
-
return { mode, values, duration: a.msec, loop: !!a.loop, done: false };
|
|
1214
|
-
});
|
|
1215
|
-
const playback = {
|
|
1216
|
-
animation,
|
|
1217
|
-
origin,
|
|
1218
|
-
tracks,
|
|
1219
|
-
elapsed_ms: state?.elapsed_ms ?? 0,
|
|
1220
|
-
paused: state?.paused ?? false,
|
|
1221
|
-
last_time: null
|
|
1222
|
-
};
|
|
1223
|
-
evaluatePlayback({ ...playback, tracks: tracks.map((t) => ({ ...t })) }, {});
|
|
1224
|
-
return playback;
|
|
1225
|
-
}
|
|
1226
|
-
function evaluatePlayback(playback, model) {
|
|
1227
|
-
const updates = [];
|
|
1228
|
-
for (const track of playback.tracks) {
|
|
1229
|
-
if (track.done) continue;
|
|
1230
|
-
const fraction = playback.elapsed_ms / track.duration;
|
|
1231
|
-
const p = track.loop ? fraction : Math.min(fraction, 1);
|
|
1232
|
-
for (const [key, value] of track.values) {
|
|
1233
|
-
const from = playback.origin[key];
|
|
1234
|
-
const to = track.mode === "delta" ? from + value * p : from * (1 - p) + value * p;
|
|
1235
|
-
updates.push([key, number(to, `${key} result`, isScale(key))]);
|
|
1647
|
+
};
|
|
1648
|
+
job.worker.onerror = (e) => {
|
|
1649
|
+
e.preventDefault();
|
|
1650
|
+
end(
|
|
1651
|
+
new Error(
|
|
1652
|
+
e.message || "Worker failed; check worker URL and Content Security Policy"
|
|
1653
|
+
)
|
|
1654
|
+
);
|
|
1655
|
+
};
|
|
1656
|
+
job.worker.onmessageerror = () => end(new Error("Invalid worker response"));
|
|
1657
|
+
job.timer = setTimeout(
|
|
1658
|
+
() => end(new Error("File processing timed out")),
|
|
1659
|
+
this.timeout
|
|
1660
|
+
);
|
|
1661
|
+
job.worker.postMessage({ id: job.id, task: job.task }, job.transfer);
|
|
1662
|
+
} catch (error) {
|
|
1663
|
+
end(error);
|
|
1664
|
+
}
|
|
1236
1665
|
}
|
|
1237
1666
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
scaleChanged ||= isScale(key) && model[key] !== value;
|
|
1241
|
-
model[key] = value;
|
|
1667
|
+
clear() {
|
|
1668
|
+
for (const job of [...this.queue, ...this.active]) this.cancel(job);
|
|
1242
1669
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
return playback.tracks.some((track) => !track.done);
|
|
1247
|
-
}
|
|
1248
|
-
function advancePlayback(playback, model, time) {
|
|
1249
|
-
if (playback.paused) return true;
|
|
1250
|
-
if (playback.last_time !== null)
|
|
1251
|
-
playback.elapsed_ms = number(
|
|
1252
|
-
playback.elapsed_ms + Math.max(0, time - playback.last_time),
|
|
1253
|
-
"elapsed time"
|
|
1254
|
-
);
|
|
1255
|
-
playback.last_time = time;
|
|
1256
|
-
return evaluatePlayback(playback, model);
|
|
1257
|
-
}
|
|
1258
|
-
function playbackState(playback, suspended = false) {
|
|
1259
|
-
return {
|
|
1260
|
-
status: playback.paused ? "paused" : "running",
|
|
1261
|
-
elapsed_ms: playback.elapsed_ms,
|
|
1262
|
-
suspended,
|
|
1263
|
-
tracks: playback.tracks.filter((t) => !t.done).map((t) => ({
|
|
1264
|
-
mode: t.mode,
|
|
1265
|
-
msec: t.duration,
|
|
1266
|
-
loop: t.loop,
|
|
1267
|
-
progress: t.loop ? playback.elapsed_ms % t.duration / t.duration : Math.min(1, playback.elapsed_ms / t.duration)
|
|
1268
|
-
}))
|
|
1269
|
-
};
|
|
1270
|
-
}
|
|
1271
|
-
function savePlayback(model) {
|
|
1272
|
-
const p = model._animation;
|
|
1273
|
-
if (!p) return null;
|
|
1274
|
-
const factor = model.unitScale ?? 1;
|
|
1275
|
-
const animation = structuredClone(p.animation), origin = { ...p.origin };
|
|
1276
|
-
for (const key of animationKeys.filter(isScale)) {
|
|
1277
|
-
origin[key] *= factor;
|
|
1278
|
-
for (const track of Object.values(animation))
|
|
1279
|
-
if (track[key] !== void 0) track[key] *= factor;
|
|
1670
|
+
dispose() {
|
|
1671
|
+
this.disposed = true;
|
|
1672
|
+
this.clear();
|
|
1280
1673
|
}
|
|
1281
|
-
|
|
1282
|
-
version: 1,
|
|
1283
|
-
origin,
|
|
1284
|
-
elapsed_ms: p.elapsed_ms,
|
|
1285
|
-
paused: p.paused,
|
|
1286
|
-
center: [...model.center]
|
|
1287
|
-
});
|
|
1288
|
-
return { animation: normalizeAnimation(animation), animation_state };
|
|
1289
|
-
}
|
|
1674
|
+
};
|
|
1290
1675
|
|
|
1291
1676
|
// src/scene-import.js
|
|
1292
1677
|
var sceneFormat = (name) => String(name || "").match(/\.(vsb|vsj)(?:[?#]|$)/i)?.[1].toLowerCase() || "";
|
|
@@ -1347,8 +1732,8 @@ function importScene(source, options = {}) {
|
|
|
1347
1732
|
await this.when_ready;
|
|
1348
1733
|
check();
|
|
1349
1734
|
oldCamera = this.get_camera_state();
|
|
1350
|
-
const
|
|
1351
|
-
if (!["vsb", "vsj"].includes(
|
|
1735
|
+
const format2 = options.format || sceneFormat(typeof source === "string" ? source : source.name);
|
|
1736
|
+
if (!["vsb", "vsj"].includes(format2))
|
|
1352
1737
|
throw new Error("Choose VSB or VSJ for a scene");
|
|
1353
1738
|
let remote, relay;
|
|
1354
1739
|
if (typeof source === "string") {
|
|
@@ -1381,8 +1766,8 @@ function importScene(source, options = {}) {
|
|
|
1381
1766
|
source,
|
|
1382
1767
|
remote,
|
|
1383
1768
|
relay,
|
|
1384
|
-
format,
|
|
1385
|
-
name: `scene.${
|
|
1769
|
+
format: format2,
|
|
1770
|
+
name: `scene.${format2}`,
|
|
1386
1771
|
limits
|
|
1387
1772
|
},
|
|
1388
1773
|
(p) => {
|
|
@@ -1403,6 +1788,10 @@ function importScene(source, options = {}) {
|
|
|
1403
1788
|
job = null;
|
|
1404
1789
|
check();
|
|
1405
1790
|
const { scene, files, base } = documentData;
|
|
1791
|
+
const bookmarks = mergeBookmarks(
|
|
1792
|
+
this.get_camera_bookmarks(),
|
|
1793
|
+
scene.camera_bookmarks || []
|
|
1794
|
+
);
|
|
1406
1795
|
this._sceneBytes -= held;
|
|
1407
1796
|
held = Object.values(files || {}).reduce((n, b) => n + b.byteLength, 0) + JSON.stringify(scene).length * 2;
|
|
1408
1797
|
this._sceneBytes += held;
|
|
@@ -1494,6 +1883,8 @@ function importScene(source, options = {}) {
|
|
|
1494
1883
|
if (scene.auto_rotate !== void 0)
|
|
1495
1884
|
this.set_auto_rotate(scene.auto_rotate);
|
|
1496
1885
|
this._restoreCamera = scene.camera_state || (hadModels ? oldCamera : null);
|
|
1886
|
+
this.camera_bookmarks = bookmarks;
|
|
1887
|
+
if (scene.display_units) this.display_units = scene.display_units;
|
|
1497
1888
|
committed = true;
|
|
1498
1889
|
return { model_ids: ids, scene, used_files: [...usedFiles] };
|
|
1499
1890
|
})().catch((error) => {
|
|
@@ -1514,61 +1905,6 @@ function importScene(source, options = {}) {
|
|
|
1514
1905
|
return promise;
|
|
1515
1906
|
}
|
|
1516
1907
|
|
|
1517
|
-
// src/print-options.js
|
|
1518
|
-
function printOptions(input = {}) {
|
|
1519
|
-
const level = input.level ?? "basic";
|
|
1520
|
-
if (!["basic", "detailed"].includes(level))
|
|
1521
|
-
throw new Error("Choose basic or detailed print checks");
|
|
1522
|
-
const number2 = (v, name, max = 1e9) => {
|
|
1523
|
-
if (!Number.isFinite(v) || v <= 0 || v > max)
|
|
1524
|
-
throw new Error(`Invalid ${name}`);
|
|
1525
|
-
return v;
|
|
1526
|
-
};
|
|
1527
|
-
let profile = null;
|
|
1528
|
-
if (input.profile != null) {
|
|
1529
|
-
if (input.profile.technology !== "fdm")
|
|
1530
|
-
throw new Error("Only FDM profiles are supported");
|
|
1531
|
-
profile = { technology: "fdm" };
|
|
1532
|
-
for (const key of ["min_wall_mm", "line_width_mm"])
|
|
1533
|
-
if (input.profile[key] != null)
|
|
1534
|
-
profile[key] = number2(input.profile[key], key);
|
|
1535
|
-
if (input.profile.overhang_degrees != null)
|
|
1536
|
-
profile.overhang_degrees = number2(
|
|
1537
|
-
input.profile.overhang_degrees,
|
|
1538
|
-
"overhang angle",
|
|
1539
|
-
89.9
|
|
1540
|
-
);
|
|
1541
|
-
if (input.profile.build_volume_mm != null) {
|
|
1542
|
-
if (!Array.isArray(input.profile.build_volume_mm) || input.profile.build_volume_mm.length !== 3)
|
|
1543
|
-
throw new Error("Build volume requires X, Y and Z");
|
|
1544
|
-
profile.build_volume_mm = input.profile.build_volume_mm.map(
|
|
1545
|
-
(v) => number2(v, "build volume")
|
|
1546
|
-
);
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
const time = number2(input.max_time_ms ?? 3e4, "check time limit", 12e4);
|
|
1550
|
-
return {
|
|
1551
|
-
level,
|
|
1552
|
-
profile,
|
|
1553
|
-
apply_transforms: input.apply_transforms !== false,
|
|
1554
|
-
units_confirmed: input.units_confirmed === true,
|
|
1555
|
-
max_time_ms: time
|
|
1556
|
-
};
|
|
1557
|
-
}
|
|
1558
|
-
var printMemory = (triangles) => 64 * 1024 ** 2 + triangles * 2400;
|
|
1559
|
-
var printSignature = (model) => JSON.stringify([
|
|
1560
|
-
model.x,
|
|
1561
|
-
model.y,
|
|
1562
|
-
model.z,
|
|
1563
|
-
model.rotationx,
|
|
1564
|
-
model.rotationy,
|
|
1565
|
-
model.rotationz,
|
|
1566
|
-
model.scaleVector,
|
|
1567
|
-
model.center,
|
|
1568
|
-
model.units,
|
|
1569
|
-
model.unitScale
|
|
1570
|
-
]);
|
|
1571
|
-
|
|
1572
1908
|
// src/print-controller.js
|
|
1573
1909
|
var reports = /* @__PURE__ */ new WeakMap();
|
|
1574
1910
|
function reportCurrent(viewer, report) {
|
|
@@ -1666,7 +2002,9 @@ async function checkPrintability(options = {}) {
|
|
|
1666
2002
|
rotationy: 0,
|
|
1667
2003
|
rotationz: 0,
|
|
1668
2004
|
center: [0, 0, 0],
|
|
1669
|
-
scaleVector: [
|
|
2005
|
+
scaleVector: [1, 1, 1].map(
|
|
2006
|
+
() => model.unitScale * (model.size_state?.unit_factor ?? 1)
|
|
2007
|
+
)
|
|
1670
2008
|
};
|
|
1671
2009
|
progress({
|
|
1672
2010
|
phase: "Starting",
|
|
@@ -1790,8 +2128,8 @@ function exportFilename(filename, extension) {
|
|
|
1790
2128
|
function exportOptions(options = {}) {
|
|
1791
2129
|
const aliases = { stp: "step", igs: "iges" };
|
|
1792
2130
|
const requested = String(options.format || "").toLowerCase();
|
|
1793
|
-
const
|
|
1794
|
-
if (!["stl", "obj", "3mf", "step", "iges"].includes(
|
|
2131
|
+
const format2 = aliases[requested] || requested;
|
|
2132
|
+
if (!["stl", "obj", "3mf", "step", "iges"].includes(format2))
|
|
1795
2133
|
throw new Error("Export format must be stl, obj, 3mf, step or iges");
|
|
1796
2134
|
const units = options.units ?? "mm";
|
|
1797
2135
|
if (!["mm", "inch"].includes(units))
|
|
@@ -1801,10 +2139,10 @@ function exportOptions(options = {}) {
|
|
|
1801
2139
|
throw new Error(`${key} must be a boolean`);
|
|
1802
2140
|
if (options.on_progress !== void 0 && typeof options.on_progress !== "function")
|
|
1803
2141
|
throw new Error("on_progress must be a function");
|
|
1804
|
-
const colors = options.colors !== false && ["obj", "3mf"].includes(
|
|
1805
|
-
const extension =
|
|
2142
|
+
const colors = options.colors !== false && ["obj", "3mf"].includes(format2);
|
|
2143
|
+
const extension = format2 === "obj" && colors ? "zip" : format2;
|
|
1806
2144
|
return {
|
|
1807
|
-
format,
|
|
2145
|
+
format: format2,
|
|
1808
2146
|
units,
|
|
1809
2147
|
colors,
|
|
1810
2148
|
apply_transforms: options.apply_transforms !== false,
|
|
@@ -1997,7 +2335,9 @@ var modelKeys = [
|
|
|
1997
2335
|
"section",
|
|
1998
2336
|
"units",
|
|
1999
2337
|
"animation",
|
|
2000
|
-
"animation_state"
|
|
2338
|
+
"animation_state",
|
|
2339
|
+
"size_state",
|
|
2340
|
+
"display_name"
|
|
2001
2341
|
];
|
|
2002
2342
|
var finite = (v, name) => {
|
|
2003
2343
|
if (typeof v !== "number" || !Number.isFinite(v))
|
|
@@ -2029,7 +2369,7 @@ function modelOptions(input) {
|
|
|
2029
2369
|
return out;
|
|
2030
2370
|
}
|
|
2031
2371
|
var StlViewer = class _StlViewer {
|
|
2032
|
-
static version = "2.0.0-rc.
|
|
2372
|
+
static version = "2.0.0-rc.6";
|
|
2033
2373
|
static workerURL = null;
|
|
2034
2374
|
constructor(parent, options = {}) {
|
|
2035
2375
|
if (!parent?.appendChild)
|
|
@@ -2058,6 +2398,8 @@ var StlViewer = class _StlViewer {
|
|
|
2058
2398
|
this._sessionActive = false;
|
|
2059
2399
|
this._frame = 0;
|
|
2060
2400
|
this._lastTime = 0;
|
|
2401
|
+
this.camera_bookmarks = [];
|
|
2402
|
+
this.display_units = "mm";
|
|
2061
2403
|
this.grid = null;
|
|
2062
2404
|
this._gridAuto = false;
|
|
2063
2405
|
this.camera = {
|
|
@@ -2394,6 +2736,13 @@ var StlViewer = class _StlViewer {
|
|
|
2394
2736
|
finite(m[key], key);
|
|
2395
2737
|
if (key.startsWith("scale")) positive(m[key], key);
|
|
2396
2738
|
}
|
|
2739
|
+
if (supplied.size_state)
|
|
2740
|
+
m.size_state = validateSizeState(supplied.size_state);
|
|
2741
|
+
if (supplied.display_name != null) {
|
|
2742
|
+
if (typeof supplied.display_name !== "string" || supplied.display_name.length > 1024)
|
|
2743
|
+
throw new Error("Invalid model name");
|
|
2744
|
+
m.display_name = supplied.display_name;
|
|
2745
|
+
}
|
|
2397
2746
|
m.opacity = supplied.opacity ?? supplied.mesh?.material?.opacity ?? 1;
|
|
2398
2747
|
finite(m.opacity, "opacity");
|
|
2399
2748
|
if (m.opacity < 0 || m.opacity > 1)
|
|
@@ -2545,7 +2894,7 @@ var StlViewer = class _StlViewer {
|
|
|
2545
2894
|
format: supplied.remote_format,
|
|
2546
2895
|
relay,
|
|
2547
2896
|
source: typeof source === "string" ? new URL(source, document.baseURI).href : source,
|
|
2548
|
-
name: m.filename,
|
|
2897
|
+
name: m.display_name || m.filename,
|
|
2549
2898
|
limits,
|
|
2550
2899
|
features: features.smooth || features.edges || features.wire ? features : null
|
|
2551
2900
|
},
|
|
@@ -2595,6 +2944,7 @@ var StlViewer = class _StlViewer {
|
|
|
2595
2944
|
}
|
|
2596
2945
|
_deleteModel(m) {
|
|
2597
2946
|
this._stopAnimation(m, "removed");
|
|
2947
|
+
for (const controller of m._measureJobs || []) controller.abort();
|
|
2598
2948
|
for (const [controller, models] of this._printJobs || [])
|
|
2599
2949
|
if (models.has(m)) controller.abort();
|
|
2600
2950
|
this._printCache = null;
|
|
@@ -2622,6 +2972,8 @@ var StlViewer = class _StlViewer {
|
|
|
2622
2972
|
if (this.killsign) return;
|
|
2623
2973
|
this._generation++;
|
|
2624
2974
|
this._printCache = null;
|
|
2975
|
+
this.camera_bookmarks = [];
|
|
2976
|
+
this.display_units = "mm";
|
|
2625
2977
|
this._pool?.clear();
|
|
2626
2978
|
for (const c of this._aborters) c.abort();
|
|
2627
2979
|
this._aborters.clear();
|
|
@@ -3061,12 +3413,42 @@ var StlViewer = class _StlViewer {
|
|
|
3061
3413
|
type
|
|
3062
3414
|
);
|
|
3063
3415
|
}
|
|
3064
|
-
|
|
3416
|
+
get_model_size(id) {
|
|
3417
|
+
return modelSize(this._model(id));
|
|
3418
|
+
}
|
|
3419
|
+
resize_model(id, factors) {
|
|
3420
|
+
return resizeModel.call(this, id, factors);
|
|
3421
|
+
}
|
|
3422
|
+
set_file_units(id, units) {
|
|
3423
|
+
return setFileUnits.call(this, id, units);
|
|
3424
|
+
}
|
|
3425
|
+
reset_model_size(id) {
|
|
3426
|
+
return resetModelSize.call(this, id);
|
|
3427
|
+
}
|
|
3428
|
+
duplicate_model(id) {
|
|
3429
|
+
return duplicateModel.call(this, id);
|
|
3430
|
+
}
|
|
3431
|
+
set_camera_view(name) {
|
|
3432
|
+
return cameraView.call(this, name);
|
|
3433
|
+
}
|
|
3434
|
+
set_display_units(units) {
|
|
3435
|
+
this.display_units = validateUnits(units);
|
|
3436
|
+
}
|
|
3437
|
+
get_camera_bookmarks() {
|
|
3438
|
+
return structuredClone(this.camera_bookmarks || []);
|
|
3439
|
+
}
|
|
3440
|
+
set_camera_bookmarks(bookmarks) {
|
|
3441
|
+
this.camera_bookmarks = validateBookmarks(bookmarks);
|
|
3442
|
+
}
|
|
3443
|
+
measure_model(id, options) {
|
|
3444
|
+
return measureModel.call(this, id, options);
|
|
3445
|
+
}
|
|
3446
|
+
get_model_info(id, options = {}) {
|
|
3065
3447
|
return this._guard(() => {
|
|
3066
3448
|
const m = this._model(id), g = m.geometry;
|
|
3067
3449
|
if (!g) return null;
|
|
3068
3450
|
const s = m.scaleVector, key = s.join(",");
|
|
3069
|
-
if (!m._stats || m._stats.key !== key)
|
|
3451
|
+
if (options.measurements !== false && (!m._stats || m._stats.key !== key))
|
|
3070
3452
|
m._stats = { key, ...scaledStats(g, s) };
|
|
3071
3453
|
const factor = m.units === "inch" ? 1 / 25.4 : 1;
|
|
3072
3454
|
return {
|
|
@@ -3082,8 +3464,8 @@ var StlViewer = class _StlViewer {
|
|
|
3082
3464
|
section: m.section ? { ...m.section } : null,
|
|
3083
3465
|
color: m.color ?? (g.colors ? "#ffffff" : "#909090"),
|
|
3084
3466
|
scale: { x: m.scalex, y: m.scaley, z: m.scalez },
|
|
3085
|
-
volume: m._stats.volume * factor ** 3,
|
|
3086
|
-
area: m._stats.area * factor ** 2,
|
|
3467
|
+
volume: options.measurements === false ? null : m._stats.volume * factor ** 3,
|
|
3468
|
+
area: options.measurements === false ? null : m._stats.area * factor ** 2,
|
|
3087
3469
|
triangles: g.triangles,
|
|
3088
3470
|
bounds: { min: [...g.bounds.min], max: [...g.bounds.max] },
|
|
3089
3471
|
has_colors: !!g.colors,
|
|
@@ -3171,7 +3553,9 @@ var StlViewer = class _StlViewer {
|
|
|
3171
3553
|
rotationx: 0,
|
|
3172
3554
|
rotationy: 0,
|
|
3173
3555
|
rotationz: 0,
|
|
3174
|
-
scaleVector: [
|
|
3556
|
+
scaleVector: [1, 1, 1].map(
|
|
3557
|
+
() => m.unitScale * (m.size_state?.unit_factor ?? 1)
|
|
3558
|
+
)
|
|
3175
3559
|
};
|
|
3176
3560
|
const matrix = modelMatrix(source), factor = settings.units === "inch" ? 1 / 25.4 : 1;
|
|
3177
3561
|
for (let c = 0; c < 4; c++)
|
|
@@ -3239,8 +3623,8 @@ var StlViewer = class _StlViewer {
|
|
|
3239
3623
|
);
|
|
3240
3624
|
}
|
|
3241
3625
|
async export_scene(options = {}) {
|
|
3242
|
-
const
|
|
3243
|
-
if (!["vsb", "vsj"].includes(
|
|
3626
|
+
const format2 = String(options.format || "").toLowerCase();
|
|
3627
|
+
if (!["vsb", "vsj"].includes(format2))
|
|
3244
3628
|
throw new Error("Scene format must be vsb or vsj");
|
|
3245
3629
|
if (options.force_basename !== void 0 && typeof options.force_basename !== "boolean")
|
|
3246
3630
|
throw new Error("force_basename must be a boolean");
|
|
@@ -3263,7 +3647,7 @@ var StlViewer = class _StlViewer {
|
|
|
3263
3647
|
if (!models.length) throw new Error("No models to export");
|
|
3264
3648
|
if (models.some((m) => !m.geometry))
|
|
3265
3649
|
throw new Error("A model has not finished loading");
|
|
3266
|
-
if (models.length + (
|
|
3650
|
+
if (models.length + (format2 === "vsb" ? 1 : 0) > this.limits.max_archive_entries)
|
|
3267
3651
|
throw new Error("Scene model count limit exceeded");
|
|
3268
3652
|
const triangles = models.reduce((sum, m) => sum + m.geometry.triangles, 0);
|
|
3269
3653
|
if (triangles > this.limits.max_triangles)
|
|
@@ -3271,19 +3655,19 @@ var StlViewer = class _StlViewer {
|
|
|
3271
3655
|
const scene = this.get_vsj(
|
|
3272
3656
|
true,
|
|
3273
3657
|
!!options.force_basename,
|
|
3274
|
-
|
|
3658
|
+
format2 === "vsb"
|
|
3275
3659
|
);
|
|
3276
3660
|
const json = JSON.stringify(scene), manifestBytes = new Blob([json]).size;
|
|
3277
3661
|
if (manifestBytes > this.limits.max_file_bytes)
|
|
3278
3662
|
throw new Error("Scene manifest size limit exceeded");
|
|
3279
|
-
if (
|
|
3663
|
+
if (format2 === "vsb" && manifestBytes + models.length * 84 + triangles * 50 > this.limits.max_uncompressed_bytes)
|
|
3280
3664
|
throw new Error("Scene archive limit exceeded");
|
|
3281
|
-
const working = MEMORY_OVERHEAD + json.length * 8 + (
|
|
3665
|
+
const working = MEMORY_OVERHEAD + json.length * 8 + (format2 === "vsb" ? triangles * 512 : 0);
|
|
3282
3666
|
assertMemory(this.limits, this._occupiedMemory() + working, "scene export");
|
|
3283
3667
|
this._exportBytes += working;
|
|
3284
3668
|
let job, cancel, callbackError;
|
|
3285
3669
|
try {
|
|
3286
|
-
const snapshot =
|
|
3670
|
+
const snapshot = format2 === "vsb" ? models.map((m, index) => ({
|
|
3287
3671
|
id: m.id,
|
|
3288
3672
|
triangles: m.geometry.triangles,
|
|
3289
3673
|
positions: m.geometry.positions.slice(),
|
|
@@ -3298,7 +3682,7 @@ var StlViewer = class _StlViewer {
|
|
|
3298
3682
|
});
|
|
3299
3683
|
checkAbort();
|
|
3300
3684
|
let blob;
|
|
3301
|
-
if (
|
|
3685
|
+
if (format2 === "vsj") {
|
|
3302
3686
|
blob = new Blob([json], { type: "application/json" });
|
|
3303
3687
|
options.on_progress?.({
|
|
3304
3688
|
loaded: models.length,
|
|
@@ -3338,8 +3722,8 @@ var StlViewer = class _StlViewer {
|
|
|
3338
3722
|
checkAbort();
|
|
3339
3723
|
return {
|
|
3340
3724
|
blob,
|
|
3341
|
-
format,
|
|
3342
|
-
filename: exportFilename(options.filename,
|
|
3725
|
+
format: format2,
|
|
3726
|
+
filename: exportFilename(options.filename, format2),
|
|
3343
3727
|
model_ids: models.map((m) => m.id),
|
|
3344
3728
|
triangles,
|
|
3345
3729
|
representation: "scene"
|
|
@@ -3361,6 +3745,8 @@ var StlViewer = class _StlViewer {
|
|
|
3361
3745
|
canvas_height: this.canvas_height,
|
|
3362
3746
|
bg_color: this.bg_color,
|
|
3363
3747
|
camera_state: this.get_camera_state(),
|
|
3748
|
+
camera_bookmarks: this.get_camera_bookmarks(),
|
|
3749
|
+
display_units: this.display_units || "mm",
|
|
3364
3750
|
auto_rotate: this.auto_rotate,
|
|
3365
3751
|
mouse_zoom: this.mouse_zoom,
|
|
3366
3752
|
auto_resize: this.auto_resize,
|
|
@@ -3381,6 +3767,11 @@ var StlViewer = class _StlViewer {
|
|
|
3381
3767
|
id: for_vsb ? -1 : m.id,
|
|
3382
3768
|
filename: for_vsb ? `${m.id}.stl` : force_basename ? basename(m.filename) : m.filename,
|
|
3383
3769
|
units: m.units,
|
|
3770
|
+
size_state: {
|
|
3771
|
+
...structuredClone(sizeState(m)),
|
|
3772
|
+
unit_factor: for_vsb && !m._animation ? 1 : m.unitScale * (sizeState(m).unit_factor ?? 1)
|
|
3773
|
+
},
|
|
3774
|
+
...m.display_name ? { display_name: m.display_name } : {},
|
|
3384
3775
|
display: m.display,
|
|
3385
3776
|
opacity: m.opacity,
|
|
3386
3777
|
view_edges: m.view_edges,
|
|
@@ -3503,6 +3894,13 @@ var StlViewer = class _StlViewer {
|
|
|
3503
3894
|
);
|
|
3504
3895
|
else if (scene.grid) this.set_grid(true);
|
|
3505
3896
|
if (scene.camera_state) this._restoreCamera = scene.camera_state;
|
|
3897
|
+
const bookmarks = mergeBookmarks(
|
|
3898
|
+
this.get_camera_bookmarks(),
|
|
3899
|
+
scene.camera_bookmarks || []
|
|
3900
|
+
);
|
|
3901
|
+
const displayUnits = validateUnits(
|
|
3902
|
+
scene.display_units || this.display_units || "mm"
|
|
3903
|
+
);
|
|
3506
3904
|
const incoming = [];
|
|
3507
3905
|
try {
|
|
3508
3906
|
for (const input of scene.models) {
|
|
@@ -3550,6 +3948,8 @@ var StlViewer = class _StlViewer {
|
|
|
3550
3948
|
this._startAnimation(m, model.animation, model.animation_state);
|
|
3551
3949
|
m._sceneImportPending = false;
|
|
3552
3950
|
}
|
|
3951
|
+
this.camera_bookmarks = bookmarks;
|
|
3952
|
+
this.display_units = displayUnits;
|
|
3553
3953
|
} catch (error) {
|
|
3554
3954
|
for (const { m } of incoming)
|
|
3555
3955
|
if (this._models.get(m.id) === m) this.remove_model(m.id);
|
|
@@ -3693,6 +4093,299 @@ var StlViewer = class _StlViewer {
|
|
|
3693
4093
|
};
|
|
3694
4094
|
var viewer_default = StlViewer;
|
|
3695
4095
|
|
|
4096
|
+
// src/app-tools.js
|
|
4097
|
+
var toolsControls = [
|
|
4098
|
+
"size-details",
|
|
4099
|
+
"size-fields",
|
|
4100
|
+
"size-x",
|
|
4101
|
+
"size-y",
|
|
4102
|
+
"size-z",
|
|
4103
|
+
"size-lock",
|
|
4104
|
+
"size-percent",
|
|
4105
|
+
"size-apply",
|
|
4106
|
+
"size-cancel",
|
|
4107
|
+
"size-reset",
|
|
4108
|
+
"size-error",
|
|
4109
|
+
"file-units",
|
|
4110
|
+
"file-units-note",
|
|
4111
|
+
"display-units",
|
|
4112
|
+
"size-unit-label",
|
|
4113
|
+
"duplicate-model",
|
|
4114
|
+
"camera-preset",
|
|
4115
|
+
"bookmark-name",
|
|
4116
|
+
"bookmark-list",
|
|
4117
|
+
"bookmark-save",
|
|
4118
|
+
"bookmark-restore",
|
|
4119
|
+
"bookmark-rename",
|
|
4120
|
+
"bookmark-delete",
|
|
4121
|
+
"camera-error",
|
|
4122
|
+
"info-area",
|
|
4123
|
+
"info-volume",
|
|
4124
|
+
"measure-model",
|
|
4125
|
+
"measure-cancel",
|
|
4126
|
+
"measure-status"
|
|
4127
|
+
];
|
|
4128
|
+
var axes2 = ["x", "y", "z"];
|
|
4129
|
+
var format = new Intl.NumberFormat(void 0, { maximumSignificantDigits: 7 });
|
|
4130
|
+
function bindAppTools({
|
|
4131
|
+
$,
|
|
4132
|
+
listen,
|
|
4133
|
+
window: window2,
|
|
4134
|
+
getViewer,
|
|
4135
|
+
getState,
|
|
4136
|
+
changeSize,
|
|
4137
|
+
duplicate,
|
|
4138
|
+
refresh,
|
|
4139
|
+
announce
|
|
4140
|
+
}) {
|
|
4141
|
+
let draftKind = null, lastAxis = 0, base = null, selected = null, measurement = null, disposed = false;
|
|
4142
|
+
const error = (id, message = "") => {
|
|
4143
|
+
$(id).textContent = message;
|
|
4144
|
+
$(id).hidden = !message;
|
|
4145
|
+
};
|
|
4146
|
+
const factor = () => getViewer()?.display_units === "inch" ? 25.4 : 1;
|
|
4147
|
+
const value = (v) => Number(v.toPrecision(10));
|
|
4148
|
+
function syncInfo() {
|
|
4149
|
+
const v = getViewer(), { selectedId, busy } = getState(), m = v?._models.get(selectedId);
|
|
4150
|
+
const s = m?.scaleVector, cached = m?._measurements;
|
|
4151
|
+
const valid = cached && cached.positions === m.geometry.positions && cached.key === s.join(",");
|
|
4152
|
+
const unit = factor() === 1 ? "mm" : "in";
|
|
4153
|
+
const area = valid ? cached.result.area_mm2 : s && s[0] === s[1] && s[1] === s[2] ? m.geometry.area * s[0] ** 2 : null;
|
|
4154
|
+
$("info-area").textContent = area == null ? "Calculate" : format.format(area / factor() ** 2) + " " + unit + "\xB2";
|
|
4155
|
+
$("info-volume").textContent = valid ? cached.result.volume_valid ? format.format(cached.result.volume_mm3 / factor() ** 3) + " " + unit + "\xB3" : "Unavailable" : "Not checked";
|
|
4156
|
+
if (!measurement)
|
|
4157
|
+
$("measure-status").textContent = valid ? cached.result.volume_reason : "Calculate to check enclosed volume. Runs on your device.";
|
|
4158
|
+
$("measure-model").disabled = busy || !m || !!measurement;
|
|
4159
|
+
$("measure-cancel").hidden = !measurement;
|
|
4160
|
+
}
|
|
4161
|
+
function syncBookmarks() {
|
|
4162
|
+
const v = getViewer(), previous = $("bookmark-list").value, bookmarks = v?.get_camera_bookmarks() || [];
|
|
4163
|
+
$("bookmark-list").replaceChildren(
|
|
4164
|
+
...bookmarks.map((b) => {
|
|
4165
|
+
const option = window2.document.createElement("option");
|
|
4166
|
+
option.value = b.name;
|
|
4167
|
+
option.textContent = b.name;
|
|
4168
|
+
return option;
|
|
4169
|
+
})
|
|
4170
|
+
);
|
|
4171
|
+
if (bookmarks.some((b) => b.name === previous))
|
|
4172
|
+
$("bookmark-list").value = previous;
|
|
4173
|
+
for (const id of [
|
|
4174
|
+
"bookmark-restore",
|
|
4175
|
+
"bookmark-rename",
|
|
4176
|
+
"bookmark-delete",
|
|
4177
|
+
"bookmark-list"
|
|
4178
|
+
])
|
|
4179
|
+
$(id).disabled = !bookmarks.length;
|
|
4180
|
+
$("bookmark-save").disabled = !v || bookmarks.length >= 32;
|
|
4181
|
+
}
|
|
4182
|
+
function sync() {
|
|
4183
|
+
if (disposed) return;
|
|
4184
|
+
const { selectedId, busy } = getState(), v = getViewer(), m = v?._models.get(selectedId);
|
|
4185
|
+
if (selectedId !== selected) {
|
|
4186
|
+
measurement?.abort();
|
|
4187
|
+
selected = selectedId;
|
|
4188
|
+
}
|
|
4189
|
+
draftKind = null;
|
|
4190
|
+
base = m?.geometry ? v.get_model_size(selectedId) : null;
|
|
4191
|
+
$("size-fields").disabled = busy || !base;
|
|
4192
|
+
$("duplicate-model").disabled = busy || !base;
|
|
4193
|
+
$("display-units").value = v?.display_units || "mm";
|
|
4194
|
+
$("size-unit-label").textContent = factor() === 1 ? "Dimensions in mm" : "Dimensions in inches";
|
|
4195
|
+
$("file-units").value = base?.file_units || "mm";
|
|
4196
|
+
$("file-units").disabled = busy || !base?.editable;
|
|
4197
|
+
$("file-units-note").textContent = base?.editable ? "STL/OBJ have no units. Millimetres are assumed; choose inches to correct the physical size." : "Explicit file units were converted to millimetres on import.";
|
|
4198
|
+
axes2.forEach((a, i) => {
|
|
4199
|
+
$("size-" + a).value = base ? value(base.dimensions_mm[i] / factor()) : "";
|
|
4200
|
+
$("size-" + a).disabled = !base || base.dimensions_mm[i] === 0;
|
|
4201
|
+
});
|
|
4202
|
+
const ratios = base?.baseline_mm.map((v2, i) => v2 ? base.dimensions_mm[i] / v2 : null).filter((v2) => v2 != null) || [];
|
|
4203
|
+
$("size-percent").value = ratios.length && ratios.every((v2) => Math.abs(v2 / ratios[0] - 1) < 1e-8) ? value(ratios[0] * 100) : "";
|
|
4204
|
+
error("size-error");
|
|
4205
|
+
syncInfo();
|
|
4206
|
+
syncBookmarks();
|
|
4207
|
+
}
|
|
4208
|
+
const attempt = (id, action) => {
|
|
4209
|
+
try {
|
|
4210
|
+
action();
|
|
4211
|
+
error(id);
|
|
4212
|
+
} catch (e) {
|
|
4213
|
+
error(id, e.message);
|
|
4214
|
+
}
|
|
4215
|
+
};
|
|
4216
|
+
axes2.forEach(
|
|
4217
|
+
(a, i) => listen($("size-" + a), "input", () => {
|
|
4218
|
+
if (!base) return;
|
|
4219
|
+
draftKind = "axis";
|
|
4220
|
+
lastAxis = i;
|
|
4221
|
+
const n = $("size-" + a).valueAsNumber;
|
|
4222
|
+
if (!Number.isFinite(n) || n <= 0) return;
|
|
4223
|
+
if ($("size-lock").checked)
|
|
4224
|
+
axes2.forEach((axis, j) => {
|
|
4225
|
+
if (i !== j)
|
|
4226
|
+
$("size-" + axis).value = value(
|
|
4227
|
+
n * base.dimensions_mm[j] / base.dimensions_mm[i]
|
|
4228
|
+
);
|
|
4229
|
+
});
|
|
4230
|
+
$("size-percent").value = "";
|
|
4231
|
+
})
|
|
4232
|
+
);
|
|
4233
|
+
listen($("size-percent"), "input", () => {
|
|
4234
|
+
draftKind = "percent";
|
|
4235
|
+
const n = $("size-percent").valueAsNumber;
|
|
4236
|
+
if (base && Number.isFinite(n) && n > 0)
|
|
4237
|
+
axes2.forEach(
|
|
4238
|
+
(a, i) => $("size-" + a).value = value(
|
|
4239
|
+
base.baseline_mm[i] * n / 100 / factor()
|
|
4240
|
+
)
|
|
4241
|
+
);
|
|
4242
|
+
});
|
|
4243
|
+
listen(
|
|
4244
|
+
$("size-apply"),
|
|
4245
|
+
"click",
|
|
4246
|
+
() => attempt("size-error", () => {
|
|
4247
|
+
if (!base) return;
|
|
4248
|
+
if (!draftKind) return;
|
|
4249
|
+
const current = getViewer().get_model_size(selected).dimensions_mm;
|
|
4250
|
+
let desired = axes2.map((a) => $("size-" + a).valueAsNumber * factor());
|
|
4251
|
+
if (draftKind === "percent") {
|
|
4252
|
+
const percent = $("size-percent").valueAsNumber;
|
|
4253
|
+
if (!Number.isFinite(percent) || percent <= 0)
|
|
4254
|
+
throw new Error("Enter a positive scale percentage.");
|
|
4255
|
+
desired = base.baseline_mm.map((v) => v * percent / 100);
|
|
4256
|
+
} else if ($("size-lock").checked) {
|
|
4257
|
+
const ratio = desired[lastAxis] / base.dimensions_mm[lastAxis];
|
|
4258
|
+
desired = base.dimensions_mm.map((v) => v * ratio);
|
|
4259
|
+
}
|
|
4260
|
+
let ratios = desired.map((n, i) => {
|
|
4261
|
+
if (!Number.isFinite(n) || n < 0 || current[i] > 0 && n === 0)
|
|
4262
|
+
throw new Error("Enter positive dimensions for each non-flat axis.");
|
|
4263
|
+
return current[i] ? n / current[i] : 1;
|
|
4264
|
+
});
|
|
4265
|
+
if (ratios.every((v) => Math.abs(v / ratios[0] - 1) < 1e-12))
|
|
4266
|
+
ratios = ratios.map(() => ratios[0]);
|
|
4267
|
+
changeSize(() => getViewer().resize_model(selected, ratios));
|
|
4268
|
+
announce("Model size updated.");
|
|
4269
|
+
})
|
|
4270
|
+
);
|
|
4271
|
+
listen($("size-cancel"), "click", sync);
|
|
4272
|
+
listen(
|
|
4273
|
+
$("size-reset"),
|
|
4274
|
+
"click",
|
|
4275
|
+
() => attempt("size-error", () => {
|
|
4276
|
+
changeSize(() => getViewer().reset_model_size(selected));
|
|
4277
|
+
announce("Original size restored; file-unit correction retained.");
|
|
4278
|
+
})
|
|
4279
|
+
);
|
|
4280
|
+
listen(
|
|
4281
|
+
$("file-units"),
|
|
4282
|
+
"change",
|
|
4283
|
+
() => attempt("size-error", () => {
|
|
4284
|
+
changeSize(
|
|
4285
|
+
() => getViewer().set_file_units(selected, $("file-units").value)
|
|
4286
|
+
);
|
|
4287
|
+
announce("File units corrected.");
|
|
4288
|
+
})
|
|
4289
|
+
);
|
|
4290
|
+
listen($("display-units"), "change", () => {
|
|
4291
|
+
getViewer()?.set_display_units($("display-units").value);
|
|
4292
|
+
refresh();
|
|
4293
|
+
});
|
|
4294
|
+
listen($("duplicate-model"), "click", () => attempt("size-error", duplicate));
|
|
4295
|
+
listen(
|
|
4296
|
+
$("camera-preset"),
|
|
4297
|
+
"change",
|
|
4298
|
+
() => attempt("camera-error", () => {
|
|
4299
|
+
if ($("camera-preset").value)
|
|
4300
|
+
getViewer().set_camera_view($("camera-preset").value);
|
|
4301
|
+
announce("Camera view updated.");
|
|
4302
|
+
$("camera-preset").value = "";
|
|
4303
|
+
})
|
|
4304
|
+
);
|
|
4305
|
+
const editBookmarks = (action) => attempt("camera-error", () => {
|
|
4306
|
+
const v = getViewer(), bookmarks = v.get_camera_bookmarks();
|
|
4307
|
+
action(bookmarks, v);
|
|
4308
|
+
v.set_camera_bookmarks(bookmarks);
|
|
4309
|
+
syncBookmarks();
|
|
4310
|
+
});
|
|
4311
|
+
listen(
|
|
4312
|
+
$("bookmark-save"),
|
|
4313
|
+
"click",
|
|
4314
|
+
() => editBookmarks((b, v) => {
|
|
4315
|
+
b.push({ name: $("bookmark-name").value, camera: v.get_camera_state() });
|
|
4316
|
+
})
|
|
4317
|
+
);
|
|
4318
|
+
listen(
|
|
4319
|
+
$("bookmark-rename"),
|
|
4320
|
+
"click",
|
|
4321
|
+
() => editBookmarks((b) => {
|
|
4322
|
+
b.find((b2) => b2.name === $("bookmark-list").value).name = $("bookmark-name").value;
|
|
4323
|
+
})
|
|
4324
|
+
);
|
|
4325
|
+
listen(
|
|
4326
|
+
$("bookmark-delete"),
|
|
4327
|
+
"click",
|
|
4328
|
+
() => editBookmarks((b) => {
|
|
4329
|
+
b.splice(
|
|
4330
|
+
b.findIndex((b2) => b2.name === $("bookmark-list").value),
|
|
4331
|
+
1
|
|
4332
|
+
);
|
|
4333
|
+
})
|
|
4334
|
+
);
|
|
4335
|
+
listen(
|
|
4336
|
+
$("bookmark-restore"),
|
|
4337
|
+
"click",
|
|
4338
|
+
() => attempt("camera-error", () => {
|
|
4339
|
+
const b = getViewer().get_camera_bookmarks().find((b2) => b2.name === $("bookmark-list").value);
|
|
4340
|
+
if (b) {
|
|
4341
|
+
getViewer().set_camera_state(b.camera);
|
|
4342
|
+
announce("Saved view restored.");
|
|
4343
|
+
}
|
|
4344
|
+
})
|
|
4345
|
+
);
|
|
4346
|
+
listen(
|
|
4347
|
+
$("bookmark-list"),
|
|
4348
|
+
"change",
|
|
4349
|
+
() => $("bookmark-name").value = $("bookmark-list").value
|
|
4350
|
+
);
|
|
4351
|
+
listen($("measure-cancel"), "click", () => measurement?.abort());
|
|
4352
|
+
listen($("measure-model"), "click", async () => {
|
|
4353
|
+
const id = selected, v = getViewer(), controller = new window2.AbortController();
|
|
4354
|
+
measurement = controller;
|
|
4355
|
+
syncInfo();
|
|
4356
|
+
$("measure-status").textContent = "Calculating area and checking volume\u2026";
|
|
4357
|
+
try {
|
|
4358
|
+
await v.measure_model(id, {
|
|
4359
|
+
signal: controller.signal,
|
|
4360
|
+
on_progress: (p) => {
|
|
4361
|
+
if (selected === id && !disposed)
|
|
4362
|
+
$("measure-status").textContent = p.phase + "\u2026";
|
|
4363
|
+
}
|
|
4364
|
+
});
|
|
4365
|
+
if (!disposed && selected === id) announce("Measurements ready.");
|
|
4366
|
+
} catch (e) {
|
|
4367
|
+
if (!disposed && selected === id)
|
|
4368
|
+
$("measure-status").textContent = e.name === "AbortError" ? "Calculation cancelled." : e.message;
|
|
4369
|
+
} finally {
|
|
4370
|
+
const message = $("measure-status").textContent;
|
|
4371
|
+
if (measurement === controller) measurement = null;
|
|
4372
|
+
if (!disposed) {
|
|
4373
|
+
syncInfo();
|
|
4374
|
+
if (!v._models.get(id)?._measurements && selected === id)
|
|
4375
|
+
$("measure-status").textContent = message;
|
|
4376
|
+
}
|
|
4377
|
+
}
|
|
4378
|
+
});
|
|
4379
|
+
return {
|
|
4380
|
+
sync,
|
|
4381
|
+
syncInfo,
|
|
4382
|
+
dispose() {
|
|
4383
|
+
disposed = true;
|
|
4384
|
+
measurement?.abort();
|
|
4385
|
+
}
|
|
4386
|
+
};
|
|
4387
|
+
}
|
|
4388
|
+
|
|
3696
4389
|
// src/memory-policy.js
|
|
3697
4390
|
var GiB = 1024 ** 3;
|
|
3698
4391
|
var MiB = 1024 ** 2;
|
|
@@ -3724,9 +4417,9 @@ async function estimateFile(file) {
|
|
|
3724
4417
|
}
|
|
3725
4418
|
|
|
3726
4419
|
// src/model-transform.js
|
|
3727
|
-
var
|
|
3728
|
-
var vector2 = (p) =>
|
|
3729
|
-
var object2 = (p) => Object.fromEntries(
|
|
4420
|
+
var axes3 = ["x", "y", "z"];
|
|
4421
|
+
var vector2 = (p) => axes3.map((a) => p[a]);
|
|
4422
|
+
var object2 = (p) => Object.fromEntries(axes3.map((a, i) => [a, p[i]]));
|
|
3730
4423
|
var dot2 = (a, b) => a.reduce((sum, v, i) => sum + v * b[i], 0);
|
|
3731
4424
|
var norm2 = (a) => {
|
|
3732
4425
|
const n = Math.hypot(...a) || 1;
|
|
@@ -3758,7 +4451,7 @@ function rotationMatrix(degrees = { x: 0, y: 0, z: 0 }) {
|
|
|
3758
4451
|
var centerOf = (bounds) => bounds.min.map((v, i) => (v + bounds.max[i]) / 2);
|
|
3759
4452
|
function modelCenter(model) {
|
|
3760
4453
|
return object2(
|
|
3761
|
-
centerOf(model.bounds).map((v, i) => v + model.position[
|
|
4454
|
+
centerOf(model.bounds).map((v, i) => v + model.position[axes3[i]])
|
|
3762
4455
|
);
|
|
3763
4456
|
}
|
|
3764
4457
|
function modelRadius(model) {
|
|
@@ -3769,7 +4462,7 @@ function modelRadius(model) {
|
|
|
3769
4462
|
}
|
|
3770
4463
|
function enginePosition(model) {
|
|
3771
4464
|
const c = centerOf(model.bounds), rotated = transform2(rotationMatrix(model.rotation), c);
|
|
3772
|
-
return object2(c.map((v, i) => model.position[
|
|
4465
|
+
return object2(c.map((v, i) => model.position[axes3[i]] + v - rotated[i]));
|
|
3773
4466
|
}
|
|
3774
4467
|
function modelBounds(model) {
|
|
3775
4468
|
if (!model.rotation || !Object.values(model.rotation).some((v) => v % 360))
|
|
@@ -4110,16 +4803,16 @@ function bindAppExport({
|
|
|
4110
4803
|
$("save-error").textContent = "";
|
|
4111
4804
|
};
|
|
4112
4805
|
function formatChanged() {
|
|
4113
|
-
const
|
|
4806
|
+
const format2 = $("save-format").value;
|
|
4114
4807
|
if (!$("save-name").value || $("save-name").value === lastSuggestedName) {
|
|
4115
4808
|
suggestedName();
|
|
4116
4809
|
}
|
|
4117
4810
|
if ($("save-mesh-note")) $("save-mesh-note").hidden = isScene();
|
|
4118
4811
|
if ($("save-scene-note")) $("save-scene-note").hidden = !isScene();
|
|
4119
|
-
$("save-colors-row").hidden = !["obj", "3mf"].includes(
|
|
4120
|
-
$("save-cad-note").hidden = !["step", "iges"].includes(
|
|
4812
|
+
$("save-colors-row").hidden = !["obj", "3mf"].includes(format2);
|
|
4813
|
+
$("save-cad-note").hidden = !["step", "iges"].includes(format2);
|
|
4121
4814
|
$("save-format-note").textContent = $("save-format").selectedOptions[0]?.dataset.note || "";
|
|
4122
|
-
$("save-submit").textContent =
|
|
4815
|
+
$("save-submit").textContent = format2 === "obj" && $("save-colors").checked ? "Download ZIP" : "Download file";
|
|
4123
4816
|
sync();
|
|
4124
4817
|
}
|
|
4125
4818
|
function suggestedName() {
|
|
@@ -4570,7 +5263,7 @@ var animationControls = [
|
|
|
4570
5263
|
"animation-status",
|
|
4571
5264
|
"animation-error"
|
|
4572
5265
|
];
|
|
4573
|
-
var
|
|
5266
|
+
var axes4 = ["x", "y", "z"];
|
|
4574
5267
|
function syncAnimatedRecord(record, model) {
|
|
4575
5268
|
if (!record || !model?.geometry) return;
|
|
4576
5269
|
record.bounds = Object.fromEntries(
|
|
@@ -4582,11 +5275,11 @@ function syncAnimatedRecord(record, model) {
|
|
|
4582
5275
|
])
|
|
4583
5276
|
);
|
|
4584
5277
|
record.rotation = Object.fromEntries(
|
|
4585
|
-
|
|
5278
|
+
axes4.map((a) => [a, model["rotation" + a] * 180 / Math.PI % 360])
|
|
4586
5279
|
);
|
|
4587
5280
|
record.position = { x: 0, y: 0, z: 0 };
|
|
4588
5281
|
const offset = enginePosition(record);
|
|
4589
|
-
for (const a of
|
|
5282
|
+
for (const a of axes4) record.position[a] = model[a] - offset[a];
|
|
4590
5283
|
}
|
|
4591
5284
|
function centerAnimationPivot(record, model) {
|
|
4592
5285
|
syncAnimatedRecord(record, model);
|
|
@@ -4599,10 +5292,10 @@ function centerAnimationPivot(record, model) {
|
|
|
4599
5292
|
const rotation2 = rotationMatrix(record.rotation);
|
|
4600
5293
|
const original = record.originalPosition || { x: 0, y: 0, z: 0 };
|
|
4601
5294
|
record.originalPosition = Object.fromEntries(
|
|
4602
|
-
|
|
5295
|
+
axes4.map((a, i) => [a, original[a] + delta[i]])
|
|
4603
5296
|
);
|
|
4604
5297
|
for (let i = 0; i < 3; i++)
|
|
4605
|
-
model[
|
|
5298
|
+
model[axes4[i]] += delta.reduce(
|
|
4606
5299
|
(sum, v, j) => sum + rotation2[i * 3 + j] * v,
|
|
4607
5300
|
0
|
|
4608
5301
|
);
|
|
@@ -4611,7 +5304,7 @@ function centerAnimationPivot(record, model) {
|
|
|
4611
5304
|
syncAnimatedRecord(record, model);
|
|
4612
5305
|
}
|
|
4613
5306
|
function spinDefinition(axis, seconds, direction) {
|
|
4614
|
-
if (!
|
|
5307
|
+
if (!axes4.includes(axis) || !Number.isFinite(seconds) || seconds < 0.1 || seconds > 3600 || !["1", "-1"].includes(direction))
|
|
4615
5308
|
throw new Error("Choose an axis and a duration from 0.1 to 3,600 seconds.");
|
|
4616
5309
|
return {
|
|
4617
5310
|
delta: {
|
|
@@ -4868,6 +5561,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4868
5561
|
missing.push(...printControls.filter((id) => !$(id)));
|
|
4869
5562
|
if (animationControls.some((id) => $(id)))
|
|
4870
5563
|
missing.push(...animationControls.filter((id) => !$(id)));
|
|
5564
|
+
if (toolsControls.some((id) => $(id)))
|
|
5565
|
+
missing.push(...toolsControls.filter((id) => !$(id)));
|
|
4871
5566
|
if (missing.length)
|
|
4872
5567
|
throw new Error("Missing ViewSTL controls: " + missing.join(", "));
|
|
4873
5568
|
const abort = new window2.AbortController();
|
|
@@ -4896,6 +5591,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4896
5591
|
let exportUI;
|
|
4897
5592
|
let printUI;
|
|
4898
5593
|
let animationUI;
|
|
5594
|
+
let toolsUI;
|
|
4899
5595
|
let sceneImport;
|
|
4900
5596
|
let draining = false, busy = false, gridEnabled = false, gridHeight = 0, gridSpacing = 1, moveMode = false, transformTool = "move", modelMover, dragArrangement, background = "transparent";
|
|
4901
5597
|
const viewNames = {
|
|
@@ -4944,6 +5640,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4944
5640
|
$(name + "-panel").hidden = !open;
|
|
4945
5641
|
$(name + "-button").setAttribute("aria-expanded", String(open));
|
|
4946
5642
|
syncSelection();
|
|
5643
|
+
if (open && name === "info") populateInfo();
|
|
5644
|
+
if (open && (name === "scene" || name === "view")) toolsUI?.sync();
|
|
4947
5645
|
}
|
|
4948
5646
|
function fileSize(size) {
|
|
4949
5647
|
return size == null ? "Sample model" : size < 1024 ** 2 ? `${formatNumber.format(size / 1024)} KiB` : size < 1024 ** 3 ? `${formatNumber.format(size / 1024 ** 2)} MiB` : `${formatNumber.format(size / 1024 ** 3)} GiB`;
|
|
@@ -4970,12 +5668,13 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4970
5668
|
}
|
|
4971
5669
|
function syncControls() {
|
|
4972
5670
|
syncAnimatedRecord(records.get(modelID), viewer?._models.get(modelID));
|
|
4973
|
-
const info = modelID === null ? null : viewer.get_model_info(modelID), mode = info?.display === "smooth" ? "flat" : info?.display || "flat";
|
|
5671
|
+
const info = modelID === null ? null : viewer.get_model_info(modelID, { measurements: false }), mode = info?.display === "smooth" ? "flat" : info?.display || "flat";
|
|
4974
5672
|
for (const input of modeInputs) {
|
|
4975
5673
|
input.checked = input.value === mode;
|
|
4976
5674
|
input.disabled = busy;
|
|
4977
5675
|
}
|
|
4978
|
-
$("view-label").textContent = viewNames[mode];
|
|
5676
|
+
$("view-label").textContent = toolsUI ? "View" : viewNames[mode];
|
|
5677
|
+
$("view-button").title = "View \xB7 " + viewNames[mode];
|
|
4979
5678
|
$("smooth-shading").checked = info?.smooth_shading || false;
|
|
4980
5679
|
$("smooth-shading").disabled = busy || mode === "wireframe";
|
|
4981
5680
|
$("view-status").textContent = mode === "wireframe" ? "Smooth shading is available in surface views." : "Shading changes the appearance, not the geometry.";
|
|
@@ -4999,14 +5698,17 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4999
5698
|
exportUI?.sync();
|
|
5000
5699
|
printUI?.sync();
|
|
5001
5700
|
animationUI?.sync();
|
|
5701
|
+
toolsUI?.sync();
|
|
5002
5702
|
}
|
|
5003
5703
|
function populateInfo() {
|
|
5004
5704
|
if (modelID === null) return;
|
|
5005
|
-
const info = viewer.get_model_info(modelID), record = records.get(modelID),
|
|
5705
|
+
const info = viewer.get_model_info(modelID, { measurements: false }), record = records.get(modelID), factor = viewer.display_units === "inch" ? 25.4 : 1;
|
|
5006
5706
|
$("info-triangles").textContent = info.triangles.toLocaleString();
|
|
5007
|
-
$("info-dimensions").textContent = [info.dims.x, info.dims.y, info.dims.z].map((x) => formatNumber.format(x)).join(" \xD7 ") +
|
|
5707
|
+
$("info-dimensions").textContent = [info.dims.x, info.dims.y, info.dims.z].map((x) => formatNumber.format(x / factor)).join(" \xD7 ") + (factor === 1 ? " mm" : " in");
|
|
5008
5708
|
$("info-size").textContent = fileSize(record.size);
|
|
5009
|
-
|
|
5709
|
+
const size = viewer.get_model_size(modelID);
|
|
5710
|
+
$("unit-note").textContent = size.editable ? "File coordinates interpreted as " + (size.file_units === "inch" ? "inches." : "millimetres (assumed).") + " Change file units in Models \u2192 Size & units." : "Explicit file units were converted to millimetres on import.";
|
|
5711
|
+
toolsUI?.syncInfo();
|
|
5010
5712
|
}
|
|
5011
5713
|
function renderList() {
|
|
5012
5714
|
$("model-list").replaceChildren(
|
|
@@ -5108,7 +5810,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5108
5810
|
input.removeAttribute("aria-invalid");
|
|
5109
5811
|
}
|
|
5110
5812
|
$("position-error").hidden = true;
|
|
5111
|
-
$("position-units").textContent = record?.animationPivot ? "Position of the model center in the scene
|
|
5813
|
+
$("position-units").textContent = record?.animationPivot ? "Position of the model center in the scene, in mm." : "Offset from file coordinates, in mm.";
|
|
5112
5814
|
$("move-model-button").disabled = !record || busy;
|
|
5113
5815
|
$("reset-position").disabled = !record;
|
|
5114
5816
|
syncRotation(preserveFocus);
|
|
@@ -5191,11 +5893,11 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5191
5893
|
$("model-tool").value = transformTool;
|
|
5192
5894
|
$("move-gesture-message").textContent = toolHints[transformTool] + " \xB7 Scroll to adjust depth";
|
|
5193
5895
|
}
|
|
5194
|
-
function setMoveMode(enabled, tool = transformTool) {
|
|
5896
|
+
function setMoveMode(enabled, tool = transformTool, retainPaused = false) {
|
|
5195
5897
|
modelMover?.setEnabled(false);
|
|
5196
5898
|
transformTool = tool;
|
|
5197
5899
|
moveMode = !!enabled && modelID !== null && !busy;
|
|
5198
|
-
if (moveMode) prepareManual(modelID);
|
|
5900
|
+
if (moveMode && !retainPaused) prepareManual(modelID);
|
|
5199
5901
|
modelMover?.setEnabled(moveMode);
|
|
5200
5902
|
syncTool();
|
|
5201
5903
|
$("move-mode").hidden = !moveMode;
|
|
@@ -5372,6 +6074,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5372
6074
|
function refreshAnimatedPose() {
|
|
5373
6075
|
if (disposed || modelID === null) return;
|
|
5374
6076
|
syncAnimatedRecord(records.get(modelID), viewer?._models.get(modelID));
|
|
6077
|
+
if (!$("info-panel").hidden) populateInfo();
|
|
5375
6078
|
if (!$("scene-panel").hidden && $("position-details").open)
|
|
5376
6079
|
syncPosition(true);
|
|
5377
6080
|
}
|
|
@@ -5496,7 +6199,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5496
6199
|
if (queue.length && !draining) drainPromise = drainQueue();
|
|
5497
6200
|
return drainPromise || Promise.resolve();
|
|
5498
6201
|
}
|
|
5499
|
-
function openURL(value,
|
|
6202
|
+
function openURL(value, format2 = "") {
|
|
5500
6203
|
assertActive();
|
|
5501
6204
|
if (!options.url_import_endpoint)
|
|
5502
6205
|
throw new Error("URL imports are not configured.");
|
|
@@ -5505,7 +6208,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5505
6208
|
throw new Error(
|
|
5506
6209
|
"Use a public HTTP or HTTPS file URL without login credentials."
|
|
5507
6210
|
);
|
|
5508
|
-
if (
|
|
6211
|
+
if (format2 && !/^(stl|obj|3mf|step|stp|iges|igs|vsb|vsj)$/.test(format2))
|
|
5509
6212
|
throw new Error("Choose a supported 3D format.");
|
|
5510
6213
|
url.hash = "";
|
|
5511
6214
|
const name = decodeURIComponent(
|
|
@@ -5514,9 +6217,9 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5514
6217
|
queue.push({
|
|
5515
6218
|
name,
|
|
5516
6219
|
remote: url.href,
|
|
5517
|
-
format,
|
|
6220
|
+
format: format2,
|
|
5518
6221
|
size: 0,
|
|
5519
|
-
scene: ["vsb", "vsj"].includes(
|
|
6222
|
+
scene: ["vsb", "vsj"].includes(format2) ? format2 : sceneFormat(name)
|
|
5520
6223
|
});
|
|
5521
6224
|
if (!draining) drainPromise = drainQueue();
|
|
5522
6225
|
return drainPromise || Promise.resolve();
|
|
@@ -5585,7 +6288,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5585
6288
|
await engine.when_idle();
|
|
5586
6289
|
if (token !== generation) continue;
|
|
5587
6290
|
loadingID = null;
|
|
5588
|
-
const loaded = lastLoadedID === id ? engine.get_model_info(id) : null;
|
|
6291
|
+
const loaded = lastLoadedID === id ? engine.get_model_info(id, { measurements: false }) : null;
|
|
5589
6292
|
if (!loaded) {
|
|
5590
6293
|
announce(source.name + " could not be opened.");
|
|
5591
6294
|
continue;
|
|
@@ -5677,7 +6380,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5677
6380
|
);
|
|
5678
6381
|
const record = {
|
|
5679
6382
|
id,
|
|
5680
|
-
name: String(m.orig_filename || m.filename).split(/[\\/]/).pop().split(/[?#]/)[0],
|
|
6383
|
+
name: String(m.display_name || m.orig_filename || m.filename).split(/[\\/]/).pop().split(/[?#]/)[0],
|
|
5681
6384
|
size: g.file_bytes ?? m.local_file?.size ?? null,
|
|
5682
6385
|
units: g.units || m.units,
|
|
5683
6386
|
bounds,
|
|
@@ -6006,6 +6709,44 @@ function mountViewstlApp(root, options = {}) {
|
|
|
6006
6709
|
refreshPose: refreshAnimatedPose,
|
|
6007
6710
|
announce
|
|
6008
6711
|
});
|
|
6712
|
+
if ($("size-details"))
|
|
6713
|
+
toolsUI = bindAppTools({
|
|
6714
|
+
$,
|
|
6715
|
+
listen,
|
|
6716
|
+
window: window2,
|
|
6717
|
+
getViewer: () => viewer,
|
|
6718
|
+
getState: () => ({ selectedId: modelID, busy }),
|
|
6719
|
+
refresh: renderScene,
|
|
6720
|
+
announce,
|
|
6721
|
+
changeSize(operation) {
|
|
6722
|
+
setMoveMode(false);
|
|
6723
|
+
const record = records.get(modelID);
|
|
6724
|
+
syncAnimatedRecord(record, viewer._models.get(modelID));
|
|
6725
|
+
operation();
|
|
6726
|
+
syncAnimatedRecord(record, viewer._models.get(modelID));
|
|
6727
|
+
$("scene-arrangement").value = "custom";
|
|
6728
|
+
renderScene();
|
|
6729
|
+
},
|
|
6730
|
+
duplicate() {
|
|
6731
|
+
const original = records.get(modelID), id = viewer.duplicate_model(modelID), m = viewer._models.get(id);
|
|
6732
|
+
const record = {
|
|
6733
|
+
...original,
|
|
6734
|
+
id,
|
|
6735
|
+
name: m.display_name,
|
|
6736
|
+
position: { ...original.position },
|
|
6737
|
+
rotation: { ...original.rotation }
|
|
6738
|
+
};
|
|
6739
|
+
syncAnimatedRecord(record, m);
|
|
6740
|
+
record.originalPosition = { ...record.position };
|
|
6741
|
+
record.originalRotation = { ...record.rotation };
|
|
6742
|
+
records.set(id, record);
|
|
6743
|
+
nextID = Math.max(nextID, id + 1);
|
|
6744
|
+
$("scene-arrangement").value = "custom";
|
|
6745
|
+
selectModel(id);
|
|
6746
|
+
setMoveMode(true, "move", true);
|
|
6747
|
+
announce("Copy selected at the same position. Drag to move it.");
|
|
6748
|
+
}
|
|
6749
|
+
});
|
|
6009
6750
|
updateGrid();
|
|
6010
6751
|
setBusy(false);
|
|
6011
6752
|
setMoveMode(false);
|
|
@@ -6054,6 +6795,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
6054
6795
|
exportUI?.dispose();
|
|
6055
6796
|
printUI?.dispose();
|
|
6056
6797
|
animationUI?.dispose();
|
|
6798
|
+
toolsUI?.dispose();
|
|
6057
6799
|
cancelLoading();
|
|
6058
6800
|
setMoveMode(false);
|
|
6059
6801
|
appearanceRequest++;
|