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.
- simview/__init__.py +69 -0
- simview/__main__.py +174 -0
- simview/launcher.py +109 -0
- simview/live.py +204 -0
- simview/merge.py +593 -0
- simview/model.py +690 -0
- simview/py.typed +0 -0
- simview/scene.py +603 -0
- simview/server.py +739 -0
- simview/state.py +190 -0
- simview/static/css/controls.css +36 -0
- simview/static/js/SimView.js +601 -0
- simview/static/js/components/AnimationController.js +654 -0
- simview/static/js/components/BatchManager.js +198 -0
- simview/static/js/components/InteractionController.js +446 -0
- simview/static/js/components/InteractionControls.js +116 -0
- simview/static/js/components/Scene.js +166 -0
- simview/static/js/components/StateStore.js +188 -0
- simview/static/js/config.js +233 -0
- simview/static/js/main.js +3 -0
- simview/static/js/objects/Body.js +616 -0
- simview/static/js/objects/StaticObject.js +181 -0
- simview/static/js/objects/Terrain.js +514 -0
- simview/static/js/objects/utils.js +378 -0
- simview/static/js/ui/AnalysisPanel.js +233 -0
- simview/static/js/ui/BatchLegend.js +207 -0
- simview/static/js/ui/BodyStateWindow.js +521 -0
- simview/static/js/ui/Controls.js +586 -0
- simview/static/js/ui/ErrorMetrics.js +664 -0
- simview/static/js/ui/Legend.js +87 -0
- simview/static/js/ui/PlaybackControls.js +314 -0
- simview/static/js/ui/ScalarPlotter.js +575 -0
- simview/static/js/utils/blobCodec.js +73 -0
- simview/static/js/utils/bodyTransforms.js +151 -0
- simview/static/js/utils/csv.js +46 -0
- simview/static/js/utils/errorMath.js +69 -0
- simview/static/js/utils/injectStyles.js +16 -0
- simview/static/js/utils/interpolate.js +65 -0
- simview/static/js/utils/liveFollow.js +17 -0
- simview/static/js/utils/loadRecordingLibs.js +34 -0
- simview/static/js/utils/viewState.js +226 -0
- simview/static/lib/chroma-js-3.1.2/index.min.js +8 -0
- simview/static/lib/chroma-js-3.1.2/src/Color.js +49 -0
- simview/static/lib/chroma-js-3.1.2/src/chroma.js +10 -0
- simview/static/lib/chroma-js-3.1.2/src/colors/colorbrewer.js +81 -0
- simview/static/lib/chroma-js-3.1.2/src/colors/w3cx11.js +164 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/average.js +88 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/bezier.js +86 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/blend.js +57 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/cubehelix.js +87 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/mix.js +19 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/random.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/scale.js +394 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/_hsx.js +59 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/hcg.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/hsi.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/hsl.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/hsv.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/index.js +1 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/lab.js +19 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/lch.js +13 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/lrgb.js +19 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/num.js +15 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/oklab.js +19 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/oklch.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/rgb.js +18 -0
- simview/static/lib/chroma-js-3.1.2/src/io/cmyk/cmyk2rgb.js +16 -0
- simview/static/lib/chroma-js-3.1.2/src/io/cmyk/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/cmyk/rgb2cmyk.js +17 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/css2rgb.js +238 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/hsl2css.js +26 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/lab2css.js +24 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/lch2css.js +24 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/oklab2css.js +16 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/oklch2css.js +16 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/rgb2css.js +61 -0
- simview/static/lib/chroma-js-3.1.2/src/io/gl/index.js +22 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hcg/hcg2rgb.js +55 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hcg/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hcg/rgb2hcg.js +23 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hex/hex2rgb.js +56 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hex/index.js +29 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hex/rgb2hex.js +29 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsi/hsi2rgb.js +45 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsi/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsi/rgb2hsi.js +31 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsl/hsl2rgb.js +35 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsl/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsl/rgb2hsl.js +45 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsv/hsv2rgb.js +47 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsv/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsv/rgb2hsv.js +32 -0
- simview/static/lib/chroma-js-3.1.2/src/io/input.js +4 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lab/index.js +28 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lab/lab-constants.js +120 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lab/lab2rgb.js +101 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lab/rgb2lab.js +67 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lch/hcl2rgb.js +9 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lch/index.js +35 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lch/lab2lch.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2lab.js +18 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2rgb.js +13 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lch/rgb2lch.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/io/named/index.js +30 -0
- simview/static/lib/chroma-js-3.1.2/src/io/num/index.js +32 -0
- simview/static/lib/chroma-js-3.1.2/src/io/num/num2rgb.js +13 -0
- simview/static/lib/chroma-js-3.1.2/src/io/num/rgb2num.js +8 -0
- simview/static/lib/chroma-js-3.1.2/src/io/oklab/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/oklab/oklab2rgb.js +34 -0
- simview/static/lib/chroma-js-3.1.2/src/io/oklab/rgb2oklab.js +37 -0
- simview/static/lib/chroma-js-3.1.2/src/io/oklch/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/oklch/oklch2rgb.js +13 -0
- simview/static/lib/chroma-js-3.1.2/src/io/oklch/rgb2oklch.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/io/rgb/index.js +44 -0
- simview/static/lib/chroma-js-3.1.2/src/io/temp/index.js +22 -0
- simview/static/lib/chroma-js-3.1.2/src/io/temp/rgb2temperature.js +30 -0
- simview/static/lib/chroma-js-3.1.2/src/io/temp/temperature2rgb.js +39 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/alpha.js +13 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/clipped.js +5 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/darken.js +17 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/get.js +13 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/luminance.js +54 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/mix.js +10 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/premultiply.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/saturate.js +15 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/set.js +43 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/shade.js +11 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/analyze.js +191 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/clip_rgb.js +15 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/contrast.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/contrastAPCA.js +68 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/delta-e.js +62 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/distance.js +17 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/last.js +8 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/limit.js +5 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/multiply-matrices.js +36 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/scales.js +15 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/type.js +18 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/unpack.js +17 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/valid.js +11 -0
- simview/static/lib/chroma-js-3.1.2/src/version.js +2 -0
- simview/static/lib/download.js +132 -0
- simview/static/lib/js-colormaps.js +19335 -0
- simview/static/lib/tar.js +334 -0
- simview/static/lib/three-0.174.0/addons/controls/OrbitControls.js +1556 -0
- simview/static/lib/three-0.174.0/addons/libs/lil-gui.module.min.js +8 -0
- simview/static/lib/three-0.174.0/three.core.js +48830 -0
- simview/static/lib/three-0.174.0/three.module.js +17313 -0
- simview/static/lib/uPlot.esm.js +6140 -0
- simview/static/lib/uPlot.min.css +2 -0
- simview/static/textures/contacts/red-cross0.png +0 -0
- simview/static/textures/points/ball0.png +0 -0
- simview/static/textures/points/ball1.png +0 -0
- simview/templates/index.html +81 -0
- simview/utils.py +41 -0
- simview-3.2.dist-info/METADATA +552 -0
- simview-3.2.dist-info/RECORD +163 -0
- simview-3.2.dist-info/WHEEL +5 -0
- simview-3.2.dist-info/entry_points.txt +2 -0
- simview-3.2.dist-info/licenses/LICENSE +28 -0
- 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
|