@3driseai/3drise-viewer 0.1.17 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/components/Galaxy.d.ts.map +1 -1
  2. package/dist/components/Galaxy.js +2 -0
  3. package/dist/components/ObjectNode.d.ts.map +1 -1
  4. package/dist/components/ObjectNode.js +9 -2
  5. package/dist/components/ParticlesGenerator.d.ts +10 -0
  6. package/dist/components/ParticlesGenerator.d.ts.map +1 -0
  7. package/dist/components/ParticlesGenerator.js +23 -0
  8. package/dist/components/effects/ShockwaveEffect.js +2 -2
  9. package/dist/components/index.d.ts +3 -0
  10. package/dist/components/index.d.ts.map +1 -1
  11. package/dist/components/index.js +2 -0
  12. package/dist/components/particles/BioluminescentWakeParticles.d.ts +12 -0
  13. package/dist/components/particles/BioluminescentWakeParticles.d.ts.map +1 -0
  14. package/dist/components/particles/BioluminescentWakeParticles.js +1123 -0
  15. package/dist/components/particles/BrownianDustParticles.d.ts +12 -0
  16. package/dist/components/particles/BrownianDustParticles.d.ts.map +1 -0
  17. package/dist/components/particles/BrownianDustParticles.js +757 -0
  18. package/dist/components/particles/ElectronOrbitalsParticles.d.ts +12 -0
  19. package/dist/components/particles/ElectronOrbitalsParticles.d.ts.map +1 -0
  20. package/dist/components/particles/ElectronOrbitalsParticles.js +944 -0
  21. package/dist/components/particles/GrinderSparksParticles.d.ts +12 -0
  22. package/dist/components/particles/GrinderSparksParticles.d.ts.map +1 -0
  23. package/dist/components/particles/GrinderSparksParticles.js +744 -0
  24. package/dist/components/particles/TidalStreamParticles.d.ts +12 -0
  25. package/dist/components/particles/TidalStreamParticles.d.ts.map +1 -0
  26. package/dist/components/particles/TidalStreamParticles.js +788 -0
  27. package/dist/components/particles/index.d.ts +11 -0
  28. package/dist/components/particles/index.d.ts.map +1 -0
  29. package/dist/components/particles/index.js +6 -0
  30. package/dist/utils/particleTrails.d.ts +70 -0
  31. package/dist/utils/particleTrails.d.ts.map +1 -0
  32. package/dist/utils/particleTrails.js +369 -0
  33. package/dist/utils/pointerField.d.ts +28 -0
  34. package/dist/utils/pointerField.d.ts.map +1 -0
  35. package/dist/utils/pointerField.js +103 -0
  36. package/package.json +2 -2
