Files
upbeatBytes/tools/glb-split/project.mjs
T
thejayman77 89c0fbe1f6 Sync repo to deployed state: SEO recovery, Publishing Desk, Play games, emoji picker
The deploy pipeline runs from the working tree, so a wave of shipped features
had never been committed. This snapshots git to what's actually running.

SEO impression recovery (live + verified):
- Duplicate /a/{id} now 301-redirect to their canonical twin instead of 404
  (a hard 404 silently dropped already-indexed URLs and tanked impressions).
- Dedup representative selection reworked: accepted/serveable -> established
  rep (URL stability) -> quality score, so an accepted page never retires to a
  rejected rep and an indexed canonical doesn't churn when a newer twin arrives.
- HEAD /a/{id} returns the same status as GET (api_route GET+HEAD) instead of
  falling through to the static mount and 404ing.
- `dedup --force-recluster`: cycle-locked, model-free re-cluster to re-apply the
  policy to the existing corpus (shared cycle_lock context manager).
- CLI honors GOODNEWS_DB for its default --db (was silently ignored).

Publishing Desk (admin tool to post highlights to X via Web Intents):
- publishing.py queue/rank/handle-resolution; admin UI; full searchable emoji
  picker (bundled data, no CDN) for the blurb editor.

Play games + site:
- Bloom (word-wheel), Memory Match, daily ritual set, Zen Den (dev-gated).
- English-only language gate; source prospecting; paywall + dedup hardening.

Tests: full suite green (349). Ignores tightened (node_modules, data/*.db).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 11:32:27 -04:00

61 lines
3.2 KiB
JavaScript

// Render UB's silhouette as ASCII through the ACTUAL camera + transform, so we can
// literally see the tail's on-screen shape. Body='#', side fins='f', tail='T'.
import { NodeIO } from '@gltf-transform/core';
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
const yaw = parseFloat(process.argv[3] ?? (Math.PI / 2 + 0.10));
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
const doc = await io.read(process.argv[2]);
const root = doc.getRoot();
const joints = root.listSkins()[0].listJoints();
const tailSet = new Set();
const mark = (n) => { tailSet.add(n); n.listChildren().forEach(mark); };
joints.filter((n) => /^Tail_Fork_(Top|Bottom)$/.test(n.getName())).forEach(mark);
const grp = joints.map((n) => tailSet.has(n) ? 'T' : (n.getName().startsWith('Spine') ? '#' : 'f'));
const mesh = root.listMeshes()[0];
const prim = mesh.listPrimitives().reduce((a, b) => (b.getIndices().getCount() > a.getIndices().getCount() ? b : a));
const pos = prim.getAttribute('POSITION'), jA = prim.getAttribute('JOINTS_0'), wA = prim.getAttribute('WEIGHTS_0');
const N = pos.getCount();
// match aquarium.js: center, scale, rotate yaw about Y, position = -center
const box = { min: [1e9, 1e9, 1e9], max: [-1e9, -1e9, -1e9] };
const p = [0, 0, 0];
for (let i = 0; i < N; i++) { pos.getElement(i, p); for (let d = 0; d < 3; d++) { box.min[d] = Math.min(box.min[d], p[d]); box.max[d] = Math.max(box.max[d], p[d]); } }
const center = box.min.map((m, d) => (m + box.max[d]) / 2);
const s = 2.6 / Math.max(...box.max.map((m, d) => m - box.min[d]));
const cy = Math.cos(yaw), sy = Math.sin(yaw);
// camera lookAt
const cam = [0, 0.25, 4.6], tgt = [0, 0, 0];
const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
const norm = (a) => { const l = Math.hypot(...a); return [a[0] / l, a[1] / l, a[2] / l]; };
const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
const f = norm(sub(tgt, cam)), r = norm(cross(f, [0, 1, 0])), u = cross(r, f);
const aspect = 4 / 3, t = Math.tan((42 * Math.PI / 180) / 2);
const COLS = 96, ROWS = 40;
const grid = Array.from({ length: ROWS }, () => Array(COLS).fill(' '));
const zbuf = Array.from({ length: ROWS }, () => Array(COLS).fill(1e9));
const j = [0, 0, 0, 0], w = [0, 0, 0, 0];
for (let i = 0; i < N; i++) {
jA.getElement(i, j); wA.getElement(i, w);
let best = 0, bw = -1; for (let k = 0; k < 4; k++) if (w[k] > bw) { bw = w[k]; best = j[k]; }
const g = grp[best];
pos.getElement(i, p);
// scale, rotate Y, translate by -center
const x = p[0] * s, y = p[1] * s, z = p[2] * s;
const wx = (cy * x + sy * z) - center[0];
const wy = y - center[1];
const wz = (-sy * x + cy * z) - center[2];
const rel = [wx - cam[0], wy - cam[1], wz - cam[2]];
const vz = dot(rel, f); if (vz <= 0.01) continue;
const ndcX = dot(rel, r) / (vz * t * aspect), ndcY = dot(rel, u) / (vz * t);
const col = Math.round((ndcX + 1) / 2 * (COLS - 1)), row = Math.round((1 - (ndcY + 1) / 2) * (ROWS - 1));
if (col < 0 || col >= COLS || row < 0 || row >= ROWS) continue;
if (vz < zbuf[row][col]) { zbuf[row][col] = vz; grid[row][col] = g; }
}
console.log(`yaw=${yaw.toFixed(3)} (# body, f fins, T tail)`);
console.log(grid.map((r) => r.join('')).join('\n'));