@omrips/viewstl 2.0.0-rc.5 → 2.0.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/build/THIRD-PARTY-NOTICES.txt +1006 -1006
- package/build/cad/occt-import-js.source.txt +19 -19
- package/build/chunks/archive-QK7XZSVT.js +1 -0
- package/build/chunks/{cad-4JYHORYZ.js → cad-QGVO6UGR.js} +1 -1
- package/build/chunks/chunk-6AW5U52H.js +1 -0
- package/build/chunks/{chunk-Y6VY64LY.js → chunk-CVGQTUJ5.js} +2 -2
- package/build/chunks/chunk-FUXP2W57.js +1 -0
- package/build/chunks/chunk-LFTKNL2V.js +1 -0
- package/build/chunks/{chunk-PQYEV7WH.js → chunk-T6PPRDOE.js} +1 -1
- package/build/chunks/chunk-V6KYGP62.js +1 -0
- package/build/chunks/{export-VM6OQCQJ.js → export-2CFMTPJ3.js} +3 -3
- package/build/chunks/{export-cad-QXCB6UDU.js → export-cad-XOXIE726.js} +1 -1
- package/build/chunks/{export-zip-PVXETVLK.js → export-zip-I2ADLFJI.js} +1 -1
- package/build/chunks/measure-3LDALPCN.js +1 -0
- package/build/chunks/print-check-WWHTT5PW.js +1 -0
- package/build/chunks/{scene-export-ZIXMUNLI.js → scene-export-FPPL466I.js} +1 -1
- package/build/chunks/scene-reader-WXTRELY3.js +1 -0
- package/build/node/analysis-worker.js +2 -1
- package/build/stl_viewer.min.js +5 -5
- package/build/viewstl.cjs +1134 -392
- package/build/viewstl.d.cts +48 -0
- package/build/viewstl.d.ts +48 -0
- package/build/viewstl.js +1134 -392
- package/build/viewstl.min.js +5 -5
- package/build/viewstl.worker.js +1 -1
- package/docs/releases/2.0.0-rc.6.md +7 -0
- package/docs/v2-usage.md +24 -0
- package/docs/v2-verification.md +28 -0
- package/package.json +2 -2
- package/build/chunks/archive-JU75RPTU.js +0 -1
- package/build/chunks/chunk-IK3WL53C.js +0 -1
- package/build/chunks/chunk-OP2L7L4N.js +0 -1
- package/build/chunks/print-check-NJ4IONC5.js +0 -1
- package/build/chunks/scene-reader-JGHERXR2.js +0 -1
- package/docs/releases/2.0.0-rc.5.md +0 -46
package/build/viewstl.js
CHANGED
|
@@ -1,51 +1,3 @@
|
|
|
1
|
-
// src/appearance.js
|
|
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
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]);
|
|
@@ -178,41 +130,625 @@ function rayBox(origin, direction, min, max) {
|
|
|
178
130
|
if (origin[a] < min[a] || origin[a] > max[a]) return false;
|
|
179
131
|
continue;
|
|
180
132
|
}
|
|
181
|
-
let x = (min[a] - origin[a]) / direction[a], y = (max[a] - origin[a]) / direction[a];
|
|
182
|
-
if (x > y) [x, y] = [y, x];
|
|
183
|
-
near = Math.max(near, x);
|
|
184
|
-
far = Math.min(far, y);
|
|
185
|
-
if (far < near) return false;
|
|
133
|
+
let x = (min[a] - origin[a]) / direction[a], y = (max[a] - origin[a]) / direction[a];
|
|
134
|
+
if (x > y) [x, y] = [y, x];
|
|
135
|
+
near = Math.max(near, x);
|
|
136
|
+
far = Math.min(far, y);
|
|
137
|
+
if (far < near) return false;
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
function rayTriangle(o, d, p, i) {
|
|
142
|
+
const a = [p[i], p[i + 1], p[i + 2]], b = sub([p[i + 3], p[i + 4], p[i + 5]], a), c = sub([p[i + 6], p[i + 7], p[i + 8]], a);
|
|
143
|
+
const h = cross(d, c), det = dot(b, h);
|
|
144
|
+
if (Math.abs(det) < 1e-12) return Infinity;
|
|
145
|
+
const s = sub(o, a), u = dot(s, h) / det;
|
|
146
|
+
if (u < 0 || u > 1) return Infinity;
|
|
147
|
+
const q = cross(s, b), v = dot(d, q) / det;
|
|
148
|
+
if (v < 0 || u + v > 1) return Infinity;
|
|
149
|
+
const t = dot(c, q) / det;
|
|
150
|
+
return t >= 0 ? t : Infinity;
|
|
151
|
+
}
|
|
152
|
+
|
|
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
|
|
325
|
+
var MEMORY_OVERHEAD = 64 * 1024 * 1024;
|
|
326
|
+
var TRIANGLE_MEMORY = 224;
|
|
327
|
+
function geometryMemory(g) {
|
|
328
|
+
if (!g) return 0;
|
|
329
|
+
return g.triangles * TRIANGLE_MEMORY + (g.smoothNormals?.byteLength || 0) * 4 + ((g.edges?.byteLength || 0) + (g.wire?.byteLength || 0)) * 2;
|
|
330
|
+
}
|
|
331
|
+
function assertMemory(limits, bytes, operation = "model") {
|
|
332
|
+
if (bytes > (limits.max_memory_bytes ?? Number.MAX_SAFE_INTEGER))
|
|
333
|
+
throw new Error(
|
|
334
|
+
`Estimated memory for this ${operation} exceeds the device safety budget. Use a smaller model or a device with more memory.`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
function derivedMemory(triangles, features) {
|
|
338
|
+
return MEMORY_OVERHEAD + triangles * (TRIANGLE_MEMORY + (features.smooth ? 1536 : 0) + (features.edges || features.wire ? 2048 : 0));
|
|
339
|
+
}
|
|
340
|
+
|
|
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);
|
|
186
703
|
}
|
|
187
|
-
return true;
|
|
188
|
-
}
|
|
189
|
-
function rayTriangle(o, d, p, i) {
|
|
190
|
-
const a = [p[i], p[i + 1], p[i + 2]], b = sub([p[i + 3], p[i + 4], p[i + 5]], a), c = sub([p[i + 6], p[i + 7], p[i + 8]], a);
|
|
191
|
-
const h = cross(d, c), det = dot(b, h);
|
|
192
|
-
if (Math.abs(det) < 1e-12) return Infinity;
|
|
193
|
-
const s = sub(o, a), u = dot(s, h) / det;
|
|
194
|
-
if (u < 0 || u > 1) return Infinity;
|
|
195
|
-
const q = cross(s, b), v = dot(d, q) / det;
|
|
196
|
-
if (v < 0 || u + v > 1) return Infinity;
|
|
197
|
-
const t = dot(c, q) / det;
|
|
198
|
-
return t >= 0 ? t : Infinity;
|
|
199
704
|
}
|
|
200
705
|
|
|
201
|
-
// src/
|
|
202
|
-
var
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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 };
|
|
207
722
|
}
|
|
208
|
-
function
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
};
|
|
213
730
|
}
|
|
214
|
-
function
|
|
215
|
-
|
|
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;
|
|
216
752
|
}
|
|
217
753
|
|
|
218
754
|
// src/geometry.js
|
|
@@ -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
|
}
|
|
@@ -1061,205 +1617,34 @@ var WorkerPool = class {
|
|
|
1061
1617
|
return;
|
|
1062
1618
|
}
|
|
1063
1619
|
end(data.error ? new Error(data.error) : null, data.result);
|
|
1064
|
-
};
|
|
1065
|
-
job.worker.onerror = (e) => {
|
|
1066
|
-
e.preventDefault();
|
|
1067
|
-
end(
|
|
1068
|
-
new Error(
|
|
1069
|
-
e.message || "Worker failed; check worker URL and Content Security Policy"
|
|
1070
|
-
)
|
|
1071
|
-
);
|
|
1072
|
-
};
|
|
1073
|
-
job.worker.onmessageerror = () => end(new Error("Invalid worker response"));
|
|
1074
|
-
job.timer = setTimeout(
|
|
1075
|
-
() => end(new Error("File processing timed out")),
|
|
1076
|
-
this.timeout
|
|
1077
|
-
);
|
|
1078
|
-
job.worker.postMessage({ id: job.id, task: job.task }, job.transfer);
|
|
1079
|
-
} catch (error) {
|
|
1080
|
-
end(error);
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
clear() {
|
|
1085
|
-
for (const job of [...this.queue, ...this.active]) this.cancel(job);
|
|
1086
|
-
}
|
|
1087
|
-
dispose() {
|
|
1088
|
-
this.disposed = true;
|
|
1089
|
-
this.clear();
|
|
1090
|
-
}
|
|
1091
|
-
};
|
|
1092
|
-
|
|
1093
|
-
// src/animation.js
|
|
1094
|
-
var animationKeys = [
|
|
1095
|
-
"x",
|
|
1096
|
-
"y",
|
|
1097
|
-
"z",
|
|
1098
|
-
"rotationx",
|
|
1099
|
-
"rotationy",
|
|
1100
|
-
"rotationz",
|
|
1101
|
-
"scalex",
|
|
1102
|
-
"scaley",
|
|
1103
|
-
"scalez"
|
|
1104
|
-
];
|
|
1105
|
-
var isScale = (key) => key.startsWith("scale");
|
|
1106
|
-
var number = (value, name, positive2 = false) => {
|
|
1107
|
-
if (typeof value !== "number" || !Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER || positive2 && value <= 0)
|
|
1108
|
-
throw new Error(`Invalid animation ${name}`);
|
|
1109
|
-
return value;
|
|
1110
|
-
};
|
|
1111
|
-
var object = (value, name) => {
|
|
1112
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1113
|
-
throw new Error(`Invalid animation ${name}`);
|
|
1114
|
-
return value;
|
|
1115
|
-
};
|
|
1116
|
-
function animationPose(model) {
|
|
1117
|
-
return Object.fromEntries(
|
|
1118
|
-
animationKeys.map((key) => [
|
|
1119
|
-
key,
|
|
1120
|
-
number(
|
|
1121
|
-
model[key] ?? (isScale(key) ? model.scale ?? 1 : 0),
|
|
1122
|
-
key,
|
|
1123
|
-
isScale(key)
|
|
1124
|
-
)
|
|
1125
|
-
])
|
|
1126
|
-
);
|
|
1127
|
-
}
|
|
1128
|
-
function normalizeAnimation(input) {
|
|
1129
|
-
object(input, "definition");
|
|
1130
|
-
const result = {}, claimed = /* @__PURE__ */ new Set();
|
|
1131
|
-
for (const mode of ["delta", "exact"]) {
|
|
1132
|
-
if (input[mode] == null) continue;
|
|
1133
|
-
const source = object(input[mode], mode), values = {};
|
|
1134
|
-
const msec = number(source.msec ?? 300, "duration", true);
|
|
1135
|
-
if (source.loop !== void 0 && typeof source.loop !== "boolean")
|
|
1136
|
-
throw new Error("Animation loop must be a boolean");
|
|
1137
|
-
const loop = mode === "delta" && !!source.loop;
|
|
1138
|
-
for (const key of animationKeys) {
|
|
1139
|
-
const value = source[key] ?? (isScale(key) ? source.scale : void 0);
|
|
1140
|
-
if (value === void 0) continue;
|
|
1141
|
-
if (claimed.has(key))
|
|
1142
|
-
throw new Error(`Animation delta/exact conflict: ${key}`);
|
|
1143
|
-
number(value, key, mode === "exact" && isScale(key));
|
|
1144
|
-
if (loop && isScale(key) && value < 0)
|
|
1145
|
-
throw new Error("Looping scale deltas cannot be negative");
|
|
1146
|
-
values[key] = value;
|
|
1147
|
-
claimed.add(key);
|
|
1148
|
-
}
|
|
1149
|
-
if (!Object.keys(values).length)
|
|
1150
|
-
throw new Error("Animation track needs a transform");
|
|
1151
|
-
result[mode] = { ...values, msec, ...loop ? { loop: true } : {} };
|
|
1152
|
-
}
|
|
1153
|
-
if (!Object.keys(result).length)
|
|
1154
|
-
throw new Error("Animation needs delta or exact");
|
|
1155
|
-
return result;
|
|
1156
|
-
}
|
|
1157
|
-
function normalizePlaybackState(input) {
|
|
1158
|
-
object(input, "playback state");
|
|
1159
|
-
if (input.version !== 1 || typeof input.paused !== "boolean")
|
|
1160
|
-
throw new Error("Invalid animation playback state version or paused flag");
|
|
1161
|
-
const elapsed_ms = number(input.elapsed_ms, "elapsed time");
|
|
1162
|
-
if (elapsed_ms < 0) throw new Error("Invalid animation elapsed time");
|
|
1163
|
-
object(input.origin, "origin");
|
|
1164
|
-
for (const key of animationKeys) number(input.origin[key], key, isScale(key));
|
|
1165
|
-
if (!Array.isArray(input.center) || input.center.length !== 3)
|
|
1166
|
-
throw new Error("Invalid animation pivot");
|
|
1167
|
-
return {
|
|
1168
|
-
version: 1,
|
|
1169
|
-
elapsed_ms,
|
|
1170
|
-
paused: input.paused,
|
|
1171
|
-
origin: animationPose(input.origin),
|
|
1172
|
-
center: input.center.map((v) => number(v, "pivot"))
|
|
1173
|
-
};
|
|
1174
|
-
}
|
|
1175
|
-
function createPlayback(definition, model, saved) {
|
|
1176
|
-
const animation = normalizeAnimation(definition);
|
|
1177
|
-
const state = saved == null ? null : normalizePlaybackState(saved);
|
|
1178
|
-
const origin = state?.origin ?? animationPose(model);
|
|
1179
|
-
const tracks = Object.entries(animation).map(([mode, a]) => {
|
|
1180
|
-
const values = Object.entries(a).filter(
|
|
1181
|
-
([key]) => animationKeys.includes(key)
|
|
1182
|
-
);
|
|
1183
|
-
if (mode === "delta")
|
|
1184
|
-
for (const [key, value] of values)
|
|
1185
|
-
number(origin[key] + value, `${key} endpoint`, isScale(key));
|
|
1186
|
-
return { mode, values, duration: a.msec, loop: !!a.loop, done: false };
|
|
1187
|
-
});
|
|
1188
|
-
const playback = {
|
|
1189
|
-
animation,
|
|
1190
|
-
origin,
|
|
1191
|
-
tracks,
|
|
1192
|
-
elapsed_ms: state?.elapsed_ms ?? 0,
|
|
1193
|
-
paused: state?.paused ?? false,
|
|
1194
|
-
last_time: null
|
|
1195
|
-
};
|
|
1196
|
-
evaluatePlayback({ ...playback, tracks: tracks.map((t) => ({ ...t })) }, {});
|
|
1197
|
-
return playback;
|
|
1198
|
-
}
|
|
1199
|
-
function evaluatePlayback(playback, model) {
|
|
1200
|
-
const updates = [];
|
|
1201
|
-
for (const track of playback.tracks) {
|
|
1202
|
-
if (track.done) continue;
|
|
1203
|
-
const fraction = playback.elapsed_ms / track.duration;
|
|
1204
|
-
const p = track.loop ? fraction : Math.min(fraction, 1);
|
|
1205
|
-
for (const [key, value] of track.values) {
|
|
1206
|
-
const from = playback.origin[key];
|
|
1207
|
-
const to = track.mode === "delta" ? from + value * p : from * (1 - p) + value * p;
|
|
1208
|
-
updates.push([key, number(to, `${key} result`, isScale(key))]);
|
|
1620
|
+
};
|
|
1621
|
+
job.worker.onerror = (e) => {
|
|
1622
|
+
e.preventDefault();
|
|
1623
|
+
end(
|
|
1624
|
+
new Error(
|
|
1625
|
+
e.message || "Worker failed; check worker URL and Content Security Policy"
|
|
1626
|
+
)
|
|
1627
|
+
);
|
|
1628
|
+
};
|
|
1629
|
+
job.worker.onmessageerror = () => end(new Error("Invalid worker response"));
|
|
1630
|
+
job.timer = setTimeout(
|
|
1631
|
+
() => end(new Error("File processing timed out")),
|
|
1632
|
+
this.timeout
|
|
1633
|
+
);
|
|
1634
|
+
job.worker.postMessage({ id: job.id, task: job.task }, job.transfer);
|
|
1635
|
+
} catch (error) {
|
|
1636
|
+
end(error);
|
|
1637
|
+
}
|
|
1209
1638
|
}
|
|
1210
1639
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
scaleChanged ||= isScale(key) && model[key] !== value;
|
|
1214
|
-
model[key] = value;
|
|
1640
|
+
clear() {
|
|
1641
|
+
for (const job of [...this.queue, ...this.active]) this.cancel(job);
|
|
1215
1642
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
return playback.tracks.some((track) => !track.done);
|
|
1220
|
-
}
|
|
1221
|
-
function advancePlayback(playback, model, time) {
|
|
1222
|
-
if (playback.paused) return true;
|
|
1223
|
-
if (playback.last_time !== null)
|
|
1224
|
-
playback.elapsed_ms = number(
|
|
1225
|
-
playback.elapsed_ms + Math.max(0, time - playback.last_time),
|
|
1226
|
-
"elapsed time"
|
|
1227
|
-
);
|
|
1228
|
-
playback.last_time = time;
|
|
1229
|
-
return evaluatePlayback(playback, model);
|
|
1230
|
-
}
|
|
1231
|
-
function playbackState(playback, suspended = false) {
|
|
1232
|
-
return {
|
|
1233
|
-
status: playback.paused ? "paused" : "running",
|
|
1234
|
-
elapsed_ms: playback.elapsed_ms,
|
|
1235
|
-
suspended,
|
|
1236
|
-
tracks: playback.tracks.filter((t) => !t.done).map((t) => ({
|
|
1237
|
-
mode: t.mode,
|
|
1238
|
-
msec: t.duration,
|
|
1239
|
-
loop: t.loop,
|
|
1240
|
-
progress: t.loop ? playback.elapsed_ms % t.duration / t.duration : Math.min(1, playback.elapsed_ms / t.duration)
|
|
1241
|
-
}))
|
|
1242
|
-
};
|
|
1243
|
-
}
|
|
1244
|
-
function savePlayback(model) {
|
|
1245
|
-
const p = model._animation;
|
|
1246
|
-
if (!p) return null;
|
|
1247
|
-
const factor = model.unitScale ?? 1;
|
|
1248
|
-
const animation = structuredClone(p.animation), origin = { ...p.origin };
|
|
1249
|
-
for (const key of animationKeys.filter(isScale)) {
|
|
1250
|
-
origin[key] *= factor;
|
|
1251
|
-
for (const track of Object.values(animation))
|
|
1252
|
-
if (track[key] !== void 0) track[key] *= factor;
|
|
1643
|
+
dispose() {
|
|
1644
|
+
this.disposed = true;
|
|
1645
|
+
this.clear();
|
|
1253
1646
|
}
|
|
1254
|
-
|
|
1255
|
-
version: 1,
|
|
1256
|
-
origin,
|
|
1257
|
-
elapsed_ms: p.elapsed_ms,
|
|
1258
|
-
paused: p.paused,
|
|
1259
|
-
center: [...model.center]
|
|
1260
|
-
});
|
|
1261
|
-
return { animation: normalizeAnimation(animation), animation_state };
|
|
1262
|
-
}
|
|
1647
|
+
};
|
|
1263
1648
|
|
|
1264
1649
|
// src/scene-import.js
|
|
1265
1650
|
var sceneFormat = (name) => String(name || "").match(/\.(vsb|vsj)(?:[?#]|$)/i)?.[1].toLowerCase() || "";
|
|
@@ -1320,8 +1705,8 @@ function importScene(source, options = {}) {
|
|
|
1320
1705
|
await this.when_ready;
|
|
1321
1706
|
check();
|
|
1322
1707
|
oldCamera = this.get_camera_state();
|
|
1323
|
-
const
|
|
1324
|
-
if (!["vsb", "vsj"].includes(
|
|
1708
|
+
const format2 = options.format || sceneFormat(typeof source === "string" ? source : source.name);
|
|
1709
|
+
if (!["vsb", "vsj"].includes(format2))
|
|
1325
1710
|
throw new Error("Choose VSB or VSJ for a scene");
|
|
1326
1711
|
let remote, relay;
|
|
1327
1712
|
if (typeof source === "string") {
|
|
@@ -1354,8 +1739,8 @@ function importScene(source, options = {}) {
|
|
|
1354
1739
|
source,
|
|
1355
1740
|
remote,
|
|
1356
1741
|
relay,
|
|
1357
|
-
format,
|
|
1358
|
-
name: `scene.${
|
|
1742
|
+
format: format2,
|
|
1743
|
+
name: `scene.${format2}`,
|
|
1359
1744
|
limits
|
|
1360
1745
|
},
|
|
1361
1746
|
(p) => {
|
|
@@ -1376,6 +1761,10 @@ function importScene(source, options = {}) {
|
|
|
1376
1761
|
job = null;
|
|
1377
1762
|
check();
|
|
1378
1763
|
const { scene, files, base } = documentData;
|
|
1764
|
+
const bookmarks = mergeBookmarks(
|
|
1765
|
+
this.get_camera_bookmarks(),
|
|
1766
|
+
scene.camera_bookmarks || []
|
|
1767
|
+
);
|
|
1379
1768
|
this._sceneBytes -= held;
|
|
1380
1769
|
held = Object.values(files || {}).reduce((n, b) => n + b.byteLength, 0) + JSON.stringify(scene).length * 2;
|
|
1381
1770
|
this._sceneBytes += held;
|
|
@@ -1467,6 +1856,8 @@ function importScene(source, options = {}) {
|
|
|
1467
1856
|
if (scene.auto_rotate !== void 0)
|
|
1468
1857
|
this.set_auto_rotate(scene.auto_rotate);
|
|
1469
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;
|
|
1470
1861
|
committed = true;
|
|
1471
1862
|
return { model_ids: ids, scene, used_files: [...usedFiles] };
|
|
1472
1863
|
})().catch((error) => {
|
|
@@ -1487,61 +1878,6 @@ function importScene(source, options = {}) {
|
|
|
1487
1878
|
return promise;
|
|
1488
1879
|
}
|
|
1489
1880
|
|
|
1490
|
-
// src/print-options.js
|
|
1491
|
-
function printOptions(input = {}) {
|
|
1492
|
-
const level = input.level ?? "basic";
|
|
1493
|
-
if (!["basic", "detailed"].includes(level))
|
|
1494
|
-
throw new Error("Choose basic or detailed print checks");
|
|
1495
|
-
const number2 = (v, name, max = 1e9) => {
|
|
1496
|
-
if (!Number.isFinite(v) || v <= 0 || v > max)
|
|
1497
|
-
throw new Error(`Invalid ${name}`);
|
|
1498
|
-
return v;
|
|
1499
|
-
};
|
|
1500
|
-
let profile = null;
|
|
1501
|
-
if (input.profile != null) {
|
|
1502
|
-
if (input.profile.technology !== "fdm")
|
|
1503
|
-
throw new Error("Only FDM profiles are supported");
|
|
1504
|
-
profile = { technology: "fdm" };
|
|
1505
|
-
for (const key of ["min_wall_mm", "line_width_mm"])
|
|
1506
|
-
if (input.profile[key] != null)
|
|
1507
|
-
profile[key] = number2(input.profile[key], key);
|
|
1508
|
-
if (input.profile.overhang_degrees != null)
|
|
1509
|
-
profile.overhang_degrees = number2(
|
|
1510
|
-
input.profile.overhang_degrees,
|
|
1511
|
-
"overhang angle",
|
|
1512
|
-
89.9
|
|
1513
|
-
);
|
|
1514
|
-
if (input.profile.build_volume_mm != null) {
|
|
1515
|
-
if (!Array.isArray(input.profile.build_volume_mm) || input.profile.build_volume_mm.length !== 3)
|
|
1516
|
-
throw new Error("Build volume requires X, Y and Z");
|
|
1517
|
-
profile.build_volume_mm = input.profile.build_volume_mm.map(
|
|
1518
|
-
(v) => number2(v, "build volume")
|
|
1519
|
-
);
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
const time = number2(input.max_time_ms ?? 3e4, "check time limit", 12e4);
|
|
1523
|
-
return {
|
|
1524
|
-
level,
|
|
1525
|
-
profile,
|
|
1526
|
-
apply_transforms: input.apply_transforms !== false,
|
|
1527
|
-
units_confirmed: input.units_confirmed === true,
|
|
1528
|
-
max_time_ms: time
|
|
1529
|
-
};
|
|
1530
|
-
}
|
|
1531
|
-
var printMemory = (triangles) => 64 * 1024 ** 2 + triangles * 2400;
|
|
1532
|
-
var printSignature = (model) => JSON.stringify([
|
|
1533
|
-
model.x,
|
|
1534
|
-
model.y,
|
|
1535
|
-
model.z,
|
|
1536
|
-
model.rotationx,
|
|
1537
|
-
model.rotationy,
|
|
1538
|
-
model.rotationz,
|
|
1539
|
-
model.scaleVector,
|
|
1540
|
-
model.center,
|
|
1541
|
-
model.units,
|
|
1542
|
-
model.unitScale
|
|
1543
|
-
]);
|
|
1544
|
-
|
|
1545
1881
|
// src/print-controller.js
|
|
1546
1882
|
var reports = /* @__PURE__ */ new WeakMap();
|
|
1547
1883
|
function reportCurrent(viewer, report) {
|
|
@@ -1639,7 +1975,9 @@ async function checkPrintability(options = {}) {
|
|
|
1639
1975
|
rotationy: 0,
|
|
1640
1976
|
rotationz: 0,
|
|
1641
1977
|
center: [0, 0, 0],
|
|
1642
|
-
scaleVector: [
|
|
1978
|
+
scaleVector: [1, 1, 1].map(
|
|
1979
|
+
() => model.unitScale * (model.size_state?.unit_factor ?? 1)
|
|
1980
|
+
)
|
|
1643
1981
|
};
|
|
1644
1982
|
progress({
|
|
1645
1983
|
phase: "Starting",
|
|
@@ -1763,8 +2101,8 @@ function exportFilename(filename, extension) {
|
|
|
1763
2101
|
function exportOptions(options = {}) {
|
|
1764
2102
|
const aliases = { stp: "step", igs: "iges" };
|
|
1765
2103
|
const requested = String(options.format || "").toLowerCase();
|
|
1766
|
-
const
|
|
1767
|
-
if (!["stl", "obj", "3mf", "step", "iges"].includes(
|
|
2104
|
+
const format2 = aliases[requested] || requested;
|
|
2105
|
+
if (!["stl", "obj", "3mf", "step", "iges"].includes(format2))
|
|
1768
2106
|
throw new Error("Export format must be stl, obj, 3mf, step or iges");
|
|
1769
2107
|
const units = options.units ?? "mm";
|
|
1770
2108
|
if (!["mm", "inch"].includes(units))
|
|
@@ -1774,10 +2112,10 @@ function exportOptions(options = {}) {
|
|
|
1774
2112
|
throw new Error(`${key} must be a boolean`);
|
|
1775
2113
|
if (options.on_progress !== void 0 && typeof options.on_progress !== "function")
|
|
1776
2114
|
throw new Error("on_progress must be a function");
|
|
1777
|
-
const colors = options.colors !== false && ["obj", "3mf"].includes(
|
|
1778
|
-
const extension =
|
|
2115
|
+
const colors = options.colors !== false && ["obj", "3mf"].includes(format2);
|
|
2116
|
+
const extension = format2 === "obj" && colors ? "zip" : format2;
|
|
1779
2117
|
return {
|
|
1780
|
-
format,
|
|
2118
|
+
format: format2,
|
|
1781
2119
|
units,
|
|
1782
2120
|
colors,
|
|
1783
2121
|
apply_transforms: options.apply_transforms !== false,
|
|
@@ -1970,7 +2308,9 @@ var modelKeys = [
|
|
|
1970
2308
|
"section",
|
|
1971
2309
|
"units",
|
|
1972
2310
|
"animation",
|
|
1973
|
-
"animation_state"
|
|
2311
|
+
"animation_state",
|
|
2312
|
+
"size_state",
|
|
2313
|
+
"display_name"
|
|
1974
2314
|
];
|
|
1975
2315
|
var finite = (v, name) => {
|
|
1976
2316
|
if (typeof v !== "number" || !Number.isFinite(v))
|
|
@@ -2002,7 +2342,7 @@ function modelOptions(input) {
|
|
|
2002
2342
|
return out;
|
|
2003
2343
|
}
|
|
2004
2344
|
var StlViewer = class _StlViewer {
|
|
2005
|
-
static version = "2.0.0-rc.
|
|
2345
|
+
static version = "2.0.0-rc.6";
|
|
2006
2346
|
static workerURL = null;
|
|
2007
2347
|
constructor(parent, options = {}) {
|
|
2008
2348
|
if (!parent?.appendChild)
|
|
@@ -2031,6 +2371,8 @@ var StlViewer = class _StlViewer {
|
|
|
2031
2371
|
this._sessionActive = false;
|
|
2032
2372
|
this._frame = 0;
|
|
2033
2373
|
this._lastTime = 0;
|
|
2374
|
+
this.camera_bookmarks = [];
|
|
2375
|
+
this.display_units = "mm";
|
|
2034
2376
|
this.grid = null;
|
|
2035
2377
|
this._gridAuto = false;
|
|
2036
2378
|
this.camera = {
|
|
@@ -2367,6 +2709,13 @@ var StlViewer = class _StlViewer {
|
|
|
2367
2709
|
finite(m[key], key);
|
|
2368
2710
|
if (key.startsWith("scale")) positive(m[key], key);
|
|
2369
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
|
+
}
|
|
2370
2719
|
m.opacity = supplied.opacity ?? supplied.mesh?.material?.opacity ?? 1;
|
|
2371
2720
|
finite(m.opacity, "opacity");
|
|
2372
2721
|
if (m.opacity < 0 || m.opacity > 1)
|
|
@@ -2518,7 +2867,7 @@ var StlViewer = class _StlViewer {
|
|
|
2518
2867
|
format: supplied.remote_format,
|
|
2519
2868
|
relay,
|
|
2520
2869
|
source: typeof source === "string" ? new URL(source, document.baseURI).href : source,
|
|
2521
|
-
name: m.filename,
|
|
2870
|
+
name: m.display_name || m.filename,
|
|
2522
2871
|
limits,
|
|
2523
2872
|
features: features.smooth || features.edges || features.wire ? features : null
|
|
2524
2873
|
},
|
|
@@ -2568,6 +2917,7 @@ var StlViewer = class _StlViewer {
|
|
|
2568
2917
|
}
|
|
2569
2918
|
_deleteModel(m) {
|
|
2570
2919
|
this._stopAnimation(m, "removed");
|
|
2920
|
+
for (const controller of m._measureJobs || []) controller.abort();
|
|
2571
2921
|
for (const [controller, models] of this._printJobs || [])
|
|
2572
2922
|
if (models.has(m)) controller.abort();
|
|
2573
2923
|
this._printCache = null;
|
|
@@ -2595,6 +2945,8 @@ var StlViewer = class _StlViewer {
|
|
|
2595
2945
|
if (this.killsign) return;
|
|
2596
2946
|
this._generation++;
|
|
2597
2947
|
this._printCache = null;
|
|
2948
|
+
this.camera_bookmarks = [];
|
|
2949
|
+
this.display_units = "mm";
|
|
2598
2950
|
this._pool?.clear();
|
|
2599
2951
|
for (const c of this._aborters) c.abort();
|
|
2600
2952
|
this._aborters.clear();
|
|
@@ -3034,12 +3386,42 @@ var StlViewer = class _StlViewer {
|
|
|
3034
3386
|
type
|
|
3035
3387
|
);
|
|
3036
3388
|
}
|
|
3037
|
-
|
|
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 = {}) {
|
|
3038
3420
|
return this._guard(() => {
|
|
3039
3421
|
const m = this._model(id), g = m.geometry;
|
|
3040
3422
|
if (!g) return null;
|
|
3041
3423
|
const s = m.scaleVector, key = s.join(",");
|
|
3042
|
-
if (!m._stats || m._stats.key !== key)
|
|
3424
|
+
if (options.measurements !== false && (!m._stats || m._stats.key !== key))
|
|
3043
3425
|
m._stats = { key, ...scaledStats(g, s) };
|
|
3044
3426
|
const factor = m.units === "inch" ? 1 / 25.4 : 1;
|
|
3045
3427
|
return {
|
|
@@ -3055,8 +3437,8 @@ var StlViewer = class _StlViewer {
|
|
|
3055
3437
|
section: m.section ? { ...m.section } : null,
|
|
3056
3438
|
color: m.color ?? (g.colors ? "#ffffff" : "#909090"),
|
|
3057
3439
|
scale: { x: m.scalex, y: m.scaley, z: m.scalez },
|
|
3058
|
-
volume: m._stats.volume * factor ** 3,
|
|
3059
|
-
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,
|
|
3060
3442
|
triangles: g.triangles,
|
|
3061
3443
|
bounds: { min: [...g.bounds.min], max: [...g.bounds.max] },
|
|
3062
3444
|
has_colors: !!g.colors,
|
|
@@ -3144,7 +3526,9 @@ var StlViewer = class _StlViewer {
|
|
|
3144
3526
|
rotationx: 0,
|
|
3145
3527
|
rotationy: 0,
|
|
3146
3528
|
rotationz: 0,
|
|
3147
|
-
scaleVector: [
|
|
3529
|
+
scaleVector: [1, 1, 1].map(
|
|
3530
|
+
() => m.unitScale * (m.size_state?.unit_factor ?? 1)
|
|
3531
|
+
)
|
|
3148
3532
|
};
|
|
3149
3533
|
const matrix = modelMatrix(source), factor = settings.units === "inch" ? 1 / 25.4 : 1;
|
|
3150
3534
|
for (let c = 0; c < 4; c++)
|
|
@@ -3212,8 +3596,8 @@ var StlViewer = class _StlViewer {
|
|
|
3212
3596
|
);
|
|
3213
3597
|
}
|
|
3214
3598
|
async export_scene(options = {}) {
|
|
3215
|
-
const
|
|
3216
|
-
if (!["vsb", "vsj"].includes(
|
|
3599
|
+
const format2 = String(options.format || "").toLowerCase();
|
|
3600
|
+
if (!["vsb", "vsj"].includes(format2))
|
|
3217
3601
|
throw new Error("Scene format must be vsb or vsj");
|
|
3218
3602
|
if (options.force_basename !== void 0 && typeof options.force_basename !== "boolean")
|
|
3219
3603
|
throw new Error("force_basename must be a boolean");
|
|
@@ -3236,7 +3620,7 @@ var StlViewer = class _StlViewer {
|
|
|
3236
3620
|
if (!models.length) throw new Error("No models to export");
|
|
3237
3621
|
if (models.some((m) => !m.geometry))
|
|
3238
3622
|
throw new Error("A model has not finished loading");
|
|
3239
|
-
if (models.length + (
|
|
3623
|
+
if (models.length + (format2 === "vsb" ? 1 : 0) > this.limits.max_archive_entries)
|
|
3240
3624
|
throw new Error("Scene model count limit exceeded");
|
|
3241
3625
|
const triangles = models.reduce((sum, m) => sum + m.geometry.triangles, 0);
|
|
3242
3626
|
if (triangles > this.limits.max_triangles)
|
|
@@ -3244,19 +3628,19 @@ var StlViewer = class _StlViewer {
|
|
|
3244
3628
|
const scene = this.get_vsj(
|
|
3245
3629
|
true,
|
|
3246
3630
|
!!options.force_basename,
|
|
3247
|
-
|
|
3631
|
+
format2 === "vsb"
|
|
3248
3632
|
);
|
|
3249
3633
|
const json = JSON.stringify(scene), manifestBytes = new Blob([json]).size;
|
|
3250
3634
|
if (manifestBytes > this.limits.max_file_bytes)
|
|
3251
3635
|
throw new Error("Scene manifest size limit exceeded");
|
|
3252
|
-
if (
|
|
3636
|
+
if (format2 === "vsb" && manifestBytes + models.length * 84 + triangles * 50 > this.limits.max_uncompressed_bytes)
|
|
3253
3637
|
throw new Error("Scene archive limit exceeded");
|
|
3254
|
-
const working = MEMORY_OVERHEAD + json.length * 8 + (
|
|
3638
|
+
const working = MEMORY_OVERHEAD + json.length * 8 + (format2 === "vsb" ? triangles * 512 : 0);
|
|
3255
3639
|
assertMemory(this.limits, this._occupiedMemory() + working, "scene export");
|
|
3256
3640
|
this._exportBytes += working;
|
|
3257
3641
|
let job, cancel, callbackError;
|
|
3258
3642
|
try {
|
|
3259
|
-
const snapshot =
|
|
3643
|
+
const snapshot = format2 === "vsb" ? models.map((m, index) => ({
|
|
3260
3644
|
id: m.id,
|
|
3261
3645
|
triangles: m.geometry.triangles,
|
|
3262
3646
|
positions: m.geometry.positions.slice(),
|
|
@@ -3271,7 +3655,7 @@ var StlViewer = class _StlViewer {
|
|
|
3271
3655
|
});
|
|
3272
3656
|
checkAbort();
|
|
3273
3657
|
let blob;
|
|
3274
|
-
if (
|
|
3658
|
+
if (format2 === "vsj") {
|
|
3275
3659
|
blob = new Blob([json], { type: "application/json" });
|
|
3276
3660
|
options.on_progress?.({
|
|
3277
3661
|
loaded: models.length,
|
|
@@ -3311,8 +3695,8 @@ var StlViewer = class _StlViewer {
|
|
|
3311
3695
|
checkAbort();
|
|
3312
3696
|
return {
|
|
3313
3697
|
blob,
|
|
3314
|
-
format,
|
|
3315
|
-
filename: exportFilename(options.filename,
|
|
3698
|
+
format: format2,
|
|
3699
|
+
filename: exportFilename(options.filename, format2),
|
|
3316
3700
|
model_ids: models.map((m) => m.id),
|
|
3317
3701
|
triangles,
|
|
3318
3702
|
representation: "scene"
|
|
@@ -3334,6 +3718,8 @@ var StlViewer = class _StlViewer {
|
|
|
3334
3718
|
canvas_height: this.canvas_height,
|
|
3335
3719
|
bg_color: this.bg_color,
|
|
3336
3720
|
camera_state: this.get_camera_state(),
|
|
3721
|
+
camera_bookmarks: this.get_camera_bookmarks(),
|
|
3722
|
+
display_units: this.display_units || "mm",
|
|
3337
3723
|
auto_rotate: this.auto_rotate,
|
|
3338
3724
|
mouse_zoom: this.mouse_zoom,
|
|
3339
3725
|
auto_resize: this.auto_resize,
|
|
@@ -3354,6 +3740,11 @@ var StlViewer = class _StlViewer {
|
|
|
3354
3740
|
id: for_vsb ? -1 : m.id,
|
|
3355
3741
|
filename: for_vsb ? `${m.id}.stl` : force_basename ? basename(m.filename) : m.filename,
|
|
3356
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 } : {},
|
|
3357
3748
|
display: m.display,
|
|
3358
3749
|
opacity: m.opacity,
|
|
3359
3750
|
view_edges: m.view_edges,
|
|
@@ -3476,6 +3867,13 @@ var StlViewer = class _StlViewer {
|
|
|
3476
3867
|
);
|
|
3477
3868
|
else if (scene.grid) this.set_grid(true);
|
|
3478
3869
|
if (scene.camera_state) this._restoreCamera = scene.camera_state;
|
|
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
|
+
);
|
|
3479
3877
|
const incoming = [];
|
|
3480
3878
|
try {
|
|
3481
3879
|
for (const input of scene.models) {
|
|
@@ -3523,6 +3921,8 @@ var StlViewer = class _StlViewer {
|
|
|
3523
3921
|
this._startAnimation(m, model.animation, model.animation_state);
|
|
3524
3922
|
m._sceneImportPending = false;
|
|
3525
3923
|
}
|
|
3924
|
+
this.camera_bookmarks = bookmarks;
|
|
3925
|
+
this.display_units = displayUnits;
|
|
3526
3926
|
} catch (error) {
|
|
3527
3927
|
for (const { m } of incoming)
|
|
3528
3928
|
if (this._models.get(m.id) === m) this.remove_model(m.id);
|
|
@@ -3665,6 +4065,299 @@ var StlViewer = class _StlViewer {
|
|
|
3665
4065
|
}
|
|
3666
4066
|
};
|
|
3667
4067
|
|
|
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
|
+
|
|
3668
4361
|
// src/memory-policy.js
|
|
3669
4362
|
var GiB = 1024 ** 3;
|
|
3670
4363
|
var MiB = 1024 ** 2;
|
|
@@ -3696,9 +4389,9 @@ async function estimateFile(file) {
|
|
|
3696
4389
|
}
|
|
3697
4390
|
|
|
3698
4391
|
// src/model-transform.js
|
|
3699
|
-
var
|
|
3700
|
-
var vector2 = (p) =>
|
|
3701
|
-
var object2 = (p) => Object.fromEntries(
|
|
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]]));
|
|
3702
4395
|
var dot2 = (a, b) => a.reduce((sum, v, i) => sum + v * b[i], 0);
|
|
3703
4396
|
var norm2 = (a) => {
|
|
3704
4397
|
const n = Math.hypot(...a) || 1;
|
|
@@ -3730,7 +4423,7 @@ function rotationMatrix(degrees = { x: 0, y: 0, z: 0 }) {
|
|
|
3730
4423
|
var centerOf = (bounds) => bounds.min.map((v, i) => (v + bounds.max[i]) / 2);
|
|
3731
4424
|
function modelCenter(model) {
|
|
3732
4425
|
return object2(
|
|
3733
|
-
centerOf(model.bounds).map((v, i) => v + model.position[
|
|
4426
|
+
centerOf(model.bounds).map((v, i) => v + model.position[axes3[i]])
|
|
3734
4427
|
);
|
|
3735
4428
|
}
|
|
3736
4429
|
function modelRadius(model) {
|
|
@@ -3741,7 +4434,7 @@ function modelRadius(model) {
|
|
|
3741
4434
|
}
|
|
3742
4435
|
function enginePosition(model) {
|
|
3743
4436
|
const c = centerOf(model.bounds), rotated = transform2(rotationMatrix(model.rotation), c);
|
|
3744
|
-
return object2(c.map((v, i) => model.position[
|
|
4437
|
+
return object2(c.map((v, i) => model.position[axes3[i]] + v - rotated[i]));
|
|
3745
4438
|
}
|
|
3746
4439
|
function modelBounds(model) {
|
|
3747
4440
|
if (!model.rotation || !Object.values(model.rotation).some((v) => v % 360))
|
|
@@ -4082,16 +4775,16 @@ function bindAppExport({
|
|
|
4082
4775
|
$("save-error").textContent = "";
|
|
4083
4776
|
};
|
|
4084
4777
|
function formatChanged() {
|
|
4085
|
-
const
|
|
4778
|
+
const format2 = $("save-format").value;
|
|
4086
4779
|
if (!$("save-name").value || $("save-name").value === lastSuggestedName) {
|
|
4087
4780
|
suggestedName();
|
|
4088
4781
|
}
|
|
4089
4782
|
if ($("save-mesh-note")) $("save-mesh-note").hidden = isScene();
|
|
4090
4783
|
if ($("save-scene-note")) $("save-scene-note").hidden = !isScene();
|
|
4091
|
-
$("save-colors-row").hidden = !["obj", "3mf"].includes(
|
|
4092
|
-
$("save-cad-note").hidden = !["step", "iges"].includes(
|
|
4784
|
+
$("save-colors-row").hidden = !["obj", "3mf"].includes(format2);
|
|
4785
|
+
$("save-cad-note").hidden = !["step", "iges"].includes(format2);
|
|
4093
4786
|
$("save-format-note").textContent = $("save-format").selectedOptions[0]?.dataset.note || "";
|
|
4094
|
-
$("save-submit").textContent =
|
|
4787
|
+
$("save-submit").textContent = format2 === "obj" && $("save-colors").checked ? "Download ZIP" : "Download file";
|
|
4095
4788
|
sync();
|
|
4096
4789
|
}
|
|
4097
4790
|
function suggestedName() {
|
|
@@ -4542,7 +5235,7 @@ var animationControls = [
|
|
|
4542
5235
|
"animation-status",
|
|
4543
5236
|
"animation-error"
|
|
4544
5237
|
];
|
|
4545
|
-
var
|
|
5238
|
+
var axes4 = ["x", "y", "z"];
|
|
4546
5239
|
function syncAnimatedRecord(record, model) {
|
|
4547
5240
|
if (!record || !model?.geometry) return;
|
|
4548
5241
|
record.bounds = Object.fromEntries(
|
|
@@ -4554,11 +5247,11 @@ function syncAnimatedRecord(record, model) {
|
|
|
4554
5247
|
])
|
|
4555
5248
|
);
|
|
4556
5249
|
record.rotation = Object.fromEntries(
|
|
4557
|
-
|
|
5250
|
+
axes4.map((a) => [a, model["rotation" + a] * 180 / Math.PI % 360])
|
|
4558
5251
|
);
|
|
4559
5252
|
record.position = { x: 0, y: 0, z: 0 };
|
|
4560
5253
|
const offset = enginePosition(record);
|
|
4561
|
-
for (const a of
|
|
5254
|
+
for (const a of axes4) record.position[a] = model[a] - offset[a];
|
|
4562
5255
|
}
|
|
4563
5256
|
function centerAnimationPivot(record, model) {
|
|
4564
5257
|
syncAnimatedRecord(record, model);
|
|
@@ -4571,10 +5264,10 @@ function centerAnimationPivot(record, model) {
|
|
|
4571
5264
|
const rotation2 = rotationMatrix(record.rotation);
|
|
4572
5265
|
const original = record.originalPosition || { x: 0, y: 0, z: 0 };
|
|
4573
5266
|
record.originalPosition = Object.fromEntries(
|
|
4574
|
-
|
|
5267
|
+
axes4.map((a, i) => [a, original[a] + delta[i]])
|
|
4575
5268
|
);
|
|
4576
5269
|
for (let i = 0; i < 3; i++)
|
|
4577
|
-
model[
|
|
5270
|
+
model[axes4[i]] += delta.reduce(
|
|
4578
5271
|
(sum, v, j) => sum + rotation2[i * 3 + j] * v,
|
|
4579
5272
|
0
|
|
4580
5273
|
);
|
|
@@ -4583,7 +5276,7 @@ function centerAnimationPivot(record, model) {
|
|
|
4583
5276
|
syncAnimatedRecord(record, model);
|
|
4584
5277
|
}
|
|
4585
5278
|
function spinDefinition(axis, seconds, direction) {
|
|
4586
|
-
if (!
|
|
5279
|
+
if (!axes4.includes(axis) || !Number.isFinite(seconds) || seconds < 0.1 || seconds > 3600 || !["1", "-1"].includes(direction))
|
|
4587
5280
|
throw new Error("Choose an axis and a duration from 0.1 to 3,600 seconds.");
|
|
4588
5281
|
return {
|
|
4589
5282
|
delta: {
|
|
@@ -4840,6 +5533,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4840
5533
|
missing.push(...printControls.filter((id) => !$(id)));
|
|
4841
5534
|
if (animationControls.some((id) => $(id)))
|
|
4842
5535
|
missing.push(...animationControls.filter((id) => !$(id)));
|
|
5536
|
+
if (toolsControls.some((id) => $(id)))
|
|
5537
|
+
missing.push(...toolsControls.filter((id) => !$(id)));
|
|
4843
5538
|
if (missing.length)
|
|
4844
5539
|
throw new Error("Missing ViewSTL controls: " + missing.join(", "));
|
|
4845
5540
|
const abort = new window2.AbortController();
|
|
@@ -4868,6 +5563,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4868
5563
|
let exportUI;
|
|
4869
5564
|
let printUI;
|
|
4870
5565
|
let animationUI;
|
|
5566
|
+
let toolsUI;
|
|
4871
5567
|
let sceneImport;
|
|
4872
5568
|
let draining = false, busy = false, gridEnabled = false, gridHeight = 0, gridSpacing = 1, moveMode = false, transformTool = "move", modelMover, dragArrangement, background = "transparent";
|
|
4873
5569
|
const viewNames = {
|
|
@@ -4916,6 +5612,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4916
5612
|
$(name + "-panel").hidden = !open;
|
|
4917
5613
|
$(name + "-button").setAttribute("aria-expanded", String(open));
|
|
4918
5614
|
syncSelection();
|
|
5615
|
+
if (open && name === "info") populateInfo();
|
|
5616
|
+
if (open && (name === "scene" || name === "view")) toolsUI?.sync();
|
|
4919
5617
|
}
|
|
4920
5618
|
function fileSize(size) {
|
|
4921
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`;
|
|
@@ -4942,12 +5640,13 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4942
5640
|
}
|
|
4943
5641
|
function syncControls() {
|
|
4944
5642
|
syncAnimatedRecord(records.get(modelID), viewer?._models.get(modelID));
|
|
4945
|
-
const info = modelID === null ? null : viewer.get_model_info(modelID), mode = info?.display === "smooth" ? "flat" : info?.display || "flat";
|
|
5643
|
+
const info = modelID === null ? null : viewer.get_model_info(modelID, { measurements: false }), mode = info?.display === "smooth" ? "flat" : info?.display || "flat";
|
|
4946
5644
|
for (const input of modeInputs) {
|
|
4947
5645
|
input.checked = input.value === mode;
|
|
4948
5646
|
input.disabled = busy;
|
|
4949
5647
|
}
|
|
4950
|
-
$("view-label").textContent = viewNames[mode];
|
|
5648
|
+
$("view-label").textContent = toolsUI ? "View" : viewNames[mode];
|
|
5649
|
+
$("view-button").title = "View \xB7 " + viewNames[mode];
|
|
4951
5650
|
$("smooth-shading").checked = info?.smooth_shading || false;
|
|
4952
5651
|
$("smooth-shading").disabled = busy || mode === "wireframe";
|
|
4953
5652
|
$("view-status").textContent = mode === "wireframe" ? "Smooth shading is available in surface views." : "Shading changes the appearance, not the geometry.";
|
|
@@ -4971,14 +5670,17 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4971
5670
|
exportUI?.sync();
|
|
4972
5671
|
printUI?.sync();
|
|
4973
5672
|
animationUI?.sync();
|
|
5673
|
+
toolsUI?.sync();
|
|
4974
5674
|
}
|
|
4975
5675
|
function populateInfo() {
|
|
4976
5676
|
if (modelID === null) return;
|
|
4977
|
-
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;
|
|
4978
5678
|
$("info-triangles").textContent = info.triangles.toLocaleString();
|
|
4979
|
-
$("info-dimensions").textContent = [info.dims.x, info.dims.y, info.dims.z].map((x) => formatNumber.format(x)).join(" \xD7 ") +
|
|
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");
|
|
4980
5680
|
$("info-size").textContent = fileSize(record.size);
|
|
4981
|
-
|
|
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();
|
|
4982
5684
|
}
|
|
4983
5685
|
function renderList() {
|
|
4984
5686
|
$("model-list").replaceChildren(
|
|
@@ -5080,7 +5782,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5080
5782
|
input.removeAttribute("aria-invalid");
|
|
5081
5783
|
}
|
|
5082
5784
|
$("position-error").hidden = true;
|
|
5083
|
-
$("position-units").textContent = record?.animationPivot ? "Position of the model center in the scene
|
|
5785
|
+
$("position-units").textContent = record?.animationPivot ? "Position of the model center in the scene, in mm." : "Offset from file coordinates, in mm.";
|
|
5084
5786
|
$("move-model-button").disabled = !record || busy;
|
|
5085
5787
|
$("reset-position").disabled = !record;
|
|
5086
5788
|
syncRotation(preserveFocus);
|
|
@@ -5163,11 +5865,11 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5163
5865
|
$("model-tool").value = transformTool;
|
|
5164
5866
|
$("move-gesture-message").textContent = toolHints[transformTool] + " \xB7 Scroll to adjust depth";
|
|
5165
5867
|
}
|
|
5166
|
-
function setMoveMode(enabled, tool = transformTool) {
|
|
5868
|
+
function setMoveMode(enabled, tool = transformTool, retainPaused = false) {
|
|
5167
5869
|
modelMover?.setEnabled(false);
|
|
5168
5870
|
transformTool = tool;
|
|
5169
5871
|
moveMode = !!enabled && modelID !== null && !busy;
|
|
5170
|
-
if (moveMode) prepareManual(modelID);
|
|
5872
|
+
if (moveMode && !retainPaused) prepareManual(modelID);
|
|
5171
5873
|
modelMover?.setEnabled(moveMode);
|
|
5172
5874
|
syncTool();
|
|
5173
5875
|
$("move-mode").hidden = !moveMode;
|
|
@@ -5344,6 +6046,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5344
6046
|
function refreshAnimatedPose() {
|
|
5345
6047
|
if (disposed || modelID === null) return;
|
|
5346
6048
|
syncAnimatedRecord(records.get(modelID), viewer?._models.get(modelID));
|
|
6049
|
+
if (!$("info-panel").hidden) populateInfo();
|
|
5347
6050
|
if (!$("scene-panel").hidden && $("position-details").open)
|
|
5348
6051
|
syncPosition(true);
|
|
5349
6052
|
}
|
|
@@ -5468,7 +6171,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5468
6171
|
if (queue.length && !draining) drainPromise = drainQueue();
|
|
5469
6172
|
return drainPromise || Promise.resolve();
|
|
5470
6173
|
}
|
|
5471
|
-
function openURL(value,
|
|
6174
|
+
function openURL(value, format2 = "") {
|
|
5472
6175
|
assertActive();
|
|
5473
6176
|
if (!options.url_import_endpoint)
|
|
5474
6177
|
throw new Error("URL imports are not configured.");
|
|
@@ -5477,7 +6180,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5477
6180
|
throw new Error(
|
|
5478
6181
|
"Use a public HTTP or HTTPS file URL without login credentials."
|
|
5479
6182
|
);
|
|
5480
|
-
if (
|
|
6183
|
+
if (format2 && !/^(stl|obj|3mf|step|stp|iges|igs|vsb|vsj)$/.test(format2))
|
|
5481
6184
|
throw new Error("Choose a supported 3D format.");
|
|
5482
6185
|
url.hash = "";
|
|
5483
6186
|
const name = decodeURIComponent(
|
|
@@ -5486,9 +6189,9 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5486
6189
|
queue.push({
|
|
5487
6190
|
name,
|
|
5488
6191
|
remote: url.href,
|
|
5489
|
-
format,
|
|
6192
|
+
format: format2,
|
|
5490
6193
|
size: 0,
|
|
5491
|
-
scene: ["vsb", "vsj"].includes(
|
|
6194
|
+
scene: ["vsb", "vsj"].includes(format2) ? format2 : sceneFormat(name)
|
|
5492
6195
|
});
|
|
5493
6196
|
if (!draining) drainPromise = drainQueue();
|
|
5494
6197
|
return drainPromise || Promise.resolve();
|
|
@@ -5557,7 +6260,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5557
6260
|
await engine.when_idle();
|
|
5558
6261
|
if (token !== generation) continue;
|
|
5559
6262
|
loadingID = null;
|
|
5560
|
-
const loaded = lastLoadedID === id ? engine.get_model_info(id) : null;
|
|
6263
|
+
const loaded = lastLoadedID === id ? engine.get_model_info(id, { measurements: false }) : null;
|
|
5561
6264
|
if (!loaded) {
|
|
5562
6265
|
announce(source.name + " could not be opened.");
|
|
5563
6266
|
continue;
|
|
@@ -5649,7 +6352,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5649
6352
|
);
|
|
5650
6353
|
const record = {
|
|
5651
6354
|
id,
|
|
5652
|
-
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],
|
|
5653
6356
|
size: g.file_bytes ?? m.local_file?.size ?? null,
|
|
5654
6357
|
units: g.units || m.units,
|
|
5655
6358
|
bounds,
|
|
@@ -5978,6 +6681,44 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5978
6681
|
refreshPose: refreshAnimatedPose,
|
|
5979
6682
|
announce
|
|
5980
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
|
+
});
|
|
5981
6722
|
updateGrid();
|
|
5982
6723
|
setBusy(false);
|
|
5983
6724
|
setMoveMode(false);
|
|
@@ -6026,6 +6767,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
6026
6767
|
exportUI?.dispose();
|
|
6027
6768
|
printUI?.dispose();
|
|
6028
6769
|
animationUI?.dispose();
|
|
6770
|
+
toolsUI?.dispose();
|
|
6029
6771
|
cancelLoading();
|
|
6030
6772
|
setMoveMode(false);
|
|
6031
6773
|
appearanceRequest++;
|