@@ -0,0 +1,944 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useLayoutEffect, useMemo, useRef } from 'react';
3
+ import * as THREE from 'three';
4
+ import { useFrame } from '@react-three/fiber';
5
+ import { applyGenerativeAnimations } from '../../utils/handleGenerativeEffectsAnimations.js';
6
+ import { createPointerUniforms, stepPointerField, usePointerField } from '../../utils/pointerField.js';
7
+ import { createTrailUniforms, applyTrailConfig, createCpuTrail } from '../../utils/particleTrails.js';
8
+ import { useObjectReady } from '../../hooks/useObjectReady.js';
9
+ /**
10
+ * Quantum Orbitals — A hydrogen electron cloud as Bohmian trajectories: a superposition of two orbitals breathing at the Bohr frequency, coloured by quantum phase; the cursor is a photon, a magnet or a detector. Use for science and tech scenes.
11
+ */
12
+ // Quantum Orbitals — an electron cloud in a hydrogen atom, as Bohmian trajectories.
13
+ // Movement: the state is a superposition psi = cos(chi) psi_A + sin(chi) e^{i delta} psi_B of two
14
+ // hydrogen eigenstates psi_nlm = R_nl(r) Theta_lm(theta) e^{i m phi} e^{-i E_n t},
15
+ // E_n = -1/(2 n^2) (atomic units). Each particle follows the de Broglie–Bohm guidance law
16
+ // v = Im(grad psi / psi). Particles start distributed as |psi|^2 (exact rejection sampling)
17
+ // and equivariance keeps them so for all time: the cloud breathes and rotates at the Bohr
18
+ // frequency (E_A - E_B) while every particle circulates along its own current line.
19
+ // R_nl comes from associated Laguerre polynomials and Theta_lm from associated Legendre
20
+ // functions, tabulated once and normalised numerically.
21
+ // Colour: the phase of psi at the particle (arg psi) mapped round a two-colour cycle, brightness
22
+ // by density; a raymarched |psi|^2 haze shows the probability cloud the particles sample.
23
+ // Mouse (pointerMode):
24
+ // photon — a resonant light source: its field drives Rabi oscillations between the two states
25
+ // (faster the closer it is), so the cloud morphs A -> B -> A while it shines; released,
26
+ // the atom decays back by spontaneous emission and sheds photons.
27
+ // magnet — a magnetic field pointing from the nucleus to the cursor: the Zeeman term makes the
28
+ // whole cloud precess about the field at the Larmor frequency (exact: a rotation).
29
+ // measure — a position detector: it clicks at a rate proportional to the probability inside its
30
+ // window (silent at nodes, busy in lobes); each click localises the ensemble into a
31
+ // packet at the detected point, which then re-spreads into the stationary cloud.
32
+ // Trails (trailMode): streak = long exposure, comet = Bohm trajectories, echo = afterglow.
33
+ //
34
+ // Generated from the approved lab module (particle lab, batch 2). Sim units = object-local / size.
35
+ /** mulberry32 — seeded PRNG, returns a function giving floats in [0,1). */
36
+ function mulberry32(seed) {
37
+ let a = seed >>> 0;
38
+ return function () {
39
+ a = (a + 0x6d2b79f5) >>> 0;
40
+ let t = a;
41
+ t = Math.imul(t ^ (t >>> 15), t | 1);
42
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
43
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
44
+ };
45
+ }
46
+ /** Viewport-height hook so point sizes are pixel-correct at any canvas size. */
47
+ function attachViewH(obj, u) {
48
+ const scratch = new THREE.Vector2();
49
+ obj.onBeforeRender = (renderer) => {
50
+ renderer.getDrawingBufferSize(scratch);
51
+ u.uViewH.value = scratch.y;
52
+ };
53
+ }
54
+ function colorUniform(hex) {
55
+ return { value: new THREE.Color(hex) };
56
+ }
57
+ /** Quality levels scale counts, raymarch samples and trail caps. Structural. */
58
+ const QUALITY_SCALE = { low: 0.5, medium: 1, high: 1.6 };
59
+ function qualityScale(q) { return QUALITY_SCALE[q] || 1; }
60
+ /** Mix the object's own base seed with the `seed` knob into a mulberry32 seed. */
61
+ function seedMix(base, seed) {
62
+ let h = (base ^ Math.imul((Math.round(seed) | 0) + 0x9e37, 0x85ebca6b)) >>> 0;
63
+ h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35) >>> 0;
64
+ return (h ^ (h >>> 16)) >>> 0;
65
+ }
66
+ /** Fixed-step clock: the same motion at 30, 60 or 144 Hz. `run(d, step)` advances d sim-seconds
67
+ * in steps of exactly H (at most maxSteps per call; a longer backlog is dropped, not caught up). */
68
+ function createFixedStep(H, maxSteps) {
69
+ let acc = 0;
70
+ return {
71
+ H,
72
+ run(d, step) {
73
+ acc += Math.max(d, 0);
74
+ let k = 0;
75
+ while (acc >= H && k < maxSteps) {
76
+ step(H);
77
+ acc -= H;
78
+ k++;
79
+ }
80
+ if (k === maxSteps)
81
+ acc = Math.min(acc, H);
82
+ return acc / H; // leftover fraction, for render interpolation
83
+ },
84
+ reset() { acc = 0; },
85
+ };
86
+ }
87
+ /** Selection bounds: an invisible sphere the editor can pick, frame and size (never rendered,
88
+ * still raycastable and counted by Box3.setFromObject). */
89
+ function createPickProxy(center, radius) {
90
+ const geo = new THREE.SphereGeometry(1, 16, 10);
91
+ const mat = new THREE.MeshBasicMaterial({ visible: false });
92
+ const mesh = new THREE.Mesh(geo, mat);
93
+ mesh.name = 'pickProxy';
94
+ mesh.position.set(center[0], center[1], center[2]);
95
+ mesh.scale.setScalar(radius);
96
+ return { mesh, dispose() { geo.dispose(); mat.dispose(); } };
97
+ }
98
+ const _frustum = new THREE.Frustum();
99
+ const _pm = new THREE.Matrix4();
100
+ const _sphere = new THREE.Sphere();
101
+ /** Off-screen test: false when the object's bounding sphere is outside the camera frustum. */
102
+ function inView(object, camera, center, radius, size) {
103
+ if (!camera || !camera.projectionMatrix)
104
+ return true;
105
+ object.updateWorldMatrix(true, false);
106
+ _sphere.center.set(center[0], center[1], center[2]).multiplyScalar(size).applyMatrix4(object.matrixWorld);
107
+ _sphere.radius = radius * size * object.matrixWorld.getMaxScaleOnAxis();
108
+ _pm.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
109
+ _frustum.setFromProjectionMatrix(_pm);
110
+ return _frustum.intersectsSphere(_sphere);
111
+ }
112
+ const POINTER_MODES = ['photon', 'magnet', 'measure'];
113
+ const STATES = ['1s', '2s', '2p0', '2p+1', '3s', '3p0', '3p+1', '3d0', '3d+1', '3d+2', '4s', '4p+1', '4d+1', '4d+2', '4f0', '4f+1', '4f+2', '4f+3'];
114
+ const L_OF = { s: 0, p: 1, d: 2, f: 3 };
115
+ const stateLabel = (k) => `${k.slice(0, 2)} (m = ${k.slice(2)})`;
116
+ const stateOptions = () => Object.fromEntries(STATES.map((k) => [stateLabel(k), k]));
117
+ const NR = 1024, NT = 512;
118
+ const TS = 12; // atomic time units per second at timeScale 1
119
+ const H_STEP = 1 / 45;
120
+ const BOUNDS = { center: [0, 0, 0], radius: 1.2 };
121
+ const MAX_PHOT = 24;
122
+ function parseState(k) {
123
+ const n = +k[0], l = L_OF[k[1]], m = +(k.slice(2).replace('+', '')) || 0;
124
+ return { n, l, m: Math.abs(m) };
125
+ }
126
+ // Hydrogen radial function tables R(r), R'(r) on [0, rmax], normalised: int R^2 r^2 dr = 1.
127
+ function radialTable(n, l, rmax) {
128
+ const R = new Float64Array(NR), dR = new Float64Array(NR);
129
+ const k = n - l - 1, alpha = 2 * l + 1;
130
+ for (let i = 0; i < NR; i++) {
131
+ const r = (i / (NR - 1)) * rmax;
132
+ const x = (2 * r) / n;
133
+ let L0 = 1, L1 = 1 + alpha - x, Lk = k === 0 ? 1 : L1;
134
+ for (let j = 1; j < k; j++) {
135
+ const L2 = ((2 * j + 1 + alpha - x) * L1 - (j + alpha) * L0) / (j + 1);
136
+ L0 = L1;
137
+ L1 = L2;
138
+ Lk = L2;
139
+ }
140
+ R[i] = Math.pow(x, l) * Math.exp(-x / 2) * Lk;
141
+ }
142
+ let norm = 0;
143
+ const dr = rmax / (NR - 1);
144
+ for (let i = 0; i < NR; i++) {
145
+ const r = i * dr;
146
+ norm += R[i] * R[i] * r * r * dr;
147
+ }
148
+ const s = 1 / Math.sqrt(norm);
149
+ for (let i = 0; i < NR; i++)
150
+ R[i] *= s;
151
+ for (let i = 0; i < NR; i++)
152
+ dR[i] = (R[Math.min(i + 1, NR - 1)] - R[Math.max(i - 1, 0)]) / (dr * (i === 0 || i === NR - 1 ? 1 : 2));
153
+ return { R, dR };
154
+ }
155
+ // Theta_lm(theta) ~ P_l^m(cos theta), normalised so int Theta^2 sin(theta) dtheta = 1 / (2 pi).
156
+ function angularTable(l, m) {
157
+ const T = new Float64Array(NT), dT = new Float64Array(NT);
158
+ for (let i = 0; i < NT; i++) {
159
+ const th = (i / (NT - 1)) * Math.PI;
160
+ const x = Math.cos(th), sx = Math.sin(th);
161
+ let pmm = 1;
162
+ for (let j = 1; j <= m; j++)
163
+ pmm *= -(2 * j - 1) * sx;
164
+ let val;
165
+ if (l === m)
166
+ val = pmm;
167
+ else {
168
+ let pm1 = x * (2 * m + 1) * pmm;
169
+ if (l === m + 1)
170
+ val = pm1;
171
+ else {
172
+ let p0 = pmm;
173
+ for (let ll = m + 2; ll <= l; ll++) {
174
+ const p2 = (x * (2 * ll - 1) * pm1 - (ll + m - 1) * p0) / (ll - m);
175
+ p0 = pm1;
176
+ pm1 = p2;
177
+ }
178
+ val = pm1;
179
+ }
180
+ }
181
+ T[i] = val;
182
+ }
183
+ let norm = 0;
184
+ const dth = Math.PI / (NT - 1);
185
+ for (let i = 0; i < NT; i++)
186
+ norm += T[i] * T[i] * Math.sin(i * dth) * dth;
187
+ const s = 1 / Math.sqrt(norm * 2 * Math.PI);
188
+ for (let i = 0; i < NT; i++)
189
+ T[i] *= s;
190
+ for (let i = 0; i < NT; i++)
191
+ dT[i] = (T[Math.min(i + 1, NT - 1)] - T[Math.max(i - 1, 0)]) / (dth * (i === 0 || i === NT - 1 ? 1 : 2));
192
+ return { T, dT };
193
+ }
194
+ function cdfSample(pdf, rng) {
195
+ const c = new Float64Array(pdf.length);
196
+ let acc = 0;
197
+ for (let i = 0; i < pdf.length; i++) {
198
+ acc += pdf[i];
199
+ c[i] = acc;
200
+ }
201
+ return () => {
202
+ const u = rng() * acc;
203
+ let lo = 0, hi = pdf.length - 1;
204
+ while (lo < hi) {
205
+ const mid = (lo + hi) >> 1;
206
+ if (c[mid] < u)
207
+ lo = mid + 1;
208
+ else
209
+ hi = mid;
210
+ }
211
+ const prev = lo > 0 ? c[lo - 1] : 0;
212
+ return (lo + (u - prev) / Math.max(c[lo] - prev, 1e-30)) / (pdf.length - 1);
213
+ };
214
+ }
215
+ function createUniforms() {
216
+ return {
217
+ uTime: { value: 0 },
218
+ uSize: { value: 1 },
219
+ uViewH: { value: 800 },
220
+ uIntensity: { value: 1 },
221
+ uSpeed: { value: 1 },
222
+ uOpacity: { value: 0.9 },
223
+ uColor: colorUniform('#ffffff'),
224
+ uParticleCount: { value: 0 },
225
+ uMix: { value: 0.35 },
226
+ uTimeScale: { value: 1 },
227
+ uPhaseColor: colorUniform('#ffffff'),
228
+ uCloudDensity: { value: 0.6 },
229
+ uPhaseFronts: { value: 1 },
230
+ uPointSize: { value: 1 },
231
+ uPointerMode: { value: 0 },
232
+ uPointerStrength: { value: 1 },
233
+ uPointerRadius: { value: 1 },
234
+ uPointerColor: colorUniform('#ffffff'),
235
+ // wavefunction state for the haze (set by the simulation)
236
+ uTables: { value: null },
237
+ uRS: { value: 20 }, // Bohr radii per sim unit
238
+ uRMax: { value: 40 },
239
+ uAmp: { value: new THREE.Vector2(1, 0) }, // cos chi, sin chi
240
+ uM: { value: new THREE.Vector2(0, 0) }, // m_A, m_B
241
+ uPhA: { value: new THREE.Vector2(1, 0) }, // e^{-i E_A t}
242
+ uPhB: { value: new THREE.Vector2(1, 0) }, // e^{-i E_B t + i delta}
243
+ uFrameInv: { value: new THREE.Matrix3() }, // world -> body (magnet precession)
244
+ uCollapse: { value: new THREE.Vector4(0, 0, 0, 0) }, // detected point xyz, kappa
245
+ uCamLocal: { value: new THREE.Vector3(0, 0, 3) },
246
+ ...createPointerUniforms(),
247
+ ...createTrailUniforms(),
248
+ };
249
+ }
250
+ function applyConfig(u, c, size) {
251
+ u.uSize.value = size;
252
+ u.uIntensity.value = c.intensity;
253
+ u.uSpeed.value = c.speed;
254
+ u.uOpacity.value = c.opacity;
255
+ u.uColor.value.set(c.color);
256
+ u.uParticleCount.value = c.particleCount;
257
+ u.uMix.value = c.mix;
258
+ u.uTimeScale.value = c.timeScale;
259
+ u.uPhaseColor.value.set(c.phaseColor);
260
+ u.uCloudDensity.value = c.cloudDensity;
261
+ u.uPhaseFronts.value = c.phaseFronts;
262
+ u.uPointSize.value = c.pointSize;
263
+ u.uPointerMode.value = POINTER_MODES.indexOf(c.pointerMode); // -1 = 'none' (off)
264
+ u.uPointerStrength.value = c.pointerStrength;
265
+ u.uPointerRadius.value = c.pointerRadius;
266
+ u.uPointerColor.value.set(c.pointerColor);
267
+ applyTrailConfig(u, c);
268
+ }
269
+ const BASE_U = 'uniform float uSize, uViewH, uIntensity, uOpacity;';
270
+ // Phase colour: a cycle through color -> white-ish mid -> phaseColor -> mid -> color.
271
+ const PHASE_GLSL = /* glsl */ `
272
+ uniform vec3 uColor, uPhaseColor;
273
+ vec3 phaseRamp(float c, float s) {
274
+ float t = 0.5 - 0.5 * c; // 0 at phase 0, 1 at phase pi
275
+ vec3 mid = mix(uColor, uPhaseColor, 0.5) * 0.7 + vec3(0.3) * (0.4 + 0.2 * s);
276
+ return t < 0.5 ? mix(uColor, mid, t * 2.0) : mix(mid, uPhaseColor, t * 2.0 - 1.0);
277
+ }
278
+ `;
279
+ const partVertex = /* glsl */ `
280
+ ${BASE_U}
281
+ ${PHASE_GLSL}
282
+ uniform float uPointSize;
283
+ attribute vec2 aPhase; // cos, sin of arg psi
284
+ attribute float aSeed;
285
+ varying vec3 vCol;
286
+ varying float vA;
287
+ void main() {
288
+ vec4 mv = modelViewMatrix * vec4(position * uSize, 1.0);
289
+ float px = uSize * uViewH * 0.0042 * uPointSize * (0.8 + 0.4 * aSeed) / -mv.z;
290
+ vCol = phaseRamp(aPhase.x, aPhase.y) * uIntensity * 0.8;
291
+ vA = uOpacity;
292
+ gl_PointSize = clamp(px, 1.0, 48.0);
293
+ gl_Position = projectionMatrix * mv;
294
+ }
295
+ `;
296
+ const partFragment = /* glsl */ `
297
+ varying vec3 vCol;
298
+ varying float vA;
299
+ void main() {
300
+ vec2 d = gl_PointCoord * 2.0 - 1.0;
301
+ float r2 = dot(d, d);
302
+ if (r2 > 1.0) discard;
303
+ float a = (exp(-r2 * 9.0) + 0.18 * exp(-r2 * 2.5)) * (1.0 - r2) * vA;
304
+ if (a < 0.003) discard;
305
+ gl_FragColor = vec4(vCol, a);
306
+ }
307
+ `;
308
+ // |psi|^2 haze: raymarch a sphere, evaluating the same superposition from the tables.
309
+ const WAVE_GLSL = /* glsl */ `
310
+ uniform sampler2D uTables;
311
+ uniform float uRS, uRMax;
312
+ uniform vec2 uAmp, uM, uPhA, uPhB;
313
+ uniform mat3 uFrameInv;
314
+ vec2 cmul(vec2 a, vec2 b) { return vec2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); }
315
+ vec2 cpow(vec2 z, float m) {
316
+ vec2 r = vec2(1.0, 0.0);
317
+ if (m > 0.5) r = z;
318
+ if (m > 1.5) r = cmul(r, z);
319
+ if (m > 2.5) r = cmul(r, z);
320
+ return r;
321
+ }
322
+ // psi at a body-frame point (sim units), returns (Re, Im)
323
+ vec2 psiAt(vec3 pb) {
324
+ vec3 p = pb * uRS;
325
+ float r = length(p);
326
+ float th = acos(clamp(p.y / max(r, 1e-5), -1.0, 1.0));
327
+ float rho = length(p.xz);
328
+ vec2 z = rho > 1e-5 ? vec2(p.x, p.z) / rho : vec2(1.0, 0.0);
329
+ vec4 Rr = texture2D(uTables, vec2((r / uRMax) * ${((NR - 1) / NR).toFixed(5)} + ${(0.5 / NR).toFixed(6)}, 0.25));
330
+ vec4 Tt = texture2D(uTables, vec2((th / 3.14159265) * ${((NR - 1) / NR).toFixed(5)} + ${(0.5 / NR).toFixed(6)}, 0.75));
331
+ float inside = step(r, uRMax);
332
+ float F = uAmp.x * Rr.x * Tt.x * inside, G = uAmp.y * Rr.y * Tt.y * inside;
333
+ vec2 wA = cmul(cpow(z, uM.x), uPhA), wB = cmul(cpow(z, uM.y), uPhB);
334
+ return F * wA + G * wB;
335
+ }
336
+ `;
337
+ const volVertex = /* glsl */ `
338
+ uniform float uSize;
339
+ varying vec3 vLocal;
340
+ void main() {
341
+ vLocal = position;
342
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position * uSize, 1.0);
343
+ }
344
+ `;
345
+ const volFragment = /* glsl */ `
346
+ ${BASE_U}
347
+ ${PHASE_GLSL}
348
+ ${WAVE_GLSL}
349
+ uniform float uCloudDensity, uPhaseFronts;
350
+ uniform vec4 uCollapse;
351
+ uniform vec3 uCamLocal;
352
+ varying vec3 vLocal;
353
+ void main() {
354
+ vec3 ro = uCamLocal;
355
+ vec3 rd = normalize(vLocal - ro);
356
+ float R = ${BOUNDS.radius.toFixed(2)};
357
+ float b = dot(ro, rd), cc = dot(ro, ro) - R * R;
358
+ float disc = b * b - cc;
359
+ if (disc <= 0.0) discard;
360
+ float sq = sqrt(disc);
361
+ float tn = max(-b - sq, 0.0), tf = -b + sq;
362
+ if (tf <= tn) discard;
363
+ const int N = 40;
364
+ float dt = (tf - tn) / float(N);
365
+ float jit = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));
366
+ vec3 acc = vec3(0.0);
367
+ for (int i = 0; i < N; i++) {
368
+ vec3 p = ro + rd * (tn + (float(i) + jit) * dt);
369
+ vec2 ps = psiAt(uFrameInv * p);
370
+ float d2 = dot(ps, ps);
371
+ float mag = sqrt(max(d2, 1e-20));
372
+ vec2 ph = ps / mag;
373
+ // phase fronts: the surfaces S = 0 and S = pi (the particles cross them along grad S)
374
+ float front = pow(abs(ph.x), 40.0);
375
+ acc += phaseRamp(ph.x, ph.y) * d2 * (0.25 + 3.0 * uPhaseFronts * front);
376
+ }
377
+ // density per sim volume = |psi|^2 * RS^3; the collapse empties the stationary cloud a little
378
+ vec3 rgb = acc * dt * pow(uRS, 3.0) * uCloudDensity * uIntensity * uOpacity * 0.5 * (1.0 - 0.6 * uCollapse.w);
379
+ float I = max(max(rgb.r, rgb.g), rgb.b);
380
+ if (I < 0.0008) discard;
381
+ I = min(I, 1.0);
382
+ gl_FragColor = vec4(rgb / max(max(max(rgb.r, rgb.g), rgb.b), 1e-4), I);
383
+ }
384
+ `;
385
+ // Glyphs: the atom's own reactions — nucleus, detector clicks, emitted photons (nothing at the cursor).
386
+ const glyphVertex = /* glsl */ `
387
+ uniform float uSize, uViewH;
388
+ attribute vec4 aG; // kind, age, strength, unused
389
+ varying vec4 vG;
390
+ void main() {
391
+ vec4 mv = modelViewMatrix * vec4(position * uSize, 1.0);
392
+ float kind = aG.x;
393
+ float s = kind < 0.5 ? 0.035 : kind < 1.5 ? 0.08 + aG.y * 0.45 : 0.03; // nucleus, click flash, photon
394
+ vG = aG;
395
+ gl_PointSize = clamp(uSize * uViewH * s / -mv.z, 0.0, 300.0) * step(0.001, aG.z);
396
+ gl_Position = projectionMatrix * mv;
397
+ }
398
+ `;
399
+ const glyphFragment = /* glsl */ `
400
+ uniform vec3 uPointerColor;
401
+ uniform float uOpacity, uIntensity;
402
+ varying vec4 vG;
403
+ void main() {
404
+ vec2 d = gl_PointCoord * 2.0 - 1.0;
405
+ float r2 = dot(d, d);
406
+ if (r2 > 1.0) discard;
407
+ float r = sqrt(r2);
408
+ float kind = vG.x;
409
+ float a; vec3 col = uPointerColor;
410
+ if (kind < 0.5) { a = exp(-r2 * 30.0) * 1.4 + exp(-r2 * 6.0) * 0.2; col = vec3(1.0, 0.93, 0.82); }
411
+ else if (kind < 1.5) a = (exp(-pow((r - 0.7 * min(vG.y * 2.5, 1.0)) / 0.08, 2.0)) * 1.2 + exp(-r2 * 30.0) * 3.0) * (1.0 - vG.y);
412
+ else a = exp(-r2 * 20.0) * (1.0 - vG.y);
413
+ a *= (1.0 - r2) * vG.z * uOpacity;
414
+ if (a < 0.003) discard;
415
+ gl_FragColor = vec4(col * uIntensity, a);
416
+ }
417
+ `;
418
+ function build(u, c, size) {
419
+ const q = qualityScale(c.quality);
420
+ const n = Math.max(1, Math.round(c.particleCount * q));
421
+ const rng = mulberry32(seedMix(0xa70b, c.seed));
422
+ const SA = parseState(STATES.includes(c.stateA) ? c.stateA : '4f+2');
423
+ const SB = parseState(STATES.includes(c.stateB) ? c.stateB : '3d+1');
424
+ const nmax = Math.max(SA.n, SB.n);
425
+ const RS = 2.0 * nmax * nmax + 3; // Bohr radii per sim unit: the cloud fills ~ radius 1
426
+ const RMAX = RS * BOUNDS.radius;
427
+ const EA = -0.5 / (SA.n * SA.n), EB = -0.5 / (SB.n * SB.n);
428
+ const mA = SA.m, mB = SB.m;
429
+ const radA = radialTable(SA.n, SA.l, RMAX), radB = radialTable(SB.n, SB.l, RMAX);
430
+ const angA = angularTable(SA.l, SA.m), angB = angularTable(SB.l, SB.m);
431
+ // interleaved tables for the hot loop: r-table [RA, RA', RB, RB'], theta-table [TA, TA', TB, TB', sin, cos]
432
+ const RT = new Float64Array(NR * 4), TT = new Float64Array(NT * 6);
433
+ for (let i = 0; i < NR; i++) {
434
+ RT[i * 4] = radA.R[i];
435
+ RT[i * 4 + 1] = radA.dR[i];
436
+ RT[i * 4 + 2] = radB.R[i];
437
+ RT[i * 4 + 3] = radB.dR[i];
438
+ }
439
+ for (let i = 0; i < NT; i++) {
440
+ const th = (i / (NT - 1)) * Math.PI;
441
+ TT[i * 6] = angA.T[i];
442
+ TT[i * 6 + 1] = angA.dT[i];
443
+ TT[i * 6 + 2] = angB.T[i];
444
+ TT[i * 6 + 3] = angB.dT[i];
445
+ TT[i * 6 + 4] = Math.sin(th);
446
+ TT[i * 6 + 5] = Math.cos(th);
447
+ }
448
+ // GPU tables: row 0 = R_A, R_B over r; row 1 = Theta_A, Theta_B over theta (half floats, linear filtered)
449
+ const tex = new Uint16Array(NR * 2 * 4);
450
+ for (let i = 0; i < NR; i++) {
451
+ tex[i * 4] = THREE.DataUtils.toHalfFloat(radA.R[i]);
452
+ tex[i * 4 + 1] = THREE.DataUtils.toHalfFloat(radB.R[i]);
453
+ const ti = Math.min(Math.round((i / (NR - 1)) * (NT - 1)), NT - 1);
454
+ tex[(NR + i) * 4] = THREE.DataUtils.toHalfFloat(angA.T[ti]);
455
+ tex[(NR + i) * 4 + 1] = THREE.DataUtils.toHalfFloat(angB.T[ti]);
456
+ }
457
+ const tables = new THREE.DataTexture(tex, NR, 2, THREE.RGBAFormat, THREE.HalfFloatType);
458
+ tables.magFilter = THREE.LinearFilter;
459
+ tables.minFilter = THREE.LinearFilter;
460
+ tables.wrapS = THREE.ClampToEdgeWrapping;
461
+ tables.wrapT = THREE.ClampToEdgeWrapping;
462
+ tables.needsUpdate = true;
463
+ u.uTables.value = tables;
464
+ u.uRS.value = RS;
465
+ u.uRMax.value = RMAX;
466
+ u.uM.value.set(mA, mB);
467
+ // ---- state ----
468
+ let chi = Math.asin(Math.sqrt(Math.min(Math.max(u.uMix.value, 0), 1)));
469
+ let delta = 0;
470
+ let tau = 0;
471
+ // ---- initial positions: exact |psi(0)|^2 sampling (mixture proposal + interference rejection) ----
472
+ const rA = cdfSample(Array.from(radA.R, (v, i) => v * v * ((i / (NR - 1)) * RMAX) ** 2), rng);
473
+ const rB = cdfSample(Array.from(radB.R, (v, i) => v * v * ((i / (NR - 1)) * RMAX) ** 2), rng);
474
+ const tA = cdfSample(Array.from(angA.T, (v, i) => v * v * Math.sin((i / (NT - 1)) * Math.PI)), rng);
475
+ const tB = cdfSample(Array.from(angB.T, (v, i) => v * v * Math.sin((i / (NT - 1)) * Math.PI)), rng);
476
+ const pr = new Float64Array(n), pth = new Float64Array(n), pc = new Float64Array(n), psn = new Float64Array(n);
477
+ function lookup(r, th) {
478
+ const fr = Math.min(Math.max(r / RMAX, 0), 1) * (NR - 1);
479
+ const i0 = Math.min(fr | 0, NR - 2), t = fr - i0;
480
+ const ft = Math.min(Math.max(th / Math.PI, 0), 1) * (NT - 1);
481
+ const j0 = Math.min(ft | 0, NT - 2), w = ft - j0;
482
+ const RA = RT[i0 * 4] + (RT[i0 * 4 + 4] - RT[i0 * 4]) * t;
483
+ const RB = RT[i0 * 4 + 2] + (RT[i0 * 4 + 6] - RT[i0 * 4 + 2]) * t;
484
+ const TA = TT[j0 * 6] + (TT[j0 * 6 + 6] - TT[j0 * 6]) * w;
485
+ const TB = TT[j0 * 6 + 2] + (TT[j0 * 6 + 8] - TT[j0 * 6 + 2]) * w;
486
+ return [RA * TA, RB * TB];
487
+ }
488
+ function sampleAll(chiNow) {
489
+ const a = Math.cos(chiNow), b = Math.sin(chiNow);
490
+ for (let i = 0; i < n; i++) {
491
+ for (let tries = 0; tries < 200; tries++) {
492
+ const useA = rng() < a * a;
493
+ const r = (useA ? rA() : rB()) * RMAX;
494
+ const th = (useA ? tA() : tB()) * Math.PI;
495
+ const ph = rng() * 6.2831853;
496
+ const [fa, fb] = lookup(r, th);
497
+ const F = a * fa, G = b * fb;
498
+ const dPh = (mA - mB) * ph;
499
+ const num = F * F + G * G + 2 * F * G * Math.cos(dPh);
500
+ const den = 2 * (F * F + G * G) + 1e-30;
501
+ if (rng() * den <= num || tries === 199) {
502
+ pr[i] = r;
503
+ pth[i] = th;
504
+ pc[i] = Math.cos(ph);
505
+ psn[i] = Math.sin(ph);
506
+ break;
507
+ }
508
+ }
509
+ }
510
+ }
511
+ sampleAll(chi);
512
+ const pos = new Float32Array(n * 3), vel = new Float32Array(n * 3), phase = new Float32Array(n * 2);
513
+ const seedA = new Float32Array(n);
514
+ for (let i = 0; i < n; i++)
515
+ seedA[i] = rng();
516
+ // ---- pointer state ----
517
+ const Q = new THREE.Matrix4(); // body -> world rotation (magnet precession)
518
+ const Qi = new THREE.Matrix3();
519
+ const axis = new THREE.Vector3(0, 1, 0);
520
+ const qtmp = new THREE.Matrix4();
521
+ const col = { x: 0, y: 0, z: 0, k: 0, t: 0 }; // collapse point (world), strength, target
522
+ const clickRng = mulberry32(seedMix(0xc11c, c.seed));
523
+ let clickAge = 9, clickX = 0, clickY = 0, clickZ = 0;
524
+ const PH = new Float32Array(MAX_PHOT * 7); // x,y,z, vx,vy,vz, age
525
+ for (let k = 0; k < MAX_PHOT; k++)
526
+ PH[k * 7 + 6] = 9;
527
+ let phHead = 0, emitAcc = 0;
528
+ const hp = new THREE.Vector3();
529
+ function step(h) {
530
+ const S = u.uPointerStrength.value, act = u.uPointerActive.value, mode = u.uPointerMode.value;
531
+ const Rr = u.uPointerRadius.value;
532
+ hp.copy(u.uPointer.value);
533
+ const dtau = h * TS * u.uTimeScale.value;
534
+ tau += dtau;
535
+ // photon: Rabi drive toward the other state while it shines, spontaneous decay back when released
536
+ const chi0 = Math.asin(Math.sqrt(Math.min(Math.max(u.uMix.value, 0), 1)));
537
+ if (mode === 0 && S > 0.01) {
538
+ const dist = hp.length();
539
+ const reach = 0.9 * Rr;
540
+ const omega = 0.9 * S * act / (1 + (dist * dist) / (reach * reach));
541
+ chi += omega * h;
542
+ if (omega < 0.02) {
543
+ const before = Math.sin(chi) ** 2;
544
+ // chi and chi + pi are the same physical state: relax to the nearest copy of chi0
545
+ const target = chi0 + Math.PI * Math.round((chi - chi0) / Math.PI);
546
+ chi += (target - chi) * Math.min(1, h * 0.45);
547
+ const lost = Math.max(before - Math.sin(chi) ** 2, 0);
548
+ emitAcc += lost * 60; // photons shed as population returns
549
+ }
550
+ }
551
+ else {
552
+ const target = chi0 + Math.PI * Math.round((chi - chi0) / Math.PI);
553
+ chi += (target - chi) * Math.min(1, h * 0.8);
554
+ }
555
+ // magnet: Larmor precession about the pointer direction
556
+ if (mode === 1 && S > 0.01 && act > 0.01) {
557
+ axis.copy(hp);
558
+ if (axis.lengthSq() > 1e-6) {
559
+ axis.normalize();
560
+ qtmp.makeRotationAxis(axis, 0.8 * S * act * h);
561
+ Q.premultiply(qtmp);
562
+ }
563
+ }
564
+ Qi.setFromMatrix4(Q).transpose();
565
+ u.uFrameInv.value.copy(Qi);
566
+ const a = Math.cos(chi), b = Math.sin(chi);
567
+ u.uAmp.value.set(a, b);
568
+ const eAc = Math.cos(EA * tau), eAs = -Math.sin(EA * tau);
569
+ const eBc = Math.cos(-EB * tau + delta), eBs = Math.sin(-EB * tau + delta);
570
+ u.uPhA.value.set(eAc, eAs);
571
+ u.uPhB.value.set(eBc, eBs);
572
+ const qe = Q.elements;
573
+ const q00 = qe[0], q10 = qe[1], q20 = qe[2], q01 = qe[4], q11 = qe[5], q21 = qe[6], q02 = qe[8], q12 = qe[9], q22 = qe[10];
574
+ // detector: count probability in the window, click at a Poisson rate, collapse
575
+ const measuring = mode === 2 && S > 0.01 && act > 0.05;
576
+ const Rd = 0.14 * Rr, Rd2 = Rd * Rd;
577
+ let inWin = 0, pick = -1;
578
+ // the collapse eases in fast (0.12 s) and releases slowly (the packet re-spreads)
579
+ col.t *= Math.exp(-h / 1.1);
580
+ col.k += (col.t - col.k) * Math.min(1, h / 0.12);
581
+ const kap = col.k, cx = col.x, cy = col.y, cz = col.z;
582
+ const Rc2 = 2 * 0.3 * 0.3 * Rr * Rr;
583
+ clickAge += h;
584
+ const RSi = 1 / RS;
585
+ for (let i = 0; i < n; i++) {
586
+ let r = pr[i], th = pth[i], cph = pc[i], sph = psn[i];
587
+ // tables
588
+ let fr = r * (NR - 1) / RMAX;
589
+ if (fr > NR - 1.001)
590
+ fr = NR - 1.001;
591
+ const i0 = fr | 0, t = fr - i0, o = i0 * 4;
592
+ let ft = th * (NT - 1) / Math.PI;
593
+ if (ft < 0)
594
+ ft = 0;
595
+ else if (ft > NT - 1.001)
596
+ ft = NT - 1.001;
597
+ const j0 = ft | 0, w = ft - j0, p = j0 * 6;
598
+ const RA = RT[o] + (RT[o + 4] - RT[o]) * t, RAd = RT[o + 1] + (RT[o + 5] - RT[o + 1]) * t;
599
+ const RB = RT[o + 2] + (RT[o + 6] - RT[o + 2]) * t, RBd = RT[o + 3] + (RT[o + 7] - RT[o + 3]) * t;
600
+ const TA = TT[p] + (TT[p + 6] - TT[p]) * w, TAd = TT[p + 1] + (TT[p + 7] - TT[p + 1]) * w;
601
+ const TB = TT[p + 2] + (TT[p + 8] - TT[p + 2]) * w, TBd = TT[p + 3] + (TT[p + 9] - TT[p + 3]) * w;
602
+ const sT = TT[p + 4] + (TT[p + 10] - TT[p + 4]) * w, cT = TT[p + 5] + (TT[p + 11] - TT[p + 5]) * w;
603
+ const F = a * RA * TA, Fr = a * RAd * TA, Ft = a * RA * TAd;
604
+ const G = b * RB * TB, Gr = b * RBd * TB, Gt = b * RB * TBd;
605
+ // e^{i m phi} by complex powers of (cos phi, sin phi) — no trig per particle
606
+ let zAc = 1, zAs = 0, zBc = 1, zBs = 0;
607
+ const z2c = cph * cph - sph * sph, z2s = 2 * cph * sph;
608
+ if (mA === 1) {
609
+ zAc = cph;
610
+ zAs = sph;
611
+ }
612
+ else if (mA === 2) {
613
+ zAc = z2c;
614
+ zAs = z2s;
615
+ }
616
+ else if (mA === 3) {
617
+ zAc = z2c * cph - z2s * sph;
618
+ zAs = z2c * sph + z2s * cph;
619
+ }
620
+ if (mB === 1) {
621
+ zBc = cph;
622
+ zBs = sph;
623
+ }
624
+ else if (mB === 2) {
625
+ zBc = z2c;
626
+ zBs = z2s;
627
+ }
628
+ else if (mB === 3) {
629
+ zBc = z2c * cph - z2s * sph;
630
+ zBs = z2c * sph + z2s * cph;
631
+ }
632
+ const cA = zAc * eAc - zAs * eAs, sA = zAc * eAs + zAs * eAc;
633
+ const cB = zBc * eBc - zBs * eBs, sB = zBc * eBs + zBs * eBc;
634
+ const cD = cA * cB + sA * sB, sD = sA * cB - cA * sB;
635
+ const rho = F * F + G * G + 2 * F * G * cD + 1e-30;
636
+ const re = F * cA + G * cB, im = F * sA + G * sB;
637
+ const mag = Math.sqrt(re * re + im * im) + 1e-30;
638
+ phase[i * 2] = re / mag;
639
+ phase[i * 2 + 1] = im / mag;
640
+ // Bohm guidance v = Im(grad psi / psi) in spherical components
641
+ const rs = r > 1e-3 ? r : 1e-3;
642
+ const st = sT > 1e-3 ? sT : 1e-3;
643
+ let dr = (sD * (Fr * G - Gr * F) / rho) * dtau;
644
+ let dth = (sD * (Ft * G - Gt * F) / (rho * rs * rs)) * dtau;
645
+ let dph = ((mA * F * F + mB * G * G + F * G * cD * (mA + mB)) / (rho * rs * rs * st * st)) * dtau;
646
+ const lim = 0.3 + 0.02 * r;
647
+ if (dr > lim)
648
+ dr = lim;
649
+ else if (dr < -lim)
650
+ dr = -lim;
651
+ if (dth > 0.06)
652
+ dth = 0.06;
653
+ else if (dth < -0.06)
654
+ dth = -0.06;
655
+ if (dph > 0.3)
656
+ dph = 0.3;
657
+ else if (dph < -0.3)
658
+ dph = -0.3;
659
+ r += dr;
660
+ th += dth;
661
+ if (r < 0) {
662
+ r = -r;
663
+ th = Math.PI - th;
664
+ cph = -cph;
665
+ sph = -sph;
666
+ }
667
+ if (r > RMAX * 0.98)
668
+ r = RMAX * 0.98;
669
+ if (th < 0) {
670
+ th = -th;
671
+ cph = -cph;
672
+ sph = -sph;
673
+ }
674
+ else if (th > Math.PI) {
675
+ th = 2 * Math.PI - th;
676
+ cph = -cph;
677
+ sph = -sph;
678
+ }
679
+ // rotate (cos phi, sin phi) by dph (second order) and renormalise
680
+ const qc = 1 - 0.5 * dph * dph, qs = dph - dph * dph * dph / 6;
681
+ let nc = cph * qc - sph * qs, ns = sph * qc + cph * qs;
682
+ const nl = 1 / Math.sqrt(nc * nc + ns * ns);
683
+ nc *= nl;
684
+ ns *= nl;
685
+ pr[i] = r;
686
+ pth[i] = th;
687
+ pc[i] = nc;
688
+ psn[i] = ns;
689
+ // body -> world (y is the quantisation axis), sim units
690
+ const rsn = r * sT * RSi;
691
+ const bx = rsn * nc, by = r * cT * RSi, bz = rsn * ns;
692
+ let x = q00 * bx + q01 * by + q02 * bz, y = q10 * bx + q11 * by + q12 * bz, z = q20 * bx + q21 * by + q22 * bz;
693
+ if (measuring) {
694
+ const ex = x - hp.x, ey = y - hp.y, ez = z - hp.z;
695
+ if (ex * ex + ey * ey + ez * ez < Rd2) {
696
+ inWin++;
697
+ if (clickRng() * inWin < 1)
698
+ pick = i;
699
+ }
700
+ }
701
+ if (kap > 0.003) {
702
+ // collapse map: a smooth, invertible contraction toward the detected point
703
+ const ex = x - cx, ey = y - cy, ez = z - cz;
704
+ const e2 = ex * ex + ey * ey + ez * ez;
705
+ if (e2 < 9 * Rc2) {
706
+ const f = 1 - kap * Math.exp(-e2 / Rc2);
707
+ x = cx + ex * f;
708
+ y = cy + ey * f;
709
+ z = cz + ez * f;
710
+ }
711
+ }
712
+ const j = i * 3;
713
+ vel[j] = (x - pos[j]) / h;
714
+ vel[j + 1] = (y - pos[j + 1]) / h;
715
+ vel[j + 2] = (z - pos[j + 2]) / h;
716
+ pos[j] = x;
717
+ pos[j + 1] = y;
718
+ pos[j + 2] = z;
719
+ }
720
+ // detector click: rate ~ efficiency x probability in the window
721
+ if (measuring && inWin > 0 && pick >= 0) {
722
+ const P = inWin / n;
723
+ const rate = 60 * S * P;
724
+ if (clickRng() < rate * h && clickAge > 0.25) {
725
+ const j = pick * 3;
726
+ col.x = pos[j];
727
+ col.y = pos[j + 1];
728
+ col.z = pos[j + 2];
729
+ col.t = 0.88;
730
+ clickX = col.x;
731
+ clickY = col.y;
732
+ clickZ = col.z;
733
+ clickAge = 0;
734
+ }
735
+ }
736
+ u.uCollapse.value.set(col.x, col.y, col.z, col.k);
737
+ // spontaneous-emission photons
738
+ while (emitAcc >= 1) {
739
+ emitAcc -= 1;
740
+ const k = phHead % MAX_PHOT;
741
+ phHead++;
742
+ let dx = clickRng() * 2 - 1, dy = clickRng() * 2 - 1, dz = clickRng() * 2 - 1;
743
+ const l = Math.hypot(dx, dy, dz) || 1;
744
+ dx /= l;
745
+ dy /= l;
746
+ dz /= l;
747
+ PH[k * 7] = dx * 0.12;
748
+ PH[k * 7 + 1] = dy * 0.12;
749
+ PH[k * 7 + 2] = dz * 0.12;
750
+ PH[k * 7 + 3] = dx * 1.4;
751
+ PH[k * 7 + 4] = dy * 1.4;
752
+ PH[k * 7 + 5] = dz * 1.4;
753
+ PH[k * 7 + 6] = 0;
754
+ }
755
+ for (let k = 0; k < MAX_PHOT; k++) {
756
+ if (PH[k * 7 + 6] > 1.5)
757
+ continue;
758
+ PH[k * 7 + 6] += h;
759
+ PH[k * 7] += PH[k * 7 + 3] * h;
760
+ PH[k * 7 + 1] += PH[k * 7 + 4] * h;
761
+ PH[k * 7 + 2] += PH[k * 7 + 5] * h;
762
+ }
763
+ }
764
+ const stepper = createFixedStep(H_STEP, 4);
765
+ for (let s = 0; s < 3; s++)
766
+ step(H_STEP); // fill velocities
767
+ // ---- geometry ----
768
+ const geo = new THREE.BufferGeometry();
769
+ const posAttr = new THREE.BufferAttribute(pos, 3).setUsage(THREE.DynamicDrawUsage);
770
+ const phAttr = new THREE.BufferAttribute(phase, 2).setUsage(THREE.DynamicDrawUsage);
771
+ geo.setAttribute('position', posAttr);
772
+ geo.setAttribute('aPhase', phAttr);
773
+ geo.setAttribute('aSeed', new THREE.BufferAttribute(seedA, 1));
774
+ const mat = new THREE.ShaderMaterial({
775
+ uniforms: u, vertexShader: partVertex, fragmentShader: partFragment,
776
+ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
777
+ });
778
+ const parts = new THREE.Points(geo, mat);
779
+ parts.frustumCulled = false;
780
+ attachViewH(parts, u);
781
+ const volGeo = new THREE.SphereGeometry(BOUNDS.radius, 40, 24);
782
+ const volMat = new THREE.ShaderMaterial({
783
+ uniforms: u, vertexShader: volVertex, fragmentShader: volFragment,
784
+ transparent: true, depthWrite: false, depthTest: false, side: THREE.BackSide, blending: THREE.AdditiveBlending,
785
+ });
786
+ const haze = new THREE.Mesh(volGeo, volMat);
787
+ haze.frustumCulled = false;
788
+ const NG = 2 + MAX_PHOT;
789
+ const gp = new Float32Array(NG * 3), ga = new Float32Array(NG * 4);
790
+ const gpAttr = new THREE.BufferAttribute(gp, 3).setUsage(THREE.DynamicDrawUsage);
791
+ const gaAttr = new THREE.BufferAttribute(ga, 4).setUsage(THREE.DynamicDrawUsage);
792
+ const ggeo = new THREE.BufferGeometry();
793
+ ggeo.setAttribute('position', gpAttr);
794
+ ggeo.setAttribute('aG', gaAttr);
795
+ const gmat = new THREE.ShaderMaterial({
796
+ uniforms: u, vertexShader: glyphVertex, fragmentShader: glyphFragment,
797
+ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
798
+ });
799
+ const glyphs = new THREE.Points(ggeo, gmat);
800
+ glyphs.frustumCulled = false;
801
+ const object = new THREE.Group();
802
+ const proxy = createPickProxy(BOUNDS.center, BOUNDS.radius);
803
+ object.add(haze, parts, glyphs, proxy.mesh);
804
+ const anchor = new THREE.Vector3(0, 0, 0);
805
+ function uploadGlyphs() {
806
+ ga.set([0, 0, 1, 0], 0); // nucleus
807
+ gp[3] = clickX;
808
+ gp[4] = clickY;
809
+ gp[5] = clickZ; // detector click
810
+ ga.set([1, Math.min(clickAge / 0.6, 1), clickAge < 0.6 ? 1 : 0, 0], 4);
811
+ for (let k = 0; k < MAX_PHOT; k++) { // spontaneous-emission photons
812
+ const g = 2 + k;
813
+ gp[g * 3] = PH[k * 7];
814
+ gp[g * 3 + 1] = PH[k * 7 + 1];
815
+ gp[g * 3 + 2] = PH[k * 7 + 2];
816
+ const age = PH[k * 7 + 6];
817
+ ga.set([2, Math.min(age / 1.5, 1), age < 1.5 ? 1 : 0, 0], g * 4);
818
+ }
819
+ gpAttr.needsUpdate = true;
820
+ gaAttr.needsUpdate = true;
821
+ }
822
+ // ---- trails ----
823
+ const trailGLSL = /* glsl */ `
824
+ uniform vec3 uColor;
825
+ vec3 trailShade(vec3 p, float f) { return uColor * 0.7; }
826
+ vec3 trailView(vec3 mv, out float mag) { mag = 1.0; return mv; }
827
+ `;
828
+ const materials = [mat, volMat, gmat];
829
+ let trail = null, trailKey = '';
830
+ function buildTrails(mode, share) {
831
+ const key = mode + ':' + share;
832
+ if (key === trailKey)
833
+ return;
834
+ trailKey = key;
835
+ if (trail) {
836
+ object.remove(trail.object);
837
+ trail.dispose();
838
+ trail = null;
839
+ }
840
+ materials.length = 3;
841
+ trail = createCpuTrail(mode, { u, n, pos, vel, glsl: trailGLSL, sizePx: 0.0035, spacing: 0.0025 });
842
+ if (!trail)
843
+ return;
844
+ object.add(trail.object);
845
+ materials.push(...trail.materials);
846
+ }
847
+ buildTrails(c.trailMode, c.trailShare);
848
+ uploadGlyphs();
849
+ return {
850
+ object,
851
+ materials,
852
+ apply(cfg) { if (cfg)
853
+ buildTrails(cfg.trailMode, cfg.trailShare); },
854
+ update(dt, camera, pointer) {
855
+ const rdt = Math.min(Math.max(dt, 0), 1 / 20);
856
+ if (camera && !inView(object, camera, BOUNDS.center, BOUNDS.radius, u.uSize.value))
857
+ return;
858
+ const d = rdt * u.uSpeed.value;
859
+ u.uTime.value += d;
860
+ if (camera) {
861
+ object.worldToLocal(u.uCamLocal.value.copy(camera.position)).divideScalar(u.uSize.value);
862
+ if (pointer)
863
+ stepPointerField(u, pointer, camera, object, u.uSize.value, rdt, anchor);
864
+ }
865
+ u.uVelScale.value = u.uSpeed.value;
866
+ let simD = 0;
867
+ stepper.run(d, (hh) => { step(hh); simD += hh; });
868
+ if (simD > 0) {
869
+ posAttr.needsUpdate = true;
870
+ phAttr.needsUpdate = true;
871
+ uploadGlyphs();
872
+ if (trail)
873
+ trail.update(simD);
874
+ }
875
+ },
876
+ seek(t) { for (let s = 0; s < Math.round(t / H_STEP); s++)
877
+ step(H_STEP); posAttr.needsUpdate = true; phAttr.needsUpdate = true; },
878
+ dispose() {
879
+ geo.dispose();
880
+ mat.dispose();
881
+ volGeo.dispose();
882
+ volMat.dispose();
883
+ ggeo.dispose();
884
+ gmat.dispose();
885
+ tables.dispose();
886
+ proxy.dispose();
887
+ if (trail)
888
+ trail.dispose();
889
+ },
890
+ _state: { pos, vel, phase, pr },
891
+ };
892
+ }
893
+ /** Defaults for every knob, so an older saved config still renders (identical to the catalog entry). */
894
+ const DEFAULTS = {
895
+ intensity: 1,
896
+ speed: 1,
897
+ opacity: 0.9,
898
+ size: 1,
899
+ color: '#6fd6ff',
900
+ particleCount: 24000,
901
+ stateA: '4f+2',
902
+ stateB: '3d+1',
903
+ mix: 0.35,
904
+ timeScale: 1,
905
+ phaseColor: '#ff6ad5',
906
+ cloudDensity: 0.7,
907
+ phaseFronts: 1,
908
+ pointSize: 0.8,
909
+ seed: 1,
910
+ quality: 'medium',
911
+ pointerMode: 'photon',
912
+ pointerStrength: 1,
913
+ pointerRadius: 1,
914
+ pointerColor: '#fff2c4',
915
+ trailMode: 'comet',
916
+ trailLength: 2.5,
917
+ trailOpacity: 0.45,
918
+ trailColor: '#7a8cff',
919
+ trailShare: 0.3,
920
+ };
921
+ const ElectronOrbitalsParticles = ({ size, config, animations, onObjectReady, onObjectDispose }) => {
922
+ const rootRef = useRef(null);
923
+ const animCache = useRef({});
924
+ useObjectReady(rootRef, config, onObjectReady, onObjectDispose);
925
+ const s = size > 0 ? size : 1;
926
+ const cfg = useMemo(() => ({ ...DEFAULTS, ...config }), [config]);
927
+ // uniforms outlive the objects, so keyframe animations survive a rebuild
928
+ const uniforms = useMemo(() => createUniforms(), []);
929
+ // rebuilt only when a structural knob changes (trails rebuild inside apply())
930
+ const instance = useMemo(() => build(uniforms, cfg, s), [uniforms, cfg.particleCount, cfg.stateA, cfg.stateB, cfg.seed, cfg.quality]);
931
+ useEffect(() => () => instance.dispose(), [instance]);
932
+ useLayoutEffect(() => {
933
+ applyConfig(uniforms, cfg, s);
934
+ instance.apply(cfg);
935
+ }, [instance, uniforms, cfg, s]);
936
+ const pointer = usePointerField();
937
+ useFrame((state, delta) => {
938
+ instance.update(Math.min(delta, 0.1), state.camera, pointer);
939
+ if (animations?.length)
940
+ applyGenerativeAnimations(instance.materials, animations, animCache.current, Date.now(), false);
941
+ });
942
+ return (_jsx("group", { ref: rootRef, children: _jsx("primitive", { object: instance.object }) }));
943
+ };
944
+ export default ElectronOrbitalsParticles;