/* ============================================================ qft/scene.js — assemble a QFT plate from params. Per-field origins (each lattice movable), multi-E8 clusters scattered around the canvas, and explicit cross-field LINKS (Feynman propagator symbols connecting a vertex in one field to a vertex in another — the "interactions" between fields). Returns: { grids: [...], links: [...], hash, plate, exposure, lab } Each grid: { id, propagator, vertices:[{x,y}], edges:[{a,b}] } Each link: { a:{x,y}, b:{x,y}, propagator: 'photon'|'scalar'|'gluon' } ============================================================ */ import { makeRng, cyrb53, pick } from '../rng.js'; import { buildCubic, buildSchlegel, buildE8, buildNautilus, buildRipples } from './topology.js'; import { distortVertices, applyWavepackets, applyVortices, applyStandingWaves } from './distortion.js'; const LABS = [ 'LATTICE QFT · LANL', 'BNL · ALGEBRAIC GEOMETRY GROUP', 'IHES · BURES-SUR-YVETTE', 'PERIMETER INSTITUTE · WATERLOO', 'INST. THEORETICAL PHYSICS · COPENHAGEN', ]; function rotateScale(verts, angle, scale) { const c = Math.cos(angle), s = Math.sin(angle); for (const v of verts) { const x = v.x * c - v.y * s; const y = v.x * s + v.y * c; v.x = x * scale; v.y = y * scale; } } function translateVerts(verts, dx, dy) { for (const v of verts) { v.x += dx; v.y += dy; } } export function generateQFTScene(params) { // ---- cubic ---- (cubicN controls density: 1=27v, 2=125v, 3=343v) const cubic = buildCubic(Math.max(1, params.cubicN | 0)); rotateScale(cubic.vertices, params.cubicRot, params.cubicScale); translateVerts(cubic.vertices, params.cubicOriginX, params.cubicOriginY); cubic.id = 'cubic'; cubic.propagator = 'photon'; // ---- schlegel ---- (outer/inner radius and 3D rotation now parameterised) const schl = buildSchlegel( params.schlegelOuterR ?? 0.78, params.schlegelInnerR ?? 0.32, params.schlegelRot3D ?? 0.42, ); rotateScale(schl.vertices, params.schlegelRot, params.schlegelScale); translateVerts(schl.vertices, params.schlegelOriginX, params.schlegelOriginY); schl.id = 'schlegel'; schl.propagator = 'scalar'; // ---- E8 clusters ---- // If params.e8Origins (array of {x,y}) is provided, place clusters exactly // at those positions (count comes from the array length). Otherwise auto- // distribute on a ring of radius e8OriginRadius. const e8Rng = makeRng(params.seed, 'qft-e8'); const e8Instances = []; let e8Origins; if (Array.isArray(params.e8Origins) && params.e8Origins.length) { e8Origins = params.e8Origins; } else { const N = Math.max(1, params.e8Count | 0); e8Origins = []; const angleJitter = 0.35; for (let i = 0; i < N; i++) { const baseAng = (i / N) * Math.PI * 2; const ang = baseAng + (e8Rng() - 0.5) * angleJitter; const radius = N === 1 ? 0 : params.e8OriginRadius; e8Origins.push({ x: Math.cos(ang) * radius, y: Math.sin(ang) * radius }); } } for (let i = 0; i < e8Origins.length; i++) { const o = e8Origins[i]; // per-cluster scale override: e8Origins[i] may include {x, y, scale} const clusterScale = (typeof o.scale === 'number') ? o.scale : params.e8Scale; // pick rosette geometry: open NAUTILUS spiral (default for chaotic-growth feel) // or closed concentric RINGS (the older E8-Coxeter-style mandala). const style = params.e8Style ?? 'nautilus'; const e = style === 'rings' ? buildE8(4, 18) : buildNautilus(params.nautilusTurns ?? 2.6, params.nautilusPerTurn ?? 14, params.nautilusGrowth ?? 0.21); rotateScale(e.vertices, params.e8Rot + (e8Rng() - 0.5) * 0.6, clusterScale); translateVerts(e.vertices, o.x, o.y); e.id = 'e8'; e.instance = i; e.propagator = 'gluon'; e8Instances.push(e); } // ---- RIPPLES — concentric wavefronts from source points ---- // params.ripples: [{ x, y, count?, r0?, dR?, propagator? }] const rippleInstances = []; if (Array.isArray(params.ripples)) { for (let i = 0; i < params.ripples.length; i++) { const rs = params.ripples[i]; const r = buildRipples( { x: rs.x, y: rs.y }, rs.count ?? 6, rs.r0 ?? 0.06, rs.dR ?? 0.09, rs.segments ?? 40, ); r.id = 'ripple'; r.instance = i; r.propagator = rs.propagator ?? 'photon'; rippleInstances.push(r); } } const allFields = [cubic, schl, ...e8Instances, ...rippleInstances]; // ---- FIELD PERTURBATIONS — applied to cubic + schlegel after scale/rotate ---- // wavepackets: Gaussian bulge/dimple (Schrödinger particle as field feature) if (Array.isArray(params.wavepackets) && params.wavepackets.length) { applyWavepackets(cubic.vertices, params.wavepackets); applyWavepackets(schl.vertices, params.wavepackets); } // vortices: swirling Gaussian rotation (angular momentum / spin) if (Array.isArray(params.vortices) && params.vortices.length) { applyVortices(cubic.vertices, params.vortices); applyVortices(schl.vertices, params.vortices); } // standing waves: global plane-wave sinusoidal modulation (Chladni / nodal lines) if (Array.isArray(params.standingWaves) && params.standingWaves.length) { applyStandingWaves(cubic.vertices, params.standingWaves); applyStandingWaves(schl.vertices, params.standingWaves); } // distortion (per-field salt so neighbouring lattices warp differently) for (const g of allFields) { const salt = 'dist:' + g.id + (g.instance != null ? ':' + g.instance : ''); distortVertices(g.vertices, params.distRCalm, params.distRMax, params.distStrength, params.seed, salt); } // composition offset — translate the whole scene on the paper if (params.compositionOffsetX || params.compositionOffsetY) { for (const g of allFields) translateVerts(g.vertices, params.compositionOffsetX, params.compositionOffsetY); } // ---- cross-field links ---- const linkRng = makeRng(params.seed, 'qft-links'); const propChoices = ['photon', 'scalar', 'gluon']; const linkCount = Math.max(0, params.linkCount | 0); const links = []; // pair fields by their INDEX in allFields so multi-E8 clusters can interact // with cubic/schlegel and with each other for (let i = 0; i < linkCount && allFields.length >= 2; i++) { let f1Idx, f2Idx, attempts = 0; do { f1Idx = Math.floor(linkRng() * allFields.length); f2Idx = Math.floor(linkRng() * allFields.length); attempts++; } while ((f1Idx === f2Idx || allFields[f1Idx].id === allFields[f2Idx].id) && attempts < 20); const f1 = allFields[f1Idx], f2 = allFields[f2Idx]; const v1 = f1.vertices[Math.floor(linkRng() * f1.vertices.length)]; const v2 = f2.vertices[Math.floor(linkRng() * f2.vertices.length)]; const prop = params.linkPropagator === 'mixed' ? propChoices[Math.floor(linkRng() * propChoices.length)] : (params.linkPropagator || 'photon'); // per-link curvature: random sign × random magnitude up to params.linkCurvature. // a fraction of links stay straight (curvature=0) so the result is mixed-tangle. let curvature = 0; const lc = params.linkCurvature ?? 0; if (lc > 1e-3) { // 30% straight, 70% curved; signed if (linkRng() > 0.30) { const sign = linkRng() < 0.5 ? -1 : 1; curvature = sign * lc * (0.45 + linkRng() * 0.55); } } links.push({ a: { x: v1.x, y: v1.y }, b: { x: v2.x, y: v2.y }, propagator: prop, curvature }); } // archival metadata const hash = cyrb53(params.seed); const ds = parseInt(hash.slice(0, 8), 16); const plate = (parseInt(hash.slice(-3), 16) % 999).toString().padStart(3, '0'); const year = 1985 + (ds % 30); const month = 1 + ((ds >> 4) % 12); const day = 1 + ((ds >> 8) % 28); const exposure = `${year}.${String(month).padStart(2, '0')}.${String(day).padStart(2, '0')}`; const lab = pick(makeRng(params.seed, 'qft-lab'), LABS); return { grids: allFields, links, hash, plate, exposure, lab }; }