diff --git a/.gitignore b/.gitignore
index a54cf36..94dd417 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,4 @@ data/geo_audit*.json
logs/
data/art_cache/
data/img_cache/
+tools/glb-split/incoming/
diff --git a/frontend/src/lib/zen/aquarium.js b/frontend/src/lib/zen/aquarium.js
index 7b4ee25..e7c2eb4 100644
--- a/frontend/src/lib/zen/aquarium.js
+++ b/frontend/src/lib/zen/aquarium.js
@@ -16,22 +16,20 @@
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
-const MODEL_URL = '/models/ub-split.glb';
+const MODEL_URL = '/models/ub-angelfish.glb';
-// Converged Phase-A starting point (Claude + Codex): a hair off strict profile so
-// the mouth/chin reads naturally; tail OPAQUE alpha-tested (one coherent tail, no
-// blend bleed); fins translucent + single-sided (kills the double-sided ghosting
-// that read as an off-center dorsal). All live-tunable at /zen?debug=1.
+// UB is now a marine angelfish (Queen): ONE mesh, TWO materials (…_body, …_fins — fins
+// covers the caudal tail too), and a single trimmed Idle loop. Body renders OPAQUE +
+// single-sided; fins default to OPAQUE alpha-tested (clean silhouette, no blend bleed —
+// the koi's lesson) but can go translucent. All live-tunable at /zen?debug=1.
export const DEFAULTS = {
- yaw: Math.PI / 2 + 0.10, // ub.rotation.y — slight 3/4 view
+ yaw: Math.PI / 2, // ub.rotation.y — side-on-ish (tune live; head faces ±Z)
pitch: 0, // ub.rotation.x — nose up/down
- tailTranslucent: false, // false = opaque alpha-tested (coherent); true = blended
- tailSide: 'front', // front | back | double
- tailAlphaTest: 0.035,
- tailOpacity: 1.0, // only when translucent
- finSide: 'front', // front | back | double
- finOpacity: 0.75,
- finAlphaTest: 0.02,
+ scale: 1, // multiplier on the auto-fit size
+ finTranslucent: false, // false = opaque alpha-tested (coherent); true = blended
+ finSide: 'double', // front | back | double (fins are thin → double reads fuller)
+ finOpacity: 0.9, // only when translucent
+ finAlphaTest: 0.5, // clip the fin-edge alpha
paused: false,
frame: 0, // 0..1 scrub position when paused
};
@@ -76,43 +74,41 @@ export async function createAquarium(canvas, initial = {}) {
const center = box.getCenter(new THREE.Vector3());
ub.position.sub(center);
const maxDim = Math.max(size.x, size.y, size.z) || 1;
- ub.scale.setScalar(2.6 / maxDim);
+ const baseScale = 2.6 / maxDim; // auto-fit; params.scale fine-tunes it live
- // Collect the three named materials/meshes so applyMaterials can retarget them
- // live without re-traversing semantics.
+ // Classify the two meshes by material-name substring (…_body / …_fins) so
+ // applyMaterials can retarget them live regardless of the species prefix.
const part = {};
- ub.traverse((o) => { if (o.isMesh && o.material?.name) part[o.material.name] = o; });
+ ub.traverse((o) => {
+ if (!o.isMesh || !o.material?.name) return;
+ const n = o.material.name.toLowerCase();
+ if (n.includes('body')) part.body = o; else if (n.includes('fin')) part.fins = o;
+ });
function applyMaterials() {
- const body = part.UB_Body, fins = part.UB_Fins, tail = part.UB_Tail;
- if (body) {
+ const body = part.body, fins = part.fins;
+ if (body) { // body: always OPAQUE, single-sided
const m = body.material;
m.transparent = false; m.opacity = 1; m.alphaTest = 0;
m.depthWrite = true; m.depthTest = true; m.side = THREE.FrontSide;
body.renderOrder = 1; m.needsUpdate = true;
}
- if (fins) {
+ if (fins) { // fins (+ tail): opaque alpha-tested by default
const m = fins.material;
- m.transparent = true; m.opacity = params.finOpacity; m.alphaTest = params.finAlphaTest;
- m.alphaToCoverage = true; m.depthWrite = false; m.depthTest = true;
- m.side = SIDE[params.finSide] ?? THREE.FrontSide;
- fins.renderOrder = 3; m.needsUpdate = true;
- }
- if (tail) {
- const m = tail.material;
- m.alphaTest = params.tailAlphaTest; m.alphaToCoverage = true; m.depthTest = true;
- m.side = SIDE[params.tailSide] ?? THREE.FrontSide;
- if (params.tailTranslucent) {
- m.transparent = true; m.opacity = params.tailOpacity; m.depthWrite = false;
+ m.alphaTest = params.finAlphaTest; m.alphaToCoverage = true; m.depthTest = true;
+ m.side = SIDE[params.finSide] ?? THREE.DoubleSide;
+ if (params.finTranslucent) {
+ m.transparent = true; m.opacity = params.finOpacity; m.depthWrite = false;
} else {
- m.transparent = false; m.opacity = 1; m.depthWrite = true; // opaque, coherent
+ m.transparent = false; m.opacity = 1; m.depthWrite = true; // clean, coherent
}
- tail.renderOrder = 2; m.needsUpdate = true;
+ fins.renderOrder = 2; m.needsUpdate = true;
}
}
function applyTransform() {
ub.rotation.set(params.pitch, params.yaw, 0);
+ ub.scale.setScalar(baseScale * (params.scale ?? 1));
}
applyMaterials();
@@ -124,7 +120,7 @@ export async function createAquarium(canvas, initial = {}) {
const mixer = new THREE.AnimationMixer(ub);
const actions = {};
for (const clip of gltf.animations) actions[clip.name] = mixer.clipAction(clip);
- const baseClip = (actions.Idle_swim ? actions.Idle_swim : mixer.clipAction(gltf.animations[0]));
+ const baseClip = mixer.clipAction(gltf.animations[0]); // the trimmed Idle loop
baseClip.play();
const baseDuration = baseClip.getClip().duration || 1;
mixer.timeScale = reduced ? 0.6 : 1; // calmer when reduced-motion
diff --git a/frontend/src/routes/zen/+page.svelte b/frontend/src/routes/zen/+page.svelte
index b55b549..ccfb62d 100644
--- a/frontend/src/routes/zen/+page.svelte
+++ b/frontend/src/routes/zen/+page.svelte
@@ -2,7 +2,7 @@
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
- import { auth } from '$lib/auth.svelte.js';
+ import { auth, refresh as refreshAuth } from '$lib/auth.svelte.js';
import { isDevGated, blockedForViewer } from '$lib/devgate.js';
import Footer from '$lib/components/Footer.svelte';
@@ -25,12 +25,17 @@
}
onMount(() => {
- // Dev-gated while UB is being ironed out: non-admins (no preview token) bounce.
- if (blockedForViewer('zen', auth.user, $page.url)) { goto('/play'); return; }
- debug = $page.url.searchParams.get('debug') === '1';
let h;
let cancelled = false; // guard the async load against an early unmount
(async () => {
+ // Don't decide the gate until we actually KNOW who's here. A cold load / hard
+ // refresh / direct link mounts before auth has revalidated, so auth.user is still
+ // null and an admin would be wrongly bounced. Revalidate first, THEN gate.
+ if (!auth.ready) { try { await refreshAuth(); } catch { /* offline → treat as gated */ } }
+ if (cancelled) return;
+ // Dev-gated while UB is being ironed out: non-admins (no preview token) bounce.
+ if (blockedForViewer('zen', auth.user, $page.url)) { goto('/play'); return; }
+ debug = $page.url.searchParams.get('debug') === '1';
try {
// WebGL guard — fall back to a warm card rather than a blank canvas.
const t = document.createElement('canvas');
@@ -88,27 +93,20 @@
+
- Tail
-
-
-
- {#if dbg.tailTranslucent}
-
- {/if}
-
-
- Fins
+ Fins & tail
+
-
+
+ {#if dbg.finTranslucent}
+
+ {/if}
diff --git a/frontend/static/models/ub-angelfish.glb b/frontend/static/models/ub-angelfish.glb
new file mode 100644
index 0000000..54ba103
Binary files /dev/null and b/frontend/static/models/ub-angelfish.glb differ
diff --git a/frontend/static/models/ub-split.glb b/frontend/static/models/ub-split.glb
deleted file mode 100644
index 7442cc8..0000000
Binary files a/frontend/static/models/ub-split.glb and /dev/null differ
diff --git a/frontend/static/models/ub.glb b/frontend/static/models/ub.glb
deleted file mode 100644
index d51050a..0000000
Binary files a/frontend/static/models/ub.glb and /dev/null differ
diff --git a/tools/glb-split/angelfish-clips.json b/tools/glb-split/angelfish-clips.json
new file mode 100644
index 0000000..523a565
--- /dev/null
+++ b/tools/glb-split/angelfish-clips.json
@@ -0,0 +1,315 @@
+{
+ "note": "Angelfish Animated Pack 10 (CGTrader). One baked 75.67s/2270-frame Take at 30fps in each GLB. GLB frame = Maya frame + 110. Slice with THREE.AnimationUtils.subclip(clip,name,startFrame,endFrame,30).",
+ "fps": 30,
+ "total_frames": 2270,
+ "clips": {
+ "Swim1_norm": {
+ "glb_frames": [
+ 110,
+ 185
+ ],
+ "seconds": [
+ 3.667,
+ 6.167
+ ],
+ "maya_frames": [
+ 0,
+ 75
+ ]
+ },
+ "Turn_R_in": {
+ "glb_frames": [
+ 186,
+ 255
+ ],
+ "seconds": [
+ 6.2,
+ 8.5
+ ],
+ "maya_frames": [
+ 76,
+ 145
+ ]
+ },
+ "Turn_R_loop": {
+ "glb_frames": [
+ 256,
+ 330
+ ],
+ "seconds": [
+ 8.533,
+ 11.0
+ ],
+ "maya_frames": [
+ 146,
+ 220
+ ]
+ },
+ "Turn_R_out": {
+ "glb_frames": [
+ 331,
+ 409
+ ],
+ "seconds": [
+ 11.033,
+ 13.633
+ ],
+ "maya_frames": [
+ 221,
+ 299
+ ]
+ },
+ "Swim2_Fast": {
+ "glb_frames": [
+ 410,
+ 450
+ ],
+ "seconds": [
+ 13.667,
+ 15.0
+ ],
+ "maya_frames": [
+ 300,
+ 340
+ ]
+ },
+ "Turn_L_in": {
+ "glb_frames": [
+ 486,
+ 535
+ ],
+ "seconds": [
+ 16.2,
+ 17.833
+ ],
+ "maya_frames": [
+ 376,
+ 425
+ ]
+ },
+ "Turn_L_loop": {
+ "glb_frames": [
+ 536,
+ 610
+ ],
+ "seconds": [
+ 17.867,
+ 20.333
+ ],
+ "maya_frames": [
+ 426,
+ 500
+ ]
+ },
+ "Turn_L_out": {
+ "glb_frames": [
+ 611,
+ 710
+ ],
+ "seconds": [
+ 20.367,
+ 23.667
+ ],
+ "maya_frames": [
+ 501,
+ 600
+ ]
+ },
+ "Attack": {
+ "glb_frames": [
+ 711,
+ 751
+ ],
+ "seconds": [
+ 23.7,
+ 25.033
+ ],
+ "maya_frames": [
+ 601,
+ 641
+ ]
+ },
+ "Eat_Swim": {
+ "glb_frames": [
+ 752,
+ 811
+ ],
+ "seconds": [
+ 25.067,
+ 27.033
+ ],
+ "maya_frames": [
+ 642,
+ 701
+ ]
+ },
+ "Eat_Ground": {
+ "glb_frames": [
+ 812,
+ 852
+ ],
+ "seconds": [
+ 27.067,
+ 28.4
+ ],
+ "maya_frames": [
+ 702,
+ 742
+ ]
+ },
+ "Eat_Wall": {
+ "glb_frames": [
+ 853,
+ 893
+ ],
+ "seconds": [
+ 28.433,
+ 29.767
+ ],
+ "maya_frames": [
+ 743,
+ 783
+ ]
+ },
+ "Turn_L_Fast": {
+ "glb_frames": [
+ 894,
+ 923
+ ],
+ "seconds": [
+ 29.8,
+ 30.767
+ ],
+ "maya_frames": [
+ 784,
+ 813
+ ]
+ },
+ "Turn_R_Fast": {
+ "glb_frames": [
+ 924,
+ 953
+ ],
+ "seconds": [
+ 30.8,
+ 31.767
+ ],
+ "maya_frames": [
+ 814,
+ 843
+ ]
+ },
+ "Swim3_Long_Wide": {
+ "glb_frames": [
+ 954,
+ 1401
+ ],
+ "seconds": [
+ 31.8,
+ 46.7
+ ],
+ "maya_frames": [
+ 844,
+ 1291
+ ]
+ },
+ "Swim4_Long_Near": {
+ "glb_frames": [
+ 1402,
+ 1852
+ ],
+ "seconds": [
+ 46.733,
+ 61.733
+ ],
+ "maya_frames": [
+ 1292,
+ 1742
+ ]
+ },
+ "Death1 Sink Start": {
+ "glb_frames": [
+ 1853,
+ 1930
+ ],
+ "seconds": [
+ 61.767,
+ 64.333
+ ],
+ "maya_frames": [
+ 1743,
+ 1820
+ ]
+ },
+ "Death1 Sink Loop": {
+ "glb_frames": [
+ 1930,
+ 1987
+ ],
+ "seconds": [
+ 64.333,
+ 66.233
+ ],
+ "maya_frames": [
+ 1820,
+ 1877
+ ]
+ },
+ "Death2 Float Start": {
+ "glb_frames": [
+ 1988,
+ 2120
+ ],
+ "seconds": [
+ 66.267,
+ 70.667
+ ],
+ "maya_frames": [
+ 1878,
+ 2010
+ ]
+ },
+ "Death2 Float Loop": {
+ "glb_frames": [
+ 2121,
+ 2210
+ ],
+ "seconds": [
+ 70.7,
+ 73.667
+ ],
+ "maya_frames": [
+ 2011,
+ 2100
+ ]
+ },
+ "Jump": {
+ "glb_frames": [
+ 2211,
+ 2270
+ ],
+ "seconds": [
+ 73.7,
+ 75.667
+ ],
+ "maya_frames": [
+ 2101,
+ 2160
+ ]
+ },
+ "Idle": {
+ "glb_frames": [
+ 0,
+ 109
+ ],
+ "seconds": [
+ 0.0,
+ 3.633
+ ],
+ "maya_frames": [
+ -110,
+ -1
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/glb-split/inspect.mjs b/tools/glb-split/inspect.mjs
new file mode 100644
index 0000000..943b477
--- /dev/null
+++ b/tools/glb-split/inspect.mjs
@@ -0,0 +1,71 @@
+// Generic GLB structural audit (species-agnostic). For each file: mesh/poly counts,
+// materials with alphaMode + doubleSided (the koi's failure was one BLEND double-sided
+// mesh), textures, skin/joint count, animation clip names (must carry Idle/Swim in the
+// GLB, not just the Maya source), bbox proportions, and a crude "two-tail" check —
+// histogram of the rear-slice vertices along the thin (left/right) axis: a single peak
+// at centre = one tail; two side peaks with a centre gap = a forked / double tail.
+import { NodeIO } from '@gltf-transform/core';
+import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
+
+const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
+const path = process.argv[2];
+const doc = await io.read(path);
+const root = doc.getRoot();
+const base = path.split('/').pop();
+
+const meshes = root.listMeshes();
+let prims = 0, tris = 0, verts = 0;
+const bb = { min: [1e9, 1e9, 1e9], max: [-1e9, -1e9, -1e9] };
+const allPos = [];
+for (const m of meshes) for (const p of m.listPrimitives()) {
+ prims++;
+ const pos = p.getAttribute('POSITION');
+ const idx = p.getIndices();
+ tris += idx ? idx.getCount() / 3 : pos.getCount() / 3;
+ verts += pos.getCount();
+ const v = [0, 0, 0];
+ for (let i = 0; i < pos.getCount(); i++) {
+ pos.getElement(i, v);
+ allPos.push(v[0], v[1], v[2]);
+ for (let d = 0; d < 3; d++) { bb.min[d] = Math.min(bb.min[d], v[d]); bb.max[d] = Math.max(bb.max[d], v[d]); }
+ }
+}
+const size = bb.max.map((m, d) => m - bb.min[d]);
+const mats = root.listMaterials();
+const texs = root.listTextures();
+const skin = root.listSkins()[0];
+const joints = skin ? skin.listJoints() : [];
+const anims = root.listAnimations();
+
+// axes: width = thinnest (fish are laterally compressed); length = longest of the other two.
+const widthAxis = size.indexOf(Math.min(...size));
+const other = [0, 1, 2].filter((d) => d !== widthAxis);
+const lenAxis = other[0] === undefined ? 0 : (size[other[0]] >= size[other[1]] ? other[0] : other[1]);
+// rear slice = last 22% along the length axis (either end — pick the end with fewer verts = the tail, not the body)
+const loEnd = bb.min[lenAxis] + 0.22 * size[lenAxis];
+const hiEnd = bb.max[lenAxis] - 0.22 * size[lenAxis];
+let loN = 0, hiN = 0;
+for (let i = 0; i < allPos.length; i += 3) { const L = allPos[i + lenAxis]; if (L <= loEnd) loN++; else if (L >= hiEnd) hiN++; }
+const tailIsLo = loN <= hiN;
+const wc = (bb.min[widthAxis] + bb.max[widthAxis]) / 2, wHalf = size[widthAxis] / 2 || 1;
+const bins = new Array(11).fill(0);
+let tailN = 0;
+for (let i = 0; i < allPos.length; i += 3) {
+ const L = allPos[i + lenAxis];
+ if (tailIsLo ? L > loEnd : L < hiEnd) continue;
+ tailN++;
+ const t = (allPos[i + widthAxis] - wc) / wHalf; // -1..1 across width
+ bins[Math.max(0, Math.min(10, Math.round((t + 1) * 5)))]++;
+}
+const centreFrac = tailN ? (bins[4] + bins[5] + bins[6]) / tailN : 0; // fraction near centre-plane
+
+const AX = ['X', 'Y', 'Z'];
+console.log(`\n== ${base} ==`);
+console.log(` meshes ${meshes.length} prims ${prims} tris ${Math.round(tris)} verts ${verts}`);
+console.log(` materials ${mats.length}: ${mats.map((m) => `${m.getName() || '—'}[${m.getAlphaMode()}${m.getDoubleSided() ? ',2SIDED' : ''}]`).join(' ')}`);
+console.log(` textures ${texs.length} skin joints ${joints.length}`);
+console.log(` anims ${anims.length}: ${anims.map((a) => a.getName()).join(', ') || '(none in GLB!)'}`);
+console.log(` bbox size ${AX.map((a, d) => `${a}=${size[d].toFixed(2)}`).join(' ')} (len axis ${AX[lenAxis]}, width axis ${AX[widthAxis]})`);
+console.log(` tail slice: ${tailN} verts, ${(centreFrac * 100).toFixed(0)}% near centre-plane → ${centreFrac > 0.5 ? 'SINGLE tail (centred)' : 'possible fork/side-lobes — eyeball it'}`);
+const tj = joints.filter((j) => /tail|caudal|fork|fin/i.test(j.getName())).map((j) => j.getName());
+if (tj.length) console.log(` fin/tail joints: ${tj.join(', ')}`);
diff --git a/tools/glb-split/package-lock.json b/tools/glb-split/package-lock.json
index d70f96f..4420527 100644
--- a/tools/glb-split/package-lock.json
+++ b/tools/glb-split/package-lock.json
@@ -7,7 +7,17 @@
"name": "glb-split",
"dependencies": {
"@gltf-transform/core": "^4.4.0",
- "@gltf-transform/extensions": "^4.4.0"
+ "@gltf-transform/extensions": "^4.4.0",
+ "@gltf-transform/functions": "^4.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
}
},
"node_modules/@gltf-transform/core": {
@@ -33,15 +43,604 @@
"url": "https://github.com/sponsors/donmccurdy"
}
},
+ "node_modules/@gltf-transform/functions": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@gltf-transform/functions/-/functions-4.4.0.tgz",
+ "integrity": "sha512-CaSTAVAd2NXNWsxdgvq090rKHqy7AQlcNWV4ec7xtQyS8WEv3S3gVN27ikWmdB8nWEsXUbOIDhtPMLbXI6xDJg==",
+ "dependencies": {
+ "@gltf-transform/core": "^4.4.0",
+ "@gltf-transform/extensions": "^4.4.0",
+ "ktx-parse": "^1.1.0",
+ "ndarray": "^1.0.19",
+ "ndarray-lanczos": "^0.3.0",
+ "ndarray-pixels": "^5.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/donmccurdy"
+ }
+ },
+ "node_modules/@img/colour": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
+ "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
+ "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
+ "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
+ "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
+ "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
+ "cpu": [
+ "arm"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
+ "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
+ "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
+ "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
+ "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
+ "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
+ "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
+ "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
+ "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
+ "cpu": [
+ "arm"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
+ "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-ppc64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
+ "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-ppc64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-riscv64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
+ "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-riscv64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
+ "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
+ "cpu": [
+ "s390x"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
+ "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
+ "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
+ "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
+ "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
+ "cpu": [
+ "wasm32"
+ ],
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.7.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
+ "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
+ "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
+ "cpu": [
+ "ia32"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
+ "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@types/ndarray": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/@types/ndarray/-/ndarray-1.0.14.tgz",
+ "integrity": "sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg=="
+ },
+ "node_modules/cwise-compiler": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/cwise-compiler/-/cwise-compiler-1.1.3.tgz",
+ "integrity": "sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==",
+ "dependencies": {
+ "uniq": "^1.0.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/iota-array": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz",
+ "integrity": "sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA=="
+ },
+ "node_modules/is-buffer": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
+ },
"node_modules/ktx-parse": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-1.1.0.tgz",
"integrity": "sha512-mKp3y+FaYgR7mXWAbyyzpa/r1zDWeaunH+INJO4fou3hb45XuNSwar+7llrRyvpMWafxSIi99RNFJ05MHedaJQ=="
},
+ "node_modules/ndarray": {
+ "version": "1.0.19",
+ "resolved": "https://registry.npmjs.org/ndarray/-/ndarray-1.0.19.tgz",
+ "integrity": "sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ==",
+ "dependencies": {
+ "iota-array": "^1.0.0",
+ "is-buffer": "^1.0.2"
+ }
+ },
+ "node_modules/ndarray-lanczos": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/ndarray-lanczos/-/ndarray-lanczos-0.3.0.tgz",
+ "integrity": "sha512-5kBmmG3Zvyj77qxIAC4QFLKuYdDIBJwCG+DukT6jQHNa1Ft74/hPH1z5mbQXeHBt8yvGPBGVrr3wEOdJPYYZYg==",
+ "dependencies": {
+ "@types/ndarray": "^1.0.11",
+ "ndarray": "^1.0.19"
+ }
+ },
+ "node_modules/ndarray-ops": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/ndarray-ops/-/ndarray-ops-1.2.2.tgz",
+ "integrity": "sha512-BppWAFRjMYF7N/r6Ie51q6D4fs0iiGmeXIACKY66fLpnwIui3Wc3CXiD/30mgLbDjPpSLrsqcp3Z62+IcHZsDw==",
+ "dependencies": {
+ "cwise-compiler": "^1.0.0"
+ }
+ },
+ "node_modules/ndarray-pixels": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ndarray-pixels/-/ndarray-pixels-5.0.1.tgz",
+ "integrity": "sha512-IBtrpefpqlI8SPDCGjXk4v5NV5z7r3JSuCbfuEEXaM0vrOJtNGgYUa4C3Lt5H+qWdYF4BCPVFsnXhNC7QvZwkw==",
+ "dependencies": {
+ "@types/ndarray": "^1.0.14",
+ "ndarray": "^1.0.19",
+ "ndarray-ops": "^1.2.2",
+ "sharp": "^0.34.0"
+ }
+ },
"node_modules/property-graph": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/property-graph/-/property-graph-4.1.0.tgz",
"integrity": "sha512-AvPcP7XECNWy4LGmFQ77k7un4lSKM4eS29PTvW4ck95uYeLxXPWJM7hLuBqK91FaHqCcgJvIUCuNJjjxKE7VKQ=="
+ },
+ "node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sharp": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
+ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@img/colour": "^1.0.0",
+ "detect-libc": "^2.1.2",
+ "semver": "^7.7.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.34.5",
+ "@img/sharp-darwin-x64": "0.34.5",
+ "@img/sharp-libvips-darwin-arm64": "1.2.4",
+ "@img/sharp-libvips-darwin-x64": "1.2.4",
+ "@img/sharp-libvips-linux-arm": "1.2.4",
+ "@img/sharp-libvips-linux-arm64": "1.2.4",
+ "@img/sharp-libvips-linux-ppc64": "1.2.4",
+ "@img/sharp-libvips-linux-riscv64": "1.2.4",
+ "@img/sharp-libvips-linux-s390x": "1.2.4",
+ "@img/sharp-libvips-linux-x64": "1.2.4",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
+ "@img/sharp-linux-arm": "0.34.5",
+ "@img/sharp-linux-arm64": "0.34.5",
+ "@img/sharp-linux-ppc64": "0.34.5",
+ "@img/sharp-linux-riscv64": "0.34.5",
+ "@img/sharp-linux-s390x": "0.34.5",
+ "@img/sharp-linux-x64": "0.34.5",
+ "@img/sharp-linuxmusl-arm64": "0.34.5",
+ "@img/sharp-linuxmusl-x64": "0.34.5",
+ "@img/sharp-wasm32": "0.34.5",
+ "@img/sharp-win32-arm64": "0.34.5",
+ "@img/sharp-win32-ia32": "0.34.5",
+ "@img/sharp-win32-x64": "0.34.5"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "optional": true
+ },
+ "node_modules/uniq": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
+ "integrity": "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA=="
}
}
}
diff --git a/tools/glb-split/package.json b/tools/glb-split/package.json
index 11202b9..829cd71 100644
--- a/tools/glb-split/package.json
+++ b/tools/glb-split/package.json
@@ -1 +1 @@
-{"name":"glb-split","private":true,"type":"module","dependencies":{"@gltf-transform/core":"^4.4.0","@gltf-transform/extensions":"^4.4.0"}}
\ No newline at end of file
+{"name":"glb-split","private":true,"type":"module","dependencies":{"@gltf-transform/core":"^4.4.0","@gltf-transform/extensions":"^4.4.0","@gltf-transform/functions":"^4.4.0"}}
\ No newline at end of file
diff --git a/tools/glb-split/trim-idle.mjs b/tools/glb-split/trim-idle.mjs
new file mode 100644
index 0000000..e244a27
--- /dev/null
+++ b/tools/glb-split/trim-idle.mjs
@@ -0,0 +1,39 @@
+// Trim a GLB's baked animation down to a [0, tEnd] window (seconds) — for the angelfish
+// pack that's the Idle clip (GLB frames 0–109 ≈ 3.633s), which drops ~95% of the animation
+// data (the full Take is 75.67s). Geometry/skin/textures are untouched. Shared input
+// accessors are sliced once; outputs per-sampler. Usage: node trim-idle.mjs in.glb out.glb 3.633
+import { NodeIO } from '@gltf-transform/core';
+import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
+import { prune } from '@gltf-transform/functions';
+
+const [, , inPath, outPath, tEndStr] = process.argv;
+const tEnd = parseFloat(tEndStr);
+const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
+const doc = await io.read(inPath);
+const root = doc.getRoot();
+
+// Pass 1: per UNIQUE input time-track, compute how many keyframes fall in [0, tEnd]
+// (BEFORE mutating — all channels share one input), then slice the input once.
+const keepByInput = new Map();
+for (const anim of root.listAnimations())
+ for (const s of anim.listSamplers()) {
+ const input = s.getInput();
+ if (keepByInput.has(input)) continue;
+ const times = input.getArray();
+ let last = 0;
+ for (let i = 0; i < times.length; i++) { if (times[i] <= tEnd + 1e-6) last = i; else break; }
+ const keep = last + 1;
+ keepByInput.set(input, keep);
+ if (keep >= 2 && keep < times.length) input.setArray(times.slice(0, keep));
+ }
+// Pass 2: slice every sampler's OUTPUT to its input's keep count.
+for (const anim of root.listAnimations())
+ for (const s of anim.listSamplers()) {
+ const keep = keepByInput.get(s.getInput());
+ const output = s.getOutput();
+ const comps = output.getElementSize();
+ if (keep * comps < output.getArray().length) output.setArray(output.getArray().slice(0, keep * comps));
+ }
+await doc.transform(prune()); // repack buffers + drop the orphaned (trimmed-away) data
+await io.write(outPath, doc);
+console.log('wrote', outPath);