@omrips/viewstl 2.0.0-rc.4 → 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 +19 -173
- package/build/chunks/archive-QK7XZSVT.js +1 -0
- package/build/{v2/chunks/cad-VY42CMP4.js → chunks/cad-QGVO6UGR.js} +1 -1
- package/build/chunks/chunk-6AW5U52H.js +1 -0
- package/build/{v2/chunks/chunk-R3HNTB5A.js → chunks/chunk-CVGQTUJ5.js} +2 -2
- package/build/chunks/chunk-FUXP2W57.js +1 -0
- package/build/chunks/chunk-LFTKNL2V.js +1 -0
- package/build/{v2/chunks/chunk-ZMK3UZZE.js → chunks/chunk-T6PPRDOE.js} +1 -1
- package/build/chunks/chunk-V6KYGP62.js +1 -0
- package/build/{v2/chunks/export-7OP4ZGGJ.js → chunks/export-2CFMTPJ3.js} +3 -3
- package/build/{v2/chunks/export-cad-QPBT756V.js → chunks/export-cad-XOXIE726.js} +1 -1
- package/build/{v2/chunks/export-zip-EDCWH5PX.js → chunks/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/{v2/chunks/scene-export-KQBS7VRJ.js → chunks/scene-export-FPPL466I.js} +1 -1
- package/build/chunks/scene-reader-WXTRELY3.js +1 -0
- package/build/{v2/node → node}/analysis-worker.js +13 -12
- package/build/{v2/node → node}/index.cjs +7 -7
- package/build/{v2/node → node}/index.js +6 -6
- package/build/stl_viewer.min.js +51 -0
- package/build/{v2/viewstl.cjs → viewstl.cjs} +1641 -272
- package/build/{v2/viewstl.d.cts → viewstl.d.cts} +98 -0
- package/build/{v2/viewstl.d.ts → viewstl.d.ts} +98 -0
- package/build/{v2/viewstl.js → viewstl.js} +1641 -272
- package/build/viewstl.min.js +51 -0
- package/build/viewstl.worker.js +1 -0
- package/docs/releases/2.0.0-rc.6.md +7 -0
- package/docs/v2-compatibility.md +5 -5
- package/docs/v2-usage.md +152 -21
- package/docs/v2-verification.md +169 -0
- package/legacy/README.md +14 -8
- package/package.json +18 -35
- package/build/v2/README.md +0 -15
- package/build/v2/chunks/archive-VGS4J4Z7.js +0 -1
- package/build/v2/chunks/chunk-JH3H3JWA.js +0 -1
- package/build/v2/chunks/chunk-U33KANV7.js +0 -1
- package/build/v2/chunks/print-check-2PU7CSL3.js +0 -1
- package/build/v2/chunks/scene-reader-APUJOPUR.js +0 -1
- package/build/v2/stl_viewer.min.js +0 -51
- package/build/v2/viewstl.min.js +0 -51
- package/build/v2/viewstl.worker.js +0 -1
- /package/build/{v2/THIRD-PARTY-NOTICES.txt → THIRD-PARTY-NOTICES.txt} +0 -0
- /package/build/{v2/cad → cad}/occt-import-js.js +0 -0
- /package/build/{v2/cad → cad}/occt-import-js.source.txt +0 -0
- /package/build/{v2/cad → cad}/occt-import-js.wasm +0 -0
- /package/build/{v2/cad → cad}/prepare-cad.source.txt +0 -0
- /package/build/{v2/chunks/chunk-JS56KC3I.js → chunks/chunk-W5KVLGVR.js} +0 -0
- /package/build/{v2/chunks → chunks}/chunk-ZSV4PTIA.js +0 -0
- /package/build/{v2/node → node}/index.d.cts +0 -0
- /package/build/{v2/node → node}/index.d.ts +0 -0
|
@@ -16,7 +16,7 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
16
|
};
|
|
17
17
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
18
|
|
|
19
|
-
// src/
|
|
19
|
+
// src/commonjs.js
|
|
20
20
|
var commonjs_exports = {};
|
|
21
21
|
__export(commonjs_exports, {
|
|
22
22
|
StlViewer: () => StlViewer,
|
|
@@ -25,55 +25,7 @@ __export(commonjs_exports, {
|
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(commonjs_exports);
|
|
27
27
|
|
|
28
|
-
// src/
|
|
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
|
-
// src/v2/math.js
|
|
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]);
|
|
79
31
|
var mul = (a, s) => a.map((v) => v * s);
|
|
@@ -225,7 +177,178 @@ function rayTriangle(o, d, p, i) {
|
|
|
225
177
|
return t >= 0 ? t : Infinity;
|
|
226
178
|
}
|
|
227
179
|
|
|
228
|
-
// src/
|
|
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
|
|
229
352
|
var MEMORY_OVERHEAD = 64 * 1024 * 1024;
|
|
230
353
|
var TRIANGLE_MEMORY = 224;
|
|
231
354
|
function geometryMemory(g) {
|
|
@@ -242,7 +365,420 @@ function derivedMemory(triangles, features) {
|
|
|
242
365
|
return MEMORY_OVERHEAD + triangles * (TRIANGLE_MEMORY + (features.smooth ? 1536 : 0) + (features.edges || features.wire ? 2048 : 0));
|
|
243
366
|
}
|
|
244
367
|
|
|
245
|
-
// src/
|
|
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);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
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 };
|
|
749
|
+
}
|
|
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
|
+
};
|
|
757
|
+
}
|
|
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;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/geometry.js
|
|
246
782
|
var DEFAULT_LIMITS = Object.freeze({
|
|
247
783
|
max_file_bytes: 256 * 1024 * 1024,
|
|
248
784
|
max_triangles: 5e6,
|
|
@@ -390,7 +926,7 @@ function exportSTL(g, scale = [1, 1, 1], center = [0, 0, 0]) {
|
|
|
390
926
|
return buffer;
|
|
391
927
|
}
|
|
392
928
|
|
|
393
|
-
// src/
|
|
929
|
+
// src/infinite-grid.js
|
|
394
930
|
var vertex = `#version 300 es
|
|
395
931
|
precision highp float;
|
|
396
932
|
out vec2 screen;
|
|
@@ -482,7 +1018,7 @@ var InfiniteGrid = class {
|
|
|
482
1018
|
}
|
|
483
1019
|
};
|
|
484
1020
|
|
|
485
|
-
// src/
|
|
1021
|
+
// src/selection-frame.js
|
|
486
1022
|
function selectionFrame(bounds) {
|
|
487
1023
|
const padding = Math.max(
|
|
488
1024
|
Math.max(...bounds.max.map((v, i) => v - bounds.min[i])) * 0.015,
|
|
@@ -500,7 +1036,7 @@ function selectionFrame(bounds) {
|
|
|
500
1036
|
return new Float32Array(lines);
|
|
501
1037
|
}
|
|
502
1038
|
|
|
503
|
-
// src/
|
|
1039
|
+
// src/renderer.js
|
|
504
1040
|
var vertex2 = `#version 300 es
|
|
505
1041
|
precision highp float;
|
|
506
1042
|
layout(location=0) in vec3 position;
|
|
@@ -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
|
}
|
|
@@ -921,7 +1477,7 @@ var Renderer = class {
|
|
|
921
1477
|
}
|
|
922
1478
|
};
|
|
923
1479
|
|
|
924
|
-
// src/
|
|
1480
|
+
// src/controls.js
|
|
925
1481
|
var Controls = class {
|
|
926
1482
|
constructor(canvas, camera, invalidate, pick, type = 0) {
|
|
927
1483
|
this.enabled = true;
|
|
@@ -1032,7 +1588,7 @@ var Controls = class {
|
|
|
1032
1588
|
}
|
|
1033
1589
|
};
|
|
1034
1590
|
|
|
1035
|
-
// src/
|
|
1591
|
+
// src/worker-pool.js
|
|
1036
1592
|
var WorkerPool = class {
|
|
1037
1593
|
constructor(url, { count = 2, timeout = 12e4, factory } = {}) {
|
|
1038
1594
|
this.url = url;
|
|
@@ -1117,7 +1673,7 @@ var WorkerPool = class {
|
|
|
1117
1673
|
}
|
|
1118
1674
|
};
|
|
1119
1675
|
|
|
1120
|
-
// src/
|
|
1676
|
+
// src/scene-import.js
|
|
1121
1677
|
var sceneFormat = (name) => String(name || "").match(/\.(vsb|vsj)(?:[?#]|$)/i)?.[1].toLowerCase() || "";
|
|
1122
1678
|
function localSceneFile(reference, files) {
|
|
1123
1679
|
let path;
|
|
@@ -1151,7 +1707,7 @@ function sceneURL(reference, base) {
|
|
|
1151
1707
|
return url.href;
|
|
1152
1708
|
}
|
|
1153
1709
|
|
|
1154
|
-
// src/
|
|
1710
|
+
// src/scene-import-controller.js
|
|
1155
1711
|
function importScene(source, options = {}) {
|
|
1156
1712
|
const controller = new AbortController(), signal = controller.signal;
|
|
1157
1713
|
const generation = this._generation, ids = [], added = /* @__PURE__ */ new Map(), usedFiles = /* @__PURE__ */ new Set();
|
|
@@ -1176,8 +1732,8 @@ function importScene(source, options = {}) {
|
|
|
1176
1732
|
await this.when_ready;
|
|
1177
1733
|
check();
|
|
1178
1734
|
oldCamera = this.get_camera_state();
|
|
1179
|
-
const
|
|
1180
|
-
if (!["vsb", "vsj"].includes(
|
|
1735
|
+
const format2 = options.format || sceneFormat(typeof source === "string" ? source : source.name);
|
|
1736
|
+
if (!["vsb", "vsj"].includes(format2))
|
|
1181
1737
|
throw new Error("Choose VSB or VSJ for a scene");
|
|
1182
1738
|
let remote, relay;
|
|
1183
1739
|
if (typeof source === "string") {
|
|
@@ -1210,8 +1766,8 @@ function importScene(source, options = {}) {
|
|
|
1210
1766
|
source,
|
|
1211
1767
|
remote,
|
|
1212
1768
|
relay,
|
|
1213
|
-
format,
|
|
1214
|
-
name: `scene.${
|
|
1769
|
+
format: format2,
|
|
1770
|
+
name: `scene.${format2}`,
|
|
1215
1771
|
limits
|
|
1216
1772
|
},
|
|
1217
1773
|
(p) => {
|
|
@@ -1232,6 +1788,10 @@ function importScene(source, options = {}) {
|
|
|
1232
1788
|
job = null;
|
|
1233
1789
|
check();
|
|
1234
1790
|
const { scene, files, base } = documentData;
|
|
1791
|
+
const bookmarks = mergeBookmarks(
|
|
1792
|
+
this.get_camera_bookmarks(),
|
|
1793
|
+
scene.camera_bookmarks || []
|
|
1794
|
+
);
|
|
1235
1795
|
this._sceneBytes -= held;
|
|
1236
1796
|
held = Object.values(files || {}).reduce((n, b) => n + b.byteLength, 0) + JSON.stringify(scene).length * 2;
|
|
1237
1797
|
this._sceneBytes += held;
|
|
@@ -1266,7 +1826,7 @@ function importScene(source, options = {}) {
|
|
|
1266
1826
|
progress({ phase: "opening-model", loaded: index, total: models.length });
|
|
1267
1827
|
check();
|
|
1268
1828
|
const input = models[index], id = this.max_model_id + 1;
|
|
1269
|
-
this.add_model({ ...input, id });
|
|
1829
|
+
this.add_model({ ...input, id, animation: null, animation_state: null });
|
|
1270
1830
|
const model = this._models.get(id);
|
|
1271
1831
|
if (!model) throw new Error(this.error || "Invalid scene model");
|
|
1272
1832
|
ids.push(id);
|
|
@@ -1291,7 +1851,9 @@ function importScene(source, options = {}) {
|
|
|
1291
1851
|
}),
|
|
1292
1852
|
delta
|
|
1293
1853
|
);
|
|
1294
|
-
|
|
1854
|
+
if (input.animation)
|
|
1855
|
+
model.center = [...input.animation_state?.center ?? desired];
|
|
1856
|
+
else ["x", "y", "z"].forEach((axis, i) => model[axis] += shift[i]);
|
|
1295
1857
|
if ([...this._models.values()].reduce(
|
|
1296
1858
|
(n, m) => n + (m.geometry?.triangles || 0),
|
|
1297
1859
|
0
|
|
@@ -1304,7 +1866,12 @@ function importScene(source, options = {}) {
|
|
|
1304
1866
|
total: models.length
|
|
1305
1867
|
});
|
|
1306
1868
|
check();
|
|
1307
|
-
for (
|
|
1869
|
+
for (let index = 0; index < ids.length; index++) {
|
|
1870
|
+
const model = added.get(ids[index]), input = models[index];
|
|
1871
|
+
if (input.animation)
|
|
1872
|
+
this._startAnimation(model, input.animation, input.animation_state);
|
|
1873
|
+
model._sceneImportPending = false;
|
|
1874
|
+
}
|
|
1308
1875
|
if (scene.bg_color !== void 0) this.set_bg_color(scene.bg_color);
|
|
1309
1876
|
if (scene.infinite_grid)
|
|
1310
1877
|
this.set_infinite_grid(
|
|
@@ -1316,6 +1883,8 @@ function importScene(source, options = {}) {
|
|
|
1316
1883
|
if (scene.auto_rotate !== void 0)
|
|
1317
1884
|
this.set_auto_rotate(scene.auto_rotate);
|
|
1318
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;
|
|
1319
1888
|
committed = true;
|
|
1320
1889
|
return { model_ids: ids, scene, used_files: [...usedFiles] };
|
|
1321
1890
|
})().catch((error) => {
|
|
@@ -1336,62 +1905,7 @@ function importScene(source, options = {}) {
|
|
|
1336
1905
|
return promise;
|
|
1337
1906
|
}
|
|
1338
1907
|
|
|
1339
|
-
// src/
|
|
1340
|
-
function printOptions(input = {}) {
|
|
1341
|
-
const level = input.level ?? "basic";
|
|
1342
|
-
if (!["basic", "detailed"].includes(level))
|
|
1343
|
-
throw new Error("Choose basic or detailed print checks");
|
|
1344
|
-
const number = (v, name, max = 1e9) => {
|
|
1345
|
-
if (!Number.isFinite(v) || v <= 0 || v > max)
|
|
1346
|
-
throw new Error(`Invalid ${name}`);
|
|
1347
|
-
return v;
|
|
1348
|
-
};
|
|
1349
|
-
let profile = null;
|
|
1350
|
-
if (input.profile != null) {
|
|
1351
|
-
if (input.profile.technology !== "fdm")
|
|
1352
|
-
throw new Error("Only FDM profiles are supported");
|
|
1353
|
-
profile = { technology: "fdm" };
|
|
1354
|
-
for (const key of ["min_wall_mm", "line_width_mm"])
|
|
1355
|
-
if (input.profile[key] != null)
|
|
1356
|
-
profile[key] = number(input.profile[key], key);
|
|
1357
|
-
if (input.profile.overhang_degrees != null)
|
|
1358
|
-
profile.overhang_degrees = number(
|
|
1359
|
-
input.profile.overhang_degrees,
|
|
1360
|
-
"overhang angle",
|
|
1361
|
-
89.9
|
|
1362
|
-
);
|
|
1363
|
-
if (input.profile.build_volume_mm != null) {
|
|
1364
|
-
if (!Array.isArray(input.profile.build_volume_mm) || input.profile.build_volume_mm.length !== 3)
|
|
1365
|
-
throw new Error("Build volume requires X, Y and Z");
|
|
1366
|
-
profile.build_volume_mm = input.profile.build_volume_mm.map(
|
|
1367
|
-
(v) => number(v, "build volume")
|
|
1368
|
-
);
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
const time = number(input.max_time_ms ?? 3e4, "check time limit", 12e4);
|
|
1372
|
-
return {
|
|
1373
|
-
level,
|
|
1374
|
-
profile,
|
|
1375
|
-
apply_transforms: input.apply_transforms !== false,
|
|
1376
|
-
units_confirmed: input.units_confirmed === true,
|
|
1377
|
-
max_time_ms: time
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
var printMemory = (triangles) => 64 * 1024 ** 2 + triangles * 2400;
|
|
1381
|
-
var printSignature = (model) => JSON.stringify([
|
|
1382
|
-
model.x,
|
|
1383
|
-
model.y,
|
|
1384
|
-
model.z,
|
|
1385
|
-
model.rotationx,
|
|
1386
|
-
model.rotationy,
|
|
1387
|
-
model.rotationz,
|
|
1388
|
-
model.scaleVector,
|
|
1389
|
-
model.center,
|
|
1390
|
-
model.units,
|
|
1391
|
-
model.unitScale
|
|
1392
|
-
]);
|
|
1393
|
-
|
|
1394
|
-
// src/v2/print-controller.js
|
|
1908
|
+
// src/print-controller.js
|
|
1395
1909
|
var reports = /* @__PURE__ */ new WeakMap();
|
|
1396
1910
|
function reportCurrent(viewer, report) {
|
|
1397
1911
|
const saved = reports.get(report);
|
|
@@ -1488,7 +2002,9 @@ async function checkPrintability(options = {}) {
|
|
|
1488
2002
|
rotationy: 0,
|
|
1489
2003
|
rotationz: 0,
|
|
1490
2004
|
center: [0, 0, 0],
|
|
1491
|
-
scaleVector: [
|
|
2005
|
+
scaleVector: [1, 1, 1].map(
|
|
2006
|
+
() => model.unitScale * (model.size_state?.unit_factor ?? 1)
|
|
2007
|
+
)
|
|
1492
2008
|
};
|
|
1493
2009
|
progress({
|
|
1494
2010
|
phase: "Starting",
|
|
@@ -1604,7 +2120,7 @@ function showPrintability(report, { issue, model_id = report.model_ids[0] } = {}
|
|
|
1604
2120
|
return lines.length / 6;
|
|
1605
2121
|
}
|
|
1606
2122
|
|
|
1607
|
-
// src/
|
|
2123
|
+
// src/export-options.js
|
|
1608
2124
|
function exportFilename(filename, extension) {
|
|
1609
2125
|
const name = String(filename || "scene").split(/[\\/]/).pop().split(/[?#]/)[0].replace(/[<>:"|*\x00-\x1f\x7f]/g, "_").replace(/\.(?:stl|obj|3mf|step|stp|iges|igs|zip|vsb|vsj)$/i, "").replace(/[. ]+$/g, "").slice(0, 160) || "scene";
|
|
1610
2126
|
return `${name}.${extension}`;
|
|
@@ -1612,8 +2128,8 @@ function exportFilename(filename, extension) {
|
|
|
1612
2128
|
function exportOptions(options = {}) {
|
|
1613
2129
|
const aliases = { stp: "step", igs: "iges" };
|
|
1614
2130
|
const requested = String(options.format || "").toLowerCase();
|
|
1615
|
-
const
|
|
1616
|
-
if (!["stl", "obj", "3mf", "step", "iges"].includes(
|
|
2131
|
+
const format2 = aliases[requested] || requested;
|
|
2132
|
+
if (!["stl", "obj", "3mf", "step", "iges"].includes(format2))
|
|
1617
2133
|
throw new Error("Export format must be stl, obj, 3mf, step or iges");
|
|
1618
2134
|
const units = options.units ?? "mm";
|
|
1619
2135
|
if (!["mm", "inch"].includes(units))
|
|
@@ -1623,10 +2139,10 @@ function exportOptions(options = {}) {
|
|
|
1623
2139
|
throw new Error(`${key} must be a boolean`);
|
|
1624
2140
|
if (options.on_progress !== void 0 && typeof options.on_progress !== "function")
|
|
1625
2141
|
throw new Error("on_progress must be a function");
|
|
1626
|
-
const colors = options.colors !== false && ["obj", "3mf"].includes(
|
|
1627
|
-
const extension =
|
|
2142
|
+
const colors = options.colors !== false && ["obj", "3mf"].includes(format2);
|
|
2143
|
+
const extension = format2 === "obj" && colors ? "zip" : format2;
|
|
1628
2144
|
return {
|
|
1629
|
-
format,
|
|
2145
|
+
format: format2,
|
|
1630
2146
|
units,
|
|
1631
2147
|
colors,
|
|
1632
2148
|
apply_transforms: options.apply_transforms !== false,
|
|
@@ -1645,7 +2161,7 @@ function exportMemory(models, options) {
|
|
|
1645
2161
|
return MEMORY_OVERHEAD + triangles * perTriangle[options.format] + models.length * 4096;
|
|
1646
2162
|
}
|
|
1647
2163
|
|
|
1648
|
-
// src/
|
|
2164
|
+
// src/io.js
|
|
1649
2165
|
async function readSource(source, limit, progress = () => {
|
|
1650
2166
|
}, signal) {
|
|
1651
2167
|
let stream, total = 0;
|
|
@@ -1707,7 +2223,7 @@ async function readSource(source, limit, progress = () => {
|
|
|
1707
2223
|
return data.buffer;
|
|
1708
2224
|
}
|
|
1709
2225
|
|
|
1710
|
-
// src/
|
|
2226
|
+
// src/three-bridge.js
|
|
1711
2227
|
function fromThreeMesh(mesh, limits) {
|
|
1712
2228
|
if (!mesh?.isMesh && !mesh?.geometry)
|
|
1713
2229
|
throw new Error("Expected a Three.js mesh");
|
|
@@ -1780,7 +2296,7 @@ function toThreeMesh(model, THREE) {
|
|
|
1780
2296
|
return mesh;
|
|
1781
2297
|
}
|
|
1782
2298
|
|
|
1783
|
-
// src/
|
|
2299
|
+
// src/viewer.js
|
|
1784
2300
|
var transformKeys = [
|
|
1785
2301
|
"x",
|
|
1786
2302
|
"y",
|
|
@@ -1818,7 +2334,10 @@ var modelKeys = [
|
|
|
1818
2334
|
"smooth_shading",
|
|
1819
2335
|
"section",
|
|
1820
2336
|
"units",
|
|
1821
|
-
"animation"
|
|
2337
|
+
"animation",
|
|
2338
|
+
"animation_state",
|
|
2339
|
+
"size_state",
|
|
2340
|
+
"display_name"
|
|
1822
2341
|
];
|
|
1823
2342
|
var finite = (v, name) => {
|
|
1824
2343
|
if (typeof v !== "number" || !Number.isFinite(v))
|
|
@@ -1850,7 +2369,7 @@ function modelOptions(input) {
|
|
|
1850
2369
|
return out;
|
|
1851
2370
|
}
|
|
1852
2371
|
var StlViewer = class _StlViewer {
|
|
1853
|
-
static version = "2.0.0-rc.
|
|
2372
|
+
static version = "2.0.0-rc.6";
|
|
1854
2373
|
static workerURL = null;
|
|
1855
2374
|
constructor(parent, options = {}) {
|
|
1856
2375
|
if (!parent?.appendChild)
|
|
@@ -1879,6 +2398,8 @@ var StlViewer = class _StlViewer {
|
|
|
1879
2398
|
this._sessionActive = false;
|
|
1880
2399
|
this._frame = 0;
|
|
1881
2400
|
this._lastTime = 0;
|
|
2401
|
+
this.camera_bookmarks = [];
|
|
2402
|
+
this.display_units = "mm";
|
|
1882
2403
|
this.grid = null;
|
|
1883
2404
|
this._gridAuto = false;
|
|
1884
2405
|
this.camera = {
|
|
@@ -1900,7 +2421,9 @@ var StlViewer = class _StlViewer {
|
|
|
1900
2421
|
"model_loaded_callback",
|
|
1901
2422
|
"load_error_callback",
|
|
1902
2423
|
"loading_progress_callback",
|
|
1903
|
-
"on_model_drop"
|
|
2424
|
+
"on_model_drop",
|
|
2425
|
+
"animation_state_callback",
|
|
2426
|
+
"animation_complete_callback"
|
|
1904
2427
|
])
|
|
1905
2428
|
this[name] = options[name] ?? null;
|
|
1906
2429
|
this.onmousedown_callback = options.on_model_mousedown ?? options.on_model_mouseclick ?? null;
|
|
@@ -1939,11 +2462,15 @@ var StlViewer = class _StlViewer {
|
|
|
1939
2462
|
this.parent_element.appendChild(this.canvas);
|
|
1940
2463
|
this.renderer = new Renderer(
|
|
1941
2464
|
this.canvas,
|
|
1942
|
-
() =>
|
|
1943
|
-
|
|
1944
|
-
|
|
2465
|
+
() => {
|
|
2466
|
+
this._suspendAnimations(true);
|
|
2467
|
+
this._message(
|
|
2468
|
+
"Graphics interrupted. Waiting for the browser to restore rendering\u2026"
|
|
2469
|
+
);
|
|
2470
|
+
},
|
|
1945
2471
|
() => {
|
|
1946
2472
|
this._removeMessage();
|
|
2473
|
+
this._suspendAnimations(this.canvas.ownerDocument.hidden);
|
|
1947
2474
|
this._invalidate();
|
|
1948
2475
|
},
|
|
1949
2476
|
(e) => this._error(e)
|
|
@@ -2008,6 +2535,14 @@ var StlViewer = class _StlViewer {
|
|
|
2008
2535
|
}
|
|
2009
2536
|
};
|
|
2010
2537
|
this.canvas.addEventListener("keydown", this._key);
|
|
2538
|
+
this._visibility = () => this._suspendAnimations(
|
|
2539
|
+
this.canvas.ownerDocument.hidden || this.renderer.lost
|
|
2540
|
+
);
|
|
2541
|
+
this.canvas.ownerDocument.addEventListener(
|
|
2542
|
+
"visibilitychange",
|
|
2543
|
+
this._visibility
|
|
2544
|
+
);
|
|
2545
|
+
this._animationSuspended = this.canvas.ownerDocument.hidden;
|
|
2011
2546
|
this.ready = true;
|
|
2012
2547
|
this._readyResolve(this);
|
|
2013
2548
|
this._callback("ready_callback");
|
|
@@ -2022,6 +2557,11 @@ var StlViewer = class _StlViewer {
|
|
|
2022
2557
|
this.controls?.dispose();
|
|
2023
2558
|
this.renderer?.dispose();
|
|
2024
2559
|
this._resizeObserver?.disconnect();
|
|
2560
|
+
if (this._visibility)
|
|
2561
|
+
this.canvas.ownerDocument.removeEventListener(
|
|
2562
|
+
"visibilitychange",
|
|
2563
|
+
this._visibility
|
|
2564
|
+
);
|
|
2025
2565
|
}
|
|
2026
2566
|
}
|
|
2027
2567
|
_callback(name, ...args) {
|
|
@@ -2070,7 +2610,7 @@ var StlViewer = class _StlViewer {
|
|
|
2070
2610
|
const diagnostic = this.renderer?.diagnostic;
|
|
2071
2611
|
if (diagnostic && printSignature(diagnostic.model) !== diagnostic.signature)
|
|
2072
2612
|
this.renderer.setDiagnostic(null);
|
|
2073
|
-
if (this.killsign || !this.renderer || this.renderer.lost || this._frame)
|
|
2613
|
+
if (this.killsign || !this.renderer || this.renderer.lost || this._frame || this._animationSuspended)
|
|
2074
2614
|
return;
|
|
2075
2615
|
this._frame = requestAnimationFrame((t) => {
|
|
2076
2616
|
this._frame = 0;
|
|
@@ -2082,12 +2622,15 @@ var StlViewer = class _StlViewer {
|
|
|
2082
2622
|
this._lastTime = time;
|
|
2083
2623
|
let animated = false;
|
|
2084
2624
|
for (const m of this._models.values())
|
|
2085
|
-
if (m.geometry && m._animation) {
|
|
2625
|
+
if (m.geometry && !m._sceneImportPending && m._animation && !m._animation.paused) {
|
|
2086
2626
|
this._animate(m, time);
|
|
2087
|
-
animated ||= !!m._animation;
|
|
2627
|
+
animated ||= !!m._animation && !m._animation.paused;
|
|
2088
2628
|
}
|
|
2089
2629
|
if (this.auto_rotate && this.controls.enabled)
|
|
2090
2630
|
this.controls.orbit(elapsed * 0.3, 0);
|
|
2631
|
+
const diagnostic = this.renderer.diagnostic;
|
|
2632
|
+
if (diagnostic && printSignature(diagnostic.model) !== diagnostic.signature)
|
|
2633
|
+
this.renderer.setDiagnostic(null);
|
|
2091
2634
|
this._clip();
|
|
2092
2635
|
try {
|
|
2093
2636
|
this.renderer.draw(
|
|
@@ -2193,6 +2736,13 @@ var StlViewer = class _StlViewer {
|
|
|
2193
2736
|
finite(m[key], key);
|
|
2194
2737
|
if (key.startsWith("scale")) positive(m[key], key);
|
|
2195
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
|
+
}
|
|
2196
2746
|
m.opacity = supplied.opacity ?? supplied.mesh?.material?.opacity ?? 1;
|
|
2197
2747
|
finite(m.opacity, "opacity");
|
|
2198
2748
|
if (m.opacity < 0 || m.opacity > 1)
|
|
@@ -2211,6 +2761,11 @@ var StlViewer = class _StlViewer {
|
|
|
2211
2761
|
m.color = supplied.color ?? (supplied.mesh?.material?.color?.getHexString ? "#" + supplied.mesh.material.color.getHexString() : null);
|
|
2212
2762
|
if (m.color && m.color !== "transparent") colorRGB(m.color);
|
|
2213
2763
|
m.view_edges = !!supplied.view_edges;
|
|
2764
|
+
m.animation = supplied.animation == null ? null : normalizeAnimation(supplied.animation);
|
|
2765
|
+
m.animation_state = supplied.animation_state == null ? null : normalizePlaybackState(supplied.animation_state);
|
|
2766
|
+
if (m.animation_state && !m.animation)
|
|
2767
|
+
throw new Error("Playback state needs an animation");
|
|
2768
|
+
if (m.animation) createPlayback(m.animation, m, m.animation_state);
|
|
2214
2769
|
Object.defineProperty(m, "scaleVector", {
|
|
2215
2770
|
get: () => [
|
|
2216
2771
|
m.scalex * m.unitScale,
|
|
@@ -2339,7 +2894,7 @@ var StlViewer = class _StlViewer {
|
|
|
2339
2894
|
format: supplied.remote_format,
|
|
2340
2895
|
relay,
|
|
2341
2896
|
source: typeof source === "string" ? new URL(source, document.baseURI).href : source,
|
|
2342
|
-
name: m.filename,
|
|
2897
|
+
name: m.display_name || m.filename,
|
|
2343
2898
|
limits,
|
|
2344
2899
|
features: features.smooth || features.edges || features.wire ? features : null
|
|
2345
2900
|
},
|
|
@@ -2363,7 +2918,10 @@ var StlViewer = class _StlViewer {
|
|
|
2363
2918
|
);
|
|
2364
2919
|
if ((supplied.mesh || supplied.data) && (features.smooth || features.edges || features.wire))
|
|
2365
2920
|
await this._derive(m, features);
|
|
2366
|
-
if (
|
|
2921
|
+
if (m.animation_state) m.center = [...m.animation_state.center];
|
|
2922
|
+
if (m.animation && !m._animation)
|
|
2923
|
+
this._startAnimation(m, m.animation, m.animation_state);
|
|
2924
|
+
delete m.animation_state;
|
|
2367
2925
|
delete m.mesh;
|
|
2368
2926
|
delete m.data;
|
|
2369
2927
|
this._invalidate();
|
|
@@ -2385,6 +2943,8 @@ var StlViewer = class _StlViewer {
|
|
|
2385
2943
|
this._invalidate();
|
|
2386
2944
|
}
|
|
2387
2945
|
_deleteModel(m) {
|
|
2946
|
+
this._stopAnimation(m, "removed");
|
|
2947
|
+
for (const controller of m._measureJobs || []) controller.abort();
|
|
2388
2948
|
for (const [controller, models] of this._printJobs || [])
|
|
2389
2949
|
if (models.has(m)) controller.abort();
|
|
2390
2950
|
this._printCache = null;
|
|
@@ -2412,9 +2972,12 @@ var StlViewer = class _StlViewer {
|
|
|
2412
2972
|
if (this.killsign) return;
|
|
2413
2973
|
this._generation++;
|
|
2414
2974
|
this._printCache = null;
|
|
2975
|
+
this.camera_bookmarks = [];
|
|
2976
|
+
this.display_units = "mm";
|
|
2415
2977
|
this._pool?.clear();
|
|
2416
2978
|
for (const c of this._aborters) c.abort();
|
|
2417
2979
|
this._aborters.clear();
|
|
2980
|
+
for (const m of this._models.values()) this._stopAnimation(m, "clean");
|
|
2418
2981
|
this._models.clear();
|
|
2419
2982
|
this._pending.clear();
|
|
2420
2983
|
this._jobs.clear();
|
|
@@ -2446,6 +3009,11 @@ var StlViewer = class _StlViewer {
|
|
|
2446
3009
|
window.removeEventListener("resize", this._windowResize);
|
|
2447
3010
|
this.renderer?.dispose();
|
|
2448
3011
|
if (this.canvas) {
|
|
3012
|
+
if (this._visibility)
|
|
3013
|
+
this.canvas.ownerDocument.removeEventListener(
|
|
3014
|
+
"visibilitychange",
|
|
3015
|
+
this._visibility
|
|
3016
|
+
);
|
|
2449
3017
|
this.canvas.removeEventListener("drop", this._drop);
|
|
2450
3018
|
this.canvas.removeEventListener("dragover", this._drag);
|
|
2451
3019
|
this.canvas.removeEventListener("keydown", this._key);
|
|
@@ -2584,6 +3152,10 @@ var StlViewer = class _StlViewer {
|
|
|
2584
3152
|
set_position(id, x, y, z) {
|
|
2585
3153
|
return this._guard(() => {
|
|
2586
3154
|
const m = this._model(id);
|
|
3155
|
+
[x, y, z].forEach((v) => {
|
|
3156
|
+
if (v !== void 0) finite(v, "position");
|
|
3157
|
+
});
|
|
3158
|
+
this._stopAnimation(m, "manual");
|
|
2587
3159
|
[x, y, z].forEach((v, i) => {
|
|
2588
3160
|
if (v !== void 0) m[["x", "y", "z"][i]] = finite(v, "position");
|
|
2589
3161
|
});
|
|
@@ -2597,8 +3169,12 @@ var StlViewer = class _StlViewer {
|
|
|
2597
3169
|
if (keepPrevious) {
|
|
2598
3170
|
if (x / m.scalex !== y / m.scaley || x / m.scalex !== z / m.scalez)
|
|
2599
3171
|
throw new Error("keep_prev_scale requires a uniform geometry factor");
|
|
3172
|
+
this._stopAnimation(m, "manual");
|
|
2600
3173
|
m.unitScale *= x / m.scalex;
|
|
2601
|
-
} else
|
|
3174
|
+
} else {
|
|
3175
|
+
this._stopAnimation(m, "manual");
|
|
3176
|
+
[m.scalex, m.scaley, m.scalez] = [x, y, z];
|
|
3177
|
+
}
|
|
2602
3178
|
m._stats = null;
|
|
2603
3179
|
this._invalidate();
|
|
2604
3180
|
});
|
|
@@ -2612,6 +3188,10 @@ var StlViewer = class _StlViewer {
|
|
|
2612
3188
|
set_rotation(id, x, y, z, increment = false) {
|
|
2613
3189
|
return this._guard(() => {
|
|
2614
3190
|
const m = this._model(id);
|
|
3191
|
+
[x, y, z].forEach((v) => {
|
|
3192
|
+
if (v !== void 0) finite(v, "rotation");
|
|
3193
|
+
});
|
|
3194
|
+
this._stopAnimation(m, "manual");
|
|
2615
3195
|
[x, y, z].forEach((v, i) => {
|
|
2616
3196
|
const k = ["rotationx", "rotationy", "rotationz"][i];
|
|
2617
3197
|
if (v !== void 0)
|
|
@@ -2665,6 +3245,7 @@ var StlViewer = class _StlViewer {
|
|
|
2665
3245
|
if (!["mm", "inch"].includes(units))
|
|
2666
3246
|
throw new Error("Units must be mm or inch");
|
|
2667
3247
|
const m = this._model(id);
|
|
3248
|
+
this._stopAnimation(m, "manual");
|
|
2668
3249
|
if (scale && m.units !== units)
|
|
2669
3250
|
m.unitScale *= units === "inch" ? 25.4 : 1 / 25.4;
|
|
2670
3251
|
m.units = units;
|
|
@@ -2832,12 +3413,42 @@ var StlViewer = class _StlViewer {
|
|
|
2832
3413
|
type
|
|
2833
3414
|
);
|
|
2834
3415
|
}
|
|
2835
|
-
|
|
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 = {}) {
|
|
2836
3447
|
return this._guard(() => {
|
|
2837
3448
|
const m = this._model(id), g = m.geometry;
|
|
2838
3449
|
if (!g) return null;
|
|
2839
3450
|
const s = m.scaleVector, key = s.join(",");
|
|
2840
|
-
if (!m._stats || m._stats.key !== key)
|
|
3451
|
+
if (options.measurements !== false && (!m._stats || m._stats.key !== key))
|
|
2841
3452
|
m._stats = { key, ...scaledStats(g, s) };
|
|
2842
3453
|
const factor = m.units === "inch" ? 1 / 25.4 : 1;
|
|
2843
3454
|
return {
|
|
@@ -2853,8 +3464,8 @@ var StlViewer = class _StlViewer {
|
|
|
2853
3464
|
section: m.section ? { ...m.section } : null,
|
|
2854
3465
|
color: m.color ?? (g.colors ? "#ffffff" : "#909090"),
|
|
2855
3466
|
scale: { x: m.scalex, y: m.scaley, z: m.scalez },
|
|
2856
|
-
volume: m._stats.volume * factor ** 3,
|
|
2857
|
-
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,
|
|
2858
3469
|
triangles: g.triangles,
|
|
2859
3470
|
bounds: { min: [...g.bounds.min], max: [...g.bounds.max] },
|
|
2860
3471
|
has_colors: !!g.colors,
|
|
@@ -2942,7 +3553,9 @@ var StlViewer = class _StlViewer {
|
|
|
2942
3553
|
rotationx: 0,
|
|
2943
3554
|
rotationy: 0,
|
|
2944
3555
|
rotationz: 0,
|
|
2945
|
-
scaleVector: [
|
|
3556
|
+
scaleVector: [1, 1, 1].map(
|
|
3557
|
+
() => m.unitScale * (m.size_state?.unit_factor ?? 1)
|
|
3558
|
+
)
|
|
2946
3559
|
};
|
|
2947
3560
|
const matrix = modelMatrix(source), factor = settings.units === "inch" ? 1 / 25.4 : 1;
|
|
2948
3561
|
for (let c = 0; c < 4; c++)
|
|
@@ -3010,8 +3623,8 @@ var StlViewer = class _StlViewer {
|
|
|
3010
3623
|
);
|
|
3011
3624
|
}
|
|
3012
3625
|
async export_scene(options = {}) {
|
|
3013
|
-
const
|
|
3014
|
-
if (!["vsb", "vsj"].includes(
|
|
3626
|
+
const format2 = String(options.format || "").toLowerCase();
|
|
3627
|
+
if (!["vsb", "vsj"].includes(format2))
|
|
3015
3628
|
throw new Error("Scene format must be vsb or vsj");
|
|
3016
3629
|
if (options.force_basename !== void 0 && typeof options.force_basename !== "boolean")
|
|
3017
3630
|
throw new Error("force_basename must be a boolean");
|
|
@@ -3034,7 +3647,7 @@ var StlViewer = class _StlViewer {
|
|
|
3034
3647
|
if (!models.length) throw new Error("No models to export");
|
|
3035
3648
|
if (models.some((m) => !m.geometry))
|
|
3036
3649
|
throw new Error("A model has not finished loading");
|
|
3037
|
-
if (models.length + (
|
|
3650
|
+
if (models.length + (format2 === "vsb" ? 1 : 0) > this.limits.max_archive_entries)
|
|
3038
3651
|
throw new Error("Scene model count limit exceeded");
|
|
3039
3652
|
const triangles = models.reduce((sum, m) => sum + m.geometry.triangles, 0);
|
|
3040
3653
|
if (triangles > this.limits.max_triangles)
|
|
@@ -3042,18 +3655,26 @@ var StlViewer = class _StlViewer {
|
|
|
3042
3655
|
const scene = this.get_vsj(
|
|
3043
3656
|
true,
|
|
3044
3657
|
!!options.force_basename,
|
|
3045
|
-
|
|
3658
|
+
format2 === "vsb"
|
|
3046
3659
|
);
|
|
3047
3660
|
const json = JSON.stringify(scene), manifestBytes = new Blob([json]).size;
|
|
3048
3661
|
if (manifestBytes > this.limits.max_file_bytes)
|
|
3049
3662
|
throw new Error("Scene manifest size limit exceeded");
|
|
3050
|
-
if (
|
|
3663
|
+
if (format2 === "vsb" && manifestBytes + models.length * 84 + triangles * 50 > this.limits.max_uncompressed_bytes)
|
|
3051
3664
|
throw new Error("Scene archive limit exceeded");
|
|
3052
|
-
const working = MEMORY_OVERHEAD + json.length * 8 + (
|
|
3665
|
+
const working = MEMORY_OVERHEAD + json.length * 8 + (format2 === "vsb" ? triangles * 512 : 0);
|
|
3053
3666
|
assertMemory(this.limits, this._occupiedMemory() + working, "scene export");
|
|
3054
3667
|
this._exportBytes += working;
|
|
3055
3668
|
let job, cancel, callbackError;
|
|
3056
3669
|
try {
|
|
3670
|
+
const snapshot = format2 === "vsb" ? models.map((m, index) => ({
|
|
3671
|
+
id: m.id,
|
|
3672
|
+
triangles: m.geometry.triangles,
|
|
3673
|
+
positions: m.geometry.positions.slice(),
|
|
3674
|
+
colors: m.geometry.colors?.slice() || null,
|
|
3675
|
+
scale: scene.models[index].animation_state ? [1, 1, 1] : m.scaleVector,
|
|
3676
|
+
center: scene.models[index].animation_state ? [0, 0, 0] : [...m.center]
|
|
3677
|
+
})) : null;
|
|
3057
3678
|
options.on_progress?.({
|
|
3058
3679
|
loaded: 0,
|
|
3059
3680
|
total: models.length,
|
|
@@ -3061,7 +3682,7 @@ var StlViewer = class _StlViewer {
|
|
|
3061
3682
|
});
|
|
3062
3683
|
checkAbort();
|
|
3063
3684
|
let blob;
|
|
3064
|
-
if (
|
|
3685
|
+
if (format2 === "vsj") {
|
|
3065
3686
|
blob = new Blob([json], { type: "application/json" });
|
|
3066
3687
|
options.on_progress?.({
|
|
3067
3688
|
loaded: models.length,
|
|
@@ -3069,14 +3690,6 @@ var StlViewer = class _StlViewer {
|
|
|
3069
3690
|
phase: "complete"
|
|
3070
3691
|
});
|
|
3071
3692
|
} else {
|
|
3072
|
-
const snapshot = models.map((m) => ({
|
|
3073
|
-
id: m.id,
|
|
3074
|
-
triangles: m.geometry.triangles,
|
|
3075
|
-
positions: m.geometry.positions.slice(),
|
|
3076
|
-
colors: m.geometry.colors?.slice() || null,
|
|
3077
|
-
scale: m.scaleVector,
|
|
3078
|
-
center: [...m.center]
|
|
3079
|
-
}));
|
|
3080
3693
|
job = this._pool.run(
|
|
3081
3694
|
{
|
|
3082
3695
|
kind: "export-scene",
|
|
@@ -3109,8 +3722,8 @@ var StlViewer = class _StlViewer {
|
|
|
3109
3722
|
checkAbort();
|
|
3110
3723
|
return {
|
|
3111
3724
|
blob,
|
|
3112
|
-
format,
|
|
3113
|
-
filename: exportFilename(options.filename,
|
|
3725
|
+
format: format2,
|
|
3726
|
+
filename: exportFilename(options.filename, format2),
|
|
3114
3727
|
model_ids: models.map((m) => m.id),
|
|
3115
3728
|
triangles,
|
|
3116
3729
|
representation: "scene"
|
|
@@ -3132,6 +3745,8 @@ var StlViewer = class _StlViewer {
|
|
|
3132
3745
|
canvas_height: this.canvas_height,
|
|
3133
3746
|
bg_color: this.bg_color,
|
|
3134
3747
|
camera_state: this.get_camera_state(),
|
|
3748
|
+
camera_bookmarks: this.get_camera_bookmarks(),
|
|
3749
|
+
display_units: this.display_units || "mm",
|
|
3135
3750
|
auto_rotate: this.auto_rotate,
|
|
3136
3751
|
mouse_zoom: this.mouse_zoom,
|
|
3137
3752
|
auto_resize: this.auto_resize,
|
|
@@ -3152,6 +3767,11 @@ var StlViewer = class _StlViewer {
|
|
|
3152
3767
|
id: for_vsb ? -1 : m.id,
|
|
3153
3768
|
filename: for_vsb ? `${m.id}.stl` : force_basename ? basename(m.filename) : m.filename,
|
|
3154
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 } : {},
|
|
3155
3775
|
display: m.display,
|
|
3156
3776
|
opacity: m.opacity,
|
|
3157
3777
|
view_edges: m.view_edges,
|
|
@@ -3162,13 +3782,21 @@ var StlViewer = class _StlViewer {
|
|
|
3162
3782
|
model.orig_filename = basename(m.orig_filename || m.filename);
|
|
3163
3783
|
for (const key of transformKeys)
|
|
3164
3784
|
if (!key.startsWith("scale")) model[key] = m[key];
|
|
3165
|
-
if (!for_vsb) {
|
|
3785
|
+
if (!for_vsb || m._animation) {
|
|
3166
3786
|
model.scalex = m.scalex * m.unitScale;
|
|
3167
3787
|
model.scaley = m.scaley * m.unitScale;
|
|
3168
3788
|
model.scalez = m.scalez * m.unitScale;
|
|
3169
3789
|
}
|
|
3170
3790
|
if (m.color) model.color = m.color;
|
|
3171
|
-
|
|
3791
|
+
const playback = savePlayback(m);
|
|
3792
|
+
if (playback) Object.assign(model, playback);
|
|
3793
|
+
else if (!for_vsb) {
|
|
3794
|
+
const center = this.center_models ? m.geometry.bounds.min.map(
|
|
3795
|
+
(v, i) => (v + m.geometry.bounds.max[i]) / 2
|
|
3796
|
+
) : [0, 0, 0];
|
|
3797
|
+
const position = transform(modelMatrix(m), center);
|
|
3798
|
+
["x", "y", "z"].forEach((axis, i) => model[axis] = position[i]);
|
|
3799
|
+
}
|
|
3172
3800
|
data.models.push(model);
|
|
3173
3801
|
}
|
|
3174
3802
|
return as_js_obj ? data : JSON.stringify(data);
|
|
@@ -3182,7 +3810,10 @@ var StlViewer = class _StlViewer {
|
|
|
3182
3810
|
get_vsb() {
|
|
3183
3811
|
return {
|
|
3184
3812
|
vsj: this.get_vsj(true, true, true),
|
|
3185
|
-
files: [...this._models.values()].filter((m) => m.geometry).map((m) => ({
|
|
3813
|
+
files: [...this._models.values()].filter((m) => m.geometry).map((m) => ({
|
|
3814
|
+
id: m.id,
|
|
3815
|
+
bin: m._animation ? exportSTL(m.geometry, [1, 1, 1], [0, 0, 0]) : this.get_stl_bin(m.id)
|
|
3816
|
+
}))
|
|
3186
3817
|
};
|
|
3187
3818
|
}
|
|
3188
3819
|
async get_vsb_blob() {
|
|
@@ -3263,20 +3894,66 @@ var StlViewer = class _StlViewer {
|
|
|
3263
3894
|
);
|
|
3264
3895
|
else if (scene.grid) this.set_grid(true);
|
|
3265
3896
|
if (scene.camera_state) this._restoreCamera = scene.camera_state;
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
model
|
|
3279
|
-
|
|
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
|
+
);
|
|
3904
|
+
const incoming = [];
|
|
3905
|
+
try {
|
|
3906
|
+
for (const input of scene.models) {
|
|
3907
|
+
if (!input || typeof input !== "object")
|
|
3908
|
+
throw new Error("Invalid scene model");
|
|
3909
|
+
const model = modelOptions(input);
|
|
3910
|
+
delete model.mesh;
|
|
3911
|
+
delete model.data;
|
|
3912
|
+
delete model.local_file;
|
|
3913
|
+
if (files) {
|
|
3914
|
+
if (typeof model.filename !== "string" || !Object.hasOwn(files, model.filename))
|
|
3915
|
+
throw new Error("VSB model file is missing");
|
|
3916
|
+
model.local_file = new File(
|
|
3917
|
+
[files[model.filename]],
|
|
3918
|
+
model.filename
|
|
3919
|
+
);
|
|
3920
|
+
} else if (model.filename)
|
|
3921
|
+
model.filename = new URL(model.filename, base).href;
|
|
3922
|
+
const id = model.id === void 0 || model.id === -1 ? this.max_model_id + 1 : model.id;
|
|
3923
|
+
if (this._models.has(id))
|
|
3924
|
+
throw new Error(`Invalid or duplicate model id: ${id}`);
|
|
3925
|
+
this.add_model({
|
|
3926
|
+
...model,
|
|
3927
|
+
id,
|
|
3928
|
+
animation: null,
|
|
3929
|
+
animation_state: null
|
|
3930
|
+
});
|
|
3931
|
+
const m = this._models.get(id);
|
|
3932
|
+
if (!m) throw new Error(this.error || "Invalid scene model");
|
|
3933
|
+
m._sceneImportPending = true;
|
|
3934
|
+
incoming.push({ m, model, pending: this._pending.get(id) });
|
|
3935
|
+
}
|
|
3936
|
+
await Promise.all(incoming.map((item) => item.pending));
|
|
3937
|
+
if (generation !== this._generation || this.killsign) return;
|
|
3938
|
+
for (const { m, model } of incoming) {
|
|
3939
|
+
if (this._models.get(m.id) !== m || !m.geometry)
|
|
3940
|
+
throw new Error("A scene model could not be opened");
|
|
3941
|
+
if (model.animation)
|
|
3942
|
+
createPlayback(model.animation, m, model.animation_state);
|
|
3943
|
+
else if (model.animation_state)
|
|
3944
|
+
throw new Error("Playback state needs an animation");
|
|
3945
|
+
}
|
|
3946
|
+
for (const { m, model } of incoming) {
|
|
3947
|
+
if (model.animation)
|
|
3948
|
+
this._startAnimation(m, model.animation, model.animation_state);
|
|
3949
|
+
m._sceneImportPending = false;
|
|
3950
|
+
}
|
|
3951
|
+
this.camera_bookmarks = bookmarks;
|
|
3952
|
+
this.display_units = displayUnits;
|
|
3953
|
+
} catch (error) {
|
|
3954
|
+
for (const { m } of incoming)
|
|
3955
|
+
if (this._models.get(m.id) === m) this.remove_model(m.id);
|
|
3956
|
+
throw error;
|
|
3280
3957
|
}
|
|
3281
3958
|
})().catch((e) => {
|
|
3282
3959
|
if (generation === this._generation && e.name !== "AbortError")
|
|
@@ -3298,56 +3975,101 @@ var StlViewer = class _StlViewer {
|
|
|
3298
3975
|
return this._guard(() => {
|
|
3299
3976
|
const m = this._model(id);
|
|
3300
3977
|
if (!animation) {
|
|
3301
|
-
m
|
|
3302
|
-
|
|
3978
|
+
this._stopAnimation(m, "stopped");
|
|
3979
|
+
this._invalidate();
|
|
3303
3980
|
return;
|
|
3304
3981
|
}
|
|
3305
|
-
|
|
3306
|
-
for (const mode of ["delta", "exact"])
|
|
3307
|
-
if (copy[mode]) {
|
|
3308
|
-
const a = copy[mode];
|
|
3309
|
-
positive(a.msec ?? 300, "animation duration");
|
|
3310
|
-
const values = {};
|
|
3311
|
-
for (const key of transformKeys) {
|
|
3312
|
-
const value = a[key] ?? (key.startsWith("scale") ? a.scale : void 0);
|
|
3313
|
-
if (value !== void 0) {
|
|
3314
|
-
finite(value, key);
|
|
3315
|
-
if (mode === "exact" && key.startsWith("scale"))
|
|
3316
|
-
positive(value, key);
|
|
3317
|
-
values[key] = value;
|
|
3318
|
-
}
|
|
3319
|
-
}
|
|
3320
|
-
tracks.push({
|
|
3321
|
-
mode,
|
|
3322
|
-
values,
|
|
3323
|
-
duration: a.msec ?? 300,
|
|
3324
|
-
loop: mode === "delta" && !!a.loop,
|
|
3325
|
-
from: Object.fromEntries(transformKeys.map((k) => [k, m[k]]))
|
|
3326
|
-
});
|
|
3327
|
-
}
|
|
3328
|
-
if (!tracks.length) throw new Error("Animation needs delta or exact");
|
|
3329
|
-
m.animation = copy;
|
|
3330
|
-
m._animation = { start: null, tracks };
|
|
3982
|
+
this._startAnimation(m, animation);
|
|
3331
3983
|
this._invalidate();
|
|
3332
3984
|
});
|
|
3333
3985
|
}
|
|
3986
|
+
_startAnimation(m, definition, saved) {
|
|
3987
|
+
const playback = createPlayback(definition, m, saved);
|
|
3988
|
+
if (m._animation) this._stopAnimation(m, "replaced");
|
|
3989
|
+
if (saved) m.center = [...saved.center];
|
|
3990
|
+
m.animation = playback.animation;
|
|
3991
|
+
m._animation = playback;
|
|
3992
|
+
delete m.animation_state;
|
|
3993
|
+
m._animationResult = null;
|
|
3994
|
+
const running = evaluatePlayback(playback, m);
|
|
3995
|
+
if (running) this._animationEvent(m, saved ? "restored" : "started");
|
|
3996
|
+
else this._completeAnimation(m);
|
|
3997
|
+
}
|
|
3998
|
+
_animationEvent(m, reason) {
|
|
3999
|
+
const state = this._animationState(m);
|
|
4000
|
+
queueMicrotask(() => {
|
|
4001
|
+
this._callback("animation_state_callback", m.id, state, reason);
|
|
4002
|
+
if (reason === "completed")
|
|
4003
|
+
this._callback("animation_complete_callback", m.id, state);
|
|
4004
|
+
});
|
|
4005
|
+
}
|
|
4006
|
+
_animationState(m) {
|
|
4007
|
+
return m._animation ? playbackState(m._animation, !!this._animationSuspended) : {
|
|
4008
|
+
...m._animationResult ?? { status: "idle", elapsed_ms: 0 },
|
|
4009
|
+
suspended: false,
|
|
4010
|
+
tracks: []
|
|
4011
|
+
};
|
|
4012
|
+
}
|
|
4013
|
+
_stopAnimation(m, reason) {
|
|
4014
|
+
if (!m._animation && !m.animation) return;
|
|
4015
|
+
m._animationResult = {
|
|
4016
|
+
status: "stopped",
|
|
4017
|
+
elapsed_ms: m._animation?.elapsed_ms ?? 0
|
|
4018
|
+
};
|
|
4019
|
+
m._animation = null;
|
|
4020
|
+
m.animation = null;
|
|
4021
|
+
delete m.animation_state;
|
|
4022
|
+
this._animationEvent(m, reason);
|
|
4023
|
+
}
|
|
4024
|
+
_completeAnimation(m) {
|
|
4025
|
+
m._animationResult = {
|
|
4026
|
+
status: "completed",
|
|
4027
|
+
elapsed_ms: m._animation.elapsed_ms
|
|
4028
|
+
};
|
|
4029
|
+
m._animation = null;
|
|
4030
|
+
m.animation = null;
|
|
4031
|
+
this._animationEvent(m, "completed");
|
|
4032
|
+
}
|
|
4033
|
+
pause_model_animation(id) {
|
|
4034
|
+
return this._guard(() => {
|
|
4035
|
+
const m = this._model(id);
|
|
4036
|
+
if (!m._animation || m._animation.paused) return;
|
|
4037
|
+
m._animation.paused = true;
|
|
4038
|
+
m._animation.last_time = null;
|
|
4039
|
+
this._animationEvent(m, "paused");
|
|
4040
|
+
this._invalidate();
|
|
4041
|
+
});
|
|
4042
|
+
}
|
|
4043
|
+
resume_model_animation(id) {
|
|
4044
|
+
return this._guard(() => {
|
|
4045
|
+
const m = this._model(id);
|
|
4046
|
+
if (!m._animation || !m._animation.paused) return;
|
|
4047
|
+
m._animation.paused = false;
|
|
4048
|
+
m._animation.last_time = null;
|
|
4049
|
+
this._animationEvent(m, "resumed");
|
|
4050
|
+
this._invalidate();
|
|
4051
|
+
});
|
|
4052
|
+
}
|
|
4053
|
+
get_model_animation_state(id) {
|
|
4054
|
+
return this._guard(() => this._animationState(this._model(id)));
|
|
4055
|
+
}
|
|
4056
|
+
_suspendAnimations(suspended) {
|
|
4057
|
+
this._animationSuspended = !!suspended;
|
|
4058
|
+
this._lastTime = 0;
|
|
4059
|
+
for (const m of this._models.values())
|
|
4060
|
+
if (m._animation) m._animation.last_time = null;
|
|
4061
|
+
if (suspended) {
|
|
4062
|
+
cancelAnimationFrame(this._frame);
|
|
4063
|
+
this._frame = 0;
|
|
4064
|
+
} else this._invalidate();
|
|
4065
|
+
}
|
|
3334
4066
|
_animate(m, time) {
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
for (const [k, v] of Object.entries(t.values))
|
|
3342
|
-
m[k] = k.startsWith("scale") ? Math.max(
|
|
3343
|
-
1e-6,
|
|
3344
|
-
t.mode === "delta" ? t.from[k] + v * p : t.from[k] + (v - t.from[k]) * p
|
|
3345
|
-
) : t.mode === "delta" ? t.from[k] + v * p : t.from[k] + (v - t.from[k]) * p;
|
|
3346
|
-
}
|
|
3347
|
-
m._stats = null;
|
|
3348
|
-
if (!running) {
|
|
3349
|
-
m._animation = null;
|
|
3350
|
-
m.animation = null;
|
|
4067
|
+
if (this._animationSuspended || !m._animation) return;
|
|
4068
|
+
try {
|
|
4069
|
+
if (!advancePlayback(m._animation, m, time)) this._completeAnimation(m);
|
|
4070
|
+
} catch (error) {
|
|
4071
|
+
this._stopAnimation(m, "error");
|
|
4072
|
+
this._error(error);
|
|
3351
4073
|
}
|
|
3352
4074
|
}
|
|
3353
4075
|
async get_image_blob(type = "image/png") {
|
|
@@ -3371,7 +4093,300 @@ var StlViewer = class _StlViewer {
|
|
|
3371
4093
|
};
|
|
3372
4094
|
var viewer_default = StlViewer;
|
|
3373
4095
|
|
|
3374
|
-
// src/
|
|
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
|
+
|
|
4389
|
+
// src/memory-policy.js
|
|
3375
4390
|
var GiB = 1024 ** 3;
|
|
3376
4391
|
var MiB = 1024 ** 2;
|
|
3377
4392
|
function memoryPolicy(reportedGiB) {
|
|
@@ -3401,10 +4416,10 @@ async function estimateFile(file) {
|
|
|
3401
4416
|
return 64 * MiB + (file.size || 0) * 3;
|
|
3402
4417
|
}
|
|
3403
4418
|
|
|
3404
|
-
// src/
|
|
3405
|
-
var
|
|
3406
|
-
var vector2 = (p) =>
|
|
3407
|
-
var
|
|
4419
|
+
// src/model-transform.js
|
|
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]]));
|
|
3408
4423
|
var dot2 = (a, b) => a.reduce((sum, v, i) => sum + v * b[i], 0);
|
|
3409
4424
|
var norm2 = (a) => {
|
|
3410
4425
|
const n = Math.hypot(...a) || 1;
|
|
@@ -3435,8 +4450,8 @@ function rotationMatrix(degrees = { x: 0, y: 0, z: 0 }) {
|
|
|
3435
4450
|
}
|
|
3436
4451
|
var centerOf = (bounds) => bounds.min.map((v, i) => (v + bounds.max[i]) / 2);
|
|
3437
4452
|
function modelCenter(model) {
|
|
3438
|
-
return
|
|
3439
|
-
centerOf(model.bounds).map((v, i) => v + model.position[
|
|
4453
|
+
return object2(
|
|
4454
|
+
centerOf(model.bounds).map((v, i) => v + model.position[axes3[i]])
|
|
3440
4455
|
);
|
|
3441
4456
|
}
|
|
3442
4457
|
function modelRadius(model) {
|
|
@@ -3447,7 +4462,7 @@ function modelRadius(model) {
|
|
|
3447
4462
|
}
|
|
3448
4463
|
function enginePosition(model) {
|
|
3449
4464
|
const c = centerOf(model.bounds), rotated = transform2(rotationMatrix(model.rotation), c);
|
|
3450
|
-
return
|
|
4465
|
+
return object2(c.map((v, i) => model.position[axes3[i]] + v - rotated[i]));
|
|
3451
4466
|
}
|
|
3452
4467
|
function modelBounds(model) {
|
|
3453
4468
|
if (!model.rotation || !Object.values(model.rotation).some((v) => v % 360))
|
|
@@ -3500,12 +4515,12 @@ function depthPosition(model, camera, amount) {
|
|
|
3500
4515
|
const clearance = Math.max(model.radius || 1e-6, 1e-6) * 1.1 / Math.max(0.05, dot2(direction, forward));
|
|
3501
4516
|
const desired = (distance || clearance) * Math.exp(Math.max(-6, Math.min(6, amount)));
|
|
3502
4517
|
const next = amount < 0 ? Math.max(Math.min(distance, clearance), desired) : Math.max(distance, Math.min(1e12, desired));
|
|
3503
|
-
return
|
|
4518
|
+
return object2(
|
|
3504
4519
|
vector2(model.position).map((v, i) => v + direction[i] * (next - distance))
|
|
3505
4520
|
);
|
|
3506
4521
|
}
|
|
3507
4522
|
|
|
3508
|
-
// src/
|
|
4523
|
+
// src/scene-layout.js
|
|
3509
4524
|
function scenePlacement(models, mode) {
|
|
3510
4525
|
if (mode === "custom")
|
|
3511
4526
|
return models.map((m) => ({
|
|
@@ -3543,7 +4558,7 @@ function sceneFloor(models, placement) {
|
|
|
3543
4558
|
) : 0;
|
|
3544
4559
|
}
|
|
3545
4560
|
|
|
3546
|
-
// src/
|
|
4561
|
+
// src/model-move.js
|
|
3547
4562
|
var vector3 = (p) => [p.x, p.y, p.z];
|
|
3548
4563
|
var sub2 = (a, b) => a.map((v, i) => v - b[i]);
|
|
3549
4564
|
var dot3 = (a, b) => a.reduce((n, v, i) => n + v * b[i], 0);
|
|
@@ -3709,7 +4724,7 @@ function bindModelMove(canvas, {
|
|
|
3709
4724
|
};
|
|
3710
4725
|
}
|
|
3711
4726
|
|
|
3712
|
-
// src/
|
|
4727
|
+
// src/drop-source.js
|
|
3713
4728
|
function dropKind(transfer) {
|
|
3714
4729
|
const types = Array.from(transfer?.types || [], (type) => type.toLowerCase());
|
|
3715
4730
|
if (types.includes("files") || transfer?.files?.length) return "files";
|
|
@@ -3749,7 +4764,7 @@ function droppedURLs(transfer) {
|
|
|
3749
4764
|
return [...urls];
|
|
3750
4765
|
}
|
|
3751
4766
|
|
|
3752
|
-
// src/
|
|
4767
|
+
// src/app-export.js
|
|
3753
4768
|
var exportControls = [
|
|
3754
4769
|
"save-button",
|
|
3755
4770
|
"save-dialog",
|
|
@@ -3788,16 +4803,16 @@ function bindAppExport({
|
|
|
3788
4803
|
$("save-error").textContent = "";
|
|
3789
4804
|
};
|
|
3790
4805
|
function formatChanged() {
|
|
3791
|
-
const
|
|
4806
|
+
const format2 = $("save-format").value;
|
|
3792
4807
|
if (!$("save-name").value || $("save-name").value === lastSuggestedName) {
|
|
3793
4808
|
suggestedName();
|
|
3794
4809
|
}
|
|
3795
4810
|
if ($("save-mesh-note")) $("save-mesh-note").hidden = isScene();
|
|
3796
4811
|
if ($("save-scene-note")) $("save-scene-note").hidden = !isScene();
|
|
3797
|
-
$("save-colors-row").hidden = !["obj", "3mf"].includes(
|
|
3798
|
-
$("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);
|
|
3799
4814
|
$("save-format-note").textContent = $("save-format").selectedOptions[0]?.dataset.note || "";
|
|
3800
|
-
$("save-submit").textContent =
|
|
4815
|
+
$("save-submit").textContent = format2 === "obj" && $("save-colors").checked ? "Download ZIP" : "Download file";
|
|
3801
4816
|
sync();
|
|
3802
4817
|
}
|
|
3803
4818
|
function suggestedName() {
|
|
@@ -3919,7 +4934,7 @@ function bindAppExport({
|
|
|
3919
4934
|
};
|
|
3920
4935
|
}
|
|
3921
4936
|
|
|
3922
|
-
// src/
|
|
4937
|
+
// src/app-print.js
|
|
3923
4938
|
var printControls = [
|
|
3924
4939
|
"print-button",
|
|
3925
4940
|
"print-panel",
|
|
@@ -3953,6 +4968,7 @@ function bindAppPrint({
|
|
|
3953
4968
|
}) {
|
|
3954
4969
|
let job = null, report = null, timer = null, disposed = false, scopeKey = "", previousScope = "", unavailable = false;
|
|
3955
4970
|
const panel = $("print-panel"), document2 = panel.ownerDocument;
|
|
4971
|
+
const pausedForCheck = /* @__PURE__ */ new Set();
|
|
3956
4972
|
const summary = {
|
|
3957
4973
|
issues_found: "Issues found",
|
|
3958
4974
|
incomplete: "Check incomplete",
|
|
@@ -3974,6 +4990,15 @@ function bindAppPrint({
|
|
|
3974
4990
|
const s = getState();
|
|
3975
4991
|
$("print-button").disabled = s.busy || !s.models.length;
|
|
3976
4992
|
if (panel.hidden) return;
|
|
4993
|
+
for (const id of pausedForCheck)
|
|
4994
|
+
if (!s.models.some((m) => m.id === id) || getViewer()?.get_model_animation_state(id)?.status !== "paused")
|
|
4995
|
+
pausedForCheck.delete(id);
|
|
4996
|
+
if ($("print-resume")) {
|
|
4997
|
+
$("print-resume").hidden = !pausedForCheck.size;
|
|
4998
|
+
$("print-resume").disabled = !!job || s.busy;
|
|
4999
|
+
}
|
|
5000
|
+
if ($("print-animation-note"))
|
|
5001
|
+
$("print-animation-note").hidden = !pausedForCheck.size;
|
|
3977
5002
|
const wasUnavailable = unavailable;
|
|
3978
5003
|
unavailable = s.busy || !s.models.length;
|
|
3979
5004
|
$("print-run").disabled = !!job || unavailable;
|
|
@@ -4120,6 +5145,19 @@ function bindAppPrint({
|
|
|
4120
5145
|
listen($("print-fields"), "change", changed);
|
|
4121
5146
|
listen($("print-clear"), "click", clear);
|
|
4122
5147
|
listen($("print-cancel"), "click", cancel);
|
|
5148
|
+
if ($("print-resume"))
|
|
5149
|
+
listen($("print-resume"), "click", () => {
|
|
5150
|
+
if (job || getState().busy) return;
|
|
5151
|
+
for (const id of pausedForCheck)
|
|
5152
|
+
if (getState().models.some((m) => m.id === id) && getViewer().get_model_animation_state(id)?.status === "paused")
|
|
5153
|
+
getViewer().resume_model_animation(id);
|
|
5154
|
+
pausedForCheck.clear();
|
|
5155
|
+
report = null;
|
|
5156
|
+
clear();
|
|
5157
|
+
$("print-results").replaceChildren();
|
|
5158
|
+
$("print-status").textContent = "Animation resumed. Pause and run the check again for the new pose.";
|
|
5159
|
+
sync();
|
|
5160
|
+
});
|
|
4123
5161
|
listen($("print-results"), "click", (e) => {
|
|
4124
5162
|
const b = e.target.closest("button[data-issue]");
|
|
4125
5163
|
if (!b || !report) return;
|
|
@@ -4163,6 +5201,13 @@ function bindAppPrint({
|
|
|
4163
5201
|
}
|
|
4164
5202
|
}
|
|
4165
5203
|
scopeKey = key();
|
|
5204
|
+
for (const id of stateIds()) {
|
|
5205
|
+
if (getViewer().get_model_animation_state(id)?.status === "running") {
|
|
5206
|
+
getViewer().pause_model_animation(id);
|
|
5207
|
+
pausedForCheck.add(id);
|
|
5208
|
+
}
|
|
5209
|
+
}
|
|
5210
|
+
sync();
|
|
4166
5211
|
const result = await getViewer().check_printability({
|
|
4167
5212
|
model_ids: stateIds(),
|
|
4168
5213
|
level: $("print-level").value,
|
|
@@ -4200,11 +5245,216 @@ function bindAppPrint({
|
|
|
4200
5245
|
disposed = true;
|
|
4201
5246
|
close(false);
|
|
4202
5247
|
report = null;
|
|
5248
|
+
pausedForCheck.clear();
|
|
5249
|
+
}
|
|
5250
|
+
};
|
|
5251
|
+
}
|
|
5252
|
+
|
|
5253
|
+
// src/app-animation.js
|
|
5254
|
+
var animationControls = [
|
|
5255
|
+
"animation-details",
|
|
5256
|
+
"animation-fields",
|
|
5257
|
+
"animation-axis",
|
|
5258
|
+
"animation-seconds",
|
|
5259
|
+
"animation-direction",
|
|
5260
|
+
"animation-play",
|
|
5261
|
+
"animation-pause",
|
|
5262
|
+
"animation-stop",
|
|
5263
|
+
"animation-status",
|
|
5264
|
+
"animation-error"
|
|
5265
|
+
];
|
|
5266
|
+
var axes4 = ["x", "y", "z"];
|
|
5267
|
+
function syncAnimatedRecord(record, model) {
|
|
5268
|
+
if (!record || !model?.geometry) return;
|
|
5269
|
+
record.bounds = Object.fromEntries(
|
|
5270
|
+
["min", "max"].map((key) => [
|
|
5271
|
+
key,
|
|
5272
|
+
model.geometry.bounds[key].map(
|
|
5273
|
+
(v, i) => (v - model.center[i]) * model.scaleVector[i]
|
|
5274
|
+
)
|
|
5275
|
+
])
|
|
5276
|
+
);
|
|
5277
|
+
record.rotation = Object.fromEntries(
|
|
5278
|
+
axes4.map((a) => [a, model["rotation" + a] * 180 / Math.PI % 360])
|
|
5279
|
+
);
|
|
5280
|
+
record.position = { x: 0, y: 0, z: 0 };
|
|
5281
|
+
const offset = enginePosition(record);
|
|
5282
|
+
for (const a of axes4) record.position[a] = model[a] - offset[a];
|
|
5283
|
+
}
|
|
5284
|
+
function centerAnimationPivot(record, model) {
|
|
5285
|
+
syncAnimatedRecord(record, model);
|
|
5286
|
+
const center = model.geometry.bounds.min.map(
|
|
5287
|
+
(v, i) => (v + model.geometry.bounds.max[i]) / 2
|
|
5288
|
+
);
|
|
5289
|
+
const delta = center.map(
|
|
5290
|
+
(v, i) => (v - model.center[i]) * model.scaleVector[i]
|
|
5291
|
+
);
|
|
5292
|
+
const rotation2 = rotationMatrix(record.rotation);
|
|
5293
|
+
const original = record.originalPosition || { x: 0, y: 0, z: 0 };
|
|
5294
|
+
record.originalPosition = Object.fromEntries(
|
|
5295
|
+
axes4.map((a, i) => [a, original[a] + delta[i]])
|
|
5296
|
+
);
|
|
5297
|
+
for (let i = 0; i < 3; i++)
|
|
5298
|
+
model[axes4[i]] += delta.reduce(
|
|
5299
|
+
(sum, v, j) => sum + rotation2[i * 3 + j] * v,
|
|
5300
|
+
0
|
|
5301
|
+
);
|
|
5302
|
+
model.center = center;
|
|
5303
|
+
record.animationPivot = true;
|
|
5304
|
+
syncAnimatedRecord(record, model);
|
|
5305
|
+
}
|
|
5306
|
+
function spinDefinition(axis, seconds, direction) {
|
|
5307
|
+
if (!axes4.includes(axis) || !Number.isFinite(seconds) || seconds < 0.1 || seconds > 3600 || !["1", "-1"].includes(direction))
|
|
5308
|
+
throw new Error("Choose an axis and a duration from 0.1 to 3,600 seconds.");
|
|
5309
|
+
return {
|
|
5310
|
+
delta: {
|
|
5311
|
+
["rotation" + axis]: 2 * Math.PI * Number(direction),
|
|
5312
|
+
msec: seconds * 1e3,
|
|
5313
|
+
loop: true
|
|
5314
|
+
}
|
|
5315
|
+
};
|
|
5316
|
+
}
|
|
5317
|
+
function bindAppAnimation({
|
|
5318
|
+
$,
|
|
5319
|
+
listen,
|
|
5320
|
+
window: window2,
|
|
5321
|
+
getState,
|
|
5322
|
+
getViewer,
|
|
5323
|
+
beforePlay,
|
|
5324
|
+
refreshPose,
|
|
5325
|
+
announce
|
|
5326
|
+
}) {
|
|
5327
|
+
const settings = /* @__PURE__ */ new Map();
|
|
5328
|
+
let selected = null, timer = null, disposed = false;
|
|
5329
|
+
const state = () => {
|
|
5330
|
+
const s = getState();
|
|
5331
|
+
return {
|
|
5332
|
+
...s,
|
|
5333
|
+
playback: s.selectedId == null ? null : getViewer()?.get_model_animation_state(s.selectedId)
|
|
5334
|
+
};
|
|
5335
|
+
};
|
|
5336
|
+
function sync() {
|
|
5337
|
+
if (disposed) return;
|
|
5338
|
+
const s = state(), available = s.models.some((m) => m.id === s.selectedId);
|
|
5339
|
+
for (const id of settings.keys())
|
|
5340
|
+
if (!s.models.some((m) => m.id === id)) settings.delete(id);
|
|
5341
|
+
$("animation-details").hidden = !available;
|
|
5342
|
+
$("animation-fields").disabled = s.busy || !available;
|
|
5343
|
+
if (selected !== s.selectedId) {
|
|
5344
|
+
selected = s.selectedId;
|
|
5345
|
+
let value = settings.get(selected);
|
|
5346
|
+
if (!value) {
|
|
5347
|
+
value = { axis: "z", seconds: 5, direction: "1" };
|
|
5348
|
+
const a = getViewer()?._models.get(selected)?.animation;
|
|
5349
|
+
const entries = Object.entries(a?.delta || {}).filter(
|
|
5350
|
+
([k, v]) => k.startsWith("rotation") && v !== 0
|
|
5351
|
+
);
|
|
5352
|
+
if (a?.delta?.loop && !a.exact && entries.length === 1) {
|
|
5353
|
+
value = {
|
|
5354
|
+
axis: entries[0][0].slice(-1),
|
|
5355
|
+
seconds: a.delta.msec * 2 * Math.PI / Math.abs(entries[0][1]) / 1e3,
|
|
5356
|
+
direction: entries[0][1] < 0 ? "-1" : "1"
|
|
5357
|
+
};
|
|
5358
|
+
}
|
|
5359
|
+
settings.set(selected, value);
|
|
5360
|
+
}
|
|
5361
|
+
$("animation-axis").value = value.axis;
|
|
5362
|
+
$("animation-seconds").value = value.seconds;
|
|
5363
|
+
$("animation-direction").value = value.direction;
|
|
5364
|
+
$("animation-error").hidden = true;
|
|
5365
|
+
}
|
|
5366
|
+
const status = s.playback?.status || "idle", active = ["running", "paused"].includes(status);
|
|
5367
|
+
$("animation-play").textContent = active ? "Restart spin" : "Play";
|
|
5368
|
+
$("animation-pause").textContent = status === "paused" ? "Resume" : "Pause";
|
|
5369
|
+
$("animation-pause").disabled = !active;
|
|
5370
|
+
$("animation-stop").disabled = !active;
|
|
5371
|
+
const label = {
|
|
5372
|
+
idle: "Ready",
|
|
5373
|
+
running: "Playing",
|
|
5374
|
+
paused: "Paused",
|
|
5375
|
+
stopped: "Stopped",
|
|
5376
|
+
completed: "Finished"
|
|
5377
|
+
}[status];
|
|
5378
|
+
const text = `${label}. ${active ? "Stop keeps the current pose." : "Spin the selected model around its center."}`;
|
|
5379
|
+
if ($("animation-status").textContent !== text)
|
|
5380
|
+
$("animation-status").textContent = text;
|
|
5381
|
+
if (status === "running" && !timer)
|
|
5382
|
+
timer = window2.setInterval(() => {
|
|
5383
|
+
if (!disposed && !window2.document.hidden) refreshPose();
|
|
5384
|
+
}, 250);
|
|
5385
|
+
if (status !== "running" && timer) {
|
|
5386
|
+
window2.clearInterval(timer);
|
|
5387
|
+
timer = null;
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
function play(keepPaused = false) {
|
|
5391
|
+
const s = state();
|
|
5392
|
+
if (s.busy || s.selectedId == null) return;
|
|
5393
|
+
try {
|
|
5394
|
+
const value = {
|
|
5395
|
+
axis: $("animation-axis").value,
|
|
5396
|
+
seconds: $("animation-seconds").valueAsNumber,
|
|
5397
|
+
direction: $("animation-direction").value
|
|
5398
|
+
};
|
|
5399
|
+
const definition = spinDefinition(
|
|
5400
|
+
value.axis,
|
|
5401
|
+
value.seconds,
|
|
5402
|
+
value.direction
|
|
5403
|
+
);
|
|
5404
|
+
beforePlay(s.selectedId);
|
|
5405
|
+
getViewer().animate_model(s.selectedId, definition);
|
|
5406
|
+
if (keepPaused && s.playback?.status === "paused")
|
|
5407
|
+
getViewer().pause_model_animation(s.selectedId);
|
|
5408
|
+
settings.set(s.selectedId, value);
|
|
5409
|
+
$("animation-error").hidden = true;
|
|
5410
|
+
announce(
|
|
5411
|
+
keepPaused && s.playback?.status === "paused" ? "Spin updated. Still paused." : "Animation playing."
|
|
5412
|
+
);
|
|
5413
|
+
sync();
|
|
5414
|
+
} catch (error) {
|
|
5415
|
+
$("animation-error").textContent = error.message;
|
|
5416
|
+
$("animation-error").hidden = false;
|
|
5417
|
+
}
|
|
5418
|
+
}
|
|
5419
|
+
listen($("animation-play"), "click", () => play());
|
|
5420
|
+
listen($("animation-pause"), "click", () => {
|
|
5421
|
+
const s = state();
|
|
5422
|
+
if (s.busy || s.selectedId == null) return;
|
|
5423
|
+
getViewer()[s.playback?.status === "paused" ? "resume_model_animation" : "pause_model_animation"](s.selectedId);
|
|
5424
|
+
sync();
|
|
5425
|
+
});
|
|
5426
|
+
listen($("animation-stop"), "click", () => {
|
|
5427
|
+
const s = state();
|
|
5428
|
+
if (!s.busy && s.selectedId != null)
|
|
5429
|
+
getViewer().animate_model(s.selectedId, null);
|
|
5430
|
+
sync();
|
|
5431
|
+
});
|
|
5432
|
+
for (const id of [
|
|
5433
|
+
"animation-axis",
|
|
5434
|
+
"animation-seconds",
|
|
5435
|
+
"animation-direction"
|
|
5436
|
+
])
|
|
5437
|
+
listen($(id), "change", () => {
|
|
5438
|
+
if (["running", "paused"].includes(state().playback?.status)) play(true);
|
|
5439
|
+
else
|
|
5440
|
+
settings.set(selected, {
|
|
5441
|
+
axis: $("animation-axis").value,
|
|
5442
|
+
seconds: $("animation-seconds").valueAsNumber,
|
|
5443
|
+
direction: $("animation-direction").value
|
|
5444
|
+
});
|
|
5445
|
+
});
|
|
5446
|
+
sync();
|
|
5447
|
+
return {
|
|
5448
|
+
sync,
|
|
5449
|
+
dispose() {
|
|
5450
|
+
disposed = true;
|
|
5451
|
+
window2.clearInterval(timer);
|
|
5452
|
+
settings.clear();
|
|
4203
5453
|
}
|
|
4204
5454
|
};
|
|
4205
5455
|
}
|
|
4206
5456
|
|
|
4207
|
-
// src/
|
|
5457
|
+
// src/app.js
|
|
4208
5458
|
var mountedRoots = /* @__PURE__ */ new WeakMap();
|
|
4209
5459
|
var requiredControls = [
|
|
4210
5460
|
"background-color",
|
|
@@ -4309,6 +5559,10 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4309
5559
|
missing.push(...exportControls.filter((id) => !$(id)));
|
|
4310
5560
|
if (printControls.some((id) => $(id)))
|
|
4311
5561
|
missing.push(...printControls.filter((id) => !$(id)));
|
|
5562
|
+
if (animationControls.some((id) => $(id)))
|
|
5563
|
+
missing.push(...animationControls.filter((id) => !$(id)));
|
|
5564
|
+
if (toolsControls.some((id) => $(id)))
|
|
5565
|
+
missing.push(...toolsControls.filter((id) => !$(id)));
|
|
4312
5566
|
if (missing.length)
|
|
4313
5567
|
throw new Error("Missing ViewSTL controls: " + missing.join(", "));
|
|
4314
5568
|
const abort = new window2.AbortController();
|
|
@@ -4336,6 +5590,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4336
5590
|
let viewerPromise, viewer, generation = 0, appearanceRequest = 0, modelID = null, nextID = 0, loadingID = null;
|
|
4337
5591
|
let exportUI;
|
|
4338
5592
|
let printUI;
|
|
5593
|
+
let animationUI;
|
|
5594
|
+
let toolsUI;
|
|
4339
5595
|
let sceneImport;
|
|
4340
5596
|
let draining = false, busy = false, gridEnabled = false, gridHeight = 0, gridSpacing = 1, moveMode = false, transformTool = "move", modelMover, dragArrangement, background = "transparent";
|
|
4341
5597
|
const viewNames = {
|
|
@@ -4384,6 +5640,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4384
5640
|
$(name + "-panel").hidden = !open;
|
|
4385
5641
|
$(name + "-button").setAttribute("aria-expanded", String(open));
|
|
4386
5642
|
syncSelection();
|
|
5643
|
+
if (open && name === "info") populateInfo();
|
|
5644
|
+
if (open && (name === "scene" || name === "view")) toolsUI?.sync();
|
|
4387
5645
|
}
|
|
4388
5646
|
function fileSize(size) {
|
|
4389
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`;
|
|
@@ -4409,12 +5667,14 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4409
5667
|
$("background-value").textContent = background === "transparent" ? "Default" : background.toUpperCase();
|
|
4410
5668
|
}
|
|
4411
5669
|
function syncControls() {
|
|
4412
|
-
|
|
5670
|
+
syncAnimatedRecord(records.get(modelID), viewer?._models.get(modelID));
|
|
5671
|
+
const info = modelID === null ? null : viewer.get_model_info(modelID, { measurements: false }), mode = info?.display === "smooth" ? "flat" : info?.display || "flat";
|
|
4413
5672
|
for (const input of modeInputs) {
|
|
4414
5673
|
input.checked = input.value === mode;
|
|
4415
5674
|
input.disabled = busy;
|
|
4416
5675
|
}
|
|
4417
|
-
$("view-label").textContent = viewNames[mode];
|
|
5676
|
+
$("view-label").textContent = toolsUI ? "View" : viewNames[mode];
|
|
5677
|
+
$("view-button").title = "View \xB7 " + viewNames[mode];
|
|
4418
5678
|
$("smooth-shading").checked = info?.smooth_shading || false;
|
|
4419
5679
|
$("smooth-shading").disabled = busy || mode === "wireframe";
|
|
4420
5680
|
$("view-status").textContent = mode === "wireframe" ? "Smooth shading is available in surface views." : "Shading changes the appearance, not the geometry.";
|
|
@@ -4437,14 +5697,18 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4437
5697
|
syncPosition();
|
|
4438
5698
|
exportUI?.sync();
|
|
4439
5699
|
printUI?.sync();
|
|
5700
|
+
animationUI?.sync();
|
|
5701
|
+
toolsUI?.sync();
|
|
4440
5702
|
}
|
|
4441
5703
|
function populateInfo() {
|
|
4442
5704
|
if (modelID === null) return;
|
|
4443
|
-
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;
|
|
4444
5706
|
$("info-triangles").textContent = info.triangles.toLocaleString();
|
|
4445
|
-
$("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");
|
|
4446
5708
|
$("info-size").textContent = fileSize(record.size);
|
|
4447
|
-
|
|
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();
|
|
4448
5712
|
}
|
|
4449
5713
|
function renderList() {
|
|
4450
5714
|
$("model-list").replaceChildren(
|
|
@@ -4516,6 +5780,10 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4516
5780
|
}
|
|
4517
5781
|
function arrangeScene() {
|
|
4518
5782
|
setMoveMode(false);
|
|
5783
|
+
for (const record of records.values())
|
|
5784
|
+
syncAnimatedRecord(record, viewer?._models.get(record.id));
|
|
5785
|
+
if ($("scene-arrangement").value === "custom") return;
|
|
5786
|
+
for (const record of records.values()) prepareManual(record.id);
|
|
4519
5787
|
if ($("scene-arrangement").value === "original")
|
|
4520
5788
|
for (const record of records.values())
|
|
4521
5789
|
record.rotation = {
|
|
@@ -4532,19 +5800,20 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4532
5800
|
updateGrid();
|
|
4533
5801
|
viewer?.set_auto_zoom();
|
|
4534
5802
|
}
|
|
4535
|
-
function syncPosition() {
|
|
5803
|
+
function syncPosition(preserveFocus = false) {
|
|
4536
5804
|
const record = records.get(modelID);
|
|
4537
5805
|
for (const axis of ["x", "y", "z"]) {
|
|
4538
5806
|
const input = $("position-" + axis);
|
|
4539
|
-
|
|
5807
|
+
if (!preserveFocus || document2.activeElement !== input)
|
|
5808
|
+
input.value = Number((record?.position?.[axis] || 0).toPrecision(12));
|
|
4540
5809
|
input.disabled = !record;
|
|
4541
5810
|
input.removeAttribute("aria-invalid");
|
|
4542
5811
|
}
|
|
4543
5812
|
$("position-error").hidden = true;
|
|
4544
|
-
$("position-units").textContent = record?.
|
|
5813
|
+
$("position-units").textContent = record?.animationPivot ? "Position of the model center in the scene, in mm." : "Offset from file coordinates, in mm.";
|
|
4545
5814
|
$("move-model-button").disabled = !record || busy;
|
|
4546
5815
|
$("reset-position").disabled = !record;
|
|
4547
|
-
syncRotation();
|
|
5816
|
+
syncRotation(preserveFocus);
|
|
4548
5817
|
for (const id of [
|
|
4549
5818
|
"rotate-model-button",
|
|
4550
5819
|
"depth-model-button",
|
|
@@ -4573,14 +5842,16 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4573
5842
|
viewer.set_position(record.id, p.x, p.y, p.z);
|
|
4574
5843
|
}
|
|
4575
5844
|
function selectedTransform() {
|
|
5845
|
+
prepareManual(modelID);
|
|
4576
5846
|
const record = records.get(modelID);
|
|
4577
5847
|
return record ? { ...record, center: modelCenter(record), radius: modelRadius(record) } : null;
|
|
4578
5848
|
}
|
|
4579
|
-
function syncRotation() {
|
|
5849
|
+
function syncRotation(preserveFocus = false) {
|
|
4580
5850
|
const record = records.get(modelID);
|
|
4581
5851
|
for (const axis of ["x", "y", "z"]) {
|
|
4582
5852
|
const input = $("rotation-" + axis);
|
|
4583
|
-
|
|
5853
|
+
if (!preserveFocus || document2.activeElement !== input)
|
|
5854
|
+
input.value = Number((record?.rotation?.[axis] || 0).toPrecision(10));
|
|
4584
5855
|
input.disabled = !record;
|
|
4585
5856
|
input.removeAttribute("aria-invalid");
|
|
4586
5857
|
}
|
|
@@ -4622,10 +5893,11 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4622
5893
|
$("model-tool").value = transformTool;
|
|
4623
5894
|
$("move-gesture-message").textContent = toolHints[transformTool] + " \xB7 Scroll to adjust depth";
|
|
4624
5895
|
}
|
|
4625
|
-
function setMoveMode(enabled, tool = transformTool) {
|
|
5896
|
+
function setMoveMode(enabled, tool = transformTool, retainPaused = false) {
|
|
4626
5897
|
modelMover?.setEnabled(false);
|
|
4627
5898
|
transformTool = tool;
|
|
4628
5899
|
moveMode = !!enabled && modelID !== null && !busy;
|
|
5900
|
+
if (moveMode && !retainPaused) prepareManual(modelID);
|
|
4629
5901
|
modelMover?.setEnabled(moveMode);
|
|
4630
5902
|
syncTool();
|
|
4631
5903
|
$("move-mode").hidden = !moveMode;
|
|
@@ -4649,6 +5921,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4649
5921
|
const input = $("rotation-" + axis);
|
|
4650
5922
|
listen(input, "input", () => {
|
|
4651
5923
|
if (modelID === null || !input.value.trim()) return;
|
|
5924
|
+
prepareManual(modelID);
|
|
4652
5925
|
if (applyRotation(
|
|
4653
5926
|
modelID,
|
|
4654
5927
|
{ ...records.get(modelID).rotation, [axis]: input.valueAsNumber },
|
|
@@ -4660,6 +5933,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4660
5933
|
});
|
|
4661
5934
|
listen(input, "change", () => {
|
|
4662
5935
|
if (modelID === null) return;
|
|
5936
|
+
prepareManual(modelID);
|
|
4663
5937
|
if (!input.value.trim() || !applyRotation(modelID, {
|
|
4664
5938
|
...records.get(modelID).rotation,
|
|
4665
5939
|
[axis]: input.valueAsNumber
|
|
@@ -4672,6 +5946,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4672
5946
|
listen($("reset-rotation"), "click", () => {
|
|
4673
5947
|
setMoveMode(false);
|
|
4674
5948
|
if (modelID !== null) {
|
|
5949
|
+
prepareManual(modelID);
|
|
4675
5950
|
applyRotation(modelID, {
|
|
4676
5951
|
...records.get(modelID).originalRotation || { x: 0, y: 0, z: 0 }
|
|
4677
5952
|
});
|
|
@@ -4711,6 +5986,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4711
5986
|
const input = $("position-" + axis);
|
|
4712
5987
|
listen(input, "input", () => {
|
|
4713
5988
|
if (modelID === null || !input.value.trim()) return;
|
|
5989
|
+
prepareManual(modelID);
|
|
4714
5990
|
if (applyPosition(
|
|
4715
5991
|
modelID,
|
|
4716
5992
|
{ ...records.get(modelID).position, [axis]: input.valueAsNumber },
|
|
@@ -4722,6 +5998,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4722
5998
|
});
|
|
4723
5999
|
listen(input, "change", () => {
|
|
4724
6000
|
if (modelID === null) return;
|
|
6001
|
+
prepareManual(modelID);
|
|
4725
6002
|
if (!input.value.trim() || !applyPosition(modelID, {
|
|
4726
6003
|
...records.get(modelID).position,
|
|
4727
6004
|
[axis]: input.valueAsNumber
|
|
@@ -4738,6 +6015,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4738
6015
|
listen($("reset-position"), "click", () => {
|
|
4739
6016
|
setMoveMode(false);
|
|
4740
6017
|
if (modelID !== null) {
|
|
6018
|
+
prepareManual(modelID);
|
|
4741
6019
|
applyPosition(modelID, {
|
|
4742
6020
|
...records.get(modelID).originalPosition || { x: 0, y: 0, z: 0 }
|
|
4743
6021
|
});
|
|
@@ -4788,6 +6066,30 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4788
6066
|
renderScene();
|
|
4789
6067
|
announce(name + " removed.");
|
|
4790
6068
|
}
|
|
6069
|
+
function prepareManual(id) {
|
|
6070
|
+
if (!records.has(id)) return;
|
|
6071
|
+
syncAnimatedRecord(records.get(id), viewer?._models.get(id));
|
|
6072
|
+
viewer?.animate_model(id, null);
|
|
6073
|
+
}
|
|
6074
|
+
function refreshAnimatedPose() {
|
|
6075
|
+
if (disposed || modelID === null) return;
|
|
6076
|
+
syncAnimatedRecord(records.get(modelID), viewer?._models.get(modelID));
|
|
6077
|
+
if (!$("info-panel").hidden) populateInfo();
|
|
6078
|
+
if (!$("scene-panel").hidden && $("position-details").open)
|
|
6079
|
+
syncPosition(true);
|
|
6080
|
+
}
|
|
6081
|
+
function animationChanged(id, state, reason) {
|
|
6082
|
+
if (disposed) return;
|
|
6083
|
+
if (reason === "restored" && state.status === "running" && window2.matchMedia?.("(prefers-reduced-motion: reduce)").matches)
|
|
6084
|
+
viewer.pause_model_animation(id);
|
|
6085
|
+
syncAnimatedRecord(records.get(id), viewer?._models.get(id));
|
|
6086
|
+
if (["started", "restored", "resumed"].includes(reason))
|
|
6087
|
+
$("scene-arrangement").value = "custom";
|
|
6088
|
+
if (id === modelID) refreshAnimatedPose();
|
|
6089
|
+
animationUI?.sync();
|
|
6090
|
+
printUI?.sync();
|
|
6091
|
+
options.animation_state_callback?.(id, state, reason);
|
|
6092
|
+
}
|
|
4791
6093
|
async function getViewer() {
|
|
4792
6094
|
if (!viewerPromise)
|
|
4793
6095
|
viewerPromise = Promise.resolve().then(async () => {
|
|
@@ -4802,6 +6104,10 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4802
6104
|
center_models: false,
|
|
4803
6105
|
bg_color: background,
|
|
4804
6106
|
allow_drag_and_drop: false,
|
|
6107
|
+
animation_state_callback: animationChanged,
|
|
6108
|
+
animation_complete_callback: (...args) => {
|
|
6109
|
+
if (!disposed) options.animation_complete_callback?.(...args);
|
|
6110
|
+
},
|
|
4805
6111
|
camera_state: {
|
|
4806
6112
|
position: { x: 90, y: -130, z: 90 },
|
|
4807
6113
|
target: { x: 0, y: 0, z: 0 },
|
|
@@ -4893,7 +6199,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4893
6199
|
if (queue.length && !draining) drainPromise = drainQueue();
|
|
4894
6200
|
return drainPromise || Promise.resolve();
|
|
4895
6201
|
}
|
|
4896
|
-
function openURL(value,
|
|
6202
|
+
function openURL(value, format2 = "") {
|
|
4897
6203
|
assertActive();
|
|
4898
6204
|
if (!options.url_import_endpoint)
|
|
4899
6205
|
throw new Error("URL imports are not configured.");
|
|
@@ -4902,7 +6208,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4902
6208
|
throw new Error(
|
|
4903
6209
|
"Use a public HTTP or HTTPS file URL without login credentials."
|
|
4904
6210
|
);
|
|
4905
|
-
if (
|
|
6211
|
+
if (format2 && !/^(stl|obj|3mf|step|stp|iges|igs|vsb|vsj)$/.test(format2))
|
|
4906
6212
|
throw new Error("Choose a supported 3D format.");
|
|
4907
6213
|
url.hash = "";
|
|
4908
6214
|
const name = decodeURIComponent(
|
|
@@ -4911,9 +6217,9 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4911
6217
|
queue.push({
|
|
4912
6218
|
name,
|
|
4913
6219
|
remote: url.href,
|
|
4914
|
-
format,
|
|
6220
|
+
format: format2,
|
|
4915
6221
|
size: 0,
|
|
4916
|
-
scene: ["vsb", "vsj"].includes(
|
|
6222
|
+
scene: ["vsb", "vsj"].includes(format2) ? format2 : sceneFormat(name)
|
|
4917
6223
|
});
|
|
4918
6224
|
if (!draining) drainPromise = drainQueue();
|
|
4919
6225
|
return drainPromise || Promise.resolve();
|
|
@@ -4982,7 +6288,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4982
6288
|
await engine.when_idle();
|
|
4983
6289
|
if (token !== generation) continue;
|
|
4984
6290
|
loadingID = null;
|
|
4985
|
-
const loaded = lastLoadedID === id ? engine.get_model_info(id) : null;
|
|
6291
|
+
const loaded = lastLoadedID === id ? engine.get_model_info(id, { measurements: false }) : null;
|
|
4986
6292
|
if (!loaded) {
|
|
4987
6293
|
announce(source.name + " could not be opened.");
|
|
4988
6294
|
continue;
|
|
@@ -5074,7 +6380,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5074
6380
|
);
|
|
5075
6381
|
const record = {
|
|
5076
6382
|
id,
|
|
5077
|
-
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],
|
|
5078
6384
|
size: g.file_bytes ?? m.local_file?.size ?? null,
|
|
5079
6385
|
units: g.units || m.units,
|
|
5080
6386
|
bounds,
|
|
@@ -5383,6 +6689,64 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5383
6689
|
},
|
|
5384
6690
|
announce
|
|
5385
6691
|
});
|
|
6692
|
+
if ($("animation-details"))
|
|
6693
|
+
animationUI = bindAppAnimation({
|
|
6694
|
+
$,
|
|
6695
|
+
listen,
|
|
6696
|
+
window: window2,
|
|
6697
|
+
getState: () => ({
|
|
6698
|
+
busy,
|
|
6699
|
+
selectedId: modelID,
|
|
6700
|
+
models: [...records.values()]
|
|
6701
|
+
}),
|
|
6702
|
+
getViewer: () => viewer,
|
|
6703
|
+
beforePlay(id) {
|
|
6704
|
+
setMoveMode(false);
|
|
6705
|
+
prepareManual(id);
|
|
6706
|
+
centerAnimationPivot(records.get(id), viewer._models.get(id));
|
|
6707
|
+
$("scene-arrangement").value = "custom";
|
|
6708
|
+
},
|
|
6709
|
+
refreshPose: refreshAnimatedPose,
|
|
6710
|
+
announce
|
|
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
|
+
});
|
|
5386
6750
|
updateGrid();
|
|
5387
6751
|
setBusy(false);
|
|
5388
6752
|
setMoveMode(false);
|
|
@@ -5407,6 +6771,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5407
6771
|
},
|
|
5408
6772
|
getState() {
|
|
5409
6773
|
assertActive();
|
|
6774
|
+
for (const record of records.values())
|
|
6775
|
+
syncAnimatedRecord(record, viewer?._models.get(record.id));
|
|
5410
6776
|
return {
|
|
5411
6777
|
selectedId: modelID,
|
|
5412
6778
|
busy,
|
|
@@ -5419,7 +6785,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5419
6785
|
units: record.units,
|
|
5420
6786
|
position: { ...record.position },
|
|
5421
6787
|
rotation: { ...record.rotation },
|
|
5422
|
-
color: record.color
|
|
6788
|
+
color: record.color,
|
|
6789
|
+
animation: viewer.get_model_animation_state(record.id)
|
|
5423
6790
|
}))
|
|
5424
6791
|
};
|
|
5425
6792
|
},
|
|
@@ -5427,6 +6794,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5427
6794
|
if (disposed) return;
|
|
5428
6795
|
exportUI?.dispose();
|
|
5429
6796
|
printUI?.dispose();
|
|
6797
|
+
animationUI?.dispose();
|
|
6798
|
+
toolsUI?.dispose();
|
|
5430
6799
|
cancelLoading();
|
|
5431
6800
|
setMoveMode(false);
|
|
5432
6801
|
appearanceRequest++;
|