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