@omrips/viewstl 2.0.0-rc.3 → 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 -170
- 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 +158 -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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/appearance.js
|
|
2
2
|
var DISPLAYS = [
|
|
3
3
|
"flat",
|
|
4
4
|
"smooth",
|
|
@@ -46,7 +46,7 @@ function sectionPlane(bounds, section) {
|
|
|
46
46
|
return plane;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// src/
|
|
49
|
+
// src/math.js
|
|
50
50
|
var add = (a, b) => a.map((v, i) => v + b[i]);
|
|
51
51
|
var sub = (a, b) => a.map((v, i) => v - b[i]);
|
|
52
52
|
var mul = (a, s) => a.map((v) => v * s);
|
|
@@ -198,7 +198,7 @@ function rayTriangle(o, d, p, i) {
|
|
|
198
198
|
return t >= 0 ? t : Infinity;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
// src/
|
|
201
|
+
// src/memory.js
|
|
202
202
|
var MEMORY_OVERHEAD = 64 * 1024 * 1024;
|
|
203
203
|
var TRIANGLE_MEMORY = 224;
|
|
204
204
|
function geometryMemory(g) {
|
|
@@ -215,7 +215,7 @@ function derivedMemory(triangles, features) {
|
|
|
215
215
|
return MEMORY_OVERHEAD + triangles * (TRIANGLE_MEMORY + (features.smooth ? 1536 : 0) + (features.edges || features.wire ? 2048 : 0));
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
// src/
|
|
218
|
+
// src/geometry.js
|
|
219
219
|
var DEFAULT_LIMITS = Object.freeze({
|
|
220
220
|
max_file_bytes: 256 * 1024 * 1024,
|
|
221
221
|
max_triangles: 5e6,
|
|
@@ -363,7 +363,7 @@ function exportSTL(g, scale = [1, 1, 1], center = [0, 0, 0]) {
|
|
|
363
363
|
return buffer;
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
-
// src/
|
|
366
|
+
// src/infinite-grid.js
|
|
367
367
|
var vertex = `#version 300 es
|
|
368
368
|
precision highp float;
|
|
369
369
|
out vec2 screen;
|
|
@@ -455,7 +455,7 @@ var InfiniteGrid = class {
|
|
|
455
455
|
}
|
|
456
456
|
};
|
|
457
457
|
|
|
458
|
-
// src/
|
|
458
|
+
// src/selection-frame.js
|
|
459
459
|
function selectionFrame(bounds) {
|
|
460
460
|
const padding = Math.max(
|
|
461
461
|
Math.max(...bounds.max.map((v, i) => v - bounds.min[i])) * 0.015,
|
|
@@ -473,7 +473,7 @@ function selectionFrame(bounds) {
|
|
|
473
473
|
return new Float32Array(lines);
|
|
474
474
|
}
|
|
475
475
|
|
|
476
|
-
// src/
|
|
476
|
+
// src/renderer.js
|
|
477
477
|
var vertex2 = `#version 300 es
|
|
478
478
|
precision highp float;
|
|
479
479
|
layout(location=0) in vec3 position;
|
|
@@ -894,7 +894,7 @@ var Renderer = class {
|
|
|
894
894
|
}
|
|
895
895
|
};
|
|
896
896
|
|
|
897
|
-
// src/
|
|
897
|
+
// src/controls.js
|
|
898
898
|
var Controls = class {
|
|
899
899
|
constructor(canvas, camera, invalidate, pick, type = 0) {
|
|
900
900
|
this.enabled = true;
|
|
@@ -1005,7 +1005,7 @@ var Controls = class {
|
|
|
1005
1005
|
}
|
|
1006
1006
|
};
|
|
1007
1007
|
|
|
1008
|
-
// src/
|
|
1008
|
+
// src/worker-pool.js
|
|
1009
1009
|
var WorkerPool = class {
|
|
1010
1010
|
constructor(url, { count = 2, timeout = 12e4, factory } = {}) {
|
|
1011
1011
|
this.url = url;
|
|
@@ -1090,7 +1090,178 @@ var WorkerPool = class {
|
|
|
1090
1090
|
}
|
|
1091
1091
|
};
|
|
1092
1092
|
|
|
1093
|
-
// src/
|
|
1093
|
+
// src/animation.js
|
|
1094
|
+
var animationKeys = [
|
|
1095
|
+
"x",
|
|
1096
|
+
"y",
|
|
1097
|
+
"z",
|
|
1098
|
+
"rotationx",
|
|
1099
|
+
"rotationy",
|
|
1100
|
+
"rotationz",
|
|
1101
|
+
"scalex",
|
|
1102
|
+
"scaley",
|
|
1103
|
+
"scalez"
|
|
1104
|
+
];
|
|
1105
|
+
var isScale = (key) => key.startsWith("scale");
|
|
1106
|
+
var number = (value, name, positive2 = false) => {
|
|
1107
|
+
if (typeof value !== "number" || !Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER || positive2 && value <= 0)
|
|
1108
|
+
throw new Error(`Invalid animation ${name}`);
|
|
1109
|
+
return value;
|
|
1110
|
+
};
|
|
1111
|
+
var object = (value, name) => {
|
|
1112
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1113
|
+
throw new Error(`Invalid animation ${name}`);
|
|
1114
|
+
return value;
|
|
1115
|
+
};
|
|
1116
|
+
function animationPose(model) {
|
|
1117
|
+
return Object.fromEntries(
|
|
1118
|
+
animationKeys.map((key) => [
|
|
1119
|
+
key,
|
|
1120
|
+
number(
|
|
1121
|
+
model[key] ?? (isScale(key) ? model.scale ?? 1 : 0),
|
|
1122
|
+
key,
|
|
1123
|
+
isScale(key)
|
|
1124
|
+
)
|
|
1125
|
+
])
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
function normalizeAnimation(input) {
|
|
1129
|
+
object(input, "definition");
|
|
1130
|
+
const result = {}, claimed = /* @__PURE__ */ new Set();
|
|
1131
|
+
for (const mode of ["delta", "exact"]) {
|
|
1132
|
+
if (input[mode] == null) continue;
|
|
1133
|
+
const source = object(input[mode], mode), values = {};
|
|
1134
|
+
const msec = number(source.msec ?? 300, "duration", true);
|
|
1135
|
+
if (source.loop !== void 0 && typeof source.loop !== "boolean")
|
|
1136
|
+
throw new Error("Animation loop must be a boolean");
|
|
1137
|
+
const loop = mode === "delta" && !!source.loop;
|
|
1138
|
+
for (const key of animationKeys) {
|
|
1139
|
+
const value = source[key] ?? (isScale(key) ? source.scale : void 0);
|
|
1140
|
+
if (value === void 0) continue;
|
|
1141
|
+
if (claimed.has(key))
|
|
1142
|
+
throw new Error(`Animation delta/exact conflict: ${key}`);
|
|
1143
|
+
number(value, key, mode === "exact" && isScale(key));
|
|
1144
|
+
if (loop && isScale(key) && value < 0)
|
|
1145
|
+
throw new Error("Looping scale deltas cannot be negative");
|
|
1146
|
+
values[key] = value;
|
|
1147
|
+
claimed.add(key);
|
|
1148
|
+
}
|
|
1149
|
+
if (!Object.keys(values).length)
|
|
1150
|
+
throw new Error("Animation track needs a transform");
|
|
1151
|
+
result[mode] = { ...values, msec, ...loop ? { loop: true } : {} };
|
|
1152
|
+
}
|
|
1153
|
+
if (!Object.keys(result).length)
|
|
1154
|
+
throw new Error("Animation needs delta or exact");
|
|
1155
|
+
return result;
|
|
1156
|
+
}
|
|
1157
|
+
function normalizePlaybackState(input) {
|
|
1158
|
+
object(input, "playback state");
|
|
1159
|
+
if (input.version !== 1 || typeof input.paused !== "boolean")
|
|
1160
|
+
throw new Error("Invalid animation playback state version or paused flag");
|
|
1161
|
+
const elapsed_ms = number(input.elapsed_ms, "elapsed time");
|
|
1162
|
+
if (elapsed_ms < 0) throw new Error("Invalid animation elapsed time");
|
|
1163
|
+
object(input.origin, "origin");
|
|
1164
|
+
for (const key of animationKeys) number(input.origin[key], key, isScale(key));
|
|
1165
|
+
if (!Array.isArray(input.center) || input.center.length !== 3)
|
|
1166
|
+
throw new Error("Invalid animation pivot");
|
|
1167
|
+
return {
|
|
1168
|
+
version: 1,
|
|
1169
|
+
elapsed_ms,
|
|
1170
|
+
paused: input.paused,
|
|
1171
|
+
origin: animationPose(input.origin),
|
|
1172
|
+
center: input.center.map((v) => number(v, "pivot"))
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
function createPlayback(definition, model, saved) {
|
|
1176
|
+
const animation = normalizeAnimation(definition);
|
|
1177
|
+
const state = saved == null ? null : normalizePlaybackState(saved);
|
|
1178
|
+
const origin = state?.origin ?? animationPose(model);
|
|
1179
|
+
const tracks = Object.entries(animation).map(([mode, a]) => {
|
|
1180
|
+
const values = Object.entries(a).filter(
|
|
1181
|
+
([key]) => animationKeys.includes(key)
|
|
1182
|
+
);
|
|
1183
|
+
if (mode === "delta")
|
|
1184
|
+
for (const [key, value] of values)
|
|
1185
|
+
number(origin[key] + value, `${key} endpoint`, isScale(key));
|
|
1186
|
+
return { mode, values, duration: a.msec, loop: !!a.loop, done: false };
|
|
1187
|
+
});
|
|
1188
|
+
const playback = {
|
|
1189
|
+
animation,
|
|
1190
|
+
origin,
|
|
1191
|
+
tracks,
|
|
1192
|
+
elapsed_ms: state?.elapsed_ms ?? 0,
|
|
1193
|
+
paused: state?.paused ?? false,
|
|
1194
|
+
last_time: null
|
|
1195
|
+
};
|
|
1196
|
+
evaluatePlayback({ ...playback, tracks: tracks.map((t) => ({ ...t })) }, {});
|
|
1197
|
+
return playback;
|
|
1198
|
+
}
|
|
1199
|
+
function evaluatePlayback(playback, model) {
|
|
1200
|
+
const updates = [];
|
|
1201
|
+
for (const track of playback.tracks) {
|
|
1202
|
+
if (track.done) continue;
|
|
1203
|
+
const fraction = playback.elapsed_ms / track.duration;
|
|
1204
|
+
const p = track.loop ? fraction : Math.min(fraction, 1);
|
|
1205
|
+
for (const [key, value] of track.values) {
|
|
1206
|
+
const from = playback.origin[key];
|
|
1207
|
+
const to = track.mode === "delta" ? from + value * p : from * (1 - p) + value * p;
|
|
1208
|
+
updates.push([key, number(to, `${key} result`, isScale(key))]);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
let scaleChanged = false;
|
|
1212
|
+
for (const [key, value] of updates) {
|
|
1213
|
+
scaleChanged ||= isScale(key) && model[key] !== value;
|
|
1214
|
+
model[key] = value;
|
|
1215
|
+
}
|
|
1216
|
+
for (const track of playback.tracks)
|
|
1217
|
+
if (!track.loop && playback.elapsed_ms >= track.duration) track.done = true;
|
|
1218
|
+
if (scaleChanged) model._stats = null;
|
|
1219
|
+
return playback.tracks.some((track) => !track.done);
|
|
1220
|
+
}
|
|
1221
|
+
function advancePlayback(playback, model, time) {
|
|
1222
|
+
if (playback.paused) return true;
|
|
1223
|
+
if (playback.last_time !== null)
|
|
1224
|
+
playback.elapsed_ms = number(
|
|
1225
|
+
playback.elapsed_ms + Math.max(0, time - playback.last_time),
|
|
1226
|
+
"elapsed time"
|
|
1227
|
+
);
|
|
1228
|
+
playback.last_time = time;
|
|
1229
|
+
return evaluatePlayback(playback, model);
|
|
1230
|
+
}
|
|
1231
|
+
function playbackState(playback, suspended = false) {
|
|
1232
|
+
return {
|
|
1233
|
+
status: playback.paused ? "paused" : "running",
|
|
1234
|
+
elapsed_ms: playback.elapsed_ms,
|
|
1235
|
+
suspended,
|
|
1236
|
+
tracks: playback.tracks.filter((t) => !t.done).map((t) => ({
|
|
1237
|
+
mode: t.mode,
|
|
1238
|
+
msec: t.duration,
|
|
1239
|
+
loop: t.loop,
|
|
1240
|
+
progress: t.loop ? playback.elapsed_ms % t.duration / t.duration : Math.min(1, playback.elapsed_ms / t.duration)
|
|
1241
|
+
}))
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
function savePlayback(model) {
|
|
1245
|
+
const p = model._animation;
|
|
1246
|
+
if (!p) return null;
|
|
1247
|
+
const factor = model.unitScale ?? 1;
|
|
1248
|
+
const animation = structuredClone(p.animation), origin = { ...p.origin };
|
|
1249
|
+
for (const key of animationKeys.filter(isScale)) {
|
|
1250
|
+
origin[key] *= factor;
|
|
1251
|
+
for (const track of Object.values(animation))
|
|
1252
|
+
if (track[key] !== void 0) track[key] *= factor;
|
|
1253
|
+
}
|
|
1254
|
+
const animation_state = normalizePlaybackState({
|
|
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
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// src/scene-import.js
|
|
1094
1265
|
var sceneFormat = (name) => String(name || "").match(/\.(vsb|vsj)(?:[?#]|$)/i)?.[1].toLowerCase() || "";
|
|
1095
1266
|
function localSceneFile(reference, files) {
|
|
1096
1267
|
let path;
|
|
@@ -1124,7 +1295,7 @@ function sceneURL(reference, base) {
|
|
|
1124
1295
|
return url.href;
|
|
1125
1296
|
}
|
|
1126
1297
|
|
|
1127
|
-
// src/
|
|
1298
|
+
// src/scene-import-controller.js
|
|
1128
1299
|
function importScene(source, options = {}) {
|
|
1129
1300
|
const controller = new AbortController(), signal = controller.signal;
|
|
1130
1301
|
const generation = this._generation, ids = [], added = /* @__PURE__ */ new Map(), usedFiles = /* @__PURE__ */ new Set();
|
|
@@ -1239,7 +1410,7 @@ function importScene(source, options = {}) {
|
|
|
1239
1410
|
progress({ phase: "opening-model", loaded: index, total: models.length });
|
|
1240
1411
|
check();
|
|
1241
1412
|
const input = models[index], id = this.max_model_id + 1;
|
|
1242
|
-
this.add_model({ ...input, id });
|
|
1413
|
+
this.add_model({ ...input, id, animation: null, animation_state: null });
|
|
1243
1414
|
const model = this._models.get(id);
|
|
1244
1415
|
if (!model) throw new Error(this.error || "Invalid scene model");
|
|
1245
1416
|
ids.push(id);
|
|
@@ -1264,7 +1435,9 @@ function importScene(source, options = {}) {
|
|
|
1264
1435
|
}),
|
|
1265
1436
|
delta
|
|
1266
1437
|
);
|
|
1267
|
-
|
|
1438
|
+
if (input.animation)
|
|
1439
|
+
model.center = [...input.animation_state?.center ?? desired];
|
|
1440
|
+
else ["x", "y", "z"].forEach((axis, i) => model[axis] += shift[i]);
|
|
1268
1441
|
if ([...this._models.values()].reduce(
|
|
1269
1442
|
(n, m) => n + (m.geometry?.triangles || 0),
|
|
1270
1443
|
0
|
|
@@ -1277,7 +1450,12 @@ function importScene(source, options = {}) {
|
|
|
1277
1450
|
total: models.length
|
|
1278
1451
|
});
|
|
1279
1452
|
check();
|
|
1280
|
-
for (
|
|
1453
|
+
for (let index = 0; index < ids.length; index++) {
|
|
1454
|
+
const model = added.get(ids[index]), input = models[index];
|
|
1455
|
+
if (input.animation)
|
|
1456
|
+
this._startAnimation(model, input.animation, input.animation_state);
|
|
1457
|
+
model._sceneImportPending = false;
|
|
1458
|
+
}
|
|
1281
1459
|
if (scene.bg_color !== void 0) this.set_bg_color(scene.bg_color);
|
|
1282
1460
|
if (scene.infinite_grid)
|
|
1283
1461
|
this.set_infinite_grid(
|
|
@@ -1309,12 +1487,12 @@ function importScene(source, options = {}) {
|
|
|
1309
1487
|
return promise;
|
|
1310
1488
|
}
|
|
1311
1489
|
|
|
1312
|
-
// src/
|
|
1490
|
+
// src/print-options.js
|
|
1313
1491
|
function printOptions(input = {}) {
|
|
1314
1492
|
const level = input.level ?? "basic";
|
|
1315
1493
|
if (!["basic", "detailed"].includes(level))
|
|
1316
1494
|
throw new Error("Choose basic or detailed print checks");
|
|
1317
|
-
const
|
|
1495
|
+
const number2 = (v, name, max = 1e9) => {
|
|
1318
1496
|
if (!Number.isFinite(v) || v <= 0 || v > max)
|
|
1319
1497
|
throw new Error(`Invalid ${name}`);
|
|
1320
1498
|
return v;
|
|
@@ -1326,9 +1504,9 @@ function printOptions(input = {}) {
|
|
|
1326
1504
|
profile = { technology: "fdm" };
|
|
1327
1505
|
for (const key of ["min_wall_mm", "line_width_mm"])
|
|
1328
1506
|
if (input.profile[key] != null)
|
|
1329
|
-
profile[key] =
|
|
1507
|
+
profile[key] = number2(input.profile[key], key);
|
|
1330
1508
|
if (input.profile.overhang_degrees != null)
|
|
1331
|
-
profile.overhang_degrees =
|
|
1509
|
+
profile.overhang_degrees = number2(
|
|
1332
1510
|
input.profile.overhang_degrees,
|
|
1333
1511
|
"overhang angle",
|
|
1334
1512
|
89.9
|
|
@@ -1337,11 +1515,11 @@ function printOptions(input = {}) {
|
|
|
1337
1515
|
if (!Array.isArray(input.profile.build_volume_mm) || input.profile.build_volume_mm.length !== 3)
|
|
1338
1516
|
throw new Error("Build volume requires X, Y and Z");
|
|
1339
1517
|
profile.build_volume_mm = input.profile.build_volume_mm.map(
|
|
1340
|
-
(v) =>
|
|
1518
|
+
(v) => number2(v, "build volume")
|
|
1341
1519
|
);
|
|
1342
1520
|
}
|
|
1343
1521
|
}
|
|
1344
|
-
const time =
|
|
1522
|
+
const time = number2(input.max_time_ms ?? 3e4, "check time limit", 12e4);
|
|
1345
1523
|
return {
|
|
1346
1524
|
level,
|
|
1347
1525
|
profile,
|
|
@@ -1364,7 +1542,7 @@ var printSignature = (model) => JSON.stringify([
|
|
|
1364
1542
|
model.unitScale
|
|
1365
1543
|
]);
|
|
1366
1544
|
|
|
1367
|
-
// src/
|
|
1545
|
+
// src/print-controller.js
|
|
1368
1546
|
var reports = /* @__PURE__ */ new WeakMap();
|
|
1369
1547
|
function reportCurrent(viewer, report) {
|
|
1370
1548
|
const saved = reports.get(report);
|
|
@@ -1577,7 +1755,7 @@ function showPrintability(report, { issue, model_id = report.model_ids[0] } = {}
|
|
|
1577
1755
|
return lines.length / 6;
|
|
1578
1756
|
}
|
|
1579
1757
|
|
|
1580
|
-
// src/
|
|
1758
|
+
// src/export-options.js
|
|
1581
1759
|
function exportFilename(filename, extension) {
|
|
1582
1760
|
const name = String(filename || "scene").split(/[\\/]/).pop().split(/[?#]/)[0].replace(/[<>:"|*\x00-\x1f\x7f]/g, "_").replace(/\.(?:stl|obj|3mf|step|stp|iges|igs|zip|vsb|vsj)$/i, "").replace(/[. ]+$/g, "").slice(0, 160) || "scene";
|
|
1583
1761
|
return `${name}.${extension}`;
|
|
@@ -1618,7 +1796,7 @@ function exportMemory(models, options) {
|
|
|
1618
1796
|
return MEMORY_OVERHEAD + triangles * perTriangle[options.format] + models.length * 4096;
|
|
1619
1797
|
}
|
|
1620
1798
|
|
|
1621
|
-
// src/
|
|
1799
|
+
// src/io.js
|
|
1622
1800
|
async function readSource(source, limit, progress = () => {
|
|
1623
1801
|
}, signal) {
|
|
1624
1802
|
let stream, total = 0;
|
|
@@ -1680,7 +1858,7 @@ async function readSource(source, limit, progress = () => {
|
|
|
1680
1858
|
return data.buffer;
|
|
1681
1859
|
}
|
|
1682
1860
|
|
|
1683
|
-
// src/
|
|
1861
|
+
// src/three-bridge.js
|
|
1684
1862
|
function fromThreeMesh(mesh, limits) {
|
|
1685
1863
|
if (!mesh?.isMesh && !mesh?.geometry)
|
|
1686
1864
|
throw new Error("Expected a Three.js mesh");
|
|
@@ -1753,7 +1931,7 @@ function toThreeMesh(model, THREE) {
|
|
|
1753
1931
|
return mesh;
|
|
1754
1932
|
}
|
|
1755
1933
|
|
|
1756
|
-
// src/
|
|
1934
|
+
// src/viewer.js
|
|
1757
1935
|
var transformKeys = [
|
|
1758
1936
|
"x",
|
|
1759
1937
|
"y",
|
|
@@ -1791,7 +1969,8 @@ var modelKeys = [
|
|
|
1791
1969
|
"smooth_shading",
|
|
1792
1970
|
"section",
|
|
1793
1971
|
"units",
|
|
1794
|
-
"animation"
|
|
1972
|
+
"animation",
|
|
1973
|
+
"animation_state"
|
|
1795
1974
|
];
|
|
1796
1975
|
var finite = (v, name) => {
|
|
1797
1976
|
if (typeof v !== "number" || !Number.isFinite(v))
|
|
@@ -1823,7 +2002,7 @@ function modelOptions(input) {
|
|
|
1823
2002
|
return out;
|
|
1824
2003
|
}
|
|
1825
2004
|
var StlViewer = class _StlViewer {
|
|
1826
|
-
static version = "2.0.0-rc.
|
|
2005
|
+
static version = "2.0.0-rc.5";
|
|
1827
2006
|
static workerURL = null;
|
|
1828
2007
|
constructor(parent, options = {}) {
|
|
1829
2008
|
if (!parent?.appendChild)
|
|
@@ -1873,7 +2052,9 @@ var StlViewer = class _StlViewer {
|
|
|
1873
2052
|
"model_loaded_callback",
|
|
1874
2053
|
"load_error_callback",
|
|
1875
2054
|
"loading_progress_callback",
|
|
1876
|
-
"on_model_drop"
|
|
2055
|
+
"on_model_drop",
|
|
2056
|
+
"animation_state_callback",
|
|
2057
|
+
"animation_complete_callback"
|
|
1877
2058
|
])
|
|
1878
2059
|
this[name] = options[name] ?? null;
|
|
1879
2060
|
this.onmousedown_callback = options.on_model_mousedown ?? options.on_model_mouseclick ?? null;
|
|
@@ -1912,11 +2093,15 @@ var StlViewer = class _StlViewer {
|
|
|
1912
2093
|
this.parent_element.appendChild(this.canvas);
|
|
1913
2094
|
this.renderer = new Renderer(
|
|
1914
2095
|
this.canvas,
|
|
1915
|
-
() =>
|
|
1916
|
-
|
|
1917
|
-
|
|
2096
|
+
() => {
|
|
2097
|
+
this._suspendAnimations(true);
|
|
2098
|
+
this._message(
|
|
2099
|
+
"Graphics interrupted. Waiting for the browser to restore rendering\u2026"
|
|
2100
|
+
);
|
|
2101
|
+
},
|
|
1918
2102
|
() => {
|
|
1919
2103
|
this._removeMessage();
|
|
2104
|
+
this._suspendAnimations(this.canvas.ownerDocument.hidden);
|
|
1920
2105
|
this._invalidate();
|
|
1921
2106
|
},
|
|
1922
2107
|
(e) => this._error(e)
|
|
@@ -1981,6 +2166,14 @@ var StlViewer = class _StlViewer {
|
|
|
1981
2166
|
}
|
|
1982
2167
|
};
|
|
1983
2168
|
this.canvas.addEventListener("keydown", this._key);
|
|
2169
|
+
this._visibility = () => this._suspendAnimations(
|
|
2170
|
+
this.canvas.ownerDocument.hidden || this.renderer.lost
|
|
2171
|
+
);
|
|
2172
|
+
this.canvas.ownerDocument.addEventListener(
|
|
2173
|
+
"visibilitychange",
|
|
2174
|
+
this._visibility
|
|
2175
|
+
);
|
|
2176
|
+
this._animationSuspended = this.canvas.ownerDocument.hidden;
|
|
1984
2177
|
this.ready = true;
|
|
1985
2178
|
this._readyResolve(this);
|
|
1986
2179
|
this._callback("ready_callback");
|
|
@@ -1995,6 +2188,11 @@ var StlViewer = class _StlViewer {
|
|
|
1995
2188
|
this.controls?.dispose();
|
|
1996
2189
|
this.renderer?.dispose();
|
|
1997
2190
|
this._resizeObserver?.disconnect();
|
|
2191
|
+
if (this._visibility)
|
|
2192
|
+
this.canvas.ownerDocument.removeEventListener(
|
|
2193
|
+
"visibilitychange",
|
|
2194
|
+
this._visibility
|
|
2195
|
+
);
|
|
1998
2196
|
}
|
|
1999
2197
|
}
|
|
2000
2198
|
_callback(name, ...args) {
|
|
@@ -2043,7 +2241,7 @@ var StlViewer = class _StlViewer {
|
|
|
2043
2241
|
const diagnostic = this.renderer?.diagnostic;
|
|
2044
2242
|
if (diagnostic && printSignature(diagnostic.model) !== diagnostic.signature)
|
|
2045
2243
|
this.renderer.setDiagnostic(null);
|
|
2046
|
-
if (this.killsign || !this.renderer || this.renderer.lost || this._frame)
|
|
2244
|
+
if (this.killsign || !this.renderer || this.renderer.lost || this._frame || this._animationSuspended)
|
|
2047
2245
|
return;
|
|
2048
2246
|
this._frame = requestAnimationFrame((t) => {
|
|
2049
2247
|
this._frame = 0;
|
|
@@ -2055,12 +2253,15 @@ var StlViewer = class _StlViewer {
|
|
|
2055
2253
|
this._lastTime = time;
|
|
2056
2254
|
let animated = false;
|
|
2057
2255
|
for (const m of this._models.values())
|
|
2058
|
-
if (m.geometry && m._animation) {
|
|
2256
|
+
if (m.geometry && !m._sceneImportPending && m._animation && !m._animation.paused) {
|
|
2059
2257
|
this._animate(m, time);
|
|
2060
|
-
animated ||= !!m._animation;
|
|
2258
|
+
animated ||= !!m._animation && !m._animation.paused;
|
|
2061
2259
|
}
|
|
2062
2260
|
if (this.auto_rotate && this.controls.enabled)
|
|
2063
2261
|
this.controls.orbit(elapsed * 0.3, 0);
|
|
2262
|
+
const diagnostic = this.renderer.diagnostic;
|
|
2263
|
+
if (diagnostic && printSignature(diagnostic.model) !== diagnostic.signature)
|
|
2264
|
+
this.renderer.setDiagnostic(null);
|
|
2064
2265
|
this._clip();
|
|
2065
2266
|
try {
|
|
2066
2267
|
this.renderer.draw(
|
|
@@ -2184,6 +2385,11 @@ var StlViewer = class _StlViewer {
|
|
|
2184
2385
|
m.color = supplied.color ?? (supplied.mesh?.material?.color?.getHexString ? "#" + supplied.mesh.material.color.getHexString() : null);
|
|
2185
2386
|
if (m.color && m.color !== "transparent") colorRGB(m.color);
|
|
2186
2387
|
m.view_edges = !!supplied.view_edges;
|
|
2388
|
+
m.animation = supplied.animation == null ? null : normalizeAnimation(supplied.animation);
|
|
2389
|
+
m.animation_state = supplied.animation_state == null ? null : normalizePlaybackState(supplied.animation_state);
|
|
2390
|
+
if (m.animation_state && !m.animation)
|
|
2391
|
+
throw new Error("Playback state needs an animation");
|
|
2392
|
+
if (m.animation) createPlayback(m.animation, m, m.animation_state);
|
|
2187
2393
|
Object.defineProperty(m, "scaleVector", {
|
|
2188
2394
|
get: () => [
|
|
2189
2395
|
m.scalex * m.unitScale,
|
|
@@ -2336,7 +2542,10 @@ var StlViewer = class _StlViewer {
|
|
|
2336
2542
|
);
|
|
2337
2543
|
if ((supplied.mesh || supplied.data) && (features.smooth || features.edges || features.wire))
|
|
2338
2544
|
await this._derive(m, features);
|
|
2339
|
-
if (
|
|
2545
|
+
if (m.animation_state) m.center = [...m.animation_state.center];
|
|
2546
|
+
if (m.animation && !m._animation)
|
|
2547
|
+
this._startAnimation(m, m.animation, m.animation_state);
|
|
2548
|
+
delete m.animation_state;
|
|
2340
2549
|
delete m.mesh;
|
|
2341
2550
|
delete m.data;
|
|
2342
2551
|
this._invalidate();
|
|
@@ -2358,6 +2567,7 @@ var StlViewer = class _StlViewer {
|
|
|
2358
2567
|
this._invalidate();
|
|
2359
2568
|
}
|
|
2360
2569
|
_deleteModel(m) {
|
|
2570
|
+
this._stopAnimation(m, "removed");
|
|
2361
2571
|
for (const [controller, models] of this._printJobs || [])
|
|
2362
2572
|
if (models.has(m)) controller.abort();
|
|
2363
2573
|
this._printCache = null;
|
|
@@ -2388,6 +2598,7 @@ var StlViewer = class _StlViewer {
|
|
|
2388
2598
|
this._pool?.clear();
|
|
2389
2599
|
for (const c of this._aborters) c.abort();
|
|
2390
2600
|
this._aborters.clear();
|
|
2601
|
+
for (const m of this._models.values()) this._stopAnimation(m, "clean");
|
|
2391
2602
|
this._models.clear();
|
|
2392
2603
|
this._pending.clear();
|
|
2393
2604
|
this._jobs.clear();
|
|
@@ -2419,6 +2630,11 @@ var StlViewer = class _StlViewer {
|
|
|
2419
2630
|
window.removeEventListener("resize", this._windowResize);
|
|
2420
2631
|
this.renderer?.dispose();
|
|
2421
2632
|
if (this.canvas) {
|
|
2633
|
+
if (this._visibility)
|
|
2634
|
+
this.canvas.ownerDocument.removeEventListener(
|
|
2635
|
+
"visibilitychange",
|
|
2636
|
+
this._visibility
|
|
2637
|
+
);
|
|
2422
2638
|
this.canvas.removeEventListener("drop", this._drop);
|
|
2423
2639
|
this.canvas.removeEventListener("dragover", this._drag);
|
|
2424
2640
|
this.canvas.removeEventListener("keydown", this._key);
|
|
@@ -2557,6 +2773,10 @@ var StlViewer = class _StlViewer {
|
|
|
2557
2773
|
set_position(id, x, y, z) {
|
|
2558
2774
|
return this._guard(() => {
|
|
2559
2775
|
const m = this._model(id);
|
|
2776
|
+
[x, y, z].forEach((v) => {
|
|
2777
|
+
if (v !== void 0) finite(v, "position");
|
|
2778
|
+
});
|
|
2779
|
+
this._stopAnimation(m, "manual");
|
|
2560
2780
|
[x, y, z].forEach((v, i) => {
|
|
2561
2781
|
if (v !== void 0) m[["x", "y", "z"][i]] = finite(v, "position");
|
|
2562
2782
|
});
|
|
@@ -2570,8 +2790,12 @@ var StlViewer = class _StlViewer {
|
|
|
2570
2790
|
if (keepPrevious) {
|
|
2571
2791
|
if (x / m.scalex !== y / m.scaley || x / m.scalex !== z / m.scalez)
|
|
2572
2792
|
throw new Error("keep_prev_scale requires a uniform geometry factor");
|
|
2793
|
+
this._stopAnimation(m, "manual");
|
|
2573
2794
|
m.unitScale *= x / m.scalex;
|
|
2574
|
-
} else
|
|
2795
|
+
} else {
|
|
2796
|
+
this._stopAnimation(m, "manual");
|
|
2797
|
+
[m.scalex, m.scaley, m.scalez] = [x, y, z];
|
|
2798
|
+
}
|
|
2575
2799
|
m._stats = null;
|
|
2576
2800
|
this._invalidate();
|
|
2577
2801
|
});
|
|
@@ -2585,6 +2809,10 @@ var StlViewer = class _StlViewer {
|
|
|
2585
2809
|
set_rotation(id, x, y, z, increment = false) {
|
|
2586
2810
|
return this._guard(() => {
|
|
2587
2811
|
const m = this._model(id);
|
|
2812
|
+
[x, y, z].forEach((v) => {
|
|
2813
|
+
if (v !== void 0) finite(v, "rotation");
|
|
2814
|
+
});
|
|
2815
|
+
this._stopAnimation(m, "manual");
|
|
2588
2816
|
[x, y, z].forEach((v, i) => {
|
|
2589
2817
|
const k = ["rotationx", "rotationy", "rotationz"][i];
|
|
2590
2818
|
if (v !== void 0)
|
|
@@ -2638,6 +2866,7 @@ var StlViewer = class _StlViewer {
|
|
|
2638
2866
|
if (!["mm", "inch"].includes(units))
|
|
2639
2867
|
throw new Error("Units must be mm or inch");
|
|
2640
2868
|
const m = this._model(id);
|
|
2869
|
+
this._stopAnimation(m, "manual");
|
|
2641
2870
|
if (scale && m.units !== units)
|
|
2642
2871
|
m.unitScale *= units === "inch" ? 25.4 : 1 / 25.4;
|
|
2643
2872
|
m.units = units;
|
|
@@ -3027,6 +3256,14 @@ var StlViewer = class _StlViewer {
|
|
|
3027
3256
|
this._exportBytes += working;
|
|
3028
3257
|
let job, cancel, callbackError;
|
|
3029
3258
|
try {
|
|
3259
|
+
const snapshot = format === "vsb" ? models.map((m, index) => ({
|
|
3260
|
+
id: m.id,
|
|
3261
|
+
triangles: m.geometry.triangles,
|
|
3262
|
+
positions: m.geometry.positions.slice(),
|
|
3263
|
+
colors: m.geometry.colors?.slice() || null,
|
|
3264
|
+
scale: scene.models[index].animation_state ? [1, 1, 1] : m.scaleVector,
|
|
3265
|
+
center: scene.models[index].animation_state ? [0, 0, 0] : [...m.center]
|
|
3266
|
+
})) : null;
|
|
3030
3267
|
options.on_progress?.({
|
|
3031
3268
|
loaded: 0,
|
|
3032
3269
|
total: models.length,
|
|
@@ -3042,14 +3279,6 @@ var StlViewer = class _StlViewer {
|
|
|
3042
3279
|
phase: "complete"
|
|
3043
3280
|
});
|
|
3044
3281
|
} else {
|
|
3045
|
-
const snapshot = models.map((m) => ({
|
|
3046
|
-
id: m.id,
|
|
3047
|
-
triangles: m.geometry.triangles,
|
|
3048
|
-
positions: m.geometry.positions.slice(),
|
|
3049
|
-
colors: m.geometry.colors?.slice() || null,
|
|
3050
|
-
scale: m.scaleVector,
|
|
3051
|
-
center: [...m.center]
|
|
3052
|
-
}));
|
|
3053
3282
|
job = this._pool.run(
|
|
3054
3283
|
{
|
|
3055
3284
|
kind: "export-scene",
|
|
@@ -3135,13 +3364,21 @@ var StlViewer = class _StlViewer {
|
|
|
3135
3364
|
model.orig_filename = basename(m.orig_filename || m.filename);
|
|
3136
3365
|
for (const key of transformKeys)
|
|
3137
3366
|
if (!key.startsWith("scale")) model[key] = m[key];
|
|
3138
|
-
if (!for_vsb) {
|
|
3367
|
+
if (!for_vsb || m._animation) {
|
|
3139
3368
|
model.scalex = m.scalex * m.unitScale;
|
|
3140
3369
|
model.scaley = m.scaley * m.unitScale;
|
|
3141
3370
|
model.scalez = m.scalez * m.unitScale;
|
|
3142
3371
|
}
|
|
3143
3372
|
if (m.color) model.color = m.color;
|
|
3144
|
-
|
|
3373
|
+
const playback = savePlayback(m);
|
|
3374
|
+
if (playback) Object.assign(model, playback);
|
|
3375
|
+
else if (!for_vsb) {
|
|
3376
|
+
const center = this.center_models ? m.geometry.bounds.min.map(
|
|
3377
|
+
(v, i) => (v + m.geometry.bounds.max[i]) / 2
|
|
3378
|
+
) : [0, 0, 0];
|
|
3379
|
+
const position = transform(modelMatrix(m), center);
|
|
3380
|
+
["x", "y", "z"].forEach((axis, i) => model[axis] = position[i]);
|
|
3381
|
+
}
|
|
3145
3382
|
data.models.push(model);
|
|
3146
3383
|
}
|
|
3147
3384
|
return as_js_obj ? data : JSON.stringify(data);
|
|
@@ -3155,7 +3392,10 @@ var StlViewer = class _StlViewer {
|
|
|
3155
3392
|
get_vsb() {
|
|
3156
3393
|
return {
|
|
3157
3394
|
vsj: this.get_vsj(true, true, true),
|
|
3158
|
-
files: [...this._models.values()].filter((m) => m.geometry).map((m) => ({
|
|
3395
|
+
files: [...this._models.values()].filter((m) => m.geometry).map((m) => ({
|
|
3396
|
+
id: m.id,
|
|
3397
|
+
bin: m._animation ? exportSTL(m.geometry, [1, 1, 1], [0, 0, 0]) : this.get_stl_bin(m.id)
|
|
3398
|
+
}))
|
|
3159
3399
|
};
|
|
3160
3400
|
}
|
|
3161
3401
|
async get_vsb_blob() {
|
|
@@ -3236,20 +3476,57 @@ var StlViewer = class _StlViewer {
|
|
|
3236
3476
|
);
|
|
3237
3477
|
else if (scene.grid) this.set_grid(true);
|
|
3238
3478
|
if (scene.camera_state) this._restoreCamera = scene.camera_state;
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3479
|
+
const incoming = [];
|
|
3480
|
+
try {
|
|
3481
|
+
for (const input of scene.models) {
|
|
3482
|
+
if (!input || typeof input !== "object")
|
|
3483
|
+
throw new Error("Invalid scene model");
|
|
3484
|
+
const model = modelOptions(input);
|
|
3485
|
+
delete model.mesh;
|
|
3486
|
+
delete model.data;
|
|
3487
|
+
delete model.local_file;
|
|
3488
|
+
if (files) {
|
|
3489
|
+
if (typeof model.filename !== "string" || !Object.hasOwn(files, model.filename))
|
|
3490
|
+
throw new Error("VSB model file is missing");
|
|
3491
|
+
model.local_file = new File(
|
|
3492
|
+
[files[model.filename]],
|
|
3493
|
+
model.filename
|
|
3494
|
+
);
|
|
3495
|
+
} else if (model.filename)
|
|
3496
|
+
model.filename = new URL(model.filename, base).href;
|
|
3497
|
+
const id = model.id === void 0 || model.id === -1 ? this.max_model_id + 1 : model.id;
|
|
3498
|
+
if (this._models.has(id))
|
|
3499
|
+
throw new Error(`Invalid or duplicate model id: ${id}`);
|
|
3500
|
+
this.add_model({
|
|
3501
|
+
...model,
|
|
3502
|
+
id,
|
|
3503
|
+
animation: null,
|
|
3504
|
+
animation_state: null
|
|
3505
|
+
});
|
|
3506
|
+
const m = this._models.get(id);
|
|
3507
|
+
if (!m) throw new Error(this.error || "Invalid scene model");
|
|
3508
|
+
m._sceneImportPending = true;
|
|
3509
|
+
incoming.push({ m, model, pending: this._pending.get(id) });
|
|
3510
|
+
}
|
|
3511
|
+
await Promise.all(incoming.map((item) => item.pending));
|
|
3512
|
+
if (generation !== this._generation || this.killsign) return;
|
|
3513
|
+
for (const { m, model } of incoming) {
|
|
3514
|
+
if (this._models.get(m.id) !== m || !m.geometry)
|
|
3515
|
+
throw new Error("A scene model could not be opened");
|
|
3516
|
+
if (model.animation)
|
|
3517
|
+
createPlayback(model.animation, m, model.animation_state);
|
|
3518
|
+
else if (model.animation_state)
|
|
3519
|
+
throw new Error("Playback state needs an animation");
|
|
3520
|
+
}
|
|
3521
|
+
for (const { m, model } of incoming) {
|
|
3522
|
+
if (model.animation)
|
|
3523
|
+
this._startAnimation(m, model.animation, model.animation_state);
|
|
3524
|
+
m._sceneImportPending = false;
|
|
3525
|
+
}
|
|
3526
|
+
} catch (error) {
|
|
3527
|
+
for (const { m } of incoming)
|
|
3528
|
+
if (this._models.get(m.id) === m) this.remove_model(m.id);
|
|
3529
|
+
throw error;
|
|
3253
3530
|
}
|
|
3254
3531
|
})().catch((e) => {
|
|
3255
3532
|
if (generation === this._generation && e.name !== "AbortError")
|
|
@@ -3271,56 +3548,101 @@ var StlViewer = class _StlViewer {
|
|
|
3271
3548
|
return this._guard(() => {
|
|
3272
3549
|
const m = this._model(id);
|
|
3273
3550
|
if (!animation) {
|
|
3274
|
-
m
|
|
3275
|
-
|
|
3551
|
+
this._stopAnimation(m, "stopped");
|
|
3552
|
+
this._invalidate();
|
|
3276
3553
|
return;
|
|
3277
3554
|
}
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3555
|
+
this._startAnimation(m, animation);
|
|
3556
|
+
this._invalidate();
|
|
3557
|
+
});
|
|
3558
|
+
}
|
|
3559
|
+
_startAnimation(m, definition, saved) {
|
|
3560
|
+
const playback = createPlayback(definition, m, saved);
|
|
3561
|
+
if (m._animation) this._stopAnimation(m, "replaced");
|
|
3562
|
+
if (saved) m.center = [...saved.center];
|
|
3563
|
+
m.animation = playback.animation;
|
|
3564
|
+
m._animation = playback;
|
|
3565
|
+
delete m.animation_state;
|
|
3566
|
+
m._animationResult = null;
|
|
3567
|
+
const running = evaluatePlayback(playback, m);
|
|
3568
|
+
if (running) this._animationEvent(m, saved ? "restored" : "started");
|
|
3569
|
+
else this._completeAnimation(m);
|
|
3570
|
+
}
|
|
3571
|
+
_animationEvent(m, reason) {
|
|
3572
|
+
const state = this._animationState(m);
|
|
3573
|
+
queueMicrotask(() => {
|
|
3574
|
+
this._callback("animation_state_callback", m.id, state, reason);
|
|
3575
|
+
if (reason === "completed")
|
|
3576
|
+
this._callback("animation_complete_callback", m.id, state);
|
|
3577
|
+
});
|
|
3578
|
+
}
|
|
3579
|
+
_animationState(m) {
|
|
3580
|
+
return m._animation ? playbackState(m._animation, !!this._animationSuspended) : {
|
|
3581
|
+
...m._animationResult ?? { status: "idle", elapsed_ms: 0 },
|
|
3582
|
+
suspended: false,
|
|
3583
|
+
tracks: []
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
_stopAnimation(m, reason) {
|
|
3587
|
+
if (!m._animation && !m.animation) return;
|
|
3588
|
+
m._animationResult = {
|
|
3589
|
+
status: "stopped",
|
|
3590
|
+
elapsed_ms: m._animation?.elapsed_ms ?? 0
|
|
3591
|
+
};
|
|
3592
|
+
m._animation = null;
|
|
3593
|
+
m.animation = null;
|
|
3594
|
+
delete m.animation_state;
|
|
3595
|
+
this._animationEvent(m, reason);
|
|
3596
|
+
}
|
|
3597
|
+
_completeAnimation(m) {
|
|
3598
|
+
m._animationResult = {
|
|
3599
|
+
status: "completed",
|
|
3600
|
+
elapsed_ms: m._animation.elapsed_ms
|
|
3601
|
+
};
|
|
3602
|
+
m._animation = null;
|
|
3603
|
+
m.animation = null;
|
|
3604
|
+
this._animationEvent(m, "completed");
|
|
3605
|
+
}
|
|
3606
|
+
pause_model_animation(id) {
|
|
3607
|
+
return this._guard(() => {
|
|
3608
|
+
const m = this._model(id);
|
|
3609
|
+
if (!m._animation || m._animation.paused) return;
|
|
3610
|
+
m._animation.paused = true;
|
|
3611
|
+
m._animation.last_time = null;
|
|
3612
|
+
this._animationEvent(m, "paused");
|
|
3613
|
+
this._invalidate();
|
|
3614
|
+
});
|
|
3615
|
+
}
|
|
3616
|
+
resume_model_animation(id) {
|
|
3617
|
+
return this._guard(() => {
|
|
3618
|
+
const m = this._model(id);
|
|
3619
|
+
if (!m._animation || !m._animation.paused) return;
|
|
3620
|
+
m._animation.paused = false;
|
|
3621
|
+
m._animation.last_time = null;
|
|
3622
|
+
this._animationEvent(m, "resumed");
|
|
3304
3623
|
this._invalidate();
|
|
3305
3624
|
});
|
|
3306
3625
|
}
|
|
3626
|
+
get_model_animation_state(id) {
|
|
3627
|
+
return this._guard(() => this._animationState(this._model(id)));
|
|
3628
|
+
}
|
|
3629
|
+
_suspendAnimations(suspended) {
|
|
3630
|
+
this._animationSuspended = !!suspended;
|
|
3631
|
+
this._lastTime = 0;
|
|
3632
|
+
for (const m of this._models.values())
|
|
3633
|
+
if (m._animation) m._animation.last_time = null;
|
|
3634
|
+
if (suspended) {
|
|
3635
|
+
cancelAnimationFrame(this._frame);
|
|
3636
|
+
this._frame = 0;
|
|
3637
|
+
} else this._invalidate();
|
|
3638
|
+
}
|
|
3307
3639
|
_animate(m, time) {
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
for (const [k, v] of Object.entries(t.values))
|
|
3315
|
-
m[k] = k.startsWith("scale") ? Math.max(
|
|
3316
|
-
1e-6,
|
|
3317
|
-
t.mode === "delta" ? t.from[k] + v * p : t.from[k] + (v - t.from[k]) * p
|
|
3318
|
-
) : t.mode === "delta" ? t.from[k] + v * p : t.from[k] + (v - t.from[k]) * p;
|
|
3319
|
-
}
|
|
3320
|
-
m._stats = null;
|
|
3321
|
-
if (!running) {
|
|
3322
|
-
m._animation = null;
|
|
3323
|
-
m.animation = null;
|
|
3640
|
+
if (this._animationSuspended || !m._animation) return;
|
|
3641
|
+
try {
|
|
3642
|
+
if (!advancePlayback(m._animation, m, time)) this._completeAnimation(m);
|
|
3643
|
+
} catch (error) {
|
|
3644
|
+
this._stopAnimation(m, "error");
|
|
3645
|
+
this._error(error);
|
|
3324
3646
|
}
|
|
3325
3647
|
}
|
|
3326
3648
|
async get_image_blob(type = "image/png") {
|
|
@@ -3343,7 +3665,7 @@ var StlViewer = class _StlViewer {
|
|
|
3343
3665
|
}
|
|
3344
3666
|
};
|
|
3345
3667
|
|
|
3346
|
-
// src/
|
|
3668
|
+
// src/memory-policy.js
|
|
3347
3669
|
var GiB = 1024 ** 3;
|
|
3348
3670
|
var MiB = 1024 ** 2;
|
|
3349
3671
|
function memoryPolicy(reportedGiB) {
|
|
@@ -3373,10 +3695,10 @@ async function estimateFile(file) {
|
|
|
3373
3695
|
return 64 * MiB + (file.size || 0) * 3;
|
|
3374
3696
|
}
|
|
3375
3697
|
|
|
3376
|
-
// src/
|
|
3698
|
+
// src/model-transform.js
|
|
3377
3699
|
var axes = ["x", "y", "z"];
|
|
3378
3700
|
var vector2 = (p) => axes.map((a) => p[a]);
|
|
3379
|
-
var
|
|
3701
|
+
var object2 = (p) => Object.fromEntries(axes.map((a, i) => [a, p[i]]));
|
|
3380
3702
|
var dot2 = (a, b) => a.reduce((sum, v, i) => sum + v * b[i], 0);
|
|
3381
3703
|
var norm2 = (a) => {
|
|
3382
3704
|
const n = Math.hypot(...a) || 1;
|
|
@@ -3407,7 +3729,7 @@ function rotationMatrix(degrees = { x: 0, y: 0, z: 0 }) {
|
|
|
3407
3729
|
}
|
|
3408
3730
|
var centerOf = (bounds) => bounds.min.map((v, i) => (v + bounds.max[i]) / 2);
|
|
3409
3731
|
function modelCenter(model) {
|
|
3410
|
-
return
|
|
3732
|
+
return object2(
|
|
3411
3733
|
centerOf(model.bounds).map((v, i) => v + model.position[axes[i]])
|
|
3412
3734
|
);
|
|
3413
3735
|
}
|
|
@@ -3419,7 +3741,7 @@ function modelRadius(model) {
|
|
|
3419
3741
|
}
|
|
3420
3742
|
function enginePosition(model) {
|
|
3421
3743
|
const c = centerOf(model.bounds), rotated = transform2(rotationMatrix(model.rotation), c);
|
|
3422
|
-
return
|
|
3744
|
+
return object2(c.map((v, i) => model.position[axes[i]] + v - rotated[i]));
|
|
3423
3745
|
}
|
|
3424
3746
|
function modelBounds(model) {
|
|
3425
3747
|
if (!model.rotation || !Object.values(model.rotation).some((v) => v % 360))
|
|
@@ -3472,12 +3794,12 @@ function depthPosition(model, camera, amount) {
|
|
|
3472
3794
|
const clearance = Math.max(model.radius || 1e-6, 1e-6) * 1.1 / Math.max(0.05, dot2(direction, forward));
|
|
3473
3795
|
const desired = (distance || clearance) * Math.exp(Math.max(-6, Math.min(6, amount)));
|
|
3474
3796
|
const next = amount < 0 ? Math.max(Math.min(distance, clearance), desired) : Math.max(distance, Math.min(1e12, desired));
|
|
3475
|
-
return
|
|
3797
|
+
return object2(
|
|
3476
3798
|
vector2(model.position).map((v, i) => v + direction[i] * (next - distance))
|
|
3477
3799
|
);
|
|
3478
3800
|
}
|
|
3479
3801
|
|
|
3480
|
-
// src/
|
|
3802
|
+
// src/scene-layout.js
|
|
3481
3803
|
function scenePlacement(models, mode) {
|
|
3482
3804
|
if (mode === "custom")
|
|
3483
3805
|
return models.map((m) => ({
|
|
@@ -3515,7 +3837,7 @@ function sceneFloor(models, placement) {
|
|
|
3515
3837
|
) : 0;
|
|
3516
3838
|
}
|
|
3517
3839
|
|
|
3518
|
-
// src/
|
|
3840
|
+
// src/model-move.js
|
|
3519
3841
|
var vector3 = (p) => [p.x, p.y, p.z];
|
|
3520
3842
|
var sub2 = (a, b) => a.map((v, i) => v - b[i]);
|
|
3521
3843
|
var dot3 = (a, b) => a.reduce((n, v, i) => n + v * b[i], 0);
|
|
@@ -3681,7 +4003,7 @@ function bindModelMove(canvas, {
|
|
|
3681
4003
|
};
|
|
3682
4004
|
}
|
|
3683
4005
|
|
|
3684
|
-
// src/
|
|
4006
|
+
// src/drop-source.js
|
|
3685
4007
|
function dropKind(transfer) {
|
|
3686
4008
|
const types = Array.from(transfer?.types || [], (type) => type.toLowerCase());
|
|
3687
4009
|
if (types.includes("files") || transfer?.files?.length) return "files";
|
|
@@ -3721,7 +4043,7 @@ function droppedURLs(transfer) {
|
|
|
3721
4043
|
return [...urls];
|
|
3722
4044
|
}
|
|
3723
4045
|
|
|
3724
|
-
// src/
|
|
4046
|
+
// src/app-export.js
|
|
3725
4047
|
var exportControls = [
|
|
3726
4048
|
"save-button",
|
|
3727
4049
|
"save-dialog",
|
|
@@ -3891,7 +4213,7 @@ function bindAppExport({
|
|
|
3891
4213
|
};
|
|
3892
4214
|
}
|
|
3893
4215
|
|
|
3894
|
-
// src/
|
|
4216
|
+
// src/app-print.js
|
|
3895
4217
|
var printControls = [
|
|
3896
4218
|
"print-button",
|
|
3897
4219
|
"print-panel",
|
|
@@ -3925,6 +4247,7 @@ function bindAppPrint({
|
|
|
3925
4247
|
}) {
|
|
3926
4248
|
let job = null, report = null, timer = null, disposed = false, scopeKey = "", previousScope = "", unavailable = false;
|
|
3927
4249
|
const panel = $("print-panel"), document2 = panel.ownerDocument;
|
|
4250
|
+
const pausedForCheck = /* @__PURE__ */ new Set();
|
|
3928
4251
|
const summary = {
|
|
3929
4252
|
issues_found: "Issues found",
|
|
3930
4253
|
incomplete: "Check incomplete",
|
|
@@ -3946,6 +4269,15 @@ function bindAppPrint({
|
|
|
3946
4269
|
const s = getState();
|
|
3947
4270
|
$("print-button").disabled = s.busy || !s.models.length;
|
|
3948
4271
|
if (panel.hidden) return;
|
|
4272
|
+
for (const id of pausedForCheck)
|
|
4273
|
+
if (!s.models.some((m) => m.id === id) || getViewer()?.get_model_animation_state(id)?.status !== "paused")
|
|
4274
|
+
pausedForCheck.delete(id);
|
|
4275
|
+
if ($("print-resume")) {
|
|
4276
|
+
$("print-resume").hidden = !pausedForCheck.size;
|
|
4277
|
+
$("print-resume").disabled = !!job || s.busy;
|
|
4278
|
+
}
|
|
4279
|
+
if ($("print-animation-note"))
|
|
4280
|
+
$("print-animation-note").hidden = !pausedForCheck.size;
|
|
3949
4281
|
const wasUnavailable = unavailable;
|
|
3950
4282
|
unavailable = s.busy || !s.models.length;
|
|
3951
4283
|
$("print-run").disabled = !!job || unavailable;
|
|
@@ -4092,6 +4424,19 @@ function bindAppPrint({
|
|
|
4092
4424
|
listen($("print-fields"), "change", changed);
|
|
4093
4425
|
listen($("print-clear"), "click", clear);
|
|
4094
4426
|
listen($("print-cancel"), "click", cancel);
|
|
4427
|
+
if ($("print-resume"))
|
|
4428
|
+
listen($("print-resume"), "click", () => {
|
|
4429
|
+
if (job || getState().busy) return;
|
|
4430
|
+
for (const id of pausedForCheck)
|
|
4431
|
+
if (getState().models.some((m) => m.id === id) && getViewer().get_model_animation_state(id)?.status === "paused")
|
|
4432
|
+
getViewer().resume_model_animation(id);
|
|
4433
|
+
pausedForCheck.clear();
|
|
4434
|
+
report = null;
|
|
4435
|
+
clear();
|
|
4436
|
+
$("print-results").replaceChildren();
|
|
4437
|
+
$("print-status").textContent = "Animation resumed. Pause and run the check again for the new pose.";
|
|
4438
|
+
sync();
|
|
4439
|
+
});
|
|
4095
4440
|
listen($("print-results"), "click", (e) => {
|
|
4096
4441
|
const b = e.target.closest("button[data-issue]");
|
|
4097
4442
|
if (!b || !report) return;
|
|
@@ -4135,6 +4480,13 @@ function bindAppPrint({
|
|
|
4135
4480
|
}
|
|
4136
4481
|
}
|
|
4137
4482
|
scopeKey = key();
|
|
4483
|
+
for (const id of stateIds()) {
|
|
4484
|
+
if (getViewer().get_model_animation_state(id)?.status === "running") {
|
|
4485
|
+
getViewer().pause_model_animation(id);
|
|
4486
|
+
pausedForCheck.add(id);
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
sync();
|
|
4138
4490
|
const result = await getViewer().check_printability({
|
|
4139
4491
|
model_ids: stateIds(),
|
|
4140
4492
|
level: $("print-level").value,
|
|
@@ -4172,11 +4524,216 @@ function bindAppPrint({
|
|
|
4172
4524
|
disposed = true;
|
|
4173
4525
|
close(false);
|
|
4174
4526
|
report = null;
|
|
4527
|
+
pausedForCheck.clear();
|
|
4175
4528
|
}
|
|
4176
4529
|
};
|
|
4177
4530
|
}
|
|
4178
4531
|
|
|
4179
|
-
// src/
|
|
4532
|
+
// src/app-animation.js
|
|
4533
|
+
var animationControls = [
|
|
4534
|
+
"animation-details",
|
|
4535
|
+
"animation-fields",
|
|
4536
|
+
"animation-axis",
|
|
4537
|
+
"animation-seconds",
|
|
4538
|
+
"animation-direction",
|
|
4539
|
+
"animation-play",
|
|
4540
|
+
"animation-pause",
|
|
4541
|
+
"animation-stop",
|
|
4542
|
+
"animation-status",
|
|
4543
|
+
"animation-error"
|
|
4544
|
+
];
|
|
4545
|
+
var axes2 = ["x", "y", "z"];
|
|
4546
|
+
function syncAnimatedRecord(record, model) {
|
|
4547
|
+
if (!record || !model?.geometry) return;
|
|
4548
|
+
record.bounds = Object.fromEntries(
|
|
4549
|
+
["min", "max"].map((key) => [
|
|
4550
|
+
key,
|
|
4551
|
+
model.geometry.bounds[key].map(
|
|
4552
|
+
(v, i) => (v - model.center[i]) * model.scaleVector[i]
|
|
4553
|
+
)
|
|
4554
|
+
])
|
|
4555
|
+
);
|
|
4556
|
+
record.rotation = Object.fromEntries(
|
|
4557
|
+
axes2.map((a) => [a, model["rotation" + a] * 180 / Math.PI % 360])
|
|
4558
|
+
);
|
|
4559
|
+
record.position = { x: 0, y: 0, z: 0 };
|
|
4560
|
+
const offset = enginePosition(record);
|
|
4561
|
+
for (const a of axes2) record.position[a] = model[a] - offset[a];
|
|
4562
|
+
}
|
|
4563
|
+
function centerAnimationPivot(record, model) {
|
|
4564
|
+
syncAnimatedRecord(record, model);
|
|
4565
|
+
const center = model.geometry.bounds.min.map(
|
|
4566
|
+
(v, i) => (v + model.geometry.bounds.max[i]) / 2
|
|
4567
|
+
);
|
|
4568
|
+
const delta = center.map(
|
|
4569
|
+
(v, i) => (v - model.center[i]) * model.scaleVector[i]
|
|
4570
|
+
);
|
|
4571
|
+
const rotation2 = rotationMatrix(record.rotation);
|
|
4572
|
+
const original = record.originalPosition || { x: 0, y: 0, z: 0 };
|
|
4573
|
+
record.originalPosition = Object.fromEntries(
|
|
4574
|
+
axes2.map((a, i) => [a, original[a] + delta[i]])
|
|
4575
|
+
);
|
|
4576
|
+
for (let i = 0; i < 3; i++)
|
|
4577
|
+
model[axes2[i]] += delta.reduce(
|
|
4578
|
+
(sum, v, j) => sum + rotation2[i * 3 + j] * v,
|
|
4579
|
+
0
|
|
4580
|
+
);
|
|
4581
|
+
model.center = center;
|
|
4582
|
+
record.animationPivot = true;
|
|
4583
|
+
syncAnimatedRecord(record, model);
|
|
4584
|
+
}
|
|
4585
|
+
function spinDefinition(axis, seconds, direction) {
|
|
4586
|
+
if (!axes2.includes(axis) || !Number.isFinite(seconds) || seconds < 0.1 || seconds > 3600 || !["1", "-1"].includes(direction))
|
|
4587
|
+
throw new Error("Choose an axis and a duration from 0.1 to 3,600 seconds.");
|
|
4588
|
+
return {
|
|
4589
|
+
delta: {
|
|
4590
|
+
["rotation" + axis]: 2 * Math.PI * Number(direction),
|
|
4591
|
+
msec: seconds * 1e3,
|
|
4592
|
+
loop: true
|
|
4593
|
+
}
|
|
4594
|
+
};
|
|
4595
|
+
}
|
|
4596
|
+
function bindAppAnimation({
|
|
4597
|
+
$,
|
|
4598
|
+
listen,
|
|
4599
|
+
window: window2,
|
|
4600
|
+
getState,
|
|
4601
|
+
getViewer,
|
|
4602
|
+
beforePlay,
|
|
4603
|
+
refreshPose,
|
|
4604
|
+
announce
|
|
4605
|
+
}) {
|
|
4606
|
+
const settings = /* @__PURE__ */ new Map();
|
|
4607
|
+
let selected = null, timer = null, disposed = false;
|
|
4608
|
+
const state = () => {
|
|
4609
|
+
const s = getState();
|
|
4610
|
+
return {
|
|
4611
|
+
...s,
|
|
4612
|
+
playback: s.selectedId == null ? null : getViewer()?.get_model_animation_state(s.selectedId)
|
|
4613
|
+
};
|
|
4614
|
+
};
|
|
4615
|
+
function sync() {
|
|
4616
|
+
if (disposed) return;
|
|
4617
|
+
const s = state(), available = s.models.some((m) => m.id === s.selectedId);
|
|
4618
|
+
for (const id of settings.keys())
|
|
4619
|
+
if (!s.models.some((m) => m.id === id)) settings.delete(id);
|
|
4620
|
+
$("animation-details").hidden = !available;
|
|
4621
|
+
$("animation-fields").disabled = s.busy || !available;
|
|
4622
|
+
if (selected !== s.selectedId) {
|
|
4623
|
+
selected = s.selectedId;
|
|
4624
|
+
let value = settings.get(selected);
|
|
4625
|
+
if (!value) {
|
|
4626
|
+
value = { axis: "z", seconds: 5, direction: "1" };
|
|
4627
|
+
const a = getViewer()?._models.get(selected)?.animation;
|
|
4628
|
+
const entries = Object.entries(a?.delta || {}).filter(
|
|
4629
|
+
([k, v]) => k.startsWith("rotation") && v !== 0
|
|
4630
|
+
);
|
|
4631
|
+
if (a?.delta?.loop && !a.exact && entries.length === 1) {
|
|
4632
|
+
value = {
|
|
4633
|
+
axis: entries[0][0].slice(-1),
|
|
4634
|
+
seconds: a.delta.msec * 2 * Math.PI / Math.abs(entries[0][1]) / 1e3,
|
|
4635
|
+
direction: entries[0][1] < 0 ? "-1" : "1"
|
|
4636
|
+
};
|
|
4637
|
+
}
|
|
4638
|
+
settings.set(selected, value);
|
|
4639
|
+
}
|
|
4640
|
+
$("animation-axis").value = value.axis;
|
|
4641
|
+
$("animation-seconds").value = value.seconds;
|
|
4642
|
+
$("animation-direction").value = value.direction;
|
|
4643
|
+
$("animation-error").hidden = true;
|
|
4644
|
+
}
|
|
4645
|
+
const status = s.playback?.status || "idle", active = ["running", "paused"].includes(status);
|
|
4646
|
+
$("animation-play").textContent = active ? "Restart spin" : "Play";
|
|
4647
|
+
$("animation-pause").textContent = status === "paused" ? "Resume" : "Pause";
|
|
4648
|
+
$("animation-pause").disabled = !active;
|
|
4649
|
+
$("animation-stop").disabled = !active;
|
|
4650
|
+
const label = {
|
|
4651
|
+
idle: "Ready",
|
|
4652
|
+
running: "Playing",
|
|
4653
|
+
paused: "Paused",
|
|
4654
|
+
stopped: "Stopped",
|
|
4655
|
+
completed: "Finished"
|
|
4656
|
+
}[status];
|
|
4657
|
+
const text = `${label}. ${active ? "Stop keeps the current pose." : "Spin the selected model around its center."}`;
|
|
4658
|
+
if ($("animation-status").textContent !== text)
|
|
4659
|
+
$("animation-status").textContent = text;
|
|
4660
|
+
if (status === "running" && !timer)
|
|
4661
|
+
timer = window2.setInterval(() => {
|
|
4662
|
+
if (!disposed && !window2.document.hidden) refreshPose();
|
|
4663
|
+
}, 250);
|
|
4664
|
+
if (status !== "running" && timer) {
|
|
4665
|
+
window2.clearInterval(timer);
|
|
4666
|
+
timer = null;
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4669
|
+
function play(keepPaused = false) {
|
|
4670
|
+
const s = state();
|
|
4671
|
+
if (s.busy || s.selectedId == null) return;
|
|
4672
|
+
try {
|
|
4673
|
+
const value = {
|
|
4674
|
+
axis: $("animation-axis").value,
|
|
4675
|
+
seconds: $("animation-seconds").valueAsNumber,
|
|
4676
|
+
direction: $("animation-direction").value
|
|
4677
|
+
};
|
|
4678
|
+
const definition = spinDefinition(
|
|
4679
|
+
value.axis,
|
|
4680
|
+
value.seconds,
|
|
4681
|
+
value.direction
|
|
4682
|
+
);
|
|
4683
|
+
beforePlay(s.selectedId);
|
|
4684
|
+
getViewer().animate_model(s.selectedId, definition);
|
|
4685
|
+
if (keepPaused && s.playback?.status === "paused")
|
|
4686
|
+
getViewer().pause_model_animation(s.selectedId);
|
|
4687
|
+
settings.set(s.selectedId, value);
|
|
4688
|
+
$("animation-error").hidden = true;
|
|
4689
|
+
announce(
|
|
4690
|
+
keepPaused && s.playback?.status === "paused" ? "Spin updated. Still paused." : "Animation playing."
|
|
4691
|
+
);
|
|
4692
|
+
sync();
|
|
4693
|
+
} catch (error) {
|
|
4694
|
+
$("animation-error").textContent = error.message;
|
|
4695
|
+
$("animation-error").hidden = false;
|
|
4696
|
+
}
|
|
4697
|
+
}
|
|
4698
|
+
listen($("animation-play"), "click", () => play());
|
|
4699
|
+
listen($("animation-pause"), "click", () => {
|
|
4700
|
+
const s = state();
|
|
4701
|
+
if (s.busy || s.selectedId == null) return;
|
|
4702
|
+
getViewer()[s.playback?.status === "paused" ? "resume_model_animation" : "pause_model_animation"](s.selectedId);
|
|
4703
|
+
sync();
|
|
4704
|
+
});
|
|
4705
|
+
listen($("animation-stop"), "click", () => {
|
|
4706
|
+
const s = state();
|
|
4707
|
+
if (!s.busy && s.selectedId != null)
|
|
4708
|
+
getViewer().animate_model(s.selectedId, null);
|
|
4709
|
+
sync();
|
|
4710
|
+
});
|
|
4711
|
+
for (const id of [
|
|
4712
|
+
"animation-axis",
|
|
4713
|
+
"animation-seconds",
|
|
4714
|
+
"animation-direction"
|
|
4715
|
+
])
|
|
4716
|
+
listen($(id), "change", () => {
|
|
4717
|
+
if (["running", "paused"].includes(state().playback?.status)) play(true);
|
|
4718
|
+
else
|
|
4719
|
+
settings.set(selected, {
|
|
4720
|
+
axis: $("animation-axis").value,
|
|
4721
|
+
seconds: $("animation-seconds").valueAsNumber,
|
|
4722
|
+
direction: $("animation-direction").value
|
|
4723
|
+
});
|
|
4724
|
+
});
|
|
4725
|
+
sync();
|
|
4726
|
+
return {
|
|
4727
|
+
sync,
|
|
4728
|
+
dispose() {
|
|
4729
|
+
disposed = true;
|
|
4730
|
+
window2.clearInterval(timer);
|
|
4731
|
+
settings.clear();
|
|
4732
|
+
}
|
|
4733
|
+
};
|
|
4734
|
+
}
|
|
4735
|
+
|
|
4736
|
+
// src/app.js
|
|
4180
4737
|
var mountedRoots = /* @__PURE__ */ new WeakMap();
|
|
4181
4738
|
var requiredControls = [
|
|
4182
4739
|
"background-color",
|
|
@@ -4281,6 +4838,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4281
4838
|
missing.push(...exportControls.filter((id) => !$(id)));
|
|
4282
4839
|
if (printControls.some((id) => $(id)))
|
|
4283
4840
|
missing.push(...printControls.filter((id) => !$(id)));
|
|
4841
|
+
if (animationControls.some((id) => $(id)))
|
|
4842
|
+
missing.push(...animationControls.filter((id) => !$(id)));
|
|
4284
4843
|
if (missing.length)
|
|
4285
4844
|
throw new Error("Missing ViewSTL controls: " + missing.join(", "));
|
|
4286
4845
|
const abort = new window2.AbortController();
|
|
@@ -4308,6 +4867,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4308
4867
|
let viewerPromise, viewer, generation = 0, appearanceRequest = 0, modelID = null, nextID = 0, loadingID = null;
|
|
4309
4868
|
let exportUI;
|
|
4310
4869
|
let printUI;
|
|
4870
|
+
let animationUI;
|
|
4311
4871
|
let sceneImport;
|
|
4312
4872
|
let draining = false, busy = false, gridEnabled = false, gridHeight = 0, gridSpacing = 1, moveMode = false, transformTool = "move", modelMover, dragArrangement, background = "transparent";
|
|
4313
4873
|
const viewNames = {
|
|
@@ -4381,6 +4941,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4381
4941
|
$("background-value").textContent = background === "transparent" ? "Default" : background.toUpperCase();
|
|
4382
4942
|
}
|
|
4383
4943
|
function syncControls() {
|
|
4944
|
+
syncAnimatedRecord(records.get(modelID), viewer?._models.get(modelID));
|
|
4384
4945
|
const info = modelID === null ? null : viewer.get_model_info(modelID), mode = info?.display === "smooth" ? "flat" : info?.display || "flat";
|
|
4385
4946
|
for (const input of modeInputs) {
|
|
4386
4947
|
input.checked = input.value === mode;
|
|
@@ -4409,6 +4970,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4409
4970
|
syncPosition();
|
|
4410
4971
|
exportUI?.sync();
|
|
4411
4972
|
printUI?.sync();
|
|
4973
|
+
animationUI?.sync();
|
|
4412
4974
|
}
|
|
4413
4975
|
function populateInfo() {
|
|
4414
4976
|
if (modelID === null) return;
|
|
@@ -4488,6 +5050,10 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4488
5050
|
}
|
|
4489
5051
|
function arrangeScene() {
|
|
4490
5052
|
setMoveMode(false);
|
|
5053
|
+
for (const record of records.values())
|
|
5054
|
+
syncAnimatedRecord(record, viewer?._models.get(record.id));
|
|
5055
|
+
if ($("scene-arrangement").value === "custom") return;
|
|
5056
|
+
for (const record of records.values()) prepareManual(record.id);
|
|
4491
5057
|
if ($("scene-arrangement").value === "original")
|
|
4492
5058
|
for (const record of records.values())
|
|
4493
5059
|
record.rotation = {
|
|
@@ -4504,19 +5070,20 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4504
5070
|
updateGrid();
|
|
4505
5071
|
viewer?.set_auto_zoom();
|
|
4506
5072
|
}
|
|
4507
|
-
function syncPosition() {
|
|
5073
|
+
function syncPosition(preserveFocus = false) {
|
|
4508
5074
|
const record = records.get(modelID);
|
|
4509
5075
|
for (const axis of ["x", "y", "z"]) {
|
|
4510
5076
|
const input = $("position-" + axis);
|
|
4511
|
-
|
|
5077
|
+
if (!preserveFocus || document2.activeElement !== input)
|
|
5078
|
+
input.value = Number((record?.position?.[axis] || 0).toPrecision(12));
|
|
4512
5079
|
input.disabled = !record;
|
|
4513
5080
|
input.removeAttribute("aria-invalid");
|
|
4514
5081
|
}
|
|
4515
5082
|
$("position-error").hidden = true;
|
|
4516
|
-
$("position-units").textContent = record?.units === "mm" ? "Offset from file coordinates, in mm." : "Offset from file coordinates, in model units.";
|
|
5083
|
+
$("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.";
|
|
4517
5084
|
$("move-model-button").disabled = !record || busy;
|
|
4518
5085
|
$("reset-position").disabled = !record;
|
|
4519
|
-
syncRotation();
|
|
5086
|
+
syncRotation(preserveFocus);
|
|
4520
5087
|
for (const id of [
|
|
4521
5088
|
"rotate-model-button",
|
|
4522
5089
|
"depth-model-button",
|
|
@@ -4545,14 +5112,16 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4545
5112
|
viewer.set_position(record.id, p.x, p.y, p.z);
|
|
4546
5113
|
}
|
|
4547
5114
|
function selectedTransform() {
|
|
5115
|
+
prepareManual(modelID);
|
|
4548
5116
|
const record = records.get(modelID);
|
|
4549
5117
|
return record ? { ...record, center: modelCenter(record), radius: modelRadius(record) } : null;
|
|
4550
5118
|
}
|
|
4551
|
-
function syncRotation() {
|
|
5119
|
+
function syncRotation(preserveFocus = false) {
|
|
4552
5120
|
const record = records.get(modelID);
|
|
4553
5121
|
for (const axis of ["x", "y", "z"]) {
|
|
4554
5122
|
const input = $("rotation-" + axis);
|
|
4555
|
-
|
|
5123
|
+
if (!preserveFocus || document2.activeElement !== input)
|
|
5124
|
+
input.value = Number((record?.rotation?.[axis] || 0).toPrecision(10));
|
|
4556
5125
|
input.disabled = !record;
|
|
4557
5126
|
input.removeAttribute("aria-invalid");
|
|
4558
5127
|
}
|
|
@@ -4598,6 +5167,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4598
5167
|
modelMover?.setEnabled(false);
|
|
4599
5168
|
transformTool = tool;
|
|
4600
5169
|
moveMode = !!enabled && modelID !== null && !busy;
|
|
5170
|
+
if (moveMode) prepareManual(modelID);
|
|
4601
5171
|
modelMover?.setEnabled(moveMode);
|
|
4602
5172
|
syncTool();
|
|
4603
5173
|
$("move-mode").hidden = !moveMode;
|
|
@@ -4621,6 +5191,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4621
5191
|
const input = $("rotation-" + axis);
|
|
4622
5192
|
listen(input, "input", () => {
|
|
4623
5193
|
if (modelID === null || !input.value.trim()) return;
|
|
5194
|
+
prepareManual(modelID);
|
|
4624
5195
|
if (applyRotation(
|
|
4625
5196
|
modelID,
|
|
4626
5197
|
{ ...records.get(modelID).rotation, [axis]: input.valueAsNumber },
|
|
@@ -4632,6 +5203,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4632
5203
|
});
|
|
4633
5204
|
listen(input, "change", () => {
|
|
4634
5205
|
if (modelID === null) return;
|
|
5206
|
+
prepareManual(modelID);
|
|
4635
5207
|
if (!input.value.trim() || !applyRotation(modelID, {
|
|
4636
5208
|
...records.get(modelID).rotation,
|
|
4637
5209
|
[axis]: input.valueAsNumber
|
|
@@ -4644,6 +5216,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4644
5216
|
listen($("reset-rotation"), "click", () => {
|
|
4645
5217
|
setMoveMode(false);
|
|
4646
5218
|
if (modelID !== null) {
|
|
5219
|
+
prepareManual(modelID);
|
|
4647
5220
|
applyRotation(modelID, {
|
|
4648
5221
|
...records.get(modelID).originalRotation || { x: 0, y: 0, z: 0 }
|
|
4649
5222
|
});
|
|
@@ -4683,6 +5256,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4683
5256
|
const input = $("position-" + axis);
|
|
4684
5257
|
listen(input, "input", () => {
|
|
4685
5258
|
if (modelID === null || !input.value.trim()) return;
|
|
5259
|
+
prepareManual(modelID);
|
|
4686
5260
|
if (applyPosition(
|
|
4687
5261
|
modelID,
|
|
4688
5262
|
{ ...records.get(modelID).position, [axis]: input.valueAsNumber },
|
|
@@ -4694,6 +5268,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4694
5268
|
});
|
|
4695
5269
|
listen(input, "change", () => {
|
|
4696
5270
|
if (modelID === null) return;
|
|
5271
|
+
prepareManual(modelID);
|
|
4697
5272
|
if (!input.value.trim() || !applyPosition(modelID, {
|
|
4698
5273
|
...records.get(modelID).position,
|
|
4699
5274
|
[axis]: input.valueAsNumber
|
|
@@ -4710,6 +5285,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4710
5285
|
listen($("reset-position"), "click", () => {
|
|
4711
5286
|
setMoveMode(false);
|
|
4712
5287
|
if (modelID !== null) {
|
|
5288
|
+
prepareManual(modelID);
|
|
4713
5289
|
applyPosition(modelID, {
|
|
4714
5290
|
...records.get(modelID).originalPosition || { x: 0, y: 0, z: 0 }
|
|
4715
5291
|
});
|
|
@@ -4760,6 +5336,29 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4760
5336
|
renderScene();
|
|
4761
5337
|
announce(name + " removed.");
|
|
4762
5338
|
}
|
|
5339
|
+
function prepareManual(id) {
|
|
5340
|
+
if (!records.has(id)) return;
|
|
5341
|
+
syncAnimatedRecord(records.get(id), viewer?._models.get(id));
|
|
5342
|
+
viewer?.animate_model(id, null);
|
|
5343
|
+
}
|
|
5344
|
+
function refreshAnimatedPose() {
|
|
5345
|
+
if (disposed || modelID === null) return;
|
|
5346
|
+
syncAnimatedRecord(records.get(modelID), viewer?._models.get(modelID));
|
|
5347
|
+
if (!$("scene-panel").hidden && $("position-details").open)
|
|
5348
|
+
syncPosition(true);
|
|
5349
|
+
}
|
|
5350
|
+
function animationChanged(id, state, reason) {
|
|
5351
|
+
if (disposed) return;
|
|
5352
|
+
if (reason === "restored" && state.status === "running" && window2.matchMedia?.("(prefers-reduced-motion: reduce)").matches)
|
|
5353
|
+
viewer.pause_model_animation(id);
|
|
5354
|
+
syncAnimatedRecord(records.get(id), viewer?._models.get(id));
|
|
5355
|
+
if (["started", "restored", "resumed"].includes(reason))
|
|
5356
|
+
$("scene-arrangement").value = "custom";
|
|
5357
|
+
if (id === modelID) refreshAnimatedPose();
|
|
5358
|
+
animationUI?.sync();
|
|
5359
|
+
printUI?.sync();
|
|
5360
|
+
options.animation_state_callback?.(id, state, reason);
|
|
5361
|
+
}
|
|
4763
5362
|
async function getViewer() {
|
|
4764
5363
|
if (!viewerPromise)
|
|
4765
5364
|
viewerPromise = Promise.resolve().then(async () => {
|
|
@@ -4774,6 +5373,10 @@ function mountViewstlApp(root, options = {}) {
|
|
|
4774
5373
|
center_models: false,
|
|
4775
5374
|
bg_color: background,
|
|
4776
5375
|
allow_drag_and_drop: false,
|
|
5376
|
+
animation_state_callback: animationChanged,
|
|
5377
|
+
animation_complete_callback: (...args) => {
|
|
5378
|
+
if (!disposed) options.animation_complete_callback?.(...args);
|
|
5379
|
+
},
|
|
4777
5380
|
camera_state: {
|
|
4778
5381
|
position: { x: 90, y: -130, z: 90 },
|
|
4779
5382
|
target: { x: 0, y: 0, z: 0 },
|
|
@@ -5355,6 +5958,26 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5355
5958
|
},
|
|
5356
5959
|
announce
|
|
5357
5960
|
});
|
|
5961
|
+
if ($("animation-details"))
|
|
5962
|
+
animationUI = bindAppAnimation({
|
|
5963
|
+
$,
|
|
5964
|
+
listen,
|
|
5965
|
+
window: window2,
|
|
5966
|
+
getState: () => ({
|
|
5967
|
+
busy,
|
|
5968
|
+
selectedId: modelID,
|
|
5969
|
+
models: [...records.values()]
|
|
5970
|
+
}),
|
|
5971
|
+
getViewer: () => viewer,
|
|
5972
|
+
beforePlay(id) {
|
|
5973
|
+
setMoveMode(false);
|
|
5974
|
+
prepareManual(id);
|
|
5975
|
+
centerAnimationPivot(records.get(id), viewer._models.get(id));
|
|
5976
|
+
$("scene-arrangement").value = "custom";
|
|
5977
|
+
},
|
|
5978
|
+
refreshPose: refreshAnimatedPose,
|
|
5979
|
+
announce
|
|
5980
|
+
});
|
|
5358
5981
|
updateGrid();
|
|
5359
5982
|
setBusy(false);
|
|
5360
5983
|
setMoveMode(false);
|
|
@@ -5379,6 +6002,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5379
6002
|
},
|
|
5380
6003
|
getState() {
|
|
5381
6004
|
assertActive();
|
|
6005
|
+
for (const record of records.values())
|
|
6006
|
+
syncAnimatedRecord(record, viewer?._models.get(record.id));
|
|
5382
6007
|
return {
|
|
5383
6008
|
selectedId: modelID,
|
|
5384
6009
|
busy,
|
|
@@ -5391,7 +6016,8 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5391
6016
|
units: record.units,
|
|
5392
6017
|
position: { ...record.position },
|
|
5393
6018
|
rotation: { ...record.rotation },
|
|
5394
|
-
color: record.color
|
|
6019
|
+
color: record.color,
|
|
6020
|
+
animation: viewer.get_model_animation_state(record.id)
|
|
5395
6021
|
}))
|
|
5396
6022
|
};
|
|
5397
6023
|
},
|
|
@@ -5399,6 +6025,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5399
6025
|
if (disposed) return;
|
|
5400
6026
|
exportUI?.dispose();
|
|
5401
6027
|
printUI?.dispose();
|
|
6028
|
+
animationUI?.dispose();
|
|
5402
6029
|
cancelLoading();
|
|
5403
6030
|
setMoveMode(false);
|
|
5404
6031
|
appearanceRequest++;
|
|
@@ -5416,7 +6043,7 @@ function mountViewstlApp(root, options = {}) {
|
|
|
5416
6043
|
return api;
|
|
5417
6044
|
}
|
|
5418
6045
|
|
|
5419
|
-
// src/
|
|
6046
|
+
// src/index.js
|
|
5420
6047
|
StlViewer.workerURL = new URL("./viewstl.worker.js", import.meta.url).href;
|
|
5421
6048
|
var index_default = StlViewer;
|
|
5422
6049
|
export {
|