@omrips/viewstl 2.0.0-rc.4 → 2.0.0-rc.5
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/{v2/THIRD-PARTY-NOTICES.txt → THIRD-PARTY-NOTICES.txt} +1006 -1006
- package/build/{v2/cad → cad}/occt-import-js.source.txt +19 -19
- package/build/chunks/archive-JU75RPTU.js +1 -0
- package/build/{v2/chunks/cad-VY42CMP4.js → chunks/cad-4JYHORYZ.js} +1 -1
- package/build/{v2/chunks/chunk-JH3H3JWA.js → chunks/chunk-OP2L7L4N.js} +1 -1
- package/build/{v2/chunks/chunk-ZMK3UZZE.js → chunks/chunk-PQYEV7WH.js} +1 -1
- package/build/{v2/chunks/chunk-R3HNTB5A.js → chunks/chunk-Y6VY64LY.js} +1 -1
- package/build/{v2/chunks/export-7OP4ZGGJ.js → chunks/export-VM6OQCQJ.js} +3 -3
- package/build/{v2/chunks/export-cad-QPBT756V.js → chunks/export-cad-QXCB6UDU.js} +1 -1
- package/build/{v2/chunks/export-zip-EDCWH5PX.js → chunks/export-zip-PVXETVLK.js} +1 -1
- package/build/{v2/chunks/print-check-2PU7CSL3.js → chunks/print-check-NJ4IONC5.js} +1 -1
- package/build/{v2/chunks/scene-export-KQBS7VRJ.js → chunks/scene-export-ZIXMUNLI.js} +1 -1
- package/build/chunks/scene-reader-JGHERXR2.js +1 -0
- package/build/{v2/node → node}/analysis-worker.js +11 -11
- 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} +752 -125
- package/build/{v2/viewstl.d.cts → viewstl.d.cts} +50 -0
- package/build/{v2/viewstl.d.ts → viewstl.d.ts} +50 -0
- package/build/{v2/viewstl.js → viewstl.js} +752 -125
- package/build/viewstl.min.js +51 -0
- package/build/{v2/viewstl.worker.js → viewstl.worker.js} +1 -1
- package/docs/releases/2.0.0-rc.5.md +46 -0
- package/docs/v2-compatibility.md +5 -5
- package/docs/v2-usage.md +128 -21
- package/docs/v2-verification.md +141 -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/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/cad → cad}/occt-import-js.js +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-U33KANV7.js → chunks/chunk-IK3WL53C.js} +0 -0
- /package/build/{v2/chunks/chunk-JS56KC3I.js → chunks/chunk-W5KVLGVR.js} +0 -0
- /package/build/{v2/chunks → chunks}/chunk-ZSV4PTIA.js +0 -0
- /package/build/{v2/node → node}/index.d.cts +0 -0
- /package/build/{v2/node → node}/index.d.ts +0 -0
|
@@ -16,7 +16,7 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
16
|
};
|
|
17
17
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
18
|
|
|
19
|
-
// src/
|
|
19
|
+
// src/commonjs.js
|
|
20
20
|
var commonjs_exports = {};
|
|
21
21
|
__export(commonjs_exports, {
|
|
22
22
|
StlViewer: () => StlViewer,
|
|
@@ -25,7 +25,7 @@ __export(commonjs_exports, {
|
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(commonjs_exports);
|
|
27
27
|
|
|
28
|
-
// src/
|
|
28
|
+
// src/appearance.js
|
|
29
29
|
var DISPLAYS = [
|
|
30
30
|
"flat",
|
|
31
31
|
"smooth",
|
|
@@ -73,7 +73,7 @@ function sectionPlane(bounds, section) {
|
|
|
73
73
|
return plane;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
// src/
|
|
76
|
+
// src/math.js
|
|
77
77
|
var add = (a, b) => a.map((v, i) => v + b[i]);
|
|
78
78
|
var sub = (a, b) => a.map((v, i) => v - b[i]);
|
|
79
79
|
var mul = (a, s) => a.map((v) => v * s);
|
|
@@ -225,7 +225,7 @@ function rayTriangle(o, d, p, i) {
|
|
|
225
225
|
return t >= 0 ? t : Infinity;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
// src/
|
|
228
|
+
// src/memory.js
|
|
229
229
|
var MEMORY_OVERHEAD = 64 * 1024 * 1024;
|
|
230
230
|
var TRIANGLE_MEMORY = 224;
|
|
231
231
|
function geometryMemory(g) {
|
|
@@ -242,7 +242,7 @@ function derivedMemory(triangles, features) {
|
|
|
242
242
|
return MEMORY_OVERHEAD + triangles * (TRIANGLE_MEMORY + (features.smooth ? 1536 : 0) + (features.edges || features.wire ? 2048 : 0));
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
// src/
|
|
245
|
+
// src/geometry.js
|
|
246
246
|
var DEFAULT_LIMITS = Object.freeze({
|
|
247
247
|
max_file_bytes: 256 * 1024 * 1024,
|
|
248
248
|
max_triangles: 5e6,
|
|
@@ -390,7 +390,7 @@ function exportSTL(g, scale = [1, 1, 1], center = [0, 0, 0]) {
|
|
|
390
390
|
return buffer;
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
-
// src/
|
|
393
|
+
// src/infinite-grid.js
|
|
394
394
|
var vertex = `#version 300 es
|
|
395
395
|
precision highp float;
|
|
396
396
|
out vec2 screen;
|
|
@@ -482,7 +482,7 @@ var InfiniteGrid = class {
|
|
|
482
482
|
}
|
|
483
483
|
};
|
|
484
484
|
|
|
485
|
-
// src/
|
|
485
|
+
// src/selection-frame.js
|
|
486
486
|
function selectionFrame(bounds) {
|
|
487
487
|
const padding = Math.max(
|
|
488
488
|
Math.max(...bounds.max.map((v, i) => v - bounds.min[i])) * 0.015,
|
|
@@ -500,7 +500,7 @@ function selectionFrame(bounds) {
|
|
|
500
500
|
return new Float32Array(lines);
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
-
// src/
|
|
503
|
+
// src/renderer.js
|
|
504
504
|
var vertex2 = `#version 300 es
|
|
505
505
|
precision highp float;
|
|
506
506
|
layout(location=0) in vec3 position;
|
|
@@ -921,7 +921,7 @@ var Renderer = class {
|
|
|
921
921
|
}
|
|
922
922
|
};
|
|
923
923
|
|
|
924
|
-
// src/
|
|
924
|
+
// src/controls.js
|
|
925
925
|
var Controls = class {
|
|
926
926
|
constructor(canvas, camera, invalidate, pick, type = 0) {
|
|
927
927
|
this.enabled = true;
|
|
@@ -1032,7 +1032,7 @@ var Controls = class {
|
|
|
1032
1032
|
}
|
|
1033
1033
|
};
|
|
1034
1034
|
|
|
1035
|
-
// src/
|
|
1035
|
+
// src/worker-pool.js
|
|
1036
1036
|
var WorkerPool = class {
|
|
1037
1037
|
constructor(url, { count = 2, timeout = 12e4, factory } = {}) {
|
|
1038
1038
|
this.url = url;
|
|
@@ -1117,7 +1117,178 @@ var WorkerPool = class {
|
|
|
1117
1117
|
}
|
|
1118
1118
|
};
|
|
1119
1119
|
|
|
1120
|
-
// src/
|
|
1120
|
+
// src/animation.js
|
|
1121
|
+
var animationKeys = [
|
|
1122
|
+
"x",
|
|
1123
|
+
"y",
|
|
1124
|
+
"z",
|
|
1125
|
+
"rotationx",
|
|
1126
|
+
"rotationy",
|
|
1127
|
+
"rotationz",
|
|
1128
|
+
"scalex",
|
|
1129
|
+
"scaley",
|
|
1130
|
+
"scalez"
|
|
1131
|
+
];
|
|
1132
|
+
var isScale = (key) => key.startsWith("scale");
|
|
1133
|
+
var number = (value, name, positive2 = false) => {
|
|
1134
|
+
if (typeof value !== "number" || !Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER || positive2 && value <= 0)
|
|
1135
|
+
throw new Error(`Invalid animation ${name}`);
|
|
1136
|
+
return value;
|
|
1137
|
+
};
|
|
1138
|
+
var object = (value, name) => {
|
|
1139
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1140
|
+
throw new Error(`Invalid animation ${name}`);
|
|
1141
|
+
return value;
|
|
1142
|
+
};
|
|
1143
|
+
function animationPose(model) {
|
|
1144
|
+
return Object.fromEntries(
|
|
1145
|
+
animationKeys.map((key) => [
|
|
1146
|
+
key,
|
|
1147
|
+
number(
|
|
1148
|
+
model[key] ?? (isScale(key) ? model.scale ?? 1 : 0),
|
|
1149
|
+
key,
|
|
1150
|
+
isScale(key)
|
|
1151
|
+
)
|
|
1152
|
+
])
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
function normalizeAnimation(input) {
|
|
1156
|
+
object(input, "definition");
|
|
1157
|
+
const result = {}, claimed = /* @__PURE__ */ new Set();
|
|
1158
|
+
for (const mode of ["delta", "exact"]) {
|
|
1159
|
+
if (input[mode] == null) continue;
|
|
1160
|
+
const source = object(input[mode], mode), values = {};
|
|
1161
|
+
const msec = number(source.msec ?? 300, "duration", true);
|
|
1162
|
+
if (source.loop !== void 0 && typeof source.loop !== "boolean")
|
|
1163
|
+
throw new Error("Animation loop must be a boolean");
|
|
1164
|
+
const loop = mode === "delta" && !!source.loop;
|
|
1165
|
+
for (const key of animationKeys) {
|
|
1166
|
+
const value = source[key] ?? (isScale(key) ? source.scale : void 0);
|
|
1167
|
+
if (value === void 0) continue;
|
|
1168
|
+
if (claimed.has(key))
|
|
1169
|
+
throw new Error(`Animation delta/exact conflict: ${key}`);
|
|
1170
|
+
number(value, key, mode === "exact" && isScale(key));
|
|
1171
|
+
if (loop && isScale(key) && value < 0)
|
|
1172
|
+
throw new Error("Looping scale deltas cannot be negative");
|
|
1173
|
+
values[key] = value;
|
|
1174
|
+
claimed.add(key);
|
|
1175
|
+
}
|
|
1176
|
+
if (!Object.keys(values).length)
|
|
1177
|
+
throw new Error("Animation track needs a transform");
|
|
1178
|
+
result[mode] = { ...values, msec, ...loop ? { loop: true } : {} };
|
|
1179
|
+
}
|
|
1180
|
+
if (!Object.keys(result).length)
|
|
1181
|
+
throw new Error("Animation needs delta or exact");
|
|
1182
|
+
return result;
|
|
1183
|
+
}
|
|
1184
|
+
function normalizePlaybackState(input) {
|
|
1185
|
+
object(input, "playback state");
|
|
1186
|
+
if (input.version !== 1 || typeof input.paused !== "boolean")
|
|
1187
|
+
throw new Error("Invalid animation playback state version or paused flag");
|
|
1188
|
+
const elapsed_ms = number(input.elapsed_ms, "elapsed time");
|
|
1189
|
+
if (elapsed_ms < 0) throw new Error("Invalid animation elapsed time");
|
|
1190
|
+
object(input.origin, "origin");
|
|
1191
|
+
for (const key of animationKeys) number(input.origin[key], key, isScale(key));
|
|
1192
|
+
if (!Array.isArray(input.center) || input.center.length !== 3)
|
|
1193
|
+
throw new Error("Invalid animation pivot");
|
|
1194
|
+
return {
|
|
1195
|
+
version: 1,
|
|
1196
|
+
elapsed_ms,
|
|
1197
|
+
paused: input.paused,
|
|
1198
|
+
origin: animationPose(input.origin),
|
|
1199
|
+
center: input.center.map((v) => number(v, "pivot"))
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
function createPlayback(definition, model, saved) {
|
|
1203
|
+
const animation = normalizeAnimation(definition);
|
|
1204
|
+
const state = saved == null ? null : normalizePlaybackState(saved);
|
|
1205
|
+
const origin = state?.origin ?? animationPose(model);
|
|
1206
|
+
const tracks = Object.entries(animation).map(([mode, a]) => {
|
|
1207
|
+
const values = Object.entries(a).filter(
|
|
1208
|
+
([key]) => animationKeys.includes(key)
|
|
1209
|
+
);
|
|
1210
|
+
if (mode === "delta")
|
|
1211
|
+
for (const [key, value] of values)
|
|
1212
|
+
number(origin[key] + value, `${key} endpoint`, isScale(key));
|
|
1213
|
+
return { mode, values, duration: a.msec, loop: !!a.loop, done: false };
|
|
1214
|
+
});
|
|
1215
|
+
const playback = {
|
|
1216
|
+
animation,
|
|
1217
|
+
origin,
|
|
1218
|
+
tracks,
|
|
1219
|
+
elapsed_ms: state?.elapsed_ms ?? 0,
|
|
1220
|
+
paused: state?.paused ?? false,
|
|
1221
|
+
last_time: null
|
|
1222
|
+
};
|
|
1223
|
+
evaluatePlayback({ ...playback, tracks: tracks.map((t) => ({ ...t })) }, {});
|
|
1224
|
+
return playback;
|
|
1225
|
+
}
|
|
1226
|
+
function evaluatePlayback(playback, model) {
|
|
1227
|
+
const updates = [];
|
|
1228
|
+
for (const track of playback.tracks) {
|
|
1229
|
+
if (track.done) continue;
|
|
1230
|
+
const fraction = playback.elapsed_ms / track.duration;
|
|
1231
|
+
const p = track.loop ? fraction : Math.min(fraction, 1);
|
|
1232
|
+
for (const [key, value] of track.values) {
|
|
1233
|
+
const from = playback.origin[key];
|
|
1234
|
+
const to = track.mode === "delta" ? from + value * p : from * (1 - p) + value * p;
|
|
1235
|
+
updates.push([key, number(to, `${key} result`, isScale(key))]);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
let scaleChanged = false;
|
|
1239
|
+
for (const [key, value] of updates) {
|
|
1240
|
+
scaleChanged ||= isScale(key) && model[key] !== value;
|
|
1241
|
+
model[key] = value;
|
|
1242
|
+
}
|
|
1243
|
+
for (const track of playback.tracks)
|
|
1244
|
+
if (!track.loop && playback.elapsed_ms >= track.duration) track.done = true;
|
|
1245
|
+
if (scaleChanged) model._stats = null;
|
|
1246
|
+
return playback.tracks.some((track) => !track.done);
|
|
1247
|
+
}
|
|
1248
|
+
function advancePlayback(playback, model, time) {
|
|
1249
|
+
if (playback.paused) return true;
|
|
1250
|
+
if (playback.last_time !== null)
|
|
1251
|
+
playback.elapsed_ms = number(
|
|
1252
|
+
playback.elapsed_ms + Math.max(0, time - playback.last_time),
|
|
1253
|
+
"elapsed time"
|
|
1254
|
+
);
|
|
1255
|
+
playback.last_time = time;
|
|
1256
|
+
return evaluatePlayback(playback, model);
|
|
1257
|
+
}
|
|
1258
|
+
function playbackState(playback, suspended = false) {
|
|
1259
|
+
return {
|
|
1260
|
+
status: playback.paused ? "paused" : "running",
|
|
1261
|
+
elapsed_ms: playback.elapsed_ms,
|
|
1262
|
+
suspended,
|
|
1263
|
+
tracks: playback.tracks.filter((t) => !t.done).map((t) => ({
|
|
1264
|
+
mode: t.mode,
|
|
1265
|
+
msec: t.duration,
|
|
1266
|
+
loop: t.loop,
|
|
1267
|
+
progress: t.loop ? playback.elapsed_ms % t.duration / t.duration : Math.min(1, playback.elapsed_ms / t.duration)
|
|
1268
|
+
}))
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
function savePlayback(model) {
|
|
1272
|
+
const p = model._animation;
|
|
1273
|
+
if (!p) return null;
|
|
1274
|
+
const factor = model.unitScale ?? 1;
|
|
1275
|
+
const animation = structuredClone(p.animation), origin = { ...p.origin };
|
|
1276
|
+
for (const key of animationKeys.filter(isScale)) {
|
|
1277
|
+
origin[key] *= factor;
|
|
1278
|
+
for (const track of Object.values(animation))
|
|
1279
|
+
if (track[key] !== void 0) track[key] *= factor;
|
|
1280
|
+
}
|
|
1281
|
+
const animation_state = normalizePlaybackState({
|
|
1282
|
+
version: 1,
|
|
1283
|
+
origin,
|
|
1284
|
+
elapsed_ms: p.elapsed_ms,
|
|
1285
|
+
paused: p.paused,
|
|
1286
|
+
center: [...model.center]
|
|
1287
|
+
});
|
|
1288
|
+
return { animation: normalizeAnimation(animation), animation_state };
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// src/scene-import.js
|
|
1121
1292
|
var sceneFormat = (name) => String(name || "").match(/\.(vsb|vsj)(?:[?#]|$)/i)?.[1].toLowerCase() || "";
|
|
1122
1293
|
function localSceneFile(reference, files) {
|
|
1123
1294
|
let path;
|
|
@@ -1151,7 +1322,7 @@ function sceneURL(reference, base) {
|
|
|
1151
1322
|
return url.href;
|
|
1152
1323
|
}
|
|
1153
1324
|
|
|
1154
|
-
// src/
|
|
1325
|
+
// src/scene-import-controller.js
|
|
1155
1326
|
function importScene(source, options = {}) {
|
|
1156
1327
|
const controller = new AbortController(), signal = controller.signal;
|
|
1157
1328
|
const generation = this._generation, ids = [], added = /* @__PURE__ */ new Map(), usedFiles = /* @__PURE__ */ new Set();
|
|
@@ -1266,7 +1437,7 @@ function importScene(source, options = {}) {
|
|
|
1266
1437
|
progress({ phase: "opening-model", loaded: index, total: models.length });
|
|
1267
1438
|
check();
|
|
1268
1439
|
const input = models[index], id = this.max_model_id + 1;
|
|
1269
|
-
this.add_model({ ...input, id });
|
|
1440
|
+
this.add_model({ ...input, id, animation: null, animation_state: null });
|
|
1270
1441
|
const model = this._models.get(id);
|
|
1271
1442
|
if (!model) throw new Error(this.error || "Invalid scene model");
|
|
1272
1443
|
ids.push(id);
|
|
@@ -1291,7 +1462,9 @@ function importScene(source, options = {}) {
|
|
|
1291
1462
|
}),
|
|
1292
1463
|
delta
|
|
1293
1464
|
);
|
|
1294
|
-
|
|
1465
|
+
if (input.animation)
|
|
1466
|
+
model.center = [...input.animation_state?.center ?? desired];
|
|
1467
|
+
else ["x", "y", "z"].forEach((axis, i) => model[axis] += shift[i]);
|
|
1295
1468
|
if ([...this._models.values()].reduce(
|
|
1296
1469
|
(n, m) => n + (m.geometry?.triangles || 0),
|
|
1297
1470
|
0
|
|
@@ -1304,7 +1477,12 @@ function importScene(source, options = {}) {
|
|
|
1304
1477
|
total: models.length
|
|
1305
1478
|
});
|
|
1306
1479
|
check();
|
|
1307
|
-
for (
|
|
1480
|
+
for (let index = 0; index < ids.length; index++) {
|
|
1481
|
+
const model = added.get(ids[index]), input = models[index];
|
|
1482
|
+
if (input.animation)
|
|
1483
|
+
this._startAnimation(model, input.animation, input.animation_state);
|
|
1484
|
+
model._sceneImportPending = false;
|
|
1485
|
+
}
|
|
1308
1486
|
if (scene.bg_color !== void 0) this.set_bg_color(scene.bg_color);
|
|
1309
1487
|
if (scene.infinite_grid)
|
|
1310
1488
|
this.set_infinite_grid(
|
|
@@ -1336,12 +1514,12 @@ function importScene(source, options = {}) {
|
|
|
1336
1514
|
return promise;
|
|
1337
1515
|
}
|
|
1338
1516
|
|
|
1339
|
-
// src/
|
|
1517
|
+
// src/print-options.js
|
|
1340
1518
|
function printOptions(input = {}) {
|
|
1341
1519
|
const level = input.level ?? "basic";
|
|
1342
1520
|
if (!["basic", "detailed"].includes(level))
|
|
1343
1521
|
throw new Error("Choose basic or detailed print checks");
|
|
1344
|
-
const
|
|
1522
|
+
const number2 = (v, name, max = 1e9) => {
|
|
1345
1523
|
if (!Number.isFinite(v) || v <= 0 || v > max)
|
|
1346
1524
|
throw new Error(`Invalid ${name}`);
|
|
1347
1525
|
return v;
|
|
@@ -1353,9 +1531,9 @@ function printOptions(input = {}) {
|
|
|
1353
1531
|
profile = { technology: "fdm" };
|
|
1354
1532
|
for (const key of ["min_wall_mm", "line_width_mm"])
|
|
1355
1533
|
if (input.profile[key] != null)
|
|
1356
|
-
profile[key] =
|
|
1534
|
+
profile[key] = number2(input.profile[key], key);
|
|
1357
1535
|
if (input.profile.overhang_degrees != null)
|
|
1358
|
-
profile.overhang_degrees =
|
|
1536
|
+
profile.overhang_degrees = number2(
|
|
1359
1537
|
input.profile.overhang_degrees,
|
|
1360
1538
|
"overhang angle",
|
|
1361
1539
|
89.9
|
|
@@ -1364,11 +1542,11 @@ function printOptions(input = {}) {
|
|
|
1364
1542
|
if (!Array.isArray(input.profile.build_volume_mm) || input.profile.build_volume_mm.length !== 3)
|
|
1365
1543
|
throw new Error("Build volume requires X, Y and Z");
|
|
1366
1544
|
profile.build_volume_mm = input.profile.build_volume_mm.map(
|
|
1367
|
-
(v) =>
|
|
1545
|
+
(v) => number2(v, "build volume")
|
|
1368
1546
|
);
|
|
1369
1547
|
}
|
|
1370
1548
|
}
|
|
1371
|
-
const time =
|
|
1549
|
+
const time = number2(input.max_time_ms ?? 3e4, "check time limit", 12e4);
|
|
1372
1550
|
return {
|
|
1373
1551
|
level,
|
|
1374
1552
|
profile,
|
|
@@ -1391,7 +1569,7 @@ var printSignature = (model) => JSON.stringify([
|
|
|
1391
1569
|
model.unitScale
|
|
1392
1570
|
]);
|
|
1393
1571
|
|
|
1394
|
-
// src/
|
|
1572
|
+
// src/print-controller.js
|
|
1395
1573
|
var reports = /* @__PURE__ */ new WeakMap();
|
|
1396
1574
|
function reportCurrent(viewer, report) {
|
|
1397
1575
|
const saved = reports.get(report);
|
|
@@ -1604,7 +1782,7 @@ function showPrintability(report, { issue, model_id = report.model_ids[0] } = {}
|
|
|
1604
1782
|
return lines.length / 6;
|
|
1605
1783
|
}
|
|
1606
1784
|
|
|
1607
|
-
// src/
|
|
1785
|
+
// src/export-options.js
|
|
1608
1786
|
function exportFilename(filename, extension) {
|
|
1609
1787
|
const name = String(filename || "scene").split(/[\\/]/).pop().split(/[?#]/)[0].replace(/[<>:"|*\x00-\x1f\x7f]/g, "_").replace(/\.(?:stl|obj|3mf|step|stp|iges|igs|zip|vsb|vsj)$/i, "").replace(/[. ]+$/g, "").slice(0, 160) || "scene";
|
|
1610
1788
|
return `${name}.${extension}`;
|
|
@@ -1645,7 +1823,7 @@ function exportMemory(models, options) {
|
|
|
1645
1823
|
return MEMORY_OVERHEAD + triangles * perTriangle[options.format] + models.length * 4096;
|
|
1646
1824
|
}
|
|
1647
1825
|
|
|
1648
|
-
// src/
|
|
1826
|
+
// src/io.js
|
|
1649
1827
|
async function readSource(source, limit, progress = () => {
|
|
1650
1828
|
}, signal) {
|
|
1651
1829
|
let stream, total = 0;
|
|
@@ -1707,7 +1885,7 @@ async function readSource(source, limit, progress = () => {
|
|
|
1707
1885
|
return data.buffer;
|
|
1708
1886
|
}
|
|
1709
1887
|
|
|
1710
|
-
// src/
|
|
1888
|
+
// src/three-bridge.js
|
|
1711
1889
|
function fromThreeMesh(mesh, limits) {
|
|
1712
1890
|
if (!mesh?.isMesh && !mesh?.geometry)
|
|
1713
1891
|
throw new Error("Expected a Three.js mesh");
|
|
@@ -1780,7 +1958,7 @@ function toThreeMesh(model, THREE) {
|
|
|
1780
1958
|
return mesh;
|
|
1781
1959
|
}
|
|
1782
1960
|
|
|
1783
|
-
// src/
|
|
1961
|
+
// src/viewer.js
|
|
1784
1962
|
var transformKeys = [
|
|
1785
1963
|
"x",
|
|
1786
1964
|
"y",
|
|
@@ -1818,7 +1996,8 @@ var modelKeys = [
|
|
|
1818
1996
|
"smooth_shading",
|
|
1819
1997
|
"section",
|
|
1820
1998
|
"units",
|
|
1821
|
-
"animation"
|
|
1999
|
+
"animation",
|
|
2000
|
+
"animation_state"
|
|
1822
2001
|
];
|
|
1823
2002
|
var finite = (v, name) => {
|
|
1824
2003
|
if (typeof v !== "number" || !Number.isFinite(v))
|
|
@@ -1850,7 +2029,7 @@ function modelOptions(input) {
|
|
|
1850
2029
|
return out;
|
|
1851
2030
|
}
|
|
1852
2031
|
var StlViewer = class _StlViewer {
|
|
1853
|
-
static version = "2.0.0-rc.
|
|
2032
|
+
static version = "2.0.0-rc.5";
|
|
1854
2033
|
static workerURL = null;
|
|
1855
2034
|
constructor(parent, options = {}) {
|
|
1856
2035
|
if (!parent?.appendChild)
|
|
@@ -1900,7 +2079,9 @@ var StlViewer = class _StlViewer {
|
|
|
1900
2079
|
"model_loaded_callback",
|
|
1901
2080
|
"load_error_callback",
|
|
1902
2081
|
"loading_progress_callback",
|
|
1903
|
-
"on_model_drop"
|
|
2082
|
+
"on_model_drop",
|
|
2083
|
+
"animation_state_callback",
|
|
2084
|
+
"animation_complete_callback"
|
|
1904
2085
|
])
|
|
1905
2086
|
this[name] = options[name] ?? null;
|
|
1906
2087
|
this.onmousedown_callback = options.on_model_mousedown ?? options.on_model_mouseclick ?? null;
|
|
@@ -1939,11 +2120,15 @@ var StlViewer = class _StlViewer {
|
|
|
1939
2120
|
this.parent_element.appendChild(this.canvas);
|
|
1940
2121
|
this.renderer = new Renderer(
|
|
1941
2122
|
this.canvas,
|
|
1942
|
-
() =>
|
|
1943
|
-
|
|
1944
|
-
|
|
2123
|
+
() => {
|
|
2124
|
+
this._suspendAnimations(true);
|
|
2125
|
+
this._message(
|
|
2126
|
+
"Graphics interrupted. Waiting for the browser to restore rendering\u2026"
|
|
2127
|
+
);
|
|
2128
|
+
},
|
|
1945
2129
|
() => {
|
|
1946
2130
|
this._removeMessage();
|
|
2131
|
+
this._suspendAnimations(this.canvas.ownerDocument.hidden);
|
|
1947
2132
|
this._invalidate();
|
|
1948
2133
|
},
|
|
1949
2134
|
(e) => this._error(e)
|
|
@@ -2008,6 +2193,14 @@ var StlViewer = class _StlViewer {
|
|
|
2008
2193
|
}
|
|
2009
2194
|
};
|
|
2010
2195
|
this.canvas.addEventListener("keydown", this._key);
|
|
2196
|
+
this._visibility = () => this._suspendAnimations(
|
|
2197
|
+
this.canvas.ownerDocument.hidden || this.renderer.lost
|
|
2198
|
+
);
|
|
2199
|
+
this.canvas.ownerDocument.addEventListener(
|
|
2200
|
+
"visibilitychange",
|
|
2201
|
+
this._visibility
|
|
2202
|
+
);
|
|
2203
|
+
this._animationSuspended = this.canvas.ownerDocument.hidden;
|
|
2011
2204
|
this.ready = true;
|
|
2012
2205
|
this._readyResolve(this);
|
|
2013
2206
|
this._callback("ready_callback");
|
|
@@ -2022,6 +2215,11 @@ var StlViewer = class _StlViewer {
|
|
|
2022
2215
|
this.controls?.dispose();
|
|
2023
2216
|
this.renderer?.dispose();
|
|
2024
2217
|
this._resizeObserver?.disconnect();
|
|
2218
|
+
if (this._visibility)
|
|
2219
|
+
this.canvas.ownerDocument.removeEventListener(
|
|
2220
|
+
"visibilitychange",
|
|
2221
|
+
this._visibility
|
|
2222
|
+
);
|
|
2025
2223
|
}
|
|
2026
2224
|
}
|
|
2027
2225
|
_callback(name, ...args) {
|
|
@@ -2070,7 +2268,7 @@ var StlViewer = class _StlViewer {
|
|
|
2070
2268
|
const diagnostic = this.renderer?.diagnostic;
|
|
2071
2269
|
if (diagnostic && printSignature(diagnostic.model) !== diagnostic.signature)
|
|
2072
2270
|
this.renderer.setDiagnostic(null);
|
|
2073
|
-
if (this.killsign || !this.renderer || this.renderer.lost || this._frame)
|
|
2271
|
+
if (this.killsign || !this.renderer || this.renderer.lost || this._frame || this._animationSuspended)
|
|
2074
2272
|
return;
|
|
2075
2273
|
this._frame = requestAnimationFrame((t) => {
|
|
2076
2274
|
this._frame = 0;
|
|
@@ -2082,12 +2280,15 @@ var StlViewer = class _StlViewer {
|
|
|
2082
2280
|
this._lastTime = time;
|
|
2083
2281
|
let animated = false;
|
|
2084
2282
|
for (const m of this._models.values())
|
|
2085
|
-
if (m.geometry && m._animation) {
|
|
2283
|
+
if (m.geometry && !m._sceneImportPending && m._animation && !m._animation.paused) {
|
|
2086
2284
|
this._animate(m, time);
|
|
2087
|
-
animated ||= !!m._animation;
|
|
2285
|
+
animated ||= !!m._animation && !m._animation.paused;
|
|
2088
2286
|
}
|
|
2089
2287
|
if (this.auto_rotate && this.controls.enabled)
|
|
2090
2288
|
this.controls.orbit(elapsed * 0.3, 0);
|
|
2289
|
+
const diagnostic = this.renderer.diagnostic;
|
|
2290
|
+
if (diagnostic && printSignature(diagnostic.model) !== diagnostic.signature)
|
|
2291
|
+
this.renderer.setDiagnostic(null);
|
|
2091
2292
|
this._clip();
|
|
2092
2293
|
try {
|
|
2093
2294
|
this.renderer.draw(
|
|
@@ -2211,6 +2412,11 @@ var StlViewer = class _StlViewer {
|
|
|
2211
2412
|
m.color = supplied.color ?? (supplied.mesh?.material?.color?.getHexString ? "#" + supplied.mesh.material.color.getHexString() : null);
|
|
2212
2413
|
if (m.color && m.color !== "transparent") colorRGB(m.color);
|
|
2213
2414
|
m.view_edges = !!supplied.view_edges;
|
|
2415
|
+
m.animation = supplied.animation == null ? null : normalizeAnimation(supplied.animation);
|
|
2416
|
+
m.animation_state = supplied.animation_state == null ? null : normalizePlaybackState(supplied.animation_state);
|
|
2417
|
+
if (m.animation_state && !m.animation)
|
|
2418
|
+
throw new Error("Playback state needs an animation");
|
|
2419
|
+
if (m.animation) createPlayback(m.animation, m, m.animation_state);
|
|
2214
2420
|
Object.defineProperty(m, "scaleVector", {
|
|
2215
2421
|
get: () => [
|
|
2216
2422
|
m.scalex * m.unitScale,
|
|
@@ -2363,7 +2569,10 @@ var StlViewer = class _StlViewer {
|
|
|
2363
2569
|
);
|
|
2364
2570
|
if ((supplied.mesh || supplied.data) && (features.smooth || features.edges || features.wire))
|
|
2365
2571
|
await this._derive(m, features);
|
|
2366
|
-
if (
|
|
2572
|
+
if (m.animation_state) m.center = [...m.animation_state.center];
|
|
2573
|
+
if (m.animation && !m._animation)
|
|
2574
|
+
this._startAnimation(m, m.animation, m.animation_state);
|
|
2575
|
+
delete m.animation_state;
|
|
2367
2576
|
delete m.mesh;
|
|
2368
2577
|
delete m.data;
|
|
2369
2578
|
this._invalidate();
|
|
@@ -2385,6 +2594,7 @@ var StlViewer = class _StlViewer {
|
|
|
2385
2594
|
this._invalidate();
|
|
2386
2595
|
}
|
|
2387
2596
|
_deleteModel(m) {
|
|
2597
|
+
this._stopAnimation(m, "removed");
|
|
2388
2598
|
for (const [controller, models] of this._printJobs || [])
|
|
2389
2599
|
if (models.has(m)) controller.abort();
|
|
2390
2600
|
this._printCache = null;
|
|
@@ -2415,6 +2625,7 @@ var StlViewer = class _StlViewer {
|
|
|
2415
2625
|
this._pool?.clear();
|
|
2416
2626
|
for (const c of this._aborters) c.abort();
|
|
2417
2627
|
this._aborters.clear();
|
|
2628
|
+
for (const m of this._models.values()) this._stopAnimation(m, "clean");
|
|
2418
2629
|
this._models.clear();
|
|
2419
2630
|
this._pending.clear();
|
|
2420
2631
|
this._jobs.clear();
|
|
@@ -2446,6 +2657,11 @@ var StlViewer = class _StlViewer {
|
|
|
2446
2657
|
window.removeEventListener("resize", this._windowResize);
|
|
2447
2658
|
this.renderer?.dispose();
|
|
2448
2659
|
if (this.canvas) {
|
|
2660
|
+
if (this._visibility)
|
|
2661
|
+
this.canvas.ownerDocument.removeEventListener(
|
|
2662
|
+
"visibilitychange",
|
|
2663
|
+
this._visibility
|
|
2664
|
+
);
|
|
2449
2665
|
this.canvas.removeEventListener("drop", this._drop);
|
|
2450
2666
|
this.canvas.removeEventListener("dragover", this._drag);
|
|
2451
2667
|
this.canvas.removeEventListener("keydown", this._key);
|
|
@@ -2584,6 +2800,10 @@ var StlViewer = class _StlViewer {
|
|
|
2584
2800
|
set_position(id, x, y, z) {
|
|
2585
2801
|
return this._guard(() => {
|
|
2586
2802
|
const m = this._model(id);
|
|
2803
|
+
[x, y, z].forEach((v) => {
|
|
2804
|
+
if (v !== void 0) finite(v, "position");
|
|
2805
|
+
});
|
|
2806
|
+
this._stopAnimation(m, "manual");
|
|
2587
2807
|
[x, y, z].forEach((v, i) => {
|
|
2588
2808
|
if (v !== void 0) m[["x", "y", "z"][i]] = finite(v, "position");
|
|
2589
2809
|
});
|
|
@@ -2597,8 +2817,12 @@ var StlViewer = class _StlViewer {
|
|
|
2597
2817
|
if (keepPrevious) {
|
|
2598
2818
|
if (x / m.scalex !== y / m.scaley || x / m.scalex !== z / m.scalez)
|
|
2599
2819
|
throw new Error("keep_prev_scale requires a uniform geometry factor");
|
|
2820
|
+
this._stopAnimation(m, "manual");
|
|
2600
2821
|
m.unitScale *= x / m.scalex;
|
|
2601
|
-
} else
|
|
2822
|
+
} else {
|
|
2823
|
+
this._stopAnimation(m, "manual");
|
|
2824
|
+
[m.scalex, m.scaley, m.scalez] = [x, y, z];
|
|
2825
|
+
}
|
|
2602
2826
|
m._stats = null;
|
|
2603
2827
|
this._invalidate();
|
|
2604
2828
|
});
|
|
@@ -2612,6 +2836,10 @@ var StlViewer = class _StlViewer {
|
|
|
2612
2836
|
set_rotation(id, x, y, z, increment = false) {
|
|
2613
2837
|
return this._guard(() => {
|
|
2614
2838
|
const m = this._model(id);
|
|
2839
|
+
[x, y, z].forEach((v) => {
|
|
2840
|
+
if (v !== void 0) finite(v, "rotation");
|
|
2841
|
+
});
|
|
2842
|
+
this._stopAnimation(m, "manual");
|
|
2615
2843
|
[x, y, z].forEach((v, i) => {
|
|
2616
2844
|
const k = ["rotationx", "rotationy", "rotationz"][i];
|
|
2617
2845
|
if (v !== void 0)
|
|
@@ -2665,6 +2893,7 @@ var StlViewer = class _StlViewer {
|
|
|
2665
2893
|
if (!["mm", "inch"].includes(units))
|
|
2666
2894
|
throw new Error("Units must be mm or inch");
|
|
2667
2895
|
const m = this._model(id);
|
|
2896
|
+
this._stopAnimation(m, "manual");
|
|
2668
2897
|
if (scale && m.units !== units)
|
|
2669
2898
|
m.unitScale *= units === "inch" ? 25.4 : 1 / 25.4;
|
|
2670
2899
|
m.units = units;
|
|
@@ -3054,6 +3283,14 @@ var StlViewer = class _StlViewer {
|
|
|
3054
3283
|
this._exportBytes += working;
|
|
3055
3284
|
let job, cancel, callbackError;
|
|
3056
3285
|
try {
|
|
3286
|
+
const snapshot = format === "vsb" ? models.map((m, index) => ({
|
|
3287
|
+
id: m.id,
|
|
3288
|
+
triangles: m.geometry.triangles,
|
|
3289
|
+
positions: m.geometry.positions.slice(),
|
|
3290
|
+
colors: m.geometry.colors?.slice() || null,
|
|
3291
|
+
scale: scene.models[index].animation_state ? [1, 1, 1] : m.scaleVector,
|
|
3292
|
+
center: scene.models[index].animation_state ? [0, 0, 0] : [...m.center]
|
|
3293
|
+
})) : null;
|
|
3057
3294
|
options.on_progress?.({
|
|
3058
3295
|
loaded: 0,
|
|
3059
3296
|
total: models.length,
|
|
@@ -3069,14 +3306,6 @@ var StlViewer = class _StlViewer {
|
|
|
3069
3306
|
phase: "complete"
|
|
3070
3307
|
});
|
|
3071
3308
|
} else {
|
|
3072
|
-
const snapshot = models.map((m) => ({
|
|
3073
|
-
id: m.id,
|
|
3074
|
-
triangles: m.geometry.triangles,
|
|
3075
|
-
positions: m.geometry.positions.slice(),
|
|
3076
|
-
colors: m.geometry.colors?.slice() || null,
|
|
3077
|
-
scale: m.scaleVector,
|
|
3078
|
-
center: [...m.center]
|
|
3079
|
-
}));
|
|
3080
3309
|
job = this._pool.run(
|
|
3081
3310
|
{
|
|
3082
3311
|
kind: "export-scene",
|
|
@@ -3162,13 +3391,21 @@ var StlViewer = class _StlViewer {
|
|
|
3162
3391
|
model.orig_filename = basename(m.orig_filename || m.filename);
|
|
3163
3392
|
for (const key of transformKeys)
|
|
3164
3393
|
if (!key.startsWith("scale")) model[key] = m[key];
|
|
3165
|
-
if (!for_vsb) {
|
|
3394
|
+
if (!for_vsb || m._animation) {
|
|
3166
3395
|
model.scalex = m.scalex * m.unitScale;
|
|
3167
3396
|
model.scaley = m.scaley * m.unitScale;
|
|
3168
3397
|
model.scalez = m.scalez * m.unitScale;
|
|
3169
3398
|
}
|
|
3170
3399
|
if (m.color) model.color = m.color;
|
|
3171
|
-
|
|
3400
|
+
const playback = savePlayback(m);
|
|
3401
|
+
if (playback) Object.assign(model, playback);
|
|
3402
|
+
else if (!for_vsb) {
|
|
3403
|
+
const center = this.center_models ? m.geometry.bounds.min.map(
|
|
3404
|
+
(v, i) => (v + m.geometry.bounds.max[i]) / 2
|
|
3405
|
+
) : [0, 0, 0];
|
|
3406
|
+
const position = transform(modelMatrix(m), center);
|
|
3407
|
+
["x", "y", "z"].forEach((axis, i) => model[axis] = position[i]);
|
|
3408
|
+
}
|
|
3172
3409
|
data.models.push(model);
|
|
3173
3410
|
}
|
|
3174
3411
|
return as_js_obj ? data : JSON.stringify(data);
|
|
@@ -3182,7 +3419,10 @@ var StlViewer = class _StlViewer {
|
|
|
3182
3419
|
get_vsb() {
|
|
3183
3420
|
return {
|
|
3184
3421
|
vsj: this.get_vsj(true, true, true),
|
|
3185
|
-
files: [...this._models.values()].filter((m) => m.geometry).map((m) => ({
|
|
3422
|
+
files: [...this._models.values()].filter((m) => m.geometry).map((m) => ({
|
|
3423
|
+
id: m.id,
|
|
3424
|
+
bin: m._animation ? exportSTL(m.geometry, [1, 1, 1], [0, 0, 0]) : this.get_stl_bin(m.id)
|
|
3425
|
+
}))
|
|
3186
3426
|
};
|
|
3187
3427
|
}
|
|
3188
3428
|
async get_vsb_blob() {
|
|
@@ -3263,20 +3503,57 @@ var StlViewer = class _StlViewer {
|
|
|
3263
3503
|
);
|
|
3264
3504
|
else if (scene.grid) this.set_grid(true);
|
|
3265
3505
|
if (scene.camera_state) this._restoreCamera = scene.camera_state;
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3506
|
+
const incoming = [];
|
|
3507
|
+
try {
|
|
3508
|
+
for (const input of scene.models) {
|
|
3509
|
+
if (!input || typeof input !== "object")
|
|
3510
|
+
throw new Error("Invalid scene model");
|
|
3511
|
+
const model = modelOptions(input);
|
|
3512
|
+
delete model.mesh;
|
|
3513
|
+
delete model.data;
|
|
3514
|
+
delete model.local_file;
|
|
3515
|
+
if (files) {
|
|
3516
|
+
if (typeof model.filename !== "string" || !Object.hasOwn(files, model.filename))
|
|
3517
|
+
throw new Error("VSB model file is missing");
|
|
3518
|
+
model.local_file = new File(
|
|
3519
|
+
[files[model.filename]],
|
|
3520
|
+
model.filename
|
|
3521
|
+
);
|
|
3522
|
+
} else if (model.filename)
|
|
3523
|
+
model.filename = new URL(model.filename, base).href;
|
|
3524
|
+
const id = model.id === void 0 || model.id === -1 ? this.max_model_id + 1 : model.id;
|
|
3525
|
+
if (this._models.has(id))
|
|
3526
|
+
throw new Error(`Invalid or duplicate model id: ${id}`);
|
|
3527
|
+
this.add_model({
|
|
3528
|
+
...model,
|
|
3529
|
+
id,
|
|
3530
|
+
animation: null,
|
|
3531
|
+
animation_state: null
|
|
3532
|
+
});
|
|
3533
|
+
const m = this._models.get(id);
|
|
3534
|
+
if (!m) throw new Error(this.error || "Invalid scene model");
|
|
3535
|
+
m._sceneImportPending = true;
|
|
3536
|
+
incoming.push({ m, model, pending: this._pending.get(id) });
|
|
3537
|
+
}
|
|
3538
|
+
await Promise.all(incoming.map((item) => item.pending));
|
|
3539
|
+
if (generation !== this._generation || this.killsign) return;
|
|
3540
|
+
for (const { m, model } of incoming) {
|
|
3541
|
+
if (this._models.get(m.id) !== m || !m.geometry)
|
|
3542
|
+
throw new Error("A scene model could not be opened");
|
|
3543
|
+
if (model.animation)
|
|
3544
|
+
createPlayback(model.animation, m, model.animation_state);
|
|
3545
|
+
else if (model.animation_state)
|
|
3546
|
+
throw new Error("Playback state needs an animation");
|
|
3547
|
+
}
|
|
3548
|
+
for (const { m, model } of incoming) {
|
|
3549
|
+
if (model.animation)
|
|
3550
|
+
this._startAnimation(m, model.animation, model.animation_state);
|
|
3551
|
+
m._sceneImportPending = false;
|
|
3552
|
+
}
|
|
3553
|
+
} catch (error) {
|
|
3554
|
+
for (const { m } of incoming)
|
|
3555
|
+
if (this._models.get(m.id) === m) this.remove_model(m.id);
|
|
3556
|
+
throw error;
|
|
3280
3557
|
}
|
|
3281
3558
|
})().catch((e) => {
|
|
3282
3559
|
if (generation === this._generation && e.name !== "AbortError")
|
|
@@ -3298,56 +3575,101 @@ var StlViewer = class _StlViewer {
|
|
|
3298
3575
|
return this._guard(() => {
|
|
3299
3576
|
const m = this._model(id);
|
|
3300
3577
|
if (!animation) {
|
|
3301
|
-
m
|
|
3302
|
-
|
|
3578
|
+
this._stopAnimation(m, "stopped");
|
|
3579
|
+
this._invalidate();
|
|
3303
3580
|
return;
|
|
3304
3581
|
}
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3582
|
+
this._startAnimation(m, animation);
|
|
3583
|
+
this._invalidate();
|
|
3584
|
+
});
|
|
3585
|
+
}
|
|
3586
|
+
_startAnimation(m, definition, saved) {
|
|
3587
|
+
const playback = createPlayback(definition, m, saved);
|
|
3588
|
+
if (m._animation) this._stopAnimation(m, "replaced");
|
|
3589
|
+
if (saved) m.center = [...saved.center];
|
|
3590
|
+
m.animation = playback.animation;
|
|
3591
|
+
m._animation = playback;
|
|
3592
|
+
delete m.animation_state;
|
|
3593
|
+
m._animationResult = null;
|
|
3594
|
+
const running = evaluatePlayback(playback, m);
|
|
3595
|
+
if (running) this._animationEvent(m, saved ? "restored" : "started");
|
|
3596
|
+
else this._completeAnimation(m);
|
|
3597
|
+
}
|
|
3598
|
+
_animationEvent(m, reason) {
|
|
3599
|
+
const state = this._animationState(m);
|
|
3600
|
+
queueMicrotask(() => {
|
|
3601
|
+
this._callback("animation_state_callback", m.id, state, reason);
|
|
3602
|
+
if (reason === "completed")
|
|
3603
|
+
this._callback("animation_complete_callback", m.id, state);
|
|
3604
|
+
});
|
|
3605
|
+
}
|
|
3606
|
+
_animationState(m) {
|
|
3607
|
+
return m._animation ? playbackState(m._animation, !!this._animationSuspended) : {
|
|
3608
|
+
...m._animationResult ?? { status: "idle", elapsed_ms: 0 },
|
|
3609
|
+
suspended: false,
|
|
3610
|
+
tracks: []
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
_stopAnimation(m, reason) {
|
|
3614
|
+
if (!m._animation && !m.animation) return;
|
|
3615
|
+
m._animationResult = {
|
|
3616
|
+
status: "stopped",
|
|
3617
|
+
elapsed_ms: m._animation?.elapsed_ms ?? 0
|
|
3618
|
+
};
|
|
3619
|
+
m._animation = null;
|
|
3620
|
+
m.animation = null;
|
|
3621
|
+
delete m.animation_state;
|
|
3622
|
+
this._animationEvent(m, reason);
|
|
3623
|
+
}
|
|
3624
|
+
_completeAnimation(m) {
|
|
3625
|
+
m._animationResult = {
|
|
3626
|
+
status: "completed",
|
|
3627
|
+
elapsed_ms: m._animation.elapsed_ms
|
|
3628
|
+
};
|
|
3629
|
+
m._animation = null;
|
|
3630
|
+
m.animation = null;
|
|
3631
|
+
this._animationEvent(m, "completed");
|
|
3632
|
+
}
|
|
3633
|
+
pause_model_animation(id) {
|
|
3634
|
+
return this._guard(() => {
|
|
3635
|
+
const m = this._model(id);
|
|
3636
|
+
if (!m._animation || m._animation.paused) return;
|
|
3637
|
+
m._animation.paused = true;
|
|
3638
|
+
m._animation.last_time = null;
|
|
3639
|
+
this._animationEvent(m, "paused");
|
|
3640
|
+
this._invalidate();
|
|
3641
|
+
});
|
|
3642
|
+
}
|
|
3643
|
+
resume_model_animation(id) {
|
|
3644
|
+
return this._guard(() => {
|
|
3645
|
+
const m = this._model(id);
|
|
3646
|
+
if (!m._animation || !m._animation.paused) return;
|
|
3647
|
+
m._animation.paused = false;
|
|
3648
|
+
m._animation.last_time = null;
|
|
3649
|
+
this._animationEvent(m, "resumed");
|
|
3331
3650
|
this._invalidate();
|
|
3332
3651
|
});
|
|
3333
3652
|
}
|
|
3653
|
+
get_model_animation_state(id) {
|
|
3654
|
+
return this._guard(() => this._animationState(this._model(id)));
|
|
3655
|
+
}
|
|
3656
|
+
_suspendAnimations(suspended) {
|
|
3657
|
+
this._animationSuspended = !!suspended;
|
|
3658
|
+
this._lastTime = 0;
|
|
3659
|
+
for (const m of this._models.values())
|
|
3660
|
+
if (m._animation) m._animation.last_time = null;
|
|
3661
|
+
if (suspended) {
|
|
3662
|
+
cancelAnimationFrame(this._frame);
|
|
3663
|
+
this._frame = 0;
|
|
3664
|
+
} else this._invalidate();
|
|
3665
|
+
}
|
|
3334
3666
|
_animate(m, time) {
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
for (const [k, v] of Object.entries(t.values))
|
|
3342
|
-
m[k] = k.startsWith("scale") ? Math.max(
|
|
3343
|
-
1e-6,
|
|
3344
|
-
t.mode === "delta" ? t.from[k] + v * p : t.from[k] + (v - t.from[k]) * p
|
|
3345
|
-
) : t.mode === "delta" ? t.from[k] + v * p : t.from[k] + (v - t.from[k]) * p;
|
|
3346
|
-
}
|
|
3347
|
-
m._stats = null;
|
|
3348
|
-
if (!running) {
|
|
3349
|
-
m._animation = null;
|
|
3350
|
-
m.animation = null;
|
|
3667
|
+
if (this._animationSuspended || !m._animation) return;
|
|
3668
|
+
try {
|
|
3669
|
+
if (!advancePlayback(m._animation, m, time)) this._completeAnimation(m);
|
|
3670
|
+
} catch (error) {
|
|
3671
|
+
this._stopAnimation(m, "error");
|
|
3672
|
+
this._error(error);
|
|
3351
3673
|
}
|
|
3352
3674
|
}
|
|
3353
3675
|
async get_image_blob(type = "image/png") {
|
|
@@ -3371,7 +3693,7 @@ var StlViewer = class _StlViewer {
|
|
|
3371
3693
|
};
|
|
3372
3694
|
var viewer_default = StlViewer;
|
|
3373
3695
|
|
|
3374
|
-
// src/
|
|
3696
|
+
// src/memory-policy.js
|
|
3375
3697
|
var GiB = 1024 ** 3;
|
|
3376
3698
|
var MiB = 1024 ** 2;
|
|
3377
3699
|
function memoryPolicy(reportedGiB) {
|
|
@@ -3401,10 +3723,10 @@ async function estimateFile(file) {
|
|
|
3401
3723
|
return 64 * MiB + (file.size || 0) * 3;
|
|
3402
3724
|
}
|
|
3403
3725
|
|
|
3404
|
-
// src/
|
|
3726
|
+
// src/model-transform.js
|
|
3405
3727
|
var axes = ["x", "y", "z"];
|
|
3406
3728
|
var vector2 = (p) => axes.map((a) => p[a]);
|
|
3407
|
-
var
|
|
3729
|
+
var object2 = (p) => Object.fromEntries(axes.map((a, i) => [a, p[i]]));
|
|
3408
3730
|
var dot2 = (a, b) => a.reduce((sum, v, i) => sum + v * b[i], 0);
|
|
3409
3731
|
var norm2 = (a) => {
|
|
3410
3732
|
const n = Math.hypot(...a) || 1;
|
|
@@ -3435,7 +3757,7 @@ function rotationMatrix(degrees = { x: 0, y: 0, z: 0 }) {
|
|
|
3435
3757
|
}
|
|
3436
3758
|
var centerOf = (bounds) => bounds.min.map((v, i) => (v + bounds.max[i]) / 2);
|
|
3437
3759
|
function modelCenter(model) {
|
|
3438
|
-
return
|
|
3760
|
+
return object2(
|
|
3439
3761
|
centerOf(model.bounds).map((v, i) => v + model.position[axes[i]])
|
|
3440
3762
|
);
|
|
3441
3763
|
}
|
|
@@ -3447,7 +3769,7 @@ function modelRadius(model) {
|
|
|
3447
3769
|
}
|
|
3448
3770
|
function enginePosition(model) {
|
|
3449
3771
|
const c = centerOf(model.bounds), rotated = transform2(rotationMatrix(model.rotation), c);
|
|
3450
|
-
return
|
|
3772
|
+
return object2(c.map((v, i) => model.position[axes[i]] + v - rotated[i]));
|
|
3451
3773
|
}
|
|
3452
3774
|
function modelBounds(model) {
|
|
3453
3775
|
if (!model.rotation || !Object.values(model.rotation).some((v) => v % 360))
|
|
@@ -3500,12 +3822,12 @@ function depthPosition(model, camera, amount) {
|
|
|
3500
3822
|
const clearance = Math.max(model.radius || 1e-6, 1e-6) * 1.1 / Math.max(0.05, dot2(direction, forward));
|
|
3501
3823
|
const desired = (distance || clearance) * Math.exp(Math.max(-6, Math.min(6, amount)));
|
|
3502
3824
|
const next = amount < 0 ? Math.max(Math.min(distance, clearance), desired) : Math.max(distance, Math.min(1e12, desired));
|
|
3503
|
-
return
|
|
3825
|
+
return object2(
|
|
3504
3826
|
vector2(model.position).map((v, i) => v + direction[i] * (next - distance))
|
|
3505
3827
|
);
|
|
3506
3828
|
}
|
|
3507
3829
|
|
|
3508
|
-
// src/
|
|
3830
|
+
// src/scene-layout.js
|
|
3509
3831
|
function scenePlacement(models, mode) {
|
|
3510
3832
|
if (mode === "custom")
|
|
3511
3833
|
return models.map((m) => ({
|
|
@@ -3543,7 +3865,7 @@ function sceneFloor(models, placement) {
|
|
|
3543
3865
|
) : 0;
|
|
3544
3866
|
}
|
|
3545
3867
|
|
|
3546
|
-
// src/
|
|
3868
|
+
// src/model-move.js
|
|
3547
3869
|
var vector3 = (p) => [p.x, p.y, p.z];
|
|
3548
3870
|
var sub2 = (a, b) => a.map((v, i) => v - b[i]);
|
|
3549
3871
|
var dot3 = (a, b) => a.reduce((n, v, i) => n + v * b[i], 0);
|
|
@@ -3709,7 +4031,7 @@ function bindModelMove(canvas, {
|
|
|
3709
4031
|
};
|
|
3710
4032
|
}
|
|
3711
4033
|
|
|
3712
|
-
// src/
|
|
4034
|
+
// src/drop-source.js
|
|
3713
4035
|
function dropKind(transfer) {
|
|
3714
4036
|
const types = Array.from(transfer?.types || [], (type) => type.toLowerCase());
|
|
3715
4037
|
if (types.includes("files") || transfer?.files?.length) return "files";
|
|
@@ -3749,7 +4071,7 @@ function droppedURLs(transfer) {
|
|
|
3749
4071
|
return [...urls];
|
|
3750
4072
|
}
|
|
3751
4073
|
|
|
3752
|
-
// src/
|
|
4074
|
+
// src/app-export.js
|
|
3753
4075
|
var exportControls = [
|
|
3754
4076
|
"save-button",
|
|
3755
4077
|
"save-dialog",
|
|
@@ -3919,7 +4241,7 @@ function bindAppExport({
|
|
|
3919
4241
|
};
|
|
3920
4242
|
}
|
|
3921
4243
|
|
|
3922
|
-
// src/
|
|
4244
|
+
// src/app-print.js
|
|
3923
4245
|
var printControls = [
|
|
3924
4246
|
"print-button",
|
|
3925
4247
|
"print-panel",
|
|
@@ -3953,6 +4275,7 @@ function bindAppPrint({
|
|
|
3953
4275
|
}) {
|
|
3954
4276
|
let job = null, report = null, timer = null, disposed = false, scopeKey = "", previousScope = "", unavailable = false;
|
|
3955
4277
|
const panel = $("print-panel"), document2 = panel.ownerDocument;
|
|
4278
|
+
const pausedForCheck = /* @__PURE__ */ new Set();
|
|
3956
4279
|
const summary = {
|
|
3957
4280
|
issues_found: "Issues found",
|
|
3958
4281
|
incomplete: "Check incomplete",
|
|
@@ -3974,6 +4297,15 @@ function bindAppPrint({
|
|
|
3974
4297
|
const s = getState();
|
|
3975
4298
|
$("print-button").disabled = s.busy || !s.models.length;
|
|
3976
4299
|
if (panel.hidden) return;
|
|
4300
|
+
for (const id of pausedForCheck)
|
|
4301
|
+
if (!s.models.some((m) => m.id === id) || getViewer()?.get_model_animation_state(id)?.status !== "paused")
|
|
4302
|
+
pausedForCheck.delete(id);
|
|
4303
|
+
if ($("print-resume")) {
|
|
4304
|
+
$("print-resume").hidden = !pausedForCheck.size;
|
|
4305
|
+
$("print-resume").disabled = !!job || s.busy;
|
|
4306
|
+
}
|
|
4307
|
+
if ($("print-animation-note"))
|
|
4308
|
+
$("print-animation-note").hidden = !pausedForCheck.size;
|
|
3977
4309
|
const wasUnavailable = unavailable;
|
|
3978
4310
|
unavailable = s.busy || !s.models.length;
|
|
3979
4311
|
$("print-run").disabled = !!job || unavailable;
|
|
@@ -4120,6 +4452,19 @@ function bindAppPrint({
|
|
|
4120
4452
|
listen($("print-fields"), "change", changed);
|
|
4121
4453
|
listen($("print-clear"), "click", clear);
|
|
4122
4454
|
listen($("print-cancel"), "click", cancel);
|
|
4455
|
+
if ($("print-resume"))
|
|
4456
|
+
listen($("print-resume"), "click", () => {
|
|
4457
|
+
if (job || getState().busy) return;
|
|
4458
|
+
for (const id of pausedForCheck)
|
|
4459
|
+
if (getState().models.some((m) => m.id === id) && getViewer().get_model_animation_state(id)?.status === "paused")
|
|
4460
|
+
getViewer().resume_model_animation(id);
|
|
4461
|
+
pausedForCheck.clear();
|
|
4462
|
+
report = null;
|
|
4463
|
+
clear();
|
|
4464
|
+
$("print-results").replaceChildren();
|
|
4465
|
+
$("print-status").textContent = "Animation resumed. Pause and run the check again for the new pose.";
|
|
4466
|
+
sync();
|
|
4467
|
+
});
|
|
4123
4468
|
listen($("print-results"), "click", (e) => {
|
|
4124
4469
|
const b = e.target.closest("button[data-issue]");
|
|
4125
4470
|
if (!b || !report) return;
|
|
@@ -4163,6 +4508,13 @@ function bindAppPrint({
|
|
|
4163
4508
|
}
|
|
4164
4509
|
}
|
|
4165
4510
|
scopeKey = key();
|
|
4511
|
+
for (const id of stateIds()) {
|
|
4512
|
+
if (getViewer().get_model_animation_state(id)?.status === "running") {
|
|
4513
|
+
getViewer().pause_model_animation(id);
|
|
4514
|
+
pausedForCheck.add(id);
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
sync();
|
|
4166
4518
|
const result = await getViewer().check_printability({
|
|
4167
4519
|
model_ids: stateIds(),
|
|
4168
4520
|
level: $("print-level").value,
|
|
@@ -4200,11 +4552,216 @@ function bindAppPrint({
|
|
|
4200
4552
|
disposed = true;
|
|
4201
4553
|
close(false);
|
|
4202
4554
|
report = null;
|
|
4555
|
+
pausedForCheck.clear();
|
|
4203
4556
|
}
|
|
4204
4557
|
};
|
|
4205
4558
|
}
|
|
4206
4559
|
|
|
4207
|
-
// src/
|
|
4560
|
+
// src/app-animation.js
|
|
4561
|
+
var animationControls = [
|
|
4562
|
+
"animation-details",
|
|
4563
|
+
"animation-fields",
|
|
4564
|
+
"animation-axis",
|
|
4565
|
+
"animation-seconds",
|
|
4566
|
+
"animation-direction",
|
|
4567
|
+
"animation-play",
|
|
4568
|
+
"animation-pause",
|
|
4569
|
+
"animation-stop",
|
|
4570
|
+
"animation-status",
|
|
4571
|
+
"animation-error"
|
|
4572
|
+
];
|
|
4573
|
+
var axes2 = ["x", "y", "z"];
|
|
4574
|
+
function syncAnimatedRecord(record, model) {
|
|
4575
|
+
if (!record || !model?.geometry) return;
|
|
4576
|
+
record.bounds = Object.fromEntries(
|
|
4577
|
+
["min", "max"].map((key) => [
|
|
4578
|
+
key,
|
|
4579
|
+
model.geometry.bounds[key].map(
|
|
4580
|
+
(v, i) => (v - model.center[i]) * model.scaleVector[i]
|
|
4581
|
+
)
|
|
4582
|
+
])
|
|
4583
|
+
);
|
|
4584
|
+
record.rotation = Object.fromEntries(
|
|
4585
|
+
axes2.map((a) => [a, model["rotation" + a] * 180 / Math.PI % 360])
|
|
4586
|
+
);
|
|
4587
|
+
record.position = { x: 0, y: 0, z: 0 };
|
|
4588
|
+
const offset = enginePosition(record);
|
|
4589
|
+
for (const a of axes2) record.position[a] = model[a] - offset[a];
|
|
4590
|
+
}
|
|
4591
|
+
function centerAnimationPivot(record, model) {
|
|
4592
|
+
syncAnimatedRecord(record, model);
|
|
4593
|
+
const center = model.geometry.bounds.min.map(
|
|
4594
|
+
(v, i) => (v + model.geometry.bounds.max[i]) / 2
|
|
4595
|
+
);
|
|
4596
|
+
const delta = center.map(
|
|
4597
|
+
(v, i) => (v - model.center[i]) * model.scaleVector[i]
|
|
4598
|
+
);
|
|
4599
|
+
const rotation2 = rotationMatrix(record.rotation);
|
|
4600
|
+
const original = record.originalPosition || { x: 0, y: 0, z: 0 };
|
|
4601
|
+
record.originalPosition = Object.fromEntries(
|
|
4602
|
+
axes2.map((a, i) => [a, original[a] + delta[i]])
|
|
4603
|
+
);
|
|
4604
|
+
for (let i = 0; i < 3; i++)
|
|
4605
|
+
model[axes2[i]] += delta.reduce(
|
|
4606
|
+
(sum, v, j) => sum + rotation2[i * 3 + j] * v,
|
|
4607
|
+
0
|
|
4608
|
+
);
|
|
4609
|
+
model.center = center;
|
|
4610
|
+
record.animationPivot = true;
|
|
4611
|
+
syncAnimatedRecord(record, model);
|
|
4612
|
+
}
|
|
4613
|
+
function spinDefinition(axis, seconds, direction) {
|
|
4614
|
+
if (!axes2.includes(axis) || !Number.isFinite(seconds) || seconds < 0.1 || seconds > 3600 || !["1", "-1"].includes(direction))
|
|
4615
|
+
throw new Error("Choose an axis and a duration from 0.1 to 3,600 seconds.");
|
|
4616
|
+
return {
|
|
4617
|
+
delta: {
|
|
4618
|
+
["rotation" + axis]: 2 * Math.PI * Number(direction),
|
|
4619
|
+
msec: seconds * 1e3,
|
|
4620
|
+
loop: true
|
|
4621
|
+
}
|
|
4622
|
+
};
|
|
4623
|
+
}
|
|
4624
|
+
function bindAppAnimation({
|
|
4625
|
+
$,
|
|
4626
|
+
listen,
|
|
4627
|
+
window: window2,
|
|
4628
|
+
getState,
|
|
4629
|
+
getViewer,
|
|
4630
|
+
beforePlay,
|
|
4631
|
+
refreshPose,
|
|
4632
|
+
announce
|
|
4633
|
+
}) {
|
|
4634
|
+
const settings = /* @__PURE__ */ new Map();
|
|
4635
|
+
let selected = null, timer = null, disposed = false;
|
|
4636
|
+
const state = () => {
|
|
4637
|
+
const s = getState();
|
|
4638
|
+
return {
|
|
4639
|
+
...s,
|
|
4640
|
+
playback: s.selectedId == null ? null : getViewer()?.get_model_animation_state(s.selectedId)
|
|
4641
|
+
};
|
|
4642
|
+
};
|
|
4643
|
+
function sync() {
|
|
4644
|
+
if (disposed) return;
|
|
4645
|
+
const s = state(), available = s.models.some((m) => m.id === s.selectedId);
|
|
4646
|
+
for (const id of settings.keys())
|
|
4647
|
+
if (!s.models.some((m) => m.id === id)) settings.delete(id);
|
|
4648
|
+
$("animation-details").hidden = !available;
|
|
4649
|
+
$("animation-fields").disabled = s.busy || !available;
|
|
4650
|
+
if (selected !== s.selectedId) {
|
|
4651
|
+
selected = s.selectedId;
|
|
4652
|
+
let value = settings.get(selected);
|
|
4653
|
+
if (!value) {
|
|
4654
|
+
value = { axis: "z", seconds: 5, direction: "1" };
|
|
4655
|
+
const a = getViewer()?._models.get(selected)?.animation;
|
|
4656
|
+
const entries = Object.entries(a?.delta || {}).filter(
|
|
4657
|
+
([k, v]) => k.startsWith("rotation") && v !== 0
|
|
4658
|
+
);
|
|
4659
|
+
if (a?.delta?.loop && !a.exact && entries.length === 1) {
|
|
4660
|
+
value = {
|
|
4661
|
+
axis: entries[0][0].slice(-1),
|
|
4662
|
+
seconds: a.delta.msec * 2 * Math.PI / Math.abs(entries[0][1]) / 1e3,
|
|
4663
|
+
direction: entries[0][1] < 0 ? "-1" : "1"
|
|
4664
|
+
};
|
|
4665
|
+
}
|
|
4666
|
+
settings.set(selected, value);
|
|
4667
|
+
}
|
|
4668
|
+
$("animation-axis").value = value.axis;
|
|
4669
|
+
$("animation-seconds").value = value.seconds;
|
|
4670
|
+
$("animation-direction").value = value.direction;
|
|
4671
|
+
$("animation-error").hidden = true;
|
|
4672
|
+
}
|
|
4673
|
+
const status = s.playback?.status || "idle", active = ["running", "paused"].includes(status);
|
|
4674
|
+
$("animation-play").textContent = active ? "Restart spin" : "Play";
|
|
4675
|
+
$("animation-pause").textContent = status === "paused" ? "Resume" : "Pause";
|
|
4676
|
+
$("animation-pause").disabled = !active;
|
|
4677
|
+
$("animation-stop").disabled = !active;
|
|
4678
|
+
const label = {
|
|
4679
|
+
idle: "Ready",
|
|
4680
|
+
running: "Playing",
|
|
4681
|
+
paused: "Paused",
|
|
4682
|
+
stopped: "Stopped",
|
|
4683
|
+
completed: "Finished"
|
|
4684
|
+
}[status];
|
|
4685
|
+
const text = `${label}. ${active ? "Stop keeps the current pose." : "Spin the selected model around its center."}`;
|
|
4686
|
+
if ($("animation-status").textContent !== text)
|
|
4687
|
+
$("animation-status").textContent = text;
|
|
4688
|
+
if (status === "running" && !timer)
|
|
4689
|
+
timer = window2.setInterval(() => {
|
|
4690
|
+
if (!disposed && !window2.document.hidden) refreshPose();
|
|
4691
|
+
}, 250);
|
|
4692
|
+
if (status !== "running" && timer) {
|
|
4693
|
+
window2.clearInterval(timer);
|
|
4694
|
+
timer = null;
|
|
4695
|
+
}
|
|
4696
|
+
}
|
|
4697
|
+
function play(keepPaused = false) {
|
|
4698
|
+
const s = state();
|
|
4699
|
+
if (s.busy || s.selectedId == null) return;
|
|
4700
|
+
try {
|
|
4701
|
+
const value = {
|
|
4702
|
+
axis: $("animation-axis").value,
|
|
4703
|
+
seconds: $("animation-seconds").valueAsNumber,
|
|
4704
|
+
direction: $("animation-direction").value
|
|
4705
|
+
};
|
|
4706
|
+
const definition = spinDefinition(
|
|
4707
|
+
value.axis,
|
|
4708
|
+
value.seconds,
|
|
4709
|
+
value.direction
|
|
4710
|
+
);
|
|
4711
|
+
beforePlay(s.selectedId);
|
|
4712
|
+
getViewer().animate_model(s.selectedId, definition);
|
|
4713
|
+
if (keepPaused && s.playback?.status === "paused")
|
|
4714
|
+
getViewer().pause_model_animation(s.selectedId);
|
|
4715
|
+
settings.set(s.selectedId, value);
|
|
4716
|
+
$("animation-error").hidden = true;
|
|
4717
|
+
announce(
|
|
4718
|
+
keepPaused && s.playback?.status === "paused" ? "Spin updated. Still paused." : "Animation playing."
|
|
4719
|
+
);
|
|
4720
|
+
sync();
|
|
4721
|
+
} catch (error) {
|
|
4722
|
+
$("animation-error").textContent = error.message;
|
|
4723
|
+
$("animation-error").hidden = false;
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
listen($("animation-play"), "click", () => play());
|
|
4727
|
+
listen($("animation-pause"), "click", () => {
|
|
4728
|
+
const s = state();
|
|
4729
|
+
if (s.busy || s.selectedId == null) return;
|
|
4730
|
+
getViewer()[s.playback?.status === "paused" ? "resume_model_animation" : "pause_model_animation"](s.selectedId);
|
|
4731
|
+
sync();
|
|
4732
|
+
});
|
|
4733
|
+
listen($("animation-stop"), "click", () => {
|
|
4734
|
+
const s = state();
|
|
4735
|
+
if (!s.busy && s.selectedId != null)
|
|
4736
|
+
getViewer().animate_model(s.selectedId, null);
|
|
4737
|
+
sync();
|
|
4738
|
+
});
|
|
4739
|
+
for (const id of [
|
|
4740
|
+
"animation-axis",
|
|
4741
|
+
"animation-seconds",
|
|
4742
|
+
"animation-direction"
|
|
4743
|
+
])
|
|
4744
|
+
listen($(id), "change", () => {
|
|
4745
|
+
if (["running", "paused"].includes(state().playback?.status)) play(true);
|
|
4746
|
+
else
|
|
4747
|
+
settings.set(selected, {
|
|
4748
|
+
axis: $("animation-axis").value,
|
|
4749
|
+
seconds: $("animation-seconds").valueAsNumber,
|
|
4750
|
+
direction: $("animation-direction").value
|
|
4751
|
+
});
|
|
4752
|
+
});
|
|
4753
|
+
sync();
|
|
4754
|
+
return {
|
|
4755
|
+
sync,
|
|
4756
|
+
dispose() {
|
|
4757
|
+
disposed = true;
|
|
4758
|
+
window2.clearInterval(timer);
|
|
4759
|
+
settings.clear();
|
|
4760
|
+
}
|
|
4761
|
+
};
|
|
4762
|
+
}
|
|
4763
|
+
|
|
4764
|
+
// src/app.js
|
|
4208
4765
|
var mountedRoots = /* @__PURE__ */ new WeakMap();
|
|
4209
4766
|
var requiredControls = [
|
|
4210
4767
|
"background-color",
|
|
@@ -4309,6 +4866,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4309
4866
|
missing.push(...exportControls.filter((id) => !$(id)));
|
|
4310
4867
|
if (printControls.some((id) => $(id)))
|
|
4311
4868
|
missing.push(...printControls.filter((id) => !$(id)));
|
|
4869
|
+
if (animationControls.some((id) => $(id)))
|
|
4870
|
+
missing.push(...animationControls.filter((id) => !$(id)));
|
|
4312
4871
|
if (missing.length)
|
|
4313
4872
|
throw new Error("Missing ViewSTL controls: " + missing.join(", "));
|
|
4314
4873
|
const abort = new window2.AbortController();
|
|
@@ -4336,6 +4895,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4336
4895
|
let viewerPromise, viewer, generation = 0, appearanceRequest = 0, modelID = null, nextID = 0, loadingID = null;
|
|
4337
4896
|
let exportUI;
|
|
4338
4897
|
let printUI;
|
|
4898
|
+
let animationUI;
|
|
4339
4899
|
let sceneImport;
|
|
4340
4900
|
let draining = false, busy = false, gridEnabled = false, gridHeight = 0, gridSpacing = 1, moveMode = false, transformTool = "move", modelMover, dragArrangement, background = "transparent";
|
|
4341
4901
|
const viewNames = {
|
|
@@ -4409,6 +4969,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4409
4969
|
$("background-value").textContent = background === "transparent" ? "Default" : background.toUpperCase();
|
|
4410
4970
|
}
|
|
4411
4971
|
function syncControls() {
|
|
4972
|
+
syncAnimatedRecord(records.get(modelID), viewer?._models.get(modelID));
|
|
4412
4973
|
const info = modelID === null ? null : viewer.get_model_info(modelID), mode = info?.display === "smooth" ? "flat" : info?.display || "flat";
|
|
4413
4974
|
for (const input of modeInputs) {
|
|
4414
4975
|
input.checked = input.value === mode;
|
|
@@ -4437,6 +4998,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4437
4998
|
syncPosition();
|
|
4438
4999
|
exportUI?.sync();
|
|
4439
5000
|
printUI?.sync();
|
|
5001
|
+
animationUI?.sync();
|
|
4440
5002
|
}
|
|
4441
5003
|
function populateInfo() {
|
|
4442
5004
|
if (modelID === null) return;
|
|
@@ -4516,6 +5078,10 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4516
5078
|
}
|
|
4517
5079
|
function arrangeScene() {
|
|
4518
5080
|
setMoveMode(false);
|
|
5081
|
+
for (const record of records.values())
|
|
5082
|
+
syncAnimatedRecord(record, viewer?._models.get(record.id));
|
|
5083
|
+
if ($("scene-arrangement").value === "custom") return;
|
|
5084
|
+
for (const record of records.values()) prepareManual(record.id);
|
|
4519
5085
|
if ($("scene-arrangement").value === "original")
|
|
4520
5086
|
for (const record of records.values())
|
|
4521
5087
|
record.rotation = {
|
|
@@ -4532,19 +5098,20 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4532
5098
|
updateGrid();
|
|
4533
5099
|
viewer?.set_auto_zoom();
|
|
4534
5100
|
}
|
|
4535
|
-
function syncPosition() {
|
|
5101
|
+
function syncPosition(preserveFocus = false) {
|
|
4536
5102
|
const record = records.get(modelID);
|
|
4537
5103
|
for (const axis of ["x", "y", "z"]) {
|
|
4538
5104
|
const input = $("position-" + axis);
|
|
4539
|
-
|
|
5105
|
+
if (!preserveFocus || document2.activeElement !== input)
|
|
5106
|
+
input.value = Number((record?.position?.[axis] || 0).toPrecision(12));
|
|
4540
5107
|
input.disabled = !record;
|
|
4541
5108
|
input.removeAttribute("aria-invalid");
|
|
4542
5109
|
}
|
|
4543
5110
|
$("position-error").hidden = true;
|
|
4544
|
-
$("position-units").textContent = record?.units === "mm" ? "Offset from file coordinates, in mm." : "Offset from file coordinates, in model units.";
|
|
5111
|
+
$("position-units").textContent = record?.animationPivot ? "Position of the model center in the scene." : record?.units === "mm" ? "Offset from file coordinates, in mm." : "Offset from file coordinates, in model units.";
|
|
4545
5112
|
$("move-model-button").disabled = !record || busy;
|
|
4546
5113
|
$("reset-position").disabled = !record;
|
|
4547
|
-
syncRotation();
|
|
5114
|
+
syncRotation(preserveFocus);
|
|
4548
5115
|
for (const id of [
|
|
4549
5116
|
"rotate-model-button",
|
|
4550
5117
|
"depth-model-button",
|
|
@@ -4573,14 +5140,16 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4573
5140
|
viewer.set_position(record.id, p.x, p.y, p.z);
|
|
4574
5141
|
}
|
|
4575
5142
|
function selectedTransform() {
|
|
5143
|
+
prepareManual(modelID);
|
|
4576
5144
|
const record = records.get(modelID);
|
|
4577
5145
|
return record ? { ...record, center: modelCenter(record), radius: modelRadius(record) } : null;
|
|
4578
5146
|
}
|
|
4579
|
-
function syncRotation() {
|
|
5147
|
+
function syncRotation(preserveFocus = false) {
|
|
4580
5148
|
const record = records.get(modelID);
|
|
4581
5149
|
for (const axis of ["x", "y", "z"]) {
|
|
4582
5150
|
const input = $("rotation-" + axis);
|
|
4583
|
-
|
|
5151
|
+
if (!preserveFocus || document2.activeElement !== input)
|
|
5152
|
+
input.value = Number((record?.rotation?.[axis] || 0).toPrecision(10));
|
|
4584
5153
|
input.disabled = !record;
|
|
4585
5154
|
input.removeAttribute("aria-invalid");
|
|
4586
5155
|
}
|
|
@@ -4626,6 +5195,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4626
5195
|
modelMover?.setEnabled(false);
|
|
4627
5196
|
transformTool = tool;
|
|
4628
5197
|
moveMode = !!enabled && modelID !== null && !busy;
|
|
5198
|
+
if (moveMode) prepareManual(modelID);
|
|
4629
5199
|
modelMover?.setEnabled(moveMode);
|
|
4630
5200
|
syncTool();
|
|
4631
5201
|
$("move-mode").hidden = !moveMode;
|
|
@@ -4649,6 +5219,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4649
5219
|
const input = $("rotation-" + axis);
|
|
4650
5220
|
listen(input, "input", () => {
|
|
4651
5221
|
if (modelID === null || !input.value.trim()) return;
|
|
5222
|
+
prepareManual(modelID);
|
|
4652
5223
|
if (applyRotation(
|
|
4653
5224
|
modelID,
|
|
4654
5225
|
{ ...records.get(modelID).rotation, [axis]: input.valueAsNumber },
|
|
@@ -4660,6 +5231,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4660
5231
|
});
|
|
4661
5232
|
listen(input, "change", () => {
|
|
4662
5233
|
if (modelID === null) return;
|
|
5234
|
+
prepareManual(modelID);
|
|
4663
5235
|
if (!input.value.trim() || !applyRotation(modelID, {
|
|
4664
5236
|
...records.get(modelID).rotation,
|
|
4665
5237
|
[axis]: input.valueAsNumber
|
|
@@ -4672,6 +5244,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4672
5244
|
listen($("reset-rotation"), "click", () => {
|
|
4673
5245
|
setMoveMode(false);
|
|
4674
5246
|
if (modelID !== null) {
|
|
5247
|
+
prepareManual(modelID);
|
|
4675
5248
|
applyRotation(modelID, {
|
|
4676
5249
|
...records.get(modelID).originalRotation || { x: 0, y: 0, z: 0 }
|
|
4677
5250
|
});
|
|
@@ -4711,6 +5284,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4711
5284
|
const input = $("position-" + axis);
|
|
4712
5285
|
listen(input, "input", () => {
|
|
4713
5286
|
if (modelID === null || !input.value.trim()) return;
|
|
5287
|
+
prepareManual(modelID);
|
|
4714
5288
|
if (applyPosition(
|
|
4715
5289
|
modelID,
|
|
4716
5290
|
{ ...records.get(modelID).position, [axis]: input.valueAsNumber },
|
|
@@ -4722,6 +5296,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4722
5296
|
});
|
|
4723
5297
|
listen(input, "change", () => {
|
|
4724
5298
|
if (modelID === null) return;
|
|
5299
|
+
prepareManual(modelID);
|
|
4725
5300
|
if (!input.value.trim() || !applyPosition(modelID, {
|
|
4726
5301
|
...records.get(modelID).position,
|
|
4727
5302
|
[axis]: input.valueAsNumber
|
|
@@ -4738,6 +5313,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4738
5313
|
listen($("reset-position"), "click", () => {
|
|
4739
5314
|
setMoveMode(false);
|
|
4740
5315
|
if (modelID !== null) {
|
|
5316
|
+
prepareManual(modelID);
|
|
4741
5317
|
applyPosition(modelID, {
|
|
4742
5318
|
...records.get(modelID).originalPosition || { x: 0, y: 0, z: 0 }
|
|
4743
5319
|
});
|
|
@@ -4788,6 +5364,29 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4788
5364
|
renderScene();
|
|
4789
5365
|
announce(name + " removed.");
|
|
4790
5366
|
}
|
|
5367
|
+
function prepareManual(id) {
|
|
5368
|
+
if (!records.has(id)) return;
|
|
5369
|
+
syncAnimatedRecord(records.get(id), viewer?._models.get(id));
|
|
5370
|
+
viewer?.animate_model(id, null);
|
|
5371
|
+
}
|
|
5372
|
+
function refreshAnimatedPose() {
|
|
5373
|
+
if (disposed || modelID === null) return;
|
|
5374
|
+
syncAnimatedRecord(records.get(modelID), viewer?._models.get(modelID));
|
|
5375
|
+
if (!$("scene-panel").hidden && $("position-details").open)
|
|
5376
|
+
syncPosition(true);
|
|
5377
|
+
}
|
|
5378
|
+
function animationChanged(id, state, reason) {
|
|
5379
|
+
if (disposed) return;
|
|
5380
|
+
if (reason === "restored" && state.status === "running" && window2.matchMedia?.("(prefers-reduced-motion: reduce)").matches)
|
|
5381
|
+
viewer.pause_model_animation(id);
|
|
5382
|
+
syncAnimatedRecord(records.get(id), viewer?._models.get(id));
|
|
5383
|
+
if (["started", "restored", "resumed"].includes(reason))
|
|
5384
|
+
$("scene-arrangement").value = "custom";
|
|
5385
|
+
if (id === modelID) refreshAnimatedPose();
|
|
5386
|
+
animationUI?.sync();
|
|
5387
|
+
printUI?.sync();
|
|
5388
|
+
options.animation_state_callback?.(id, state, reason);
|
|
5389
|
+
}
|
|
4791
5390
|
async function getViewer() {
|
|
4792
5391
|
if (!viewerPromise)
|
|
4793
5392
|
viewerPromise = Promise.resolve().then(async () => {
|
|
@@ -4802,6 +5401,10 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4802
5401
|
center_models: false,
|
|
4803
5402
|
bg_color: background,
|
|
4804
5403
|
allow_drag_and_drop: false,
|
|
5404
|
+
animation_state_callback: animationChanged,
|
|
5405
|
+
animation_complete_callback: (...args) => {
|
|
5406
|
+
if (!disposed) options.animation_complete_callback?.(...args);
|
|
5407
|
+
},
|
|
4805
5408
|
camera_state: {
|
|
4806
5409
|
position: { x: 90, y: -130, z: 90 },
|
|
4807
5410
|
target: { x: 0, y: 0, z: 0 },
|
|
@@ -5383,6 +5986,26 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5383
5986
|
},
|
|
5384
5987
|
announce
|
|
5385
5988
|
});
|
|
5989
|
+
if ($("animation-details"))
|
|
5990
|
+
animationUI = bindAppAnimation({
|
|
5991
|
+
$,
|
|
5992
|
+
listen,
|
|
5993
|
+
window: window2,
|
|
5994
|
+
getState: () => ({
|
|
5995
|
+
busy,
|
|
5996
|
+
selectedId: modelID,
|
|
5997
|
+
models: [...records.values()]
|
|
5998
|
+
}),
|
|
5999
|
+
getViewer: () => viewer,
|
|
6000
|
+
beforePlay(id) {
|
|
6001
|
+
setMoveMode(false);
|
|
6002
|
+
prepareManual(id);
|
|
6003
|
+
centerAnimationPivot(records.get(id), viewer._models.get(id));
|
|
6004
|
+
$("scene-arrangement").value = "custom";
|
|
6005
|
+
},
|
|
6006
|
+
refreshPose: refreshAnimatedPose,
|
|
6007
|
+
announce
|
|
6008
|
+
});
|
|
5386
6009
|
updateGrid();
|
|
5387
6010
|
setBusy(false);
|
|
5388
6011
|
setMoveMode(false);
|
|
@@ -5407,6 +6030,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5407
6030
|
},
|
|
5408
6031
|
getState() {
|
|
5409
6032
|
assertActive();
|
|
6033
|
+
for (const record of records.values())
|
|
6034
|
+
syncAnimatedRecord(record, viewer?._models.get(record.id));
|
|
5410
6035
|
return {
|
|
5411
6036
|
selectedId: modelID,
|
|
5412
6037
|
busy,
|
|
@@ -5419,7 +6044,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5419
6044
|
units: record.units,
|
|
5420
6045
|
position: { ...record.position },
|
|
5421
6046
|
rotation: { ...record.rotation },
|
|
5422
|
-
color: record.color
|
|
6047
|
+
color: record.color,
|
|
6048
|
+
animation: viewer.get_model_animation_state(record.id)
|
|
5423
6049
|
}))
|
|
5424
6050
|
};
|
|
5425
6051
|
},
|
|
@@ -5427,6 +6053,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5427
6053
|
if (disposed) return;
|
|
5428
6054
|
exportUI?.dispose();
|
|
5429
6055
|
printUI?.dispose();
|
|
6056
|
+
animationUI?.dispose();
|
|
5430
6057
|
cancelLoading();
|
|
5431
6058
|
setMoveMode(false);
|
|
5432
6059
|
appearanceRequest++;
|