simview 3.2__py3-none-any.whl

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 (163) hide show
  1. simview/__init__.py +69 -0
  2. simview/__main__.py +174 -0
  3. simview/launcher.py +109 -0
  4. simview/live.py +204 -0
  5. simview/merge.py +593 -0
  6. simview/model.py +690 -0
  7. simview/py.typed +0 -0
  8. simview/scene.py +603 -0
  9. simview/server.py +739 -0
  10. simview/state.py +190 -0
  11. simview/static/css/controls.css +36 -0
  12. simview/static/js/SimView.js +601 -0
  13. simview/static/js/components/AnimationController.js +654 -0
  14. simview/static/js/components/BatchManager.js +198 -0
  15. simview/static/js/components/InteractionController.js +446 -0
  16. simview/static/js/components/InteractionControls.js +116 -0
  17. simview/static/js/components/Scene.js +166 -0
  18. simview/static/js/components/StateStore.js +188 -0
  19. simview/static/js/config.js +233 -0
  20. simview/static/js/main.js +3 -0
  21. simview/static/js/objects/Body.js +616 -0
  22. simview/static/js/objects/StaticObject.js +181 -0
  23. simview/static/js/objects/Terrain.js +514 -0
  24. simview/static/js/objects/utils.js +378 -0
  25. simview/static/js/ui/AnalysisPanel.js +233 -0
  26. simview/static/js/ui/BatchLegend.js +207 -0
  27. simview/static/js/ui/BodyStateWindow.js +521 -0
  28. simview/static/js/ui/Controls.js +586 -0
  29. simview/static/js/ui/ErrorMetrics.js +664 -0
  30. simview/static/js/ui/Legend.js +87 -0
  31. simview/static/js/ui/PlaybackControls.js +314 -0
  32. simview/static/js/ui/ScalarPlotter.js +575 -0
  33. simview/static/js/utils/blobCodec.js +73 -0
  34. simview/static/js/utils/bodyTransforms.js +151 -0
  35. simview/static/js/utils/csv.js +46 -0
  36. simview/static/js/utils/errorMath.js +69 -0
  37. simview/static/js/utils/injectStyles.js +16 -0
  38. simview/static/js/utils/interpolate.js +65 -0
  39. simview/static/js/utils/liveFollow.js +17 -0
  40. simview/static/js/utils/loadRecordingLibs.js +34 -0
  41. simview/static/js/utils/viewState.js +226 -0
  42. simview/static/lib/chroma-js-3.1.2/index.min.js +8 -0
  43. simview/static/lib/chroma-js-3.1.2/src/Color.js +49 -0
  44. simview/static/lib/chroma-js-3.1.2/src/chroma.js +10 -0
  45. simview/static/lib/chroma-js-3.1.2/src/colors/colorbrewer.js +81 -0
  46. simview/static/lib/chroma-js-3.1.2/src/colors/w3cx11.js +164 -0
  47. simview/static/lib/chroma-js-3.1.2/src/generator/average.js +88 -0
  48. simview/static/lib/chroma-js-3.1.2/src/generator/bezier.js +86 -0
  49. simview/static/lib/chroma-js-3.1.2/src/generator/blend.js +57 -0
  50. simview/static/lib/chroma-js-3.1.2/src/generator/cubehelix.js +87 -0
  51. simview/static/lib/chroma-js-3.1.2/src/generator/mix.js +19 -0
  52. simview/static/lib/chroma-js-3.1.2/src/generator/random.js +12 -0
  53. simview/static/lib/chroma-js-3.1.2/src/generator/scale.js +394 -0
  54. simview/static/lib/chroma-js-3.1.2/src/interpolator/_hsx.js +59 -0
  55. simview/static/lib/chroma-js-3.1.2/src/interpolator/hcg.js +12 -0
  56. simview/static/lib/chroma-js-3.1.2/src/interpolator/hsi.js +12 -0
  57. simview/static/lib/chroma-js-3.1.2/src/interpolator/hsl.js +12 -0
  58. simview/static/lib/chroma-js-3.1.2/src/interpolator/hsv.js +12 -0
  59. simview/static/lib/chroma-js-3.1.2/src/interpolator/index.js +1 -0
  60. simview/static/lib/chroma-js-3.1.2/src/interpolator/lab.js +19 -0
  61. simview/static/lib/chroma-js-3.1.2/src/interpolator/lch.js +13 -0
  62. simview/static/lib/chroma-js-3.1.2/src/interpolator/lrgb.js +19 -0
  63. simview/static/lib/chroma-js-3.1.2/src/interpolator/num.js +15 -0
  64. simview/static/lib/chroma-js-3.1.2/src/interpolator/oklab.js +19 -0
  65. simview/static/lib/chroma-js-3.1.2/src/interpolator/oklch.js +12 -0
  66. simview/static/lib/chroma-js-3.1.2/src/interpolator/rgb.js +18 -0
  67. simview/static/lib/chroma-js-3.1.2/src/io/cmyk/cmyk2rgb.js +16 -0
  68. simview/static/lib/chroma-js-3.1.2/src/io/cmyk/index.js +27 -0
  69. simview/static/lib/chroma-js-3.1.2/src/io/cmyk/rgb2cmyk.js +17 -0
  70. simview/static/lib/chroma-js-3.1.2/src/io/css/css2rgb.js +238 -0
  71. simview/static/lib/chroma-js-3.1.2/src/io/css/hsl2css.js +26 -0
  72. simview/static/lib/chroma-js-3.1.2/src/io/css/index.js +27 -0
  73. simview/static/lib/chroma-js-3.1.2/src/io/css/lab2css.js +24 -0
  74. simview/static/lib/chroma-js-3.1.2/src/io/css/lch2css.js +24 -0
  75. simview/static/lib/chroma-js-3.1.2/src/io/css/oklab2css.js +16 -0
  76. simview/static/lib/chroma-js-3.1.2/src/io/css/oklch2css.js +16 -0
  77. simview/static/lib/chroma-js-3.1.2/src/io/css/rgb2css.js +61 -0
  78. simview/static/lib/chroma-js-3.1.2/src/io/gl/index.js +22 -0
  79. simview/static/lib/chroma-js-3.1.2/src/io/hcg/hcg2rgb.js +55 -0
  80. simview/static/lib/chroma-js-3.1.2/src/io/hcg/index.js +27 -0
  81. simview/static/lib/chroma-js-3.1.2/src/io/hcg/rgb2hcg.js +23 -0
  82. simview/static/lib/chroma-js-3.1.2/src/io/hex/hex2rgb.js +56 -0
  83. simview/static/lib/chroma-js-3.1.2/src/io/hex/index.js +29 -0
  84. simview/static/lib/chroma-js-3.1.2/src/io/hex/rgb2hex.js +29 -0
  85. simview/static/lib/chroma-js-3.1.2/src/io/hsi/hsi2rgb.js +45 -0
  86. simview/static/lib/chroma-js-3.1.2/src/io/hsi/index.js +27 -0
  87. simview/static/lib/chroma-js-3.1.2/src/io/hsi/rgb2hsi.js +31 -0
  88. simview/static/lib/chroma-js-3.1.2/src/io/hsl/hsl2rgb.js +35 -0
  89. simview/static/lib/chroma-js-3.1.2/src/io/hsl/index.js +27 -0
  90. simview/static/lib/chroma-js-3.1.2/src/io/hsl/rgb2hsl.js +45 -0
  91. simview/static/lib/chroma-js-3.1.2/src/io/hsv/hsv2rgb.js +47 -0
  92. simview/static/lib/chroma-js-3.1.2/src/io/hsv/index.js +27 -0
  93. simview/static/lib/chroma-js-3.1.2/src/io/hsv/rgb2hsv.js +32 -0
  94. simview/static/lib/chroma-js-3.1.2/src/io/input.js +4 -0
  95. simview/static/lib/chroma-js-3.1.2/src/io/lab/index.js +28 -0
  96. simview/static/lib/chroma-js-3.1.2/src/io/lab/lab-constants.js +120 -0
  97. simview/static/lib/chroma-js-3.1.2/src/io/lab/lab2rgb.js +101 -0
  98. simview/static/lib/chroma-js-3.1.2/src/io/lab/rgb2lab.js +67 -0
  99. simview/static/lib/chroma-js-3.1.2/src/io/lch/hcl2rgb.js +9 -0
  100. simview/static/lib/chroma-js-3.1.2/src/io/lch/index.js +35 -0
  101. simview/static/lib/chroma-js-3.1.2/src/io/lch/lab2lch.js +12 -0
  102. simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2lab.js +18 -0
  103. simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2rgb.js +13 -0
  104. simview/static/lib/chroma-js-3.1.2/src/io/lch/rgb2lch.js +12 -0
  105. simview/static/lib/chroma-js-3.1.2/src/io/named/index.js +30 -0
  106. simview/static/lib/chroma-js-3.1.2/src/io/num/index.js +32 -0
  107. simview/static/lib/chroma-js-3.1.2/src/io/num/num2rgb.js +13 -0
  108. simview/static/lib/chroma-js-3.1.2/src/io/num/rgb2num.js +8 -0
  109. simview/static/lib/chroma-js-3.1.2/src/io/oklab/index.js +27 -0
  110. simview/static/lib/chroma-js-3.1.2/src/io/oklab/oklab2rgb.js +34 -0
  111. simview/static/lib/chroma-js-3.1.2/src/io/oklab/rgb2oklab.js +37 -0
  112. simview/static/lib/chroma-js-3.1.2/src/io/oklch/index.js +27 -0
  113. simview/static/lib/chroma-js-3.1.2/src/io/oklch/oklch2rgb.js +13 -0
  114. simview/static/lib/chroma-js-3.1.2/src/io/oklch/rgb2oklch.js +12 -0
  115. simview/static/lib/chroma-js-3.1.2/src/io/rgb/index.js +44 -0
  116. simview/static/lib/chroma-js-3.1.2/src/io/temp/index.js +22 -0
  117. simview/static/lib/chroma-js-3.1.2/src/io/temp/rgb2temperature.js +30 -0
  118. simview/static/lib/chroma-js-3.1.2/src/io/temp/temperature2rgb.js +39 -0
  119. simview/static/lib/chroma-js-3.1.2/src/ops/alpha.js +13 -0
  120. simview/static/lib/chroma-js-3.1.2/src/ops/clipped.js +5 -0
  121. simview/static/lib/chroma-js-3.1.2/src/ops/darken.js +17 -0
  122. simview/static/lib/chroma-js-3.1.2/src/ops/get.js +13 -0
  123. simview/static/lib/chroma-js-3.1.2/src/ops/luminance.js +54 -0
  124. simview/static/lib/chroma-js-3.1.2/src/ops/mix.js +10 -0
  125. simview/static/lib/chroma-js-3.1.2/src/ops/premultiply.js +12 -0
  126. simview/static/lib/chroma-js-3.1.2/src/ops/saturate.js +15 -0
  127. simview/static/lib/chroma-js-3.1.2/src/ops/set.js +43 -0
  128. simview/static/lib/chroma-js-3.1.2/src/ops/shade.js +11 -0
  129. simview/static/lib/chroma-js-3.1.2/src/utils/analyze.js +191 -0
  130. simview/static/lib/chroma-js-3.1.2/src/utils/clip_rgb.js +15 -0
  131. simview/static/lib/chroma-js-3.1.2/src/utils/contrast.js +12 -0
  132. simview/static/lib/chroma-js-3.1.2/src/utils/contrastAPCA.js +68 -0
  133. simview/static/lib/chroma-js-3.1.2/src/utils/delta-e.js +62 -0
  134. simview/static/lib/chroma-js-3.1.2/src/utils/distance.js +17 -0
  135. simview/static/lib/chroma-js-3.1.2/src/utils/index.js +27 -0
  136. simview/static/lib/chroma-js-3.1.2/src/utils/last.js +8 -0
  137. simview/static/lib/chroma-js-3.1.2/src/utils/limit.js +5 -0
  138. simview/static/lib/chroma-js-3.1.2/src/utils/multiply-matrices.js +36 -0
  139. simview/static/lib/chroma-js-3.1.2/src/utils/scales.js +15 -0
  140. simview/static/lib/chroma-js-3.1.2/src/utils/type.js +18 -0
  141. simview/static/lib/chroma-js-3.1.2/src/utils/unpack.js +17 -0
  142. simview/static/lib/chroma-js-3.1.2/src/utils/valid.js +11 -0
  143. simview/static/lib/chroma-js-3.1.2/src/version.js +2 -0
  144. simview/static/lib/download.js +132 -0
  145. simview/static/lib/js-colormaps.js +19335 -0
  146. simview/static/lib/tar.js +334 -0
  147. simview/static/lib/three-0.174.0/addons/controls/OrbitControls.js +1556 -0
  148. simview/static/lib/three-0.174.0/addons/libs/lil-gui.module.min.js +8 -0
  149. simview/static/lib/three-0.174.0/three.core.js +48830 -0
  150. simview/static/lib/three-0.174.0/three.module.js +17313 -0
  151. simview/static/lib/uPlot.esm.js +6140 -0
  152. simview/static/lib/uPlot.min.css +2 -0
  153. simview/static/textures/contacts/red-cross0.png +0 -0
  154. simview/static/textures/points/ball0.png +0 -0
  155. simview/static/textures/points/ball1.png +0 -0
  156. simview/templates/index.html +81 -0
  157. simview/utils.py +41 -0
  158. simview-3.2.dist-info/METADATA +552 -0
  159. simview-3.2.dist-info/RECORD +163 -0
  160. simview-3.2.dist-info/WHEEL +5 -0
  161. simview-3.2.dist-info/entry_points.txt +2 -0
  162. simview-3.2.dist-info/licenses/LICENSE +28 -0
  163. simview-3.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,151 @@
