@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.
Files changed (43) hide show
  1. package/README.md +19 -170
  2. package/build/{v2/THIRD-PARTY-NOTICES.txt → THIRD-PARTY-NOTICES.txt} +1006 -1006
  3. package/build/{v2/cad → cad}/occt-import-js.source.txt +19 -19
  4. package/build/chunks/archive-JU75RPTU.js +1 -0
  5. package/build/{v2/chunks/cad-VY42CMP4.js → chunks/cad-4JYHORYZ.js} +1 -1
  6. package/build/{v2/chunks/chunk-JH3H3JWA.js → chunks/chunk-OP2L7L4N.js} +1 -1
  7. package/build/{v2/chunks/chunk-ZMK3UZZE.js → chunks/chunk-PQYEV7WH.js} +1 -1
  8. package/build/{v2/chunks/chunk-R3HNTB5A.js → chunks/chunk-Y6VY64LY.js} +1 -1
  9. package/build/{v2/chunks/export-7OP4ZGGJ.js → chunks/export-VM6OQCQJ.js} +3 -3
  10. package/build/{v2/chunks/export-cad-QPBT756V.js → chunks/export-cad-QXCB6UDU.js} +1 -1
  11. package/build/{v2/chunks/export-zip-EDCWH5PX.js → chunks/export-zip-PVXETVLK.js} +1 -1
  12. package/build/{v2/chunks/print-check-2PU7CSL3.js → chunks/print-check-NJ4IONC5.js} +1 -1
  13. package/build/{v2/chunks/scene-export-KQBS7VRJ.js → chunks/scene-export-ZIXMUNLI.js} +1 -1
  14. package/build/chunks/scene-reader-JGHERXR2.js +1 -0
  15. package/build/{v2/node → node}/analysis-worker.js +11 -11
  16. package/build/{v2/node → node}/index.cjs +7 -7
  17. package/build/{v2/node → node}/index.js +6 -6
  18. package/build/stl_viewer.min.js +51 -0
  19. package/build/{v2/viewstl.cjs → viewstl.cjs} +752 -125
  20. package/build/{v2/viewstl.d.cts → viewstl.d.cts} +50 -0
  21. package/build/{v2/viewstl.d.ts → viewstl.d.ts} +50 -0
  22. package/build/{v2/viewstl.js → viewstl.js} +752 -125
  23. package/build/viewstl.min.js +51 -0
  24. package/build/{v2/viewstl.worker.js → viewstl.worker.js} +1 -1
  25. package/docs/releases/2.0.0-rc.5.md +46 -0
  26. package/docs/v2-compatibility.md +5 -5
  27. package/docs/v2-usage.md +128 -21
  28. package/docs/v2-verification.md +158 -0
  29. package/legacy/README.md +14 -8
  30. package/package.json +18 -35
  31. package/build/v2/README.md +0 -15
  32. package/build/v2/chunks/archive-VGS4J4Z7.js +0 -1
  33. package/build/v2/chunks/scene-reader-APUJOPUR.js +0 -1
  34. package/build/v2/stl_viewer.min.js +0 -51
  35. package/build/v2/viewstl.min.js +0 -51
  36. /package/build/{v2/cad → cad}/occt-import-js.js +0 -0
  37. /package/build/{v2/cad → cad}/occt-import-js.wasm +0 -0
  38. /package/build/{v2/cad → cad}/prepare-cad.source.txt +0 -0
  39. /package/build/{v2/chunks/chunk-U33KANV7.js → chunks/chunk-IK3WL53C.js} +0 -0
  40. /package/build/{v2/chunks/chunk-JS56KC3I.js → chunks/chunk-W5KVLGVR.js} +0 -0
  41. /package/build/{v2/chunks → chunks}/chunk-ZSV4PTIA.js +0 -0
  42. /package/build/{v2/node → node}/index.d.cts +0 -0
  43. /package/build/{v2/node → node}/index.d.ts +0 -0
@@ -1,4 +1,4 @@
1
- // src/v2/appearance.js
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/v2/math.js
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/v2/memory.js
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/v2/geometry.js
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/v2/infinite-grid.js
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/v2/selection-frame.js
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/v2/renderer.js
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/v2/controls.js
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/v2/worker-pool.js
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/v2/scene-import.js
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/v2/scene-import-controller.js
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
- ["x", "y", "z"].forEach((axis, i) => model[axis] += shift[i]);
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 (const model of added.values()) model._sceneImportPending = false;
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/v2/print-options.js
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 number = (v, name, max = 1e9) => {
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] = number(input.profile[key], key);
1507
+ profile[key] = number2(input.profile[key], key);
1330
1508
  if (input.profile.overhang_degrees != null)
