@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.js
CHANGED
|
@@ -1,50 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
var DISPLAYS = [
|
|
3
|
-
"flat",
|
|
4
|
-
"smooth",
|
|
5
|
-
"wireframe",
|
|
6
|
-
"edges",
|
|
7
|
-
"mesh",
|
|
8
|
-
"xray",
|
|
9
|
-
"studio",
|
|
10
|
-
"orientation"
|
|
11
|
-
];
|
|
12
|
-
function validateView(display, smooth) {
|
|
13
|
-
if (!DISPLAYS.includes(display)) throw new Error("Invalid display mode");
|
|
14
|
-
if (typeof smooth !== "boolean")
|
|
15
|
-
throw new Error("smooth_shading must be a boolean");
|
|
16
|
-
return { display, smooth_shading: display === "smooth" || smooth };
|
|
17
|
-
}
|
|
18
|
-
function viewFeatures(view) {
|
|
19
|
-
return {
|
|
20
|
-
smooth: (view.smooth_shading || view.display === "smooth") && view.display !== "wireframe",
|
|
21
|
-
edges: view.view_edges || view.display === "edges",
|
|
22
|
-
wire: view.display === "wireframe",
|
|
23
|
-
edgeAngle: view.display === "edges" ? 30 : 1
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
function validateSection(section) {
|
|
27
|
-
if (section == null) return null;
|
|
28
|
-
if (!["x", "y", "z"].includes(section.axis))
|
|
29
|
-
throw new Error("Section axis must be x, y or z");
|
|
30
|
-
if (!Number.isFinite(section.position) || section.position < 0 || section.position > 1)
|
|
31
|
-
throw new Error("Section position must be between 0 and 1");
|
|
32
|
-
if (section.flip !== void 0 && typeof section.flip !== "boolean")
|
|
33
|
-
throw new Error("Section flip must be a boolean");
|
|
34
|
-
return {
|
|
35
|
-
axis: section.axis,
|
|
36
|
-
position: section.position,
|
|
37
|
-
flip: section.flip ?? false
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
function sectionPlane(bounds, section) {
|
|
41
|
-
if (!section) return [0, 0, 0, 0];
|
|
42
|
-
const axis = ["x", "y", "z"].indexOf(section.axis), sign = section.flip ? -1 : 1;
|
|
43
|
-
const plane = [0, 0, 0, 0];
|
|
44
|
-
plane[axis] = sign;
|
|
45
|
-
plane[3] = -sign * (bounds.min[axis] + (bounds.max[axis] - bounds.min[axis]) * section.position);
|
|
46
|
-
return plane;
|
|
47
|
-
}
|
|
1
|
+
//=========== Stl Viewer v2.0.0-rc.7, by Omri Rips, Viewstl.com, November 2026 ; admin@viewstl.com ===========
|
|
48
2
|
|
|
49
3
|
// src/math.js
|
|
50
4
|
var add = (a, b) => a.map((v, i) => v + b[i]);
|
|
@@ -197,22 +151,606 @@ function rayTriangle(o, d, p, i) {
|
|
|
197
151
|
const t = dot(c, q) / det;
|
|
198
152
|
return t >= 0 ? t : Infinity;
|
|
199
153
|
}
|
|
200
|
-
|
|
201
|
-
// src/
|
|
202
|
-
var
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
154
|
+
|
|
155
|
+
// src/animation.js
|
|
156
|
+
var animationKeys = [
|
|
157
|
+
"x",
|
|
158
|
+
"y",
|
|
159
|
+
"z",
|
|
160
|
+
"rotationx",
|
|
161
|
+
"rotationy",
|
|
162
|
+
"rotationz",
|
|
163
|
+
"scalex",
|
|
164
|
+
"scaley",
|
|
165
|
+
"scalez"
|
|
166
|
+
];
|
|
167
|
+
var isScale = (key) => key.startsWith("scale");
|
|
168
|
+
var number = (value, name, positive2 = false) => {
|
|
169
|
+
if (typeof value !== "number" || !Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER || positive2 && value <= 0)
|
|
170
|
+
throw new Error(`Invalid animation ${name}`);
|
|
171
|
+
return value;
|
|
172
|
+
};
|
|
173
|
+
var object = (value, name) => {
|
|
174
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
175
|
+
throw new Error(`Invalid animation ${name}`);
|
|
176
|
+
return value;
|
|
177
|
+
};
|
|
178
|
+
function animationPose(model) {
|
|
179
|
+
return Object.fromEntries(
|
|
180
|
+
animationKeys.map((key) => [
|
|
181
|
+
key,
|
|
182
|
+
number(
|
|
183
|
+
model[key] ?? (isScale(key) ? model.scale ?? 1 : 0),
|
|
184
|
+
key,
|
|
185
|
+
isScale(key)
|
|
186
|
+
)
|
|
187
|
+
])
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
function normalizeAnimation(input) {
|
|
191
|
+
object(input, "definition");
|
|
192
|
+
const result = {}, claimed = /* @__PURE__ */ new Set();
|
|
193
|
+
for (const mode of ["delta", "exact"]) {
|
|
194
|
+
if (input[mode] == null) continue;
|
|
195
|
+
const source = object(input[mode], mode), values = {};
|
|
196
|
+
const msec = number(source.msec ?? 300, "duration", true);
|
|
197
|
+
if (source.loop !== void 0 && typeof source.loop !== "boolean")
|
|
198
|
+
throw new Error("Animation loop must be a boolean");
|
|
199
|
+
const loop = mode === "delta" && !!source.loop;
|
|
200
|
+
for (const key of animationKeys) {
|
|
201
|
+
const value = source[key] ?? (isScale(key) ? source.scale : void 0);
|
|
202
|
+
if (value === void 0) continue;
|
|
203
|
+
if (claimed.has(key))
|
|
204
|
+
throw new Error(`Animation delta/exact conflict: ${key}`);
|
|
205
|
+
number(value, key, mode === "exact" && isScale(key));
|
|
206
|
+
if (loop && isScale(key) && value < 0)
|
|
207
|
+
throw new Error("Looping scale deltas cannot be negative");
|
|
208
|
+
values[key] = value;
|
|
209
|
+
claimed.add(key);
|
|
210
|
+
}
|
|
211
|
+
if (!Object.keys(values).length)
|
|
212
|
+
throw new Error("Animation track needs a transform");
|
|
213
|
+
result[mode] = { ...values, msec, ...loop ? { loop: true } : {} };
|
|
214
|
+
}
|
|
215
|
+
if (!Object.keys(result).length)
|
|
216
|
+
throw new Error("Animation needs delta or exact");
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
function normalizePlaybackState(input) {
|
|
220
|
+
object(input, "playback state");
|
|
221
|
+
if (input.version !== 1 || typeof input.paused !== "boolean")
|
|
222
|
+
throw new Error("Invalid animation playback state version or paused flag");
|
|
223
|
+
const elapsed_ms = number(input.elapsed_ms, "elapsed time");
|
|
224
|
+
if (elapsed_ms < 0) throw new Error("Invalid animation elapsed time");
|
|
225
|
+
object(input.origin, "origin");
|
|
226
|
+
for (const key of animationKeys) number(input.origin[key], key, isScale(key));
|
|
227
|
+
if (!Array.isArray(input.center) || input.center.length !== 3)
|
|
228
|
+
throw new Error("Invalid animation pivot");
|
|
229
|
+
return {
|
|
230
|
+
version: 1,
|
|
231
|
+
elapsed_ms,
|
|
232
|
+
paused: input.paused,
|
|
233
|
+
origin: animationPose(input.origin),
|
|
234
|
+
center: input.center.map((v) => number(v, "pivot"))
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function createPlayback(definition, model, saved) {
|
|
238
|
+
const animation = normalizeAnimation(definition);
|
|
239
|
+
const state = saved == null ? null : normalizePlaybackState(saved);
|
|
240
|
+
const origin = state?.origin ?? animationPose(model);
|
|
241
|
+
const tracks = Object.entries(animation).map(([mode, a]) => {
|
|
242
|
+
const values = Object.entries(a).filter(
|
|
243
|
+
([key]) => animationKeys.includes(key)
|
|
244
|
+
);
|
|
245
|
+
if (mode === "delta")
|
|
246
|
+
for (const [key, value] of values)
|
|
247
|
+
number(origin[key] + value, `${key} endpoint`, isScale(key));
|
|
248
|
+
return { mode, values, duration: a.msec, loop: !!a.loop, done: false };
|
|
249
|
+
});
|
|
250
|
+
const playback = {
|
|
251
|
+
animation,
|
|
252
|
+
origin,
|
|
253
|
+
tracks,
|
|
254
|
+
elapsed_ms: state?.elapsed_ms ?? 0,
|
|
255
|
+
paused: state?.paused ?? false,
|
|
256
|
+
last_time: null
|
|
257
|
+
};
|
|
258
|
+
evaluatePlayback({ ...playback, tracks: tracks.map((t) => ({ ...t })) }, {});
|
|
259
|
+
return playback;
|
|
260
|
+
}
|
|
261
|
+
function evaluatePlayback(playback, model) {
|
|
262
|
+
const updates = [];
|
|
263
|
+
for (const track of playback.tracks) {
|
|
264
|
+
if (track.done) continue;
|
|
265
|
+
const fraction = playback.elapsed_ms / track.duration;
|
|
266
|
+
const p = track.loop ? fraction : Math.min(fraction, 1);
|
|
267
|
+
for (const [key, value] of track.values) {
|
|
268
|
+
const from = playback.origin[key];
|
|
269
|
+
const to = track.mode === "delta" ? from + value * p : from * (1 - p) + value * p;
|
|
270
|
+
updates.push([key, number(to, `${key} result`, isScale(key))]);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
let scaleChanged = false;
|
|
274
|
+
for (const [key, value] of updates) {
|
|
275
|
+
scaleChanged ||= isScale(key) && model[key] !== value;
|
|
276
|
+
model[key] = value;
|
|
277
|
+
}
|
|
278
|
+
for (const track of playback.tracks)
|
|
279
|
+
if (!track.loop && playback.elapsed_ms >= track.duration) track.done = true;
|
|
280
|
+
if (scaleChanged) model._stats = null;
|
|
281
|
+
return playback.tracks.some((track) => !track.done);
|
|
282
|
+
}
|
|
283
|
+
function advancePlayback(playback, model, time) {
|
|
284
|
+
if (playback.paused) return true;
|
|
285
|
+
if (playback.last_time !== null)
|
|
286
|
+
playback.elapsed_ms = number(
|
|
287
|
+
playback.elapsed_ms + Math.max(0, time - playback.last_time),
|
|
288
|
+
"elapsed time"
|
|
289
|
+
);
|
|
290
|
+
playback.last_time = time;
|
|
291
|
+
return evaluatePlayback(playback, model);
|
|
292
|
+
}
|
|
293
|
+
function playbackState(playback, suspended = false) {
|
|
294
|
+
return {
|
|
295
|
+
status: playback.paused ? "paused" : "running",
|
|
296
|
+
elapsed_ms: playback.elapsed_ms,
|
|
297
|
+
suspended,
|
|
298
|
+
tracks: playback.tracks.filter((t) => !t.done).map((t) => ({
|
|
299
|
+
mode: t.mode,
|
|
300
|
+
msec: t.duration,
|
|
301
|
+
loop: t.loop,
|
|
302
|
+
progress: t.loop ? playback.elapsed_ms % t.duration / t.duration : Math.min(1, playback.elapsed_ms / t.duration)
|
|
303
|
+
}))
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function savePlayback(model) {
|
|
307
|
+
const p = model._animation;
|
|
308
|
+
if (!p) return null;
|
|
309
|
+
const factor = model.unitScale ?? 1;
|
|
310
|
+
const animation = structuredClone(p.animation), origin = { ...p.origin };
|
|
311
|
+
for (const key of animationKeys.filter(isScale)) {
|
|
312
|
+
origin[key] *= factor;
|
|
313
|
+
for (const track of Object.values(animation))
|
|
314
|
+
if (track[key] !== void 0) track[key] *= factor;
|
|
315
|
+
}
|
|
316
|
+
const animation_state = normalizePlaybackState({
|
|
317
|
+
version: 1,
|
|
318
|
+
origin,
|
|
319
|
+
elapsed_ms: p.elapsed_ms,
|
|
320
|
+
paused: p.paused,
|
|
321
|
+
center: [...model.center]
|
|
322
|
+
});
|
|
323
|
+
return { animation: normalizeAnimation(animation), animation_state };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/memory.js
|
|
327
|
+
var MEMORY_OVERHEAD = 64 * 1024 * 1024;
|
|
328
|
+
var TRIANGLE_MEMORY = 224;
|
|
329
|
+
function geometryMemory(g) {
|
|
330
|
+
if (!g) return 0;
|
|
331
|
+
return g.triangles * TRIANGLE_MEMORY + (g.smoothNormals?.byteLength || 0) * 4 + ((g.edges?.byteLength || 0) + (g.wire?.byteLength || 0)) * 2;
|
|
332
|
+
}
|
|
333
|
+
function assertMemory(limits, bytes, operation = "model") {
|
|
334
|
+
if (bytes > (limits.max_memory_bytes ?? Number.MAX_SAFE_INTEGER))
|
|
335
|
+
throw new Error(
|
|
336
|
+
`Estimated memory for this ${operation} exceeds the device safety budget. Use a smaller model or a device with more memory.`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
function derivedMemory(triangles, features) {
|
|
340
|
+
return MEMORY_OVERHEAD + triangles * (TRIANGLE_MEMORY + (features.smooth ? 1536 : 0) + (features.edges || features.wire ? 2048 : 0));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/print-options.js
|
|
344
|
+
function printOptions(input = {}) {
|
|
345
|
+
const level = input.level ?? "basic";
|
|
346
|
+
if (!["basic", "detailed"].includes(level))
|
|
347
|
+
throw new Error("Choose basic or detailed print checks");
|
|
348
|
+
const number2 = (v, name, max = 1e9) => {
|
|
349
|
+
if (!Number.isFinite(v) || v <= 0 || v > max)
|
|
350
|
+
throw new Error(`Invalid ${name}`);
|
|
351
|
+
return v;
|
|
352
|
+
};
|
|
353
|
+
let profile = null;
|
|
354
|
+
if (input.profile != null) {
|
|
355
|
+
if (input.profile.technology !== "fdm")
|
|
356
|
+
throw new Error("Only FDM profiles are supported");
|
|
357
|
+
profile = { technology: "fdm" };
|
|
358
|
+
for (const key of ["min_wall_mm", "line_width_mm"])
|
|
359
|
+
if (input.profile[key] != null)
|
|
360
|
+
profile[key] = number2(input.profile[key], key);
|
|
361
|
+
if (input.profile.overhang_degrees != null)
|
|
362
|
+
profile.overhang_degrees = number2(
|
|
363
|
+
input.profile.overhang_degrees,
|
|
364
|
+
"overhang angle",
|
|
365
|
+
89.9
|
|
366
|
+
);
|
|
367
|
+
if (input.profile.build_volume_mm != null) {
|
|
368
|
+
if (!Array.isArray(input.profile.build_volume_mm) || input.profile.build_volume_mm.length !== 3)
|
|
369
|
+
throw new Error("Build volume requires X, Y and Z");
|
|
370
|
+
profile.build_volume_mm = input.profile.build_volume_mm.map(
|
|
371
|
+
(v) => number2(v, "build volume")
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const time = number2(input.max_time_ms ?? 3e4, "check time limit", 12e4);
|
|
376
|
+
return {
|
|
377
|
+
level,
|
|
378
|
+
profile,
|
|
379
|
+
apply_transforms: input.apply_transforms !== false,
|
|
380
|
+
units_confirmed: input.units_confirmed === true,
|
|
381
|
+
max_time_ms: time
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
var printMemory = (triangles) => 64 * 1024 ** 2 + triangles * 2400;
|
|
385
|
+
var printSignature = (model) => JSON.stringify([
|
|
386
|
+
model.x,
|
|
387
|
+
model.y,
|
|
388
|
+
model.z,
|
|
389
|
+
model.rotationx,
|
|
390
|
+
model.rotationy,
|
|
391
|
+
model.rotationz,
|
|
392
|
+
model.scaleVector,
|
|
393
|
+
model.center,
|
|
394
|
+
model.units,
|
|
395
|
+
model.unitScale
|
|
396
|
+
]);
|
|
397
|
+
|
|
398
|
+
// src/model-tools.js
|
|
399
|
+
var axes = ["x", "y", "z"];
|
|
400
|
+
function validateUnits(units) {
|
|
401
|
+
if (!["mm", "inch"].includes(units)) throw new Error("Choose mm or inch");
|
|
402
|
+
return units;
|
|
403
|
+
}
|
|
404
|
+
function validateSizeState(value) {
|
|
405
|
+
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))
|
|
406
|
+
throw new Error("Invalid saved model size");
|
|
407
|
+
return {
|
|
408
|
+
version: 1,
|
|
409
|
+
baseline_mm: [...value.baseline_mm],
|
|
410
|
+
file_units: validateUnits(value.file_units),
|
|
411
|
+
editable: value.editable,
|
|
412
|
+
unit_factor: value.unit_factor ?? 1
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function sizeState(model) {
|
|
416
|
+
return model.size_state ??= {
|
|
417
|
+
version: 1,
|
|
418
|
+
baseline_mm: model.geometry.bounds.max.map(
|
|
419
|
+
(v, i) => (v - model.geometry.bounds.min[i]) * model.unitScale
|
|
420
|
+
),
|
|
421
|
+
file_units: "mm",
|
|
422
|
+
editable: !model.geometry.units,
|
|
423
|
+
unit_factor: 1
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function modelSize(model) {
|
|
427
|
+
if (!model.geometry) throw new Error("Wait for the model to finish opening");
|
|
428
|
+
const state = sizeState(model);
|
|
429
|
+
return {
|
|
430
|
+
...structuredClone(state),
|
|
431
|
+
dimensions_mm: model.geometry.bounds.max.map(
|
|
432
|
+
(v, i) => (v - model.geometry.bounds.min[i]) * model.scaleVector[i]
|
|
433
|
+
)
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function resizeModel(id, factors) {
|
|
437
|
+
const m = this._model(id);
|
|
438
|
+
modelSize(m);
|
|
439
|
+
if (!Array.isArray(factors) || factors.length !== 3 || factors.some((v) => !Number.isFinite(v) || v <= 0))
|
|
440
|
+
throw new Error("Scale must be a positive finite number");
|
|
441
|
+
const next = axes.map((a, i) => m["scale" + a] * factors[i]);
|
|
442
|
+
if (next.some((v) => !Number.isFinite(v) || v < 1e-12 || v > 1e12))
|
|
443
|
+
throw new Error("Scale is outside the supported range");
|
|
444
|
+
const center = m.geometry.bounds.min.map(
|
|
445
|
+
(v, i) => (v + m.geometry.bounds.max[i]) / 2
|
|
446
|
+
);
|
|
447
|
+
const before = transform(modelMatrix(m), center);
|
|
448
|
+
const cached = m._measurements;
|
|
449
|
+
const oldKey = m.scaleVector.join(",");
|
|
450
|
+
this._stopAnimation(m, "manual");
|
|
451
|
+
axes.forEach((a, i) => m["scale" + a] = next[i]);
|
|
452
|
+
const after = transform(modelMatrix(m), center);
|
|
453
|
+
axes.forEach((a, i) => m[a] += before[i] - after[i]);
|
|
454
|
+
m._stats = null;
|
|
455
|
+
if (cached?.key === oldKey && cached.positions === m.geometry.positions && factors.every((v) => v === factors[0])) {
|
|
456
|
+
m._measurements = {
|
|
457
|
+
...cached,
|
|
458
|
+
key: m.scaleVector.join(","),
|
|
459
|
+
result: {
|
|
460
|
+
...cached.result,
|
|
461
|
+
area_mm2: cached.result.area_mm2 * factors[0] ** 2,
|
|
462
|
+
volume_mm3: cached.result.volume_mm3 == null ? null : cached.result.volume_mm3 * factors[0] ** 3
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
this._invalidate();
|
|
467
|
+
return modelSize(m);
|
|
468
|
+
}
|
|
469
|
+
function setFileUnits(id, units) {
|
|
470
|
+
validateUnits(units);
|
|
471
|
+
const m = this._model(id), state = sizeState(m);
|
|
472
|
+
if (!state.editable)
|
|
473
|
+
throw new Error(
|
|
474
|
+
"This model has explicit file units, already converted to millimetres"
|
|
475
|
+
);
|
|
476
|
+
if (state.file_units === units) return modelSize(m);
|
|
477
|
+
const factor = units === "inch" ? 25.4 : 1 / 25.4;
|
|
478
|
+
const result = resizeModel.call(this, id, [factor, factor, factor]);
|
|
479
|
+
state.baseline_mm = state.baseline_mm.map((v) => v * factor);
|
|
480
|
+
state.file_units = units;
|
|
481
|
+
state.unit_factor = (state.unit_factor ?? 1) * factor;
|
|
482
|
+
return { ...result, ...structuredClone(state) };
|
|
483
|
+
}
|
|
484
|
+
function resetModelSize(id) {
|
|
485
|
+
const size = modelSize(this._model(id));
|
|
486
|
+
return resizeModel.call(
|
|
487
|
+
this,
|
|
488
|
+
id,
|
|
489
|
+
size.dimensions_mm.map((v, i) => v ? size.baseline_mm[i] / v : 1)
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
function duplicateModel(id) {
|
|
493
|
+
const source = this._model(id);
|
|
494
|
+
modelSize(source);
|
|
495
|
+
if (source._sceneImportPending)
|
|
496
|
+
throw new Error("Wait for the scene to finish opening");
|
|
497
|
+
if (this._models.size + 1 > this.limits.max_archive_entries || [...this._models.values()].reduce(
|
|
498
|
+
(n, m) => n + (m.geometry?.triangles || 0),
|
|
499
|
+
source.geometry.triangles
|
|
500
|
+
) > this.limits.max_triangles)
|
|
501
|
+
throw new Error("Scene model or triangle limit exceeded");
|
|
502
|
+
assertMemory(
|
|
503
|
+
this.limits,
|
|
504
|
+
this._occupiedMemory() + geometryMemory(source.geometry),
|
|
505
|
+
"duplicate"
|
|
506
|
+
);
|
|
507
|
+
const copy = {};
|
|
508
|
+
for (const key of [
|
|
509
|
+
"filename",
|
|
510
|
+
"orig_filename",
|
|
511
|
+
"local_file",
|
|
512
|
+
"x",
|
|
513
|
+
"y",
|
|
514
|
+
"z",
|
|
515
|
+
"rotationx",
|
|
516
|
+
"rotationy",
|
|
517
|
+
"rotationz",
|
|
518
|
+
"opacity",
|
|
519
|
+
"units",
|
|
520
|
+
"display",
|
|
521
|
+
"view_edges",
|
|
522
|
+
"smooth_shading",
|
|
523
|
+
"color"
|
|
524
|
+
])
|
|
525
|
+
copy[key] = source[key];
|
|
526
|
+
copy.id = this.max_model_id + 1;
|
|
527
|
+
if (!Number.isSafeInteger(copy.id))
|
|
528
|
+
throw new Error("Model ID limit exceeded");
|
|
529
|
+
copy.display_name = (source.display_name || source.orig_filename || source.filename || "Model").split(/[\\/]/).pop() + " \u2014 copy";
|
|
530
|
+
copy.center = [...source.center];
|
|
531
|
+
copy.unitScale = 1;
|
|
532
|
+
axes.forEach((a, i) => copy["scale" + a] = source.scaleVector[i]);
|
|
533
|
+
Object.defineProperty(copy, "scaleVector", {
|
|
534
|
+
get: () => axes.map((a) => copy["scale" + a] * copy.unitScale)
|
|
535
|
+
});
|
|
536
|
+
copy.geometry = { ...source.geometry };
|
|
537
|
+
copy.section = source.section ? { ...source.section } : null;
|
|
538
|
+
copy.size_state = structuredClone(sizeState(source));
|
|
539
|
+
copy.size_state.unit_factor = (copy.size_state.unit_factor ?? 1) * source.unitScale;
|
|
540
|
+
const saved = savePlayback(source);
|
|
541
|
+
this.max_model_id = copy.id;
|
|
542
|
+
this._models.set(copy.id, copy);
|
|
543
|
+
this.models_ref[copy.id] = this.models.length;
|
|
544
|
+
this.models.push(copy);
|
|
545
|
+
this.models_count = this._models.size;
|
|
546
|
+
if (saved)
|
|
547
|
+
this._startAnimation(copy, saved.animation, {
|
|
548
|
+
...saved.animation_state,
|
|
549
|
+
paused: true
|
|
550
|
+
});
|
|
551
|
+
this._invalidate();
|
|
552
|
+
return copy.id;
|
|
553
|
+
}
|
|
554
|
+
var cameraDirections = {
|
|
555
|
+
front: [
|
|
556
|
+
[0, -1, 0],
|
|
557
|
+
[0, 0, 1]
|
|
558
|
+
],
|
|
559
|
+
back: [
|
|
560
|
+
[0, 1, 0],
|
|
561
|
+
[0, 0, 1]
|
|
562
|
+
],
|
|
563
|
+
left: [
|
|
564
|
+
[-1, 0, 0],
|
|
565
|
+
[0, 0, 1]
|
|
566
|
+
],
|
|
567
|
+
right: [
|
|
568
|
+
[1, 0, 0],
|
|
569
|
+
[0, 0, 1]
|
|
570
|
+
],
|
|
571
|
+
top: [
|
|
572
|
+
[0, 0, 1],
|
|
573
|
+
[0, 1, 0]
|
|
574
|
+
],
|
|
575
|
+
bottom: [
|
|
576
|
+
[0, 0, -1],
|
|
577
|
+
[0, -1, 0]
|
|
578
|
+
],
|
|
579
|
+
isometric: [
|
|
580
|
+
[1, -1, 1],
|
|
581
|
+
[0, 0, 1]
|
|
582
|
+
]
|
|
583
|
+
};
|
|
584
|
+
function cameraView(name) {
|
|
585
|
+
const view = cameraDirections[name];
|
|
586
|
+
if (!view) throw new Error("Unknown camera view");
|
|
587
|
+
this.camera.position = this.camera.target.map((v, i) => v + view[0][i]);
|
|
588
|
+
this.camera.up = [...view[1]];
|
|
589
|
+
this.set_auto_zoom();
|
|
590
|
+
}
|
|
591
|
+
function validateBookmarks(bookmarks) {
|
|
592
|
+
if (!Array.isArray(bookmarks) || bookmarks.length > 32)
|
|
593
|
+
throw new Error("Save up to 32 camera views");
|
|
594
|
+
const names = /* @__PURE__ */ new Set();
|
|
595
|
+
return bookmarks.map((b) => {
|
|
596
|
+
const name = typeof b?.name === "string" ? b.name.trim() : "";
|
|
597
|
+
if (!name || name.length > 80 || /[\x00-\x1f]/.test(name) || names.has(name))
|
|
598
|
+
throw new Error("Use a unique view name of 1\u201380 characters");
|
|
599
|
+
names.add(name);
|
|
600
|
+
const state = {};
|
|
601
|
+
for (const key of ["position", "target", "up"]) {
|
|
602
|
+
if (!b.camera?.[key] || axes.some(
|
|
603
|
+
(a) => !Number.isFinite(b.camera[key][a]) || Math.abs(b.camera[key][a]) > 1e15
|
|
604
|
+
))
|
|
605
|
+
throw new Error("Invalid saved camera");
|
|
606
|
+
state[key] = Object.fromEntries(axes.map((a) => [a, b.camera[key][a]]));
|
|
607
|
+
}
|
|
608
|
+
const d = axes.map((a) => state.position[a] - state.target[a]), u = axes.map((a) => state.up[a]);
|
|
609
|
+
if (Math.hypot(
|
|
610
|
+
d[1] * u[2] - d[2] * u[1],
|
|
611
|
+
d[2] * u[0] - d[0] * u[2],
|
|
612
|
+
d[0] * u[1] - d[1] * u[0]
|
|
613
|
+
) < 1e-12)
|
|
614
|
+
throw new Error("Degenerate saved camera");
|
|
615
|
+
return { name, camera: state };
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
function mergeBookmarks(existing, incoming) {
|
|
619
|
+
const result = validateBookmarks(existing), names = new Set(result.map((b) => b.name));
|
|
620
|
+
for (const b of validateBookmarks(incoming)) {
|
|
621
|
+
let name = b.name, n = 2;
|
|
622
|
+
while (names.has(name)) name = b.name.slice(0, 70) + ` (${n++})`;
|
|
623
|
+
names.add(name);
|
|
624
|
+
result.push({ ...b, name });
|
|
625
|
+
}
|
|
626
|
+
return validateBookmarks(result);
|
|
627
|
+
}
|
|
628
|
+
async function measureModel(id, options = {}) {
|
|
629
|
+
await this.when_ready;
|
|
630
|
+
const m = this._model(id), g = m.geometry;
|
|
631
|
+
if (!g || m._sceneImportPending)
|
|
632
|
+
throw new Error("Wait for the model to finish opening");
|
|
633
|
+
const scale = m.scaleVector, key = scale.join(","), cached = m._measurements;
|
|
634
|
+
if (options.signal?.aborted)
|
|
635
|
+
throw new DOMException("Measurement cancelled", "AbortError");
|
|
636
|
+
if (cached?.positions === g.positions && cached.key === key)
|
|
637
|
+
return { ...cached.result };
|
|
638
|
+
const working = printMemory(g.triangles), occupied = this._occupiedMemory();
|
|
639
|
+
assertMemory(this.limits, occupied + working, "measurements");
|
|
640
|
+
const release = this._reserveMemory(
|
|
641
|
+
m,
|
|
642
|
+
working + geometryMemory(g),
|
|
643
|
+
"measurements"
|
|
644
|
+
);
|
|
645
|
+
const controller = new AbortController(), cancel = () => controller.abort();
|
|
646
|
+
this._aborters.add(controller);
|
|
647
|
+
(m._measureJobs ??= /* @__PURE__ */ new Set()).add(controller);
|
|
648
|
+
options.signal?.addEventListener("abort", cancel, { once: true });
|
|
649
|
+
let job, callbackError;
|
|
650
|
+
const check = () => {
|
|
651
|
+
if (controller.signal.aborted || this.killsign)
|
|
652
|
+
throw new DOMException("Measurement cancelled", "AbortError");
|
|
653
|
+
if (this._models.get(id) !== m || m.geometry.positions !== g.positions || m.scaleVector.join(",") !== key)
|
|
654
|
+
throw new Error("Model size changed. Calculate again.");
|
|
655
|
+
};
|
|
656
|
+
try {
|
|
657
|
+
const positions = new Float32Array(g.positions.length);
|
|
658
|
+
for (let start = 0; start < positions.length; start += 262144) {
|
|
659
|
+
check();
|
|
660
|
+
positions.set(g.positions.subarray(start, start + 262144), start);
|
|
661
|
+
if (start + 262144 < positions.length)
|
|
662
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
663
|
+
}
|
|
664
|
+
job = this._pool.run(
|
|
665
|
+
{
|
|
666
|
+
kind: "measure",
|
|
667
|
+
positions,
|
|
668
|
+
scale,
|
|
669
|
+
area: g.area,
|
|
670
|
+
max_time_ms: options.max_time_ms ?? 3e4,
|
|
671
|
+
limits: {
|
|
672
|
+
...this.limits,
|
|
673
|
+
max_memory_bytes: this.limits.max_memory_bytes - occupied
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
(progress) => {
|
|
677
|
+
try {
|
|
678
|
+
options.on_progress?.(progress);
|
|
679
|
+
} catch (error) {
|
|
680
|
+
callbackError = error;
|
|
681
|
+
controller.abort();
|
|
682
|
+
}
|
|
683
|
+
},
|
|
684
|
+
[positions.buffer]
|
|
685
|
+
);
|
|
686
|
+
controller.signal.addEventListener("abort", () => job.cancel(), {
|
|
687
|
+
once: true
|
|
688
|
+
});
|
|
689
|
+
check();
|
|
690
|
+
let result;
|
|
691
|
+
try {
|
|
692
|
+
result = await job;
|
|
693
|
+
} catch (error) {
|
|
694
|
+
throw callbackError || error;
|
|
695
|
+
}
|
|
696
|
+
check();
|
|
697
|
+
m._measurements = { positions: g.positions, key, result };
|
|
698
|
+
return { ...result };
|
|
699
|
+
} finally {
|
|
700
|
+
job?.cancel?.();
|
|
701
|
+
release();
|
|
702
|
+
this._aborters.delete(controller);
|
|
703
|
+
m._measureJobs.delete(controller);
|
|
704
|
+
options.signal?.removeEventListener("abort", cancel);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// src/appearance.js
|
|
709
|
+
var DISPLAYS = [
|
|
710
|
+
"flat",
|
|
711
|
+
"smooth",
|
|
712
|
+
"wireframe",
|
|
713
|
+
"edges",
|
|
714
|
+
"mesh",
|
|
715
|
+
"xray",
|
|
716
|
+
"studio",
|
|
717
|
+
"orientation"
|
|
718
|
+
];
|
|
719
|
+
function validateView(display, smooth) {
|
|
720
|
+
if (!DISPLAYS.includes(display)) throw new Error("Invalid display mode");
|
|
721
|
+
if (typeof smooth !== "boolean")
|
|
722
|
+
throw new Error("smooth_shading must be a boolean");
|
|
723
|
+
return { display, smooth_shading: display === "smooth" || smooth };
|
|
724
|
+
}
|
|
725
|
+
function viewFeatures(view) {
|
|
726
|
+
return {
|
|
727
|
+
smooth: (view.smooth_shading || view.display === "smooth") && view.display !== "wireframe",
|
|
728
|
+
edges: view.view_edges || view.display === "edges",
|
|
729
|
+
wire: view.display === "wireframe",
|
|
730
|
+
edgeAngle: view.display === "edges" ? 30 : 1
|
|
731
|
+
};
|
|
207
732
|
}
|
|
208
|
-
function
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
733
|
+
function validateSection(section) {
|
|
734
|
+
if (section == null) return null;
|
|
735
|
+
if (!["x", "y", "z"].includes(section.axis))
|
|
736
|
+
throw new Error("Section axis must be x, y or z");
|
|
737
|
+
if (!Number.isFinite(section.position) || section.position < 0 || section.position > 1)
|
|
738
|
+
throw new Error("Section position must be between 0 and 1");
|
|
739
|
+
if (section.flip !== void 0 && typeof section.flip !== "boolean")
|
|
740
|
+
throw new Error("Section flip must be a boolean");
|
|
741
|
+
return {
|
|
742
|
+
axis: section.axis,
|
|
743
|
+
position: section.position,
|
|
744
|
+
flip: section.flip ?? false
|
|
745
|
+
};
|
|
213
746
|
}
|
|
214
|
-
function
|
|
215
|
-
|
|
747
|
+
function sectionPlane(bounds, section) {
|
|
748
|
+
if (!section) return [0, 0, 0, 0];
|
|
749
|
+
const axis = ["x", "y", "z"].indexOf(section.axis), sign = section.flip ? -1 : 1;
|
|
750
|
+
const plane = [0, 0, 0, 0];
|
|
751
|
+
plane[axis] = sign;
|
|
752
|
+
plane[3] = -sign * (bounds.min[axis] + (bounds.max[axis] - bounds.min[axis]) * section.position);
|
|
753
|
+
return plane;
|
|
216
754
|
}
|
|
217
755
|
|
|
218
756
|
// src/geometry.js
|
|
@@ -509,6 +1047,7 @@ var Renderer = class {
|
|
|
509
1047
|
this.canvas = canvas;
|
|
510
1048
|
this.domElement = canvas;
|
|
511
1049
|
this.resources = /* @__PURE__ */ new Map();
|
|
1050
|
+
this.sharedResources = /* @__PURE__ */ new WeakMap();
|
|
512
1051
|
this.frames = 0;
|
|
513
1052
|
this.lost = false;
|
|
514
1053
|
this.gl = canvas.getContext("webgl2", {
|
|
@@ -525,6 +1064,7 @@ var Renderer = class {
|
|
|
525
1064
|
e.preventDefault();
|
|
526
1065
|
this.lost = true;
|
|
527
1066
|
this.resources.clear();
|
|
1067
|
+
this.sharedResources = /* @__PURE__ */ new WeakMap();
|
|
528
1068
|
this.gridResource = null;
|
|
529
1069
|
this.infiniteGrid = null;
|
|
530
1070
|
this.selectionResource = null;
|
|
@@ -632,7 +1172,12 @@ var Renderer = class {
|
|
|
632
1172
|
if (this.diagnostic?.model === model) this.setDiagnostic(null);
|
|
633
1173
|
const resources = this.resources.get(model);
|
|
634
1174
|
if (resources)
|
|
635
|
-
for (const resource of Object.values(resources))
|
|
1175
|
+
for (const resource of Object.values(resources)) {
|
|
1176
|
+
if (--resource.users === 0) {
|
|
1177
|
+
this.sharedResources.get(resource.source)?.delete(resource);
|
|
1178
|
+
this.free(resource);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
636
1181
|
this.resources.delete(model);
|
|
637
1182
|
}
|
|
638
1183
|
clear() {
|
|
@@ -646,8 +1191,10 @@ var Renderer = class {
|
|
|
646
1191
|
}
|
|
647
1192
|
get liveBuffers() {
|
|
648
1193
|
let n = (this.gridResource?.buffers.length || 0) + (this.selectionResource?.buffers.length || 0) + (this.diagnosticResource?.buffers.length || 0);
|
|
1194
|
+
const unique = /* @__PURE__ */ new Set();
|
|
649
1195
|
for (const r of this.resources.values())
|
|
650
|
-
for (const b of Object.values(r))
|
|
1196
|
+
for (const b of Object.values(r)) unique.add(b);
|
|
1197
|
+
for (const b of unique) n += b.buffers.length;
|
|
651
1198
|
return n;
|
|
652
1199
|
}
|
|
653
1200
|
setDiagnostic(value) {
|
|
@@ -713,12 +1260,23 @@ var Renderer = class {
|
|
|
713
1260
|
this.resources.set(model, r);
|
|
714
1261
|
}
|
|
715
1262
|
if (!r[mode]) {
|
|
716
|
-
const g = model.geometry;
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1263
|
+
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;
|
|
1264
|
+
let entries = this.sharedResources.get(source);
|
|
1265
|
+
if (!entries) this.sharedResources.set(source, entries = /* @__PURE__ */ new Set());
|
|
1266
|
+
let resource = [...entries].find(
|
|
1267
|
+
(r2) => r2.normals === normals && r2.colors === colors
|
|
721
1268
|
);
|
|
1269
|
+
if (!resource) {
|
|
1270
|
+
resource = Object.assign(this.upload(source, normals, colors), {
|
|
1271
|
+
source,
|
|
1272
|
+
normals,
|
|
1273
|
+
colors,
|
|
1274
|
+
users: 0
|
|
1275
|
+
});
|
|
1276
|
+
entries.add(resource);
|
|
1277
|
+
}
|
|
1278
|
+
resource.users++;
|
|
1279
|
+
r[mode] = resource;
|
|
722
1280
|
}
|
|
723
1281
|
return r[mode];
|
|
724
1282
|
}
|
|
@@ -1053,213 +1611,42 @@ var WorkerPool = class {
|
|
|
1053
1611
|
this.pump();
|
|
1054
1612
|
};
|
|
1055
1613
|
try {
|
|
1056
|
-
job.worker = this.factory ? this.factory() : new Worker(this.url, { type: "module", name: "ViewSTL parser" });
|
|
1057
|
-
job.worker.onmessage = ({ data }) => {
|
|
1058
|
-
if (data.id !== job.id) return;
|
|
1059
|
-
if (data.progress) {
|
|
1060
|
-
job.progress?.(data.progress);
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
end(data.error ? new Error(data.error) : null, data.result);
|
|
1064
|
-
};
|
|
1065
|
-
job.worker.onerror = (e) => {
|
|
1066
|
-
e.preventDefault();
|
|
1067
|
-
end(
|
|
1068
|
-
new Error(
|
|
1069
|
-
e.message || "Worker failed; check worker URL and Content Security Policy"
|
|
1070
|
-
)
|
|
1071
|
-
);
|
|
1072
|
-
};
|
|
1073
|
-
job.worker.onmessageerror = () => end(new Error("Invalid worker response"));
|
|
1074
|
-
job.timer = setTimeout(
|
|
1075
|
-
() => end(new Error("File processing timed out")),
|
|
1076
|
-
this.timeout
|
|
1077
|
-
);
|
|
1078
|
-
job.worker.postMessage({ id: job.id, task: job.task }, job.transfer);
|
|
1079
|
-
} catch (error) {
|
|
1080
|
-
end(error);
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
clear() {
|
|
1085
|
-
for (const job of [...this.queue, ...this.active]) this.cancel(job);
|
|
1086
|
-
}
|
|
1087
|
-
dispose() {
|
|
1088
|
-
this.disposed = true;
|
|
1089
|
-
this.clear();
|
|
1090
|
-
}
|
|
1091
|
-
};
|
|
1092
|
-
|
|
1093
|
-
// src/animation.js
|
|
1094
|
-
var animationKeys = [
|
|
1095
|
-
"x",
|
|
1096
|
-
"y",
|
|
1097
|
-
"z",
|
|
1098
|
-
"rotationx",
|
|
1099
|
-
"rotationy",
|
|
1100
|
-
"rotationz",
|
|
1101
|
-
"scalex",
|
|
1102
|
-
"scaley",
|
|
1103
|
-
"scalez"
|
|
1104
|
-
];
|
|
1105
|
-
var isScale = (key) => key.startsWith("scale");
|
|
1106
|
-
var number = (value, name, positive2 = false) => {
|
|
1107
|
-
if (typeof value !== "number" || !Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER || positive2 && value <= 0)
|
|
1108
|
-
throw new Error(`Invalid animation ${name}`);
|
|
1109
|
-
return value;
|
|
1110
|
-
};
|
|
1111
|
-
var object = (value, name) => {
|
|
1112
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1113
|
-
throw new Error(`Invalid animation ${name}`);
|
|
1114
|
-
return value;
|
|
1115
|
-
};
|
|
1116
|
-
function animationPose(model) {
|
|
1117
|
-
return Object.fromEntries(
|
|
1118
|
-
animationKeys.map((key) => [
|
|
1119
|
-
key,
|
|
1120
|
-
number(
|
|
1121
|
-
model[key] ?? (isScale(key) ? model.scale ?? 1 : 0),
|
|
1122
|
-
key,
|
|
1123
|
-
isScale(key)
|
|
1124
|
-
)
|
|
1125
|
-
])
|
|
1126
|
-
);
|
|
1127
|
-
}
|
|
1128
|
-
function normalizeAnimation(input) {
|
|
1129
|
-
object(input, "definition");
|
|
1130
|
-
const result = {}, claimed = /* @__PURE__ */ new Set();
|
|
1131
|
-
for (const mode of ["delta", "exact"]) {
|
|
1132
|
-
if (input[mode] == null) continue;
|
|
1133
|
-
const source = object(input[mode], mode), values = {};
|
|
1134
|
-
const msec = number(source.msec ?? 300, "duration", true);
|
|
1135
|
-
if (source.loop !== void 0 && typeof source.loop !== "boolean")
|
|
1136
|
-
throw new Error("Animation loop must be a boolean");
|
|
1137
|
-
const loop = mode === "delta" && !!source.loop;
|
|
1138
|
-
for (const key of animationKeys) {
|
|
1139
|
-
const value = source[key] ?? (isScale(key) ? source.scale : void 0);
|
|
1140
|
-
if (value === void 0) continue;
|
|
1141
|
-
if (claimed.has(key))
|
|
1142
|
-
throw new Error(`Animation delta/exact conflict: ${key}`);
|
|
1143
|
-
number(value, key, mode === "exact" && isScale(key));
|
|
1144
|
-
if (loop && isScale(key) && value < 0)
|
|
1145
|
-
throw new Error("Looping scale deltas cannot be negative");
|
|
1146
|
-
values[key] = value;
|
|
1147
|
-
claimed.add(key);
|
|
1148
|
-
}
|
|
1149
|
-
if (!Object.keys(values).length)
|
|
1150
|
-
throw new Error("Animation track needs a transform");
|
|
1151
|
-
result[mode] = { ...values, msec, ...loop ? { loop: true } : {} };
|
|
1152
|
-
}
|
|
1153
|
-
if (!Object.keys(result).length)
|
|
1154
|
-
throw new Error("Animation needs delta or exact");
|
|
1155
|
-
return result;
|
|
1156
|
-
}
|
|
1157
|
-
function normalizePlaybackState(input) {
|
|
1158
|
-
object(input, "playback state");
|
|
1159
|
-
if (input.version !== 1 || typeof input.paused !== "boolean")
|
|
1160
|
-
throw new Error("Invalid animation playback state version or paused flag");
|
|
1161
|
-
const elapsed_ms = number(input.elapsed_ms, "elapsed time");
|
|
1162
|
-
if (elapsed_ms < 0) throw new Error("Invalid animation elapsed time");
|
|
1163
|
-
object(input.origin, "origin");
|
|
1164
|
-
for (const key of animationKeys) number(input.origin[key], key, isScale(key));
|
|
1165
|
-
if (!Array.isArray(input.center) || input.center.length !== 3)
|
|
1166
|
-
throw new Error("Invalid animation pivot");
|
|
1167
|
-
return {
|
|
1168
|
-
version: 1,
|
|
1169
|
-
elapsed_ms,
|
|
1170
|
-
paused: input.paused,
|
|
1171
|
-
origin: animationPose(input.origin),
|
|
1172
|
-
center: input.center.map((v) => number(v, "pivot"))
|
|
1173
|
-
};
|
|
1174
|
-
}
|
|
1175
|
-
function createPlayback(definition, model, saved) {
|
|
1176
|
-
const animation = normalizeAnimation(definition);
|
|
1177
|
-
const state = saved == null ? null : normalizePlaybackState(saved);
|
|
1178
|
-
const origin = state?.origin ?? animationPose(model);
|
|
1179
|
-
const tracks = Object.entries(animation).map(([mode, a]) => {
|
|
1180
|
-
const values = Object.entries(a).filter(
|
|
1181
|
-
([key]) => animationKeys.includes(key)
|
|
1182
|
-
);
|
|
1183
|
-
if (mode === "delta")
|
|
1184
|
-
for (const [key, value] of values)
|
|
1185
|
-
number(origin[key] + value, `${key} endpoint`, isScale(key));
|
|
1186
|
-
return { mode, values, duration: a.msec, loop: !!a.loop, done: false };
|
|
1187
|
-
});
|
|
1188
|
-
const playback = {
|
|
1189
|
-
animation,
|
|
1190
|
-
origin,
|
|
1191
|
-
tracks,
|
|
1192
|
-
elapsed_ms: state?.elapsed_ms ?? 0,
|
|
1193
|
-
paused: state?.paused ?? false,
|
|
1194
|
-
last_time: null
|
|
1195
|
-
};
|
|
1196
|
-
evaluatePlayback({ ...playback, tracks: tracks.map((t) => ({ ...t })) }, {});
|
|
1197
|
-
return playback;
|
|
1198
|
-
}
|
|
1199
|
-
function evaluatePlayback(playback, model) {
|
|
1200
|
-
const updates = [];
|
|
1201
|
-
for (const track of playback.tracks) {
|
|
1202
|
-
if (track.done) continue;
|
|
1203
|
-
const fraction = playback.elapsed_ms / track.duration;
|
|
1204
|
-
const p = track.loop ? fraction : Math.min(fraction, 1);
|
|
1205
|
-
for (const [key, value] of track.values) {
|
|
1206
|
-
const from = playback.origin[key];
|
|
1207
|
-
const to = track.mode === "delta" ? from + value * p : from * (1 - p) + value * p;
|
|
1208
|
-
updates.push([key, number(to, `${key} result`, isScale(key))]);
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
let scaleChanged = false;
|
|
1212
|
-
for (const [key, value] of updates) {
|
|
1213
|
-
scaleChanged ||= isScale(key) && model[key] !== value;
|
|
1214
|
-
model[key] = value;
|
|
1215
|
-
}
|
|
1216
|
-
for (const track of playback.tracks)
|
|
1217
|
-
if (!track.loop && playback.elapsed_ms >= track.duration) track.done = true;
|
|
1218
|
-
if (scaleChanged) model._stats = null;
|
|
1219
|
-
return playback.tracks.some((track) => !track.done);
|
|
1220
|
-
}
|
|
1221
|
-
function advancePlayback(playback, model, time) {
|
|
1222
|
-
if (playback.paused) return true;
|
|
1223
|
-
if (playback.last_time !== null)
|
|
1224
|
-
playback.elapsed_ms = number(
|
|
1225
|
-
playback.elapsed_ms + Math.max(0, time - playback.last_time),
|
|
1226
|
-
"elapsed time"
|
|
1227
|
-
);
|
|
1228
|
-
playback.last_time = time;
|
|
1229
|
-
return evaluatePlayback(playback, model);
|
|
1230
|
-
}
|
|
1231
|
-
function playbackState(playback, suspended = false) {
|
|
1232
|
-
return {
|
|
1233
|
-
status: playback.paused ? "paused" : "running",
|
|
1234
|
-
elapsed_ms: playback.elapsed_ms,
|
|
1235
|
-
suspended,
|
|
1236
|
-
tracks: playback.tracks.filter((t) => !t.done).map((t) => ({
|
|
1237
|
-
mode: t.mode,
|
|
1238
|
-
msec: t.duration,
|
|
1239
|
-
loop: t.loop,
|
|
1240
|
-
progress: t.loop ? playback.elapsed_ms % t.duration / t.duration : Math.min(1, playback.elapsed_ms / t.duration)
|
|
1241
|
-
}))
|
|
1242
|
-
};
|
|
1243
|
-
}
|
|
1244
|
-
function savePlayback(model) {
|
|
1245
|
-
const p = model._animation;
|
|
1246
|
-
if (!p) return null;
|
|
1247
|
-
const factor = model.unitScale ?? 1;
|
|
1248
|
-
const animation = structuredClone(p.animation), origin = { ...p.origin };
|
|
1249
|
-
for (const key of animationKeys.filter(isScale)) {
|
|
1250
|
-
origin[key] *= factor;
|
|
1251
|
-
for (const track of Object.values(animation))
|
|
1252
|
-
if (track[key] !== void 0) track[key] *= factor;
|
|
1614
|
+
job.worker = this.factory ? this.factory() : new Worker(this.url, { type: "module", name: "ViewSTL parser" });
|
|
1615
|
+
job.worker.onmessage = ({ data }) => {
|
|
1616
|
+
if (data.id !== job.id) return;
|
|
1617
|
+
if (data.progress) {
|
|
1618
|
+
job.progress?.(data.progress);
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
end(data.error ? new Error(data.error) : null, data.result);
|
|
1622
|
+
};
|
|
1623
|
+
job.worker.onerror = (e) => {
|
|
1624
|
+
e.preventDefault();
|
|
1625
|
+
end(
|
|
1626
|
+
new Error(
|
|
1627
|
+
e.message || "Worker failed; check worker URL and Content Security Policy"
|
|
1628
|
+
)
|
|
1629
|
+
);
|
|
1630
|
+
};
|
|
1631
|
+
job.worker.onmessageerror = () => end(new Error("Invalid worker response"));
|
|
1632
|
+
job.timer = setTimeout(
|
|
1633
|
+
() => end(new Error("File processing timed out")),
|
|
1634
|
+
this.timeout
|
|
1635
|
+
);
|
|
1636
|
+
job.worker.postMessage({ id: job.id, task: job.task }, job.transfer);
|
|
1637
|
+
} catch (error) {
|
|
1638
|
+
end(error);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1253
1641
|
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
}
|
|
1642
|
+
clear() {
|
|
1643
|
+
for (const job of [...this.queue, ...this.active]) this.cancel(job);
|
|
1644
|
+
}
|
|
1645
|
+
dispose() {
|
|
1646
|
+
this.disposed = true;
|
|
1647
|
+
this.clear();
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1263
1650
|
|
|
1264
1651
|
// src/scene-import.js
|
|
1265
1652
|
var sceneFormat = (name) => String(name || "").match(/\.(vsb|vsj)(?:[?#]|$)/i)?.[1].toLowerCase() || "";
|
|
@@ -1320,8 +1707,8 @@ function importScene(source, options = {}) {
|
|
|
1320
1707
|
await this.when_ready;
|
|
1321
1708
|
check();
|
|
1322
1709
|
oldCamera = this.get_camera_state();
|
|
1323
|
-
const
|
|
1324
|
-
if (!["vsb", "vsj"].includes(
|
|
1710
|
+
const format2 = options.format || sceneFormat(typeof source === "string" ? source : source.name);
|
|
1711
|
+
if (!["vsb", "vsj"].includes(format2))
|
|
1325
1712
|
throw new Error("Choose VSB or VSJ for a scene");
|
|
1326
1713
|
let remote, relay;
|
|
1327
1714
|
if (typeof source === "string") {
|
|
@@ -1354,8 +1741,8 @@ function importScene(source, options = {}) {
|
|
|
1354
1741
|
source,
|
|
1355
1742
|
remote,
|
|
1356
1743
|
relay,
|
|
1357
|
-
format,
|
|
1358
|
-
name: `scene.${
|
|
1744
|
+
format: format2,
|
|
1745
|
+
name: `scene.${format2}`,
|
|
1359
1746
|
limits
|
|
1360
1747
|
},
|
|
1361
1748
|
(p) => {
|
|
@@ -1376,6 +1763,10 @@ function importScene(source, options = {}) {
|
|
|
1376
1763
|
job = null;
|
|
1377
1764
|
check();
|
|
1378
1765
|
const { scene, files, base } = documentData;
|
|
1766
|
+
const bookmarks = mergeBookmarks(
|
|
1767
|
+
this.get_camera_bookmarks(),
|
|
1768
|
+
scene.camera_bookmarks || []
|
|
1769
|
+
);
|
|
1379
1770
|
this._sceneBytes -= held;
|
|
1380
1771
|
held = Object.values(files || {}).reduce((n, b) => n + b.byteLength, 0) + JSON.stringify(scene).length * 2;
|
|
1381
1772
|
this._sceneBytes += held;
|
|
@@ -1467,6 +1858,8 @@ function importScene(source, options = {}) {
|
|
|
1467
1858
|
if (scene.auto_rotate !== void 0)
|
|
1468
1859
|
this.set_auto_rotate(scene.auto_rotate);
|
|
1469
1860
|
this._restoreCamera = scene.camera_state || (hadModels ? oldCamera : null);
|
|
1861
|
+
this.camera_bookmarks = bookmarks;
|
|
1862
|
+
if (scene.display_units) this.display_units = scene.display_units;
|
|
1470
1863
|
committed = true;
|
|
1471
1864
|
return { model_ids: ids, scene, used_files: [...usedFiles] };
|
|
1472
1865
|
})().catch((error) => {
|
|
@@ -1487,61 +1880,6 @@ function importScene(source, options = {}) {
|
|
|
1487
1880
|
return promise;
|
|
1488
1881
|
}
|
|
1489
1882
|
|
|
1490
|
-
// src/print-options.js
|
|
1491
|
-
function printOptions(input = {}) {
|
|
1492
|
-
const level = input.level ?? "basic";
|
|
1493
|
-
if (!["basic", "detailed"].includes(level))
|
|
1494
|
-
throw new Error("Choose basic or detailed print checks");
|
|
1495
|
-
const number2 = (v, name, max = 1e9) => {
|
|
1496
|
-
if (!Number.isFinite(v) || v <= 0 || v > max)
|
|
1497
|
-
throw new Error(`Invalid ${name}`);
|
|
1498
|
-
return v;
|
|
1499
|
-
};
|
|
1500
|
-
let profile = null;
|
|
1501
|
-
if (input.profile != null) {
|
|
1502
|
-
if (input.profile.technology !== "fdm")
|
|
1503
|
-
throw new Error("Only FDM profiles are supported");
|
|
1504
|
-
profile = { technology: "fdm" };
|
|
1505
|
-
for (const key of ["min_wall_mm", "line_width_mm"])
|
|
1506
|
-
if (input.profile[key] != null)
|
|
1507
|
-
profile[key] = number2(input.profile[key], key);
|
|
1508
|
-
if (input.profile.overhang_degrees != null)
|
|
1509
|
-
profile.overhang_degrees = number2(
|
|
1510
|
-
input.profile.overhang_degrees,
|
|
1511
|
-
"overhang angle",
|
|
1512
|
-
89.9
|
|
1513
|
-
);
|
|
1514
|
-
if (input.profile.build_volume_mm != null) {
|
|
1515
|
-
if (!Array.isArray(input.profile.build_volume_mm) || input.profile.build_volume_mm.length !== 3)
|
|
1516
|
-
throw new Error("Build volume requires X, Y and Z");
|
|
1517
|
-
profile.build_volume_mm = input.profile.build_volume_mm.map(
|
|
1518
|
-
(v) => number2(v, "build volume")
|
|
1519
|
-
);
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
const time = number2(input.max_time_ms ?? 3e4, "check time limit", 12e4);
|
|
1523
|
-
return {
|
|
1524
|
-
level,
|
|
1525
|
-
profile,
|
|
1526
|
-
apply_transforms: input.apply_transforms !== false,
|
|
1527
|
-
units_confirmed: input.units_confirmed === true,
|
|
1528
|
-
max_time_ms: time
|
|
1529
|
-
};
|
|
1530
|
-
}
|
|
1531
|
-
var printMemory = (triangles) => 64 * 1024 ** 2 + triangles * 2400;
|
|
1532
|
-
var printSignature = (model) => JSON.stringify([
|
|
1533
|
-
model.x,
|
|
1534
|
-
model.y,
|
|
1535
|
-
model.z,
|
|
1536
|
-
model.rotationx,
|
|
1537
|
-
model.rotationy,
|
|
1538
|
-
model.rotationz,
|
|
1539
|
-
model.scaleVector,
|
|
1540
|
-
model.center,
|
|
1541
|
-
model.units,
|
|
1542
|
-
model.unitScale
|
|
1543
|
-
]);
|
|
1544
|
-
|
|
1545
1883
|
// src/print-controller.js
|
|
1546
1884
|
var reports = /* @__PURE__ */ new WeakMap();
|
|
1547
1885
|
function reportCurrent(viewer, report) {
|
|
@@ -1639,7 +1977,9 @@ async function checkPrintability(options = {}) {
|
|
|
1639
1977
|
rotationy: 0,
|
|
1640
1978
|
rotationz: 0,
|
|
1641
1979
|
center: [0, 0, 0],
|
|
1642
|
-
scaleVector: [
|
|
1980
|
+
scaleVector: [1, 1, 1].map(
|
|
1981
|
+
() => model.unitScale * (model.size_state?.unit_factor ?? 1)
|
|
1982
|
+
)
|
|
1643
1983
|
};
|
|
1644
1984
|
progress({
|
|
1645
1985
|
phase: "Starting",
|
|
@@ -1763,8 +2103,8 @@ function exportFilename(filename, extension) {
|
|
|
1763
2103
|
function exportOptions(options = {}) {
|
|
1764
2104
|
const aliases = { stp: "step", igs: "iges" };
|
|
1765
2105
|
const requested = String(options.format || "").toLowerCase();
|
|
1766
|
-
const
|
|
1767
|
-
if (!["stl", "obj", "3mf", "step", "iges"].includes(
|
|
2106
|
+
const format2 = aliases[requested] || requested;
|
|
2107
|
+
if (!["stl", "obj", "3mf", "step", "iges"].includes(format2))
|
|
1768
2108
|
throw new Error("Export format must be stl, obj, 3mf, step or iges");
|
|
1769
2109
|
const units = options.units ?? "mm";
|
|
1770
2110
|
if (!["mm", "inch"].includes(units))
|
|
@@ -1774,10 +2114,10 @@ function exportOptions(options = {}) {
|
|
|
1774
2114
|
throw new Error(`${key} must be a boolean`);
|
|
1775
2115
|
if (options.on_progress !== void 0 && typeof options.on_progress !== "function")
|
|
1776
2116
|
throw new Error("on_progress must be a function");
|
|
1777
|
-
const colors = options.colors !== false && ["obj", "3mf"].includes(
|
|
1778
|
-
const extension =
|
|
2117
|
+
const colors = options.colors !== false && ["obj", "3mf"].includes(format2);
|
|
2118
|
+
const extension = format2 === "obj" && colors ? "zip" : format2;
|
|
1779
2119
|
return {
|
|
1780
|
-
format,
|
|
2120
|
+
format: format2,
|
|
1781
2121
|
units,
|
|
1782
2122
|
colors,
|
|
1783
2123
|
apply_transforms: options.apply_transforms !== false,
|
|
@@ -1970,7 +2310,9 @@ var modelKeys = [
|
|
|
1970
2310
|
"section",
|
|
1971
2311
|
"units",
|
|
1972
2312
|
"animation",
|
|
1973
|
-
"animation_state"
|
|
2313
|
+
"animation_state",
|
|
2314
|
+
"size_state",
|
|
2315
|
+
"display_name"
|
|
1974
2316
|
];
|
|
1975
2317
|
var finite = (v, name) => {
|
|
1976
2318
|
if (typeof v !== "number" || !Number.isFinite(v))
|
|
@@ -2002,7 +2344,7 @@ function modelOptions(input) {
|
|
|
2002
2344
|
return out;
|
|
2003
2345
|
}
|
|
2004
2346
|
var StlViewer = class _StlViewer {
|
|
2005
|
-
static version = "2.0.0-rc.
|
|
2347
|
+
static version = "2.0.0-rc.7";
|
|
2006
2348
|
static workerURL = null;
|
|
2007
2349
|
constructor(parent, options = {}) {
|
|
2008
2350
|
if (!parent?.appendChild)
|
|
@@ -2031,6 +2373,8 @@ var StlViewer = class _StlViewer {
|
|
|
2031
2373
|
this._sessionActive = false;
|
|
2032
2374
|
this._frame = 0;
|
|
2033
2375
|
this._lastTime = 0;
|
|
2376
|
+
this.camera_bookmarks = [];
|
|
2377
|
+
this.display_units = "mm";
|
|
2034
2378
|
this.grid = null;
|
|
2035
2379
|
this._gridAuto = false;
|
|
2036
2380
|
this.camera = {
|
|
@@ -2367,6 +2711,13 @@ var StlViewer = class _StlViewer {
|
|
|
2367
2711
|
finite(m[key], key);
|
|
2368
2712
|
if (key.startsWith("scale")) positive(m[key], key);
|
|
2369
2713
|
}
|
|
2714
|
+
if (supplied.size_state)
|
|
2715
|
+
m.size_state = validateSizeState(supplied.size_state);
|
|
2716
|
+
if (supplied.display_name != null) {
|
|
2717
|
+
if (typeof supplied.display_name !== "string" || supplied.display_name.length > 1024)
|
|
2718
|
+
throw new Error("Invalid model name");
|
|
2719
|
+
m.display_name = supplied.display_name;
|
|
2720
|
+
}
|
|
2370
2721
|
m.opacity = supplied.opacity ?? supplied.mesh?.material?.opacity ?? 1;
|
|
2371
2722
|
finite(m.opacity, "opacity");
|
|
2372
2723
|
if (m.opacity < 0 || m.opacity > 1)
|
|
@@ -2518,7 +2869,7 @@ var StlViewer = class _StlViewer {
|
|
|
2518
2869
|
format: supplied.remote_format,
|
|
2519
2870
|
relay,
|
|
2520
2871
|
source: typeof source === "string" ? new URL(source, document.baseURI).href : source,
|
|
2521
|
-
name: m.filename,
|
|
2872
|
+
name: m.display_name || m.filename,
|
|
2522
2873
|
limits,
|
|
2523
2874
|
features: features.smooth || features.edges || features.wire ? features : null
|
|
2524
2875
|
},
|
|
@@ -2568,6 +2919,7 @@ var StlViewer = class _StlViewer {
|
|
|
2568
2919
|
}
|
|
2569
2920
|
_deleteModel(m) {
|
|
2570
2921
|
this._stopAnimation(m, "removed");
|
|
2922
|
+
for (const controller of m._measureJobs || []) controller.abort();
|
|
2571
2923
|
for (const [controller, models] of this._printJobs || [])
|
|
2572
2924
|
if (models.has(m)) controller.abort();
|
|
2573
2925
|
this._printCache = null;
|
|
@@ -2595,6 +2947,8 @@ var StlViewer = class _StlViewer {
|
|
|
2595
2947
|
if (this.killsign) return;
|
|
2596
2948
|
this._generation++;
|
|
2597
2949
|
this._printCache = null;
|
|
2950
|
+
this.camera_bookmarks = [];
|
|
2951
|
+
this.display_units = "mm";
|
|
2598
2952
|
this._pool?.clear();
|
|
2599
2953
|
for (const c of this._aborters) c.abort();
|
|
2600
2954
|
this._aborters.clear();
|
|
@@ -3034,12 +3388,42 @@ var StlViewer = class _StlViewer {
|
|
|
3034
3388
|
type
|
|
3035
3389
|
);
|
|
3036
3390
|
}
|
|
3037
|
-
|
|
3391
|
+
get_model_size(id) {
|
|
3392
|
+
return modelSize(this._model(id));
|
|
3393
|
+
}
|
|
3394
|
+
resize_model(id, factors) {
|
|
3395
|
+
return resizeModel.call(this, id, factors);
|
|
3396
|
+
}
|
|
3397
|
+
set_file_units(id, units) {
|
|
3398
|
+
return setFileUnits.call(this, id, units);
|
|
3399
|
+
}
|
|
3400
|
+
reset_model_size(id) {
|
|
3401
|
+
return resetModelSize.call(this, id);
|
|
3402
|
+
}
|
|
3403
|
+
duplicate_model(id) {
|
|
3404
|
+
return duplicateModel.call(this, id);
|
|
3405
|
+
}
|
|
3406
|
+
set_camera_view(name) {
|
|
3407
|
+
return cameraView.call(this, name);
|
|
3408
|
+
}
|
|
3409
|
+
set_display_units(units) {
|
|
3410
|
+
this.display_units = validateUnits(units);
|
|
3411
|
+
}
|
|
3412
|
+
get_camera_bookmarks() {
|
|
3413
|
+
return structuredClone(this.camera_bookmarks || []);
|
|
3414
|
+
}
|
|
3415
|
+
set_camera_bookmarks(bookmarks) {
|
|
3416
|
+
this.camera_bookmarks = validateBookmarks(bookmarks);
|
|
3417
|
+
}
|
|
3418
|
+
measure_model(id, options) {
|
|
3419
|
+
return measureModel.call(this, id, options);
|
|
3420
|
+
}
|
|
3421
|
+
get_model_info(id, options = {}) {
|
|
3038
3422
|
return this._guard(() => {
|
|
3039
3423
|
const m = this._model(id), g = m.geometry;
|
|
3040
3424
|
if (!g) return null;
|
|
3041
3425
|
const s = m.scaleVector, key = s.join(",");
|
|
3042
|
-
if (!m._stats || m._stats.key !== key)
|
|
3426
|
+
if (options.measurements !== false && (!m._stats || m._stats.key !== key))
|
|
3043
3427
|
m._stats = { key, ...scaledStats(g, s) };
|
|
3044
3428
|
const factor = m.units === "inch" ? 1 / 25.4 : 1;
|
|
3045
3429
|
return {
|
|
@@ -3055,8 +3439,8 @@ var StlViewer = class _StlViewer {
|
|
|
3055
3439
|
section: m.section ? { ...m.section } : null,
|
|
3056
3440
|
color: m.color ?? (g.colors ? "#ffffff" : "#909090"),
|
|
3057
3441
|
scale: { x: m.scalex, y: m.scaley, z: m.scalez },
|
|
3058
|
-
volume: m._stats.volume * factor ** 3,
|
|
3059
|
-
area: m._stats.area * factor ** 2,
|
|
3442
|
+
volume: options.measurements === false ? null : m._stats.volume * factor ** 3,
|
|
3443
|
+
area: options.measurements === false ? null : m._stats.area * factor ** 2,
|
|
3060
3444
|
triangles: g.triangles,
|
|
3061
3445
|
bounds: { min: [...g.bounds.min], max: [...g.bounds.max] },
|
|
3062
3446
|
has_colors: !!g.colors,
|
|
@@ -3144,7 +3528,9 @@ var StlViewer = class _StlViewer {
|
|
|
3144
3528
|
rotationx: 0,
|
|
3145
3529
|
rotationy: 0,
|
|
3146
3530
|
rotationz: 0,
|
|
3147
|
-
scaleVector: [
|
|
3531
|
+
scaleVector: [1, 1, 1].map(
|
|
3532
|
+
() => m.unitScale * (m.size_state?.unit_factor ?? 1)
|
|
3533
|
+
)
|
|
3148
3534
|
};
|
|
3149
3535
|
const matrix = modelMatrix(source), factor = settings.units === "inch" ? 1 / 25.4 : 1;
|
|
3150
3536
|
for (let c = 0; c < 4; c++)
|
|
@@ -3212,8 +3598,8 @@ var StlViewer = class _StlViewer {
|
|
|
3212
3598
|
);
|
|
3213
3599
|
}
|
|
3214
3600
|
async export_scene(options = {}) {
|
|
3215
|
-
const
|
|
3216
|
-
if (!["vsb", "vsj"].includes(
|
|
3601
|
+
const format2 = String(options.format || "").toLowerCase();
|
|
3602
|
+
if (!["vsb", "vsj"].includes(format2))
|
|
3217
3603
|
throw new Error("Scene format must be vsb or vsj");
|
|
3218
3604
|
if (options.force_basename !== void 0 && typeof options.force_basename !== "boolean")
|
|
3219
3605
|
throw new Error("force_basename must be a boolean");
|
|
@@ -3236,7 +3622,7 @@ var StlViewer = class _StlViewer {
|
|
|
3236
3622
|
if (!models.length) throw new Error("No models to export");
|
|
3237
3623
|
if (models.some((m) => !m.geometry))
|
|
3238
3624
|
throw new Error("A model has not finished loading");
|
|
3239
|
-
if (models.length + (
|
|
3625
|
+
if (models.length + (format2 === "vsb" ? 1 : 0) > this.limits.max_archive_entries)
|
|
3240
3626
|
throw new Error("Scene model count limit exceeded");
|
|
3241
3627
|
const triangles = models.reduce((sum, m) => sum + m.geometry.triangles, 0);
|
|
3242
3628
|
if (triangles > this.limits.max_triangles)
|
|
@@ -3244,19 +3630,19 @@ var StlViewer = class _StlViewer {
|
|
|
3244
3630
|
const scene = this.get_vsj(
|
|
3245
3631
|
true,
|
|
3246
3632
|
!!options.force_basename,
|
|
3247
|
-
|
|
3633
|
+
format2 === "vsb"
|
|
3248
3634
|
);
|
|
3249
3635
|
const json = JSON.stringify(scene), manifestBytes = new Blob([json]).size;
|
|
3250
3636
|
if (manifestBytes > this.limits.max_file_bytes)
|
|
3251
3637
|
throw new Error("Scene manifest size limit exceeded");
|
|
3252
|
-
if (
|
|
3638
|
+
if (format2 === "vsb" && manifestBytes + models.length * 84 + triangles * 50 > this.limits.max_uncompressed_bytes)
|
|
3253
3639
|
throw new Error("Scene archive limit exceeded");
|
|
3254
|
-
const working = MEMORY_OVERHEAD + json.length * 8 + (
|
|
3640
|
+
const working = MEMORY_OVERHEAD + json.length * 8 + (format2 === "vsb" ? triangles * 512 : 0);
|
|
3255
3641
|
assertMemory(this.limits, this._occupiedMemory() + working, "scene export");
|
|
3256
3642
|
this._exportBytes += working;
|
|
3257
3643
|
let job, cancel, callbackError;
|
|
3258
3644
|
try {
|
|
3259
|
-
const snapshot =
|
|
3645
|
+
const snapshot = format2 === "vsb" ? models.map((m, index) => ({
|
|
3260
3646
|
id: m.id,
|
|
3261
3647
|
triangles: m.geometry.triangles,
|
|
3262
3648
|
positions: m.geometry.positions.slice(),
|
|
@@ -3271,7 +3657,7 @@ var StlViewer = class _StlViewer {
|
|
|
3271
3657
|
});
|
|
3272
3658
|
checkAbort();
|
|
3273
3659
|
let blob;
|
|
3274
|
-
if (
|
|
3660
|
+
if (format2 === "vsj") {
|
|
3275
3661
|
blob = new Blob([json], { type: "application/json" });
|
|
3276
3662
|
options.on_progress?.({
|
|
3277
3663
|
loaded: models.length,
|
|
@@ -3311,8 +3697,8 @@ var StlViewer = class _StlViewer {
|
|
|
3311
3697
|
checkAbort();
|
|
3312
3698
|
return {
|
|
3313
3699
|
blob,
|
|
3314
|
-
format,
|
|
3315
|
-
filename: exportFilename(options.filename,
|
|
3700
|
+
format: format2,
|
|
3701
|
+
filename: exportFilename(options.filename, format2),
|
|
3316
3702
|
model_ids: models.map((m) => m.id),
|
|
3317
3703
|
triangles,
|
|
3318
3704
|
representation: "scene"
|
|
@@ -3334,6 +3720,8 @@ var StlViewer = class _StlViewer {
|
|
|
3334
3720
|
canvas_height: this.canvas_height,
|
|
3335
3721
|
bg_color: this.bg_color,
|
|
3336
3722
|
camera_state: this.get_camera_state(),
|
|
3723
|
+
camera_bookmarks: this.get_camera_bookmarks(),
|
|
3724
|
+
display_units: this.display_units || "mm",
|
|
3337
3725
|
auto_rotate: this.auto_rotate,
|
|
3338
3726
|
mouse_zoom: this.mouse_zoom,
|
|
3339
3727
|
auto_resize: this.auto_resize,
|
|
@@ -3354,6 +3742,11 @@ var StlViewer = class _StlViewer {
|
|
|
3354
3742
|
id: for_vsb ? -1 : m.id,
|
|
3355
3743
|
filename: for_vsb ? `${m.id}.stl` : force_basename ? basename(m.filename) : m.filename,
|
|
3356
3744
|
units: m.units,
|
|
3745
|
+
size_state: {
|
|
3746
|
+
...structuredClone(sizeState(m)),
|
|
3747
|
+
unit_factor: for_vsb && !m._animation ? 1 : m.unitScale * (sizeState(m).unit_factor ?? 1)
|
|
3748
|
+
},
|
|
3749
|
+
...m.display_name ? { display_name: m.display_name } : {},
|
|
3357
3750
|
display: m.display,
|
|
3358
3751
|
opacity: m.opacity,
|
|
3359
3752
|
view_edges: m.view_edges,
|
|
@@ -3405,7 +3798,7 @@ var StlViewer = class _StlViewer {
|
|
|
3405
3798
|
null,
|
|
3406
3799
|
scene.files.map((f) => f.bin)
|
|
3407
3800
|
);
|
|
3408
|
-
return new Blob([await job], { type: "application/
|
|
3801
|
+
return new Blob([await job], { type: "application/octet-stream" });
|
|
3409
3802
|
}
|
|
3410
3803
|
download_vsb(filename = "1.vsb") {
|
|
3411
3804
|
return this.get_vsb_blob().then(
|
|
@@ -3476,6 +3869,13 @@ var StlViewer = class _StlViewer {
|
|
|
3476
3869
|
);
|
|
3477
3870
|
else if (scene.grid) this.set_grid(true);
|
|
3478
3871
|
if (scene.camera_state) this._restoreCamera = scene.camera_state;
|
|
3872
|
+
const bookmarks = mergeBookmarks(
|
|
3873
|
+
this.get_camera_bookmarks(),
|
|
3874
|
+
scene.camera_bookmarks || []
|
|
3875
|
+
);
|
|
3876
|
+
const displayUnits = validateUnits(
|
|
3877
|
+
scene.display_units || this.display_units || "mm"
|
|
3878
|
+
);
|
|
3479
3879
|
const incoming = [];
|
|
3480
3880
|
try {
|
|
3481
3881
|
for (const input of scene.models) {
|
|
@@ -3523,6 +3923,8 @@ var StlViewer = class _StlViewer {
|
|
|
3523
3923
|
this._startAnimation(m, model.animation, model.animation_state);
|
|
3524
3924
|
m._sceneImportPending = false;
|
|
3525
3925
|
}
|
|
3926
|
+
this.camera_bookmarks = bookmarks;
|
|
3927
|
+
this.display_units = displayUnits;
|
|
3526
3928
|
} catch (error) {
|
|
3527
3929
|
for (const { m } of incoming)
|
|
3528
3930
|
if (this._models.get(m.id) === m) this.remove_model(m.id);
|
|
@@ -3665,6 +4067,299 @@ var StlViewer = class _StlViewer {
|
|
|
3665
4067
|
}
|
|
3666
4068
|
};
|
|
3667
4069
|
|
|
4070
|
+
// src/app-tools.js
|
|
4071
|
+
var toolsControls = [
|
|
4072
|
+
"size-details",
|
|
4073
|
+
"size-fields",
|
|
4074
|
+
"size-x",
|
|
4075
|
+
"size-y",
|
|
4076
|
+
"size-z",
|
|
4077
|
+
"size-lock",
|
|
4078
|
+
"size-percent",
|
|
4079
|
+
"size-apply",
|
|
4080
|
+
"size-cancel",
|
|
4081
|
+
"size-reset",
|
|
4082
|
+
"size-error",
|
|
4083
|
+
"file-units",
|
|
4084
|
+
"file-units-note",
|
|
4085
|
+
"display-units",
|
|
4086
|
+
"size-unit-label",
|
|
4087
|
+
"duplicate-model",
|
|
4088
|
+
"camera-preset",
|
|
4089
|
+
"bookmark-name",
|
|
4090
|
+
"bookmark-list",
|
|
4091
|
+
"bookmark-save",
|
|
4092
|
+
"bookmark-restore",
|
|
4093
|
+
"bookmark-rename",
|
|
4094
|
+
"bookmark-delete",
|
|
4095
|
+
"camera-error",
|
|
4096
|
+
"info-area",
|
|
4097
|
+
"info-volume",
|
|
4098
|
+
"measure-model",
|
|
4099
|
+
"measure-cancel",
|
|
4100
|
+
"measure-status"
|
|
4101
|
+
];
|
|
4102
|
+
var axes2 = ["x", "y", "z"];
|
|
4103
|
+
var format = new Intl.NumberFormat(void 0, { maximumSignificantDigits: 7 });
|
|
4104
|
+
function bindAppTools({
|
|
4105
|
+
$,
|
|
4106
|
+
listen,
|
|
4107
|
+
window: window2,
|
|
4108
|
+
getViewer,
|
|
4109
|
+
getState,
|
|
4110
|
+
changeSize,
|
|
4111
|
+
duplicate,
|
|
4112
|
+
refresh,
|
|
4113
|
+
announce
|
|
4114
|
+
}) {
|
|
4115
|
+
let draftKind = null, lastAxis = 0, base = null, selected = null, measurement = null, disposed = false;
|
|
4116
|
+
const error = (id, message = "") => {
|
|
4117
|
+
$(id).textContent = message;
|
|
4118
|
+
$(id).hidden = !message;
|
|
4119
|
+
};
|
|
4120
|
+
const factor = () => getViewer()?.display_units === "inch" ? 25.4 : 1;
|
|
4121
|
+
const value = (v) => Number(v.toPrecision(10));
|
|
4122
|
+
function syncInfo() {
|
|
4123
|
+
const v = getViewer(), { selectedId, busy } = getState(), m = v?._models.get(selectedId);
|
|
4124
|
+
const s = m?.scaleVector, cached = m?._measurements;
|
|
4125
|
+
const valid = cached && cached.positions === m.geometry.positions && cached.key === s.join(",");
|
|
4126
|
+
const unit = factor() === 1 ? "mm" : "in";
|
|
4127
|
+
const area = valid ? cached.result.area_mm2 : s && s[0] === s[1] && s[1] === s[2] ? m.geometry.area * s[0] ** 2 : null;
|
|
4128
|
+
$("info-area").textContent = area == null ? "Calculate" : format.format(area / factor() ** 2) + " " + unit + "\xB2";
|
|
4129
|
+
$("info-volume").textContent = valid ? cached.result.volume_valid ? format.format(cached.result.volume_mm3 / factor() ** 3) + " " + unit + "\xB3" : "Unavailable" : "Not checked";
|
|
4130
|
+
if (!measurement)
|
|
4131
|
+
$("measure-status").textContent = valid ? cached.result.volume_reason : "Calculate to check enclosed volume. Runs on your device.";
|
|
4132
|
+
$("measure-model").disabled = busy || !m || !!measurement;
|
|
4133
|
+
$("measure-cancel").hidden = !measurement;
|
|
4134
|
+
}
|
|
4135
|
+
function syncBookmarks() {
|
|
4136
|
+
const v = getViewer(), previous = $("bookmark-list").value, bookmarks = v?.get_camera_bookmarks() || [];
|
|
4137
|
+
$("bookmark-list").replaceChildren(
|
|
4138
|
+
...bookmarks.map((b) => {
|
|
4139
|
+
const option = window2.document.createElement("option");
|
|
4140
|
+
option.value = b.name;
|
|
4141
|
+
option.textContent = b.name;
|
|
4142
|
+
return option;
|
|
4143
|
+
})
|
|
4144
|
+
);
|
|
4145
|
+
if (bookmarks.some((b) => b.name === previous))
|
|
4146
|
+
$("bookmark-list").value = previous;
|
|
4147
|
+
for (const id of [
|
|
4148
|
+
"bookmark-restore",
|
|
4149
|
+
"bookmark-rename",
|
|
4150
|
+
"bookmark-delete",
|
|
4151
|
+
"bookmark-list"
|
|
4152
|
+
])
|
|
4153
|
+
$(id).disabled = !bookmarks.length;
|
|
4154
|
+
$("bookmark-save").disabled = !v || bookmarks.length >= 32;
|
|
4155
|
+
}
|
|
4156
|
+
function sync() {
|
|
4157
|
+
if (disposed) return;
|
|
4158
|
+
const { selectedId, busy } = getState(), v = getViewer(), m = v?._models.get(selectedId);
|
|
4159
|
+
if (selectedId !== selected) {
|
|
4160
|
+
measurement?.abort();
|
|
4161
|
+
selected = selectedId;
|
|
4162
|
+
}
|
|
4163
|
+
draftKind = null;
|
|
4164
|
+
base = m?.geometry ? v.get_model_size(selectedId) : null;
|
|
4165
|
+
$("size-fields").disabled = busy || !base;
|
|
4166
|
+
$("duplicate-model").disabled = busy || !base;
|
|
4167
|
+
$("display-units").value = v?.display_units || "mm";
|
|
4168
|
+
$("size-unit-label").textContent = factor() === 1 ? "Dimensions in mm" : "Dimensions in inches";
|
|
4169
|
+
$("file-units").value = base?.file_units || "mm";
|
|
4170
|
+
$("file-units").disabled = busy || !base?.editable;
|
|
4171
|
+
$("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.";
|
|
4172
|
+
axes2.forEach((a, i) => {
|
|
4173
|
+
$("size-" + a).value = base ? value(base.dimensions_mm[i] / factor()) : "";
|
|
4174
|
+
$("size-" + a).disabled = !base || base.dimensions_mm[i] === 0;
|
|
4175
|
+
});
|
|
4176
|
+
const ratios = base?.baseline_mm.map((v2, i) => v2 ? base.dimensions_mm[i] / v2 : null).filter((v2) => v2 != null) || [];
|
|
4177
|
+
$("size-percent").value = ratios.length && ratios.every((v2) => Math.abs(v2 / ratios[0] - 1) < 1e-8) ? value(ratios[0] * 100) : "";
|
|
4178
|
+
error("size-error");
|
|
4179
|
+
syncInfo();
|
|
4180
|
+
syncBookmarks();
|
|
4181
|
+
}
|
|
4182
|
+
const attempt = (id, action) => {
|
|
4183
|
+
try {
|
|
4184
|
+
action();
|
|
4185
|
+
error(id);
|
|
4186
|
+
} catch (e) {
|
|
4187
|
+
error(id, e.message);
|
|
4188
|
+
}
|
|
4189
|
+
};
|
|
4190
|
+
axes2.forEach(
|
|
4191
|
+
(a, i) => listen($("size-" + a), "input", () => {
|
|
4192
|
+
if (!base) return;
|
|
4193
|
+
draftKind = "axis";
|
|
4194
|
+
lastAxis = i;
|
|
4195
|
+
const n = $("size-" + a).valueAsNumber;
|
|
4196
|
+
if (!Number.isFinite(n) || n <= 0) return;
|
|
4197
|
+
if ($("size-lock").checked)
|
|
4198
|
+
axes2.forEach((axis, j) => {
|
|
4199
|
+
if (i !== j)
|
|
4200
|
+
$("size-" + axis).value = value(
|
|
4201
|
+
n * base.dimensions_mm[j] / base.dimensions_mm[i]
|
|
4202
|
+
);
|
|
4203
|
+
});
|
|
4204
|
+
$("size-percent").value = "";
|
|
4205
|
+
})
|
|
4206
|
+
);
|
|
4207
|
+
listen($("size-percent"), "input", () => {
|
|
4208
|
+
draftKind = "percent";
|
|
4209
|
+
const n = $("size-percent").valueAsNumber;
|
|
4210
|
+
if (base && Number.isFinite(n) && n > 0)
|
|
4211
|
+
axes2.forEach(
|
|
4212
|
+
(a, i) => $("size-" + a).value = value(
|
|
4213
|
+
base.baseline_mm[i] * n / 100 / factor()
|
|
4214
|
+
)
|
|
4215
|
+
);
|
|
4216
|
+
});
|
|
4217
|
+
listen(
|
|
4218
|
+
$("size-apply"),
|
|
4219
|
+
"click",
|
|
4220
|
+
() => attempt("size-error", () => {
|
|
4221
|
+
if (!base) return;
|
|
4222
|
+
if (!draftKind) return;
|
|
4223
|
+
const current = getViewer().get_model_size(selected).dimensions_mm;
|
|
4224
|
+
let desired = axes2.map((a) => $("size-" + a).valueAsNumber * factor());
|
|
4225
|
+
if (draftKind === "percent") {
|
|
4226
|
+
const percent = $("size-percent").valueAsNumber;
|
|
4227
|
+
if (!Number.isFinite(percent) || percent <= 0)
|
|
4228
|
+
throw new Error("Enter a positive scale percentage.");
|
|
4229
|
+
desired = base.baseline_mm.map((v) => v * percent / 100);
|
|
4230
|
+
} else if ($("size-lock").checked) {
|
|
4231
|
+
const ratio = desired[lastAxis] / base.dimensions_mm[lastAxis];
|
|
4232
|
+
desired = base.dimensions_mm.map((v) => v * ratio);
|
|
4233
|
+
}
|
|
4234
|
+
let ratios = desired.map((n, i) => {
|
|
4235
|
+
if (!Number.isFinite(n) || n < 0 || current[i] > 0 && n === 0)
|
|
4236
|
+
throw new Error("Enter positive dimensions for each non-flat axis.");
|
|
4237
|
+
return current[i] ? n / current[i] : 1;
|
|
4238
|
+
});
|
|
4239
|
+
if (ratios.every((v) => Math.abs(v / ratios[0] - 1) < 1e-12))
|
|
4240
|
+
ratios = ratios.map(() => ratios[0]);
|
|
4241
|
+
changeSize(() => getViewer().resize_model(selected, ratios));
|
|
4242
|
+
announce("Model size updated.");
|
|
4243
|
+
})
|
|
4244
|
+
);
|
|
4245
|
+
listen($("size-cancel"), "click", sync);
|
|
4246
|
+
listen(
|
|
4247
|
+
$("size-reset"),
|
|
4248
|
+
"click",
|
|
4249
|
+
() => attempt("size-error", () => {
|
|
4250
|
+
changeSize(() => getViewer().reset_model_size(selected));
|
|
4251
|
+
announce("Original size restored; file-unit correction retained.");
|
|
4252
|
+
})
|
|
4253
|
+
);
|
|
4254
|
+
listen(
|
|
4255
|
+
$("file-units"),
|
|
4256
|
+
"change",
|
|
4257
|
+
() => attempt("size-error", () => {
|
|
4258
|
+
changeSize(
|
|
4259
|
+
() => getViewer().set_file_units(selected, $("file-units").value)
|
|
4260
|
+
);
|
|
4261
|
+
announce("File units corrected.");
|
|
4262
|
+
})
|
|
4263
|
+
);
|
|
4264
|
+
listen($("display-units"), "change", () => {
|
|
4265
|
+
getViewer()?.set_display_units($("display-units").value);
|
|
4266
|
+
refresh();
|
|
4267
|
+
});
|
|
4268
|
+
listen($("duplicate-model"), "click", () => attempt("size-error", duplicate));
|
|
4269
|
+
listen(
|
|
4270
|
+
$("camera-preset"),
|
|
4271
|
+
"change",
|
|
4272
|
+
() => attempt("camera-error", () => {
|
|
4273
|
+
if ($("camera-preset").value)
|
|
4274
|
+
getViewer().set_camera_view($("camera-preset").value);
|
|
4275
|
+
announce("Camera view updated.");
|
|
4276
|
+
$("camera-preset").value = "";
|
|
4277
|
+
})
|
|
4278
|
+
);
|
|
4279
|
+
const editBookmarks = (action) => attempt("camera-error", () => {
|
|
4280
|
+
const v = getViewer(), bookmarks = v.get_camera_bookmarks();
|
|
4281
|
+
action(bookmarks, v);
|
|
4282
|
+
v.set_camera_bookmarks(bookmarks);
|
|
4283
|
+
syncBookmarks();
|
|
4284
|
+
});
|
|
4285
|
+
listen(
|
|
4286
|
+
$("bookmark-save"),
|
|
4287
|
+
"click",
|
|
4288
|
+
() => editBookmarks((b, v) => {
|
|
4289
|
+
b.push({ name: $("bookmark-name").value, camera: v.get_camera_state() });
|
|
4290
|
+
})
|
|
4291
|
+
);
|
|
4292
|
+
listen(
|
|
4293
|
+
$("bookmark-rename"),
|
|
4294
|
+
"click",
|
|
4295
|
+
() => editBookmarks((b) => {
|
|
4296
|
+
b.find((b2) => b2.name === $("bookmark-list").value).name = $("bookmark-name").value;
|
|
4297
|
+
})
|
|
4298
|
+
);
|
|
4299
|
+
listen(
|
|
4300
|
+
$("bookmark-delete"),
|
|
4301
|
+
"click",
|
|
4302
|
+
() => editBookmarks((b) => {
|
|
4303
|
+
b.splice(
|
|
4304
|
+
b.findIndex((b2) => b2.name === $("bookmark-list").value),
|
|
4305
|
+
1
|
|
4306
|
+
);
|
|
4307
|
+
})
|
|
4308
|
+
);
|
|
4309
|
+
listen(
|
|
4310
|
+
$("bookmark-restore"),
|
|
4311
|
+
"click",
|
|
4312
|
+
() => attempt("camera-error", () => {
|
|
4313
|
+
const b = getViewer().get_camera_bookmarks().find((b2) => b2.name === $("bookmark-list").value);
|
|
4314
|
+
if (b) {
|
|
4315
|
+
getViewer().set_camera_state(b.camera);
|
|
4316
|
+
announce("Saved view restored.");
|
|
4317
|
+
}
|
|
4318
|
+
})
|
|
4319
|
+
);
|
|
4320
|
+
listen(
|
|
4321
|
+
$("bookmark-list"),
|
|
4322
|
+
"change",
|
|
4323
|
+
() => $("bookmark-name").value = $("bookmark-list").value
|
|
4324
|
+
);
|
|
4325
|
+
listen($("measure-cancel"), "click", () => measurement?.abort());
|
|
4326
|
+
listen($("measure-model"), "click", async () => {
|
|
4327
|
+
const id = selected, v = getViewer(), controller = new window2.AbortController();
|
|
4328
|
+
measurement = controller;
|
|
4329
|
+
syncInfo();
|
|
4330
|
+
$("measure-status").textContent = "Calculating area and checking volume\u2026";
|
|
4331
|
+
try {
|
|
4332
|
+
await v.measure_model(id, {
|
|
4333
|
+
signal: controller.signal,
|
|
4334
|
+
on_progress: (p) => {
|
|
4335
|
+
if (selected === id && !disposed)
|
|
4336
|
+
$("measure-status").textContent = p.phase + "\u2026";
|
|
4337
|
+
}
|
|
4338
|
+
});
|
|
4339
|
+
if (!disposed && selected === id) announce("Measurements ready.");
|
|
4340
|
+
} catch (e) {
|
|
4341
|
+
if (!disposed && selected === id)
|
|
4342
|
+
$("measure-status").textContent = e.name === "AbortError" ? "Calculation cancelled." : e.message;
|
|
4343
|
+
} finally {
|
|
4344
|
+
const message = $("measure-status").textContent;
|
|
4345
|
+
if (measurement === controller) measurement = null;
|
|
4346
|
+
if (!disposed) {
|
|
4347
|
+
syncInfo();
|
|
4348
|
+
if (!v._models.get(id)?._measurements && selected === id)
|
|
4349
|
+
$("measure-status").textContent = message;
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
});
|
|
4353
|
+
return {
|
|
4354
|
+
sync,
|
|
4355
|
+
syncInfo,
|
|
4356
|
+
dispose() {
|
|
4357
|
+
disposed = true;
|
|
4358
|
+
measurement?.abort();
|
|
4359
|
+
}
|
|
4360
|
+
};
|
|
4361
|
+
}
|
|
4362
|
+
|
|
3668
4363
|
// src/memory-policy.js
|
|
3669
4364
|
var GiB = 1024 ** 3;
|
|
3670
4365
|
var MiB = 1024 ** 2;
|
|
@@ -3696,9 +4391,9 @@ async function estimateFile(file) {
|
|
|
3696
4391
|
}
|
|
3697
4392
|
|
|
3698
4393
|
// src/model-transform.js
|
|
3699
|
-
var
|
|
3700
|
-
var vector2 = (p) =>
|
|
3701
|
-
var object2 = (p) => Object.fromEntries(
|
|
4394
|
+
var axes3 = ["x", "y", "z"];
|
|
4395
|
+
var vector2 = (p) => axes3.map((a) => p[a]);
|
|
4396
|
+
var object2 = (p) => Object.fromEntries(axes3.map((a, i) => [a, p[i]]));
|
|
3702
4397
|
var dot2 = (a, b) => a.reduce((sum, v, i) => sum + v * b[i], 0);
|
|
3703
4398
|
var norm2 = (a) => {
|
|
3704
4399
|
const n = Math.hypot(...a) || 1;
|
|
@@ -3730,7 +4425,7 @@ function rotationMatrix(degrees = { x: 0, y: 0, z: 0 }) {
|
|
|
3730
4425
|
var centerOf = (bounds) => bounds.min.map((v, i) => (v + bounds.max[i]) / 2);
|
|
3731
4426
|
function modelCenter(model) {
|
|
3732
4427
|
return object2(
|
|
3733
|
-
centerOf(model.bounds).map((v, i) => v + model.position[
|
|
4428
|
+
centerOf(model.bounds).map((v, i) => v + model.position[axes3[i]])
|
|
3734
4429
|
);
|
|
3735
4430
|
}
|
|
3736
4431
|
function modelRadius(model) {
|
|
@@ -3741,7 +4436,7 @@ function modelRadius(model) {
|
|
|
3741
4436
|
}
|
|
3742
4437
|
function enginePosition(model) {
|
|
3743
4438
|
const c = centerOf(model.bounds), rotated = transform2(rotationMatrix(model.rotation), c);
|
|
3744
|
-
return object2(c.map((v, i) => model.position[
|
|
4439
|
+
return object2(c.map((v, i) => model.position[axes3[i]] + v - rotated[i]));
|
|
3745
4440
|
}
|
|
3746
4441
|
function modelBounds(model) {
|
|
3747
4442
|
if (!model.rotation || !Object.values(model.rotation).some((v) => v % 360))
|
|
@@ -4082,16 +4777,16 @@ function bindAppExport({
|
|
|
4082
4777
|
$("save-error").textContent = "";
|
|
4083
4778
|
};
|
|
4084
4779
|
function formatChanged() {
|
|
4085
|
-
const
|
|
4780
|
+
const format2 = $("save-format").value;
|
|
4086
4781
|
if (!$("save-name").value || $("save-name").value === lastSuggestedName) {
|
|
4087
4782
|
suggestedName();
|
|
4088
4783
|
}
|
|
4089
4784
|
if ($("save-mesh-note")) $("save-mesh-note").hidden = isScene();
|
|
4090
4785
|
if ($("save-scene-note")) $("save-scene-note").hidden = !isScene();
|
|
4091
|
-
$("save-colors-row").hidden = !["obj", "3mf"].includes(
|
|
4092
|
-
$("save-cad-note").hidden = !["step", "iges"].includes(
|
|
4786
|
+
$("save-colors-row").hidden = !["obj", "3mf"].includes(format2);
|
|
4787
|
+
$("save-cad-note").hidden = !["step", "iges"].includes(format2);
|
|
4093
4788
|
$("save-format-note").textContent = $("save-format").selectedOptions[0]?.dataset.note || "";
|
|
4094
|
-
$("save-submit").textContent =
|
|
4789
|
+
$("save-submit").textContent = format2 === "obj" && $("save-colors").checked ? "Download ZIP" : "Download file";
|
|
4095
4790
|
sync();
|
|
4096
4791
|
}
|
|
4097
4792
|
function suggestedName() {
|
|
@@ -4542,7 +5237,7 @@ var animationControls = [
|
|
|
4542
5237
|
"animation-status",
|
|
4543
5238
|
"animation-error"
|
|
4544
5239
|
];
|
|
4545
|
-
var
|
|
5240
|
+
var axes4 = ["x", "y", "z"];
|
|
4546
5241
|
function syncAnimatedRecord(record, model) {
|
|
4547
5242
|
if (!record || !model?.geometry) return;
|
|
4548
5243
|
record.bounds = Object.fromEntries(
|
|
@@ -4554,11 +5249,11 @@ function syncAnimatedRecord(record, model) {
|
|
|
4554
5249
|
])
|
|
4555
5250
|
);
|
|
4556
5251
|
record.rotation = Object.fromEntries(
|
|
4557
|
-
|
|
5252
|
+
axes4.map((a) => [a, model["rotation" + a] * 180 / Math.PI % 360])
|
|
4558
5253
|
);
|
|
4559
5254
|
record.position = { x: 0, y: 0, z: 0 };
|
|
4560
5255
|
const offset = enginePosition(record);
|
|
4561
|
-
for (const a of
|
|
5256
|
+
for (const a of axes4) record.position[a] = model[a] - offset[a];
|
|
4562
5257
|
}
|
|
4563
5258
|
function centerAnimationPivot(record, model) {
|
|
4564
5259
|
syncAnimatedRecord(record, model);
|
|
@@ -4571,10 +5266,10 @@ function centerAnimationPivot(record, model) {
|
|
|
4571
5266
|
const rotation2 = rotationMatrix(record.rotation);
|
|
4572
5267
|
const original = record.originalPosition || { x: 0, y: 0, z: 0 };
|
|
4573
5268
|
record.originalPosition = Object.fromEntries(
|
|
4574
|
-
|
|
5269
|
+
axes4.map((a, i) => [a, original[a] + delta[i]])
|
|
4575
5270
|
);
|
|
4576
5271
|
for (let i = 0; i < 3; i++)
|
|
4577
|
-
model[
|
|
5272
|
+
model[axes4[i]] += delta.reduce(
|
|
4578
5273
|
(sum, v, j) => sum + rotation2[i * 3 + j] * v,
|
|
4579
5274
|
0
|
|
4580
5275
|
);
|
|
@@ -4583,7 +5278,7 @@ function centerAnimationPivot(record, model) {
|
|
|
4583
5278
|
syncAnimatedRecord(record, model);
|
|
4584
5279
|
}
|
|
4585
5280
|
function spinDefinition(axis, seconds, direction) {
|
|
4586
|
-
if (!
|
|
5281
|
+
if (!axes4.includes(axis) || !Number.isFinite(seconds) || seconds < 0.1 || seconds > 3600 || !["1", "-1"].includes(direction))
|
|
4587
5282
|
throw new Error("Choose an axis and a duration from 0.1 to 3,600 seconds.");
|
|
4588
5283
|
return {
|
|
4589
5284
|
delta: {
|
|
@@ -4840,6 +5535,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4840
5535
|
missing.push(...printControls.filter((id) => !$(id)));
|
|
4841
5536
|
if (animationControls.some((id) => $(id)))
|
|
4842
5537
|
missing.push(...animationControls.filter((id) => !$(id)));
|
|
5538
|
+
if (toolsControls.some((id) => $(id)))
|
|
5539
|
+
missing.push(...toolsControls.filter((id) => !$(id)));
|
|
4843
5540
|
if (missing.length)
|
|
4844
5541
|
throw new Error("Missing ViewSTL controls: " + missing.join(", "));
|
|
4845
5542
|
const abort = new window2.AbortController();
|
|
@@ -4868,8 +5565,9 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4868
5565
|
let exportUI;
|
|
4869
5566
|
let printUI;
|
|
4870
5567
|
let animationUI;
|
|
5568
|
+
let toolsUI;
|
|
4871
5569
|
let sceneImport;
|
|
4872
|
-
let draining = false, busy = false, gridEnabled = false, gridHeight = 0, gridSpacing = 1, moveMode = false, transformTool = "move", modelMover, dragArrangement, background = "transparent";
|
|
5570
|
+
let draining = false, busy = false, gridEnabled = false, gridHeight = 0, gridSpacing = 1, moveMode = false, selectionMode = false, selectionPointer = null, transformTool = "move", modelMover, dragArrangement, background = "transparent";
|
|
4873
5571
|
const viewNames = {
|
|
4874
5572
|
flat: "Solid",
|
|
4875
5573
|
edges: "Solid + edges",
|
|
@@ -4880,6 +5578,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4880
5578
|
orientation: "Face orientation"
|
|
4881
5579
|
};
|
|
4882
5580
|
const modeInputs = [...root.querySelectorAll('[name="view-mode"]')];
|
|
5581
|
+
const shortcutButtons = [...root.querySelectorAll("[data-viewstl-shortcut]")];
|
|
4883
5582
|
if (options.id_prefix)
|
|
4884
5583
|
modeInputs.forEach(
|
|
4885
5584
|
(input) => input.name = options.id_prefix + "view-mode"
|
|
@@ -4899,7 +5598,44 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4899
5598
|
}
|
|
4900
5599
|
function syncSelection() {
|
|
4901
5600
|
viewer?.set_selection(
|
|
4902
|
-
!busy && records.has(modelID) && (moveMode || !$("scene-panel").hidden) ? modelID : null
|
|
5601
|
+
!busy && records.has(modelID) && (selectionMode || moveMode || !$("scene-panel").hidden) ? modelID : null
|
|
5602
|
+
);
|
|
5603
|
+
}
|
|
5604
|
+
function syncShortcuts() {
|
|
5605
|
+
for (const button of shortcutButtons) {
|
|
5606
|
+
const action = button.dataset.viewstlShortcut;
|
|
5607
|
+
button.disabled = busy || !records.size || action === "save" && !exportUI;
|
|
5608
|
+
if (action === "select")
|
|
5609
|
+
button.setAttribute("aria-pressed", String(selectionMode));
|
|
5610
|
+
}
|
|
5611
|
+
}
|
|
5612
|
+
function setSelectionMode(enabled) {
|
|
5613
|
+
if (enabled) setMoveMode(false);
|
|
5614
|
+
selectionMode = !!enabled && !busy && records.size > 0;
|
|
5615
|
+
selectionPointer = null;
|
|
5616
|
+
if (viewer) {
|
|
5617
|
+
viewer.controls.enabled = !selectionMode;
|
|
5618
|
+
viewer.canvas.setAttribute(
|
|
5619
|
+
"aria-label",
|
|
5620
|
+
selectionMode ? "Select a model with a click. Escape returns to viewing." : "3D model viewer. Drag to rotate, shift-drag to pan, scroll to zoom."
|
|
5621
|
+
);
|
|
5622
|
+
}
|
|
5623
|
+
$("workspace").classList.toggle("selecting-model-mode", selectionMode);
|
|
5624
|
+
syncShortcuts();
|
|
5625
|
+
syncSelection();
|
|
5626
|
+
}
|
|
5627
|
+
function editModelAt(event) {
|
|
5628
|
+
const hit = viewer.pick_model(event.clientX, event.clientY);
|
|
5629
|
+
if (!hit || !records.has(hit.id)) return;
|
|
5630
|
+
event.preventDefault();
|
|
5631
|
+
selectModel(hit.id);
|
|
5632
|
+
closePanels();
|
|
5633
|
+
$("scene-panel").hidden = false;
|
|
5634
|
+
$("scene-button").setAttribute("aria-expanded", "true");
|
|
5635
|
+
$("position-details").open = true;
|
|
5636
|
+
syncSelection();
|
|
5637
|
+
announce(
|
|
5638
|
+
records.get(hit.id).name + " selected. Choose Move, Rotate or Depth."
|
|
4903
5639
|
);
|
|
4904
5640
|
}
|
|
4905
5641
|
function closePanel(name) {
|
|
@@ -4916,6 +5652,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4916
5652
|
$(name + "-panel").hidden = !open;
|
|
4917
5653
|
$(name + "-button").setAttribute("aria-expanded", String(open));
|
|
4918
5654
|
syncSelection();
|
|
5655
|
+
if (open && name === "info") populateInfo();
|
|
5656
|
+
if (open && (name === "scene" || name === "view")) toolsUI?.sync();
|
|
4919
5657
|
}
|
|
4920
5658
|
function fileSize(size) {
|
|
4921
5659
|
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`;
|
|
@@ -4942,12 +5680,13 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4942
5680
|
}
|
|
4943
5681
|
function syncControls() {
|
|
4944
5682
|
syncAnimatedRecord(records.get(modelID), viewer?._models.get(modelID));
|
|
4945
|
-
const info = modelID === null ? null : viewer.get_model_info(modelID), mode = info?.display === "smooth" ? "flat" : info?.display || "flat";
|
|
5683
|
+
const info = modelID === null ? null : viewer.get_model_info(modelID, { measurements: false }), mode = info?.display === "smooth" ? "flat" : info?.display || "flat";
|
|
4946
5684
|
for (const input of modeInputs) {
|
|
4947
5685
|
input.checked = input.value === mode;
|
|
4948
5686
|
input.disabled = busy;
|
|
4949
5687
|
}
|
|
4950
|
-
$("view-label").textContent = viewNames[mode];
|
|
5688
|
+
$("view-label").textContent = toolsUI ? "View" : viewNames[mode];
|
|
5689
|
+
$("view-button").title = "View \xB7 " + viewNames[mode];
|
|
4951
5690
|
$("smooth-shading").checked = info?.smooth_shading || false;
|
|
4952
5691
|
$("smooth-shading").disabled = busy || mode === "wireframe";
|
|
4953
5692
|
$("view-status").textContent = mode === "wireframe" ? "Smooth shading is available in surface views." : "Shading changes the appearance, not the geometry.";
|
|
@@ -4971,14 +5710,18 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4971
5710
|
exportUI?.sync();
|
|
4972
5711
|
printUI?.sync();
|
|
4973
5712
|
animationUI?.sync();
|
|
5713
|
+
toolsUI?.sync();
|
|
5714
|
+
syncShortcuts();
|
|
4974
5715
|
}
|
|
4975
5716
|
function populateInfo() {
|
|
4976
5717
|
if (modelID === null) return;
|
|
4977
|
-
const info = viewer.get_model_info(modelID), record = records.get(modelID),
|
|
5718
|
+
const info = viewer.get_model_info(modelID, { measurements: false }), record = records.get(modelID), factor = viewer.display_units === "inch" ? 25.4 : 1;
|
|
4978
5719
|
$("info-triangles").textContent = info.triangles.toLocaleString();
|
|
4979
|
-
$("info-dimensions").textContent = [info.dims.x, info.dims.y, info.dims.z].map((x) => formatNumber.format(x)).join(" \xD7 ") +
|
|
5720
|
+
$("info-dimensions").textContent = [info.dims.x, info.dims.y, info.dims.z].map((x) => formatNumber.format(x / factor)).join(" \xD7 ") + (factor === 1 ? " mm" : " in");
|
|
4980
5721
|
$("info-size").textContent = fileSize(record.size);
|
|
4981
|
-
|
|
5722
|
+
const size = viewer.get_model_size(modelID);
|
|
5723
|
+
$("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.";
|
|
5724
|
+
toolsUI?.syncInfo();
|
|
4982
5725
|
}
|
|
4983
5726
|
function renderList() {
|
|
4984
5727
|
$("model-list").replaceChildren(
|
|
@@ -5009,6 +5752,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5009
5752
|
}
|
|
5010
5753
|
function renderScene() {
|
|
5011
5754
|
const has = records.size > 0;
|
|
5755
|
+
if (!has && selectionMode) setSelectionMode(false);
|
|
5012
5756
|
$("workspace").classList.toggle("has-models", has);
|
|
5013
5757
|
$("viewer").hidden = !has && !busy;
|
|
5014
5758
|
$("empty-state").hidden = has || busy;
|
|
@@ -5027,6 +5771,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5027
5771
|
function setBusy(value) {
|
|
5028
5772
|
if (value) exportUI?.close();
|
|
5029
5773
|
if (disposed) return;
|
|
5774
|
+
if (value && selectionMode) setSelectionMode(false);
|
|
5030
5775
|
if (value) setMoveMode(false);
|
|
5031
5776
|
busy = value;
|
|
5032
5777
|
$("loading-state").hidden = !value;
|
|
@@ -5080,7 +5825,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5080
5825
|
input.removeAttribute("aria-invalid");
|
|
5081
5826
|
}
|
|
5082
5827
|
$("position-error").hidden = true;
|
|
5083
|
-
$("position-units").textContent = record?.animationPivot ? "Position of the model center in the scene
|
|
5828
|
+
$("position-units").textContent = record?.animationPivot ? "Position of the model center in the scene, in mm." : "Offset from file coordinates, in mm.";
|
|
5084
5829
|
$("move-model-button").disabled = !record || busy;
|
|
5085
5830
|
$("reset-position").disabled = !record;
|
|
5086
5831
|
syncRotation(preserveFocus);
|
|
@@ -5163,11 +5908,12 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5163
5908
|
$("model-tool").value = transformTool;
|
|
5164
5909
|
$("move-gesture-message").textContent = toolHints[transformTool] + " \xB7 Scroll to adjust depth";
|
|
5165
5910
|
}
|
|
5166
|
-
function setMoveMode(enabled, tool = transformTool) {
|
|
5911
|
+
function setMoveMode(enabled, tool = transformTool, retainPaused = false) {
|
|
5912
|
+
if (enabled && selectionMode) setSelectionMode(false);
|
|
5167
5913
|
modelMover?.setEnabled(false);
|
|
5168
5914
|
transformTool = tool;
|
|
5169
5915
|
moveMode = !!enabled && modelID !== null && !busy;
|
|
5170
|
-
if (moveMode) prepareManual(modelID);
|
|
5916
|
+
if (moveMode && !retainPaused) prepareManual(modelID);
|
|
5171
5917
|
modelMover?.setEnabled(moveMode);
|
|
5172
5918
|
syncTool();
|
|
5173
5919
|
$("move-mode").hidden = !moveMode;
|
|
@@ -5176,7 +5922,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5176
5922
|
if (viewer?.canvas)
|
|
5177
5923
|
viewer.canvas.setAttribute(
|
|
5178
5924
|
"aria-label",
|
|
5179
|
-
moveMode ? toolHints[transformTool] + ". Scroll changes only its depth. Escape exits." : "3D model viewer. Drag to rotate, shift-drag to pan, scroll to zoom."
|
|
5925
|
+
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."
|
|
5180
5926
|
);
|
|
5181
5927
|
if (moveMode) {
|
|
5182
5928
|
closePanels();
|
|
@@ -5344,6 +6090,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5344
6090
|
function refreshAnimatedPose() {
|
|
5345
6091
|
if (disposed || modelID === null) return;
|
|
5346
6092
|
syncAnimatedRecord(records.get(modelID), viewer?._models.get(modelID));
|
|
6093
|
+
if (!$("info-panel").hidden) populateInfo();
|
|
5347
6094
|
if (!$("scene-panel").hidden && $("position-details").open)
|
|
5348
6095
|
syncPosition(true);
|
|
5349
6096
|
}
|
|
@@ -5413,20 +6160,39 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5413
6160
|
}
|
|
5414
6161
|
});
|
|
5415
6162
|
listen(viewer.canvas, "dblclick", (event) => {
|
|
5416
|
-
if (busy || event.button !== 0) return;
|
|
5417
|
-
|
|
5418
|
-
|
|
6163
|
+
if (busy || selectionMode || event.button !== 0) return;
|
|
6164
|
+
editModelAt(event);
|
|
6165
|
+
});
|
|
6166
|
+
listen(viewer.canvas, "pointerdown", (event) => {
|
|
6167
|
+
if (!selectionMode || busy || event.button !== 0 || !event.isPrimary)
|
|
6168
|
+
return;
|
|
6169
|
+
selectionPointer = {
|
|
6170
|
+
id: event.pointerId,
|
|
6171
|
+
x: event.clientX,
|
|
6172
|
+
y: event.clientY,
|
|
6173
|
+
moved: false
|
|
6174
|
+
};
|
|
6175
|
+
viewer.canvas.focus({ preventScroll: true });
|
|
6176
|
+
viewer.canvas.setPointerCapture(event.pointerId);
|
|
5419
6177
|
event.preventDefault();
|
|
5420
|
-
selectModel(hit.id);
|
|
5421
|
-
closePanels();
|
|
5422
|
-
$("scene-panel").hidden = false;
|
|
5423
|
-
$("scene-button").setAttribute("aria-expanded", "true");
|
|
5424
|
-
$("position-details").open = true;
|
|
5425
|
-
syncSelection();
|
|
5426
|
-
announce(
|
|
5427
|
-
records.get(hit.id).name + " selected. Choose Move, Rotate or Depth."
|
|
5428
|
-
);
|
|
5429
6178
|
});
|
|
6179
|
+
listen(viewer.canvas, "pointermove", (event) => {
|
|
6180
|
+
if (selectionPointer?.id === event.pointerId && Math.hypot(
|
|
6181
|
+
event.clientX - selectionPointer.x,
|
|
6182
|
+
event.clientY - selectionPointer.y
|
|
6183
|
+
) > 6)
|
|
6184
|
+
selectionPointer.moved = true;
|
|
6185
|
+
});
|
|
6186
|
+
listen(viewer.canvas, "pointerup", (event) => {
|
|
6187
|
+
const pointer = selectionPointer;
|
|
6188
|
+
selectionPointer = null;
|
|
6189
|
+
if (selectionMode && !busy && pointer?.id === event.pointerId && !pointer.moved)
|
|
6190
|
+
editModelAt(event);
|
|
6191
|
+
});
|
|
6192
|
+
for (const event of ["pointercancel", "lostpointercapture"])
|
|
6193
|
+
listen(viewer.canvas, event, () => {
|
|
6194
|
+
selectionPointer = null;
|
|
6195
|
+
});
|
|
5430
6196
|
return viewer;
|
|
5431
6197
|
}).catch((error) => {
|
|
5432
6198
|
modelMover?.dispose();
|
|
@@ -5468,7 +6234,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5468
6234
|
if (queue.length && !draining) drainPromise = drainQueue();
|
|
5469
6235
|
return drainPromise || Promise.resolve();
|
|
5470
6236
|
}
|
|
5471
|
-
function openURL(value,
|
|
6237
|
+
function openURL(value, format2 = "") {
|
|
5472
6238
|
assertActive();
|
|
5473
6239
|
if (!options.url_import_endpoint)
|
|
5474
6240
|
throw new Error("URL imports are not configured.");
|
|
@@ -5477,7 +6243,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5477
6243
|
throw new Error(
|
|
5478
6244
|
"Use a public HTTP or HTTPS file URL without login credentials."
|
|
5479
6245
|
);
|
|
5480
|
-
if (
|
|
6246
|
+
if (format2 && !/^(stl|obj|3mf|step|stp|iges|igs|vsb|vsj)$/.test(format2))
|
|
5481
6247
|
throw new Error("Choose a supported 3D format.");
|
|
5482
6248
|
url.hash = "";
|
|
5483
6249
|
const name = decodeURIComponent(
|
|
@@ -5486,9 +6252,9 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5486
6252
|
queue.push({
|
|
5487
6253
|
name,
|
|
5488
6254
|
remote: url.href,
|
|
5489
|
-
format,
|
|
6255
|
+
format: format2,
|
|
5490
6256
|
size: 0,
|
|
5491
|
-
scene: ["vsb", "vsj"].includes(
|
|
6257
|
+
scene: ["vsb", "vsj"].includes(format2) ? format2 : sceneFormat(name)
|
|
5492
6258
|
});
|
|
5493
6259
|
if (!draining) drainPromise = drainQueue();
|
|
5494
6260
|
return drainPromise || Promise.resolve();
|
|
@@ -5557,7 +6323,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5557
6323
|
await engine.when_idle();
|
|
5558
6324
|
if (token !== generation) continue;
|
|
5559
6325
|
loadingID = null;
|
|
5560
|
-
const loaded = lastLoadedID === id ? engine.get_model_info(id) : null;
|
|
6326
|
+
const loaded = lastLoadedID === id ? engine.get_model_info(id, { measurements: false }) : null;
|
|
5561
6327
|
if (!loaded) {
|
|
5562
6328
|
announce(source.name + " could not be opened.");
|
|
5563
6329
|
continue;
|
|
@@ -5649,7 +6415,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5649
6415
|
);
|
|
5650
6416
|
const record = {
|
|
5651
6417
|
id,
|
|
5652
|
-
name: String(m.orig_filename || m.filename).split(/[\\/]/).pop().split(/[?#]/)[0],
|
|
6418
|
+
name: String(m.display_name || m.orig_filename || m.filename).split(/[\\/]/).pop().split(/[?#]/)[0],
|
|
5653
6419
|
size: g.file_bytes ?? m.local_file?.size ?? null,
|
|
5654
6420
|
units: g.units || m.units,
|
|
5655
6421
|
bounds,
|
|
@@ -5691,6 +6457,39 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5691
6457
|
}
|
|
5692
6458
|
}
|
|
5693
6459
|
root.querySelectorAll("[data-open-file]").forEach((b) => listen(b, "click", () => $("file-input").click()));
|
|
6460
|
+
for (const button of shortcutButtons)
|
|
6461
|
+
listen(button, "click", () => {
|
|
6462
|
+
if (button.disabled || busy || !records.size) return;
|
|
6463
|
+
const action = button.dataset.viewstlShortcut;
|
|
6464
|
+
if (action === "save") $("save-button")?.click();
|
|
6465
|
+
else if (action === "select") {
|
|
6466
|
+
setSelectionMode(!selectionMode);
|
|
6467
|
+
closePanels();
|
|
6468
|
+
announce(
|
|
6469
|
+
selectionMode ? "Selection mode. Click a model; Escape returns to viewing." : "Viewing mode. Drag to rotate the scene."
|
|
6470
|
+
);
|
|
6471
|
+
} else if ([
|
|
6472
|
+
"front",
|
|
6473
|
+
"back",
|
|
6474
|
+
"left",
|
|
6475
|
+
"right",
|
|
6476
|
+
"top",
|
|
6477
|
+
"bottom",
|
|
6478
|
+
"isometric"
|
|
6479
|
+
].includes(action)) {
|
|
6480
|
+
setMoveMode(false);
|
|
6481
|
+
setSelectionMode(false);
|
|
6482
|
+
viewer.set_camera_view(action);
|
|
6483
|
+
toolsUI?.sync();
|
|
6484
|
+
announce(button.title || action + " view");
|
|
6485
|
+
}
|
|
6486
|
+
});
|
|
6487
|
+
listen(window2, "resize", () => {
|
|
6488
|
+
if (selectionMode && !shortcutButtons.some(
|
|
6489
|
+
(button) => button.dataset.viewstlShortcut === "select" && button.getClientRects().length
|
|
6490
|
+
))
|
|
6491
|
+
setSelectionMode(false);
|
|
6492
|
+
});
|
|
5694
6493
|
listen($("file-input"), "change", () => {
|
|
5695
6494
|
const files = [...$("file-input").files];
|
|
5696
6495
|
$("file-input").value = "";
|
|
@@ -5902,6 +6701,12 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5902
6701
|
listen(root, "keydown", (event) => {
|
|
5903
6702
|
if (event.key !== "Escape") return;
|
|
5904
6703
|
if ($("save-dialog")?.open) return;
|
|
6704
|
+
if (selectionMode) {
|
|
6705
|
+
setSelectionMode(false);
|
|
6706
|
+
closePanels();
|
|
6707
|
+
announce("Viewing mode. Drag to rotate the scene.");
|
|
6708
|
+
return;
|
|
6709
|
+
}
|
|
5905
6710
|
if (moveMode) {
|
|
5906
6711
|
setMoveMode(false);
|
|
5907
6712
|
announce("Move tool closed.");
|
|
@@ -5978,6 +6783,44 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5978
6783
|
refreshPose: refreshAnimatedPose,
|
|
5979
6784
|
announce
|
|
5980
6785
|
});
|
|
6786
|
+
if ($("size-details"))
|
|
6787
|
+
toolsUI = bindAppTools({
|
|
6788
|
+
$,
|
|
6789
|
+
listen,
|
|
6790
|
+
window: window2,
|
|
6791
|
+
getViewer: () => viewer,
|
|
6792
|
+
getState: () => ({ selectedId: modelID, busy }),
|
|
6793
|
+
refresh: renderScene,
|
|
6794
|
+
announce,
|
|
6795
|
+
changeSize(operation) {
|
|
6796
|
+
setMoveMode(false);
|
|
6797
|
+
const record = records.get(modelID);
|
|
6798
|
+
syncAnimatedRecord(record, viewer._models.get(modelID));
|
|
6799
|
+
operation();
|
|
6800
|
+
syncAnimatedRecord(record, viewer._models.get(modelID));
|
|
6801
|
+
$("scene-arrangement").value = "custom";
|
|
6802
|
+
renderScene();
|
|
6803
|
+
},
|
|
6804
|
+
duplicate() {
|
|
6805
|
+
const original = records.get(modelID), id = viewer.duplicate_model(modelID), m = viewer._models.get(id);
|
|
6806
|
+
const record = {
|
|
6807
|
+
...original,
|
|
6808
|
+
id,
|
|
6809
|
+
name: m.display_name,
|
|
6810
|
+
position: { ...original.position },
|
|
6811
|
+
rotation: { ...original.rotation }
|
|
6812
|
+
};
|
|
6813
|
+
syncAnimatedRecord(record, m);
|
|
6814
|
+
record.originalPosition = { ...record.position };
|
|
6815
|
+
record.originalRotation = { ...record.rotation };
|
|
6816
|
+
records.set(id, record);
|
|
6817
|
+
nextID = Math.max(nextID, id + 1);
|
|
6818
|
+
$("scene-arrangement").value = "custom";
|
|
6819
|
+
selectModel(id);
|
|
6820
|
+
setMoveMode(true, "move", true);
|
|
6821
|
+
announce("Copy selected at the same position. Drag to move it.");
|
|
6822
|
+
}
|
|
6823
|
+
});
|
|
5981
6824
|
updateGrid();
|
|
5982
6825
|
setBusy(false);
|
|
5983
6826
|
setMoveMode(false);
|
|
@@ -6026,6 +6869,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
6026
6869
|
exportUI?.dispose();
|
|
6027
6870
|
printUI?.dispose();
|
|
6028
6871
|
animationUI?.dispose();
|
|
6872
|
+
toolsUI?.dispose();
|
|
6029
6873
|
cancelLoading();
|
|
6030
6874
|
setMoveMode(false);
|
|
6031
6875
|
appearanceRequest++;
|