1
+ import * as THREE from "three";
2
+
3
+ // Resolves parent-relative body transforms (see README.md "Model (Static
4
+ // Data)" / "States (Dynamic Data)") into ordinary absolute-world transforms,
5
+ // so the rest of the viewer (Body.js, InteractionController.js, trails, ...)
6
+ // never has to know a body was parented -- it only ever sees the same
7
+ // `bodyTransform` shape it always has.
8
+
9
+ // Builds a `name -> {parent, localTransform}` lookup from `model.bodies`.
10
+ // `parent`/`localTransform` are null for an ordinary absolute-world body.
11
+ export function buildBodyMeta(modelBodies) {
12
+ const meta = new Map();
13
+ (modelBodies || []).forEach((b) => {
14
+ meta.set(b.name, {
15
+ parent: b.parent ?? null,
16
+ localTransform: b.localTransform ?? null,
17
+ });
18
+ });
19
+ return meta;
20
+ }
21
+
22
+ // Orders body names so every parent precedes its children. Each body has at
23
+ // most one parent (a forest, not a general DAG), so a plain DFS post-order
24
+ // walk suffices -- no need for a full topological-sort algorithm. Throws on
25
+ // a self-referencing, unknown, or cyclic parent chain, mirroring the
26
+ // SimViewModel.add_body/from_dict validation on the Python side (this exists
27
+ // on the client because loaded JSON has no equivalent insertion-order
28
+ // guarantee -- it may have been hand-edited or produced by another tool).
29
+ export function topoSortBodies(bodyMeta) {
30
+ const order = [];
31
+ const status = new Map(); // name -> "visiting" | "done"
32
+
33
+ function visit(name) {
34
+ if (status.get(name) === "done") return;
35
+ if (status.get(name) === "visiting") {
36
+ throw new Error(`Cycle detected in body parent chain involving '${name}'`);
37
+ }
38
+ const meta = bodyMeta.get(name);
39
+ if (meta.parent != null) {
40
+ if (meta.parent === name) {
41
+ throw new Error(`Body '${name}' cannot be its own parent`);
42
+ }
43
+ if (!bodyMeta.has(meta.parent)) {
44
+ throw new Error(
45
+ `Body '${name}' references unknown parent '${meta.parent}'`
46
+ );
47
+ }
48
+ status.set(name, "visiting");
49
+ visit(meta.parent);
50
+ }
51
+ status.set(name, "done");
52
+ order.push(name);
53
+ }
54
+
55
+ for (const name of bodyMeta.keys()) {
56
+ visit(name);
57
+ }
58
+ return order;
59
+ }
60
+
61
+ function posFromWire(t) {
62
+ return new THREE.Vector3(t[0], t[1], t[2]);
63
+ }
64
+
65
+ // Wire order is [x, y, z, w, qx, qy, qz]; THREE.Quaternion.set() takes (x, y, z, w).
66
+ function quatFromWire(t) {
67
+ return new THREE.Quaternion(t[4], t[5], t[6], t[3]);
68
+ }
69
+
70
+ function wireFromPosQuat(pos, quat) {
71
+ return [pos.x, pos.y, pos.z, quat.w, quat.x, quat.y, quat.z];
72
+ }
73
+
74
+ // Normalizes a bodyTransform field (flat array[7] for a single batch, or
75
+ // array[array[7]] for multiple) into a plain array of rows, matching the
76
+ // same convention Body.js already uses when consuming raw state data.
77
+ function toRows(bodyTransform) {
78
+ return Array.isArray(bodyTransform[0]) ? bodyTransform : [bodyTransform];
79
+ }
80
+
81
+ // Builds a `name -> rawStateEntry` lookup for one state's `bodies` array,
82
+ // expanding any grouped (list) `name` entries so each individual body name
83
+ // maps to the (shared) entry describing it.
84
+ function expandRawBodies(rawBodies) {
85
+ const map = new Map();
86
+ (rawBodies || []).forEach((entry) => {
87
+ const names = Array.isArray(entry.name) ? entry.name : [entry.name];
88
+ names.forEach((n) => map.set(n, entry));
89
+ });
90
+ return map;
91
+ }
92
+
93
+ // Resolves one state's raw `bodies` array into a `name -> resolvedBodyState`
94
+ // Map where every body's `bodyTransform` is absolute-world, ready to feed
95
+ // straight into Body.updateState / appendHistoryPointAt / setHistoryPointAt
96
+ // unchanged. A body absent from the result simply keeps whatever pose it
97
+ // already had (identical to today's behavior when a body is omitted from a
98
+ // state). `bodyMeta`/`topoOrder` come from `buildBodyMeta`/`topoSortBodies`,
99
+ // computed once at model load.
100
+ export function resolveStateBodies(bodyMeta, topoOrder, simBatches, rawBodies) {
101
+ const rawByName = expandRawBodies(rawBodies);
102
+ const resolved = new Map();
103
+
104
+ for (const name of topoOrder) {
105
+ const meta = bodyMeta.get(name);
106
+ const raw = rawByName.get(name);
107
+
108
+ if (meta.parent == null) {
109
+ // Root body: bodyTransform, if present this frame, is already
110
+ // absolute-world -- nothing to resolve.
111
+ if (raw && raw.bodyTransform) {
112
+ resolved.set(name, raw);
113
+ }
114
+ continue;
115
+ }
116
+
117
+ const parentResolved = resolved.get(meta.parent);
118
+ if (!parentResolved || !parentResolved.bodyTransform) {
119
+ // Parent has no resolved pose this frame; nothing sensible to
120
+ // compose this child against.
121
+ continue;
122
+ }
123
+ const parentRows = toRows(parentResolved.bodyTransform);
124
+
125
+ let localRows;
126
+ if (meta.localTransform) {
127
+ // Rigid attachment: same constant local offset for every batch.
128
+ localRows = [meta.localTransform];
129
+ } else if (raw && raw.bodyTransform) {
130
+ // Articulated attachment: per-frame local transform, as usual.
131
+ localRows = toRows(raw.bodyTransform);
132
+ } else {
133
+ continue; // no data to resolve this body this frame
134
+ }
135
+
136
+ const worldRows = [];
137
+ for (let i = 0; i < simBatches; i++) {
138
+ const parentRow = parentRows[Math.min(i, parentRows.length - 1)];
139
+ const localRow = localRows[Math.min(i, localRows.length - 1)];
140
+ const worldPos = posFromWire(localRow)
141
+ .applyQuaternion(quatFromWire(parentRow))
142
+ .add(posFromWire(parentRow));
143
+ const worldQuat = quatFromWire(parentRow).multiply(quatFromWire(localRow));
144
+ worldRows.push(wireFromPosQuat(worldPos, worldQuat));
145
+ }
146
+
147
+ resolved.set(name, { ...(raw || {}), bodyTransform: worldRows });
148
+ }
149
+
150
+ return resolved;
151
+ }
@@ -0,0 +1,46 @@
1
+ // Pure CSV assembly (rows -> escaped CSV string) shared by ScalarPlotter and
2
+ // ErrorMetrics CSV export buttons, plus a small browser-only download helper.
3
+
4
+ // Escapes a single CSV field per RFC 4180: wraps in double quotes if it
5
+ // contains a comma, double quote, or newline, doubling any embedded quotes.
6
+ export function escapeCsvField(field) {
7
+ const str = field === null || field === undefined ? "" : String(field);
8
+ if (/[",\n\r]/.test(str)) {
9
+ return `"${str.replace(/"/g, '""')}"`;
10
+ }
11
+ return str;
12
+ }
13
+
14
+ // Builds a CSV string (CRLF line endings, per RFC 4180) from a header array
15
+ // and an array of rows (each row an array of values, same length as header).
16
+ export function rowsToCsv(header, rows) {
17
+ const lines = [header, ...rows].map((row) =>
18
+ row.map(escapeCsvField).join(",")
19
+ );
20
+ return lines.join("\r\n") + "\r\n";
21
+ }
22
+
23
+ // Sanitizes a string (e.g. a body or batch name) for safe use inside a
24
+ // downloaded filename: replaces anything but letters, digits, dot, dash, and
25
+ // underscore with underscores, so names containing spaces, slashes, etc.
26
+ // don't break the filename or path.
27
+ export function sanitizeForFilename(name) {
28
+ const str = name === null || name === undefined ? "" : String(name);
29
+ return str.replace(/[^a-zA-Z0-9._-]+/g, "_");
30
+ }
31
+
32
+ // Triggers a browser download of `content` as a file named `filename`. No-op
33
+ // outside a browser environment (e.g. under vitest/node), so this module
34
+ // stays importable from tests without a DOM.
35
+ export function downloadCsv(filename, content) {
36
+ if (typeof document === "undefined" || typeof Blob === "undefined") return;
37
+ const blob = new Blob([content], { type: "text/csv;charset=utf-8;" });
38
+ const url = URL.createObjectURL(blob);
39
+ const link = document.createElement("a");
40
+ link.href = url;
41
+ link.download = filename;
42
+ document.body.appendChild(link);
43
+ link.click();
44
+ document.body.removeChild(link);
45
+ URL.revokeObjectURL(url);
46
+ }
@@ -0,0 +1,69 @@
1
+ // Pure error-metric math shared by ErrorMetrics.js: per-frame Euclidean
2
+ // position error (combined and per-axis signed) and quaternion geodesic
3
+ // angle error between two batches of the same body.
4
+
5
+ // Signed per-axis difference (posA - posB) for one frame, given flat
6
+ // position arrays (3 floats per frame) and a frame index.
7
+ export function positionAxisError(posA, posB, frameIndex) {
8
+ const base = frameIndex * 3;
9
+ return {
10
+ dx: posA[base] - posB[base],
11
+ dy: posA[base + 1] - posB[base + 1],
12
+ dz: posA[base + 2] - posB[base + 2],
13
+ };
14
+ }
15
+
16
+ // Combined Euclidean position error for one frame.
17
+ export function positionError(posA, posB, frameIndex) {
18
+ const { dx, dy, dz } = positionAxisError(posA, posB, frameIndex);
19
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
20
+ }
21
+
22
+ // Geodesic angle (radians) between two quaternions, given flat [w,x,y,z]
23
+ // arrays (4 floats per frame) and a frame index. Uses |dot| to collapse the
24
+ // q/-q double cover (both represent the same rotation), so the result is
25
+ // always in [0, pi/2] measured this way -- doubled by callers that want the
26
+ // full [0, pi] rotation-angle convention (see below).
27
+ function quatDot(quatA, quatB, frameIndex) {
28
+ const base = frameIndex * 4;
29
+ return (
30
+ quatA[base] * quatB[base] +
31
+ quatA[base + 1] * quatB[base + 1] +
32
+ quatA[base + 2] * quatB[base + 2] +
33
+ quatA[base + 3] * quatB[base + 3]
34
+ );
35
+ }
36
+
37
+ // Full geodesic rotation angle (radians) between two quaternions for one
38
+ // frame: 2*acos(|dot|), robust to floating-point dot products drifting
39
+ // slightly outside [-1, 1] and to either quaternion's sign (q and -q encode
40
+ // the same rotation).
41
+ export function quaternionAngleError(quatA, quatB, frameIndex) {
42
+ const dot = quatDot(quatA, quatB, frameIndex);
43
+ const clamped = Math.min(1, Math.max(-1, Math.abs(dot)));
44
+ return 2 * Math.acos(clamped);
45
+ }
46
+
47
+ // Root-mean-square of a plain numeric array. Returns 0 for an empty series
48
+ // (rather than NaN), so callers can display it without a special case.
49
+ export function rmse(values) {
50
+ if (!values || values.length === 0) return 0;
51
+ let sumSq = 0;
52
+ for (const v of values) sumSq += v * v;
53
+ return Math.sqrt(sumSq / values.length);
54
+ }
55
+
56
+ // Finds the maximum value in a plain numeric array and the index at which it
57
+ // occurs. Returns { value: 0, index: -1 } for an empty series.
58
+ export function maxWithIndex(values) {
59
+ if (!values || values.length === 0) return { value: 0, index: -1 };
60
+ let bestValue = values[0];
61
+ let bestIndex = 0;
62
+ for (let i = 1; i < values.length; i++) {
63
+ if (values[i] > bestValue) {
64
+ bestValue = values[i];
65
+ bestIndex = i;
66
+ }
67
+ }
68
+ return { value: bestValue, index: bestIndex };
69
+ }
@@ -0,0 +1,16 @@
1
+ const injectedStyleIds = new Set();
2
+
3
+ // Injects a <style> block into <head> exactly once per styleId, even across
4
+ // multiple instances of the same component. Cheap to call from a
5
+ // constructor every time -- repeat calls with an already-seen id are no-ops.
6
+ export function injectStyles(styleId, css) {
7
+ if (injectedStyleIds.has(styleId) || document.getElementById(styleId)) {
8
+ injectedStyleIds.add(styleId);
9
+ return;
10
+ }
11
+ const styleElement = document.createElement("style");
12
+ styleElement.id = styleId;
13
+ styleElement.textContent = css;
14
+ document.head.appendChild(styleElement);
15
+ injectedStyleIds.add(styleId);
16
+ }
@@ -0,0 +1,65 @@
1
+ import * as THREE from "three";
2
+
3
+ // Pure helpers for smooth interpolated playback (see AnimationController.js).
4
+ // Every function here takes two arrays of per-batch rows (row shape depends
5
+ // on the field -- 7-wide [x,y,z,w,qx,qy,qz] for bodyTransform, 3-wide for
6
+ // plain vector attributes like velocity) plus alpha in [0, 1], and returns a
7
+ // new array of interpolated rows. Row counts of `rowsA`/`rowsB` may differ
8
+ // (e.g. a body only present in one of the two bracketing frames); rows
9
+ // beyond the shorter array's length pass through unchanged from the longer
10
+ // one, matching how the rest of the viewer already clamps batch indices.
11
+
12
+ const _qa = new THREE.Quaternion();
13
+ const _qb = new THREE.Quaternion();
14
+
15
+ // Linearly interpolates a single 3-vector row: [x, y, z].
16
+ export function lerpVector3Row(a, b, alpha) {
17
+ return [
18
+ a[0] + (b[0] - a[0]) * alpha,
19
+ a[1] + (b[1] - a[1]) * alpha,
20
+ a[2] + (b[2] - a[2]) * alpha,
21
+ ];
22
+ }
23
+
24
+ // Interpolates an array of 3-vector rows (one per batch) by plain lerp.
25
+ export function lerpVectorRows(rowsA, rowsB, alpha) {
26
+ if (!rowsA || !rowsB) return rowsA || rowsB;
27
+ const n = Math.min(rowsA.length, rowsB.length);
28
+ const out = new Array(rowsA.length);
29
+ for (let i = 0; i < n; i++) {
30
+ out[i] = lerpVector3Row(rowsA[i], rowsB[i], alpha);
31
+ }
32
+ for (let i = n; i < rowsA.length; i++) out[i] = rowsA[i];
33
+ return out;
34
+ }
35
+
36
+ // Interpolates a single bodyTransform row [x, y, z, w, qx, qy, qz]: position
37
+ // lerp, quaternion slerp with shortest-path handling (negate one quaternion
38
+ // when the dot product is negative, since q and -q represent the same
39
+ // rotation but naive slerp between them takes the long way around).
40
+ export function interpolateTransformRow(a, b, alpha) {
41
+ const px = a[0] + (b[0] - a[0]) * alpha;
42
+ const py = a[1] + (b[1] - a[1]) * alpha;
43
+ const pz = a[2] + (b[2] - a[2]) * alpha;
44
+
45
+ _qa.set(a[4], a[5], a[6], a[3]); // THREE.Quaternion.set(x, y, z, w)
46
+ _qb.set(b[4], b[5], b[6], b[3]);
47
+ if (_qa.dot(_qb) < 0) {
48
+ _qb.set(-b[4], -b[5], -b[6], -b[3]);
49
+ }
50
+ _qa.slerp(_qb, alpha);
51
+
52
+ return [px, py, pz, _qa.w, _qa.x, _qa.y, _qa.z];
53
+ }
54
+
55
+ // Interpolates an array of bodyTransform rows (one per batch).
56
+ export function interpolateTransformRows(rowsA, rowsB, alpha) {
57
+ if (!rowsA || !rowsB) return rowsA || rowsB;
58
+ const n = Math.min(rowsA.length, rowsB.length);
59
+ const out = new Array(rowsA.length);
60
+ for (let i = 0; i < n; i++) {
61
+ out[i] = interpolateTransformRow(rowsA[i], rowsB[i], alpha);
62
+ }
63
+ for (let i = n; i < rowsA.length; i++) out[i] = rowsA[i];
64
+ return out;
65
+ }
@@ -0,0 +1,17 @@
1
+ // Decides whether a newly-arrived live-streaming chunk (see
2
+ // simview.live.LiveViewer / SimView.startLiveStream) should pull playback
3
+ // forward to the new last frame.
4
+ //
5
+ // Kept as a small pure function (rather than inline in SimView.processStatesChunk)
6
+ // so it can be unit-tested without instantiating a full SimView/AnimationController.
7
+ //
8
+ // True (follow) when nothing has played yet, or the viewer was parked exactly
9
+ // on the (previous) last frame and isn't mid-playback. False when the user
10
+ // scrubbed back to look at history, or is actively playing/looping -- either
11
+ // way a new chunk arriving shouldn't yank the view forward.
12
+ export function shouldFollowLive(animationController) {
13
+ const ac = animationController;
14
+ if (!ac || !ac.store || ac.store.length === 0) return true;
15
+ if (ac.isPlaying) return false;
16
+ return ac.currentStateIndex >= ac.store.length - 1;
17
+ }
@@ -0,0 +1,34 @@
1
+ // tar.js and download.js are only needed for the PNG-sequence recording
2
+ // format (each rendered frame is captured as a PNG blob and packed into a
3
+ // .tar via Tar.append()/Tar.save(), then saved with download()). The WEBM
4
+ // format needs no extra libraries at all -- it's captured natively via
5
+ // canvas.captureStream() + MediaRecorder and saved via a temporary <a
6
+ // download> link (see AnimationController.js). Both tar.js and download.js
7
+ // are UMD/global-style scripts (not ES modules), so loading them lazily
8
+ // means injecting <script> tags rather than dynamic import().
9
+ const RECORDING_LIB_PATHS = ["../../lib/tar.js", "../../lib/download.js"];
10
+
11
+ let loadPromise = null;
12
+
13
+ function loadScript(url) {
14
+ return new Promise((resolve, reject) => {
15
+ const script = document.createElement("script");
16
+ script.src = url;
17
+ script.onload = () => resolve();
18
+ script.onerror = () => reject(new Error(`Failed to load script: ${url}`));
19
+ document.head.appendChild(script);
20
+ });
21
+ }
22
+
23
+ // Loads the PNG-sequence recording libraries once (in order, since they
24
+ // attach to global scope) and caches the promise so repeat calls are no-ops.
25
+ export function loadRecordingLibs() {
26
+ if (!loadPromise) {
27
+ loadPromise = RECORDING_LIB_PATHS.reduce(
28
+ (chain, path) =>
29
+ chain.then(() => loadScript(new URL(path, import.meta.url).href)),
30
+ Promise.resolve()
31
+ );
32
+ }
33
+ return loadPromise;
34
+ }
@@ -0,0 +1,226 @@
1
+ // Shareable-view-link encode/decode (see ui/Controls.js's "Copy view link"
2
+ // button and SimView.js's apply-on-load hook). Pure functions, no DOM/THREE
3
+ // dependencies, so they're unit-testable in isolation.
4
+ //
5
+ // Format (v=1): a compact `#key=value&key=value...` hash fragment, e.g.
6
+ // #v=1&t=1.25&cam=1.5,2,3&tgt=0,0,0&fov=50&b=1&bvm=mesh&tcm=height&flags=5
7
+ // `flags` is a bitmask over the boolean toggles listed in BOOLEAN_FLAG_KEYS
8
+ // below (bit order fixed by that array's order) -- new booleans get appended
9
+ // at the end of the array in future versions so old links keep decoding
10
+ // sanely (their bits just default off for the new flag).
11
+ //
12
+ // Versioned via `v` so future formats can add/rename fields; parseViewState
13
+ // only understands v=1 today and returns null for anything else.
14
+
15
+ export const VIEW_STATE_VERSION = 1;
16
+
17
+ // Fixed bit order for the `flags` bitmask -- append-only across versions.
18
+ const BOOLEAN_FLAG_KEYS = [
19
+ "axesVisible",
20
+ "trailsVisible",
21
+ "smoothInterpolation",
22
+ "terrainProbe",
23
+ "attributeVisible.contacts",
24
+ "attributeVisible.velocity",
25
+ "attributeVisible.angularVelocity",
26
+ "attributeVisible.force",
27
+ "attributeVisible.torque",
28
+ "terrainVisualizationModes.surface",
29
+ "terrainVisualizationModes.wireframe",
30
+ "terrainVisualizationModes.normals",
31
+ ];
32
+
33
+ function getPath(obj, path) {
34
+ return path.split(".").reduce((o, k) => (o && typeof o === "object" ? o[k] : undefined), obj);
35
+ }
36
+
37
+ function setPath(obj, path, value) {
38
+ const keys = path.split(".");
39
+ let node = obj;
40
+ for (let i = 0; i < keys.length - 1; i++) {
41
+ const k = keys[i];
42
+ if (typeof node[k] !== "object" || node[k] === null) node[k] = {};
43
+ node = node[k];
44
+ }
45
+ node[keys[keys.length - 1]] = value;
46
+ }
47
+
48
+ function fmtNum(n) {
49
+ // Trim to a sane precision so the hash stays compact/human-tolerable,
50
+ // while still round-tripping to full float precision for practical
51
+ // view-restoration purposes (6 significant decimals).
52
+ if (!Number.isFinite(n)) return "0";
53
+ return Number(n.toFixed(6)).toString();
54
+ }
55
+
56
+ function fmtVec3(v) {
57
+ return [fmtNum(v.x), fmtNum(v.y), fmtNum(v.z)].join(",");
58
+ }
59
+
60
+ function parseVec3(str) {
61
+ if (typeof str !== "string") return null;
62
+ const parts = str.split(",");
63
+ if (parts.length !== 3) return null;
64
+ const nums = parts.map(Number);
65
+ if (nums.some((n) => !Number.isFinite(n))) return null;
66
+ return { x: nums[0], y: nums[1], z: nums[2] };
67
+ }
68
+
69
+ // Builds the bitmask for `state.toggles` (a flat map of BOOLEAN_FLAG_KEYS ->
70
+ // boolean, see serializeViewState's `state` shape below).
71
+ function encodeFlags(toggles) {
72
+ let mask = 0;
73
+ BOOLEAN_FLAG_KEYS.forEach((key, i) => {
74
+ if (toggles && toggles[key]) mask |= 1 << i;
75
+ });
76
+ return mask;
77
+ }
78
+
79
+ function decodeFlags(mask) {
80
+ const toggles = {};
81
+ BOOLEAN_FLAG_KEYS.forEach((key, i) => {
82
+ toggles[key] = (mask & (1 << i)) !== 0;
83
+ });
84
+ return toggles;
85
+ }
86
+
87
+ // state shape (all fields optional):
88
+ // {
89
+ // time: number,
90
+ // camera: { position: {x,y,z}, target: {x,y,z}, fov: number },
91
+ // batchIndex: number,
92
+ // bodyVisualizationMode: string,
93
+ // terrainColorMode: string,
94
+ // toggles: { [key in BOOLEAN_FLAG_KEYS]?: boolean },
95
+ // }
96
+ export function serializeViewState(state) {
97
+ if (!state || typeof state !== "object") return "";
98
+ const params = [`v=${VIEW_STATE_VERSION}`];
99
+
100
+ if (Number.isFinite(state.time)) {
101
+ params.push(`t=${fmtNum(state.time)}`);
102
+ }
103
+ if (state.camera && state.camera.position) {
104
+ params.push(`cam=${fmtVec3(state.camera.position)}`);
105
+ }
106
+ if (state.camera && state.camera.target) {
107
+ params.push(`tgt=${fmtVec3(state.camera.target)}`);
108
+ }
109
+ if (state.camera && Number.isFinite(state.camera.fov)) {
110
+ params.push(`fov=${fmtNum(state.camera.fov)}`);
111
+ }
112
+ if (Number.isInteger(state.batchIndex)) {
113
+ params.push(`b=${state.batchIndex}`);
114
+ }
115
+ if (typeof state.bodyVisualizationMode === "string" && state.bodyVisualizationMode) {
116
+ params.push(`bvm=${encodeURIComponent(state.bodyVisualizationMode)}`);
117
+ }
118
+ if (typeof state.terrainColorMode === "string" && state.terrainColorMode) {
119
+ params.push(`tcm=${encodeURIComponent(state.terrainColorMode)}`);
120
+ }
121
+ if (state.toggles && typeof state.toggles === "object") {
122
+ params.push(`flags=${encodeFlags(state.toggles)}`);
123
+ }
124
+
125
+ return `#${params.join("&")}`;
126
+ }
127
+
128
+ // Tolerant parser: malformed/unknown input never throws -- worst case it
129
+ // returns null (nothing to apply) or an object missing some keys (whatever
130
+ // could be salvaged). A bad hash must never break page load.
131
+ export function parseViewState(hash) {
132
+ try {
133
+ if (typeof hash !== "string") return null;
134
+ const trimmed = hash.startsWith("#") ? hash.slice(1) : hash;
135
+ if (!trimmed) return null;
136
+
137
+ const raw = {};
138
+ for (const pair of trimmed.split("&")) {
139
+ if (!pair) continue;
140
+ const eq = pair.indexOf("=");
141
+ if (eq === -1) continue;
142
+ const key = pair.slice(0, eq);
143
+ const value = pair.slice(eq + 1);
144
+ if (key) raw[key] = value;
145
+ }
146
+
147
+ if (!("v" in raw)) return null;
148
+ const version = parseInt(raw.v, 10);
149
+ if (version !== VIEW_STATE_VERSION) return null; // unknown/unsupported version
150
+
151
+ const state = {};
152
+
153
+ if ("t" in raw) {
154
+ const t = Number(raw.t);
155
+ if (Number.isFinite(t)) state.time = t;
156
+ }
157
+
158
+ const camPos = "cam" in raw ? parseVec3(raw.cam) : null;
159
+ const camTgt = "tgt" in raw ? parseVec3(raw.tgt) : null;
160
+ const fov = "fov" in raw ? Number(raw.fov) : null;
161
+ if (camPos || camTgt || (fov !== null && Number.isFinite(fov))) {
162
+ state.camera = {};
163
+ if (camPos) state.camera.position = camPos;
164
+ if (camTgt) state.camera.target = camTgt;
165
+ if (fov !== null && Number.isFinite(fov)) state.camera.fov = fov;
166
+ }
167
+
168
+ if ("b" in raw) {
169
+ const b = parseInt(raw.b, 10);
170
+ if (Number.isInteger(b) && b >= 0) state.batchIndex = b;
171
+ }
172
+
173
+ if ("bvm" in raw) {
174
+ try {
175
+ const bvm = decodeURIComponent(raw.bvm);
176
+ if (bvm) state.bodyVisualizationMode = bvm;
177
+ } catch {
178
+ /* malformed percent-encoding: ignore this field */
179
+ }
180
+ }
181
+
182
+ if ("tcm" in raw) {
183
+ try {
184
+ const tcm = decodeURIComponent(raw.tcm);
185
+ if (tcm) state.terrainColorMode = tcm;
186
+ } catch {
187
+ /* malformed percent-encoding: ignore this field */
188
+ }
189
+ }
190
+
191
+ if ("flags" in raw) {
192
+ const mask = parseInt(raw.flags, 10);
193
+ if (Number.isInteger(mask)) {
194
+ state.toggles = decodeFlags(mask);
195
+ }
196
+ }
197
+
198
+ return state;
199
+ } catch {
200
+ // Never let a malformed hash break page load.
201
+ return null;
202
+ }
203
+ }
204
+
205
+ // Helper for callers building the `toggles` map from a live uiState object
206
+ // (see ui/Controls.js) -- flat-keyed via getPath/setPath so nested paths
207
+ // like "attributeVisible.contacts" work without callers reimplementing the
208
+ // dotted-path walk.
209
+ export function toggleMapFromUiState(uiState) {
210
+ const toggles = {};
211
+ BOOLEAN_FLAG_KEYS.forEach((key) => {
212
+ toggles[key] = !!getPath(uiState, key);
213
+ });
214
+ return toggles;
215
+ }
216
+
217
+ // Applies a decoded `toggles` map (as produced by decodeFlags/parseViewState)
218
+ // onto a target uiState-shaped object in place.
219
+ export function applyToggleMapToUiState(uiState, toggles) {
220
+ if (!toggles) return;
221
+ BOOLEAN_FLAG_KEYS.forEach((key) => {
222
+ if (key in toggles) setPath(uiState, key, !!toggles[key]);
223
+ });
224
+ }
225
+
226
+ export { BOOLEAN_FLAG_KEYS };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Minified by jsDelivr using Terser v5.39.0.
3
+ * Original file: /npm/chroma-js@3.1.2/index.js
4
+ *
5
+ * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
6
+ */
7
+ import chroma from"./src/chroma.js";import"./src/io/named/index.js";import"./src/ops/alpha.js";import"./src/ops/clipped.js";import"./src/ops/darken.js";import"./src/ops/get.js";import"./src/ops/luminance.js";import"./src/ops/mix.js";import"./src/ops/premultiply.js";import"./src/ops/saturate.js";import"./src/ops/set.js";import"./src/ops/shade.js";import"./src/interpolator/rgb.js";import"./src/interpolator/lrgb.js";import"./src/interpolator/lab.js";import"./src/interpolator/lch.js";import"./src/interpolator/num.js";import"./src/interpolator/hcg.js";import"./src/interpolator/hsi.js";import"./src/interpolator/hsl.js";import"./src/interpolator/hsv.js";import"./src/interpolator/oklab.js";import"./src/interpolator/oklch.js";import average from"./src/generator/average.js";import bezier from"./src/generator/bezier.js";import blend from"./src/generator/blend.js";import cubehelix from"./src/generator/cubehelix.js";import mix from"./src/generator/mix.js";import random from"./src/generator/random.js";import scale from"./src/generator/scale.js";import{analyze}from"./src/utils/analyze.js";import contrast from"./src/utils/contrast.js";import contrastAPCA from"./src/utils/contrastAPCA.js";import deltaE from"./src/utils/delta-e.js";import distance from"./src/utils/distance.js";import{limits}from"./src/utils/analyze.js";import valid from"./src/utils/valid.js";import input from"./src/io/input.js";import scales from"./src/utils/scales.js";import colors from"./src/colors/w3cx11.js";import brewer from"./src/colors/colorbrewer.js";import Color from"./src/Color.js";Object.assign(chroma,{analyze:analyze,average:average,bezier:bezier,blend:blend,brewer:brewer,Color:Color,colors:colors,contrast:contrast,contrastAPCA:contrastAPCA,cubehelix:cubehelix,deltaE:deltaE,distance:distance,input:input,interpolate:mix,limits:limits,mix:mix,random:random,scale:scale,scales:scales,valid:valid});export default chroma;export{analyze,average,bezier,blend,brewer,Color,colors,contrast,contrastAPCA,cubehelix,deltaE,distance,input,limits,mix,mix as interpolate,random,scale,scales,valid};export*from"./src/io/cmyk/index.js";export*from"./src/io/css/index.js";export*from"./src/io/gl/index.js";export*from"./src/io/hcg/index.js";export*from"./src/io/hex/index.js";export*from"./src/io/hsi/index.js";export*from"./src/io/hsl/index.js";export*from"./src/io/hsv/index.js";export*from"./src/io/lab/index.js";export*from"./src/io/lch/index.js";export*from"./src/io/num/index.js";export*from"./src/io/rgb/index.js";export*from"./src/io/temp/index.js";export*from"./src/io/oklab/index.js";export*from"./src/io/oklch/index.js";
8
+ //# sourceMappingURL=/sm/d1d6f482c381dd513a8673b799d59dde187296efa5ac5889bc30b3834aa7ff07.map