const T3_ROOT = Math.sqrt(1.6); const T3_TAU = Math.PI * 2; const T3_RAD = Math.PI / 180; const T3_LB = [0, 0.2, 0.42, 0.62]; const T3_EB = [0.25, 0.5, 0.75]; const T3_SOFT = 0.035; const T3_LEVELS = 32; const T3_Q = 64; const t3State = { detail: 1, exact: false }; function t3Num(v) { if (t3State.exact) return String(Math.round(v * 1e6) / 1e6); let t = Math.round(v * 100); if (t === 0) return "0"; let sign = ""; if (t < 0) { sign = "-"; t = -t; } const whole = Math.floor(t / 100); const part = t - whole * 100; if (part === 0) return sign + whole; if (part < 10) return sign + whole + ".0" + part; return sign + whole + "." + (part % 10 === 0 ? part / 10 : part); } function t3Fix(v, scale) { const t = Math.round(v * scale) / scale; return t === 0 ? "0" : String(t); } const t3Arena = { on: false, f: null }; function t3Token(get, count, closed) { const f = t3Arena.f; const at = f.arenaN; let arena = f.arena; if (!arena || arena.length < 2 * (at + count)) { const next = new Float64Array(Math.max(512, 2 * (at + count) * 2)); if (arena) next.set(arena.subarray(0, 2 * at)); arena = f.arena = next; } const unit = t3State.exact ? 1e6 : 100; for (let i = 0; i < count; i++) { arena[2 * (at + i)] = Math.round(get(2 * i) * unit) / unit; arena[2 * (at + i) + 1] = Math.round(get(2 * i + 1) * unit) / unit; } f.arenaN = at + count; return "\u0001" + String.fromCharCode(at & 0xffff, at >>> 16, count, closed ? 1 : 0); } function t3Matrix(f, role, a, b, c, d, e, g) { const list = f.tm || (f.tm = []); const m = list[role] || (list[role] = new Float64Array(6)); m[0] = Math.round(a * 1e5) / 1e5; m[1] = Math.round(b * 1e5) / 1e5; m[2] = Math.round(c * 1e5) / 1e5; m[3] = Math.round(d * 1e5) / 1e5; m[4] = Math.round(e * 100) / 100; m[5] = Math.round(g * 100) / 100; return "\u0002"; } function t3Path(buf, count, closed) { if (count < 2) return ""; if (t3Arena.on) return t3Token((i) => buf[i], count, closed); let s = "M" + t3Num(buf[0]) + " " + t3Num(buf[1]); for (let i = 1; i < count; i++) s += "L" + t3Num(buf[2 * i]) + " " + t3Num(buf[2 * i + 1]); return closed ? s + "Z" : s; } const t3Step = (a, b, x) => { const t = Math.min(1, Math.max(0, (x - a) / (b - a))); return t * t * (3 - 2 * t); }; function t3Level(score, bands) { let u = 0; for (let i = 0; i < bands.length; i++) u += t3Step(bands[i] - T3_SOFT, bands[i] + T3_SOFT, score); return Math.round(u * T3_Q); } function t3Grow(f, name, size, Kind) { if (!f[name] || f[name].length < size) f[name] = new Kind(Math.max(size, f[name] ? f[name].length * 2 : 16)); return f[name]; } function t3Box(buf, count, box) { let x0 = Infinity; let y0 = Infinity; let x1 = -Infinity; let y1 = -Infinity; for (let i = 0; i < count; i++) { const x = buf[2 * i]; const y = buf[2 * i + 1]; if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y; } box[0] = x0; box[1] = y0; box[2] = x1; box[3] = y1; } function t3Unit(v) { const l = Math.hypot(v[0], v[1], v[2]) || 1; return [v[0] / l, v[1] / l, v[2] / l]; } function t3Rot(axis, degrees) { const [x, y, z] = t3Unit(axis); const c = Math.cos(degrees * T3_RAD); const s = Math.sin(degrees * T3_RAD); const C = 1 - c; return new Float64Array([c + x * x * C, x * y * C - z * s, x * z * C + y * s, y * x * C + z * s, c + y * y * C, y * z * C - x * s, z * x * C - y * s, z * y * C + x * s, c + z * z * C]); } function t3Mul(A, B) { const out = new Float64Array(9); for (let r = 0; r < 3; r++) for (let c = 0; c < 3; c++) out[r * 3 + c] = A[r * 3] * B[c] + A[r * 3 + 1] * B[3 + c] + A[r * 3 + 2] * B[6 + c]; return out; } const t3Apply = (R, v) => [R[0] * v[0] + R[1] * v[1] + R[2] * v[2], R[3] * v[0] + R[4] * v[1] + R[5] * v[2], R[6] * v[0] + R[7] * v[1] + R[8] * v[2]]; const T3_EYE = new Float64Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); function t3Camera(P, pose) { const a = (P.azimuth ?? 45) * T3_RAD; const e = (P.elevation ?? 30) * T3_RAD; const k = P.scale * T3_ROOT; const sa = Math.sin(a); const ca = Math.cos(a); const se = Math.sin(e); const ce = Math.cos(e); const cam = { m: new Float64Array(6), ox: 0, oy: 0, V: [0, 0, 0], L: [0, 0, 0], k, turn: 0, R: T3_EYE, t: [0, 0, 0], lx: 0, ly: 1, sa, ca, se, ce, origin: P.origin }; const lw = Math.hypot(sa, ca, 1.2); if (pose && pose.R && !(pose.turn !== undefined && pose.root)) { const R = pose.R; const t = pose.t ?? [0, 0, 0]; const r0 = [sa * k, -ca * k, 0]; const r1 = [ca * se * k, sa * se * k, -ce * k]; for (let c = 0; c < 3; c++) { cam.m[c] = r0[0] * R[c] + r0[1] * R[3 + c] + r0[2] * R[6 + c]; cam.m[3 + c] = r1[0] * R[c] + r1[1] * R[3 + c] + r1[2] * R[6 + c]; } cam.ox = P.origin[0] + r0[0] * t[0] + r0[1] * t[1] + r0[2] * t[2]; cam.oy = P.origin[1] + r1[0] * t[0] + r1[1] * t[1] + r1[2] * t[2]; const V = [ca * ce, sa * ce, se]; const L = [-sa / lw, ca / lw, 1.2 / lw]; for (let c = 0; c < 3; c++) { cam.V[c] = R[c] * V[0] + R[3 + c] * V[1] + R[6 + c] * V[2]; cam.L[c] = R[c] * L[0] + R[3 + c] * L[1] + R[6 + c] * L[2]; } cam.R = R; cam.t = t; } else { const turn = pose ? pose.turn ?? 0 : 0; const pivot = pose && pose.pivot ? pose.pivot : [0, 0]; const b = a - turn * T3_RAD; const sb = Math.sin(b); const cb = Math.cos(b); cam.m[0] = sb * k; cam.m[1] = -cb * k; cam.m[2] = 0; cam.m[3] = cb * se * k; cam.m[4] = sb * se * k; cam.m[5] = -ce * k; cam.ox = P.origin[0] + (pivot[0] * sa - pivot[1] * ca) * k; cam.oy = P.origin[1] + (pivot[0] * ca + pivot[1] * sa) * se * k; cam.V = [cb * ce, sb * ce, se]; cam.L = [-sb / lw, cb / lw, 1.2 / lw]; cam.turn = turn; const c = Math.cos(turn * T3_RAD); const s = Math.sin(turn * T3_RAD); cam.R = new Float64Array([c, -s, 0, s, c, 0, 0, 0, 1]); cam.t = [pivot[0], pivot[1], 0]; } const lh = Math.hypot(cam.L[0], cam.L[1]) || 1; cam.lx = cam.L[0] / lh; cam.ly = cam.L[1] / lh; return cam; } function t3Quat(q) { const l = Math.hypot(q[0], q[1], q[2], q[3]) || 1; const x = q[0] / l; const y = q[1] / l; const z = q[2] / l; const w = q[3] / l; return new Float64Array([1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w), 2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w), 2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)]); } function t3FreeRot(v) { if (v.R) return v.R instanceof Float64Array ? v.R : Float64Array.from(v.R); if (v.q) return t3Quat(v.q); if (v.axis) return t3Rot(v.axis, v.angle ?? 0); return T3_EYE; } function t3Poses(groups, values) { const get = (name) => (values instanceof Map ? values.get(name) : values ? values[name] : undefined) ?? 0; const list = []; for (let index = 0; index < groups.length; index++) { const g = groups[index]; const v = get(g.name); let parent = g.parent === undefined || g.parent === null || g.parent < 0 ? null : list[g.parent]; let po = parent ? groups[g.parent].origin : [0, 0, 0]; let R = T3_EYE; let t = [g.origin[0] - po[0], g.origin[1] - po[1], g.origin[2] - po[2]]; if (g.kind === "turn") R = t3Rot([0, 0, 1], v); else if (g.kind === "hinge") R = t3Rot(g.axis, v); else if (g.kind === "slide") { const d = t3Unit(g.direction); t[0] += d[0] * v; t[1] += d[1] * v; t[2] += d[2] * v; } else if (g.kind === "free" && v && typeof v === "object") { R = t3FreeRot(v); if (v.world) { parent = null; po = [0, 0, 0]; t = [g.origin[0], g.origin[1], g.origin[2]]; } if (v.t) { t[0] += v.t[0]; t[1] += v.t[1]; t[2] += v.t[2]; } } if (!parent) { const pose = { R, t, value: v }; if (g.kind === "turn") Object.assign(pose, { turn: v, pivot: [g.origin[0], g.origin[1]], root: true }); list.push(pose); continue; } const tw = t3Apply(parent.R, t); list.push({ R: t3Mul(parent.R, R), t: [tw[0] + parent.t[0], tw[1] + parent.t[1], tw[2] + parent.t[2]], value: v }); } return list; } function t3PosesMap(groups, values) { const list = t3Poses(groups, values); return new Map(groups.map((g, index) => [g.name, list[index]])); } function t3Roles(part) { const roles = []; const spec = part.spec; const details = part.details ?? {}; const add = (name, type, extra = {}) => roles.push({ name, type, ...extra }); const tone = part.tone ?? "hi"; const crease = part.crease ?? "faint"; const fadedSlots = (prefix, count, cls, lineTone) => { for (let j = 0; j < count; j++) add(`${prefix}${j}`, "slot", { cls, tone: lineTone }); }; const planes = () => (part.planes ?? []).forEach((_, j) => add(`plane${j}`, "plane")); const dots = () => (part.dots ?? []).forEach((dot, j) => add(`c${j}`, "dot", { tone: dot.tone ?? "mid", size: dot.size ?? 0.5 })); const sheets = (top) => { for (let t = 0; t < 4; t++) add(`s${t}`, "path", { cls: "iso-shade", shade: t }); if (top) add("s4", "path", { cls: "iso-top" }); }; const inner = () => { if ((details.seams && details.seams.length) || (details.ribs && details.ribs.seams)) add("inner", "path", { cls: "iso-line", tone: "faint" }); }; const ribs = () => { if (details.ribs) fadedSlots("r", spec.slots?.ribs ?? 6, "iso-line", details.ribs.tone ?? "lo"); }; if (part.kind === "prism") { add("fill", "path", { cls: "iso-fill" }); const faces = spec.ring.length / 2 + (spec.up ? 0 : 2); for (let j = 0; j < faces; j++) add(`f${j}`, "face"); if (spec.up) add("top", "xform", { cls: "iso-top" }); if (crease !== "none") add("crease", "path", { cls: "iso-line iso-crease", tone: crease }); if (spec.bevel && crease !== "none") fadedSlots("v", spec.slots?.bevel ?? 6, "iso-line iso-bevel", crease); if (details.seams && details.seams.length) add("inner", "path", { cls: "iso-line", tone: "faint" }); planes(); add("edge", "path", { cls: "iso-line iso-edge", tone }); dots(); } else if (part.kind === "round" || part.kind === "lathe") { add("fill", "path", { cls: "iso-fill" }); sheets(true); const ends = part.kind === "round" ? spec.ends : spec.caps.map((cap) => (cap ? "flat" : "open")); ends.forEach((end, j) => end === "flat" && add(`k${j}`, "face")); if (crease !== "none") add("crease", "path", { cls: "iso-line iso-crease", tone: crease }); inner(); ribs(); planes(); add("edge", "path", { cls: "iso-line iso-edge", tone }); dots(); } else if (part.kind === "ball") { add("fill", "path", { cls: "iso-fill" }); sheets(false); spec.flats.forEach((_, j) => add(`f${j}`, "face")); if (crease !== "none") add("crease", "path", { cls: "iso-line iso-crease", tone: crease }); planes(); add("edge", "path", { cls: "iso-line iso-edge", tone }); dots(); } else if (part.kind === "tube") { add("body", "path", { cls: "tb-body" }); if (spec.wide) { add("shine", "path", { cls: "tb-shine" }); add("shade", "path", { cls: "tb-shade" }); } if (spec.rings) fadedSlots("r", spec.slots?.ribs ?? 6, "tb-ring", spec.rings.tone ?? "lo"); add("edges", "path", { cls: "iso-line tb-edge", tone }); } else if (part.kind === "item") { return roles; } else if (part.kind === "fixed") { add("move", "self"); ribs(); planes(); dots(); } return roles; } function t3Planes(list) { return (list ?? []).map((plane) => ({ o: plane.o, u: plane.u, v: plane.v, n: plane.n, fade: plane.fade ?? [0.04, 0.3], billboard: Boolean(plane.billboard) })); } function t3Dots(list) { return (list ?? []).map((dot) => ({ p: dot.p, n: dot.n ?? null, size: dot.size ?? 0.5, fade: dot.fade ?? [0.04, 0.42] })); } function t3PreparePrism(run, part) { const spec = part.spec; const n = spec.ring.length / 2; run.n = n; run.up = Boolean(spec.up); const ring = Float64Array.from(spec.ring); run.ring = ring; const nx = new Float64Array(n); const ny = new Float64Array(n); for (let i = 0; i < n; i++) { const j = (i + 1) % n; const dx = ring[2 * j] - ring[2 * i]; const dy = ring[2 * j + 1] - ring[2 * i + 1]; const l = Math.hypot(dx, dy) || 1; nx[i] = dy / l; ny[i] = -dx / l; } run.nx = nx; run.ny = ny; const F = spec.up ? { o: [0, 0, 0], a: [0, 0, 1], u: [1, 0, 0], v: [0, 1, 0] } : spec.F; run.F = F; run.s0 = spec.up ? spec.z0 : spec.s0; run.s1 = spec.up ? spec.z1 : spec.s1; const lo = new Float64Array(3 * n); const hi = new Float64Array(3 * n); const N3 = new Float64Array(3 * n); for (let i = 0; i < n; i++) { const x = ring[2 * i]; const y = ring[2 * i + 1]; for (let c = 0; c < 3; c++) { const base = F.o[c] + F.u[c] * x + F.v[c] * y; lo[3 * i + c] = base + F.a[c] * run.s0; hi[3 * i + c] = base + F.a[c] * run.s1; N3[3 * i + c] = F.u[c] * nx[i] + F.v[c] * ny[i]; } } run.lo = lo; run.hi = hi; run.N3 = N3; run.sharp = new Uint8Array(n); for (let i = 0; i < n; i++) { const p = (i - 1 + n) % n; run.sharp[i] = nx[p] * nx[i] + ny[p] * ny[i] < Math.cos(0.35) ? 1 : 0; } if (spec.bevel) { const inset = new Float64Array(2 * n); for (let i = 0; i < n; i++) { const p = (i - 1 + n) % n; const mx = nx[p] + nx[i]; const my = ny[p] + ny[i]; const l = Math.hypot(mx, my) || 1; inset[2 * i] = ring[2 * i] - (mx / l) * spec.bevel; inset[2 * i + 1] = ring[2 * i + 1] - (my / l) * spec.bevel; } run.inset = inset; } run.seams = part.details && part.details.seams ? part.details.seams.slice() : null; run.cut = spec.cuts && spec.cuts.length ? spec.cuts : null; } function t3Axes(run, part) { if (run.kind === "ball") return run.flats.map((flat) => flat.n); if (run.kind === "tube") return run.tips.slice(); if (run.kind === "prism") { const out = [run.F.a]; for (let i = 0; i < run.n; i++) { const n = [run.N3[3 * i], run.N3[3 * i + 1], run.N3[3 * i + 2]]; if (out.every((m) => Math.abs(m[0] * n[0] + m[1] * n[1] + m[2] * n[2]) < 0.9995)) out.push(n); } return out; } if (run.F) return [run.F.a]; if (part.spec && part.spec.F) return [part.spec.F.a]; return []; } function t3Cloud(run, part) { const out = []; const ring = (F, s, r, count) => { for (let j = 0; j < count; j++) { const c = Math.cos((j / count) * T3_TAU); const n = Math.sin((j / count) * T3_TAU); out.push(F.o[0] + F.a[0] * s + r * (c * F.u[0] + n * F.v[0]), F.o[1] + F.a[1] * s + r * (c * F.u[1] + n * F.v[1]), F.o[2] + F.a[2] * s + r * (c * F.u[2] + n * F.v[2])); } }; if (run.kind === "prism") { for (let i = 0; i < 3 * run.n; i++) out.push(run.lo[i]); for (let i = 0; i < 3 * run.n; i++) out.push(run.hi[i]); } else if (run.kind === "round") { ring(run.F, run.s0, run.r0, 24); ring(run.F, run.s1, run.r1, 24); for (const [end, s, r] of [[0, run.s0, run.r0], [1, run.s1, run.r1]]) { if (run.ends[end] !== "dome") continue; const sign = end ? 1 : -1; for (const b of [0.5, 1.0, 1.35]) ring(run.F, s + sign * r * Math.sin(b), r * Math.cos(b), 6); out.push(run.F.o[0] + run.F.a[0] * (s + sign * r), run.F.o[1] + run.F.a[1] * (s + sign * r), run.F.o[2] + run.F.a[2] * (s + sign * r)); } } else if (run.kind === "ball") { const golden = Math.PI * (3 - Math.sqrt(5)); for (let i = 0; i < 64; i++) { const y = 1 - (2 * (i + 0.5)) / 64; const rad = Math.sqrt(1 - y * y); const w = [Math.cos(golden * i) * rad, Math.sin(golden * i) * rad, y]; const p = [run.c[0] + run.r * w[0], run.c[1] + run.r * w[1], run.c[2] + run.r * w[2]]; let inside = true; for (const flat of run.flats) if ((p[0] - run.c[0]) * flat.n[0] + (p[1] - run.c[1]) * flat.n[1] + (p[2] - run.c[2]) * flat.n[2] > flat.d + 1e-9) inside = false; if (inside) out.push(p[0], p[1], p[2]); } for (const flat of run.flats) for (let j = 0; j < 12; j++) { const c = Math.cos((j / 12) * T3_TAU) * flat.rho; const n = Math.sin((j / 12) * T3_TAU) * flat.rho; out.push(run.c[0] + flat.n[0] * flat.d + c * flat.u[0] + n * flat.v[0], run.c[1] + flat.n[1] * flat.d + c * flat.u[1] + n * flat.v[1], run.c[2] + flat.n[2] * flat.d + c * flat.u[2] + n * flat.v[2]); } } else if (run.kind === "lathe") { const lv = run.levels; const step = Math.max(4, Math.floor(lv.length / 4 / 8)) * 4; for (let i = 0; i < lv.length; i += step) ring(run.F, lv[i], lv[i + 1], 8); ring(run.F, lv[lv.length - 4], lv[lv.length - 3], 8); } else if (run.kind === "tube") { const b = run.body; const stride = Math.max(1, Math.floor(b.count / 8)); for (let i = 0; i < b.count; i += stride) { const T = t3Unit([b.D3[3 * i], b.D3[3 * i + 1], b.D3[3 * i + 2]]); const [u, v] = t3Basis(T); ring({ o: [b.P3[3 * i], b.P3[3 * i + 1], b.P3[3 * i + 2]], a: T, u, v }, 0, run.r, 8); } } else if (part.cloud) for (const v of part.cloud) out.push(v); return Float64Array.from(out); } function t3Unpack(value, grouped) { if (!value) return new Float64Array(0); if (typeof value !== "string") return Float64Array.from(value); const text = atob(value); const bytes = new Uint8Array(text.length); for (let i = 0; i < text.length; i++) bytes[i] = text.charCodeAt(i); const view = new DataView(bytes.buffer); const size = grouped ? 10 : 8; const stride = grouped ? 6 : 5; const count = bytes.length / size; const out = new Float64Array(count * stride); for (let e = 0; e < count; e++) { let o = e * size; const at = e * stride; out[at] = view.getUint16(o); out[at + 1] = view.getUint16(o + 2); o += 4; if (grouped) { out[at + 2] = view.getUint16(o) - 1; o += 2; } let x = (view.getUint16(o) / 65535) * 2 - 1; let y = (view.getUint16(o + 2) / 65535) * 2 - 1; const z = 1 - Math.abs(x) - Math.abs(y); if (z < 0) { const ox = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1); const oy = (1 - Math.abs(x)) * (y >= 0 ? 1 : -1); x = ox; y = oy; } const l = Math.hypot(x, y, z) || 1; out[at + stride - 3] = x / l; out[at + stride - 2] = y / l; out[at + stride - 1] = z / l; } return out; } function t3Prepare(data, { unit = 1 } = {}) { const P = data.P; const runs = data.parts.map((part, index) => { const run = { index, name: part.name, kind: part.kind, group: part.group ?? -1, layer: part.layer, part, roles: t3Roles(part), planes: t3Planes(part.planes), dots: t3Dots(part.dots), unit }; run.role = Object.fromEntries(run.roles.map((role, j) => [role.name, j])); if (part.kind === "prism") t3PreparePrism(run, part); else if (part.kind === "round") t3PrepareRound(run, part, P, unit); else if (part.kind === "ball") t3PrepareBall(run, part, P, unit); else if (part.kind === "lathe") t3PrepareLathe(run, part, P, unit); else if (part.kind === "tube") t3PrepareTube(run, part, data); else if (part.kind === "item") { run.version = 0; run.itemBox = new Float64Array([1e9, 1e9, -1e9, -1e9]); run.itemHull = new Float64Array(0); } else if (part.kind === "fixed") { run.anchor = part.spec.anchor; run.home = part.spec.home; run.box0 = part.spec.box; run.hull0 = Float64Array.from(part.spec.hull); run.F = part.spec.F ?? null; run.ribs = part.details && part.details.ribs ? part.details.ribs : null; } run.slots = part.spec.slots ?? {}; run.cloud = t3Cloud(run, part); run.axes = t3Axes(run, part); const cn = run.cloud.length / 3; run.centroid = [0, 0, 0]; for (let i = 0; i < cn; i++) for (let c = 0; c < 3; c++) run.centroid[c] += run.cloud[3 * i + c] / Math.max(1, cn); return run; }); const layers = data.layers.map((layer, index) => { const parts = Int32Array.from(layer.parts); const local = new Map(); parts.forEach((global, at) => local.set(global, at)); const planar = t3Unpack(data.planes && data.planes[layer.name], false); const dynamic = Int32Array.from((data.dynamic && data.dynamic[layer.name]) ?? []); const free = Int32Array.from((data.free && data.free[layer.name]) ?? []); const framed = t3Unpack(data.framed && data.framed[layer.name], true); const groupPairs = []; const raw = (data.groupPairs && data.groupPairs[layer.name]) ?? []; const members = new Map(); for (const global of parts) { const g = runs[global].group; if (!members.has(g)) members.set(g, []); members.get(g).push(global); } for (let p = 0; p < raw.length; p += 6) groupPairs.push({ a: Int32Array.from(members.get(raw[p]) ?? []), b: Int32Array.from(members.get(raw[p + 1]) ?? []), frame: raw[p + 2], n: [raw[p + 3], raw[p + 4], raw[p + 5]], boxA: new Float64Array(4), boxB: new Float64Array(4) }); const looseAt = new Uint8Array(parts.length); for (const global of free) looseAt[local.get(global)] = 1; return { index, name: layer.name, parts, local, rest: Int32Array.from(layer.rest ?? layer.parts), planar, framed, groupPairs, dynamic, free, looseAt, warm: new Map() }; }); return { P, data, unit, groups: data.groups, runs, layers }; } function t3Frame(run) { const count = run.roles.length; return { d: new Array(count).fill(""), q: new Int32Array(count), w: new Float64Array(count).fill(-1), t: new Array(count).fill(""), o: new Float64Array(count).fill(1), box: new Float64Array(4), hull: new Float64Array(64), hullN: 0, cx: new Float64Array(count), cy: new Float64Array(count), quads: null, quadN: 0 }; } const t3Buf = { a: new Float64Array(1024), b: new Float64Array(1024), c: new Float64Array(1024) }; function t3Scratch(name, size) { if (t3Buf[name].length < size) t3Buf[name] = new Float64Array(size * 2); return t3Buf[name]; } function t3SetHull(f, buf, count) { const hull = t3Grow(f, "hull", 2 * count, Float64Array); for (let i = 0; i < 2 * count; i++) hull[i] = buf[i]; f.hullN = count; } function t3Faded(f, run, prefix, lines, count) { if (!f.fade) f.fade = {}; const detail = t3State.detail; if (detail < 1) for (let i = 0; i < count; i++) lines.alpha[i] *= detail; if (detail < 0.01) count = 0; f.fade[prefix] = { key: lines.key.slice(0, count), alpha: lines.alpha.slice(0, count) }; const slots = run.slots[prefix === "v" ? "bevel" : "ribs"] ?? 6; const levels = run._levels ?? (run._levels = new Array(T3_LEVELS + 1)); for (let l = 0; l <= T3_LEVELS; l++) levels[l] = ""; for (let i = 0; i < count; i++) { const alpha = lines.alpha[i]; const level = Math.min(T3_LEVELS, Math.round(alpha * T3_LEVELS)); if (level <= 0) continue; levels[level] += lines.d[i]; } const present = []; for (let l = T3_LEVELS; l >= 1; l--) if (levels[l]) present.push([l, levels[l]]); while (present.length > slots) { let best = 1; let gap = Infinity; for (let i = 1; i < present.length; i++) { const g = present[i - 1][0] - present[i][0]; if (g < gap) { gap = g; best = i; } } const [a, da] = present[best - 1]; const [b, db] = present[best]; present.splice(best - 1, 2, [(a + b) / 2, da + db]); } for (let j = 0; j < slots; j++) { const role = run.role[`${prefix}${j}`]; if (role === undefined) continue; if (j < present.length) { f.d[role] = present[j][1]; f.o[role] = present[j][0] / T3_LEVELS; } else { f.d[role] = ""; f.o[role] = 0; } } return present.length; } const t3Lines = { d: [], alpha: [], key: [] }; const t3None = { d: [], alpha: [], key: [] }; function t3EmitPrism(run, cam, f) { const n = run.n; const m = cam.m; const ox = cam.ox; const oy = cam.oy; const V = cam.V; const L = cam.L; const lo = run.lo; const hi = run.hi; const N3 = run.N3; const bot = t3Scratch("a", 2 * n); const top = t3Scratch("b", 2 * n); for (let i = 0; i < n; i++) { const a = 3 * i; bot[2 * i] = ox + m[0] * lo[a] + m[1] * lo[a + 1] + m[2] * lo[a + 2]; bot[2 * i + 1] = oy + m[3] * lo[a] + m[4] * lo[a + 1] + m[5] * lo[a + 2]; top[2 * i] = ox + m[0] * hi[a] + m[1] * hi[a + 1] + m[2] * hi[a + 2]; top[2 * i + 1] = oy + m[3] * hi[a] + m[4] * hi[a + 1] + m[5] * hi[a + 2]; } const F = run.F; const aV = F.a[0] * V[0] + F.a[1] * V[1] + F.a[2] * V[2]; const near = aV >= 0 ? top : bot; const far = aV >= 0 ? bot : top; const facing = run._facing ?? (run._facing = new Float64Array(n)); for (let i = 0; i < n; i++) facing[i] = N3[3 * i] * V[0] + N3[3 * i + 1] * V[1] + N3[3 * i + 2] * V[2]; const on = (i) => facing[((i % n) + n) % n] > 1e-9; let i0 = -1; for (let i = 0; i < n; i++) if (on(i) && !on(i - 1)) { i0 = i; break; } const sil = t3Scratch("c", 4 * n + 8); const silAt = run._silAt ?? (run._silAt = new Int32Array(2 * n + 4)); let count = 0; let i1 = i0; const nearSet = aV >= 0 ? hi : lo; const farSet = aV >= 0 ? lo : hi; if (i0 < 0) { for (let i = 0; i < n; i++) { sil[2 * count] = near[2 * i]; sil[2 * count + 1] = near[2 * i + 1]; silAt[count] = i; count++; } } else { while (on(i1)) i1++; for (let j = i0; j <= i1; j++) { const i = j % n; sil[2 * count] = far[2 * i]; sil[2 * count + 1] = far[2 * i + 1]; silAt[count] = -1 - i; count++; } for (let j = i1; j <= i0 + n; j++) { const i = j % n; sil[2 * count] = near[2 * i]; sil[2 * count + 1] = near[2 * i + 1]; silAt[count] = i; count++; } if (count > 2 && Math.abs(sil[0] - sil[2 * count - 2]) < 1e-9 && Math.abs(sil[1] - sil[2 * count - 1]) < 1e-9) count--; } const outline = t3Path(sil, count, true); f.d[run.role.fill] = outline; if (run.cut) { const inPlane = (index) => { const at = silAt[index]; const set = at < 0 ? farSet : nearSet; const v = at < 0 ? -1 - at : at; return run.cut.some((cut) => Math.abs(cut.n[0] * set[3 * v] + cut.n[1] * set[3 * v + 1] + cut.n[2] * set[3 * v + 2] - cut.d) < 1e-3); }; const flags = new Uint8Array(count); const onSame = (i, j) => { const at = (index) => { const a = silAt[index]; const set = a < 0 ? farSet : nearSet; const v = a < 0 ? -1 - a : a; return [set[3 * v], set[3 * v + 1], set[3 * v + 2]]; }; const p = at(i); const q = at(j); return run.cut.some((cut) => Math.abs(cut.n[0] * p[0] + cut.n[1] * p[1] + cut.n[2] * p[2] - cut.d) < 1e-3 && Math.abs(cut.n[0] * q[0] + cut.n[1] * q[1] + cut.n[2] * q[2] - cut.d) < 1e-3); }; for (let j = 0; j < count; j++) flags[j] = inPlane(j) && onSame(j, (j + 1) % count) ? 1 : 0; f.d[run.role.edge] = t3Broken(sil, count, flags); } else f.d[run.role.edge] = outline; t3Box(sil, count, f.box); t3SetHull(f, sil, count); const quad = new Float64Array(8); for (let i = 0; i < n; i++) { const role = run.role[`f${i}`]; if (!(facing[i] > 1e-6)) { f.d[role] = ""; continue; } const j = (i + 1) % n; quad[0] = top[2 * i]; quad[1] = top[2 * i + 1]; quad[2] = top[2 * j]; quad[3] = top[2 * j + 1]; quad[4] = bot[2 * j]; quad[5] = bot[2 * j + 1]; quad[6] = bot[2 * i]; quad[7] = bot[2 * i + 1]; f.d[role] = t3Path(quad, 4, true); let score; if (run.up) { score = (run.nx[i] * cam.lx + run.ny[i] * cam.ly + 1) / 2; f.q[role] = t3Level(score, T3_EB); } else { score = N3[3 * i] * L[0] + N3[3 * i + 1] * L[1] + N3[3 * i + 2] * L[2]; f.q[role] = t3Level(score, T3_LB); } const e1 = Math.hypot(quad[2] - quad[0], quad[3] - quad[1]); const e2 = Math.hypot(quad[6] - quad[0], quad[7] - quad[1]); const area = Math.abs((quad[2] - quad[0]) * (quad[7] - quad[1]) - (quad[3] - quad[1]) * (quad[6] - quad[0])); const wide = area / (Math.max(e1, e2) || 1); f.w[role] = wide < 0.4 ? Math.round(wide * 100) / 100 : -1; } if (run.up) { const z = run.s1; f.t[run.role.top] = t3Arena.on ? t3Matrix(f, run.role.top, m[0], m[3], m[1], m[4], ox + m[2] * z, oy + m[5] * z) : `matrix(${t3Fix(m[0], 1e5)} ${t3Fix(m[3], 1e5)} ${t3Fix(m[1], 1e5)} ${t3Fix(m[4], 1e5)} ${t3Fix(ox + m[2] * z, 100)} ${t3Fix(oy + m[5] * z, 100)})`; } else { for (let side = 0; side < 2; side++) { const role = run.role[`f${n + side}`]; const sign = side ? 1 : -1; const face = sign * aV; if (!(face > 1e-6)) { f.d[role] = ""; continue; } const ring = side ? top : bot; f.d[role] = t3Path(ring, n, true); f.q[role] = t3Level(sign * (F.a[0] * L[0] + F.a[1] * L[1] + F.a[2] * L[2]), T3_LB); let minor = Infinity; let x0 = Infinity; let x1 = -Infinity; for (let i = 0; i < n; i++) { const j = (i + 1) % n; const ex = ring[2 * j] - ring[2 * i]; const ey = ring[2 * j + 1] - ring[2 * i + 1]; const l = Math.hypot(ex, ey); if (l < 1e-9) continue; let lo2 = Infinity; let hi2 = -Infinity; for (let q = 0; q < n; q++) { const v = ((ring[2 * q] - ring[2 * i]) * ey - (ring[2 * q + 1] - ring[2 * i + 1]) * ex) / l; if (v < lo2) lo2 = v; if (v > hi2) hi2 = v; } minor = Math.min(minor, hi2 - lo2); x0 = Math.min(x0, lo2); x1 = Math.max(x1, hi2); } f.w[role] = minor < 0.4 ? Math.round(minor * 100) / 100 : -1; } } if (run.role.crease !== undefined) { let crease = ""; if (run.up) { if (i0 >= 0) { const buf = t3Scratch("a", 0); let c = 0; const rim = t3Scratch("c", 2 * n + 4); for (let j = i0; j <= i1; j++) { const i = j % n; rim[2 * c] = top[2 * i]; rim[2 * c + 1] = top[2 * i + 1]; c++; } crease = t3Path(rim, c, false); void buf; } } else { const capOn = [-aV > 1e-6, aV > 1e-6]; const seg = new Float64Array(4); for (let i = 0; i < n; i++) { const p = (i - 1 + n) % n; if (run.sharp[i] && facing[i] > 1e-6 && facing[p] > 1e-6) { seg[0] = bot[2 * i]; seg[1] = bot[2 * i + 1]; seg[2] = top[2 * i]; seg[3] = top[2 * i + 1]; crease += t3Path(seg, 2, false); } } for (let side = 0; side < 2; side++) { if (!capOn[side] || i0 < 0) continue; const ring = side ? top : bot; const rim = t3Scratch("c", 2 * n + 4); let c = 0; for (let j = i0; j <= i1; j++) { const i = j % n; rim[2 * c] = ring[2 * i]; rim[2 * c + 1] = ring[2 * i + 1]; c++; } crease += t3Path(rim, c, false); } } f.d[run.role.crease] = crease; } if (run.inset && run.role.v0 !== undefined && t3State.detail < 0.01) f.levels = t3Faded(f, run, "v", t3None, 0); else if (run.inset && run.role.v0 !== undefined) { const lines = t3Lines; lines.d.length = 0; lines.alpha.length = 0; lines.key.length = 0; lines.key.length = 0; const capSide = aV >= 0 ? 1 : 0; const s = capSide ? run.s1 : run.s0; const capAlpha = run.up ? 1 : t3Step(0.02, 0.2, Math.abs(aV)); const ins = t3Scratch("c", 2 * n); for (let i = 0; i < n; i++) { const x = run.inset[2 * i]; const y = run.inset[2 * i + 1]; const px = F.o[0] + F.a[0] * s + F.u[0] * x + F.v[0] * y; const py = F.o[1] + F.a[1] * s + F.u[1] * x + F.v[1] * y; const pz = F.o[2] + F.a[2] * s + F.u[2] * x + F.v[2] * y; ins[2 * i] = ox + m[0] * px + m[1] * py + m[2] * pz; ins[2 * i + 1] = oy + m[3] * px + m[4] * py + m[5] * pz; } const rim = capSide ? top : bot; const seg = new Float64Array(4); for (let i = 0; i < n; i++) { const p = (i - 1 + n) % n; const ai = capAlpha * t3Step(0, 0.14, facing[i]); const ap = capAlpha * t3Step(0, 0.14, facing[p]); if (ai > 0.01) { const j = (i + 1) % n; seg[0] = ins[2 * i]; seg[1] = ins[2 * i + 1]; seg[2] = ins[2 * j]; seg[3] = ins[2 * j + 1]; lines.d.push(t3Path(seg, 2, false)); lines.alpha.push(ai); lines.key.push(2 * i); } const corner = ai * (1 - ap) + ap * (1 - ai); if (corner > 0.01) { seg[0] = ins[2 * i]; seg[1] = ins[2 * i + 1]; seg[2] = rim[2 * i]; seg[3] = rim[2 * i + 1]; lines.d.push(t3Path(seg, 2, false)); lines.alpha.push(corner); lines.key.push(2 * i + 1); } } f.levels = t3Faded(f, run, "v", lines, lines.d.length); } if (run.seams && run.role.inner !== undefined) { let inner = ""; if (i0 >= 0) { const rim = t3Scratch("c", 2 * n + 4); for (const s of run.seams) { let c = 0; for (let j = i0; j <= i1; j++) { const i = j % n; const x = run.ring[2 * i]; const y = run.ring[2 * i + 1]; const px = F.o[0] + F.a[0] * s + F.u[0] * x + F.v[0] * y; const py = F.o[1] + F.a[1] * s + F.u[1] * x + F.v[1] * y; const pz = F.o[2] + F.a[2] * s + F.u[2] * x + F.v[2] * y; rim[2 * c] = ox + m[0] * px + m[1] * py + m[2] * pz; rim[2 * c + 1] = oy + m[3] * px + m[4] * py + m[5] * pz; c++; } inner += t3Path(rim, c, false); } } f.d[run.role.inner] = inner; } } function t3Broken(buf, count, skip) { let start = -1; for (let j = 0; j < count; j++) if (skip[(j - 1 + count) % count] && !skip[j]) { start = j; break; } if (start < 0) return skip[0] ? "" : t3Path(buf, count, true); let out = ""; let run = []; for (let step = 0; step < count; step++) { const j = (start + step) % count; if (skip[j]) { if (run.length) { run.push((j + 0) % count); out += t3PathOf(run.map((i) => [buf[2 * i], buf[2 * i + 1]]), false); run = []; } continue; } run.push(j); } if (run.length) { run.push((start + count) % count); out += t3PathOf(run.map((i) => [buf[2 * i], buf[2 * i + 1]]), false); } return out; } function t3EmitPlanes(run, cam, f, sx = 0, sy = 0) { const m = cam.m; const V = cam.V; for (let j = 0; j < run.planes.length; j++) { const plane = run.planes[j]; const role = run.role[`plane${j}`]; const o = plane.o; const X = cam.ox - sx + m[0] * o[0] + m[1] * o[1] + m[2] * o[2]; const Y = cam.oy - sy + m[3] * o[0] + m[4] * o[1] + m[5] * o[2]; const facing = plane.n[0] * V[0] + plane.n[1] * V[1] + plane.n[2] * V[2]; const alpha = Math.round(t3Step(plane.fade[0], plane.fade[1], facing) * 100) / 100; f.o[role] = alpha; if (plane.billboard) { f.t[role] = t3Arena.on ? t3Matrix(f, role, 1, 0, 0, 1, X, Y) : `translate(${t3Num(X)} ${t3Num(Y)})`; continue; } const u = plane.u; const v = plane.v; const a = m[0] * u[0] + m[1] * u[1] + m[2] * u[2]; const b = m[3] * u[0] + m[4] * u[1] + m[5] * u[2]; const c = m[0] * v[0] + m[1] * v[1] + m[2] * v[2]; const d = m[3] * v[0] + m[4] * v[1] + m[5] * v[2]; f.t[role] = t3Arena.on ? t3Matrix(f, role, a, b, c, d, X, Y) : `matrix(${t3Fix(a, 1e5)} ${t3Fix(b, 1e5)} ${t3Fix(c, 1e5)} ${t3Fix(d, 1e5)} ${t3Fix(X, 100)} ${t3Fix(Y, 100)})`; } } function t3EmitDots(run, cam, f, sx = 0, sy = 0) { const m = cam.m; const V = cam.V; for (let j = 0; j < run.dots.length; j++) { const dot = run.dots[j]; const role = run.role[`c${j}`]; const p = dot.p; f.cx[role] = cam.ox - sx + m[0] * p[0] + m[1] * p[1] + m[2] * p[2]; f.cy[role] = cam.oy - sy + m[3] * p[0] + m[4] * p[1] + m[5] * p[2]; const alpha = (dot.n ? t3Step(dot.fade[0], dot.fade[1], dot.n[0] * V[0] + dot.n[1] * V[1] + dot.n[2] * V[2]) : 1) * t3State.detail; f.o[role] = alpha < 0.005 ? 0 : Math.round(alpha * 100) / 100; } } function t3EmitFixed(run, cam, f) { const a = run.anchor; const m = cam.m; const X = cam.ox + m[0] * a[0] + m[1] * a[1] + m[2] * a[2]; const Y = cam.oy + m[3] * a[0] + m[4] * a[1] + m[5] * a[2]; const dx = X - run.home[0]; const dy = Y - run.home[1]; const rx = Math.round(dx * 100) / 100; const ry = Math.round(dy * 100) / 100; f.t[run.role.move] = t3Arena.on ? t3Matrix(f, run.role.move, 1, 0, 0, 1, rx, ry) : rx || ry ? `translate(${t3Num(rx)} ${t3Num(ry)})` : ""; f.shiftX = rx; f.shiftY = ry; const b = run.box0; f.box[0] = b[0] + rx; f.box[1] = b[1] + ry; f.box[2] = b[2] + rx; f.box[3] = b[3] + ry; const count = run.hull0.length / 2; const hull = t3Grow(f, "hull", 2 * count, Float64Array); for (let i = 0; i < count; i++) { hull[2 * i] = run.hull0[2 * i] + rx; hull[2 * i + 1] = run.hull0[2 * i + 1] + ry; } f.hullN = count; if (run.ribs && run.F) t3EmitRibs(run, cam, f, run.F, run.ribs, rx, ry); t3EmitPlanes(run, cam, f, rx, ry); t3EmitDots(run, cam, f, rx, ry); } function t3EmitRibs(run, cam, f, F, ribs, sx, sy) { if (t3State.detail < 0.01) { f.levels = t3Faded(f, run, "r", t3None, 0); return; } const m = cam.m; const V = cam.V; const lines = t3Lines; lines.d.length = 0; lines.alpha.length = 0; lines.key.length = 0; const { s0, s1, count, phase = 0, twist = 0, fade = [0.1, 0.55] } = ribs; const r0 = ribs.r0 ?? ribs.r; const r1 = ribs.r1 ?? r0; const slope = (r1 - r0) / (s1 - s0 || 1); const norm = Math.hypot(1, slope); const aV = F.a[0] * V[0] + F.a[1] * V[1] + F.a[2] * V[2]; const uV = F.u[0] * V[0] + F.u[1] * V[1] + F.u[2] * V[2]; const vV = F.v[0] * V[0] + F.v[1] * V[1] + F.v[2] * V[2]; const seg = new Float64Array(4); const at = (s, r, t, out, index) => { const c = Math.cos(t); const n = Math.sin(t); const px = F.o[0] + F.a[0] * s + r * (c * F.u[0] + n * F.v[0]); const py = F.o[1] + F.a[1] * s + r * (c * F.u[1] + n * F.v[1]); const pz = F.o[2] + F.a[2] * s + r * (c * F.u[2] + n * F.v[2]); out[index] = cam.ox - sx + m[0] * px + m[1] * py + m[2] * pz; out[index + 1] = cam.oy - sy + m[3] * px + m[4] * py + m[5] * pz; }; for (let i = 0; i < count; i++) { const t = phase + (i / count) * T3_TAU; const tm = t + twist / 2; const facing = (Math.cos(tm) * uV + Math.sin(tm) * vV - slope * aV) / norm; const alpha = t3Step(fade[0], fade[1], facing); if (alpha * T3_LEVELS < 0.5) continue; at(s0, r0, t, seg, 0); at(s1, r1, t + twist, seg, 2); lines.d.push(t3Path(seg, 2, false)); lines.alpha.push(alpha); lines.key.push(i); } f.levels = t3Faded(f, run, "r", lines, lines.d.length); } function t3Geo(F, cam) { const m = cam.m; const V = cam.V; const L = cam.L; const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; const lin = (w) => [m[0] * w[0] + m[1] * w[1] + m[2] * w[2], m[3] * w[0] + m[4] * w[1] + m[5] * w[2]]; const O = [cam.ox + m[0] * F.o[0] + m[1] * F.o[1] + m[2] * F.o[2], cam.oy + m[3] * F.o[0] + m[4] * F.o[1] + m[5] * F.o[2]]; const aV = dot(F.a, V); const uV = dot(F.u, V); const vV = dot(F.v, V); const aL = dot(F.a, L); const uL = dot(F.u, L); const vL = dot(F.v, L); return { O, SA: lin(F.a), SU: lin(F.u), SV: lin(F.v), aV, aL, view: { a: aV, flat: Math.hypot(uV, vV), phi: Math.atan2(vV, uV) }, light: { a: aL, flat: Math.hypot(uL, vL), phi: Math.atan2(vL, uL) } }; } function t3At(geo, s, r, t, buf, index) { const c = Math.cos(t); const n = Math.sin(t); buf[index] = geo.O[0] + s * geo.SA[0] + r * (c * geo.SU[0] + n * geo.SV[0]); buf[index + 1] = geo.O[1] + s * geo.SA[1] + r * (c * geo.SU[1] + n * geo.SV[1]); } function t3Arcs(nr, na, view, light) { const cuts = []; if (Math.abs(nr) > 1e-9) { const visibleCut = -(na * view.a) / (nr * view.flat); if (visibleCut > -1 && visibleCut < 1) { const d = Math.acos(visibleCut); cuts.push(view.phi - d, view.phi + d); } for (let b = 0; b < T3_LB.length; b++) { const c = (T3_LB[b] - na * light.a) / (nr * light.flat); if (c > -1 && c < 1) { const d = Math.acos(c); cuts.push(light.phi - d, light.phi + d); } } } for (let i = 0; i < cuts.length; i++) { const w = ((cuts[i] % T3_TAU) + T3_TAU) % T3_TAU; cuts[i] = Math.round(w * 1e9) / 1e9; } cuts.sort((a, b) => a - b); const sorted = []; for (let i = 0; i < cuts.length; i++) if (!sorted.length || cuts[i] !== sorted[sorted.length - 1]) sorted.push(cuts[i]); const label = (angle) => { const facing = nr * view.flat * Math.cos(angle - view.phi) + na * view.a; if (facing <= 1e-9) return -1; const score = nr * light.flat * Math.cos(angle - light.phi) + na * light.a; let tone = 0; while (tone < T3_LB.length && score >= T3_LB[tone]) tone++; return tone; }; if (!sorted.length) { const tone = label(0); return tone < 0 ? [] : [{ tone, from: 0, to: T3_TAU, full: true }]; } const merged = []; for (let index = 0; index < sorted.length; index++) { const from = sorted[index]; const to = index + 1 < sorted.length ? sorted[index + 1] : sorted[0] + T3_TAU; const tone = label((from + to) / 2); const last = merged[merged.length - 1]; if (last && last.tone === tone && Math.abs(last.to - from) < 1e-9) last.to = to; else merged.push({ tone, from, to }); } if (merged.length > 1 && merged[0].tone === merged[merged.length - 1].tone && Math.abs(merged[merged.length - 1].to - T3_TAU - merged[0].from) < 1e-9) { const last = merged.pop(); merged[0].from = last.from - T3_TAU; } const out = []; for (const arc of merged) if (arc.tone >= 0) out.push(arc); return out; } function t3Strip(geo, s0, r0, s1, r1, from, to, step) { const count = Math.max(1, Math.ceil((to - from) / step)); const buf = t3Scratch("c", 4 * count + 8); for (let j = 0; j <= count; j++) { const t = from + ((to - from) * j) / count; t3At(geo, s0, r0, t, buf, 2 * j); t3At(geo, s1, r1, t, buf, 2 * (2 * count + 1 - j)); } return t3Path(t3Oriented(buf, 2 * count + 2), 2 * count + 2, true); } function t3Oriented(buf, count) { let area = 0; for (let i = 0; i < count; i++) { const j = (i + 1) % count; area += buf[2 * i] * buf[2 * j + 1] - buf[2 * j] * buf[2 * i + 1]; } if (area >= 0) return buf; for (let i = 0, j = count - 1; i < j; i++, j--) { const x = buf[2 * i]; const y = buf[2 * i + 1]; buf[2 * i] = buf[2 * j]; buf[2 * i + 1] = buf[2 * j + 1]; buf[2 * j] = x; buf[2 * j + 1] = y; } return buf; } function t3Hull(points) { const sorted = points.slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]); if (sorted.length < 3) return sorted; const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]); const lower = []; for (const p of sorted) { while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 1e-12) lower.pop(); lower.push(p); } const upper = []; for (let i = sorted.length - 1; i >= 0; i--) { const p = sorted[i]; while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 1e-12) upper.pop(); upper.push(p); } return lower.slice(0, -1).concat(upper.slice(0, -1)); } function t3PathOf(points, closed) { if (points.length < 2) return ""; if (t3Arena.on) return t3Token((i) => points[i >> 1][i & 1], points.length, closed); let s = "M" + t3Num(points[0][0]) + " " + t3Num(points[0][1]); for (let i = 1; i < points.length; i++) s += "L" + t3Num(points[i][0]) + " " + t3Num(points[i][1]); return closed ? s + "Z" : s; } function t3Basis(w) { const helper = Math.abs(w[2]) > 0.9 ? [1, 0, 0] : [0, 0, 1]; const u = t3Unit([helper[1] * w[2] - helper[2] * w[1], helper[2] * w[0] - helper[0] * w[2], helper[0] * w[1] - helper[1] * w[0]]); return [u, [w[1] * u[2] - w[2] * u[1], w[2] * u[0] - w[0] * u[2], w[0] * u[1] - w[1] * u[0]]]; } function t3Meet(A, a, B, b) { const ab = A[0] * B[0] + A[1] * B[1] + A[2] * B[2]; const det = 1 - ab * ab; if (det < 1e-12) return []; const alpha = (a - b * ab) / det; const beta = (b - a * ab) / det; const base = [A[0] * alpha + B[0] * beta, A[1] * alpha + B[1] * beta, A[2] * alpha + B[2] * beta]; const rest = 1 - (base[0] * base[0] + base[1] * base[1] + base[2] * base[2]); if (rest < 0) return []; const cr = [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 k = Math.sqrt(rest / det); return [ [base[0] + cr[0] * k, base[1] + cr[1] * k, base[2] + cr[2] * k], [base[0] - cr[0] * k, base[1] - cr[1] * k, base[2] - cr[2] * k], ]; } const t3Tables = new Map(); function t3Table(count) { let table = t3Tables.get(count); if (!table) { table = new Float64Array(2 * count); for (let j = 0; j < count; j++) { table[2 * j] = Math.cos((j / count) * T3_TAU); table[2 * j + 1] = Math.sin((j / count) * T3_TAU); } t3Tables.set(count, table); } return table; } function t3Dome(cam, C, e, r, count, sheets) { const m = cam.m; const V = cam.V; const L = cam.L; const Sx = cam.ox + m[0] * C[0] + m[1] * C[1] + m[2] * C[2]; const Sy = cam.oy + m[3] * C[0] + m[4] * C[1] + m[5] * C[2]; const flat = (n) => [Sx + r * (m[0] * n[0] + m[1] * n[1] + m[2] * n[2]), Sy + r * (m[3] * n[0] + m[4] * n[1] + m[5] * n[2])]; const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; const table = t3Table(count); const circle = (W, value, keep) => { const [U, Q] = t3Basis(W); const rad = Math.sqrt(Math.max(0, 1 - value * value)); const out = []; for (let j = 0; j < count; j++) { const cs = table[2 * j] * rad; const sn = table[2 * j + 1] * rad; const n = [W[0] * value + U[0] * cs + Q[0] * sn, W[1] * value + U[1] * cs + Q[1] * sn, W[2] * value + U[2] * cs + Q[2] * sn]; if (keep(n)) out.push(n); } return out; }; const rim = circle(V, 0, (n) => dot(n, e) >= -1e-9).map((n) => [flat(n), dot(n, L)]); const base = circle(e, 0, (n) => dot(n, V) >= -1e-9).map((n) => [flat(n), dot(n, L)]); const corners = t3Meet(V, 0, e, 0).map((n) => [flat(n), dot(n, L)]); for (let tone = 0; tone < 5; tone++) { const tau = tone === 0 ? -Infinity : T3_LB[tone - 1]; const candidates = []; for (const list of [rim, base, corners]) for (const [p, lit] of list) if (lit >= tau - 1e-9) candidates.push(p); if (tone > 0) { for (const n of circle(L, tau, (n) => dot(n, V) >= -1e-9 && dot(n, e) >= -1e-9)) candidates.push(flat(n)); for (const n of t3Meet(L, tau, V, 0)) if (dot(n, e) >= -1e-9) candidates.push(flat(n)); for (const n of t3Meet(L, tau, e, 0)) if (dot(n, V) >= -1e-9) candidates.push(flat(n)); } if (candidates.length < 3) continue; const region = t3Hull(candidates); if (region.length >= 3) sheets[tone] += t3PathOf(region, true); } } function t3BoxOf(pts, box) { let x0 = Infinity; let y0 = Infinity; let x1 = -Infinity; let y1 = -Infinity; for (const [x, y] of pts) { if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y; } box[0] = x0; box[1] = y0; box[2] = x1; box[3] = y1; } function t3Thin(points, most) { if (points.length <= most) return points; const n = points.length; const picks = []; for (let i = 0; i < most; i++) picks.push(Math.floor((i * n) / most)); const thin = picks.map((i) => points[i]); let err = 0; for (let j = 0; j < most; j++) { const a = thin[j]; const b = thin[(j + 1) % most]; const dx = b[0] - a[0]; const dy = b[1] - a[1]; const l = Math.hypot(dx, dy) || 1; const end = j + 1 < most ? picks[j + 1] : picks[0] + n; for (let i = picks[j] + 1; i < end; i++) { const p = points[i % n]; const out = ((p[0] - a[0]) * dy - (p[1] - a[1]) * dx) / l; if (Math.abs(out) > err) err = Math.abs(out); } } return t3Grown(thin, err); } function t3Grown(poly, by) { if (!(by > 0)) return poly; let area = 0; for (let i = 0; i < poly.length; i++) { const a = poly[i]; const b = poly[(i + 1) % poly.length]; area += a[0] * b[1] - b[0] * a[1]; } const sign = area >= 0 ? 1 : -1; const n = poly.length; const normals = poly.map((a, i) => { const b = poly[(i + 1) % n]; const l = Math.hypot(b[0] - a[0], b[1] - a[1]) || 1; return [(sign * (b[1] - a[1])) / l, (-sign * (b[0] - a[0])) / l]; }); return poly.map((p, i) => { const na = normals[(i - 1 + n) % n]; const nb = normals[i]; const mx = na[0] + nb[0]; const my = na[1] + nb[1]; const dd = (mx * mx + my * my) / 2 || 1; return [p[0] + (mx * by) / dd, p[1] + (my * by) / dd]; }); } function t3HullOut(f, points, most = 32) { const thin = t3Thin(points, most); const hull = t3Grow(f, "hull", 2 * thin.length, Float64Array); for (let i = 0; i < thin.length; i++) { hull[2 * i] = thin[i][0]; hull[2 * i + 1] = thin[i][1]; } f.hullN = thin.length; } function t3SupportHull(geo, cam, components, dirs) { const table = t3Table(dirs); const pts = []; const { O, SA, SU, SV } = geo; for (let j = 0; j < dirs; j++) { const dx = table[2 * j]; const dy = table[2 * j + 1]; const pa = SA[0] * dx + SA[1] * dy; const pu = SU[0] * dx + SU[1] * dy; const pv = SV[0] * dx + SV[1] * dy; const w = Math.hypot(pu, pv); let best = -Infinity; let px = 0; let py = 0; for (let c = 0; c < components.length; c += 3) { const kind = components[c]; const s = components[c + 1]; const r = components[c + 2]; if (kind === 0) { const value = s * pa + r * w; if (value > best) { best = value; const cs = w > 1e-12 ? pu / w : 1; const sn = w > 1e-12 ? pv / w : 0; px = O[0] + s * SA[0] + r * (cs * SU[0] + sn * SV[0]); py = O[1] + s * SA[1] + r * (cs * SU[1] + sn * SV[1]); } } else { const rk = r * cam.k; const value = s * pa + rk; if (value > best) { best = value; px = O[0] + s * SA[0] + rk * dx; py = O[1] + s * SA[1] + rk * dy; } } } const last = pts[pts.length - 1]; if (!last || Math.abs(last[0] - px) + Math.abs(last[1] - py) > 0.02) pts.push([px, py]); } if (pts.length > 2) { const a = pts[0]; const b = pts[pts.length - 1]; if (Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]) <= 0.02) pts.pop(); } return pts; } function t3ArcPoints(geo, s, r, from, to, k, out, cut = false) { const count = Math.max(2, Math.ceil((Math.abs(to - from) * 48) / T3_TAU), Math.ceil((Math.abs(to - from) * Math.max(r, 1e-6) * k) / 1.5)); for (let j = 0; j <= count; j++) { const t = from + ((to - from) * j) / count; const c = Math.cos(t); const n = Math.sin(t); const p = [geo.O[0] + s * geo.SA[0] + r * (c * geo.SU[0] + n * geo.SV[0]), geo.O[1] + s * geo.SA[1] + r * (c * geo.SU[1] + n * geo.SV[1])]; if (cut) p.cut = true; out.push(p); } } function t3RoundOutline(run, geo, cam) { const k = cam.k; const { s0, s1, r0, r1, ends } = run; const domes = (ends[0] === "dome" ? 1 : 0) + (ends[1] === "dome" ? 1 : 0); if (domes === 2) { const c0 = [geo.O[0] + s0 * geo.SA[0], geo.O[1] + s0 * geo.SA[1]]; const c1 = [geo.O[0] + s1 * geo.SA[0], geo.O[1] + s1 * geo.SA[1]]; const rk = r0 * k; const dx = c1[0] - c0[0]; const dy = c1[1] - c0[1]; const l = Math.hypot(dx, dy); const count = Math.max(24, Math.ceil((Math.PI * rk) / 1.5)); const out = []; if (l < 1e-6) { for (let j = 0; j < 2 * count; j++) { const t = (j / (2 * count)) * T3_TAU; out.push([c1[0] + rk * Math.cos(t), c1[1] + rk * Math.sin(t)]); } return out; } const beta = Math.atan2(dy, dx); for (let j = 0; j <= count; j++) { const t = beta - Math.PI / 2 + (Math.PI * j) / count; out.push([c1[0] + rk * Math.cos(t), c1[1] + rk * Math.sin(t)]); } for (let j = 0; j <= count; j++) { const t = beta + Math.PI / 2 + (Math.PI * j) / count; out.push([c0[0] + rk * Math.cos(t), c0[1] + rk * Math.sin(t)]); } return out; } if (domes === 1) { const pts = []; for (const [end, s, r] of [[0, s0, r0], [1, s1, r1]]) { t3ArcPoints(geo, s, r, 0, T3_TAU, k, pts); if (ends[end] !== "dome") continue; const sign = end ? 1 : -1; const c = [geo.O[0] + s * geo.SA[0], geo.O[1] + s * geo.SA[1]]; const rk = r * k; const beta = Math.atan2(sign * geo.SA[1], sign * geo.SA[0]); const count = Math.max(24, Math.ceil((Math.PI * rk) / 1.5)); for (let j = 0; j <= count; j++) { const t = beta - Math.PI / 2 + (Math.PI * j) / count; pts.push([c[0] + rk * Math.cos(t), c[1] + rk * Math.sin(t)]); } } return t3Hull(pts); } const view = geo.view; const out = []; let c; if (view.flat < 1e-9) c = run.na * view.a > 0 ? -2 : 2; else c = -(run.na * view.a) / (run.nr * view.flat); if (c <= -1 || c >= 1) { const side = c <= -1; const capFirst = -geo.aV > 0; const keepFirst = side ? !capFirst : capFirst; const s = keepFirst ? s0 : s1; const r = keepFirst ? r0 : r1; t3ArcPoints(geo, s, r, 0, T3_TAU, k, out, run.cutS !== null && Math.abs(run.cutS - s) < 1e-6); out.pop(); return out; } const d = Math.acos(c); const lo = view.phi - d; const hi = view.phi + d; const cut0 = run.cutS !== null && Math.abs(run.cutS - s0) < 1e-6; const cut1 = run.cutS !== null && Math.abs(run.cutS - s1) < 1e-6; if (-geo.aV > 0) t3ArcPoints(geo, s0, r0, hi, lo + T3_TAU, k, out, cut0); else t3ArcPoints(geo, s0, r0, lo, hi, k, out, cut0); const tail = []; if (geo.aV > 0) t3ArcPoints(geo, s1, r1, hi, lo + T3_TAU, k, tail, cut1); else t3ArcPoints(geo, s1, r1, lo, hi, k, tail, cut1); const first = out[out.length - 1]; const a = tail[0]; const b = tail[tail.length - 1]; if (Math.hypot(a[0] - first[0], a[1] - first[1]) > Math.hypot(b[0] - first[0], b[1] - first[1])) tail.reverse(); for (const p of tail) out.push(p); return out; } function t3LatheMid(a, b) { const h = b.s - a.s; const ga = -a.na / a.nr; const gb = -b.na / b.nr; const nr = a.nr + b.nr; const na = a.na + b.na; const l = Math.hypot(nr, na) || 1; return { s: (a.s + b.s) / 2, r: (a.r + b.r) / 2 + (h * (ga - gb)) / 8, nr: nr / l, na: na / l }; } function t3LatheOutline(run, geo, cam) { const k = cam.k; const view = geo.view; const lv = run.levels; const pts = []; const cut = (L) => (view.flat < 1e-9 ? Infinity : -(L.na * view.a) / (L.nr * view.flat)); const inside = (L) => L.r <= 1e-9 || Math.abs(cut(L)) < 1; const sides = (L) => { if (L.r <= 1e-9) { const p = [geo.O[0] + L.s * geo.SA[0], geo.O[1] + L.s * geo.SA[1]]; return [p, p]; } const c = Math.max(-1, Math.min(1, cut(L))); const d = Math.acos(c); return [view.phi - d, view.phi + d].map((t) => { const cs = Math.cos(t); const sn = Math.sin(t); return [geo.O[0] + L.s * geo.SA[0] + L.r * (cs * geo.SU[0] + sn * geo.SV[0]), geo.O[1] + L.s * geo.SA[1] + L.r * (cs * geo.SU[1] + sn * geo.SV[1])]; }); }; const span = (a, b) => { const pa = sides(a); const pb = sides(b); return Math.max(Math.hypot(pa[0][0] - pb[0][0], pa[0][1] - pb[0][1]), Math.hypot(pa[1][0] - pb[1][0], pa[1][1] - pb[1][1])); }; const refine = (a, b, depth) => { if (depth >= 10 || span(a, b) < 1.2) return; const mid = t3LatheMid(a, b); refine(a, mid, depth + 1); pts.push(...sides(mid)); refine(mid, b, depth + 1); }; let previous = null; for (let i = 0; i < lv.length; i += 4) { const L = { s: lv[i], r: lv[i + 1], nr: lv[i + 2], na: lv[i + 3] }; if (inside(L)) pts.push(...sides(L)); if (previous) { const pin = inside(previous); const lin = inside(L); if (pin && lin) refine(previous, L, 0); else if (pin !== lin) { let a = pin ? previous : L; let b = pin ? L : previous; const start = a; for (let it = 0; it < 14; it++) { const mid = t3LatheMid(a.s < b.s ? a : b, a.s < b.s ? b : a); if (inside(mid)) a = mid; else b = mid; } pts.push(...sides(a)); refine(start.s < a.s ? start : a, start.s < a.s ? a : start, 0); } } previous = L; } for (const i of [0, lv.length - 4]) if (lv[i + 1] > 1e-9) t3ArcPoints(geo, lv[i], lv[i + 1], 0, T3_TAU, k, pts); return t3Hull(pts); } function t3SeamArc(geo, s, r, slope, k, trim = 0.3) { const view = geo.view; const norm = Math.hypot(1, slope); let lo; let hi; if (view.flat < 1e-9) { if (!((-slope * view.a) / norm > 0)) return ""; lo = view.phi; hi = view.phi + T3_TAU; } else { const cc = (slope * view.a) / view.flat; if (cc >= 1) return ""; if (cc <= -1) { lo = view.phi; hi = view.phi + T3_TAU; } else { const d = Math.acos(cc); lo = view.phi - d; hi = view.phi + d; } } const full = hi - lo >= T3_TAU - 1e-9; const buf = t3Scratch("c", 2 * 520); if (!full) { const span = hi - lo; const cutAt = (from, sign) => { t3At(geo, s, r, from, buf, 0); const px = buf[0]; const py = buf[1]; let a = 0; let b = span * 0.25; t3At(geo, s, r, from + sign * b, buf, 0); if (Math.hypot(buf[0] - px, buf[1] - py) <= trim) return b; for (let it = 0; it < 18; it++) { const mid = (a + b) / 2; t3At(geo, s, r, from + sign * mid, buf, 0); if (Math.hypot(buf[0] - px, buf[1] - py) < trim) a = mid; else b = mid; } return (a + b) / 2; }; const d0 = cutAt(lo, 1); const d1 = cutAt(hi, -1); lo += d0; hi -= d1; } const count = Math.min(512, Math.max(4, Math.ceil(((hi - lo) * r * k) / 1.5))); for (let j = 0; j <= count; j++) t3At(geo, s, r, lo + ((hi - lo) * j) / count, buf, 2 * j); return t3Path(buf, full ? count : count + 1, full); } function t3RibLines(geo, F, cam, ribs, sx, sy) { const lines = t3Lines; lines.d.length = 0; lines.alpha.length = 0; lines.key.length = 0; const { s0, s1, count, phase = 0, twist = 0, fade = [0.1, 0.55] } = ribs; const r0 = ribs.r0 ?? ribs.r; const r1 = ribs.r1 ?? r0; const slope = (r1 - r0) / (s1 - s0 || 1); const norm = Math.hypot(1, slope); const view = geo.view; const seg = new Float64Array(4); for (let i = 0; i < count; i++) { const t = phase + (i / count) * T3_TAU; const tm = t + twist / 2; const facing = (view.flat * Math.cos(tm - view.phi) - slope * view.a) / norm; const alpha = t3Step(fade[0], fade[1], facing); if (alpha * T3_LEVELS < 0.5) continue; t3At(geo, s0, r0, t, seg, 0); t3At(geo, s1, r1, t + twist, seg, 2); seg[0] -= sx; seg[1] -= sy; seg[2] -= sx; seg[3] -= sy; lines.d.push(t3Path(seg, 2, false)); lines.alpha.push(alpha); lines.key.push(i); } return lines; } function t3PrepareRound(run, part, P, unit) { const spec = part.spec; const k = P.scale * T3_ROOT; run.F = spec.F; run.s0 = spec.s0; run.s1 = spec.s1; run.r0 = spec.r0; run.r1 = spec.r1; run.ends = spec.ends; const g = (spec.r1 - spec.r0) / (spec.s1 - spec.s0 || 1); const norm = Math.hypot(1, g); run.nr = 1 / norm; run.na = -g / norm; run.g = g; const rmax = Math.max(spec.r0, spec.r1); const around = T3_TAU * rmax * k * unit; run.fine = Math.max(around < 30 ? 16 : 24, Math.min(96, Math.ceil(around / 2.2))); run.dirs = Math.max(around < 30 ? 16 : 32, Math.min(256, Math.ceil(around / 1.5))); run.capCount = Math.max(around < 30 ? 16 : 24, Math.min(160, Math.ceil(around / 1.5))); run.domeCount = Math.max(around < 30 ? 12 : 16, Math.min(48, Math.round(run.fine * 0.75))); const comps = []; for (const [end, s, r] of [[0, spec.s0, spec.r0], [1, spec.s1, spec.r1]]) comps.push(spec.ends[end] === "dome" ? 1 : 0, s, r); run.components = comps; const details = part.details ?? {}; run.seams = (details.seams ?? []).map((s) => ({ s, r: spec.r0 + g * (s - spec.s0), slope: g })); run.ribs = details.ribs ? { ...details.ribs, r0: details.ribs.r0 ?? spec.r0 + g * (details.ribs.s0 - spec.s0), r1: details.ribs.r1 ?? spec.r0 + g * (details.ribs.s1 - spec.s0) } : null; run.cutS = null; for (const cut of spec.cuts ?? []) { const along = cut.n[0] * spec.F.a[0] + cut.n[1] * spec.F.a[1] + cut.n[2] * spec.F.a[2]; const sCut = (cut.d - (cut.n[0] * spec.F.o[0] + cut.n[1] * spec.F.o[1] + cut.n[2] * spec.F.o[2])) / along; if (Math.abs(sCut - spec.s0) < 1e-6 || Math.abs(sCut - spec.s1) < 1e-6) run.cutS = sCut; } } function t3EmitRound(run, cam, f) { const geo = t3Geo(run.F, cam); const k = cam.k; const step = T3_TAU / run.fine; const sheets = ["", "", "", "", ""]; const { s0, s1, r0, r1 } = run; for (const arc of t3Arcs(run.nr, run.na, geo.view, geo.light)) sheets[arc.tone] += t3Strip(geo, s0, r0, s1, r1, arc.from, arc.to, step); const F = run.F; for (let end = 0; end < 2; end++) { if (run.ends[end] !== "dome") continue; const s = end ? s1 : s0; const sign = end ? 1 : -1; const C = [F.o[0] + F.a[0] * s, F.o[1] + F.a[1] * s, F.o[2] + F.a[2] * s]; t3Dome(cam, C, [F.a[0] * sign, F.a[1] * sign, F.a[2] * sign], end ? r1 : r0, run.domeCount, sheets); } for (let t = 0; t < 5; t++) f.d[run.role[`s${t}`]] = sheets[t]; const pts = t3RoundOutline(run, geo, cam); const outline = t3PathOf(pts, true); f.d[run.role.fill] = outline; if (run.cutS !== null) { const buf = new Float64Array(2 * pts.length); const skip = new Uint8Array(pts.length); pts.forEach((p, j) => { buf[2 * j] = p[0]; buf[2 * j + 1] = p[1]; }); for (let j = 0; j < pts.length; j++) skip[j] = pts[j].cut && pts[(j + 1) % pts.length].cut ? 1 : 0; f.d[run.role.edge] = t3Broken(buf, pts.length, skip); } else f.d[run.role.edge] = outline; t3BoxOf(pts, f.box); t3HullOut(f, pts); const view = geo.view; let crease = ""; for (let end = 0; end < 2; end++) { if (run.ends[end] !== "flat") continue; const role = run.role[`k${end}`]; const sign = end ? 1 : -1; const s = end ? s1 : s0; const r = end ? r1 : r0; const face = sign * geo.aV; if (!(face > 1e-9)) { f.d[role] = ""; continue; } const n = run.capCount; const buf = t3Scratch("c", 2 * n); const table = t3Table(n); for (let j = 0; j < n; j++) { const c = table[2 * j]; const q = table[2 * j + 1]; buf[2 * j] = geo.O[0] + s * geo.SA[0] + r * (c * geo.SU[0] + q * geo.SV[0]); buf[2 * j + 1] = geo.O[1] + s * geo.SA[1] + r * (c * geo.SU[1] + q * geo.SV[1]); } f.d[role] = t3Path(buf, n, true); f.q[role] = t3Level(sign * geo.aL, T3_LB); const minor = 2 * r * k * Math.abs(face); f.w[role] = minor < 0.4 ? Math.round(minor * 100) / 100 : -1; if (run.role.crease === undefined) continue; let lo; let hi; if (view.flat < 1e-9) { if (!(run.na * view.a > 1e-9)) continue; lo = 0; hi = T3_TAU; } else { const cc = -(run.na * view.a) / (run.nr * view.flat); if (cc >= 1) continue; if (cc <= -1) { lo = view.phi; hi = view.phi + T3_TAU; } else { const d = Math.acos(cc); lo = view.phi - d; hi = view.phi + d; } } const count = Math.min(400, Math.max(4, Math.ceil(((hi - lo) * r * k) / 1.5))); const arc = t3Scratch("c", 2 * count + 2); for (let j = 0; j <= count; j++) t3At(geo, s, r, lo + ((hi - lo) * j) / count, arc, 2 * j); crease += t3Path(arc, count + 1, false); } if (run.role.crease !== undefined) f.d[run.role.crease] = crease; if (run.role.inner !== undefined) { let inner = ""; for (const seam of run.seams) inner += t3SeamArc(geo, seam.s, seam.r, seam.slope, k); if (run.ribs && run.ribs.seams) { const rs = run.ribs; const slope = (rs.r1 - rs.r0) / (rs.s1 - rs.s0 || 1); if (rs.seams !== "last") inner += t3SeamArc(geo, rs.s0, rs.r0, slope, k); if (rs.seams !== "first") inner += t3SeamArc(geo, rs.s1, rs.r1, slope, k); } f.d[run.role.inner] = inner; } if (run.ribs) { const lines = t3State.detail < 0.01 ? t3None : t3RibLines(geo, run.F, cam, run.ribs, 0, 0); f.levels = t3Faded(f, run, "r", lines, lines.d.length); } } function t3PrepareBall(run, part, P, unit) { const spec = part.spec; const k = P.scale * T3_ROOT; run.c = spec.c; run.r = spec.r; run.flats = spec.flats.map((flat) => { const n = t3Unit(flat.n); const rho = Math.sqrt(Math.max(0, spec.r * spec.r - flat.d * flat.d)); const [u, v] = t3Basis(n); return { n, d: flat.d, rho, u, v }; }); run.limb = Math.max(32, Math.min(160, Math.ceil((T3_TAU * spec.r * k * unit) / 1.5))); run.rimCount = run.flats.map((flat) => Math.max(20, Math.min(120, Math.ceil((T3_TAU * flat.rho * k * unit) / 1.5)))); } function t3ClipConvex(subject, clip) { if (subject.length < 3 || clip.length < 3) return []; let area = 0; for (let i = 0; i < clip.length; i++) { const a = clip[i]; const b = clip[(i + 1) % clip.length]; area += a[0] * b[1] - b[0] * a[1]; } const sign = area >= 0 ? 1 : -1; let out = subject; for (let i = 0; i < clip.length && out.length; i++) { const a = clip[i]; const b = clip[(i + 1) % clip.length]; const side = (p) => sign * ((b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0])); const input = out; out = []; for (let j = 0; j < input.length; j++) { const p = input[j]; const q = input[(j + 1) % input.length]; const sp = side(p); const sq = side(q); if (sp >= 0) out.push(p); if ((sp >= 0) !== (sq >= 0)) { const t = sp / (sp - sq); out.push([p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t]); } } } return out; } function t3EmitBall(run, cam, f) { const m = cam.m; const V = cam.V; const L = cam.L; const { c, r, flats } = run; const Cx = cam.ox + m[0] * c[0] + m[1] * c[1] + m[2] * c[2]; const Cy = cam.oy + m[3] * c[0] + m[4] * c[1] + m[5] * c[2]; const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; const flat = (w) => [Cx + r * (m[0] * w[0] + m[1] * w[1] + m[2] * w[2]), Cy + r * (m[3] * w[0] + m[4] * w[1] + m[5] * w[2])]; const inside = (w, skip = -1) => { for (let i = 0; i < flats.length; i++) if (i !== skip && dot(w, flats[i].n) * r > flats[i].d + 1e-7) return false; return true; }; const [U, W] = t3Basis(V); const n = run.limb; const table = t3Table(n); const limb = []; for (let j = 0; j < n; j++) limb.push([U[0] * table[2 * j] + W[0] * table[2 * j + 1], U[1] * table[2 * j] + W[1] * table[2 * j + 1], U[2] * table[2 * j] + W[2] * table[2 * j + 1]]); const sil = []; for (const w of limb) if (inside(w)) sil.push(flat(w)); const rims = flats.map((cut, ci) => { const count = run.rimCount[ci]; const t = t3Table(count); const list = []; for (let j = 0; j < count; j++) { const w = [(cut.n[0] * cut.d + cut.rho * (cut.u[0] * t[2 * j] + cut.v[0] * t[2 * j + 1])) / r, (cut.n[1] * cut.d + cut.rho * (cut.u[1] * t[2 * j] + cut.v[1] * t[2 * j + 1])) / r, (cut.n[2] * cut.d + cut.rho * (cut.u[2] * t[2 * j] + cut.v[2] * t[2 * j + 1])) / r]; list.push({ w, keep: inside(w, ci), p: flat(w) }); } return list; }); flats.forEach((cut, ci) => { for (const w of t3Meet(V, 0, cut.n, cut.d / r)) if (inside(w, ci)) sil.push(flat(w)); rims[ci].forEach(({ keep, p }) => keep && sil.push(p)); }); const hull = t3Hull(sil); const outline = t3PathOf(hull, true); f.d[run.role.fill] = outline; f.d[run.role.edge] = outline; f.d[run.role.s0] = outline; const capOf = (threshold) => { const points = []; for (const w of limb) if (dot(w, L) >= threshold && inside(w)) points.push(flat(w)); const radius = Math.sqrt(Math.max(0, 1 - threshold * threshold)); const [LU, LW] = t3Basis(L); for (let j = 0; j < n; j++) { const w = [L[0] * threshold + radius * (LU[0] * table[2 * j] + LW[0] * table[2 * j + 1]), L[1] * threshold + radius * (LU[1] * table[2 * j] + LW[1] * table[2 * j + 1]), L[2] * threshold + radius * (LU[2] * table[2 * j] + LW[2] * table[2 * j + 1])]; if (dot(w, V) >= -1e-9) points.push(flat(w)); } if (dot(L, V) > threshold) points.push(flat(V)); for (const w of t3Meet(L, threshold, V, 0)) if (inside(w)) points.push(flat(w)); flats.forEach((cut, ci) => { for (const w of t3Meet(V, 0, cut.n, cut.d / r)) if (inside(w, ci) && dot(w, L) >= threshold) points.push(flat(w)); for (const w of t3Meet(L, threshold, cut.n, cut.d / r)) if (inside(w, ci) && dot(w, V) >= -1e-9) points.push(flat(w)); for (const { w, keep, p } of rims[ci]) if (keep && dot(w, L) >= threshold && dot(w, V) >= -1e-9) points.push(p); }); if (points.length < 3) return ""; const region = t3ClipConvex(t3Hull(points), hull); return region.length >= 3 ? t3PathOf(region, true) : ""; }; f.d[run.role.s1] = capOf(T3_LB[1]); f.d[run.role.s2] = capOf(T3_LB[2]); f.d[run.role.s3] = capOf(T3_LB[3]); let crease = ""; flats.forEach((cut, ci) => { const role = run.role[`f${ci}`]; const face = dot(cut.n, V); if (!(face > 1e-9)) { f.d[role] = ""; return; } const pts = t3ClipConvex(rims[ci].map(({ p }) => p), hull); f.d[role] = t3PathOf(pts, true); f.q[role] = t3Level(dot(cut.n, L), T3_LB); const minor = 2 * cut.rho * cam.k * face; f.w[role] = minor < 0.4 ? Math.round(minor * 100) / 100 : -1; const list = rims[ci]; const seen = list.map(({ w, keep }) => keep && dot(w, V) > 0); const start = seen.indexOf(false); if (start < 0) { crease += t3PathOf(list.map(({ p }) => p), true); return; } let cur = null; for (let s = 1; s <= list.length; s++) { const i = (start + s) % list.length; if (seen[i]) (cur ??= []).push(list[i].p); else if (cur) { if (cur.length > 1) crease += t3PathOf(cur, false); cur = null; } } if (cur && cur.length > 1) crease += t3PathOf(cur, false); }); if (run.role.crease !== undefined) f.d[run.role.crease] = crease; let x0 = Infinity; let y0 = Infinity; let x1 = -Infinity; let y1 = -Infinity; for (const [x, y] of hull) { if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y; } f.box[0] = x0; f.box[1] = y0; f.box[2] = x1; f.box[3] = y1; t3HullOut(f, hull); } function t3PrepareLathe(run, part, P, unit) { const spec = part.spec; const k = P.scale * T3_ROOT; run.F = spec.F; run.levels = spec.levels; run.caps = spec.caps; let rmax = 0; for (let i = 0; i < spec.levels.length; i += 4) rmax = Math.max(rmax, spec.levels[i + 1]); run.fine = Math.max(24, Math.min(96, Math.ceil((T3_TAU * rmax * k * unit) / 2.2))); run.dirs = Math.max(32, Math.min(256, Math.ceil((T3_TAU * rmax * k * unit) / 1.5))); run.capCount = Math.max(24, Math.min(160, Math.ceil((T3_TAU * rmax * k * unit) / 1.5))); const comps = []; for (let i = 0; i < spec.levels.length; i += 4) comps.push(0, spec.levels[i], spec.levels[i + 1]); run.components = comps; const details = part.details ?? {}; run.seams = (details.seams ?? []).map((seam) => (typeof seam === "number" ? t3LatheSeam(spec.levels, seam) : seam)); run.ribs = details.ribs ?? null; } function t3LatheSeam(levels, s) { for (let i = 0; i + 4 < levels.length; i += 4) { const sa = levels[i]; const sb = levels[i + 4]; if ((s - sa) * (s - sb) <= 0 && sa !== sb) { const t = (s - sa) / (sb - sa); const r = levels[i + 1] + (levels[i + 5] - levels[i + 1]) * t; const slope = (levels[i + 5] - levels[i + 1]) / (sb - sa); return { s, r, slope }; } } return { s, r: levels[1], slope: 0 }; } function t3LatheSheets(levels, geo, step, buckets) { const at = (L, t) => { const c = Math.cos(t); const n = Math.sin(t); return [geo.O[0] + L.s * geo.SA[0] + L.r * (c * geo.SU[0] + n * geo.SV[0]), geo.O[1] + L.s * geo.SA[1] + L.r * (c * geo.SU[1] + n * geo.SV[1])]; }; const arcPoints = (L, from, to) => { const count = Math.max(1, Math.ceil((to - from) / step)); const out = new Array(count + 1); for (let index = 0; index <= count; index++) out[index] = at(L, from + ((to - from) * index) / count); return out; }; const oriented = (points) => { let area = 0; for (let i = 0; i < points.length; i++) { const a = points[i]; const b = points[(i + 1) % points.length]; area += a[0] * b[1] - b[0] * a[1]; } return area < 0 ? points.reverse() : points; }; const centreOf = (arc, reference) => { let mid = (arc.from + arc.to) / 2 - reference; mid = ((mid % T3_TAU) + T3_TAU) % T3_TAU; return mid; }; const present = [false, false, false, false, false]; for (const L of levels) { const groups = [null, null, null, null, null]; for (const arc of L.arcs) { if (!groups[arc.tone]) groups[arc.tone] = []; groups[arc.tone].push(arc); present[arc.tone] = true; } for (const list of groups) if (list && list.length > 1) list.sort((x, y) => centreOf(x, geo.light.phi) - centreOf(y, geo.light.phi)); L.groups = groups; } const align = (reference, arc) => { const out = { ...arc }; const centre = (reference.from + reference.to) / 2; while ((out.from + out.to) / 2 - centre > Math.PI) { out.from -= T3_TAU; out.to -= T3_TAU; } while (centre - (out.from + out.to) / 2 > Math.PI) { out.from += T3_TAU; out.to += T3_TAU; } return out; }; const overlap = (x, y) => Math.min(x.to, y.to) - Math.max(x.from, y.from); const sheet = (tone, run) => { const chains = run[0].groups[tone].map((arc) => [arc]); for (let q = 1; q < run.length; q++) { const next = run[q].groups[tone]; const used = new Uint8Array(next.length); for (const chain of chains) { const prev = chain[q - 1]; let best = -1; let score = -Infinity; for (let j = 0; j < next.length; j++) { if (used[j]) continue; const value = overlap(prev, align(prev, next[j])); if (value > score) { score = value; best = j; } } used[best] = 1; chain.push(align(prev, next[best])); } } chains.forEach((chain) => { const last = run.length - 1; const points = arcPoints(run[0], chain[0].from, chain[0].to); for (let q = 1; q < last; q++) points.push(at(run[q], chain[q].to)); points.push(...arcPoints(run[last], chain[last].from, chain[last].to).reverse()); for (let q = last - 1; q >= 1; q--) points.push(at(run[q], chain[q].from)); buckets[tone] += t3PathOf(oriented(points), true); }); }; const strip = (tone, a, b) => { const below = a.groups[tone] ?? []; const above = b.groups[tone] ?? []; const whole = (arc) => arc.to - arc.from >= T3_TAU - 1e-6; const seat = (ring, arc) => { const gap = T3_TAU - (arc.to - arc.from); return { from: arc.from - gap / 2, to: arc.to + gap / 2 }; }; const quad = (lo, hi) => { if (whole(hi) && !whole(lo)) hi = seat(hi, lo); else if (whole(lo) && !whole(hi)) lo = seat(lo, hi); const points = [...arcPoints(a, lo.from, lo.to), ...arcPoints(b, hi.from, hi.to).reverse()]; buckets[tone] += t3PathOf(oriented(points), true); }; const links = below.map(() => []); const back = above.map(() => []); below.forEach((lo, i) => above.forEach((hi, j) => { if (overlap(lo, align(lo, hi)) > -0.05) { links[i].push(j); back[j].push(i); } }), ); const doneLo = new Uint8Array(below.length); const doneHi = new Uint8Array(above.length); const share = (span, parts, flip) => { if (whole(span)) { const base = parts[0].from; const sorted = parts.map((arc) => { const shift = T3_TAU * Math.floor((arc.from - base) / T3_TAU); return { from: arc.from - shift, to: arc.to - shift }; }).sort((x, y) => x.from - y.from); const n = sorted.length; const cuts = sorted.map((arc, q) => { const next = q + 1 < n ? sorted[q + 1].from : sorted[0].from + T3_TAU; return (arc.to + next) / 2; }); sorted.forEach((arc, q) => { const piece = { from: q ? cuts[q - 1] : cuts[n - 1] - T3_TAU, to: cuts[q] }; if (flip) quad(arc, piece); else quad(piece, arc); }); return; } const sorted = parts.map((arc) => align(span, arc)).sort((x, y) => x.from - y.from); const cuts = [span.from]; for (let q = 1; q < sorted.length; q++) cuts.push(Math.min(span.to, Math.max(span.from, (sorted[q - 1].to + sorted[q].from) / 2))); cuts.push(span.to); sorted.forEach((arc, q) => { const piece = { from: cuts[q], to: cuts[q + 1] }; if (flip) quad(arc, piece); else quad(piece, arc); }); }; for (let j = 0; j < above.length; j++) { if (back[j].length < 2 || back[j].some((i) => links[i].length !== 1)) continue; share(above[j], back[j].map((i) => below[i]), false); doneHi[j] = 1; for (const i of back[j]) doneLo[i] = 1; } for (let i = 0; i < below.length; i++) { if (doneLo[i] || links[i].length < 2 || links[i].some((j) => back[j].length !== 1)) continue; share(below[i], links[i].map((j) => above[j]), true); doneLo[i] = 1; for (const j of links[i]) doneHi[j] = 1; } for (let i = 0; i < below.length; i++) { if (doneLo[i]) continue; const lo = below[i]; let best = -1; let score = -Infinity; for (const j of links[i]) { if (doneHi[j]) continue; const value = overlap(lo, align(lo, above[j])); if (value > score) { score = value; best = j; } } if (best >= 0) { doneHi[best] = 1; quad(lo, align(lo, above[best])); } else { const centre = (lo.from + lo.to) / 2; quad(lo, { from: centre, to: centre }); } } for (let j = 0; j < above.length; j++) { if (doneHi[j]) continue; const centre = (above[j].from + above[j].to) / 2; quad({ from: centre, to: centre }, above[j]); } }; for (let tone = 0; tone < 5; tone++) { if (!present[tone]) continue; for (let q = 1; q < levels.length; q++) strip(tone, levels[q - 1], levels[q]); } void sheet; } function t3EmitLathe(run, cam, f) { const geo = t3Geo(run.F, cam); const k = cam.k; const step = T3_TAU / run.fine; const sheets = ["", "", "", "", ""]; const lv = run.levels; const levelAt = (s, r, nr, na) => ({ s, r, nr, na, arcs: t3Arcs(nr, na, geo.view, geo.light) }); const same = (a, b) => { if (a.arcs.length !== b.arcs.length) return false; for (let i = 0; i < a.arcs.length; i++) if (a.arcs[i].tone !== b.arcs[i].tone || Boolean(a.arcs[i].full) !== Boolean(b.arcs[i].full)) return false; return true; }; const reach = (a, b) => Math.hypot((b.s - a.s) * Math.hypot(geo.SA[0], geo.SA[1]), (b.r - a.r) * k) + Math.abs(b.r - a.r) * k; const levels = []; const wrap = (x, ref) => x + T3_TAU * Math.round((ref - x) / T3_TAU); const close = (a, mid, b) => { const r = Math.max(a.r, b.r) * k; for (let i = 0; i < mid.arcs.length; i++) { const m = mid.arcs[i]; if (m.full) continue; for (const key of ["from", "to"]) { const x = m[key]; const guess = (wrap(a.arcs[i][key], x) + wrap(b.arcs[i][key], x)) / 2; if (Math.abs(guess - x) * r > 0.25) return false; } } return true; }; const refine = (a, b, depth) => { if (depth >= 8 || reach(a, b) < 0.1) { levels.push(b); return; } const m = t3LatheMid(a, b); const mid = levelAt(m.s, m.r, m.nr, m.na); if (same(a, b) && same(a, mid) && close(a, mid, b)) { levels.push(b); return; } refine(a, mid, depth + 1); refine(mid, b, depth + 1); }; let previous = levelAt(lv[0], lv[1], lv[2], lv[3]); levels.push(previous); for (let i = 4; i < lv.length; i += 4) { const next = levelAt(lv[i], lv[i + 1], lv[i + 2], lv[i + 3]); refine(previous, next, 0); previous = next; } t3LatheSheets(levels, geo, step, sheets); for (let t = 0; t < 5; t++) f.d[run.role[`s${t}`]] = sheets[t]; const pts = t3LatheOutline(run, geo, cam); const outline = t3PathOf(pts, true); f.d[run.role.fill] = outline; f.d[run.role.edge] = outline; t3BoxOf(pts, f.box); t3HullOut(f, pts); let crease = ""; const view = geo.view; const ends = [[0, lv[0], lv[1], -1, 4], [1, lv[lv.length - 4], lv[lv.length - 3], 1, lv.length - 8]]; for (const [end, s, r, sign, near] of ends) { const role = run.role[`k${end}`]; if (role === undefined) continue; const face = sign * geo.aV; if (!(face > 1e-9) || r <= 1e-9) { f.d[role] = ""; continue; } const n = run.capCount; const buf = t3Scratch("c", 2 * n); const table = t3Table(n); for (let j = 0; j < n; j++) { const c = table[2 * j]; const q = table[2 * j + 1]; buf[2 * j] = geo.O[0] + s * geo.SA[0] + r * (c * geo.SU[0] + q * geo.SV[0]); buf[2 * j + 1] = geo.O[1] + s * geo.SA[1] + r * (c * geo.SU[1] + q * geo.SV[1]); } f.d[role] = t3Path(buf, n, true); f.q[role] = t3Level(sign * geo.aL, T3_LB); const minor = 2 * r * k * Math.abs(face); f.w[role] = minor < 0.4 ? Math.round(minor * 100) / 100 : -1; if (run.role.crease === undefined) continue; const nr = lv[near + 2]; const na = lv[near + 3]; let lo; let hi; if (view.flat < 1e-9) { if (!(na * view.a > 1e-9)) continue; lo = 0; hi = T3_TAU; } else { const cc = -(na * view.a) / (nr * view.flat); if (cc >= 1) continue; if (cc <= -1) { lo = view.phi; hi = view.phi + T3_TAU; } else { const d = Math.acos(cc); lo = view.phi - d; hi = view.phi + d; } } const count = Math.min(400, Math.max(4, Math.ceil(((hi - lo) * r * k) / 1.5))); const arc = t3Scratch("c", 2 * count + 2); for (let j = 0; j <= count; j++) t3At(geo, s, r, lo + ((hi - lo) * j) / count, arc, 2 * j); crease += t3Path(arc, count + 1, false); } if (run.role.crease !== undefined) f.d[run.role.crease] = crease; if (run.role.inner !== undefined) { let inner = ""; for (const seam of run.seams) inner += t3SeamArc(geo, seam.s, seam.r, seam.slope, k); f.d[run.role.inner] = inner; } if (run.ribs) { const lines = t3State.detail < 0.01 ? t3None : t3RibLines(geo, run.F, cam, run.ribs, 0, 0); f.levels = t3Faded(f, run, "r", lines, lines.d.length); } } const T3_SHINE = (() => { const l = Math.hypot(-0.55, -0.835); return [-0.55 / l, -0.835 / l]; })(); function t3RouteOf(flat) { const n = flat.length / 3; const along = new Float64Array(n); for (let i = 1; i < n; i++) along[i] = along[i - 1] + Math.hypot(flat[3 * i] - flat[3 * i - 3], flat[3 * i + 1] - flat[3 * i - 2], flat[3 * i + 2] - flat[3 * i - 1]); return { flat, n, along, total: along[n - 1] }; } function t3PointAlong(route, at) { const { flat, n, along } = route; if (at <= 0) return [flat[0], flat[1], flat[2]]; if (at >= along[n - 1]) return [flat[3 * n - 3], flat[3 * n - 2], flat[3 * n - 1]]; let lo = 0; let hi = n - 1; while (hi - lo > 1) { const mid = (lo + hi) >> 1; if (along[mid] <= at) lo = mid; else hi = mid; } const span = along[hi] - along[lo] || 1; const t = (at - along[lo]) / span; return [flat[3 * lo] + (flat[3 * hi] - flat[3 * lo]) * t, flat[3 * lo + 1] + (flat[3 * hi + 1] - flat[3 * lo + 1]) * t, flat[3 * lo + 2] + (flat[3 * hi + 2] - flat[3 * lo + 2]) * t]; } function t3Cut(route, from, to) { const { flat, n, along } = route; const out = []; for (let i = 1; i < n; i++) { const start = along[i - 1]; const end = along[i]; if (end >= from && start <= to) { const length = end - start || 1; const t0 = Math.max(0, (from - start) / length); const t1 = Math.min(1, (to - start) / length); const at = (t) => [flat[3 * i - 3] + (flat[3 * i] - flat[3 * i - 3]) * t, flat[3 * i - 2] + (flat[3 * i + 1] - flat[3 * i - 2]) * t, flat[3 * i - 1] + (flat[3 * i + 2] - flat[3 * i - 1]) * t]; if (!out.length) out.push(at(t0)); out.push(at(t1)); } } return out; } function t3TangentAlong(route, at) { const a = t3PointAlong(route, Math.max(0, at - 0.6)); const b = t3PointAlong(route, Math.min(route.total, at + 0.6)); return t3Unit([b[0] - a[0], b[1] - a[1], b[2] - a[2]]); } const t3RouteCache = new WeakMap(); function t3PrepareTube(run, part, data) { const spec = part.spec; const source = data.routes[spec.route]; let route = t3RouteCache.get(source); if (!route) { route = t3RouteOf(source.points); t3RouteCache.set(source, route); } const total = route.total; const r = source.r; const step = Math.max(0.05, Math.min(source.spacing, total / 4) * 0.5); const lead = 0.5; run.r = r; run.wide = Boolean(source.wide); run.hue = source.hue ?? null; const partOf = (start, end) => { const points = t3Cut(route, Math.max(0, start), Math.min(total, end)); let walked = Math.max(0, start); const count = points.length; const P3 = new Float64Array(3 * count); const D3 = new Float64Array(3 * count); for (let i = 0; i < count; i++) { if (i > 0) walked += Math.hypot(points[i][0] - points[i - 1][0], points[i][1] - points[i - 1][1], points[i][2] - points[i - 1][2]); const a = t3PointAlong(route, Math.max(0, Math.min(total, walked - step))); const b = t3PointAlong(route, Math.max(0, Math.min(total, walked + step))); for (let c = 0; c < 3; c++) { P3[3 * i + c] = points[i][c]; D3[3 * i + c] = b[c] - a[c]; } } return { count, P3, D3 }; }; const openStart = spec.open0 && spec.from > 0; const openEnd = spec.open1 && spec.to < total; run.body = partOf(openStart ? spec.from - lead : spec.from, openEnd ? spec.to + lead : spec.to); run.stripe = openStart || openEnd ? partOf(openStart ? spec.from - 2 * lead : spec.from, openEnd ? spec.to + 2 * lead : spec.to) : run.body; const ends = [!spec.open0 && (spec.from <= 1e-6 || spec.gap0), !spec.open1 && (spec.to >= total - 1e-6 || spec.gap1)]; run.capped = [ends[0] && (spec.gap0 || spec.caps[0]), ends[1] && (spec.gap1 || spec.caps[1])]; const P3 = run.body.P3; const last = run.body.count - 1; const vec = (i, j) => t3Unit([P3[3 * i] - P3[3 * j], P3[3 * i + 1] - P3[3 * j + 1], P3[3 * i + 2] - P3[3 * j + 2]]); run.tips = [vec(1, 0), vec(last, last - 1)]; run.rings = null; if (source.rings) { const { pitch = 3, twist = 0, cross = false, fade = [0.1, 0.55], phase = 0, steps = 16 } = source.rings; const lo = openStart ? spec.from - 2 * lead : spec.from; const hi = openEnd ? spec.to + 2 * lead : spec.to; const margin = twist ? 0 : Math.min(pitch * 0.35, (hi - lo) / 2); const from = Math.max(lo, !openStart ? lo + margin : lo); const to = Math.min(hi, !openEnd ? hi - margin : hi); const segments = []; if (to > from) { const hands = twist ? (cross ? [1, -1] : [1]) : [0]; const reach = Math.abs(twist) * r; const first = Math.ceil((from - reach - phase) / pitch); const lastIndex = Math.floor((to + reach - phase) / pitch); const surface = (t, theta) => { const at = Math.max(0, Math.min(total, t)); return { c: t3PointAlong(route, at), T: t3TangentAlong(route, at), theta }; }; for (let index = first; index <= lastIndex; index++) { const station = phase + index * pitch; for (const hand of hands) { const samples = []; for (let k = 0; k <= steps; k++) { const theta = (k / steps) * Math.PI; const t = station + hand * twist * r * (k / steps - 0.5); samples.push({ t, theta }); } for (let k = 0; k < steps; k++) { let a = samples[k]; let b = samples[k + 1]; if ((a.t < from && b.t < from) || (a.t > to && b.t > to)) continue; const clipTo = (x, y, edge) => ({ t: edge, theta: x.theta + ((y.theta - x.theta) * (edge - x.t)) / (y.t - x.t || 1e-9) }); if (a.t < from) a = clipTo(a, b, from); if (b.t < from) b = clipTo(b, a, from); if (a.t > to) a = clipTo(a, b, to); if (b.t > to) b = clipTo(b, a, to); segments.push([surface(a.t, a.theta), surface(b.t, b.theta)]); } } } } run.rings = { segments, fade, tone: source.rings.tone ?? "lo" }; } } function t3Sides(part, cam, r) { const m = cam.m; const k = cam.k; const n = part.count; const out = { n, flat: new Float64Array(2 * n), left: new Float64Array(2 * n), right: new Float64Array(2 * n), shineA: new Float64Array(2 * n), shineB: new Float64Array(2 * n), shadeA: new Float64Array(2 * n), shadeB: new Float64Array(2 * n), frames: [] }; const half = r * k; for (let i = 0; i < n; i++) { const x = part.P3[3 * i]; const y = part.P3[3 * i + 1]; const z = part.P3[3 * i + 2]; const px = cam.ox + m[0] * x + m[1] * y + m[2] * z; const py = cam.oy + m[3] * x + m[4] * y + m[5] * z; const dx = m[0] * part.D3[3 * i] + m[1] * part.D3[3 * i + 1] + m[2] * part.D3[3 * i + 2]; const dy = m[3] * part.D3[3 * i] + m[4] * part.D3[3 * i + 1] + m[5] * part.D3[3 * i + 2]; const l = Math.hypot(dx, dy) || 1; const nx = -dy / l; const ny = dx / l; out.flat[2 * i] = px; out.flat[2 * i + 1] = py; out.frames.push({ p: [px, py], nx, ny, half }); out.left[2 * i] = px + nx * half; out.left[2 * i + 1] = py + ny * half; out.right[2 * i] = px - nx * half; out.right[2 * i + 1] = py - ny * half; const toward = nx * T3_SHINE[0] + ny * T3_SHINE[1]; const shineAt = half * 0.44 * toward; const shineHalf = half * 0.24; out.shineA[2 * i] = px + nx * (shineAt + shineHalf); out.shineA[2 * i + 1] = py + ny * (shineAt + shineHalf); out.shineB[2 * i] = px + nx * (shineAt - shineHalf); out.shineB[2 * i + 1] = py + ny * (shineAt - shineHalf); const shadeAt = -half * 0.62 * toward; const shadeHalf = half * 0.2; out.shadeA[2 * i] = px + nx * (shadeAt + shadeHalf); out.shadeA[2 * i + 1] = py + ny * (shadeAt + shadeHalf); out.shadeB[2 * i] = px + nx * (shadeAt - shadeHalf); out.shadeB[2 * i + 1] = py + ny * (shadeAt - shadeHalf); } return out; } const t3Pts = (buf, n) => { const out = new Array(n); for (let i = 0; i < n; i++) out[i] = [buf[2 * i], buf[2 * i + 1]]; return out; }; function t3TubeCap(cam, centre, tangent, radius, inward) { const V = cam.V; const m = cam.m; let n1 = [tangent[1] * V[2] - tangent[2] * V[1], tangent[2] * V[0] - tangent[0] * V[2], tangent[0] * V[1] - tangent[1] * V[0]]; if (Math.hypot(n1[0], n1[1], n1[2]) < 1e-6) { const helper = Math.abs(tangent[2]) > 0.9 ? [1, 0, 0] : [0, 0, 1]; n1 = [helper[1] * tangent[2] - helper[2] * tangent[1], helper[2] * tangent[0] - helper[0] * tangent[2], helper[0] * tangent[1] - helper[1] * tangent[0]]; } n1 = t3Unit(n1); const n2 = t3Unit([tangent[1] * n1[2] - tangent[2] * n1[1], tangent[2] * n1[0] - tangent[0] * n1[2], tangent[0] * n1[1] - tangent[1] * n1[0]]); const steps = 28; const proj = (p) => [cam.ox + m[0] * p[0] + m[1] * p[1] + m[2] * p[2], cam.oy + m[3] * p[0] + m[4] * p[1] + m[5] * p[2]]; const ring = new Array(steps); for (let index = 0; index < steps; index++) { const angle = (index / steps) * T3_TAU; const cs = radius * Math.cos(angle); const sn = radius * Math.sin(angle); ring[index] = proj([centre[0] + n1[0] * cs + n2[0] * sn, centre[1] + n1[1] * cs + n2[1] * sn, centre[2] + n1[2] * cs + n2[2] * sn]); } const C = proj(centre); const probe = proj([centre[0] + n2[0], centre[1] + n2[1], centre[2] + n2[2]]); const along = (probe[0] - C[0]) * inward[0] + (probe[1] - C[1]) * inward[1]; const from = along > 0 ? steps / 2 : 0; const half = (start) => Array.from({ length: steps / 2 + 1 }, (_, index) => ring[(start + index) % steps]); return { ring, arc: half(from), inner: half((from + steps / 2) % steps) }; } function t3StripeEnd(half, centre, nx, ny, from, to) { const offset = (p) => (p[0] - centre[0]) * nx + (p[1] - centre[1]) * ny; const values = half.map(offset); const lo = Math.min(from, to); const hi = Math.max(from, to); const points = []; const cross = (k, level) => { const t = (level - values[k]) / (values[k + 1] - values[k] || 1e-9); return [half[k][0] + (half[k + 1][0] - half[k][0]) * t, half[k][1] + (half[k + 1][1] - half[k][1]) * t]; }; for (let k = 0; k < half.length; k++) { if (values[k] >= lo && values[k] <= hi) points.push(half[k]); if (k < half.length - 1) for (const level of [lo, hi]) if ((values[k] - level) * (values[k + 1] - level) < 0) points.push(cross(k, level)); } return points .map((p) => [p, offset(p)]) .sort((x, y) => (from > to ? y[1] - x[1] : x[1] - y[1])) .map(([p]) => p); } function t3EmitTube(run, cam, f) { const r = run.r; const sides = t3Sides(run.body, cam, r); const bands = run.stripe === run.body ? sides : t3Sides(run.stripe, cam, r); const n = sides.n; const left = t3Pts(sides.left, n); const right = t3Pts(sides.right, n); const loop = left.concat(right.slice().reverse()); let area = 0; for (let i = 0; i < loop.length; i++) { const a = loop[i]; const b = loop[(i + 1) % loop.length]; area += a[0] * b[1] - b[0] * a[1]; } const sign = Math.sign(area); let body = t3PathOf(loop, true); let edges = t3PathOf(left, false) + t3PathOf(right, false); const tips = [null, null]; const last = n - 1; const quads = []; for (let i = 0; i < last; i++) quads.push([left[i], left[i + 1], right[i + 1], right[i]]); for (const side of [0, 1]) { if (!run.capped[side]) continue; const at = side ? last : 0; const other = side ? last - 1 : 1; const tangent = run.tips[side]; const outward = side ? tangent : [-tangent[0], -tangent[1], -tangent[2]]; const inward = [sides.flat[2 * other] - sides.flat[2 * at], sides.flat[2 * other + 1] - sides.flat[2 * at + 1]]; const centre = [run.body.P3[3 * at], run.body.P3[3 * at + 1], run.body.P3[3 * at + 2]]; const { ring, arc, inner } = t3TubeCap(cam, centre, tangent, r, inward); let ringArea = 0; for (let i = 0; i < ring.length; i++) { const a = ring[i]; const b = ring[(i + 1) % ring.length]; ringArea += a[0] * b[1] - b[0] * a[1]; } body += t3PathOf(Math.sign(ringArea) === sign ? ring : ring.slice().reverse(), true); edges += t3PathOf(arc, false); const V = cam.V; const open = outward[0] * V[0] + outward[1] * V[1] + outward[2] * V[2] > 0; if (open) edges += t3PathOf(inner, false); tips[side] = { half: open ? inner : arc, frame: sides.frames[at] }; quads.push(ring); } f.d[run.role.body] = body; f.d[run.role.edges] = edges; if (run.wide) { const toward = (frame) => frame.nx * T3_SHINE[0] + frame.ny * T3_SHINE[1]; const shineOffsets = (frame) => [frame.half * 0.44 * toward(frame) + frame.half * 0.24, frame.half * 0.44 * toward(frame) - frame.half * 0.24]; const shadeOffsets = (frame) => [-frame.half * 0.62 * toward(frame) + frame.half * 0.2, -frame.half * 0.62 * toward(frame) - frame.half * 0.2]; const bn = bands.n; const band = (A, B, offsets) => { const head = tips[1] ? t3StripeEnd(tips[1].half, tips[1].frame.p, tips[1].frame.nx, tips[1].frame.ny, offsets(tips[1].frame)[0], offsets(tips[1].frame)[1]) : []; const tail = tips[0] ? t3StripeEnd(tips[0].half, tips[0].frame.p, tips[0].frame.nx, tips[0].frame.ny, offsets(tips[0].frame)[1], offsets(tips[0].frame)[0]) : []; return t3PathOf([...t3Pts(A, bn), ...head, ...t3Pts(B, bn).reverse(), ...tail], true); }; f.d[run.role.shine] = band(bands.shineA, bands.shineB, shineOffsets); f.d[run.role.shade] = band(bands.shadeA, bands.shadeB, shadeOffsets); } if (run.rings) { const lines = t3Lines; lines.d.length = 0; lines.alpha.length = 0; lines.key.length = 0; lines.key.length = 0; const V = cam.V; const m = cam.m; const seg = new Float64Array(4); const surface = (sample, out, index) => { const T = sample.T; let n1 = [T[1] * V[2] - T[2] * V[1], T[2] * V[0] - T[0] * V[2], T[0] * V[1] - T[1] * V[0]]; const l1 = Math.hypot(n1[0], n1[1], n1[2]); if (l1 < 1e-6) return null; n1 = [n1[0] / l1, n1[1] / l1, n1[2] / l1]; let n2 = t3Unit([T[1] * n1[2] - T[2] * n1[1], T[2] * n1[0] - T[0] * n1[2], T[0] * n1[1] - T[1] * n1[0]]); if (n2[0] * V[0] + n2[1] * V[1] + n2[2] * V[2] < 0) n2 = [-n2[0], -n2[1], -n2[2]]; const cs = Math.cos(sample.theta); const sn = Math.sin(sample.theta); const normal = [n1[0] * cs + n2[0] * sn, n1[1] * cs + n2[1] * sn, n1[2] * cs + n2[2] * sn]; const p = [sample.c[0] + normal[0] * r, sample.c[1] + normal[1] * r, sample.c[2] + normal[2] * r]; out[index] = cam.ox + m[0] * p[0] + m[1] * p[1] + m[2] * p[2]; out[index + 1] = cam.oy + m[3] * p[0] + m[4] * p[1] + m[5] * p[2]; return normal[0] * V[0] + normal[1] * V[1] + normal[2] * V[2]; }; const fade = run.rings.fade; let index = -1; for (const [a, b] of run.rings.segments) { index++; const fa = surface(a, seg, 0); const fb = surface(b, seg, 2); if (fa === null || fb === null) continue; const alpha = fade ? t3Step(fade[0], fade[1], (fa + fb) / 2) : 1; if (alpha * T3_LEVELS < 0.5) continue; lines.d.push(t3Path(seg, 2, false)); lines.alpha.push(alpha); lines.key.push(index); } f.levels = t3Faded(f, run, "r", lines, lines.d.length); } const all = left.concat(right); t3BoxOf(all, f.box); for (const q of quads) for (const p of q) { if (p[0] < f.box[0]) f.box[0] = p[0]; if (p[1] < f.box[1]) f.box[1] = p[1]; if (p[0] > f.box[2]) f.box[2] = p[0]; if (p[1] > f.box[3]) f.box[3] = p[1]; } const list = f.quadList && f.quadList.length === quads.length ? f.quadList : (f.quadList = quads.map(() => [new Float64Array(0), 0])); for (let qi = 0; qi < quads.length; qi++) { const q = quads[qi]; let entry = list[qi]; if (entry[0].length < 2 * q.length) entry[0] = new Float64Array(2 * q.length); const buf = entry[0]; for (let i = 0; i < q.length; i++) { buf[2 * i] = q[i][0]; buf[2 * i + 1] = q[i][1]; } entry[1] = q.length; } f.quadN = quads.length; f.quads = f.quadList; f.hullN = 0; } function t3Emit(run, cam, out) { const f = out ?? t3Frame(run); if (t3Arena.on) { t3Arena.f = f; f.arenaN = 0; } if (run.kind === "prism") t3EmitPrism(run, cam, f); else if (run.kind === "round") t3EmitRound(run, cam, f); else if (run.kind === "ball") t3EmitBall(run, cam, f); else if (run.kind === "lathe") t3EmitLathe(run, cam, f); else if (run.kind === "tube") { t3EmitTube(run, cam, f); return f; } else if (run.kind === "fixed") { t3EmitFixed(run, cam, f); return f; } else if (run.kind === "item") { f.box[0] = run.itemBox[0]; f.box[1] = run.itemBox[1]; f.box[2] = run.itemBox[2]; f.box[3] = run.itemBox[3]; const count = run.itemHull.length / 2; const hull = t3Grow(f, "hull", 2 * Math.max(1, count), Float64Array); for (let i = 0; i < 2 * count; i++) hull[i] = run.itemHull[i]; f.hullN = count; return f; } t3EmitPlanes(run, cam, f); t3EmitDots(run, cam, f); return f; } function t3Overlap(a, b, pad) { return a[0] < b[2] - pad && b[0] < a[2] - pad && a[1] < b[3] - pad && b[1] < a[3] - pad; } function t3Apart(A, an, B, bn, pad) { for (let pass = 0; pass < 2; pass++) { const P = pass ? B : A; const pn = pass ? bn : an; for (let i = 0; i < pn; i++) { const j = (i + 1) % pn; const nx = P[2 * j + 1] - P[2 * i + 1]; const ny = P[2 * i] - P[2 * j]; const l = Math.hypot(nx, ny); if (l < 1e-9) continue; let a0 = Infinity; let a1 = -Infinity; for (let q = 0; q < an; q++) { const v = (A[2 * q] * nx + A[2 * q + 1] * ny) / l; if (v < a0) a0 = v; if (v > a1) a1 = v; } let b0 = Infinity; let b1 = -Infinity; for (let q = 0; q < bn; q++) { const v = (B[2 * q] * nx + B[2 * q + 1] * ny) / l; if (v < b0) b0 = v; if (v > b1) b1 = v; } if (a1 < b0 + pad || b1 < a0 + pad) return true; } } return false; } function t3Disjoint(fa, fb, pad) { const qa = fa.quadN ? fa.quads : null; const qb = fb.quadN ? fb.quads : null; if (!qa && !qb) return t3Apart(fa.hull, fa.hullN, fb.hull, fb.hullN, pad); const listA = qa ? fa.quadList : [[fa.hull, fa.hullN]]; const listB = qb ? fb.quadList : [[fb.hull, fb.hullN]]; for (const [A, an] of listA) for (const [B, bn] of listB) if (!t3Apart(A, an, B, bn, pad)) return false; return true; } function t3Support(cloud, R, t, dir, world) { let best = -Infinity; let at = 0; const n = cloud.length / 3; const dx = R[0] * dir[0] + R[3] * dir[1] + R[6] * dir[2]; const dy = R[1] * dir[0] + R[4] * dir[1] + R[7] * dir[2]; const dz = R[2] * dir[0] + R[5] * dir[1] + R[8] * dir[2]; for (let i = 0; i < n; i++) { const v = cloud[3 * i] * dx + cloud[3 * i + 1] * dy + cloud[3 * i + 2] * dz; if (v > best) { best = v; at = i; } } const x = cloud[3 * at]; const y = cloud[3 * at + 1]; const z = cloud[3 * at + 2]; world[0] = R[0] * x + R[1] * y + R[2] * z + t[0]; world[1] = R[3] * x + R[4] * y + R[5] * z + t[1]; world[2] = R[6] * x + R[7] * y + R[8] * z + t[2]; return best + dir[0] * t[0] + dir[1] * t[1] + dir[2] * t[2]; } function t3Gjk(cloudA, camA, cloudB, camB, warm, rounds = 8, hints = null) { let w = warm && (warm[0] || warm[1] || warm[2]) ? [warm[0], warm[1], warm[2]] : null; const a = [0, 0, 0]; const b = [0, 0, 0]; if (!w) { t3Support(cloudA, camA.R, camA.t, [1, 0, 0], a); t3Support(cloudB, camB.R, camB.t, [-1, 0, 0], b); w = [b[0] - a[0], b[1] - a[1], b[2] - a[2]]; } let margin = -Infinity; let best = w; const step = (limit) => { for (let it = 0; it < limit; it++) { const l = Math.hypot(w[0], w[1], w[2]); if (l < 1e-12) break; const n = [w[0] / l, w[1] / l, w[2] / l]; const hiA = t3Support(cloudA, camA.R, camA.t, n, a); const loB = -t3Support(cloudB, camB.R, camB.t, [-n[0], -n[1], -n[2]], b); const value = loB - hiA; if (value > margin) { margin = value; best = n; } const s = [b[0] - a[0], b[1] - a[1], b[2] - a[2]]; const ds = [w[0] - s[0], w[1] - s[1], w[2] - s[2]]; const dd = ds[0] * ds[0] + ds[1] * ds[1] + ds[2] * ds[2]; if (dd < 1e-14) break; const tt = Math.max(0, Math.min(1, (w[0] * ds[0] + w[1] * ds[1] + w[2] * ds[2]) / dd)); w = [w[0] - ds[0] * tt, w[1] - ds[1] * tt, w[2] - ds[2] * tt]; } }; step(rounds); if (margin < 0) step(24); if (margin < 0.05 && hints) { if (hints.centroids) { const [ca, cb] = hints.centroids; const wa = [camA.R[0] * ca[0] + camA.R[1] * ca[1] + camA.R[2] * ca[2] + camA.t[0], camA.R[3] * ca[0] + camA.R[4] * ca[1] + camA.R[5] * ca[2] + camA.t[1], camA.R[6] * ca[0] + camA.R[7] * ca[1] + camA.R[8] * ca[2] + camA.t[2]]; const wb = [camB.R[0] * cb[0] + camB.R[1] * cb[1] + camB.R[2] * cb[2] + camB.t[0], camB.R[3] * cb[0] + camB.R[4] * cb[1] + camB.R[5] * cb[2] + camB.t[1], camB.R[6] * cb[0] + camB.R[7] * cb[1] + camB.R[8] * cb[2] + camB.t[2]]; const n = t3Unit([wb[0] - wa[0], wb[1] - wa[1], wb[2] - wa[2]]); const hiA = t3Support(cloudA, camA.R, camA.t, n, a); const loB = -t3Support(cloudB, camB.R, camB.t, [-n[0], -n[1], -n[2]], b); if (loB - hiA > margin) { margin = loB - hiA; best = n; } } for (const [axis, cam] of hints) { const R = cam.R; const w0 = [R[0] * axis[0] + R[1] * axis[1] + R[2] * axis[2], R[3] * axis[0] + R[4] * axis[1] + R[5] * axis[2], R[6] * axis[0] + R[7] * axis[1] + R[8] * axis[2]]; for (const sign of [1, -1]) { const n = [w0[0] * sign, w0[1] * sign, w0[2] * sign]; const hiA = t3Support(cloudA, camA.R, camA.t, n, a); const loB = -t3Support(cloudB, camB.R, camB.t, [-n[0], -n[1], -n[2]], b); if (loB - hiA > margin) { margin = loB - hiA; best = n; } } } } const l = Math.hypot(best[0], best[1], best[2]) || 1; return { n: [best[0] / l, best[1] / l, best[2] / l], margin }; } function t3Order(layer, frames, cams, previous, stats, scene) { const parts = layer.parts; const count = parts.length; const local = layer.local; const rank = layer._rank ?? (layer._rank = new Int32Array(count)); if (previous) for (let at = 0; at < count; at++) rank[local.get(previous[at])] = at; else for (let at = 0; at < count; at++) rank[local.get(layer.rest[at])] = at; let groupEdges = 0; if (layer.groupPairs) for (const gp of layer.groupPairs) groupEdges += gp.a.length * gp.b.length; const maxEdges = layer.planar.length / 5 + (layer.framed ? layer.framed.length / 6 : 0) + groupEdges + layer.dynamic.length / 2 + (layer.free ? layer.free.length * count : 0) + 1; const head = layer._head ?? (layer._head = new Int32Array(count)); const next = layer._next && layer._next.length >= maxEdges ? layer._next : (layer._next = new Int32Array(maxEdges)); const to = layer._to && layer._to.length >= maxEdges ? layer._to : (layer._to = new Int32Array(maxEdges)); const live = layer._live && layer._live.length >= maxEdges ? layer._live : (layer._live = new Uint8Array(maxEdges)); const indeg = layer._indeg ?? (layer._indeg = new Int32Array(count)); head.fill(-1); indeg.fill(0); let edges = 0; const addEdge = (back, front) => { to[edges] = front; next[edges] = head[back]; live[edges] = 1; head[back] = edges; indeg[front]++; edges++; }; const runs = scene ? scene.runs : null; const groupOf = (global) => (runs ? runs[global].group : -1); const planar = layer.planar; for (let p = 0; p < planar.length; p += 5) { const i = planar[p]; const j = planar[p + 1]; const fi = frames[i]; const fj = frames[j]; if (!t3Overlap(fi.box, fj.box, -0.05)) continue; const g = groupOf(i); const V = g >= 0 ? cams[g].V : cams.world.V; const s = planar[p + 2] * V[0] + planar[p + 3] * V[1] + planar[p + 4] * V[2]; if (s < 1e-7 && s > -1e-7) continue; const li = local.get(i); const lj = local.get(j); if (s > 0) addEdge(li, lj); else addEdge(lj, li); } const framed = layer.framed; if (framed) for (let p = 0; p < framed.length; p += 6) { const i = framed[p]; const j = framed[p + 1]; if (!t3Overlap(frames[i].box, frames[j].box, -0.05)) continue; const g = framed[p + 2]; const V = g >= 0 ? cams[g].V : cams.world.V; const s = framed[p + 3] * V[0] + framed[p + 4] * V[1] + framed[p + 5] * V[2]; if (s < 1e-7 && s > -1e-7) continue; const li = local.get(i); const lj = local.get(j); if (s > 0) addEdge(li, lj); else addEdge(lj, li); } if (layer.groupPairs) for (const gp of layer.groupPairs) { const union = (list, box) => { box[0] = Infinity; box[1] = Infinity; box[2] = -Infinity; box[3] = -Infinity; for (let q = 0; q < list.length; q++) { const b = frames[list[q]].box; if (b[0] < box[0]) box[0] = b[0]; if (b[1] < box[1]) box[1] = b[1]; if (b[2] > box[2]) box[2] = b[2]; if (b[3] > box[3]) box[3] = b[3]; } }; union(gp.a, gp.boxA); union(gp.b, gp.boxB); if (!t3Overlap(gp.boxA, gp.boxB, -0.05)) continue; const V = gp.frame >= 0 ? cams[gp.frame].V : cams.world.V; const s = gp.n[0] * V[0] + gp.n[1] * V[1] + gp.n[2] * V[2]; if (s < 1e-7 && s > -1e-7) continue; for (let x = 0; x < gp.a.length; x++) { const i = gp.a[x]; const bi = frames[i].box; if (!t3Overlap(bi, gp.boxB, -0.05)) continue; for (let y = 0; y < gp.b.length; y++) { const j = gp.b[y]; if (!t3Overlap(bi, frames[j].box, -0.05)) continue; if (s > 0) addEdge(local.get(i), local.get(j)); else addEdge(local.get(j), local.get(i)); } } } const dynamic = layer.dynamic; let unseparated = 0; let solved = 0; const rel = layer.rel ?? (layer.rel = new Map()); const cross = (i, j) => { const ri = runs[i]; const rj = runs[j]; const ci = ri.group >= 0 ? cams[ri.group] : cams.world; const cj = rj.group >= 0 ? cams[rj.group] : cams.world; const A = ci.R; const B = cj.R; const dx = cj.t[0] - ci.t[0]; const dy = cj.t[1] - ci.t[1]; const dz = cj.t[2] - ci.t[2]; const key = i * 65536 + j; let memo = rel.get(key); if (!memo) { memo = { pose: new Float64Array(12), n: [0, 0, 0], margin: -Infinity, fresh: false, vi: -1, vj: -1 }; rel.set(key, memo); } const pose = memo.pose; const vi = ri.version ?? 0; const vj = rj.version ?? 0; let same = memo.fresh && memo.vi === vi && memo.vj === vj; memo.vi = vi; memo.vj = vj; let at = 0; for (let c = 0; c < 3; c++) for (let r = 0; r < 3; r++) { const v = A[c] * B[r] + A[3 + c] * B[3 + r] + A[6 + c] * B[6 + r]; if (same && Math.abs(pose[at] - v) > 1e-12) same = false; pose[at++] = v; } for (let c = 0; c < 3; c++) { const v = A[c] * dx + A[3 + c] * dy + A[6 + c] * dz; if (same && Math.abs(pose[at] - v) > 1e-9) same = false; pose[at++] = v; } let nx; let ny; let nz; if (same) { const n = memo.n; nx = A[0] * n[0] + A[1] * n[1] + A[2] * n[2]; ny = A[3] * n[0] + A[4] * n[1] + A[5] * n[2]; nz = A[6] * n[0] + A[7] * n[1] + A[8] * n[2]; } else { const n = memo.n; const warm = memo.fresh ? [A[0] * n[0] + A[1] * n[1] + A[2] * n[2], A[3] * n[0] + A[4] * n[1] + A[5] * n[2], A[6] * n[0] + A[7] * n[1] + A[8] * n[2]] : null; const hints = [...ri.axes.map((axis) => [axis, ci]), ...rj.axes.map((axis) => [axis, cj]), [[0, 0, 1], cams.world]]; hints.centroids = [ri.centroid, rj.centroid]; const hit = t3Gjk(ri.cloud, ci, rj.cloud, cj, warm, 8, hints); solved++; nx = hit.n[0]; ny = hit.n[1]; nz = hit.n[2]; n[0] = A[0] * nx + A[3] * ny + A[6] * nz; n[1] = A[1] * nx + A[4] * ny + A[7] * nz; n[2] = A[2] * nx + A[5] * ny + A[8] * nz; memo.margin = hit.margin; memo.fresh = true; } if (memo.margin < -0.08) unseparated++; const V = cams.world.V; return nx * V[0] + ny * V[1] + nz * V[2]; }; for (let p = 0; p < dynamic.length; p += 2) { const i = dynamic[p]; const j = dynamic[p + 1]; if (!t3Overlap(frames[i].box, frames[j].box, -0.05)) continue; const s = cross(i, j); if (s < 1e-7 && s > -1e-7) continue; const li = local.get(i); const lj = local.get(j); if (s > 0) addEdge(li, lj); else addEdge(lj, li); } const loose = layer.free; if (loose && loose.length) { const isLoose = layer.looseAt; for (let a = 0; a < loose.length; a++) { const i = loose[a]; const gi = runs[i].group; const bi = frames[i].box; for (let b = 0; b < count; b++) { const j = parts[b]; if (j === i || runs[j].group === gi) continue; if (isLoose[b] && j < i) continue; if (!t3Overlap(bi, frames[j].box, -0.05)) continue; const s = cross(i, j); if (s < 1e-7 && s > -1e-7) continue; const li = local.get(i); if (s > 0) addEdge(li, b); else addEdge(b, li); } } } const heap = layer._heap ?? (layer._heap = new Int32Array(count)); const done = layer._done ?? (layer._done = new Uint8Array(count)); done.fill(0); let size = 0; const less = (a, b) => rank[a] < rank[b]; const push = (v) => { let i = size++; heap[i] = v; while (i > 0) { const p = (i - 1) >> 1; if (!less(heap[i], heap[p])) break; const tmp = heap[i]; heap[i] = heap[p]; heap[p] = tmp; i = p; } }; const pop = () => { const top = heap[0]; heap[0] = heap[--size]; let i = 0; for (;;) { const l = 2 * i + 1; const r = l + 1; let s = i; if (l < size && less(heap[l], heap[s])) s = l; if (r < size && less(heap[r], heap[s])) s = r; if (s === i) break; const tmp = heap[i]; heap[i] = heap[s]; heap[s] = tmp; i = s; } return top; }; for (let v = 0; v < count; v++) if (indeg[v] === 0) push(v); const out = new Int32Array(count); let placed = 0; let forced = 0; let refined = 0; let tested = false; const mark = layer._mark && layer._mark.length >= count ? layer._mark : (layer._mark = new Uint8Array(count)); const via = layer._via && layer._via.length >= count ? layer._via : (layer._via = new Int32Array(count)); const iter = layer._iter && layer._iter.length >= count ? layer._iter : (layer._iter = new Int32Array(count)); const stack = layer._stack && layer._stack.length >= count ? layer._stack : (layer._stack = new Int32Array(count)); const checked = layer._checked && layer._checked.length >= maxEdges ? layer._checked : (layer._checked = new Uint8Array(maxEdges)); checked.fill(0, 0, edges); const breakCycle = () => { mark.fill(0, 0, count); for (let root = 0; root < count; root++) { if (done[root] || mark[root]) continue; let top = 0; stack[top++] = root; mark[root] = 1; iter[root] = head[root]; while (top) { const v = stack[top - 1]; let e = iter[v]; while (e >= 0 && (!live[e] || done[to[e]] || mark[to[e]] === 2)) e = next[e]; if (e < 0) { mark[v] = 2; top--; continue; } iter[v] = next[e]; const w = to[e]; if (mark[w] === 1) { let dropped = false; let x = v; let edge = e; for (;;) { if (!checked[edge]) { checked[edge] = 1; const a = x; const b = to[edge]; if (t3Disjoint(frames[parts[a]], frames[parts[b]], 0.1)) { live[edge] = 0; indeg[b]--; refined++; dropped = true; if (indeg[b] === 0) push(b); } } if (x === w) break; edge = via[x]; let from = -1; for (let s = 0; s < top; s++) if (stack[s] === x) from = s > 0 ? stack[s - 1] : -1; if (from < 0) break; x = from; } return dropped ? 1 : 0; } via[w] = e; mark[w] = 1; iter[w] = head[w]; stack[top++] = w; } } return -1; }; while (placed < count) { if (size === 0) { let broken = 0; for (let tries = 0; tries < 64 && size === 0; tries++) { const result = breakCycle(); if (result <= 0) break; broken++; } if (size) continue; void broken; void tested; let pick = -1; for (let v = 0; v < count; v++) if (!done[v] && (pick < 0 || indeg[v] < indeg[pick] || (indeg[v] === indeg[pick] && rank[v] < rank[pick]))) pick = v; forced++; indeg[pick] = 0; push(pick); } const v = pop(); if (done[v]) continue; done[v] = 1; out[placed++] = parts[v]; for (let e = head[v]; e >= 0; e = next[e]) { if (!live[e]) continue; const w = to[e]; if (done[w]) continue; if (--indeg[w] === 0) push(w); } } if (stats) { stats.edges = (stats.edges ?? 0) + edges; stats.forced = (stats.forced ?? 0) + forced; stats.refined = (stats.refined ?? 0) + refined; stats.unseparated = (stats.unseparated ?? 0) + unseparated; stats.solved = (stats.solved ?? 0) + solved; } return out; } function t3Keep(values) { const n = values.length; const tails = []; const tailAt = []; const back = new Int32Array(n).fill(-1); for (let i = 0; i < n; i++) { let lo = 0; let hi = tails.length; while (lo < hi) { const mid = (lo + hi) >> 1; if (tails[mid] < values[i]) lo = mid + 1; else hi = mid; } tails[lo] = values[i]; tailAt[lo] = i; back[i] = lo > 0 ? tailAt[lo - 1] : -1; } const keep = new Uint8Array(n); let k = tails.length ? tailAt[tails.length - 1] : -1; while (k >= 0) { keep[k] = 1; k = back[k]; } return keep; } function t3Cams(scene, values) { const poses = t3Poses(scene.groups, values); const cams = poses.map((pose) => t3Camera(scene.P, pose)); cams.world = t3Camera(scene.P, null); cams.poses = poses; return cams; } function t3Mount(stage, data, { onHold = null, onFrame = null, follow = null, painter = "svg", numeric = painter === "canvas", canvasLayers = null, shift = false } = {}) { const canvasSet = painter === "canvas" ? new Set(canvasLayers ?? data.layers.map((layer) => layer.name)) : new Set(); const dom = canvasSet.size < data.layers.length; const domLayer = data.layers.map((layer) => !canvasSet.has(layer.name)); const layerEls = new Map([...stage.querySelectorAll("svg[data-live]")].map((svg) => [svg.dataset.live, svg])); const first = layerEls.values().next().value; const viewWidth = first && first.viewBox && first.viewBox.baseVal ? first.viewBox.baseVal.width : 0; const unitOf = () => { const width = first ? first.getBoundingClientRect().width : 0; return viewWidth && width ? Math.round((width / viewWidth) * 100) / 100 : 1; }; let unit = unitOf(); let scene = t3Prepare(data, { unit }); const runs = () => scene.runs; const count = data.parts.length; const nodes = new Array(count); const bound = new Array(count); const cache = new Array(count); const containers = new Map(); for (const [name, svg] of layerEls) containers.set(name, svg.querySelector(".iso-parts") ?? svg); for (const run of scene.runs) { const layer = data.layers[run.layer]; const container = containers.get(layer.name); const node = container ? container.querySelector(`[data-p="${run.index}"]`) : null; nodes[run.index] = node; const map = {}; if (node) { map.move = node; node.querySelectorAll("[data-r]").forEach((el) => (map[el.getAttribute("data-r")] = el)); } bound[run.index] = run.roles.map((role) => map[role.name] ?? null); cache[run.index] = run.roles.map(() => ({ d: null, q: null, b: null, f: null, w: null, t: null, o: null, cx: null, cy: null })); } const frames = scene.runs.map((run) => t3Frame(run)); const values = Object.fromEntries(data.groups.map((g) => [g.name, data.values ? data.values[g.name] ?? 0 : 0])); let lastPoses = null; let cams = null; const orders = scene.layers.map((layer) => Int32Array.from(layer.rest)); const changes = []; const stats = { ms: 0, writes: 0, moved: 0, forced: 0, refined: 0, unseparated: 0, emitted: 0 }; let writes = 0; const write = (index) => { const run = scene.runs[index]; const f = frames[index]; const els = bound[index]; const memo = cache[index]; for (let r = 0; r < run.roles.length; r++) { const el = els[r]; if (!el) continue; const role = run.roles[r]; const c = memo[r]; const type = role.type; if (type === "path" || type === "face" || type === "slot") { const d = f.d[r]; if (c.d !== d) { el.setAttribute("d", d); c.d = d; writes++; } if (type === "face") { const q = f.q[r]; if (c.q !== q) { const band = Math.min(3, Math.floor(q / T3_Q)); const mix = (q - T3_Q * band) / T3_Q; if (c.b !== band) { el.setAttribute("data-b", String(band)); c.b = band; writes++; } if (c.f !== mix) { if (mix) el.style.setProperty("--f", String(mix)); else el.style.removeProperty("--f"); c.f = mix; writes++; } c.q = q; } const w = f.w[r]; if (c.w !== w) { if (w < 0) el.style.removeProperty("stroke-width"); else el.style.strokeWidth = String(w); c.w = w; writes++; } } else if (type === "slot") { const o = f.o[r]; if (c.o !== o) { el.setAttribute("opacity", String(Math.round(o * 1000) / 1000)); c.o = o; writes++; } } } else if (type === "xform" || type === "self" || type === "plane") { const t = f.t[r]; if (c.t !== t) { if (t) el.setAttribute("transform", t); else el.removeAttribute("transform"); c.t = t; writes++; } if (type === "plane") { const o = f.o[r]; if (c.o !== o) { el.setAttribute("opacity", String(o)); c.o = o; writes++; } } } else if (type === "dot") { const x = Math.round(f.cx[r] * 100) / 100; const y = Math.round(f.cy[r] * 100) / 100; const o = f.o[r]; if (c.cx !== x) { el.setAttribute("cx", String(x)); c.cx = x; writes++; } if (c.cy !== y) { el.setAttribute("cy", String(y)); c.cy = y; writes++; } if (c.o !== o) { el.setAttribute("fill-opacity", String(o)); c.o = o; writes++; } } } }; const samePose = (a, b) => { if (!a || !b) return false; for (let i = 0; i < 9; i++) if (a.R[i] !== b.R[i]) return false; return a.t[0] === b.t[0] && a.t[1] === b.t[1] && a.t[2] === b.t[2]; }; let lastUnit = unit; let moved = 0; let snapshotNext = false; const emittedList = []; const shiftOn = Boolean(shift); const emitAt = shiftOn ? new Array(count) : null; const shiftable = (run) => run.kind === "prism" || run.kind === "round" || run.kind === "ball" || run.kind === "lathe"; const sameView = (base, cam) => { const m = cam.m; const b = base.m; for (let i = 0; i < 6; i++) if (m[i] !== b[i]) return false; return true; }; const keepEmit = (run, cam, f) => { let base = emitAt[run.index]; if (!base) base = emitAt[run.index] = { m: new Float64Array(6), ox: 0, oy: 0, box: new Float64Array(4), hull: new Float64Array(0), hullN: 0 }; base.m.set(cam.m); base.ox = cam.ox; base.oy = cam.oy; base.box.set(f.box); if (base.hull.length < 2 * f.hullN) base.hull = new Float64Array(2 * f.hullN); for (let i = 0; i < 2 * f.hullN; i++) base.hull[i] = f.hull[i]; base.hullN = f.hullN; }; const shiftFrame = (f, base, dx, dy) => { f.box[0] = base.box[0] + dx; f.box[1] = base.box[1] + dy; f.box[2] = base.box[2] + dx; f.box[3] = base.box[3] + dy; const hull = t3Grow(f, "hull", 2 * base.hullN, Float64Array); for (let i = 0; i < base.hullN; i++) { hull[2 * i] = base.hull[2 * i] + dx; hull[2 * i + 1] = base.hull[2 * i + 1] + dy; } f.hullN = base.hullN; f.sx = dx; f.sy = dy; }; const writeShift = (index, dx, dy) => { const node = nodes[index]; if (!node) return; const text = dx || dy ? `translate(${Math.round(dx * 100) / 100} ${Math.round(dy * 100) / 100})` : ""; if (node.__shift === text) return; if (text) node.setAttribute("transform", text); else node.removeAttribute("transform"); node.__shift = text; writes++; }; const render = () => { const t0 = performance.now(); writes = 0; moved = 0; stats.orderMs = 0; t3Arena.on = false; const snapshotting = snapshotNext; snapshotNext = false; cams = t3Cams(scene, values); t3State.detail = detail; const poses = cams.poses; const fresh = lastUnit !== unit || !lastPoses; let emitted = 0; emittedList.length = 0; let shifted = 0; for (const run of scene.runs) { const g = run.group; const changed = fresh || (g >= 0 ? !samePose(poses[g], lastPoses[g]) : false); if (!changed) continue; const domRun = snapshotting || domLayer[run.layer]; const cam = g >= 0 ? cams[g] : cams.world; const f = frames[run.index]; const base = shiftOn && !fresh && !snapshotting ? emitAt[run.index] : null; if (base && sameView(base, cam)) { const dx = cam.ox - base.ox; const dy = cam.oy - base.oy; shiftFrame(f, base, dx, dy); if (domRun) writeShift(run.index, dx, dy); emittedList.push(run.index); shifted++; continue; } t3Arena.on = Boolean(numeric) && !domRun; t3Emit(run, cam, f); if (f.sx || f.sy) { f.sx = 0; f.sy = 0; } if (shiftOn && shiftable(run)) keepEmit(run, cam, f); if (domRun) { write(run.index); if (shiftOn && shiftable(run)) writeShift(run.index, 0, 0); } emittedList.push(run.index); emitted++; } stats.shifted = shifted; lastPoses = poses; lastUnit = unit; t3Arena.on = false; t3Arena.f = null; const t1 = performance.now(); stats.emitMs = t1 - t0; const orderStats = { forced: 0, refined: 0, unseparated: 0, edges: 0 }; scene.layers.forEach((layer, index) => { const previous = orders[index]; const tOrder = performance.now(); const order = t3Order(layer, frames, cams, previous, orderStats, scene); stats.orderMs += performance.now() - tOrder; let same = true; for (let j = 0; j < order.length; j++) if (order[j] !== previous[j]) { same = false; break; } if (snapshotting && !domLayer[index]) { const container = containers.get(layer.name); if (container) for (const global of order) if (nodes[global]) container.appendChild(nodes[global]); orders[index] = order; return; } if (same) return; if (!domLayer[index]) { orders[index] = order; return; } const container = containers.get(layer.name); const position = new Map(); previous.forEach((global, at) => position.set(global, at)); const keep = t3Keep(Array.from(order, (global) => position.get(global))); let after = null; const shifted = []; for (let j = order.length - 1; j >= 0; j--) { const node = nodes[order[j]]; if (!keep[j] && node && container) { container.insertBefore(node, after); moved++; shifted.push(scene.runs[order[j]].name); } if (node) after = node; } orders[index] = order; if (shifted.length) changes.push({ layer: layer.name, moved: shifted }); }); stats.ms = performance.now() - t0; stats.writes = writes; stats.moved = moved; stats.forced = orderStats.forced; stats.refined = orderStats.refined; stats.unseparated = orderStats.unseparated; stats.emitted = emitted; t3State.detail = 1; if (onFrame) onFrame(controller); return { ...stats }; }; let detail = 1; const set = (next, options) => { if (options && options.detail !== undefined) { const value = Math.max(0, Math.min(1, options.detail)); if (value !== detail) { detail = value; lastPoses = null; } } if (typeof next === "number") { const g = data.groups.find((group) => group.kind === "turn"); if (g) values[g.name] = next; } else if (next) for (const [name, value] of Object.entries(next)) values[name] = value; if (follow) Object.assign(values, follow({ ...values })); return render(); }; const itemRuns = new Map(); let unwrap = new Map(); const angleAt = (clientX, clientY, z = 0, groupName) => { const svg = first; const box = svg.getBoundingClientRect(); const vb = svg.viewBox.baseVal; const x = vb.x + ((clientX - box.left) / box.width) * vb.width; const y = vb.y + ((clientY - box.top) / box.height) * vb.height; const c = t3Camera(scene.P, null); const p = (x - c.ox) / c.k; const q = (y - c.oy + z * c.ce * c.k) / (c.se * c.k); const wx = p * c.sa + q * c.ca; const wy = -p * c.ca + q * c.sa; const group = data.groups.find((g) => g.name === groupName) ?? data.groups.find((g) => g.kind === "turn"); const pivot = group ? group.origin : [0, 0, 0]; const raw = (Math.atan2(wy - pivot[1], wx - pivot[0]) * 180) / Math.PI; const key = group ? group.name : ""; const last = unwrap.get(key); let angle = raw; if (last !== undefined) angle = last + ((((raw - last) % 360) + 540) % 360) - 180; unwrap.set(key, angle); return angle; }; const observer = typeof ResizeObserver === "function" && first ? new ResizeObserver(() => { if (!dom || !first.isConnected) return; const next = unitOf(); if (Math.abs(next - unit) < 0.05) return; unit = next; scene = t3Prepare(data, { unit }); render(); }) : null; if (observer) observer.observe(first); if (follow) Object.assign(values, follow({ ...values })); const controller = { set, render, painter, emitted: () => emittedList, snapshot: () => { snapshotNext = true; lastPoses = null; render(); lastPoses = null; render(); return [...containers.values()]; }, numeric: Boolean(numeric) && canvasSet.size > 0, canvasLayers: [...canvasSet], setUnit: (next) => { if (Math.abs(next - unit) < 1e-6) return null; unit = next; scene = t3Prepare(data, { unit }); return render(); }, angleAt, get values() { return { ...values }; }, cover: (names, options) => t3Cover(controller, names, options), order: (name) => { const index = name === undefined ? 0 : scene.layers.findIndex((layer) => layer.name === name); return index < 0 ? [] : Array.from(orders[index], (global) => scene.runs[global].name); }, orderIndex: (name) => { const index = name === undefined ? 0 : scene.layers.findIndex((layer) => layer.name === name); return index < 0 ? null : orders[index]; }, events: () => changes.splice(0, changes.length), stats: () => ({ ...stats }), frames: () => frames, cams: () => cams, scene: () => scene, pose: (name) => t3PoseOf(scene, cams, name), detach: (name) => { const index = scene.groups.findIndex((g) => g.name === name); const pose = t3PoseOf(scene, cams, name); const o = scene.groups[index].origin; return { world: true, R: pose.R, t: [pose.t[0] - o[0], pose.t[1] - o[1], pose.t[2] - o[2]] }; }, anchor: (name, point, direction) => t3Anchor(scene, cams, name, point, direction), item: (name, { cloud, box, hull }) => { const run = itemRuns.get(name) ?? scene.runs.find((r) => r.name === name); if (!run) return null; itemRuns.set(name, run); run.cloud = cloud instanceof Float64Array ? cloud : Float64Array.from(cloud); run.version = (run.version ?? 0) + 1; run.itemBox.set(box); run.itemHull = hull instanceof Float64Array ? hull : Float64Array.from(hull); const n = run.cloud.length / 3; run.centroid = [0, 0, 0]; for (let i = 0; i < n; i++) for (let c = 0; c < 3; c++) run.centroid[c] += run.cloud[3 * i + c] / Math.max(1, n); t3Emit(run, null, frames[run.index]); return nodes[run.index]; }, node: (name) => { const run = scene.runs.find((r) => r.name === name); return run ? nodes[run.index] : null; }, destroy: () => { if (observer) observer.disconnect(); if (typeof window !== "undefined" && window.__isoTurn && window.__isoTurn.controller === controller) delete window.__isoTurn; }, }; const hold = () => { if (onHold) onHold(); }; if (typeof window !== "undefined") { window.__isoTurn = { controller, set(next) { hold(); return set(next); }, get() { hold(); return { ...values }; }, order(name) { hold(); return controller.order(name); }, events() { hold(); return controller.events(); }, stats() { hold(); return controller.stats(); }, layers() { hold(); return scene.layers.map((layer) => layer.name); }, }; } render(); return controller; } function t3Enclose(points, edges) { const hull = t3Hull(points); const n = hull.length; if (n < 3) return []; let area = 0; for (let i = 0; i < n; i++) area += hull[i][0] * hull[(i + 1) % n][1] - hull[(i + 1) % n][0] * hull[i][1]; const sign = area >= 0 ? 1 : -1; let lines = []; for (let i = 0; i < n; i++) { const p = hull[i]; const q = hull[(i + 1) % n]; const dx = q[0] - p[0]; const dy = q[1] - p[1]; const l = Math.hypot(dx, dy); if (l < 1e-9) continue; const nx = (sign * dy) / l; const ny = (-sign * dx) / l; lines.push([nx, ny, -(nx * p[0] + ny * p[1])]); } const meet = (A, B) => { const det = A[0] * B[1] - A[1] * B[0]; if (Math.abs(det) < 1e-12) return null; return [(-A[2] * B[1] + B[2] * A[1]) / det, (-A[0] * B[2] + B[0] * A[2]) / det]; }; while (lines.length > edges) { let best = -1; let cost = Infinity; const m = lines.length; for (let i = 0; i < m; i++) { const prev = lines[(i - 1 + m) % m]; const self = lines[i]; const next = lines[(i + 1) % m]; const turn = prev[0] * next[1] - prev[1] * next[0]; if (sign * turn <= 1e-9) continue; const apex = meet(prev, next); const a = meet(prev, self); const b = meet(self, next); if (!apex || !a || !b) continue; const added = Math.abs((a[0] - apex[0]) * (b[1] - apex[1]) - (a[1] - apex[1]) * (b[0] - apex[0])) / 2; if (added < cost) { cost = added; best = i; } } if (best < 0) break; lines.splice(best, 1); } return lines; } function t3Cover(controller, names, { polygons = 6, edges = 10, pad = 0.3, layers = null } = {}) { const uCoverEdge = new Float32Array(polygons * edges * 3); const uCoverCount = new Float32Array(polygons); const scene = controller.scene(); const frames = controller.frames(); const sets = Array.isArray(names) ? (names.length && Array.isArray(names[0]) ? names : [names]) : [[names]]; const candidates = []; const taken = new Set(); for (const list of sets) { const targets = scene.runs.filter((run) => list.includes(run.name)); if (!targets.length) continue; const box = [Infinity, Infinity, -Infinity, -Infinity]; for (const run of targets) { const b = frames[run.index].box; box[0] = Math.min(box[0], b[0]); box[1] = Math.min(box[1], b[1]); box[2] = Math.max(box[2], b[2]); box[3] = Math.max(box[3], b[3]); } const home = targets[0].layer; scene.layers.forEach((layer, index) => { const order = controller.orderIndex(layer.name); if (!order) return; let from = 0; if (index === home) { for (let at = 0; at < order.length; at++) if (targets.some((run) => run.index === order[at])) from = at + 1; } else if (!(layers && layers.includes(layer.name)) || index < home) return; for (let at = from; at < order.length; at++) { const run = scene.runs[order[at]]; if (list.includes(run.name) || taken.has(run.index)) continue; const f = frames[run.index]; const b = f.box; if (!(b[0] < box[2] + pad && box[0] < b[2] + pad && b[1] < box[3] + pad && box[1] < b[3] + pad)) continue; if (!targets.some((target) => !t3Disjoint(frames[target.index], f, -pad))) continue; taken.add(run.index); const share = (Math.min(b[2], box[2]) - Math.max(b[0], box[0])) * (Math.min(b[3], box[3]) - Math.max(b[1], box[1])); candidates.push({ run, f, share }); } }); } candidates.sort((a, b) => b.share - a.share); const parts = []; let slot = 0; for (const { run, f } of candidates) { if (slot >= polygons) break; const points = []; if (f.quadN) for (const [buf, count] of f.quadList) for (let i = 0; i < count; i++) points.push([buf[2 * i], buf[2 * i + 1]]); else for (let i = 0; i < f.hullN; i++) points.push([f.hull[2 * i], f.hull[2 * i + 1]]); const lines = t3Enclose(points, edges); if (!lines.length) continue; lines.forEach((line, e) => { const at = (slot * edges + e) * 3; uCoverEdge[at] = line[0]; uCoverEdge[at + 1] = line[1]; uCoverEdge[at + 2] = line[2] - pad; }); uCoverCount[slot] = lines.length; parts.push(run.name); slot++; } return { uCoverEdge, uCoverCount, dropped: Math.max(0, candidates.length - slot), parts }; } function t3PoseOf(scene, cams, name) { const index = scene.groups.findIndex((g) => g.name === name); const cam = index >= 0 && cams ? cams[index] : null; return { R: cam ? Array.from(cam.R) : Array.from(T3_EYE), t: cam ? [cam.t[0], cam.t[1], cam.t[2]] : [0, 0, 0] }; } function t3Anchor(scene, cams, name, point, direction) { const index = name === null || name === undefined ? -1 : scene.groups.findIndex((g) => g.name === name); const cam = index >= 0 ? cams[index] : cams.world; const o = index >= 0 ? scene.groups[index].origin : [0, 0, 0]; const R = cam.R; const q = [point[0] - o[0], point[1] - o[1], point[2] - o[2]]; const world = [R[0] * q[0] + R[1] * q[1] + R[2] * q[2] + cam.t[0], R[3] * q[0] + R[4] * q[1] + R[5] * q[2] + cam.t[1], R[6] * q[0] + R[7] * q[1] + R[8] * q[2] + cam.t[2]]; const w = cams.world; const screen = [w.ox + w.m[0] * world[0] + w.m[1] * world[1] + w.m[2] * world[2], w.oy + w.m[3] * world[0] + w.m[4] * world[1] + w.m[5] * world[2]]; const out = { world, screen }; if (direction) out.direction = [R[0] * direction[0] + R[1] * direction[1] + R[2] * direction[2], R[3] * direction[0] + R[4] * direction[1] + R[5] * direction[2], R[6] * direction[0] + R[7] * direction[1] + R[8] * direction[2]]; return out; } function t3FloorOf(scene, name, R = T3_EYE) { const index = scene.groups.findIndex((g) => g.name === name); let low = Infinity; for (const run of scene.runs) { if (run.group !== index) continue; const c = run.cloud; for (let i = 0; i < c.length; i += 3) { const z = R[6] * c[i] + R[7] * c[i + 1] + R[8] * c[i + 2]; if (z < low) low = z; } } return low; } function t3Compose(A, B) { return Array.from(t3Mul(A, B)); } function t3PoseUniforms(controller, group, prefix = "uGroup") { const scene = controller.scene(); const index = scene.groups.findIndex((g) => g.name === group); const cams = controller.cams(); const cam = index >= 0 && cams ? cams[index] : null; const R = cam ? cam.R : T3_EYE; const t = cam ? cam.t : [0, 0, 0]; const o = index >= 0 ? scene.groups[index].origin : [0, 0, 0]; const T = [t[0] - (R[0] * o[0] + R[1] * o[1] + R[2] * o[2]), t[1] - (R[3] * o[0] + R[4] * o[1] + R[5] * o[2]), t[2] - (R[6] * o[0] + R[7] * o[1] + R[8] * o[2])]; return { [`${prefix}X`]: [R[0], R[3], R[6]], [`${prefix}Y`]: [R[1], R[4], R[7]], [`${prefix}Z`]: [R[2], R[5], R[8]], [`${prefix}T`]: T }; } const T3_MAT_KEYS = ["paper", "face", "top", "shade-0", "shade-1", "shade-2", "shade-3", "lit-top", "lit-shade", "hi", "mid", "lo", "faint", "dot", "lit"]; const T3_MAT_TUBE = ["body", "shine", "shade", "edge"]; const T3_MATERIALS = { gold: { light: ["#d3bb88", "#e4d2a8", "#f1e5c6", "#a1844f", "#bc9f69", "#d3bb88", "#e4d2a8", "#f8efd8", "#dfcb9c", "#4b3920", "#86693f", "#a98c5b", "#c1a676", "#4a381f", "#2a1d0c", "#cdb07a", "#f3e8cc", "#8e7044", "#4b3920"], dark: ["#3f3521", "#4b3f28", "#5a4c31", "#2b2416", "#352c1b", "#3f3521", "#4b3f28", "#6a5a3b", "#4f4329", "#e6d2a3", "#8a7550", "#5e4e33", "#463a26", "#e6d2a3", "#fff3d6", "#4a3e27", "#8c7853", "#251f13", "#e6d2a3"], }, chrome: { light: ["#8e959c", "#c3c8cd", "#f7f8f9", "#a2a9b0", "#6f767d", "#454b52", "#cacfd4", "#ffffff", "#bcc2c8", "#202428", "#585e65", "#878d94", "#a9afb5", "#2a2e33", "#000000", "#8b9299", "#fbfcfd", "#3c4248", "#202428"], dark: ["#2a2e32", "#4a5056", "#6e757c", "#2e3236", "#24272b", "#1a1d20", "#4a5056", "#848b92", "#3a3f44", "#dfe3e7", "#7c838a", "#4d5359", "#363b40", "#e4e7ea", "#ffffff", "#34393e", "#9aa1a8", "#16191b", "#dfe3e7"], }, steel: { light: ["#b8bdc1", "#cdd1d4", "#e5e8ea", "#8f959b", "#a4aaaf", "#b8bdc1", "#cdd1d4", "#f1f3f4", "#c6cacd", "#2b2f33", "#6b7177", "#959ba1", "#aeb3b8", "#33373b", "#000000", "#b3b8bd", "#eef0f2", "#7d8389", "#2b2f33"], dark: ["#2c3034", "#34393d", "#3e4348", "#1f2225", "#25292c", "#2c3034", "#34393d", "#4a5056", "#33383c", "#d0d4d8", "#727980", "#4a5056", "#33383d", "#d8dce0", "#ffffff", "#30353a", "#5c6369", "#1a1d20", "#d0d4d8"], }, gunmetal: { light: ["#444a51", "#50575e", "#5f666e", "#2f3439", "#393f45", "#444a51", "#50575e", "#6d757d", "#4b5258", "#0f1113", "#1d2125", "#272b30", "#30353a", "#b0b5ba", "#000000", "#3d4349", "#6e757c", "#24282c", "#0f1113"], dark: ["#1d2125", "#22272b", "#2a2f34", "#15181b", "#191c20", "#1d2125", "#22272b", "#323840", "#20252a", "#a9b0b7", "#5c636a", "#3a4046", "#2a2f34", "#b8bec4", "#ffffff", "#1f2327", "#3a4046", "#111316", "#a9b0b7"], }, rubber: { light: ["#2a2b2e", "#34363a", "#46484d", "#1b1c1e", "#222326", "#2a2b2e", "#34363a", "#55575c", "#2f3134", "#070708", "#111214", "#18191b", "#1f2023", "#6a6c70", "#000000", "#27282b", "#5b5d62", "#121314", "#050506"], dark: ["#121213", "#161618", "#1c1c1f", "#0c0c0d", "#0f0f10", "#121213", "#161618", "#232327", "#141416", "#8c8e92", "#4a4c50", "#2c2d30", "#1f2023", "#9a9ca0", "#ffffff", "#131315", "#2b2c30", "#0a0a0b", "#8c8e92"], }, brass: { light: ["#bd9646", "#d2ad5b", "#e6c87e", "#8a6a2c", "#a47f38", "#bd9646", "#d2ad5b", "#f0d796", "#c8a252", "#3f2e0e", "#765a24", "#9a7a3c", "#b08c46", "#3a2a0c", "#1f1606", "#b8913f", "#efd795", "#7d5f26", "#3f2e0e"], dark: ["#3d3017", "#49391c", "#574523", "#2a210f", "#332813", "#3d3017", "#49391c", "#66522b", "#45371b", "#e9cf8a", "#8e7438", "#5e4c25", "#44371c", "#e9cf8a", "#fff0c8", "#433419", "#8a6f36", "#21190b", "#e9cf8a"], }, copper: { light: ["#b3633e", "#c9784f", "#df9670", "#7c4128", "#9a5132", "#b3633e", "#c9784f", "#eaa985", "#be6d46", "#3a1a0c", "#6e3820", "#925037", "#a65d3f", "#3a1a0c", "#1c0b04", "#ad5f3b", "#f0b896", "#6e3720", "#3a1a0c"], dark: ["#3d1f13", "#492617", "#582e1c", "#2a160d", "#331a10", "#3d1f13", "#492617", "#673722", "#432215", "#eaa985", "#8f5236", "#5f3624", "#45271a", "#eaa985", "#ffe0cc", "#42221a", "#8a4c31", "#1f0f08", "#eaa985"], }, }; function t3MaterialCss() { const rules = []; const block = (values) => T3_MAT_KEYS.map((key, i) => `--anatomy-${key}:${values[i]}`).concat(T3_MAT_TUBE.map((key, i) => `--mat-${key}:${values[T3_MAT_KEYS.length + i]}`)).join(";"); for (const [name, { light, dark }] of Object.entries(T3_MATERIALS)) { rules.push(`[data-mat="${name}"]{${block(dark)}}`); rules.push(`.iso[data-theme="light"] [data-mat="${name}"]{${block(light)}}`); rules.push(`.iso[data-theme="dark"] [data-mat="${name}"]{${block(dark)}}`); } rules.push("[data-mat] .tb-body{fill:var(--mat-body)}"); rules.push("[data-mat] .tb-shine{fill:var(--mat-shine)}"); rules.push("[data-mat] .tb-shade{fill:var(--mat-shade)}"); rules.push("[data-mat] .tb-edge{stroke:var(--mat-edge)}"); return rules.join("\n"); } function t3Css() { const rules = []; const ramp = "--tn-0:var(--anatomy-shade-0);--tn-1:var(--anatomy-shade-1);--tn-2:var(--anatomy-shade-2);--tn-3:var(--anatomy-shade-3);--tn-4:var(--anatomy-top)"; rules.push(`.iso-live{${ramp}}`); rules.push(`.iso-live [data-mat]{${ramp}}`); rules.push(".iso-live [data-lit]{--tn-0:var(--anatomy-lit-shade);--tn-1:var(--anatomy-lit-shade);--tn-2:var(--anatomy-lit-shade);--tn-3:var(--anatomy-lit-shade);--tn-4:var(--anatomy-lit-top)}"); const colour = "color-mix(in srgb,var(--ta) calc(100% - var(--f,0) * 100%),var(--tb))"; rules.push(`.iso-tn{stroke-width:.4;stroke-linejoin:round;--ta:var(--tn-0);--tb:var(--tn-1);fill:${colour};stroke:${colour}}`); for (let i = 1; i < 4; i++) rules.push(`.iso-tn[data-b="${i}"]{--ta:var(--tn-${i});--tb:var(--tn-${i + 1})}`); rules.push(t3MaterialCss()); for (const hue of ["red", "green", "blue"]) rules.push(`.tb[data-hue="${hue}"]{--tb-ink:var(--anatomy-${hue})}`); rules.push(".tb[data-hue] .tb-body{fill:color-mix(in srgb,var(--tb-ink) 54%,var(--anatomy-shade-2))}"); rules.push(".tb[data-hue] .tb-shine{fill:color-mix(in srgb,var(--tb-ink) 16%,var(--anatomy-top))}"); rules.push(".tb[data-hue] .tb-shade{fill:color-mix(in srgb,var(--tb-ink) 30%,var(--anatomy-hi))}"); rules.push(".tb[data-hue] .tb-edge{stroke:color-mix(in srgb,var(--tb-ink) 38%,var(--anatomy-hi))}"); rules.push(".tb[data-hue][data-mat] .tb-body{fill:color-mix(in srgb,var(--tb-ink) 62%,var(--mat-body))}"); rules.push(".tb[data-hue][data-mat] .tb-shine{fill:color-mix(in srgb,var(--tb-ink) 22%,var(--mat-shine))}"); rules.push(".tb[data-hue][data-mat] .tb-shade{fill:color-mix(in srgb,var(--tb-ink) 46%,var(--mat-shade))}"); rules.push(".tb[data-hue][data-mat] .tb-edge{stroke:color-mix(in srgb,var(--tb-ink) 30%,var(--mat-edge))}"); rules.push(".iso-turn{position:relative}"); rules.push(".iso-turn>.iso-layer{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}"); return rules.join("\n"); } export const TURN = { materials: Object.keys(T3_MATERIALS), materialCss: t3MaterialCss, rotation: (axis, degrees) => Array.from(t3Rot(axis, degrees)), quat: (q) => Array.from(t3Quat(q)), compose: t3Compose, floorOf: t3FloorOf, anchor: t3Anchor, camera: t3Camera, poses: t3PosesMap, prepare: t3Prepare, emit: t3Emit, order: t3Order, mount: t3Mount, cover: t3Cover, poseUniforms: t3PoseUniforms, css: t3Css, frame: t3Frame, cams: t3Cams, gjk: t3Gjk, hintsOf: (ri, ci, rj, cj, world) => Object.assign([...ri.axes.map((axis) => [axis, ci]), ...rj.axes.map((axis) => [axis, cj]), [[0, 0, 1], world]], { centroids: [ri.centroid, rj.centroid] }), disjoint: t3Disjoint, levels: T3_LEVELS, q: T3_Q, exact: (on) => { t3State.exact = Boolean(on); } }; export const TURN_CSS = t3Css();