zen: UB is now the Queen angelfish (real model) + fix admin lockout

- Admin lockout: /zen checked blockedForViewer() before auth loaded, so a hard-refresh/
  direct-link bounced admins to /play. Now revalidate auth (await refresh if !ready)
  BEFORE the gate check.
- UB swap: retired the two-tail koi (ub.glb/ub-split.glb) for the vetted Queen angelfish.
  Trimmed the 75.67s baked Take down to just the Idle loop (tools/glb-split/trim-idle.mjs
  → 16MB → 6.9MB) → static/models/ub-angelfish.glb. aquarium.js reworked for the pack's
  ONE-mesh/TWO-material layout (…_body opaque single-sided; …_fins opaque alpha-tested,
  tunable); animation is the trimmed Idle. Debug tuner (/zen?debug=1) updated: yaw/pitch/
  scale + one fins&tail section. Still devgate IN_DEV={'zen'} — admin-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jay
2026-07-01 18:10:38 -04:00
parent e64c5ced3c
commit ce69b8cd18
11 changed files with 1075 additions and 56 deletions
+39
View File
@@ -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 0109 ≈ 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);