@omrips/viewstl 2.0.0-rc.4 → 2.0.0-rc.5

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