1331
- profile.overhang_degrees = number(
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) => number(v, "build volume")
1518
+ (v) => number2(v, "build volume")
1341
1519
  );
1342
1520
  }
1343
1521
  }
1344
- const time = number(input.max_time_ms ?? 3e4, "check time limit", 12e4);
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/v2/print-controller.js
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/v2/export-options.js
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/v2/io.js
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/v2/three-bridge.js
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/v2/viewer.js
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.3";
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
- () => this._message(
1916
- "Graphics interrupted. Waiting for the browser to restore rendering\u2026"
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 (supplied.animation) this.animate_model(m.id, supplied.animation);
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 [m.scalex, m.scaley, m.scalez] = [x, y, z];
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
- if (m.animation) model.animation = structuredClone(m.animation);
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) => ({ id: m.id, bin: this.get_stl_bin(m.id) }))
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
- for (const input of scene.models) {
3240
- if (!input || typeof input !== "object")
3241
- throw new Error("Invalid scene model");
3242
- const model = modelOptions(input);
3243
- delete model.mesh;
3244
- delete model.data;
3245
- delete model.local_file;
3246
- if (files) {
3247
- if (typeof model.filename !== "string" || !Object.hasOwn(files, model.filename))
3248
- throw new Error("VSB model file is missing");
3249
- model.local_file = new File([files[model.filename]], model.filename);
3250
- } else if (model.filename)
3251
- model.filename = new URL(model.filename, base).href;
3252
- this.add_model(model);
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.animation = null;
3275
- m._animation = null;
3551
+ this._stopAnimation(m, "stopped");
3552
+ this._invalidate();
3276
3553
  return;
3277
3554
  }
3278
- const copy = structuredClone(animation), tracks = [];
3279
- for (const mode of ["delta", "exact"])
3280
- if (copy[mode]) {
3281
- const a = copy[mode];
3282
- positive(a.msec ?? 300, "animation duration");
3283
- const values = {};
3284
- for (const key of transformKeys) {
3285
- const value = a[key] ?? (key.startsWith("scale") ? a.scale : void 0);
3286
- if (value !== void 0) {
3287
- finite(value, key);
3288
- if (mode === "exact" && key.startsWith("scale"))
3289
- positive(value, key);
3290
- values[key] = value;
3291
- }
3292
- }
3293
- tracks.push({
3294
- mode,
3295
- values,
3296
- duration: a.msec ?? 300,
3297
- loop: mode === "delta" && !!a.loop,
3298
- from: Object.fromEntries(transformKeys.map((k) => [k, m[k]]))
3299
- });
3300
- }
3301
- if (!tracks.length) throw new Error("Animation needs delta or exact");
3302
- m.animation = copy;
3303
- m._animation = { start: null, tracks };
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
- const animation = m._animation;
3309
- animation.start ??= time;
3310
- let running = false;
3311
- for (const t of animation.tracks) {
3312
- const fraction = (time - animation.start) / t.duration, p = t.loop ? fraction : Math.min(fraction, 1);
3313
- running ||= t.loop || fraction < 1;
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/v2/memory-policy.js
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/v2/model-transform.js
3698
+ // src/model-transform.js
3377
3699
  var axes = ["x", "y", "z"];
3378
3700
  var vector2 = (p) => axes.map((a) => p[a]);
3379
- var object = (p) => Object.fromEntries(axes.map((a, i) => [a, p[i]]));
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 object(
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 object(c.map((v, i) => model.position[axes[i]] + v - rotated[i]));
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 object(
3797
+ return object2(
3476
3798
  vector2(model.position).map((v, i) => v + direction[i] * (next - distance))
3477
3799
  );
3478
3800
  }
3479
3801
 
3480
- // src/v2/scene-layout.js
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/v2/model-move.js
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/v2/drop-source.js
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/v2/app-export.js
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/v2/app-print.js
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/v2/app.js
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
- input.value = Number((record?.position?.[axis] || 0).toPrecision(12));
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
- input.value = Number((record?.rotation?.[axis] || 0).toPrecision(10));
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/v2/index.js
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 {