@omrips/viewstl 2.0.0-rc.4 → 2.0.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -173
- package/build/chunks/archive-QK7XZSVT.js +1 -0
- package/build/{v2/chunks/cad-VY42CMP4.js → chunks/cad-QGVO6UGR.js} +1 -1
- package/build/chunks/chunk-6AW5U52H.js +1 -0
- package/build/{v2/chunks/chunk-R3HNTB5A.js → chunks/chunk-CVGQTUJ5.js} +2 -2
- package/build/chunks/chunk-FUXP2W57.js +1 -0
- package/build/chunks/chunk-LFTKNL2V.js +1 -0
- package/build/{v2/chunks/chunk-ZMK3UZZE.js → chunks/chunk-T6PPRDOE.js} +1 -1
- package/build/chunks/chunk-V6KYGP62.js +1 -0
- package/build/{v2/chunks/export-7OP4ZGGJ.js → chunks/export-2CFMTPJ3.js} +3 -3
- package/build/{v2/chunks/export-cad-QPBT756V.js → chunks/export-cad-XOXIE726.js} +1 -1
- package/build/{v2/chunks/export-zip-EDCWH5PX.js → chunks/export-zip-I2ADLFJI.js} +1 -1
- package/build/chunks/measure-3LDALPCN.js +1 -0
- package/build/chunks/print-check-WWHTT5PW.js +1 -0
- package/build/{v2/chunks/scene-export-KQBS7VRJ.js → chunks/scene-export-FPPL466I.js} +1 -1
- package/build/chunks/scene-reader-WXTRELY3.js +1 -0
- package/build/{v2/node → node}/analysis-worker.js +13 -12
- package/build/{v2/node → node}/index.cjs +7 -7
- package/build/{v2/node → node}/index.js +6 -6
- package/build/stl_viewer.min.js +51 -0
- package/build/{v2/viewstl.cjs → viewstl.cjs} +1641 -272
- package/build/{v2/viewstl.d.cts → viewstl.d.cts} +98 -0
- package/build/{v2/viewstl.d.ts → viewstl.d.ts} +98 -0
- package/build/{v2/viewstl.js → viewstl.js} +1641 -272
- package/build/viewstl.min.js +51 -0
- package/build/viewstl.worker.js +1 -0
- package/docs/releases/2.0.0-rc.6.md +7 -0
- package/docs/v2-compatibility.md +5 -5
- package/docs/v2-usage.md +152 -21
- package/docs/v2-verification.md +169 -0
- package/legacy/README.md +14 -8
- package/package.json +18 -35
- package/build/v2/README.md +0 -15
- package/build/v2/chunks/archive-VGS4J4Z7.js +0 -1
- package/build/v2/chunks/chunk-JH3H3JWA.js +0 -1
- package/build/v2/chunks/chunk-U33KANV7.js +0 -1
- package/build/v2/chunks/print-check-2PU7CSL3.js +0 -1
- package/build/v2/chunks/scene-reader-APUJOPUR.js +0 -1
- package/build/v2/stl_viewer.min.js +0 -51
- package/build/v2/viewstl.min.js +0 -51
- package/build/v2/viewstl.worker.js +0 -1
- /package/build/{v2/THIRD-PARTY-NOTICES.txt → THIRD-PARTY-NOTICES.txt} +0 -0
- /package/build/{v2/cad → cad}/occt-import-js.js +0 -0
- /package/build/{v2/cad → cad}/occt-import-js.source.txt +0 -0
- /package/build/{v2/cad → cad}/occt-import-js.wasm +0 -0
- /package/build/{v2/cad → cad}/prepare-cad.source.txt +0 -0
- /package/build/{v2/chunks/chunk-JS56KC3I.js → chunks/chunk-W5KVLGVR.js} +0 -0
- /package/build/{v2/chunks → chunks}/chunk-ZSV4PTIA.js +0 -0
- /package/build/{v2/node → node}/index.d.cts +0 -0
- /package/build/{v2/node → node}/index.d.ts +0 -0
|
@@ -1,52 +1,4 @@
|
|
|
1
|
-
// src/
|
|
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
|
-
}
|
|
48
|
-
|
|
49
|
-
// src/v2/math.js
|
|
1
|
+
// src/math.js
|
|
50
2
|
var add = (a, b) => a.map((v, i) => v + b[i]);
|
|
51
3
|
var sub = (a, b) => a.map((v, i) => v - b[i]);
|
|
52
4
|
var mul = (a, s) => a.map((v) => v * s);
|
|
@@ -198,7 +150,178 @@ function rayTriangle(o, d, p, i) {
|
|
|
198
150
|
return t >= 0 ? t : Infinity;
|
|
199
151
|
}
|
|
200
152
|
|
|
201
|
-
// src/
|
|
153
|
+
// src/animation.js
|
|
154
|
+
var animationKeys = [
|
|
155
|
+
"x",
|
|
156
|
+
"y",
|
|
157
|
+
"z",
|
|
158
|
+
"rotationx",
|
|
159
|
+
"rotationy",
|
|
160
|
+
"rotationz",
|
|
161
|
+
"scalex",
|
|
162
|
+
"scaley",
|
|
163
|
+
"scalez"
|
|
164
|
+
];
|
|
165
|
+
var isScale = (key) => key.startsWith("scale");
|
|
166
|
+
var number = (value, name, positive2 = false) => {
|
|
167
|
+
if (typeof value !== "number" || !Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER || positive2 && value <= 0)
|
|
168
|
+
throw new Error(`Invalid animation ${name}`);
|
|
169
|
+
return value;
|
|
170
|
+
};
|
|
171
|
+
var object = (value, name) => {
|
|
172
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
173
|
+
throw new Error(`Invalid animation ${name}`);
|
|
174
|
+
return value;
|
|
175
|
+
};
|
|
176
|
+
function animationPose(model) {
|
|
177
|
+
return Object.fromEntries(
|
|
178
|
+
animationKeys.map((key) => [
|
|
179
|
+
key,
|
|
180
|
+
number(
|
|
181
|
+
model[key] ?? (isScale(key) ? model.scale ?? 1 : 0),
|
|
182
|
+
key,
|
|
183
|
+
isScale(key)
|
|
184
|
+
)
|
|
185
|
+
])
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
function normalizeAnimation(input) {
|
|
189
|
+
object(input, "definition");
|
|
190
|
+
const result = {}, claimed = /* @__PURE__ */ new Set();
|
|
191
|
+
for (const mode of ["delta", "exact"]) {
|
|
192
|
+
if (input[mode] == null) continue;
|
|
193
|
+
const source = object(input[mode], mode), values = {};
|
|
194
|
+
const msec = number(source.msec ?? 300, "duration", true);
|
|
195
|
+
if (source.loop !== void 0 && typeof source.loop !== "boolean")
|
|
196
|
+
throw new Error("Animation loop must be a boolean");
|
|
197
|
+
const loop = mode === "delta" && !!source.loop;
|
|
198
|
+
for (const key of animationKeys) {
|
|
199
|
+
const value = source[key] ?? (isScale(key) ? source.scale : void 0);
|
|
200
|
+
if (value === void 0) continue;
|
|
201
|
+
if (claimed.has(key))
|
|
202
|
+
throw new Error(`Animation delta/exact conflict: ${key}`);
|
|
203
|
+
number(value, key, mode === "exact" && isScale(key));
|
|
204
|
+
if (loop && isScale(key) && value < 0)
|
|
205
|
+
throw new Error("Looping scale deltas cannot be negative");
|
|
206
|
+
values[key] = value;
|
|
207
|
+
claimed.add(key);
|
|
208
|
+
}
|
|
209
|
+
if (!Object.keys(values).length)
|
|
210
|
+
throw new Error("Animation track needs a transform");
|
|
211
|
+
result[mode] = { ...values, msec, ...loop ? { loop: true } : {} };
|
|
212
|
+
}
|
|
213
|
+
if (!Object.keys(result).length)
|
|
214
|
+
throw new Error("Animation needs delta or exact");
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
function normalizePlaybackState(input) {
|
|
218
|
+
object(input, "playback state");
|
|
219
|
+
if (input.version !== 1 || typeof input.paused !== "boolean")
|
|
220
|
+
throw new Error("Invalid animation playback state version or paused flag");
|
|
221
|
+
const elapsed_ms = number(input.elapsed_ms, "elapsed time");
|
|
222
|
+
if (elapsed_ms < 0) throw new Error("Invalid animation elapsed time");
|
|
223
|
+
object(input.origin, "origin");
|
|
224
|
+
for (const key of animationKeys) number(input.origin[key], key, isScale(key));
|
|
225
|
+
if (!Array.isArray(input.center) || input.center.length !== 3)
|
|
226
|
+
throw new Error("Invalid animation pivot");
|
|
227
|
+
return {
|
|
228
|
+
version: 1,
|
|
229
|
+
elapsed_ms,
|
|
230
|
+
paused: input.paused,
|
|
231
|
+
origin: animationPose(input.origin),
|
|
232
|
+
center: input.center.map((v) => number(v, "pivot"))
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function createPlayback(definition, model, saved) {
|
|
236
|
+
const animation = normalizeAnimation(definition);
|
|
237
|
+
const state = saved == null ? null : normalizePlaybackState(saved);
|
|
238
|
+
const origin = state?.origin ?? animationPose(model);
|
|
239
|
+
const tracks = Object.entries(animation).map(([mode, a]) => {
|
|
240
|
+
const values = Object.entries(a).filter(
|
|
241
|
+
([key]) => animationKeys.includes(key)
|
|
242
|
+
);
|
|
243
|
+
if (mode === "delta")
|
|
244
|
+
for (const [key, value] of values)
|
|
245
|
+
number(origin[key] + value, `${key} endpoint`, isScale(key));
|
|
246
|
+
return { mode, values, duration: a.msec, loop: !!a.loop, done: false };
|
|
247
|
+
});
|
|
248
|
+
const playback = {
|
|
249
|
+
animation,
|
|
250
|
+
origin,
|
|
251
|
+
tracks,
|
|
252
|
+
elapsed_ms: state?.elapsed_ms ?? 0,
|
|
253
|
+
paused: state?.paused ?? false,
|
|
254
|
+
last_time: null
|
|
255
|
+
};
|
|
256
|
+
evaluatePlayback({ ...playback, tracks: tracks.map((t) => ({ ...t })) }, {});
|
|
257
|
+
return playback;
|
|
258
|
+
}
|
|
259
|
+
function evaluatePlayback(playback, model) {
|
|
260
|
+
const updates = [];
|
|
261
|
+
for (const track of playback.tracks) {
|
|
262
|
+
if (track.done) continue;
|
|
263
|
+
const fraction = playback.elapsed_ms / track.duration;
|
|
264
|
+
const p = track.loop ? fraction : Math.min(fraction, 1);
|
|
265
|
+
for (const [key, value] of track.values) {
|
|
266
|
+
const from = playback.origin[key];
|
|
267
|
+
const to = track.mode === "delta" ? from + value * p : from * (1 - p) + value * p;
|
|
268
|
+
updates.push([key, number(to, `${key} result`, isScale(key))]);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
let scaleChanged = false;
|
|
272
|
+
for (const [key, value] of updates) {
|
|
273
|
+
scaleChanged ||= isScale(key) && model[key] !== value;
|
|
274
|
+
model[key] = value;
|
|
275
|
+
}
|
|
276
|
+
for (const track of playback.tracks)
|
|
277
|
+
if (!track.loop && playback.elapsed_ms >= track.duration) track.done = true;
|
|
278
|
+
if (scaleChanged) model._stats = null;
|
|
279
|
+
return playback.tracks.some((track) => !track.done);
|
|
280
|
+
}
|
|
281
|
+
function advancePlayback(playback, model, time) {
|
|
282
|
+
if (playback.paused) return true;
|
|
283
|
+
if (playback.last_time !== null)
|
|
284
|
+
playback.elapsed_ms = number(
|
|
285
|
+
playback.elapsed_ms + Math.max(0, time - playback.last_time),
|
|
286
|
+
"elapsed time"
|
|
287
|
+
);
|
|
288
|
+
playback.last_time = time;
|
|
289
|
+
return evaluatePlayback(playback, model);
|
|
290
|
+
}
|
|
291
|
+
function playbackState(playback, suspended = false) {
|
|
292
|
+
return {
|
|
293
|
+
status: playback.paused ? "paused" : "running",
|
|
294
|
+
elapsed_ms: playback.elapsed_ms,
|
|
295
|
+
suspended,
|
|
296
|
+
tracks: playback.tracks.filter((t) => !t.done).map((t) => ({
|
|
297
|
+
mode: t.mode,
|
|
298
|
+
msec: t.duration,
|
|
299
|
+
loop: t.loop,
|
|
300
|
+
progress: t.loop ? playback.elapsed_ms % t.duration / t.duration : Math.min(1, playback.elapsed_ms / t.duration)
|
|
301
|
+
}))
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function savePlayback(model) {
|
|
305
|
+
const p = model._animation;
|
|
306
|
+
if (!p) return null;
|
|
307
|
+
const factor = model.unitScale ?? 1;
|
|
308
|
+
const animation = structuredClone(p.animation), origin = { ...p.origin };
|
|
309
|
+
for (const key of animationKeys.filter(isScale)) {
|
|
310
|
+
origin[key] *= factor;
|
|
311
|
+
for (const track of Object.values(animation))
|
|
312
|
+
if (track[key] !== void 0) track[key] *= factor;
|
|
313
|
+
}
|
|
314
|
+
const animation_state = normalizePlaybackState({
|
|
315
|
+
version: 1,
|
|
316
|
+
origin,
|
|
317
|
+
elapsed_ms: p.elapsed_ms,
|
|
318
|
+
paused: p.paused,
|
|
319
|
+
center: [...model.center]
|
|
320
|
+
});
|
|
321
|
+
return { animation: normalizeAnimation(animation), animation_state };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/memory.js
|
|
202
325
|
var MEMORY_OVERHEAD = 64 * 1024 * 1024;
|
|
203
326
|
var TRIANGLE_MEMORY = 224;
|
|
204
327
|
function geometryMemory(g) {
|
|
@@ -215,7 +338,420 @@ function derivedMemory(triangles, features) {
|
|
|
215
338
|
return MEMORY_OVERHEAD + triangles * (TRIANGLE_MEMORY + (features.smooth ? 1536 : 0) + (features.edges || features.wire ? 2048 : 0));
|
|
216
339
|
}
|
|
217
340
|
|
|
218
|
-
// src/
|
|
341
|
+
// src/print-options.js
|
|
342
|
+
function printOptions(input = {}) {
|
|
343
|
+
const level = input.level ?? "basic";
|
|
344
|
+
if (!["basic", "detailed"].includes(level))
|
|
345
|
+
throw new Error("Choose basic or detailed print checks");
|
|
346
|
+
const number2 = (v, name, max = 1e9) => {
|
|
347
|
+
if (!Number.isFinite(v) || v <= 0 || v > max)
|
|
348
|
+
throw new Error(`Invalid ${name}`);
|
|
349
|
+
return v;
|
|
350
|
+
};
|
|
351
|
+
let profile = null;
|
|
352
|
+
if (input.profile != null) {
|
|
353
|
+
if (input.profile.technology !== "fdm")
|
|
354
|
+
throw new Error("Only FDM profiles are supported");
|
|
355
|
+
profile = { technology: "fdm" };
|
|
356
|
+
for (const key of ["min_wall_mm", "line_width_mm"])
|
|
357
|
+
if (input.profile[key] != null)
|
|
358
|
+
profile[key] = number2(input.profile[key], key);
|
|
359
|
+
if (input.profile.overhang_degrees != null)
|
|
360
|
+
profile.overhang_degrees = number2(
|
|
361
|
+
input.profile.overhang_degrees,
|
|
362
|
+
"overhang angle",
|
|
363
|
+
89.9
|
|
364
|
+
);
|
|
365
|
+
if (input.profile.build_volume_mm != null) {
|
|
366
|
+
if (!Array.isArray(input.profile.build_volume_mm) || input.profile.build_volume_mm.length !== 3)
|
|
367
|
+
throw new Error("Build volume requires X, Y and Z");
|
|
368
|
+
profile.build_volume_mm = input.profile.build_volume_mm.map(
|
|
369
|
+
(v) => number2(v, "build volume")
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const time = number2(input.max_time_ms ?? 3e4, "check time limit", 12e4);
|
|
374
|
+
return {
|
|
375
|
+
level,
|
|
376
|
+
profile,
|
|
377
|
+
apply_transforms: input.apply_transforms !== false,
|
|
378
|
+
units_confirmed: input.units_confirmed === true,
|
|
379
|
+
max_time_ms: time
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
var printMemory = (triangles) => 64 * 1024 ** 2 + triangles * 2400;
|
|
383
|
+
var printSignature = (model) => JSON.stringify([
|
|
384
|
+
model.x,
|
|
385
|
+
model.y,
|
|
386
|
+
model.z,
|
|
387
|
+
model.rotationx,
|
|
388
|
+
model.rotationy,
|
|
389
|
+
model.rotationz,
|
|
390
|
+
model.scaleVector,
|
|
391
|
+
model.center,
|
|
392
|
+
model.units,
|
|
393
|
+
model.unitScale
|
|
394
|
+
]);
|
|
395
|
+
|
|
396
|
+
// src/model-tools.js
|
|
397
|
+
var axes = ["x", "y", "z"];
|
|
398
|
+
function validateUnits(units) {
|
|
399
|
+
if (!["mm", "inch"].includes(units)) throw new Error("Choose mm or inch");
|
|
400
|
+
return units;
|
|
401
|
+
}
|
|
402
|
+
function validateSizeState(value) {
|
|
403
|
+
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))
|
|
404
|
+
throw new Error("Invalid saved model size");
|
|
405
|
+
return {
|
|
406
|
+
version: 1,
|
|
407
|
+
baseline_mm: [...value.baseline_mm],
|
|
408
|
+
file_units: validateUnits(value.file_units),
|
|
409
|
+
editable: value.editable,
|
|
410
|
+
unit_factor: value.unit_factor ?? 1
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function sizeState(model) {
|
|
414
|
+
return model.size_state ??= {
|
|
415
|
+
version: 1,
|
|
416
|
+
baseline_mm: model.geometry.bounds.max.map(
|
|
417
|
+
(v, i) => (v - model.geometry.bounds.min[i]) * model.unitScale
|
|
418
|
+
),
|
|
419
|
+
file_units: "mm",
|
|
420
|
+
editable: !model.geometry.units,
|
|
421
|
+
unit_factor: 1
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function modelSize(model) {
|
|
425
|
+
if (!model.geometry) throw new Error("Wait for the model to finish opening");
|
|
426
|
+
const state = sizeState(model);
|
|
427
|
+
return {
|
|
428
|
+
...structuredClone(state),
|
|
429
|
+
dimensions_mm: model.geometry.bounds.max.map(
|
|
430
|
+
(v, i) => (v - model.geometry.bounds.min[i]) * model.scaleVector[i]
|
|
431
|
+
)
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function resizeModel(id, factors) {
|
|
435
|
+
const m = this._model(id);
|
|
436
|
+
modelSize(m);
|
|
437
|
+
if (!Array.isArray(factors) || factors.length !== 3 || factors.some((v) => !Number.isFinite(v) || v <= 0))
|
|
438
|
+
throw new Error("Scale must be a positive finite number");
|
|
439
|
+
const next = axes.map((a, i) => m["scale" + a] * factors[i]);
|
|
440
|
+
if (next.some((v) => !Number.isFinite(v) || v < 1e-12 || v > 1e12))
|
|
441
|
+
throw new Error("Scale is outside the supported range");
|
|
442
|
+
const center = m.geometry.bounds.min.map(
|
|
443
|
+
(v, i) => (v + m.geometry.bounds.max[i]) / 2
|
|
444
|
+
);
|
|
445
|
+
const before = transform(modelMatrix(m), center);
|
|
446
|
+
const cached = m._measurements;
|
|
447
|
+
const oldKey = m.scaleVector.join(",");
|
|
448
|
+
this._stopAnimation(m, "manual");
|
|
449
|
+
axes.forEach((a, i) => m["scale" + a] = next[i]);
|
|
450
|
+
const after = transform(modelMatrix(m), center);
|
|
451
|
+
axes.forEach((a, i) => m[a] += before[i] - after[i]);
|
|
452
|
+
m._stats = null;
|
|
453
|
+
if (cached?.key === oldKey && cached.positions === m.geometry.positions && factors.every((v) => v === factors[0])) {
|
|
454
|
+
m._measurements = {
|
|
455
|
+
...cached,
|
|
456
|
+
key: m.scaleVector.join(","),
|
|
457
|
+
result: {
|
|
458
|
+
...cached.result,
|
|
459
|
+
area_mm2: cached.result.area_mm2 * factors[0] ** 2,
|
|
460
|
+
volume_mm3: cached.result.volume_mm3 == null ? null : cached.result.volume_mm3 * factors[0] ** 3
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
this._invalidate();
|
|
465
|
+
return modelSize(m);
|
|
466
|
+
}
|
|
467
|
+
function setFileUnits(id, units) {
|
|
468
|
+
validateUnits(units);
|
|
469
|
+
const m = this._model(id), state = sizeState(m);
|
|
470
|
+
if (!state.editable)
|
|
471
|
+
throw new Error(
|
|
472
|
+
"This model has explicit file units, already converted to millimetres"
|
|
473
|
+
);
|
|
474
|
+
if (state.file_units === units) return modelSize(m);
|
|
475
|
+
const factor = units === "inch" ? 25.4 : 1 / 25.4;
|
|
476
|
+
const result = resizeModel.call(this, id, [factor, factor, factor]);
|
|
477
|
+
state.baseline_mm = state.baseline_mm.map((v) => v * factor);
|
|
478
|
+
state.file_units = units;
|
|
479
|
+
state.unit_factor = (state.unit_factor ?? 1) * factor;
|
|
480
|
+
return { ...result, ...structuredClone(state) };
|
|
481
|
+
}
|
|
482
|
+
function resetModelSize(id) {
|
|
483
|
+
const size = modelSize(this._model(id));
|
|
484
|
+
return resizeModel.call(
|
|
485
|
+
this,
|
|
486
|
+
id,
|
|
487
|
+
size.dimensions_mm.map((v, i) => v ? size.baseline_mm[i] / v : 1)
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
function duplicateModel(id) {
|
|
491
|
+
const source = this._model(id);
|
|
492
|
+
modelSize(source);
|
|
493
|
+
if (source._sceneImportPending)
|
|
494
|
+
throw new Error("Wait for the scene to finish opening");
|
|
495
|
+
if (this._models.size + 1 > this.limits.max_archive_entries || [...this._models.values()].reduce(
|
|
496
|
+
(n, m) => n + (m.geometry?.triangles || 0),
|
|
497
|
+
source.geometry.triangles
|
|
498
|
+
) > this.limits.max_triangles)
|
|
499
|
+
throw new Error("Scene model or triangle limit exceeded");
|
|
500
|
+
assertMemory(
|
|
501
|
+
this.limits,
|
|
502
|
+
this._occupiedMemory() + geometryMemory(source.geometry),
|
|
503
|
+
"duplicate"
|
|
504
|
+
);
|
|
505
|
+
const copy = {};
|
|
506
|
+
for (const key of [
|
|
507
|
+
"filename",
|
|
508
|
+
"orig_filename",
|
|
509
|
+
"local_file",
|
|
510
|
+
"x",
|
|
511
|
+
"y",
|
|
512
|
+
"z",
|
|
513
|
+
"rotationx",
|
|
514
|
+
"rotationy",
|
|
515
|
+
"rotationz",
|
|
516
|
+
"opacity",
|
|
517
|
+
"units",
|
|
518
|
+
"display",
|
|
519
|
+
"view_edges",
|
|
520
|
+
"smooth_shading",
|
|
521
|
+
"color"
|
|
522
|
+
])
|
|
523
|
+
copy[key] = source[key];
|
|
524
|
+
copy.id = this.max_model_id + 1;
|
|
525
|
+
if (!Number.isSafeInteger(copy.id))
|
|
526
|
+
throw new Error("Model ID limit exceeded");
|
|
527
|
+
copy.display_name = (source.display_name || source.orig_filename || source.filename || "Model").split(/[\\/]/).pop() + " \u2014 copy";
|
|
528
|
+
copy.center = [...source.center];
|
|
529
|
+
copy.unitScale = 1;
|
|
530
|
+
axes.forEach((a, i) => copy["scale" + a] = source.scaleVector[i]);
|
|
531
|
+
Object.defineProperty(copy, "scaleVector", {
|
|
532
|
+
get: () => axes.map((a) => copy["scale" + a] * copy.unitScale)
|
|
533
|
+
});
|
|
534
|
+
copy.geometry = { ...source.geometry };
|
|
535
|
+
copy.section = source.section ? { ...source.section } : null;
|
|
536
|
+
copy.size_state = structuredClone(sizeState(source));
|
|
537
|
+
copy.size_state.unit_factor = (copy.size_state.unit_factor ?? 1) * source.unitScale;
|
|
538
|
+
const saved = savePlayback(source);
|
|
539
|
+
this.max_model_id = copy.id;
|
|
540
|
+
this._models.set(copy.id, copy);
|
|
541
|
+
this.models_ref[copy.id] = this.models.length;
|
|
542
|
+
this.models.push(copy);
|
|
543
|
+
this.models_count = this._models.size;
|
|
544
|
+
if (saved)
|
|
545
|
+
this._startAnimation(copy, saved.animation, {
|
|
546
|
+
...saved.animation_state,
|
|
547
|
+
paused: true
|
|
548
|
+
});
|
|
549
|
+
this._invalidate();
|
|
550
|
+
return copy.id;
|
|
551
|
+
}
|
|
552
|
+
var cameraDirections = {
|
|
553
|
+
front: [
|
|
554
|
+
[0, -1, 0],
|
|
555
|
+
[0, 0, 1]
|
|
556
|
+
],
|
|
557
|
+
back: [
|
|
558
|
+
[0, 1, 0],
|
|
559
|
+
[0, 0, 1]
|
|
560
|
+
],
|
|
561
|
+
left: [
|
|
562
|
+
[-1, 0, 0],
|
|
563
|
+
[0, 0, 1]
|
|
564
|
+
],
|
|
565
|
+
right: [
|
|
566
|
+
[1, 0, 0],
|
|
567
|
+
[0, 0, 1]
|
|
568
|
+
],
|
|
569
|
+
top: [
|
|
570
|
+
[0, 0, 1],
|
|
571
|
+
[0, 1, 0]
|
|
572
|
+
],
|
|
573
|
+
bottom: [
|
|
574
|
+
[0, 0, -1],
|
|
575
|
+
[0, -1, 0]
|
|
576
|
+
],
|
|
577
|
+
isometric: [
|
|
578
|
+
[1, -1, 1],
|
|
579
|
+
[0, 0, 1]
|
|
580
|
+
]
|
|
581
|
+
};
|
|
582
|
+
function cameraView(name) {
|
|
583
|
+
const view = cameraDirections[name];
|
|
584
|
+
if (!view) throw new Error("Unknown camera view");
|
|
585
|
+
this.camera.position = this.camera.target.map((v, i) => v + view[0][i]);
|
|
586
|
+
this.camera.up = [...view[1]];
|
|
587
|
+
this.set_auto_zoom();
|
|
588
|
+
}
|
|
589
|
+
function validateBookmarks(bookmarks) {
|
|
590
|
+
if (!Array.isArray(bookmarks) || bookmarks.length > 32)
|
|
591
|
+
throw new Error("Save up to 32 camera views");
|
|
592
|
+
const names = /* @__PURE__ */ new Set();
|
|
593
|
+
return bookmarks.map((b) => {
|
|
594
|
+
const name = typeof b?.name === "string" ? b.name.trim() : "";
|
|
595
|
+
if (!name || name.length > 80 || /[\x00-\x1f]/.test(name) || names.has(name))
|
|
596
|
+
throw new Error("Use a unique view name of 1\u201380 characters");
|
|
597
|
+
names.add(name);
|
|
598
|
+
const state = {};
|
|
599
|
+
for (const key of ["position", "target", "up"]) {
|
|
600
|
+
if (!b.camera?.[key] || axes.some(
|
|
601
|
+
(a) => !Number.isFinite(b.camera[key][a]) || Math.abs(b.camera[key][a]) > 1e15
|
|
602
|
+
))
|
|
603
|
+
throw new Error("Invalid saved camera");
|
|
604
|
+
state[key] = Object.fromEntries(axes.map((a) => [a, b.camera[key][a]]));
|
|
605
|
+
}
|
|
606
|
+
const d = axes.map((a) => state.position[a] - state.target[a]), u = axes.map((a) => state.up[a]);
|
|
607
|
+
if (Math.hypot(
|
|
608
|
+
d[1] * u[2] - d[2] * u[1],
|
|
609
|
+
d[2] * u[0] - d[0] * u[2],
|
|
610
|
+
d[0] * u[1] - d[1] * u[0]
|
|
611
|
+
) < 1e-12)
|
|
612
|
+
throw new Error("Degenerate saved camera");
|
|
613
|
+
return { name, camera: state };
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
function mergeBookmarks(existing, incoming) {
|
|
617
|
+
const result = validateBookmarks(existing), names = new Set(result.map((b) => b.name));
|
|
618
|
+
for (const b of validateBookmarks(incoming)) {
|
|
619
|
+
let name = b.name, n = 2;
|
|
620
|
+
while (names.has(name)) name = b.name.slice(0, 70) + ` (${n++})`;
|
|
621
|
+
names.add(name);
|
|
622
|
+
result.push({ ...b, name });
|
|
623
|
+
}
|
|
624
|
+
return validateBookmarks(result);
|
|
625
|
+
}
|
|
626
|
+
async function measureModel(id, options = {}) {
|
|
627
|
+
await this.when_ready;
|
|
628
|
+
const m = this._model(id), g = m.geometry;
|
|
629
|
+
if (!g || m._sceneImportPending)
|
|
630
|
+
throw new Error("Wait for the model to finish opening");
|
|
631
|
+
const scale = m.scaleVector, key = scale.join(","), cached = m._measurements;
|
|
632
|
+
if (options.signal?.aborted)
|
|
633
|
+
throw new DOMException("Measurement cancelled", "AbortError");
|
|
634
|
+
if (cached?.positions === g.positions && cached.key === key)
|
|
635
|
+
return { ...cached.result };
|
|
636
|
+
const working = printMemory(g.triangles), occupied = this._occupiedMemory();
|
|
637
|
+
assertMemory(this.limits, occupied + working, "measurements");
|
|
638
|
+
const release = this._reserveMemory(
|
|
639
|
+
m,
|
|
640
|
+
working + geometryMemory(g),
|
|
641
|
+
"measurements"
|
|
642
|
+
);
|
|
643
|
+
const controller = new AbortController(), cancel = () => controller.abort();
|
|
644
|
+
this._aborters.add(controller);
|
|
645
|
+
(m._measureJobs ??= /* @__PURE__ */ new Set()).add(controller);
|
|
646
|
+
options.signal?.addEventListener("abort", cancel, { once: true });
|
|
647
|
+
let job, callbackError;
|
|
648
|
+
const check = () => {
|
|
649
|
+
if (controller.signal.aborted || this.killsign)
|
|
650
|
+
throw new DOMException("Measurement cancelled", "AbortError");
|
|
651
|
+
if (this._models.get(id) !== m || m.geometry.positions !== g.positions || m.scaleVector.join(",") !== key)
|
|
652
|
+
throw new Error("Model size changed. Calculate again.");
|
|
653
|
+
};
|
|
654
|
+
try {
|
|
655
|
+
const positions = new Float32Array(g.positions.length);
|
|
656
|
+
for (let start = 0; start < positions.length; start += 262144) {
|
|
657
|
+
check();
|
|
658
|
+
positions.set(g.positions.subarray(start, start + 262144), start);
|
|
659
|
+
if (start + 262144 < positions.length)
|
|
660
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
661
|
+
}
|
|
662
|
+
job = this._pool.run(
|
|
663
|
+
{
|
|
664
|
+
kind: "measure",
|
|
665
|
+
positions,
|
|
666
|
+
scale,
|
|
667
|
+
area: g.area,
|
|
668
|
+
max_time_ms: options.max_time_ms ?? 3e4,
|
|
669
|
+
limits: {
|
|
670
|
+
...this.limits,
|
|
671
|
+
max_memory_bytes: this.limits.max_memory_bytes - occupied
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
(progress) => {
|
|
675
|
+
try {
|
|
676
|
+
options.on_progress?.(progress);
|
|
677
|
+
} catch (error) {
|
|
678
|
+
callbackError = error;
|
|
679
|
+
controller.abort();
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
[positions.buffer]
|
|
683
|
+
);
|
|
684
|
+
controller.signal.addEventListener("abort", () => job.cancel(), {
|
|
685
|
+
once: true
|
|
686
|
+
});
|
|
687
|
+
check();
|
|
688
|
+
let result;
|
|
689
|
+
try {
|
|
690
|
+
result = await job;
|
|
691
|
+
} catch (error) {
|
|
692
|
+
throw callbackError || error;
|
|
693
|
+
}
|
|
694
|
+
check();
|
|
695
|
+
m._measurements = { positions: g.positions, key, result };
|
|
696
|
+
return { ...result };
|
|
697
|
+
} finally {
|
|
698
|
+
job?.cancel?.();
|
|
699
|
+
release();
|
|
700
|
+
this._aborters.delete(controller);
|
|
701
|
+
m._measureJobs.delete(controller);
|
|
702
|
+
options.signal?.removeEventListener("abort", cancel);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/appearance.js
|
|
707
|
+
var DISPLAYS = [
|
|
708
|
+
"flat",
|
|
709
|
+
"smooth",
|
|
710
|
+
"wireframe",
|
|
711
|
+
"edges",
|
|
712
|
+
"mesh",
|
|
713
|
+
"xray",
|
|
714
|
+
"studio",
|
|
715
|
+
"orientation"
|
|
716
|
+
];
|
|
717
|
+
function validateView(display, smooth) {
|
|
718
|
+
if (!DISPLAYS.includes(display)) throw new Error("Invalid display mode");
|
|
719
|
+
if (typeof smooth !== "boolean")
|
|
720
|
+
throw new Error("smooth_shading must be a boolean");
|
|
721
|
+
return { display, smooth_shading: display === "smooth" || smooth };
|
|
722
|
+
}
|
|
723
|
+
function viewFeatures(view) {
|
|
724
|
+
return {
|
|
725
|
+
smooth: (view.smooth_shading || view.display === "smooth") && view.display !== "wireframe",
|
|
726
|
+
edges: view.view_edges || view.display === "edges",
|
|
727
|
+
wire: view.display === "wireframe",
|
|
728
|
+
edgeAngle: view.display === "edges" ? 30 : 1
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function validateSection(section) {
|
|
732
|
+
if (section == null) return null;
|
|
733
|
+
if (!["x", "y", "z"].includes(section.axis))
|
|
734
|
+
throw new Error("Section axis must be x, y or z");
|
|
735
|
+
if (!Number.isFinite(section.position) || section.position < 0 || section.position > 1)
|
|
736
|
+
throw new Error("Section position must be between 0 and 1");
|
|
737
|
+
if (section.flip !== void 0 && typeof section.flip !== "boolean")
|
|
738
|
+
throw new Error("Section flip must be a boolean");
|
|
739
|
+
return {
|
|
740
|
+
axis: section.axis,
|
|
741
|
+
position: section.position,
|
|
742
|
+
flip: section.flip ?? false
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
function sectionPlane(bounds, section) {
|
|
746
|
+
if (!section) return [0, 0, 0, 0];
|
|
747
|
+
const axis = ["x", "y", "z"].indexOf(section.axis), sign = section.flip ? -1 : 1;
|
|
748
|
+
const plane = [0, 0, 0, 0];
|
|
749
|
+
plane[axis] = sign;
|
|
750
|
+
plane[3] = -sign * (bounds.min[axis] + (bounds.max[axis] - bounds.min[axis]) * section.position);
|
|
751
|
+
return plane;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/geometry.js
|
|
219
755
|
var DEFAULT_LIMITS = Object.freeze({
|
|
220
756
|
max_file_bytes: 256 * 1024 * 1024,
|
|
221
757
|
max_triangles: 5e6,
|
|
@@ -363,7 +899,7 @@ function exportSTL(g, scale = [1, 1, 1], center = [0, 0, 0]) {
|
|
|
363
899
|
return buffer;
|
|
364
900
|
}
|
|
365
901
|
|
|
366
|
-
// src/
|
|
902
|
+
// src/infinite-grid.js
|
|
367
903
|
var vertex = `#version 300 es
|
|
368
904
|
precision highp float;
|
|
369
905
|
out vec2 screen;
|
|
@@ -455,7 +991,7 @@ var InfiniteGrid = class {
|
|
|
455
991
|
}
|
|
456
992
|
};
|
|
457
993
|
|
|
458
|
-
// src/
|
|
994
|
+
// src/selection-frame.js
|
|
459
995
|
function selectionFrame(bounds) {
|
|
460
996
|
const padding = Math.max(
|
|
461
997
|
Math.max(...bounds.max.map((v, i) => v - bounds.min[i])) * 0.015,
|
|
@@ -473,7 +1009,7 @@ function selectionFrame(bounds) {
|
|
|
473
1009
|
return new Float32Array(lines);
|
|
474
1010
|
}
|
|
475
1011
|
|
|
476
|
-
// src/
|
|
1012
|
+
// src/renderer.js
|
|
477
1013
|
var vertex2 = `#version 300 es
|
|
478
1014
|
precision highp float;
|
|
479
1015
|
layout(location=0) in vec3 position;
|
|
@@ -509,6 +1045,7 @@ var Renderer = class {
|
|
|
509
1045
|
this.canvas = canvas;
|
|
510
1046
|
this.domElement = canvas;
|
|
511
1047
|
this.resources = /* @__PURE__ */ new Map();
|
|
1048
|
+
this.sharedResources = /* @__PURE__ */ new WeakMap();
|
|
512
1049
|
this.frames = 0;
|
|
513
1050
|
this.lost = false;
|
|
514
1051
|
this.gl = canvas.getContext("webgl2", {
|
|
@@ -525,6 +1062,7 @@ var Renderer = class {
|
|
|
525
1062
|
e.preventDefault();
|
|
526
1063
|
this.lost = true;
|
|
527
1064
|
this.resources.clear();
|
|
1065
|
+
this.sharedResources = /* @__PURE__ */ new WeakMap();
|
|
528
1066
|
this.gridResource = null;
|
|
529
1067
|
this.infiniteGrid = null;
|
|
530
1068
|
this.selectionResource = null;
|
|
@@ -632,7 +1170,12 @@ var Renderer = class {
|
|
|
632
1170
|
if (this.diagnostic?.model === model) this.setDiagnostic(null);
|
|
633
1171
|
const resources = this.resources.get(model);
|
|
634
1172
|
if (resources)
|
|
635
|
-
for (const resource of Object.values(resources))
|
|
1173
|
+
for (const resource of Object.values(resources)) {
|
|
1174
|
+
if (--resource.users === 0) {
|
|
1175
|
+
this.sharedResources.get(resource.source)?.delete(resource);
|
|
1176
|
+
this.free(resource);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
636
1179
|
this.resources.delete(model);
|
|
637
1180
|
}
|
|
638
1181
|
clear() {
|
|
@@ -646,8 +1189,10 @@ var Renderer = class {
|
|
|
646
1189
|
}
|
|
647
1190
|
get liveBuffers() {
|
|
648
1191
|
let n = (this.gridResource?.buffers.length || 0) + (this.selectionResource?.buffers.length || 0) + (this.diagnosticResource?.buffers.length || 0);
|
|
1192
|
+
const unique = /* @__PURE__ */ new Set();
|
|
649
1193
|
for (const r of this.resources.values())
|
|
650
|
-
for (const b of Object.values(r))
|
|
1194
|
+
for (const b of Object.values(r)) unique.add(b);
|
|
1195
|
+
for (const b of unique) n += b.buffers.length;
|
|
651
1196
|
return n;
|
|
652
1197
|
}
|
|
653
1198
|
setDiagnostic(value) {
|
|
@@ -713,12 +1258,23 @@ var Renderer = class {
|
|
|
713
1258
|
this.resources.set(model, r);
|
|
714
1259
|
}
|
|
715
1260
|
if (!r[mode]) {
|
|
716
|
-
const g = model.geometry;
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1261
|
+
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;
|
|
1262
|
+
let entries = this.sharedResources.get(source);
|
|
1263
|
+
if (!entries) this.sharedResources.set(source, entries = /* @__PURE__ */ new Set());
|
|
1264
|
+
let resource = [...entries].find(
|
|
1265
|
+
(r2) => r2.normals === normals && r2.colors === colors
|
|
721
1266
|
);
|
|
1267
|
+
if (!resource) {
|
|
1268
|
+
resource = Object.assign(this.upload(source, normals, colors), {
|
|
1269
|
+
source,
|
|
1270
|
+
normals,
|
|
1271
|
+
colors,
|
|
1272
|
+
users: 0
|
|
1273
|
+
});
|
|
1274
|
+
entries.add(resource);
|
|
1275
|
+
}
|
|
1276
|
+
resource.users++;
|
|
1277
|
+
r[mode] = resource;
|
|
722
1278
|
}
|
|
723
1279
|
return r[mode];
|
|
724
1280
|
}
|
|
@@ -894,7 +1450,7 @@ var Renderer = class {
|
|
|
894
1450
|
}
|
|
895
1451
|
};
|
|
896
1452
|
|
|
897
|
-
// src/
|
|
1453
|
+
// src/controls.js
|
|
898
1454
|
var Controls = class {
|
|
899
1455
|
constructor(canvas, camera, invalidate, pick, type = 0) {
|
|
900
1456
|
this.enabled = true;
|
|
@@ -1005,7 +1561,7 @@ var Controls = class {
|
|
|
1005
1561
|
}
|
|
1006
1562
|
};
|
|
1007
1563
|
|
|
1008
|
-
// src/
|
|
1564
|
+
// src/worker-pool.js
|
|
1009
1565
|
var WorkerPool = class {
|
|
1010
1566
|
constructor(url, { count = 2, timeout = 12e4, factory } = {}) {
|
|
1011
1567
|
this.url = url;
|
|
@@ -1090,7 +1646,7 @@ var WorkerPool = class {
|
|
|
1090
1646
|
}
|
|
1091
1647
|
};
|
|
1092
1648
|
|
|
1093
|
-
// src/
|
|
1649
|
+
// src/scene-import.js
|
|
1094
1650
|
var sceneFormat = (name) => String(name || "").match(/\.(vsb|vsj)(?:[?#]|$)/i)?.[1].toLowerCase() || "";
|
|
1095
1651
|
function localSceneFile(reference, files) {
|
|
1096
1652
|
let path;
|
|
@@ -1124,7 +1680,7 @@ function sceneURL(reference, base) {
|
|
|
1124
1680
|
return url.href;
|
|
1125
1681
|
}
|
|
1126
1682
|
|
|
1127
|
-
// src/
|
|
1683
|
+
// src/scene-import-controller.js
|
|
1128
1684
|
function importScene(source, options = {}) {
|
|
1129
1685
|
const controller = new AbortController(), signal = controller.signal;
|
|
1130
1686
|
const generation = this._generation, ids = [], added = /* @__PURE__ */ new Map(), usedFiles = /* @__PURE__ */ new Set();
|
|
@@ -1149,8 +1705,8 @@ function importScene(source, options = {}) {
|
|
|
1149
1705
|
await this.when_ready;
|
|
1150
1706
|
check();
|
|
1151
1707
|
oldCamera = this.get_camera_state();
|
|
1152
|
-
const
|
|
1153
|
-
if (!["vsb", "vsj"].includes(
|
|
1708
|
+
const format2 = options.format || sceneFormat(typeof source === "string" ? source : source.name);
|
|
1709
|
+
if (!["vsb", "vsj"].includes(format2))
|
|
1154
1710
|
throw new Error("Choose VSB or VSJ for a scene");
|
|
1155
1711
|
let remote, relay;
|
|
1156
1712
|
if (typeof source === "string") {
|
|
@@ -1183,8 +1739,8 @@ function importScene(source, options = {}) {
|
|
|
1183
1739
|
source,
|
|
1184
1740
|
remote,
|
|
1185
1741
|
relay,
|
|
1186
|
-
format,
|
|
1187
|
-
name: `scene.${
|
|
1742
|
+
format: format2,
|
|
1743
|
+
name: `scene.${format2}`,
|
|
1188
1744
|
limits
|
|
1189
1745
|
},
|
|
1190
1746
|
(p) => {
|
|
@@ -1205,6 +1761,10 @@ function importScene(source, options = {}) {
|
|
|
1205
1761
|
job = null;
|
|
1206
1762
|
check();
|
|
1207
1763
|
const { scene, files, base } = documentData;
|
|
1764
|
+
const bookmarks = mergeBookmarks(
|
|
1765
|
+
this.get_camera_bookmarks(),
|
|
1766
|
+
scene.camera_bookmarks || []
|
|
1767
|
+
);
|
|
1208
1768
|
this._sceneBytes -= held;
|
|
1209
1769
|
held = Object.values(files || {}).reduce((n, b) => n + b.byteLength, 0) + JSON.stringify(scene).length * 2;
|
|
1210
1770
|
this._sceneBytes += held;
|
|
@@ -1239,7 +1799,7 @@ function importScene(source, options = {}) {
|
|
|
1239
1799
|
progress({ phase: "opening-model", loaded: index, total: models.length });
|
|
1240
1800
|
check();
|
|
1241
1801
|
const input = models[index], id = this.max_model_id + 1;
|
|
1242
|
-
this.add_model({ ...input, id });
|
|
1802
|
+
this.add_model({ ...input, id, animation: null, animation_state: null });
|
|
1243
1803
|
const model = this._models.get(id);
|
|
1244
1804
|
if (!model) throw new Error(this.error || "Invalid scene model");
|
|
1245
1805
|
ids.push(id);
|
|
@@ -1264,7 +1824,9 @@ function importScene(source, options = {}) {
|
|
|
1264
1824
|
}),
|
|
1265
1825
|
delta
|
|
1266
1826
|
);
|
|
1267
|
-
|
|
1827
|
+
if (input.animation)
|
|
1828
|
+
model.center = [...input.animation_state?.center ?? desired];
|
|
1829
|
+
else ["x", "y", "z"].forEach((axis, i) => model[axis] += shift[i]);
|
|
1268
1830
|
if ([...this._models.values()].reduce(
|
|
1269
1831
|
(n, m) => n + (m.geometry?.triangles || 0),
|
|
1270
1832
|
0
|
|
@@ -1277,7 +1839,12 @@ function importScene(source, options = {}) {
|
|
|
1277
1839
|
total: models.length
|
|
1278
1840
|
});
|
|
1279
1841
|
check();
|
|
1280
|
-
for (
|
|
1842
|
+
for (let index = 0; index < ids.length; index++) {
|
|
1843
|
+
const model = added.get(ids[index]), input = models[index];
|
|
1844
|
+
if (input.animation)
|
|
1845
|
+
this._startAnimation(model, input.animation, input.animation_state);
|
|
1846
|
+
model._sceneImportPending = false;
|
|
1847
|
+
}
|
|
1281
1848
|
if (scene.bg_color !== void 0) this.set_bg_color(scene.bg_color);
|
|
1282
1849
|
if (scene.infinite_grid)
|
|
1283
1850
|
this.set_infinite_grid(
|
|
@@ -1289,6 +1856,8 @@ function importScene(source, options = {}) {
|
|
|
1289
1856
|
if (scene.auto_rotate !== void 0)
|
|
1290
1857
|
this.set_auto_rotate(scene.auto_rotate);
|
|
1291
1858
|
this._restoreCamera = scene.camera_state || (hadModels ? oldCamera : null);
|
|
1859
|
+
this.camera_bookmarks = bookmarks;
|
|
1860
|
+
if (scene.display_units) this.display_units = scene.display_units;
|
|
1292
1861
|
committed = true;
|
|
1293
1862
|
return { model_ids: ids, scene, used_files: [...usedFiles] };
|
|
1294
1863
|
})().catch((error) => {
|
|
@@ -1309,62 +1878,7 @@ function importScene(source, options = {}) {
|
|
|
1309
1878
|
return promise;
|
|
1310
1879
|
}
|
|
1311
1880
|
|
|
1312
|
-
// src/
|
|
1313
|
-
function printOptions(input = {}) {
|
|
1314
|
-
const level = input.level ?? "basic";
|
|
1315
|
-
if (!["basic", "detailed"].includes(level))
|
|
1316
|
-
throw new Error("Choose basic or detailed print checks");
|
|
1317
|
-
const number = (v, name, max = 1e9) => {
|
|
1318
|
-
if (!Number.isFinite(v) || v <= 0 || v > max)
|
|
1319
|
-
throw new Error(`Invalid ${name}`);
|
|
1320
|
-
return v;
|
|
1321
|
-
};
|
|
1322
|
-
let profile = null;
|
|
1323
|
-
if (input.profile != null) {
|
|
1324
|
-
if (input.profile.technology !== "fdm")
|
|
1325
|
-
throw new Error("Only FDM profiles are supported");
|
|
1326
|
-
profile = { technology: "fdm" };
|
|
1327
|
-
for (const key of ["min_wall_mm", "line_width_mm"])
|
|
1328
|
-
if (input.profile[key] != null)
|
|
1329
|
-
profile[key] = number(input.profile[key], key);
|
|
1330
|
-
if (input.profile.overhang_degrees != null)
|
|
1331
|
-
profile.overhang_degrees = number(
|
|
1332
|
-
input.profile.overhang_degrees,
|
|
1333
|
-
"overhang angle",
|
|
1334
|
-
89.9
|
|
1335
|
-
);
|
|
1336
|
-
if (input.profile.build_volume_mm != null) {
|
|
1337
|
-
if (!Array.isArray(input.profile.build_volume_mm) || input.profile.build_volume_mm.length !== 3)
|
|
1338
|
-
throw new Error("Build volume requires X, Y and Z");
|
|
1339
|
-
profile.build_volume_mm = input.profile.build_volume_mm.map(
|
|
1340
|
-
(v) => number(v, "build volume")
|
|
1341
|
-
);
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
const time = number(input.max_time_ms ?? 3e4, "check time limit", 12e4);
|
|
1345
|
-
return {
|
|
1346
|
-
level,
|
|
1347
|
-
profile,
|
|
1348
|
-
apply_transforms: input.apply_transforms !== false,
|
|
1349
|
-
units_confirmed: input.units_confirmed === true,
|
|
1350
|
-
max_time_ms: time
|
|
1351
|
-
};
|
|
1352
|
-
}
|
|
1353
|
-
var printMemory = (triangles) => 64 * 1024 ** 2 + triangles * 2400;
|
|
1354
|
-
var printSignature = (model) => JSON.stringify([
|
|
1355
|
-
model.x,
|
|
1356
|
-
model.y,
|
|
1357
|
-
model.z,
|
|
1358
|
-
model.rotationx,
|
|
1359
|
-
model.rotationy,
|
|
1360
|
-
model.rotationz,
|
|
1361
|
-
model.scaleVector,
|
|
1362
|
-
model.center,
|
|
1363
|
-
model.units,
|
|
1364
|
-
model.unitScale
|
|
1365
|
-
]);
|
|
1366
|
-
|
|
1367
|
-
// src/v2/print-controller.js
|
|
1881
|
+
// src/print-controller.js
|
|
1368
1882
|
var reports = /* @__PURE__ */ new WeakMap();
|
|
1369
1883
|
function reportCurrent(viewer, report) {
|
|
1370
1884
|
const saved = reports.get(report);
|
|
@@ -1461,7 +1975,9 @@ async function checkPrintability(options = {}) {
|
|
|
1461
1975
|
rotationy: 0,
|
|
1462
1976
|
rotationz: 0,
|
|
1463
1977
|
center: [0, 0, 0],
|
|
1464
|
-
scaleVector: [
|
|
1978
|
+
scaleVector: [1, 1, 1].map(
|
|
1979
|
+
() => model.unitScale * (model.size_state?.unit_factor ?? 1)
|
|
1980
|
+
)
|
|
1465
1981
|
};
|
|
1466
1982
|
progress({
|
|
1467
1983
|
phase: "Starting",
|
|
@@ -1577,7 +2093,7 @@ function showPrintability(report, { issue, model_id = report.model_ids[0] } = {}
|
|
|
1577
2093
|
return lines.length / 6;
|
|
1578
2094
|
}
|
|
1579
2095
|
|
|
1580
|
-
// src/
|
|
2096
|
+
// src/export-options.js
|
|
1581
2097
|
function exportFilename(filename, extension) {
|
|
1582
2098
|
const name = String(filename || "scene").split(/[\\/]/).pop().split(/[?#]/)[0].replace(/[<>:"|*\x00-\x1f\x7f]/g, "_").replace(/\.(?:stl|obj|3mf|step|stp|iges|igs|zip|vsb|vsj)$/i, "").replace(/[. ]+$/g, "").slice(0, 160) || "scene";
|
|
1583
2099
|
return `${name}.${extension}`;
|
|
@@ -1585,8 +2101,8 @@ function exportFilename(filename, extension) {
|
|
|
1585
2101
|
function exportOptions(options = {}) {
|
|
1586
2102
|
const aliases = { stp: "step", igs: "iges" };
|
|
1587
2103
|
const requested = String(options.format || "").toLowerCase();
|
|
1588
|
-
const
|
|
1589
|
-
if (!["stl", "obj", "3mf", "step", "iges"].includes(
|
|
2104
|
+
const format2 = aliases[requested] || requested;
|
|
2105
|
+
if (!["stl", "obj", "3mf", "step", "iges"].includes(format2))
|
|
1590
2106
|
throw new Error("Export format must be stl, obj, 3mf, step or iges");
|
|
1591
2107
|
const units = options.units ?? "mm";
|
|
1592
2108
|
if (!["mm", "inch"].includes(units))
|
|
@@ -1596,10 +2112,10 @@ function exportOptions(options = {}) {
|
|
|
1596
2112
|
throw new Error(`${key} must be a boolean`);
|
|
1597
2113
|
if (options.on_progress !== void 0 && typeof options.on_progress !== "function")
|
|
1598
2114
|
throw new Error("on_progress must be a function");
|
|
1599
|
-
const colors = options.colors !== false && ["obj", "3mf"].includes(
|
|
1600
|
-
const extension =
|
|
2115
|
+
const colors = options.colors !== false && ["obj", "3mf"].includes(format2);
|
|
2116
|
+
const extension = format2 === "obj" && colors ? "zip" : format2;
|
|
1601
2117
|
return {
|
|
1602
|
-
format,
|
|
2118
|
+
format: format2,
|
|
1603
2119
|
units,
|
|
1604
2120
|
colors,
|
|
1605
2121
|
apply_transforms: options.apply_transforms !== false,
|
|
@@ -1618,7 +2134,7 @@ function exportMemory(models, options) {
|
|
|
1618
2134
|
return MEMORY_OVERHEAD + triangles * perTriangle[options.format] + models.length * 4096;
|
|
1619
2135
|
}
|
|
1620
2136
|
|
|
1621
|
-
// src/
|
|
2137
|
+
// src/io.js
|
|
1622
2138
|
async function readSource(source, limit, progress = () => {
|
|
1623
2139
|
}, signal) {
|
|
1624
2140
|
let stream, total = 0;
|
|
@@ -1680,7 +2196,7 @@ async function readSource(source, limit, progress = () => {
|
|
|
1680
2196
|
return data.buffer;
|
|
1681
2197
|
}
|
|
1682
2198
|
|
|
1683
|
-
// src/
|
|
2199
|
+
// src/three-bridge.js
|
|
1684
2200
|
function fromThreeMesh(mesh, limits) {
|
|
1685
2201
|
if (!mesh?.isMesh && !mesh?.geometry)
|
|
1686
2202
|
throw new Error("Expected a Three.js mesh");
|
|
@@ -1753,7 +2269,7 @@ function toThreeMesh(model, THREE) {
|
|
|
1753
2269
|
return mesh;
|
|
1754
2270
|
}
|
|
1755
2271
|
|
|
1756
|
-
// src/
|
|
2272
|
+
// src/viewer.js
|
|
1757
2273
|
var transformKeys = [
|
|
1758
2274
|
"x",
|
|
1759
2275
|
"y",
|
|
@@ -1791,7 +2307,10 @@ var modelKeys = [
|
|
|
1791
2307
|
"smooth_shading",
|
|
1792
2308
|
"section",
|
|
1793
2309
|
"units",
|
|
1794
|
-
"animation"
|
|
2310
|
+
"animation",
|
|
2311
|
+
"animation_state",
|
|
2312
|
+
"size_state",
|
|
2313
|
+
"display_name"
|
|
1795
2314
|
];
|
|
1796
2315
|
var finite = (v, name) => {
|
|
1797
2316
|
if (typeof v !== "number" || !Number.isFinite(v))
|
|
@@ -1823,7 +2342,7 @@ function modelOptions(input) {
|
|
|
1823
2342
|
return out;
|
|
1824
2343
|
}
|
|
1825
2344
|
var StlViewer = class _StlViewer {
|
|
1826
|
-
static version = "2.0.0-rc.
|
|
2345
|
+
static version = "2.0.0-rc.6";
|
|
1827
2346
|
static workerURL = null;
|
|
1828
2347
|
constructor(parent, options = {}) {
|
|
1829
2348
|
if (!parent?.appendChild)
|
|
@@ -1852,6 +2371,8 @@ var StlViewer = class _StlViewer {
|
|
|
1852
2371
|
this._sessionActive = false;
|
|
1853
2372
|
this._frame = 0;
|
|
1854
2373
|
this._lastTime = 0;
|
|
2374
|
+
this.camera_bookmarks = [];
|
|
2375
|
+
this.display_units = "mm";
|
|
1855
2376
|
this.grid = null;
|
|
1856
2377
|
this._gridAuto = false;
|
|
1857
2378
|
this.camera = {
|
|
@@ -1873,7 +2394,9 @@ var StlViewer = class _StlViewer {
|
|
|
1873
2394
|
"model_loaded_callback",
|
|
1874
2395
|
"load_error_callback",
|
|
1875
2396
|
"loading_progress_callback",
|
|
1876
|
-
"on_model_drop"
|
|
2397
|
+
"on_model_drop",
|
|
2398
|
+
"animation_state_callback",
|
|
2399
|
+
"animation_complete_callback"
|
|
1877
2400
|
])
|
|
1878
2401
|
this[name] = options[name] ?? null;
|
|
1879
2402
|
this.onmousedown_callback = options.on_model_mousedown ?? options.on_model_mouseclick ?? null;
|
|
@@ -1912,11 +2435,15 @@ var StlViewer = class _StlViewer {
|
|
|
1912
2435
|
this.parent_element.appendChild(this.canvas);
|
|
1913
2436
|
this.renderer = new Renderer(
|
|
1914
2437
|
this.canvas,
|
|
1915
|
-
() =>
|
|
1916
|
-
|
|
1917
|
-
|
|
2438
|
+
() => {
|
|
2439
|
+
this._suspendAnimations(true);
|
|
2440
|
+
this._message(
|
|
2441
|
+
"Graphics interrupted. Waiting for the browser to restore rendering\u2026"
|
|
2442
|
+
);
|
|
2443
|
+
},
|
|
1918
2444
|
() => {
|
|
1919
2445
|
this._removeMessage();
|
|
2446
|
+
this._suspendAnimations(this.canvas.ownerDocument.hidden);
|
|
1920
2447
|
this._invalidate();
|
|
1921
2448
|
},
|
|
1922
2449
|
(e) => this._error(e)
|
|
@@ -1981,6 +2508,14 @@ var StlViewer = class _StlViewer {
|
|
|
1981
2508
|
}
|
|
1982
2509
|
};
|
|
1983
2510
|
this.canvas.addEventListener("keydown", this._key);
|
|
2511
|
+
this._visibility = () => this._suspendAnimations(
|
|
2512
|
+
this.canvas.ownerDocument.hidden || this.renderer.lost
|
|
2513
|
+
);
|
|
2514
|
+
this.canvas.ownerDocument.addEventListener(
|
|
2515
|
+
"visibilitychange",
|
|
2516
|
+
this._visibility
|
|
2517
|
+
);
|
|
2518
|
+
this._animationSuspended = this.canvas.ownerDocument.hidden;
|
|
1984
2519
|
this.ready = true;
|
|
1985
2520
|
this._readyResolve(this);
|
|
1986
2521
|
this._callback("ready_callback");
|
|
@@ -1995,6 +2530,11 @@ var StlViewer = class _StlViewer {
|
|
|
1995
2530
|
this.controls?.dispose();
|
|
1996
2531
|
this.renderer?.dispose();
|
|
1997
2532
|
this._resizeObserver?.disconnect();
|
|
2533
|
+
if (this._visibility)
|
|
2534
|
+
this.canvas.ownerDocument.removeEventListener(
|
|
2535
|
+
"visibilitychange",
|
|
2536
|
+
this._visibility
|
|
2537
|
+
);
|
|
1998
2538
|
}
|
|
1999
2539
|
}
|
|
2000
2540
|
_callback(name, ...args) {
|
|
@@ -2043,7 +2583,7 @@ var StlViewer = class _StlViewer {
|
|
|
2043
2583
|
const diagnostic = this.renderer?.diagnostic;
|
|
2044
2584
|
if (diagnostic && printSignature(diagnostic.model) !== diagnostic.signature)
|
|
2045
2585
|
this.renderer.setDiagnostic(null);
|
|
2046
|
-
if (this.killsign || !this.renderer || this.renderer.lost || this._frame)
|
|
2586
|
+
if (this.killsign || !this.renderer || this.renderer.lost || this._frame || this._animationSuspended)
|
|
2047
2587
|
return;
|
|
2048
2588
|
this._frame = requestAnimationFrame((t) => {
|
|
2049
2589
|
this._frame = 0;
|
|
@@ -2055,12 +2595,15 @@ var StlViewer = class _StlViewer {
|
|
|
2055
2595
|
this._lastTime = time;
|
|
2056
2596
|
let animated = false;
|
|
2057
2597
|
for (const m of this._models.values())
|
|
2058
|
-
if (m.geometry && m._animation) {
|
|
2598
|
+
if (m.geometry && !m._sceneImportPending && m._animation && !m._animation.paused) {
|
|
2059
2599
|
this._animate(m, time);
|
|
2060
|
-
animated ||= !!m._animation;
|
|
2600
|
+
animated ||= !!m._animation && !m._animation.paused;
|
|
2061
2601
|
}
|
|
2062
2602
|
if (this.auto_rotate && this.controls.enabled)
|
|
2063
2603
|
this.controls.orbit(elapsed * 0.3, 0);
|
|
2604
|
+
const diagnostic = this.renderer.diagnostic;
|
|
2605
|
+
if (diagnostic && printSignature(diagnostic.model) !== diagnostic.signature)
|
|
2606
|
+
this.renderer.setDiagnostic(null);
|
|
2064
2607
|
this._clip();
|
|
2065
2608
|
try {
|
|
2066
2609
|
this.renderer.draw(
|
|
@@ -2166,6 +2709,13 @@ var StlViewer = class _StlViewer {
|
|
|
2166
2709
|
finite(m[key], key);
|
|
2167
2710
|
if (key.startsWith("scale")) positive(m[key], key);
|
|
2168
2711
|
}
|
|
2712
|
+
if (supplied.size_state)
|
|
2713
|
+
m.size_state = validateSizeState(supplied.size_state);
|
|
2714
|
+
if (supplied.display_name != null) {
|
|
2715
|
+
if (typeof supplied.display_name !== "string" || supplied.display_name.length > 1024)
|
|
2716
|
+
throw new Error("Invalid model name");
|
|
2717
|
+
m.display_name = supplied.display_name;
|
|
2718
|
+
}
|
|
2169
2719
|
m.opacity = supplied.opacity ?? supplied.mesh?.material?.opacity ?? 1;
|
|
2170
2720
|
finite(m.opacity, "opacity");
|
|
2171
2721
|
if (m.opacity < 0 || m.opacity > 1)
|
|
@@ -2184,6 +2734,11 @@ var StlViewer = class _StlViewer {
|
|
|
2184
2734
|
m.color = supplied.color ?? (supplied.mesh?.material?.color?.getHexString ? "#" + supplied.mesh.material.color.getHexString() : null);
|
|
2185
2735
|
if (m.color && m.color !== "transparent") colorRGB(m.color);
|
|
2186
2736
|
m.view_edges = !!supplied.view_edges;
|
|
2737
|
+
m.animation = supplied.animation == null ? null : normalizeAnimation(supplied.animation);
|
|
2738
|
+
m.animation_state = supplied.animation_state == null ? null : normalizePlaybackState(supplied.animation_state);
|
|
2739
|
+
if (m.animation_state && !m.animation)
|
|
2740
|
+
throw new Error("Playback state needs an animation");
|
|
2741
|
+
if (m.animation) createPlayback(m.animation, m, m.animation_state);
|
|
2187
2742
|
Object.defineProperty(m, "scaleVector", {
|
|
2188
2743
|
get: () => [
|
|
2189
2744
|
m.scalex * m.unitScale,
|
|
@@ -2312,7 +2867,7 @@ var StlViewer = class _StlViewer {
|
|
|
2312
2867
|
format: supplied.remote_format,
|
|
2313
2868
|
relay,
|
|
2314
2869
|
source: typeof source === "string" ? new URL(source, document.baseURI).href : source,
|
|
2315
|
-
name: m.filename,
|
|
2870
|
+
name: m.display_name || m.filename,
|
|
2316
2871
|
limits,
|
|
2317
2872
|
features: features.smooth || features.edges || features.wire ? features : null
|
|
2318
2873
|
},
|
|
@@ -2336,7 +2891,10 @@ var StlViewer = class _StlViewer {
|
|
|
2336
2891
|
);
|
|
2337
2892
|
if ((supplied.mesh || supplied.data) && (features.smooth || features.edges || features.wire))
|
|
2338
2893
|
await this._derive(m, features);
|
|
2339
|
-
if (
|
|
2894
|
+
if (m.animation_state) m.center = [...m.animation_state.center];
|
|
2895
|
+
if (m.animation && !m._animation)
|
|
2896
|
+
this._startAnimation(m, m.animation, m.animation_state);
|
|
2897
|
+
delete m.animation_state;
|
|
2340
2898
|
delete m.mesh;
|
|
2341
2899
|
delete m.data;
|
|
2342
2900
|
this._invalidate();
|
|
@@ -2358,6 +2916,8 @@ var StlViewer = class _StlViewer {
|
|
|
2358
2916
|
this._invalidate();
|
|
2359
2917
|
}
|
|
2360
2918
|
_deleteModel(m) {
|
|
2919
|
+
this._stopAnimation(m, "removed");
|
|
2920
|
+
for (const controller of m._measureJobs || []) controller.abort();
|
|
2361
2921
|
for (const [controller, models] of this._printJobs || [])
|
|
2362
2922
|
if (models.has(m)) controller.abort();
|
|
2363
2923
|
this._printCache = null;
|
|
@@ -2385,9 +2945,12 @@ var StlViewer = class _StlViewer {
|
|
|
2385
2945
|
if (this.killsign) return;
|
|
2386
2946
|
this._generation++;
|
|
2387
2947
|
this._printCache = null;
|
|
2948
|
+
this.camera_bookmarks = [];
|
|
2949
|
+
this.display_units = "mm";
|
|
2388
2950
|
this._pool?.clear();
|
|
2389
2951
|
for (const c of this._aborters) c.abort();
|
|
2390
2952
|
this._aborters.clear();
|
|
2953
|
+
for (const m of this._models.values()) this._stopAnimation(m, "clean");
|
|
2391
2954
|
this._models.clear();
|
|
2392
2955
|
this._pending.clear();
|
|
2393
2956
|
this._jobs.clear();
|
|
@@ -2419,6 +2982,11 @@ var StlViewer = class _StlViewer {
|
|
|
2419
2982
|
window.removeEventListener("resize", this._windowResize);
|
|
2420
2983
|
this.renderer?.dispose();
|
|
2421
2984
|
if (this.canvas) {
|
|
2985
|
+
if (this._visibility)
|
|
2986
|
+
this.canvas.ownerDocument.removeEventListener(
|
|
2987
|
+
"visibilitychange",
|
|
2988
|
+
this._visibility
|
|
2989
|
+
);
|
|
2422
2990
|
this.canvas.removeEventListener("drop", this._drop);
|
|
2423
2991
|
this.canvas.removeEventListener("dragover", this._drag);
|
|
2424
2992
|
this.canvas.removeEventListener("keydown", this._key);
|
|
@@ -2557,6 +3125,10 @@ var StlViewer = class _StlViewer {
|
|
|
2557
3125
|
set_position(id, x, y, z) {
|
|
2558
3126
|
return this._guard(() => {
|
|
2559
3127
|
const m = this._model(id);
|
|
3128
|
+
[x, y, z].forEach((v) => {
|
|
3129
|
+
if (v !== void 0) finite(v, "position");
|
|
3130
|
+
});
|
|
3131
|
+
this._stopAnimation(m, "manual");
|
|
2560
3132
|
[x, y, z].forEach((v, i) => {
|
|
2561
3133
|
if (v !== void 0) m[["x", "y", "z"][i]] = finite(v, "position");
|
|
2562
3134
|
});
|
|
@@ -2570,8 +3142,12 @@ var StlViewer = class _StlViewer {
|
|
|
2570
3142
|
if (keepPrevious) {
|
|
2571
3143
|
if (x / m.scalex !== y / m.scaley || x / m.scalex !== z / m.scalez)
|
|
2572
3144
|
throw new Error("keep_prev_scale requires a uniform geometry factor");
|
|
3145
|
+
this._stopAnimation(m, "manual");
|
|
2573
3146
|
m.unitScale *= x / m.scalex;
|
|
2574
|
-
} else
|
|
3147
|
+
} else {
|
|
3148
|
+
this._stopAnimation(m, "manual");
|
|
3149
|
+
[m.scalex, m.scaley, m.scalez] = [x, y, z];
|
|
3150
|
+
}
|
|
2575
3151
|
m._stats = null;
|
|
2576
3152
|
this._invalidate();
|
|
2577
3153
|
});
|
|
@@ -2585,6 +3161,10 @@ var StlViewer = class _StlViewer {
|
|
|
2585
3161
|
set_rotation(id, x, y, z, increment = false) {
|
|
2586
3162
|
return this._guard(() => {
|
|
2587
3163
|
const m = this._model(id);
|
|
3164
|
+
[x, y, z].forEach((v) => {
|
|
3165
|
+
if (v !== void 0) finite(v, "rotation");
|
|
3166
|
+
});
|
|
3167
|
+
this._stopAnimation(m, "manual");
|
|
2588
3168
|
[x, y, z].forEach((v, i) => {
|
|
2589
3169
|
const k = ["rotationx", "rotationy", "rotationz"][i];
|
|
2590
3170
|
if (v !== void 0)
|
|
@@ -2638,6 +3218,7 @@ var StlViewer = class _StlViewer {
|
|
|
2638
3218
|
if (!["mm", "inch"].includes(units))
|
|
2639
3219
|
throw new Error("Units must be mm or inch");
|
|
2640
3220
|
const m = this._model(id);
|
|
3221
|
+
this._stopAnimation(m, "manual");
|
|
2641
3222
|
if (scale && m.units !== units)
|
|
2642
3223
|
m.unitScale *= units === "inch" ? 25.4 : 1 / 25.4;
|
|
2643
3224
|
m.units = units;
|
|
@@ -2805,12 +3386,42 @@ var StlViewer = class _StlViewer {
|
|
|
2805
3386
|
type
|
|
2806
3387
|
);
|
|
2807
3388
|
}
|
|
2808
|
-
|
|
3389
|
+
get_model_size(id) {
|
|
3390
|
+
return modelSize(this._model(id));
|
|
3391
|
+
}
|
|
3392
|
+
resize_model(id, factors) {
|
|
3393
|
+
return resizeModel.call(this, id, factors);
|
|
3394
|
+
}
|
|
3395
|
+
set_file_units(id, units) {
|
|
3396
|
+
return setFileUnits.call(this, id, units);
|
|
3397
|
+
}
|
|
3398
|
+
reset_model_size(id) {
|
|
3399
|
+
return resetModelSize.call(this, id);
|
|
3400
|
+
}
|
|
3401
|
+
duplicate_model(id) {
|
|
3402
|
+
return duplicateModel.call(this, id);
|
|
3403
|
+
}
|
|
3404
|
+
set_camera_view(name) {
|
|
3405
|
+
return cameraView.call(this, name);
|
|
3406
|
+
}
|
|
3407
|
+
set_display_units(units) {
|
|
3408
|
+
this.display_units = validateUnits(units);
|
|
3409
|
+
}
|
|
3410
|
+
get_camera_bookmarks() {
|
|
3411
|
+
return structuredClone(this.camera_bookmarks || []);
|
|
3412
|
+
}
|
|
3413
|
+
set_camera_bookmarks(bookmarks) {
|
|
3414
|
+
this.camera_bookmarks = validateBookmarks(bookmarks);
|
|
3415
|
+
}
|
|
3416
|
+
measure_model(id, options) {
|
|
3417
|
+
return measureModel.call(this, id, options);
|
|
3418
|
+
}
|
|
3419
|
+
get_model_info(id, options = {}) {
|
|
2809
3420
|
return this._guard(() => {
|
|
2810
3421
|
const m = this._model(id), g = m.geometry;
|
|
2811
3422
|
if (!g) return null;
|
|
2812
3423
|
const s = m.scaleVector, key = s.join(",");
|
|
2813
|
-
if (!m._stats || m._stats.key !== key)
|
|
3424
|
+
if (options.measurements !== false && (!m._stats || m._stats.key !== key))
|
|
2814
3425
|
m._stats = { key, ...scaledStats(g, s) };
|
|
2815
3426
|
const factor = m.units === "inch" ? 1 / 25.4 : 1;
|
|
2816
3427
|
return {
|
|
@@ -2826,8 +3437,8 @@ var StlViewer = class _StlViewer {
|
|
|
2826
3437
|
section: m.section ? { ...m.section } : null,
|
|
2827
3438
|
color: m.color ?? (g.colors ? "#ffffff" : "#909090"),
|
|
2828
3439
|
scale: { x: m.scalex, y: m.scaley, z: m.scalez },
|
|
2829
|
-
volume: m._stats.volume * factor ** 3,
|
|
2830
|
-
area: m._stats.area * factor ** 2,
|
|
3440
|
+
volume: options.measurements === false ? null : m._stats.volume * factor ** 3,
|
|
3441
|
+
area: options.measurements === false ? null : m._stats.area * factor ** 2,
|
|
2831
3442
|
triangles: g.triangles,
|
|
2832
3443
|
bounds: { min: [...g.bounds.min], max: [...g.bounds.max] },
|
|
2833
3444
|
has_colors: !!g.colors,
|
|
@@ -2915,7 +3526,9 @@ var StlViewer = class _StlViewer {
|
|
|
2915
3526
|
rotationx: 0,
|
|
2916
3527
|
rotationy: 0,
|
|
2917
3528
|
rotationz: 0,
|
|
2918
|
-
scaleVector: [
|
|
3529
|
+
scaleVector: [1, 1, 1].map(
|
|
3530
|
+
() => m.unitScale * (m.size_state?.unit_factor ?? 1)
|
|
3531
|
+
)
|
|
2919
3532
|
};
|
|
2920
3533
|
const matrix = modelMatrix(source), factor = settings.units === "inch" ? 1 / 25.4 : 1;
|
|
2921
3534
|
for (let c = 0; c < 4; c++)
|
|
@@ -2983,8 +3596,8 @@ var StlViewer = class _StlViewer {
|
|
|
2983
3596
|
);
|
|
2984
3597
|
}
|
|
2985
3598
|
async export_scene(options = {}) {
|
|
2986
|
-
const
|
|
2987
|
-
if (!["vsb", "vsj"].includes(
|
|
3599
|
+
const format2 = String(options.format || "").toLowerCase();
|
|
3600
|
+
if (!["vsb", "vsj"].includes(format2))
|
|
2988
3601
|
throw new Error("Scene format must be vsb or vsj");
|
|
2989
3602
|
if (options.force_basename !== void 0 && typeof options.force_basename !== "boolean")
|
|
2990
3603
|
throw new Error("force_basename must be a boolean");
|
|
@@ -3007,7 +3620,7 @@ var StlViewer = class _StlViewer {
|
|
|
3007
3620
|
if (!models.length) throw new Error("No models to export");
|
|
3008
3621
|
if (models.some((m) => !m.geometry))
|
|
3009
3622
|
throw new Error("A model has not finished loading");
|
|
3010
|
-
if (models.length + (
|
|
3623
|
+
if (models.length + (format2 === "vsb" ? 1 : 0) > this.limits.max_archive_entries)
|
|
3011
3624
|
throw new Error("Scene model count limit exceeded");
|
|
3012
3625
|
const triangles = models.reduce((sum, m) => sum + m.geometry.triangles, 0);
|
|
3013
3626
|
if (triangles > this.limits.max_triangles)
|
|
@@ -3015,18 +3628,26 @@ var StlViewer = class _StlViewer {
|
|
|
3015
3628
|
const scene = this.get_vsj(
|
|
3016
3629
|
true,
|
|
3017
3630
|
!!options.force_basename,
|
|
3018
|
-
|
|
3631
|
+
format2 === "vsb"
|
|
3019
3632
|
);
|
|
3020
3633
|
const json = JSON.stringify(scene), manifestBytes = new Blob([json]).size;
|
|
3021
3634
|
if (manifestBytes > this.limits.max_file_bytes)
|
|
3022
3635
|
throw new Error("Scene manifest size limit exceeded");
|
|
3023
|
-
if (
|
|
3636
|
+
if (format2 === "vsb" && manifestBytes + models.length * 84 + triangles * 50 > this.limits.max_uncompressed_bytes)
|
|
3024
3637
|
throw new Error("Scene archive limit exceeded");
|
|
3025
|
-
const working = MEMORY_OVERHEAD + json.length * 8 + (
|
|
3638
|
+
const working = MEMORY_OVERHEAD + json.length * 8 + (format2 === "vsb" ? triangles * 512 : 0);
|
|
3026
3639
|
assertMemory(this.limits, this._occupiedMemory() + working, "scene export");
|
|
3027
3640
|
this._exportBytes += working;
|
|
3028
3641
|
let job, cancel, callbackError;
|
|
3029
3642
|
try {
|
|
3643
|
+
const snapshot = format2 === "vsb" ? models.map((m, index) => ({
|
|
3644
|
+
id: m.id,
|
|
3645
|
+
triangles: m.geometry.triangles,
|
|
3646
|
+
positions: m.geometry.positions.slice(),
|
|
3647
|
+
colors: m.geometry.colors?.slice() || null,
|
|
3648
|
+
scale: scene.models[index].animation_state ? [1, 1, 1] : m.scaleVector,
|
|
3649
|
+
center: scene.models[index].animation_state ? [0, 0, 0] : [...m.center]
|
|
3650
|
+
})) : null;
|
|
3030
3651
|
options.on_progress?.({
|
|
3031
3652
|
loaded: 0,
|
|
3032
3653
|
total: models.length,
|
|
@@ -3034,7 +3655,7 @@ var StlViewer = class _StlViewer {
|
|
|
3034
3655
|
});
|
|
3035
3656
|
checkAbort();
|
|
3036
3657
|
let blob;
|
|
3037
|
-
if (
|
|
3658
|
+
if (format2 === "vsj") {
|
|
3038
3659
|
blob = new Blob([json], { type: "application/json" });
|
|
3039
3660
|
options.on_progress?.({
|
|
3040
3661
|
loaded: models.length,
|
|
@@ -3042,14 +3663,6 @@ var StlViewer = class _StlViewer {
|
|
|
3042
3663
|
phase: "complete"
|
|
3043
3664
|
});
|
|
3044
3665
|
} else {
|
|
3045
|
-
const snapshot = models.map((m) => ({
|
|
3046
|
-
id: m.id,
|
|
3047
|
-
triangles: m.geometry.triangles,
|
|
3048
|
-
positions: m.geometry.positions.slice(),
|
|
3049
|
-
colors: m.geometry.colors?.slice() || null,
|
|
3050
|
-
scale: m.scaleVector,
|
|
3051
|
-
center: [...m.center]
|
|
3052
|
-
}));
|
|
3053
3666
|
job = this._pool.run(
|
|
3054
3667
|
{
|
|
3055
3668
|
kind: "export-scene",
|
|
@@ -3082,8 +3695,8 @@ var StlViewer = class _StlViewer {
|
|
|
3082
3695
|
checkAbort();
|
|
3083
3696
|
return {
|
|
3084
3697
|
blob,
|
|
3085
|
-
format,
|
|
3086
|
-
filename: exportFilename(options.filename,
|
|
3698
|
+
format: format2,
|
|
3699
|
+
filename: exportFilename(options.filename, format2),
|
|
3087
3700
|
model_ids: models.map((m) => m.id),
|
|
3088
3701
|
triangles,
|
|
3089
3702
|
representation: "scene"
|
|
@@ -3105,6 +3718,8 @@ var StlViewer = class _StlViewer {
|
|
|
3105
3718
|
canvas_height: this.canvas_height,
|
|
3106
3719
|
bg_color: this.bg_color,
|
|
3107
3720
|
camera_state: this.get_camera_state(),
|
|
3721
|
+
camera_bookmarks: this.get_camera_bookmarks(),
|
|
3722
|
+
display_units: this.display_units || "mm",
|
|
3108
3723
|
auto_rotate: this.auto_rotate,
|
|
3109
3724
|
mouse_zoom: this.mouse_zoom,
|
|
3110
3725
|
auto_resize: this.auto_resize,
|
|
@@ -3125,6 +3740,11 @@ var StlViewer = class _StlViewer {
|
|
|
3125
3740
|
id: for_vsb ? -1 : m.id,
|
|
3126
3741
|
filename: for_vsb ? `${m.id}.stl` : force_basename ? basename(m.filename) : m.filename,
|
|
3127
3742
|
units: m.units,
|
|
3743
|
+
size_state: {
|
|
3744
|
+
...structuredClone(sizeState(m)),
|
|
3745
|
+
unit_factor: for_vsb && !m._animation ? 1 : m.unitScale * (sizeState(m).unit_factor ?? 1)
|
|
3746
|
+
},
|
|
3747
|
+
...m.display_name ? { display_name: m.display_name } : {},
|
|
3128
3748
|
display: m.display,
|
|
3129
3749
|
opacity: m.opacity,
|
|
3130
3750
|
view_edges: m.view_edges,
|
|
@@ -3135,13 +3755,21 @@ var StlViewer = class _StlViewer {
|
|
|
3135
3755
|
model.orig_filename = basename(m.orig_filename || m.filename);
|
|
3136
3756
|
for (const key of transformKeys)
|
|
3137
3757
|
if (!key.startsWith("scale")) model[key] = m[key];
|
|
3138
|
-
if (!for_vsb) {
|
|
3758
|
+
if (!for_vsb || m._animation) {
|
|
3139
3759
|
model.scalex = m.scalex * m.unitScale;
|
|
3140
3760
|
model.scaley = m.scaley * m.unitScale;
|
|
3141
3761
|
model.scalez = m.scalez * m.unitScale;
|
|
3142
3762
|
}
|
|
3143
3763
|
if (m.color) model.color = m.color;
|
|
3144
|
-
|
|
3764
|
+
const playback = savePlayback(m);
|
|
3765
|
+
if (playback) Object.assign(model, playback);
|
|
3766
|
+
else if (!for_vsb) {
|
|
3767
|
+
const center = this.center_models ? m.geometry.bounds.min.map(
|
|
3768
|
+
(v, i) => (v + m.geometry.bounds.max[i]) / 2
|
|
3769
|
+
) : [0, 0, 0];
|
|
3770
|
+
const position = transform(modelMatrix(m), center);
|
|
3771
|
+
["x", "y", "z"].forEach((axis, i) => model[axis] = position[i]);
|
|
3772
|
+
}
|
|
3145
3773
|
data.models.push(model);
|
|
3146
3774
|
}
|
|
3147
3775
|
return as_js_obj ? data : JSON.stringify(data);
|
|
@@ -3155,7 +3783,10 @@ var StlViewer = class _StlViewer {
|
|
|
3155
3783
|
get_vsb() {
|
|
3156
3784
|
return {
|
|
3157
3785
|
vsj: this.get_vsj(true, true, true),
|
|
3158
|
-
files: [...this._models.values()].filter((m) => m.geometry).map((m) => ({
|
|
3786
|
+
files: [...this._models.values()].filter((m) => m.geometry).map((m) => ({
|
|
3787
|
+
id: m.id,
|
|
3788
|
+
bin: m._animation ? exportSTL(m.geometry, [1, 1, 1], [0, 0, 0]) : this.get_stl_bin(m.id)
|
|
3789
|
+
}))
|
|
3159
3790
|
};
|
|
3160
3791
|
}
|
|
3161
3792
|
async get_vsb_blob() {
|
|
@@ -3236,20 +3867,66 @@ var StlViewer = class _StlViewer {
|
|
|
3236
3867
|
);
|
|
3237
3868
|
else if (scene.grid) this.set_grid(true);
|
|
3238
3869
|
if (scene.camera_state) this._restoreCamera = scene.camera_state;
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
model
|
|
3252
|
-
|
|
3870
|
+
const bookmarks = mergeBookmarks(
|
|
3871
|
+
this.get_camera_bookmarks(),
|
|
3872
|
+
scene.camera_bookmarks || []
|
|
3873
|
+
);
|
|
3874
|
+
const displayUnits = validateUnits(
|
|
3875
|
+
scene.display_units || this.display_units || "mm"
|
|
3876
|
+
);
|
|
3877
|
+
const incoming = [];
|
|
3878
|
+
try {
|
|
3879
|
+
for (const input of scene.models) {
|
|
3880
|
+
if (!input || typeof input !== "object")
|
|
3881
|
+
throw new Error("Invalid scene model");
|
|
3882
|
+
const model = modelOptions(input);
|
|
3883
|
+
delete model.mesh;
|
|
3884
|
+
delete model.data;
|
|
3885
|
+
delete model.local_file;
|
|
3886
|
+
if (files) {
|
|
3887
|
+
if (typeof model.filename !== "string" || !Object.hasOwn(files, model.filename))
|
|
3888
|
+
throw new Error("VSB model file is missing");
|
|
3889
|
+
model.local_file = new File(
|
|
3890
|
+
[files[model.filename]],
|
|
3891
|
+
model.filename
|
|
3892
|
+
);
|
|
3893
|
+
} else if (model.filename)
|
|
3894
|
+
model.filename = new URL(model.filename, base).href;
|
|
3895
|
+
const id = model.id === void 0 || model.id === -1 ? this.max_model_id + 1 : model.id;
|
|
3896
|
+
if (this._models.has(id))
|
|
3897
|
+
throw new Error(`Invalid or duplicate model id: ${id}`);
|
|
3898
|
+
this.add_model({
|
|
3899
|
+
...model,
|
|
3900
|
+
id,
|
|
3901
|
+
animation: null,
|
|
3902
|
+
animation_state: null
|
|
3903
|
+
});
|
|
3904
|
+
const m = this._models.get(id);
|
|
3905
|
+
if (!m) throw new Error(this.error || "Invalid scene model");
|
|
3906
|
+
m._sceneImportPending = true;
|
|
3907
|
+
incoming.push({ m, model, pending: this._pending.get(id) });
|
|
3908
|
+
}
|
|
3909
|
+
await Promise.all(incoming.map((item) => item.pending));
|
|
3910
|
+
if (generation !== this._generation || this.killsign) return;
|
|
3911
|
+
for (const { m, model } of incoming) {
|
|
3912
|
+
if (this._models.get(m.id) !== m || !m.geometry)
|
|
3913
|
+
throw new Error("A scene model could not be opened");
|
|
3914
|
+
if (model.animation)
|
|
3915
|
+
createPlayback(model.animation, m, model.animation_state);
|
|
3916
|
+
else if (model.animation_state)
|
|
3917
|
+
throw new Error("Playback state needs an animation");
|
|
3918
|
+
}
|
|
3919
|
+
for (const { m, model } of incoming) {
|
|
3920
|
+
if (model.animation)
|
|
3921
|
+
this._startAnimation(m, model.animation, model.animation_state);
|
|
3922
|
+
m._sceneImportPending = false;
|
|
3923
|
+
}
|
|
3924
|
+
this.camera_bookmarks = bookmarks;
|
|
3925
|
+
this.display_units = displayUnits;
|
|
3926
|
+
} catch (error) {
|
|
3927
|
+
for (const { m } of incoming)
|
|
3928
|
+
if (this._models.get(m.id) === m) this.remove_model(m.id);
|
|
3929
|
+
throw error;
|
|
3253
3930
|
}
|
|
3254
3931
|
})().catch((e) => {
|
|
3255
3932
|
if (generation === this._generation && e.name !== "AbortError")
|
|
@@ -3271,56 +3948,101 @@ var StlViewer = class _StlViewer {
|
|
|
3271
3948
|
return this._guard(() => {
|
|
3272
3949
|
const m = this._model(id);
|
|
3273
3950
|
if (!animation) {
|
|
3274
|
-
m
|
|
3275
|
-
|
|
3951
|
+
this._stopAnimation(m, "stopped");
|
|
3952
|
+
this._invalidate();
|
|
3276
3953
|
return;
|
|
3277
3954
|
}
|
|
3278
|
-
|
|
3279
|
-
for (const mode of ["delta", "exact"])
|
|
3280
|
-
if (copy[mode]) {
|
|
3281
|
-
const a = copy[mode];
|
|
3282
|
-
positive(a.msec ?? 300, "animation duration");
|
|
3283
|
-
const values = {};
|
|
3284
|
-
for (const key of transformKeys) {
|
|
3285
|
-
const value = a[key] ?? (key.startsWith("scale") ? a.scale : void 0);
|
|
3286
|
-
if (value !== void 0) {
|
|
3287
|
-
finite(value, key);
|
|
3288
|
-
if (mode === "exact" && key.startsWith("scale"))
|
|
3289
|
-
positive(value, key);
|
|
3290
|
-
values[key] = value;
|
|
3291
|
-
}
|
|
3292
|
-
}
|
|
3293
|
-
tracks.push({
|
|
3294
|
-
mode,
|
|
3295
|
-
values,
|
|
3296
|
-
duration: a.msec ?? 300,
|
|
3297
|
-
loop: mode === "delta" && !!a.loop,
|
|
3298
|
-
from: Object.fromEntries(transformKeys.map((k) => [k, m[k]]))
|
|
3299
|
-
});
|
|
3300
|
-
}
|
|
3301
|
-
if (!tracks.length) throw new Error("Animation needs delta or exact");
|
|
3302
|
-
m.animation = copy;
|
|
3303
|
-
m._animation = { start: null, tracks };
|
|
3955
|
+
this._startAnimation(m, animation);
|
|
3304
3956
|
this._invalidate();
|
|
3305
3957
|
});
|
|
3306
3958
|
}
|
|
3959
|
+
_startAnimation(m, definition, saved) {
|
|
3960
|
+
const playback = createPlayback(definition, m, saved);
|
|
3961
|
+
if (m._animation) this._stopAnimation(m, "replaced");
|
|
3962
|
+
if (saved) m.center = [...saved.center];
|
|
3963
|
+
m.animation = playback.animation;
|
|
3964
|
+
m._animation = playback;
|
|
3965
|
+
delete m.animation_state;
|
|
3966
|
+
m._animationResult = null;
|
|
3967
|
+
const running = evaluatePlayback(playback, m);
|
|
3968
|
+
if (running) this._animationEvent(m, saved ? "restored" : "started");
|
|
3969
|
+
else this._completeAnimation(m);
|
|
3970
|
+
}
|
|
3971
|
+
_animationEvent(m, reason) {
|
|
3972
|
+
const state = this._animationState(m);
|
|
3973
|
+
queueMicrotask(() => {
|
|
3974
|
+
this._callback("animation_state_callback", m.id, state, reason);
|
|
3975
|
+
if (reason === "completed")
|
|
3976
|
+
this._callback("animation_complete_callback", m.id, state);
|
|
3977
|
+
});
|
|
3978
|
+
}
|
|
3979
|
+
_animationState(m) {
|
|
3980
|
+
return m._animation ? playbackState(m._animation, !!this._animationSuspended) : {
|
|
3981
|
+
...m._animationResult ?? { status: "idle", elapsed_ms: 0 },
|
|
3982
|
+
suspended: false,
|
|
3983
|
+
tracks: []
|
|
3984
|
+
};
|
|
3985
|
+
}
|
|
3986
|
+
_stopAnimation(m, reason) {
|
|
3987
|
+
if (!m._animation && !m.animation) return;
|
|
3988
|
+
m._animationResult = {
|
|
3989
|
+
status: "stopped",
|
|
3990
|
+
elapsed_ms: m._animation?.elapsed_ms ?? 0
|
|
3991
|
+
};
|
|
3992
|
+
m._animation = null;
|
|
3993
|
+
m.animation = null;
|
|
3994
|
+
delete m.animation_state;
|
|
3995
|
+
this._animationEvent(m, reason);
|
|
3996
|
+
}
|
|
3997
|
+
_completeAnimation(m) {
|
|
3998
|
+
m._animationResult = {
|
|
3999
|
+
status: "completed",
|
|
4000
|
+
elapsed_ms: m._animation.elapsed_ms
|
|
4001
|
+
};
|
|
4002
|
+
m._animation = null;
|
|
4003
|
+
m.animation = null;
|
|
4004
|
+
this._animationEvent(m, "completed");
|
|
4005
|
+
}
|
|
4006
|
+
pause_model_animation(id) {
|
|
4007
|
+
return this._guard(() => {
|
|
4008
|
+
const m = this._model(id);
|
|
4009
|
+
if (!m._animation || m._animation.paused) return;
|
|
4010
|
+
m._animation.paused = true;
|
|
4011
|
+
m._animation.last_time = null;
|
|
4012
|
+
this._animationEvent(m, "paused");
|
|
4013
|
+
this._invalidate();
|
|
4014
|
+
});
|
|
4015
|
+
}
|
|
4016
|
+
resume_model_animation(id) {
|
|
4017
|
+
return this._guard(() => {
|
|
4018
|
+
const m = this._model(id);
|
|
4019
|
+
if (!m._animation || !m._animation.paused) return;
|
|
4020
|
+
m._animation.paused = false;
|
|
4021
|
+
m._animation.last_time = null;
|
|
4022
|
+
this._animationEvent(m, "resumed");
|
|
4023
|
+
this._invalidate();
|
|
4024
|
+
});
|
|
4025
|
+
}
|
|
4026
|
+
get_model_animation_state(id) {
|
|
4027
|
+
return this._guard(() => this._animationState(this._model(id)));
|
|
4028
|
+
}
|
|
4029
|
+
_suspendAnimations(suspended) {
|
|
4030
|
+
this._animationSuspended = !!suspended;
|
|
4031
|
+
this._lastTime = 0;
|
|
4032
|
+
for (const m of this._models.values())
|
|
4033
|
+
if (m._animation) m._animation.last_time = null;
|
|
4034
|
+
if (suspended) {
|
|
4035
|
+
cancelAnimationFrame(this._frame);
|
|
4036
|
+
this._frame = 0;
|
|
4037
|
+
} else this._invalidate();
|
|
4038
|
+
}
|
|
3307
4039
|
_animate(m, time) {
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
for (const [k, v] of Object.entries(t.values))
|
|
3315
|
-
m[k] = k.startsWith("scale") ? Math.max(
|
|
3316
|
-
1e-6,
|
|
3317
|
-
t.mode === "delta" ? t.from[k] + v * p : t.from[k] + (v - t.from[k]) * p
|
|
3318
|
-
) : t.mode === "delta" ? t.from[k] + v * p : t.from[k] + (v - t.from[k]) * p;
|
|
3319
|
-
}
|
|
3320
|
-
m._stats = null;
|
|
3321
|
-
if (!running) {
|
|
3322
|
-
m._animation = null;
|
|
3323
|
-
m.animation = null;
|
|
4040
|
+
if (this._animationSuspended || !m._animation) return;
|
|
4041
|
+
try {
|
|
4042
|
+
if (!advancePlayback(m._animation, m, time)) this._completeAnimation(m);
|
|
4043
|
+
} catch (error) {
|
|
4044
|
+
this._stopAnimation(m, "error");
|
|
4045
|
+
this._error(error);
|
|
3324
4046
|
}
|
|
3325
4047
|
}
|
|
3326
4048
|
async get_image_blob(type = "image/png") {
|
|
@@ -3343,7 +4065,300 @@ var StlViewer = class _StlViewer {
|
|
|
3343
4065
|
}
|
|
3344
4066
|
};
|
|
3345
4067
|
|
|
3346
|
-
// src/
|
|
4068
|
+
// src/app-tools.js
|
|
4069
|
+
var toolsControls = [
|
|
4070
|
+
"size-details",
|
|
4071
|
+
"size-fields",
|
|
4072
|
+
"size-x",
|
|
4073
|
+
"size-y",
|
|
4074
|
+
"size-z",
|
|
4075
|
+
"size-lock",
|
|
4076
|
+
"size-percent",
|
|
4077
|
+
"size-apply",
|
|
4078
|
+
"size-cancel",
|
|
4079
|
+
"size-reset",
|
|
4080
|
+
"size-error",
|
|
4081
|
+
"file-units",
|
|
4082
|
+
"file-units-note",
|
|
4083
|
+
"display-units",
|
|
4084
|
+
"size-unit-label",
|
|
4085
|
+
"duplicate-model",
|
|
4086
|
+
"camera-preset",
|
|
4087
|
+
"bookmark-name",
|
|
4088
|
+
"bookmark-list",
|
|
4089
|
+
"bookmark-save",
|
|
4090
|
+
"bookmark-restore",
|
|
4091
|
+
"bookmark-rename",
|
|
4092
|
+
"bookmark-delete",
|
|
4093
|
+
"camera-error",
|
|
4094
|
+
"info-area",
|
|
4095
|
+
"info-volume",
|
|
4096
|
+
"measure-model",
|
|
4097
|
+
"measure-cancel",
|
|
4098
|
+
"measure-status"
|
|
4099
|
+
];
|
|
4100
|
+
var axes2 = ["x", "y", "z"];
|
|
4101
|
+
var format = new Intl.NumberFormat(void 0, { maximumSignificantDigits: 7 });
|
|
4102
|
+
function bindAppTools({
|
|
4103
|
+
$,
|
|
4104
|
+
listen,
|
|
4105
|
+
window: window2,
|
|
4106
|
+
getViewer,
|
|
4107
|
+
getState,
|
|
4108
|
+
changeSize,
|
|
4109
|
+
duplicate,
|
|
4110
|
+
refresh,
|
|
4111
|
+
announce
|
|
4112
|
+
}) {
|
|
4113
|
+
let draftKind = null, lastAxis = 0, base = null, selected = null, measurement = null, disposed = false;
|
|
4114
|
+
const error = (id, message = "") => {
|
|
4115
|
+
$(id).textContent = message;
|
|
4116
|
+
$(id).hidden = !message;
|
|
4117
|
+
};
|
|
4118
|
+
const factor = () => getViewer()?.display_units === "inch" ? 25.4 : 1;
|
|
4119
|
+
const value = (v) => Number(v.toPrecision(10));
|
|
4120
|
+
function syncInfo() {
|
|
4121
|
+
const v = getViewer(), { selectedId, busy } = getState(), m = v?._models.get(selectedId);
|
|
4122
|
+
const s = m?.scaleVector, cached = m?._measurements;
|
|
4123
|
+
const valid = cached && cached.positions === m.geometry.positions && cached.key === s.join(",");
|
|
4124
|
+
const unit = factor() === 1 ? "mm" : "in";
|
|
4125
|
+
const area = valid ? cached.result.area_mm2 : s && s[0] === s[1] && s[1] === s[2] ? m.geometry.area * s[0] ** 2 : null;
|
|
4126
|
+
$("info-area").textContent = area == null ? "Calculate" : format.format(area / factor() ** 2) + " " + unit + "\xB2";
|
|
4127
|
+
$("info-volume").textContent = valid ? cached.result.volume_valid ? format.format(cached.result.volume_mm3 / factor() ** 3) + " " + unit + "\xB3" : "Unavailable" : "Not checked";
|
|
4128
|
+
if (!measurement)
|
|
4129
|
+
$("measure-status").textContent = valid ? cached.result.volume_reason : "Calculate to check enclosed volume. Runs on your device.";
|
|
4130
|
+
$("measure-model").disabled = busy || !m || !!measurement;
|
|
4131
|
+
$("measure-cancel").hidden = !measurement;
|
|
4132
|
+
}
|
|
4133
|
+
function syncBookmarks() {
|
|
4134
|
+
const v = getViewer(), previous = $("bookmark-list").value, bookmarks = v?.get_camera_bookmarks() || [];
|
|
4135
|
+
$("bookmark-list").replaceChildren(
|
|
4136
|
+
...bookmarks.map((b) => {
|
|
4137
|
+
const option = window2.document.createElement("option");
|
|
4138
|
+
option.value = b.name;
|
|
4139
|
+
option.textContent = b.name;
|
|
4140
|
+
return option;
|
|
4141
|
+
})
|
|
4142
|
+
);
|
|
4143
|
+
if (bookmarks.some((b) => b.name === previous))
|
|
4144
|
+
$("bookmark-list").value = previous;
|
|
4145
|
+
for (const id of [
|
|
4146
|
+
"bookmark-restore",
|
|
4147
|
+
"bookmark-rename",
|
|
4148
|
+
"bookmark-delete",
|
|
4149
|
+
"bookmark-list"
|
|
4150
|
+
])
|
|
4151
|
+
$(id).disabled = !bookmarks.length;
|
|
4152
|
+
$("bookmark-save").disabled = !v || bookmarks.length >= 32;
|
|
4153
|
+
}
|
|
4154
|
+
function sync() {
|
|
4155
|
+
if (disposed) return;
|
|
4156
|
+
const { selectedId, busy } = getState(), v = getViewer(), m = v?._models.get(selectedId);
|
|
4157
|
+
if (selectedId !== selected) {
|
|
4158
|
+
measurement?.abort();
|
|
4159
|
+
selected = selectedId;
|
|
4160
|
+
}
|
|
4161
|
+
draftKind = null;
|
|
4162
|
+
base = m?.geometry ? v.get_model_size(selectedId) : null;
|
|
4163
|
+
$("size-fields").disabled = busy || !base;
|
|
4164
|
+
$("duplicate-model").disabled = busy || !base;
|
|
4165
|
+
$("display-units").value = v?.display_units || "mm";
|
|
4166
|
+
$("size-unit-label").textContent = factor() === 1 ? "Dimensions in mm" : "Dimensions in inches";
|
|
4167
|
+
$("file-units").value = base?.file_units || "mm";
|
|
4168
|
+
$("file-units").disabled = busy || !base?.editable;
|
|
4169
|
+
$("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.";
|
|
4170
|
+
axes2.forEach((a, i) => {
|
|
4171
|
+
$("size-" + a).value = base ? value(base.dimensions_mm[i] / factor()) : "";
|
|
4172
|
+
$("size-" + a).disabled = !base || base.dimensions_mm[i] === 0;
|
|
4173
|
+
});
|
|
4174
|
+
const ratios = base?.baseline_mm.map((v2, i) => v2 ? base.dimensions_mm[i] / v2 : null).filter((v2) => v2 != null) || [];
|
|
4175
|
+
$("size-percent").value = ratios.length && ratios.every((v2) => Math.abs(v2 / ratios[0] - 1) < 1e-8) ? value(ratios[0] * 100) : "";
|
|
4176
|
+
error("size-error");
|
|
4177
|
+
syncInfo();
|
|
4178
|
+
syncBookmarks();
|
|
4179
|
+
}
|
|
4180
|
+
const attempt = (id, action) => {
|
|
4181
|
+
try {
|
|
4182
|
+
action();
|
|
4183
|
+
error(id);
|
|
4184
|
+
} catch (e) {
|
|
4185
|
+
error(id, e.message);
|
|
4186
|
+
}
|
|
4187
|
+
};
|
|
4188
|
+
axes2.forEach(
|
|
4189
|
+
(a, i) => listen($("size-" + a), "input", () => {
|
|
4190
|
+
if (!base) return;
|
|
4191
|
+
draftKind = "axis";
|
|
4192
|
+
lastAxis = i;
|
|
4193
|
+
const n = $("size-" + a).valueAsNumber;
|
|
4194
|
+
if (!Number.isFinite(n) || n <= 0) return;
|
|
4195
|
+
if ($("size-lock").checked)
|
|
4196
|
+
axes2.forEach((axis, j) => {
|
|
4197
|
+
if (i !== j)
|
|
4198
|
+
$("size-" + axis).value = value(
|
|
4199
|
+
n * base.dimensions_mm[j] / base.dimensions_mm[i]
|
|
4200
|
+
);
|
|
4201
|
+
});
|
|
4202
|
+
$("size-percent").value = "";
|
|
4203
|
+
})
|
|
4204
|
+
);
|
|
4205
|
+
listen($("size-percent"), "input", () => {
|
|
4206
|
+
draftKind = "percent";
|
|
4207
|
+
const n = $("size-percent").valueAsNumber;
|
|
4208
|
+
if (base && Number.isFinite(n) && n > 0)
|
|
4209
|
+
axes2.forEach(
|
|
4210
|
+
(a, i) => $("size-" + a).value = value(
|
|
4211
|
+
base.baseline_mm[i] * n / 100 / factor()
|
|
4212
|
+
)
|
|
4213
|
+
);
|
|
4214
|
+
});
|
|
4215
|
+
listen(
|
|
4216
|
+
$("size-apply"),
|
|
4217
|
+
"click",
|
|
4218
|
+
() => attempt("size-error", () => {
|
|
4219
|
+
if (!base) return;
|
|
4220
|
+
if (!draftKind) return;
|
|
4221
|
+
const current = getViewer().get_model_size(selected).dimensions_mm;
|
|
4222
|
+
let desired = axes2.map((a) => $("size-" + a).valueAsNumber * factor());
|
|
4223
|
+
if (draftKind === "percent") {
|
|
4224
|
+
const percent = $("size-percent").valueAsNumber;
|
|
4225
|
+
if (!Number.isFinite(percent) || percent <= 0)
|
|
4226
|
+
throw new Error("Enter a positive scale percentage.");
|
|
4227
|
+
desired = base.baseline_mm.map((v) => v * percent / 100);
|
|
4228
|
+
} else if ($("size-lock").checked) {
|
|
4229
|
+
const ratio = desired[lastAxis] / base.dimensions_mm[lastAxis];
|
|
4230
|
+
desired = base.dimensions_mm.map((v) => v * ratio);
|
|
4231
|
+
}
|
|
4232
|
+
let ratios = desired.map((n, i) => {
|
|
4233
|
+
if (!Number.isFinite(n) || n < 0 || current[i] > 0 && n === 0)
|
|
4234
|
+
throw new Error("Enter positive dimensions for each non-flat axis.");
|
|
4235
|
+
return current[i] ? n / current[i] : 1;
|
|
4236
|
+
});
|
|
4237
|
+
if (ratios.every((v) => Math.abs(v / ratios[0] - 1) < 1e-12))
|
|
4238
|
+
ratios = ratios.map(() => ratios[0]);
|
|
4239
|
+
changeSize(() => getViewer().resize_model(selected, ratios));
|
|
4240
|
+
announce("Model size updated.");
|
|
4241
|
+
})
|
|
4242
|
+
);
|
|
4243
|
+
listen($("size-cancel"), "click", sync);
|
|
4244
|
+
listen(
|
|
4245
|
+
$("size-reset"),
|
|
4246
|
+
"click",
|
|
4247
|
+
() => attempt("size-error", () => {
|
|
4248
|
+
changeSize(() => getViewer().reset_model_size(selected));
|
|
4249
|
+
announce("Original size restored; file-unit correction retained.");
|
|
4250
|
+
})
|
|
4251
|
+
);
|
|
4252
|
+
listen(
|
|
4253
|
+
$("file-units"),
|
|
4254
|
+
"change",
|
|
4255
|
+
() => attempt("size-error", () => {
|
|
4256
|
+
changeSize(
|
|
4257
|
+
() => getViewer().set_file_units(selected, $("file-units").value)
|
|
4258
|
+
);
|
|
4259
|
+
announce("File units corrected.");
|
|
4260
|
+
})
|
|
4261
|
+
);
|
|
4262
|
+
listen($("display-units"), "change", () => {
|
|
4263
|
+
getViewer()?.set_display_units($("display-units").value);
|
|
4264
|
+
refresh();
|
|
4265
|
+
});
|
|
4266
|
+
listen($("duplicate-model"), "click", () => attempt("size-error", duplicate));
|
|
4267
|
+
listen(
|
|
4268
|
+
$("camera-preset"),
|
|
4269
|
+
"change",
|
|
4270
|
+
() => attempt("camera-error", () => {
|
|
4271
|
+
if ($("camera-preset").value)
|
|
4272
|
+
getViewer().set_camera_view($("camera-preset").value);
|
|
4273
|
+
announce("Camera view updated.");
|
|
4274
|
+
$("camera-preset").value = "";
|
|
4275
|
+
})
|
|
4276
|
+
);
|
|
4277
|
+
const editBookmarks = (action) => attempt("camera-error", () => {
|
|
4278
|
+
const v = getViewer(), bookmarks = v.get_camera_bookmarks();
|
|
4279
|
+
action(bookmarks, v);
|
|
4280
|
+
v.set_camera_bookmarks(bookmarks);
|
|
4281
|
+
syncBookmarks();
|
|
4282
|
+
});
|
|
4283
|
+
listen(
|
|
4284
|
+
$("bookmark-save"),
|
|
4285
|
+
"click",
|
|
4286
|
+
() => editBookmarks((b, v) => {
|
|
4287
|
+
b.push({ name: $("bookmark-name").value, camera: v.get_camera_state() });
|
|
4288
|
+
})
|
|
4289
|
+
);
|
|
4290
|
+
listen(
|
|
4291
|
+
$("bookmark-rename"),
|
|
4292
|
+
"click",
|
|
4293
|
+
() => editBookmarks((b) => {
|
|
4294
|
+
b.find((b2) => b2.name === $("bookmark-list").value).name = $("bookmark-name").value;
|
|
4295
|
+
})
|
|
4296
|
+
);
|
|
4297
|
+
listen(
|
|
4298
|
+
$("bookmark-delete"),
|
|
4299
|
+
"click",
|
|
4300
|
+
() => editBookmarks((b) => {
|
|
4301
|
+
b.splice(
|
|
4302
|
+
b.findIndex((b2) => b2.name === $("bookmark-list").value),
|
|
4303
|
+
1
|
|
4304
|
+
);
|
|
4305
|
+
})
|
|
4306
|
+
);
|
|
4307
|
+
listen(
|
|
4308
|
+
$("bookmark-restore"),
|
|
4309
|
+
"click",
|
|
4310
|
+
() => attempt("camera-error", () => {
|
|
4311
|
+
const b = getViewer().get_camera_bookmarks().find((b2) => b2.name === $("bookmark-list").value);
|
|
4312
|
+
if (b) {
|
|
4313
|
+
getViewer().set_camera_state(b.camera);
|
|
4314
|
+
announce("Saved view restored.");
|
|
4315
|
+
}
|
|
4316
|
+
})
|
|
4317
|
+
);
|
|
4318
|
+
listen(
|
|
4319
|
+
$("bookmark-list"),
|
|
4320
|
+
"change",
|
|
4321
|
+
() => $("bookmark-name").value = $("bookmark-list").value
|
|
4322
|
+
);
|
|
4323
|
+
listen($("measure-cancel"), "click", () => measurement?.abort());
|
|
4324
|
+
listen($("measure-model"), "click", async () => {
|
|
4325
|
+
const id = selected, v = getViewer(), controller = new window2.AbortController();
|
|
4326
|
+
measurement = controller;
|
|
4327
|
+
syncInfo();
|
|
4328
|
+
$("measure-status").textContent = "Calculating area and checking volume\u2026";
|
|
4329
|
+
try {
|
|
4330
|
+
await v.measure_model(id, {
|
|
4331
|
+
signal: controller.signal,
|
|
4332
|
+
on_progress: (p) => {
|
|
4333
|
+
if (selected === id && !disposed)
|
|
4334
|
+
$("measure-status").textContent = p.phase + "\u2026";
|
|
4335
|
+
}
|
|
4336
|
+
});
|
|
4337
|
+
if (!disposed && selected === id) announce("Measurements ready.");
|
|
4338
|
+
} catch (e) {
|
|
4339
|
+
if (!disposed && selected === id)
|
|
4340
|
+
$("measure-status").textContent = e.name === "AbortError" ? "Calculation cancelled." : e.message;
|
|
4341
|
+
} finally {
|
|
4342
|
+
const message = $("measure-status").textContent;
|
|
4343
|
+
if (measurement === controller) measurement = null;
|
|
4344
|
+
if (!disposed) {
|
|
4345
|
+
syncInfo();
|
|
4346
|
+
if (!v._models.get(id)?._measurements && selected === id)
|
|
4347
|
+
$("measure-status").textContent = message;
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
});
|
|
4351
|
+
return {
|
|
4352
|
+
sync,
|
|
4353
|
+
syncInfo,
|
|
4354
|
+
dispose() {
|
|
4355
|
+
disposed = true;
|
|
4356
|
+
measurement?.abort();
|
|
4357
|
+
}
|
|
4358
|
+
};
|
|
4359
|
+
}
|
|
4360
|
+
|
|
4361
|
+
// src/memory-policy.js
|
|
3347
4362
|
var GiB = 1024 ** 3;
|
|
3348
4363
|
var MiB = 1024 ** 2;
|
|
3349
4364
|
function memoryPolicy(reportedGiB) {
|
|
@@ -3373,10 +4388,10 @@ async function estimateFile(file) {
|
|
|
3373
4388
|
return 64 * MiB + (file.size || 0) * 3;
|
|
3374
4389
|
}
|
|
3375
4390
|
|
|
3376
|
-
// src/
|
|
3377
|
-
var
|
|
3378
|
-
var vector2 = (p) =>
|
|
3379
|
-
var
|
|
4391
|
+
// src/model-transform.js
|
|
4392
|
+
var axes3 = ["x", "y", "z"];
|
|
4393
|
+
var vector2 = (p) => axes3.map((a) => p[a]);
|
|
4394
|
+
var object2 = (p) => Object.fromEntries(axes3.map((a, i) => [a, p[i]]));
|
|
3380
4395
|
var dot2 = (a, b) => a.reduce((sum, v, i) => sum + v * b[i], 0);
|
|
3381
4396
|
var norm2 = (a) => {
|
|
3382
4397
|
const n = Math.hypot(...a) || 1;
|
|
@@ -3407,8 +4422,8 @@ function rotationMatrix(degrees = { x: 0, y: 0, z: 0 }) {
|
|
|
3407
4422
|
}
|
|
3408
4423
|
var centerOf = (bounds) => bounds.min.map((v, i) => (v + bounds.max[i]) / 2);
|
|
3409
4424
|
function modelCenter(model) {
|
|
3410
|
-
return
|
|
3411
|
-
centerOf(model.bounds).map((v, i) => v + model.position[
|
|
4425
|
+
return object2(
|
|
4426
|
+
centerOf(model.bounds).map((v, i) => v + model.position[axes3[i]])
|
|
3412
4427
|
);
|
|
3413
4428
|
}
|
|
3414
4429
|
function modelRadius(model) {
|
|
@@ -3419,7 +4434,7 @@ function modelRadius(model) {
|
|
|
3419
4434
|
}
|
|
3420
4435
|
function enginePosition(model) {
|
|
3421
4436
|
const c = centerOf(model.bounds), rotated = transform2(rotationMatrix(model.rotation), c);
|
|
3422
|
-
return
|
|
4437
|
+
return object2(c.map((v, i) => model.position[axes3[i]] + v - rotated[i]));
|
|
3423
4438
|
}
|
|
3424
4439
|
function modelBounds(model) {
|
|
3425
4440
|
if (!model.rotation || !Object.values(model.rotation).some((v) => v % 360))
|
|
@@ -3472,12 +4487,12 @@ function depthPosition(model, camera, amount) {
|
|
|
3472
4487
|
const clearance = Math.max(model.radius || 1e-6, 1e-6) * 1.1 / Math.max(0.05, dot2(direction, forward));
|
|
3473
4488
|
const desired = (distance || clearance) * Math.exp(Math.max(-6, Math.min(6, amount)));
|
|
3474
4489
|
const next = amount < 0 ? Math.max(Math.min(distance, clearance), desired) : Math.max(distance, Math.min(1e12, desired));
|
|
3475
|
-
return
|
|
4490
|
+
return object2(
|
|
3476
4491
|
vector2(model.position).map((v, i) => v + direction[i] * (next - distance))
|
|
3477
4492
|
);
|
|
3478
4493
|
}
|
|
3479
4494
|
|
|
3480
|
-
// src/
|
|
4495
|
+
// src/scene-layout.js
|
|
3481
4496
|
function scenePlacement(models, mode) {
|
|
3482
4497
|
if (mode === "custom")
|
|
3483
4498
|
return models.map((m) => ({
|
|
@@ -3515,7 +4530,7 @@ function sceneFloor(models, placement) {
|
|
|
3515
4530
|
) : 0;
|
|
3516
4531
|
}
|
|
3517
4532
|
|
|
3518
|
-
// src/
|
|
4533
|
+
// src/model-move.js
|
|
3519
4534
|
var vector3 = (p) => [p.x, p.y, p.z];
|
|
3520
4535
|
var sub2 = (a, b) => a.map((v, i) => v - b[i]);
|
|
3521
4536
|
var dot3 = (a, b) => a.reduce((n, v, i) => n + v * b[i], 0);
|
|
@@ -3681,7 +4696,7 @@ function bindModelMove(canvas, {
|
|
|
3681
4696
|
};
|
|
3682
4697
|
}
|
|
3683
4698
|
|
|
3684
|
-
// src/
|
|
4699
|
+
// src/drop-source.js
|
|
3685
4700
|
function dropKind(transfer) {
|
|
3686
4701
|
const types = Array.from(transfer?.types || [], (type) => type.toLowerCase());
|
|
3687
4702
|
if (types.includes("files") || transfer?.files?.length) return "files";
|
|
@@ -3721,7 +4736,7 @@ function droppedURLs(transfer) {
|
|
|
3721
4736
|
return [...urls];
|
|
3722
4737
|
}
|
|
3723
4738
|
|
|
3724
|
-
// src/
|
|
4739
|
+
// src/app-export.js
|
|
3725
4740
|
var exportControls = [
|
|
3726
4741
|
"save-button",
|
|
3727
4742
|
"save-dialog",
|
|
@@ -3760,16 +4775,16 @@ function bindAppExport({
|
|
|
3760
4775
|
$("save-error").textContent = "";
|
|
3761
4776
|
};
|
|
3762
4777
|
function formatChanged() {
|
|
3763
|
-
const
|
|
4778
|
+
const format2 = $("save-format").value;
|
|
3764
4779
|
if (!$("save-name").value || $("save-name").value === lastSuggestedName) {
|
|
3765
4780
|
suggestedName();
|
|
3766
4781
|
}
|
|
3767
4782
|
if ($("save-mesh-note")) $("save-mesh-note").hidden = isScene();
|
|
3768
4783
|
if ($("save-scene-note")) $("save-scene-note").hidden = !isScene();
|
|
3769
|
-
$("save-colors-row").hidden = !["obj", "3mf"].includes(
|
|
3770
|
-
$("save-cad-note").hidden = !["step", "iges"].includes(
|
|
4784
|
+
$("save-colors-row").hidden = !["obj", "3mf"].includes(format2);
|
|
4785
|
+
$("save-cad-note").hidden = !["step", "iges"].includes(format2);
|
|
3771
4786
|
$("save-format-note").textContent = $("save-format").selectedOptions[0]?.dataset.note || "";
|
|
3772
|
-
$("save-submit").textContent =
|
|
4787
|
+
$("save-submit").textContent = format2 === "obj" && $("save-colors").checked ? "Download ZIP" : "Download file";
|
|
3773
4788
|
sync();
|
|
3774
4789
|
}
|
|
3775
4790
|
function suggestedName() {
|
|
@@ -3891,7 +4906,7 @@ function bindAppExport({
|
|
|
3891
4906
|
};
|
|
3892
4907
|
}
|
|
3893
4908
|
|
|
3894
|
-
// src/
|
|
4909
|
+
// src/app-print.js
|
|
3895
4910
|
var printControls = [
|
|
3896
4911
|
"print-button",
|
|
3897
4912
|
"print-panel",
|
|
@@ -3925,6 +4940,7 @@ function bindAppPrint({
|
|
|
3925
4940
|
}) {
|
|
3926
4941
|
let job = null, report = null, timer = null, disposed = false, scopeKey = "", previousScope = "", unavailable = false;
|
|
3927
4942
|
const panel = $("print-panel"), document2 = panel.ownerDocument;
|
|
4943
|
+
const pausedForCheck = /* @__PURE__ */ new Set();
|
|
3928
4944
|
const summary = {
|
|
3929
4945
|
issues_found: "Issues found",
|
|
3930
4946
|
incomplete: "Check incomplete",
|
|
@@ -3946,6 +4962,15 @@ function bindAppPrint({
|
|
|
3946
4962
|
const s = getState();
|
|
3947
4963
|
$("print-button").disabled = s.busy || !s.models.length;
|
|
3948
4964
|
if (panel.hidden) return;
|
|
4965
|
+
for (const id of pausedForCheck)
|
|
4966
|
+
if (!s.models.some((m) => m.id === id) || getViewer()?.get_model_animation_state(id)?.status !== "paused")
|
|
4967
|
+
pausedForCheck.delete(id);
|
|
4968
|
+
if ($("print-resume")) {
|
|
4969
|
+
$("print-resume").hidden = !pausedForCheck.size;
|
|
4970
|
+
$("print-resume").disabled = !!job || s.busy;
|
|
4971
|
+
}
|
|
4972
|
+
if ($("print-animation-note"))
|
|
4973
|
+
$("print-animation-note").hidden = !pausedForCheck.size;
|
|
3949
4974
|
const wasUnavailable = unavailable;
|
|
3950
4975
|
unavailable = s.busy || !s.models.length;
|
|
3951
4976
|
$("print-run").disabled = !!job || unavailable;
|
|
@@ -4092,6 +5117,19 @@ function bindAppPrint({
|
|
|
4092
5117
|
listen($("print-fields"), "change", changed);
|
|
4093
5118
|
listen($("print-clear"), "click", clear);
|
|
4094
5119
|
listen($("print-cancel"), "click", cancel);
|
|
5120
|
+
if ($("print-resume"))
|
|
5121
|
+
listen($("print-resume"), "click", () => {
|
|
5122
|
+
if (job || getState().busy) return;
|
|
5123
|
+
for (const id of pausedForCheck)
|
|
5124
|
+
if (getState().models.some((m) => m.id === id) && getViewer().get_model_animation_state(id)?.status === "paused")
|
|
5125
|
+
getViewer().resume_model_animation(id);
|
|
5126
|
+
pausedForCheck.clear();
|
|
5127
|
+
report = null;
|
|
5128
|
+
clear();
|
|
5129
|
+
$("print-results").replaceChildren();
|
|
5130
|
+
$("print-status").textContent = "Animation resumed. Pause and run the check again for the new pose.";
|
|
5131
|
+
sync();
|
|
5132
|
+
});
|
|
4095
5133
|
listen($("print-results"), "click", (e) => {
|
|
4096
5134
|
const b = e.target.closest("button[data-issue]");
|
|
4097
5135
|
if (!b || !report) return;
|
|
@@ -4135,6 +5173,13 @@ function bindAppPrint({
|
|
|
4135
5173
|
}
|
|
4136
5174
|
}
|
|
4137
5175
|
scopeKey = key();
|
|
5176
|
+
for (const id of stateIds()) {
|
|
5177
|
+
if (getViewer().get_model_animation_state(id)?.status === "running") {
|
|
5178
|
+
getViewer().pause_model_animation(id);
|
|
5179
|
+
pausedForCheck.add(id);
|
|
5180
|
+
}
|
|
5181
|
+
}
|
|
5182
|
+
sync();
|
|
4138
5183
|
const result = await getViewer().check_printability({
|
|
4139
5184
|
model_ids: stateIds(),
|
|
4140
5185
|
level: $("print-level").value,
|
|
@@ -4172,11 +5217,216 @@ function bindAppPrint({
|
|
|
4172
5217
|
disposed = true;
|
|
4173
5218
|
close(false);
|
|
4174
5219
|
report = null;
|
|
5220
|
+
pausedForCheck.clear();
|
|
5221
|
+
}
|
|
5222
|
+
};
|
|
5223
|
+
}
|
|
5224
|
+
|
|
5225
|
+
// src/app-animation.js
|
|
5226
|
+
var animationControls = [
|
|
5227
|
+
"animation-details",
|
|
5228
|
+
"animation-fields",
|
|
5229
|
+
"animation-axis",
|
|
5230
|
+
"animation-seconds",
|
|
5231
|
+
"animation-direction",
|
|
5232
|
+
"animation-play",
|
|
5233
|
+
"animation-pause",
|
|
5234
|
+
"animation-stop",
|
|
5235
|
+
"animation-status",
|
|
5236
|
+
"animation-error"
|
|
5237
|
+
];
|
|
5238
|
+
var axes4 = ["x", "y", "z"];
|
|
5239
|
+
function syncAnimatedRecord(record, model) {
|
|
5240
|
+
if (!record || !model?.geometry) return;
|
|
5241
|
+
record.bounds = Object.fromEntries(
|
|
5242
|
+
["min", "max"].map((key) => [
|
|
5243
|
+
key,
|
|
5244
|
+
model.geometry.bounds[key].map(
|
|
5245
|
+
(v, i) => (v - model.center[i]) * model.scaleVector[i]
|
|
5246
|
+
)
|
|
5247
|
+
])
|
|
5248
|
+
);
|
|
5249
|
+
record.rotation = Object.fromEntries(
|
|
5250
|
+
axes4.map((a) => [a, model["rotation" + a] * 180 / Math.PI % 360])
|
|
5251
|
+
);
|
|
5252
|
+
record.position = { x: 0, y: 0, z: 0 };
|
|
5253
|
+
const offset = enginePosition(record);
|
|
5254
|
+
for (const a of axes4) record.position[a] = model[a] - offset[a];
|
|
5255
|
+
}
|
|
5256
|
+
function centerAnimationPivot(record, model) {
|
|
5257
|
+
syncAnimatedRecord(record, model);
|
|
5258
|
+
const center = model.geometry.bounds.min.map(
|
|
5259
|
+
(v, i) => (v + model.geometry.bounds.max[i]) / 2
|
|
5260
|
+
);
|
|
5261
|
+
const delta = center.map(
|
|
5262
|
+
(v, i) => (v - model.center[i]) * model.scaleVector[i]
|
|
5263
|
+
);
|
|
5264
|
+
const rotation2 = rotationMatrix(record.rotation);
|
|
5265
|
+
const original = record.originalPosition || { x: 0, y: 0, z: 0 };
|
|
5266
|
+
record.originalPosition = Object.fromEntries(
|
|
5267
|
+
axes4.map((a, i) => [a, original[a] + delta[i]])
|
|
5268
|
+
);
|
|
5269
|
+
for (let i = 0; i < 3; i++)
|
|
5270
|
+
model[axes4[i]] += delta.reduce(
|
|
5271
|
+
(sum, v, j) => sum + rotation2[i * 3 + j] * v,
|
|
5272
|
+
0
|
|
5273
|
+
);
|
|
5274
|
+
model.center = center;
|
|
5275
|
+
record.animationPivot = true;
|
|
5276
|
+
syncAnimatedRecord(record, model);
|
|
5277
|
+
}
|
|
5278
|
+
function spinDefinition(axis, seconds, direction) {
|
|
5279
|
+
if (!axes4.includes(axis) || !Number.isFinite(seconds) || seconds < 0.1 || seconds > 3600 || !["1", "-1"].includes(direction))
|
|
5280
|
+
throw new Error("Choose an axis and a duration from 0.1 to 3,600 seconds.");
|
|
5281
|
+
return {
|
|
5282
|
+
delta: {
|
|
5283
|
+
["rotation" + axis]: 2 * Math.PI * Number(direction),
|
|
5284
|
+
msec: seconds * 1e3,
|
|
5285
|
+
loop: true
|
|
5286
|
+
}
|
|
5287
|
+
};
|
|
5288
|
+
}
|
|
5289
|
+
function bindAppAnimation({
|
|
5290
|
+
$,
|
|
5291
|
+
listen,
|
|
5292
|
+
window: window2,
|
|
5293
|
+
getState,
|
|
5294
|
+
getViewer,
|
|
5295
|
+
beforePlay,
|
|
5296
|
+
refreshPose,
|
|
5297
|
+
announce
|
|
5298
|
+
}) {
|
|
5299
|
+
const settings = /* @__PURE__ */ new Map();
|
|
5300
|
+
let selected = null, timer = null, disposed = false;
|
|
5301
|
+
const state = () => {
|
|
5302
|
+
const s = getState();
|
|
5303
|
+
return {
|
|
5304
|
+
...s,
|
|
5305
|
+
playback: s.selectedId == null ? null : getViewer()?.get_model_animation_state(s.selectedId)
|
|
5306
|
+
};
|
|
5307
|
+
};
|
|
5308
|
+
function sync() {
|
|
5309
|
+
if (disposed) return;
|
|
5310
|
+
const s = state(), available = s.models.some((m) => m.id === s.selectedId);
|
|
5311
|
+
for (const id of settings.keys())
|
|
5312
|
+
if (!s.models.some((m) => m.id === id)) settings.delete(id);
|
|
5313
|
+
$("animation-details").hidden = !available;
|
|
5314
|
+
$("animation-fields").disabled = s.busy || !available;
|
|
5315
|
+
if (selected !== s.selectedId) {
|
|
5316
|
+
selected = s.selectedId;
|
|
5317
|
+
let value = settings.get(selected);
|
|
5318
|
+
if (!value) {
|
|
5319
|
+
value = { axis: "z", seconds: 5, direction: "1" };
|
|
5320
|
+
const a = getViewer()?._models.get(selected)?.animation;
|
|
5321
|
+
const entries = Object.entries(a?.delta || {}).filter(
|
|
5322
|
+
([k, v]) => k.startsWith("rotation") && v !== 0
|
|
5323
|
+
);
|
|
5324
|
+
if (a?.delta?.loop && !a.exact && entries.length === 1) {
|
|
5325
|
+
value = {
|
|
5326
|
+
axis: entries[0][0].slice(-1),
|
|
5327
|
+
seconds: a.delta.msec * 2 * Math.PI / Math.abs(entries[0][1]) / 1e3,
|
|
5328
|
+
direction: entries[0][1] < 0 ? "-1" : "1"
|
|
5329
|
+
};
|
|
5330
|
+
}
|
|
5331
|
+
settings.set(selected, value);
|
|
5332
|
+
}
|
|
5333
|
+
$("animation-axis").value = value.axis;
|
|
5334
|
+
$("animation-seconds").value = value.seconds;
|
|
5335
|
+
$("animation-direction").value = value.direction;
|
|
5336
|
+
$("animation-error").hidden = true;
|
|
5337
|
+
}
|
|
5338
|
+
const status = s.playback?.status || "idle", active = ["running", "paused"].includes(status);
|
|
5339
|
+
$("animation-play").textContent = active ? "Restart spin" : "Play";
|
|
5340
|
+
$("animation-pause").textContent = status === "paused" ? "Resume" : "Pause";
|
|
5341
|
+
$("animation-pause").disabled = !active;
|
|
5342
|
+
$("animation-stop").disabled = !active;
|
|
5343
|
+
const label = {
|
|
5344
|
+
idle: "Ready",
|
|
5345
|
+
running: "Playing",
|
|
5346
|
+
paused: "Paused",
|
|
5347
|
+
stopped: "Stopped",
|
|
5348
|
+
completed: "Finished"
|
|
5349
|
+
}[status];
|
|
5350
|
+
const text = `${label}. ${active ? "Stop keeps the current pose." : "Spin the selected model around its center."}`;
|
|
5351
|
+
if ($("animation-status").textContent !== text)
|
|
5352
|
+
$("animation-status").textContent = text;
|
|
5353
|
+
if (status === "running" && !timer)
|
|
5354
|
+
timer = window2.setInterval(() => {
|
|
5355
|
+
if (!disposed && !window2.document.hidden) refreshPose();
|
|
5356
|
+
}, 250);
|
|
5357
|
+
if (status !== "running" && timer) {
|
|
5358
|
+
window2.clearInterval(timer);
|
|
5359
|
+
timer = null;
|
|
5360
|
+
}
|
|
5361
|
+
}
|
|
5362
|
+
function play(keepPaused = false) {
|
|
5363
|
+
const s = state();
|
|
5364
|
+
if (s.busy || s.selectedId == null) return;
|
|
5365
|
+
try {
|
|
5366
|
+
const value = {
|
|
5367
|
+
axis: $("animation-axis").value,
|
|
5368
|
+
seconds: $("animation-seconds").valueAsNumber,
|
|
5369
|
+
direction: $("animation-direction").value
|
|
5370
|
+
};
|
|
5371
|
+
const definition = spinDefinition(
|
|
5372
|
+
value.axis,
|
|
5373
|
+
value.seconds,
|
|
5374
|
+
value.direction
|
|
5375
|
+
);
|
|
5376
|
+
beforePlay(s.selectedId);
|
|
5377
|
+
getViewer().animate_model(s.selectedId, definition);
|
|
5378
|
+
if (keepPaused && s.playback?.status === "paused")
|
|
5379
|
+
getViewer().pause_model_animation(s.selectedId);
|
|
5380
|
+
settings.set(s.selectedId, value);
|
|
5381
|
+
$("animation-error").hidden = true;
|
|
5382
|
+
announce(
|
|
5383
|
+
keepPaused && s.playback?.status === "paused" ? "Spin updated. Still paused." : "Animation playing."
|
|
5384
|
+
);
|
|
5385
|
+
sync();
|
|
5386
|
+
} catch (error) {
|
|
5387
|
+
$("animation-error").textContent = error.message;
|
|
5388
|
+
$("animation-error").hidden = false;
|
|
5389
|
+
}
|
|
5390
|
+
}
|
|
5391
|
+
listen($("animation-play"), "click", () => play());
|
|
5392
|
+
listen($("animation-pause"), "click", () => {
|
|
5393
|
+
const s = state();
|
|
5394
|
+
if (s.busy || s.selectedId == null) return;
|
|
5395
|
+
getViewer()[s.playback?.status === "paused" ? "resume_model_animation" : "pause_model_animation"](s.selectedId);
|
|
5396
|
+
sync();
|
|
5397
|
+
});
|
|
5398
|
+
listen($("animation-stop"), "click", () => {
|
|
5399
|
+
const s = state();
|
|
5400
|
+
if (!s.busy && s.selectedId != null)
|
|
5401
|
+
getViewer().animate_model(s.selectedId, null);
|
|
5402
|
+
sync();
|
|
5403
|
+
});
|
|
5404
|
+
for (const id of [
|
|
5405
|
+
"animation-axis",
|
|
5406
|
+
"animation-seconds",
|
|
5407
|
+
"animation-direction"
|
|
5408
|
+
])
|
|
5409
|
+
listen($(id), "change", () => {
|
|
5410
|
+
if (["running", "paused"].includes(state().playback?.status)) play(true);
|
|
5411
|
+
else
|
|
5412
|
+
settings.set(selected, {
|
|
5413
|
+
axis: $("animation-axis").value,
|
|
5414
|
+
seconds: $("animation-seconds").valueAsNumber,
|
|
5415
|
+
direction: $("animation-direction").value
|
|
5416
|
+
});
|
|
5417
|
+
});
|
|
5418
|
+
sync();
|
|
5419
|
+
return {
|
|
5420
|
+
sync,
|
|
5421
|
+
dispose() {
|
|
5422
|
+
disposed = true;
|
|
5423
|
+
window2.clearInterval(timer);
|
|
5424
|
+
settings.clear();
|
|
4175
5425
|
}
|
|
4176
5426
|
};
|
|
4177
5427
|
}
|
|
4178
5428
|
|
|
4179
|
-
// src/
|
|
5429
|
+
// src/app.js
|
|
4180
5430
|
var mountedRoots = /* @__PURE__ */ new WeakMap();
|
|
4181
5431
|
var requiredControls = [
|
|
4182
5432
|
"background-color",
|
|
@@ -4281,6 +5531,10 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4281
5531
|
missing.push(...exportControls.filter((id) => !$(id)));
|
|
4282
5532
|
if (printControls.some((id) => $(id)))
|
|
4283
5533
|
missing.push(...printControls.filter((id) => !$(id)));
|
|
5534
|
+
if (animationControls.some((id) => $(id)))
|
|
5535
|
+
missing.push(...animationControls.filter((id) => !$(id)));
|
|
5536
|
+
if (toolsControls.some((id) => $(id)))
|
|
5537
|
+
missing.push(...toolsControls.filter((id) => !$(id)));
|
|
4284
5538
|
if (missing.length)
|
|
4285
5539
|
throw new Error("Missing ViewSTL controls: " + missing.join(", "));
|
|
4286
5540
|
const abort = new window2.AbortController();
|
|
@@ -4308,6 +5562,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4308
5562
|
let viewerPromise, viewer, generation = 0, appearanceRequest = 0, modelID = null, nextID = 0, loadingID = null;
|
|
4309
5563
|
let exportUI;
|
|
4310
5564
|
let printUI;
|
|
5565
|
+
let animationUI;
|
|
5566
|
+
let toolsUI;
|
|
4311
5567
|
let sceneImport;
|
|
4312
5568
|
let draining = false, busy = false, gridEnabled = false, gridHeight = 0, gridSpacing = 1, moveMode = false, transformTool = "move", modelMover, dragArrangement, background = "transparent";
|
|
4313
5569
|
const viewNames = {
|
|
@@ -4356,6 +5612,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4356
5612
|
$(name + "-panel").hidden = !open;
|
|
4357
5613
|
$(name + "-button").setAttribute("aria-expanded", String(open));
|
|
4358
5614
|
syncSelection();
|
|
5615
|
+
if (open && name === "info") populateInfo();
|
|
5616
|
+
if (open && (name === "scene" || name === "view")) toolsUI?.sync();
|
|
4359
5617
|
}
|
|
4360
5618
|
function fileSize(size) {
|
|
4361
5619
|
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`;
|
|
@@ -4381,12 +5639,14 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4381
5639
|
$("background-value").textContent = background === "transparent" ? "Default" : background.toUpperCase();
|
|
4382
5640
|
}
|
|
4383
5641
|
function syncControls() {
|
|
4384
|
-
|
|
5642
|
+
syncAnimatedRecord(records.get(modelID), viewer?._models.get(modelID));
|
|
5643
|
+
const info = modelID === null ? null : viewer.get_model_info(modelID, { measurements: false }), mode = info?.display === "smooth" ? "flat" : info?.display || "flat";
|
|
4385
5644
|
for (const input of modeInputs) {
|
|
4386
5645
|
input.checked = input.value === mode;
|
|
4387
5646
|
input.disabled = busy;
|
|
4388
5647
|
}
|
|
4389
|
-
$("view-label").textContent = viewNames[mode];
|
|
5648
|
+
$("view-label").textContent = toolsUI ? "View" : viewNames[mode];
|
|
5649
|
+
$("view-button").title = "View \xB7 " + viewNames[mode];
|
|
4390
5650
|
$("smooth-shading").checked = info?.smooth_shading || false;
|
|
4391
5651
|
$("smooth-shading").disabled = busy || mode === "wireframe";
|
|
4392
5652
|
$("view-status").textContent = mode === "wireframe" ? "Smooth shading is available in surface views." : "Shading changes the appearance, not the geometry.";
|
|
@@ -4409,14 +5669,18 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4409
5669
|
syncPosition();
|
|
4410
5670
|
exportUI?.sync();
|
|
4411
5671
|
printUI?.sync();
|
|
5672
|
+
animationUI?.sync();
|
|
5673
|
+
toolsUI?.sync();
|
|
4412
5674
|
}
|
|
4413
5675
|
function populateInfo() {
|
|
4414
5676
|
if (modelID === null) return;
|
|
4415
|
-
const info = viewer.get_model_info(modelID), record = records.get(modelID),
|
|
5677
|
+
const info = viewer.get_model_info(modelID, { measurements: false }), record = records.get(modelID), factor = viewer.display_units === "inch" ? 25.4 : 1;
|
|
4416
5678
|
$("info-triangles").textContent = info.triangles.toLocaleString();
|
|
4417
|
-
$("info-dimensions").textContent = [info.dims.x, info.dims.y, info.dims.z].map((x) => formatNumber.format(x)).join(" \xD7 ") +
|
|
5679
|
+
$("info-dimensions").textContent = [info.dims.x, info.dims.y, info.dims.z].map((x) => formatNumber.format(x / factor)).join(" \xD7 ") + (factor === 1 ? " mm" : " in");
|
|
4418
5680
|
$("info-size").textContent = fileSize(record.size);
|
|
4419
|
-
|
|
5681
|
+
const size = viewer.get_model_size(modelID);
|
|
5682
|
+
$("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.";
|
|
5683
|
+
toolsUI?.syncInfo();
|
|
4420
5684
|
}
|
|
4421
5685
|
function renderList() {
|
|
4422
5686
|
$("model-list").replaceChildren(
|
|
@@ -4488,6 +5752,10 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4488
5752
|
}
|
|
4489
5753
|
function arrangeScene() {
|
|
4490
5754
|
setMoveMode(false);
|
|
5755
|
+
for (const record of records.values())
|
|
5756
|
+
syncAnimatedRecord(record, viewer?._models.get(record.id));
|
|
5757
|
+
if ($("scene-arrangement").value === "custom") return;
|
|
5758
|
+
for (const record of records.values()) prepareManual(record.id);
|
|
4491
5759
|
if ($("scene-arrangement").value === "original")
|
|
4492
5760
|
for (const record of records.values())
|
|
4493
5761
|
record.rotation = {
|
|
@@ -4504,19 +5772,20 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4504
5772
|
updateGrid();
|
|
4505
5773
|
viewer?.set_auto_zoom();
|
|
4506
5774
|
}
|
|
4507
|
-
function syncPosition() {
|
|
5775
|
+
function syncPosition(preserveFocus = false) {
|
|
4508
5776
|
const record = records.get(modelID);
|
|
4509
5777
|
for (const axis of ["x", "y", "z"]) {
|
|
4510
5778
|
const input = $("position-" + axis);
|
|
4511
|
-
|
|
5779
|
+
if (!preserveFocus || document2.activeElement !== input)
|
|
5780
|
+
input.value = Number((record?.position?.[axis] || 0).toPrecision(12));
|
|
4512
5781
|
input.disabled = !record;
|
|
4513
5782
|
input.removeAttribute("aria-invalid");
|
|
4514
5783
|
}
|
|
4515
5784
|
$("position-error").hidden = true;
|
|
4516
|
-
$("position-units").textContent = record?.
|
|
5785
|
+
$("position-units").textContent = record?.animationPivot ? "Position of the model center in the scene, in mm." : "Offset from file coordinates, in mm.";
|
|
4517
5786
|
$("move-model-button").disabled = !record || busy;
|
|
4518
5787
|
$("reset-position").disabled = !record;
|
|
4519
|
-
syncRotation();
|
|
5788
|
+
syncRotation(preserveFocus);
|
|
4520
5789
|
for (const id of [
|
|
4521
5790
|
"rotate-model-button",
|
|
4522
5791
|
"depth-model-button",
|
|
@@ -4545,14 +5814,16 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4545
5814
|
viewer.set_position(record.id, p.x, p.y, p.z);
|
|
4546
5815
|
}
|
|
4547
5816
|
function selectedTransform() {
|
|
5817
|
+
prepareManual(modelID);
|
|
4548
5818
|
const record = records.get(modelID);
|
|
4549
5819
|
return record ? { ...record, center: modelCenter(record), radius: modelRadius(record) } : null;
|
|
4550
5820
|
}
|
|
4551
|
-
function syncRotation() {
|
|
5821
|
+
function syncRotation(preserveFocus = false) {
|
|
4552
5822
|
const record = records.get(modelID);
|
|
4553
5823
|
for (const axis of ["x", "y", "z"]) {
|
|
4554
5824
|
const input = $("rotation-" + axis);
|
|
4555
|
-
|
|
5825
|
+
if (!preserveFocus || document2.activeElement !== input)
|
|
5826
|
+
input.value = Number((record?.rotation?.[axis] || 0).toPrecision(10));
|
|
4556
5827
|
input.disabled = !record;
|
|
4557
5828
|
input.removeAttribute("aria-invalid");
|
|
4558
5829
|
}
|
|
@@ -4594,10 +5865,11 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4594
5865
|
$("model-tool").value = transformTool;
|
|
4595
5866
|
$("move-gesture-message").textContent = toolHints[transformTool] + " \xB7 Scroll to adjust depth";
|
|
4596
5867
|
}
|
|
4597
|
-
function setMoveMode(enabled, tool = transformTool) {
|
|
5868
|
+
function setMoveMode(enabled, tool = transformTool, retainPaused = false) {
|
|
4598
5869
|
modelMover?.setEnabled(false);
|
|
4599
5870
|
transformTool = tool;
|
|
4600
5871
|
moveMode = !!enabled && modelID !== null && !busy;
|
|
5872
|
+
if (moveMode && !retainPaused) prepareManual(modelID);
|
|
4601
5873
|
modelMover?.setEnabled(moveMode);
|
|
4602
5874
|
syncTool();
|
|
4603
5875
|
$("move-mode").hidden = !moveMode;
|
|
@@ -4621,6 +5893,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4621
5893
|
const input = $("rotation-" + axis);
|
|
4622
5894
|
listen(input, "input", () => {
|
|
4623
5895
|
if (modelID === null || !input.value.trim()) return;
|
|
5896
|
+
prepareManual(modelID);
|
|
4624
5897
|
if (applyRotation(
|
|
4625
5898
|
modelID,
|
|
4626
5899
|
{ ...records.get(modelID).rotation, [axis]: input.valueAsNumber },
|
|
@@ -4632,6 +5905,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4632
5905
|
});
|
|
4633
5906
|
listen(input, "change", () => {
|
|
4634
5907
|
if (modelID === null) return;
|
|
5908
|
+
prepareManual(modelID);
|
|
4635
5909
|
if (!input.value.trim() || !applyRotation(modelID, {
|
|
4636
5910
|
...records.get(modelID).rotation,
|
|
4637
5911
|
[axis]: input.valueAsNumber
|
|
@@ -4644,6 +5918,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4644
5918
|
listen($("reset-rotation"), "click", () => {
|
|
4645
5919
|
setMoveMode(false);
|
|
4646
5920
|
if (modelID !== null) {
|
|
5921
|
+
prepareManual(modelID);
|
|
4647
5922
|
applyRotation(modelID, {
|
|
4648
5923
|
...records.get(modelID).originalRotation || { x: 0, y: 0, z: 0 }
|
|
4649
5924
|
});
|
|
@@ -4683,6 +5958,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4683
5958
|
const input = $("position-" + axis);
|
|
4684
5959
|
listen(input, "input", () => {
|
|
4685
5960
|
if (modelID === null || !input.value.trim()) return;
|
|
5961
|
+
prepareManual(modelID);
|
|
4686
5962
|
if (applyPosition(
|
|
4687
5963
|
modelID,
|
|
4688
5964
|
{ ...records.get(modelID).position, [axis]: input.valueAsNumber },
|
|
@@ -4694,6 +5970,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4694
5970
|
});
|
|
4695
5971
|
listen(input, "change", () => {
|
|
4696
5972
|
if (modelID === null) return;
|
|
5973
|
+
prepareManual(modelID);
|
|
4697
5974
|
if (!input.value.trim() || !applyPosition(modelID, {
|
|
4698
5975
|
...records.get(modelID).position,
|
|
4699
5976
|
[axis]: input.valueAsNumber
|
|
@@ -4710,6 +5987,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4710
5987
|
listen($("reset-position"), "click", () => {
|
|
4711
5988
|
setMoveMode(false);
|
|
4712
5989
|
if (modelID !== null) {
|
|
5990
|
+
prepareManual(modelID);
|
|
4713
5991
|
applyPosition(modelID, {
|
|
4714
5992
|
...records.get(modelID).originalPosition || { x: 0, y: 0, z: 0 }
|
|
4715
5993
|
});
|
|
@@ -4760,6 +6038,30 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4760
6038
|
renderScene();
|
|
4761
6039
|
announce(name + " removed.");
|
|
4762
6040
|
}
|
|
6041
|
+
function prepareManual(id) {
|
|
6042
|
+
if (!records.has(id)) return;
|
|
6043
|
+
syncAnimatedRecord(records.get(id), viewer?._models.get(id));
|
|
6044
|
+
viewer?.animate_model(id, null);
|
|
6045
|
+
}
|
|
6046
|
+
function refreshAnimatedPose() {
|
|
6047
|
+
if (disposed || modelID === null) return;
|
|
6048
|
+
syncAnimatedRecord(records.get(modelID), viewer?._models.get(modelID));
|
|
6049
|
+
if (!$("info-panel").hidden) populateInfo();
|
|
6050
|
+
if (!$("scene-panel").hidden && $("position-details").open)
|
|
6051
|
+
syncPosition(true);
|
|
6052
|
+
}
|
|
6053
|
+
function animationChanged(id, state, reason) {
|
|
6054
|
+
if (disposed) return;
|
|
6055
|
+
if (reason === "restored" && state.status === "running" && window2.matchMedia?.("(prefers-reduced-motion: reduce)").matches)
|
|
6056
|
+
viewer.pause_model_animation(id);
|
|
6057
|
+
syncAnimatedRecord(records.get(id), viewer?._models.get(id));
|
|
6058
|
+
if (["started", "restored", "resumed"].includes(reason))
|
|
6059
|
+
$("scene-arrangement").value = "custom";
|
|
6060
|
+
if (id === modelID) refreshAnimatedPose();
|
|
6061
|
+
animationUI?.sync();
|
|
6062
|
+
printUI?.sync();
|
|
6063
|
+
options.animation_state_callback?.(id, state, reason);
|
|
6064
|
+
}
|
|
4763
6065
|
async function getViewer() {
|
|
4764
6066
|
if (!viewerPromise)
|
|
4765
6067
|
viewerPromise = Promise.resolve().then(async () => {
|
|
@@ -4774,6 +6076,10 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4774
6076
|
center_models: false,
|
|
4775
6077
|
bg_color: background,
|
|
4776
6078
|
allow_drag_and_drop: false,
|
|
6079
|
+
animation_state_callback: animationChanged,
|
|
6080
|
+
animation_complete_callback: (...args) => {
|
|
6081
|
+
if (!disposed) options.animation_complete_callback?.(...args);
|
|
6082
|
+
},
|
|
4777
6083
|
camera_state: {
|
|
4778
6084
|
position: { x: 90, y: -130, z: 90 },
|
|
4779
6085
|
target: { x: 0, y: 0, z: 0 },
|
|
@@ -4865,7 +6171,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4865
6171
|
if (queue.length && !draining) drainPromise = drainQueue();
|
|
4866
6172
|
return drainPromise || Promise.resolve();
|
|
4867
6173
|
}
|
|
4868
|
-
function openURL(value,
|
|
6174
|
+
function openURL(value, format2 = "") {
|
|
4869
6175
|
assertActive();
|
|
4870
6176
|
if (!options.url_import_endpoint)
|
|
4871
6177
|
throw new Error("URL imports are not configured.");
|
|
@@ -4874,7 +6180,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4874
6180
|
throw new Error(
|
|
4875
6181
|
"Use a public HTTP or HTTPS file URL without login credentials."
|
|
4876
6182
|
);
|
|
4877
|
-
if (
|
|
6183
|
+
if (format2 && !/^(stl|obj|3mf|step|stp|iges|igs|vsb|vsj)$/.test(format2))
|
|
4878
6184
|
throw new Error("Choose a supported 3D format.");
|
|
4879
6185
|
url.hash = "";
|
|
4880
6186
|
const name = decodeURIComponent(
|
|
@@ -4883,9 +6189,9 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4883
6189
|
queue.push({
|
|
4884
6190
|
name,
|
|
4885
6191
|
remote: url.href,
|
|
4886
|
-
format,
|
|
6192
|
+
format: format2,
|
|
4887
6193
|
size: 0,
|
|
4888
|
-
scene: ["vsb", "vsj"].includes(
|
|
6194
|
+
scene: ["vsb", "vsj"].includes(format2) ? format2 : sceneFormat(name)
|
|
4889
6195
|
});
|
|
4890
6196
|
if (!draining) drainPromise = drainQueue();
|
|
4891
6197
|
return drainPromise || Promise.resolve();
|
|
@@ -4954,7 +6260,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4954
6260
|
await engine.when_idle();
|
|
4955
6261
|
if (token !== generation) continue;
|
|
4956
6262
|
loadingID = null;
|
|
4957
|
-
const loaded = lastLoadedID === id ? engine.get_model_info(id) : null;
|
|
6263
|
+
const loaded = lastLoadedID === id ? engine.get_model_info(id, { measurements: false }) : null;
|
|
4958
6264
|
if (!loaded) {
|
|
4959
6265
|
announce(source.name + " could not be opened.");
|
|
4960
6266
|
continue;
|
|
@@ -5046,7 +6352,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5046
6352
|
);
|
|
5047
6353
|
const record = {
|
|
5048
6354
|
id,
|
|
5049
|
-
name: String(m.orig_filename || m.filename).split(/[\\/]/).pop().split(/[?#]/)[0],
|
|
6355
|
+
name: String(m.display_name || m.orig_filename || m.filename).split(/[\\/]/).pop().split(/[?#]/)[0],
|
|
5050
6356
|
size: g.file_bytes ?? m.local_file?.size ?? null,
|
|
5051
6357
|
units: g.units || m.units,
|
|
5052
6358
|
bounds,
|
|
@@ -5355,6 +6661,64 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5355
6661
|
},
|
|
5356
6662
|
announce
|
|
5357
6663
|
});
|
|
6664
|
+
if ($("animation-details"))
|
|
6665
|
+
animationUI = bindAppAnimation({
|
|
6666
|
+
$,
|
|
6667
|
+
listen,
|
|
6668
|
+
window: window2,
|
|
6669
|
+
getState: () => ({
|
|
6670
|
+
busy,
|
|
6671
|
+
selectedId: modelID,
|
|
6672
|
+
models: [...records.values()]
|
|
6673
|
+
}),
|
|
6674
|
+
getViewer: () => viewer,
|
|
6675
|
+
beforePlay(id) {
|
|
6676
|
+
setMoveMode(false);
|
|
6677
|
+
prepareManual(id);
|
|
6678
|
+
centerAnimationPivot(records.get(id), viewer._models.get(id));
|
|
6679
|
+
$("scene-arrangement").value = "custom";
|
|
6680
|
+
},
|
|
6681
|
+
refreshPose: refreshAnimatedPose,
|
|
6682
|
+
announce
|
|
6683
|
+
});
|
|
6684
|
+
if ($("size-details"))
|
|
6685
|
+
toolsUI = bindAppTools({
|
|
6686
|
+
$,
|
|
6687
|
+
listen,
|
|
6688
|
+
window: window2,
|
|
6689
|
+
getViewer: () => viewer,
|
|
6690
|
+
getState: () => ({ selectedId: modelID, busy }),
|
|
6691
|
+
refresh: renderScene,
|
|
6692
|
+
announce,
|
|
6693
|
+
changeSize(operation) {
|
|
6694
|
+
setMoveMode(false);
|
|
6695
|
+
const record = records.get(modelID);
|
|
6696
|
+
syncAnimatedRecord(record, viewer._models.get(modelID));
|
|
6697
|
+
operation();
|
|
6698
|
+
syncAnimatedRecord(record, viewer._models.get(modelID));
|
|
6699
|
+
$("scene-arrangement").value = "custom";
|
|
6700
|
+
renderScene();
|
|
6701
|
+
},
|
|
6702
|
+
duplicate() {
|
|
6703
|
+
const original = records.get(modelID), id = viewer.duplicate_model(modelID), m = viewer._models.get(id);
|
|
6704
|
+
const record = {
|
|
6705
|
+
...original,
|
|
6706
|
+
id,
|
|
6707
|
+
name: m.display_name,
|
|
6708
|
+
position: { ...original.position },
|
|
6709
|
+
rotation: { ...original.rotation }
|
|
6710
|
+
};
|
|
6711
|
+
syncAnimatedRecord(record, m);
|
|
6712
|
+
record.originalPosition = { ...record.position };
|
|
6713
|
+
record.originalRotation = { ...record.rotation };
|
|
6714
|
+
records.set(id, record);
|
|
6715
|
+
nextID = Math.max(nextID, id + 1);
|
|
6716
|
+
$("scene-arrangement").value = "custom";
|
|
6717
|
+
selectModel(id);
|
|
6718
|
+
setMoveMode(true, "move", true);
|
|
6719
|
+
announce("Copy selected at the same position. Drag to move it.");
|
|
6720
|
+
}
|
|
6721
|
+
});
|
|
5358
6722
|
updateGrid();
|
|
5359
6723
|
setBusy(false);
|
|
5360
6724
|
setMoveMode(false);
|
|
@@ -5379,6 +6743,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5379
6743
|
},
|
|
5380
6744
|
getState() {
|
|
5381
6745
|
assertActive();
|
|
6746
|
+
for (const record of records.values())
|
|
6747
|
+
syncAnimatedRecord(record, viewer?._models.get(record.id));
|
|
5382
6748
|
return {
|
|
5383
6749
|
selectedId: modelID,
|
|
5384
6750
|
busy,
|
|
@@ -5391,7 +6757,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5391
6757
|
units: record.units,
|
|
5392
6758
|
position: { ...record.position },
|
|
5393
6759
|
rotation: { ...record.rotation },
|
|
5394
|
-
color: record.color
|
|
6760
|
+
color: record.color,
|
|
6761
|
+
animation: viewer.get_model_animation_state(record.id)
|
|
5395
6762
|
}))
|
|
5396
6763
|
};
|
|
5397
6764
|
},
|
|
@@ -5399,6 +6766,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5399
6766
|
if (disposed) return;
|
|
5400
6767
|
exportUI?.dispose();
|
|
5401
6768
|
printUI?.dispose();
|
|
6769
|
+
animationUI?.dispose();
|
|
6770
|
+
toolsUI?.dispose();
|
|
5402
6771
|
cancelLoading();
|
|
5403
6772
|
setMoveMode(false);
|
|
5404
6773
|
appearanceRequest++;
|
|
@@ -5416,7 +6785,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5416
6785
|
return api;
|
|
5417
6786
|
}
|
|
5418
6787
|
|
|
5419
|
-
// src/
|
|
6788
|
+
// src/index.js
|
|
5420
6789
|
StlViewer.workerURL = new URL("./viewstl.worker.js", import.meta.url).href;
|
|
5421
6790
|
var index_default = StlViewer;
|
|
5422
6791
|
export {
|