@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,744 @@
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
+ * Grinder Sparks — Slow-motion grinder sparks: drag-limited ballistics, radiative cooling on the blackbody scale, carbon bursts, floor bounces and reflections; the cursor moves the grinder, aims the spray or sweeps the air. Use for industrial and cinematic scenes.
11
+ */
12
+ // Grinder Sparks — an angle grinder biting steel, filmed in slow motion.
13
+ // Movement: each spark is a burning steel grain thrown tangentially off the wheel rim. Quadratic
14
+ // air drag a = -k |v| v with k = 3 rho_air C_d / (4 rho_steel d) (small grains stop sooner),
15
+ // gravity, and floor bounces with restitution and skid friction. Carbon in the steel burns
16
+ // to CO inside the grain and bursts it: the spark forks into a star of short, brighter
17
+ // sparklets that can fork again — the "spark test" that tells steels apart (`carbon`).
18
+ // Colour: temperature on the Planck locus. While a grain burns, oxidation holds it near its
19
+ // launch temperature; after burn-out it cools by radiation (eps sigma T^4) and convection
20
+ // (Nu = 2), sliding from white-yellow to orange to cherry red; visible radiance
21
+ // ~ exp(-c2 / (lambda T)) sets brightness, so it fades out around 1000 K. The floor is lit
22
+ // by the contact point (cos / d^2) and mirrors the sparks; the cut glows and cools behind
23
+ // the wheel.
24
+ // Mouse (pointerMode) — the cursor moves the grinder or the sparks; nothing is drawn at the cursor:
25
+ // wheel — the grinder follows the cursor: the contact point moves along a camera-facing plane,
26
+ // the spark rate follows Preston's law (removal ~ pressure x speed), and the cut behind
27
+ // it glows and cools.
28
+ // aim — the grinder stays put and turns: the spray fan swings toward the cursor, and pushing
29
+ // harder (a faster swing) throws more sparks.
30
+ // sweep — the cursor's motion stirs the air: a moving-body potential flow plus a trailing wake
31
+ // (in the slowed-down physics time) drags the sparks into swirls, and burning grains in the
32
+ // wake get extra oxygen and flare hotter.
33
+ // Trails (trailMode): streak = long exposure (the classic grinder photo), comet = tracer arcs,
34
+ // echo = smoulder. Trails take the spark's temperature (TRAIL_K) so they cool with it.
35
+ //
36
+ // Generated from the approved lab module (particle lab, batch 2). Sim units = object-local / size.
37
+ /** mulberry32 — seeded PRNG, returns a function giving floats in [0,1). */
38
+ function mulberry32(seed) {
39
+ let a = seed >>> 0;
40
+ return function () {
41
+ a = (a + 0x6d2b79f5) >>> 0;
42
+ let t = a;
43
+ t = Math.imul(t ^ (t >>> 15), t | 1);
44
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
45
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
46
+ };
47
+ }
48
+ /** Standard normal from a uniform PRNG (Box–Muller, one value per call). */
49
+ function gauss(rng) {
50
+ const u = Math.max(rng(), 1e-9);
51
+ const v = rng();
52
+ return Math.sqrt(-2 * Math.log(u)) * Math.cos(6.283185307 * v);
53
+ }
54
+ /** Viewport-height hook so point sizes are pixel-correct at any canvas size. */
55
+ function attachViewH(obj, u) {
56
+ const scratch = new THREE.Vector2();
57
+ obj.onBeforeRender = (renderer) => {
58
+ renderer.getDrawingBufferSize(scratch);
59
+ u.uViewH.value = scratch.y;
60
+ };
61
+ }
62
+ function colorUniform(hex) {
63
+ return { value: new THREE.Color(hex) };
64
+ }
65
+ /** Tanner Helland blackbody fit, t in kelvin -> linear-ish RGB. */
66
+ const BLACKBODY_GLSL = /* glsl */ `
67
+ vec3 blackbody(float tK) {
68
+ float t = clamp(tK, 1000.0, 40000.0) / 100.0;
69
+ float r = t <= 66.0 ? 1.0 : clamp(1.29293618606 * pow(t - 60.0, -0.1332047592), 0.0, 1.0);
70
+ float g = t <= 66.0 ? clamp(0.39008157876 * log(t) - 0.63184144378, 0.0, 1.0)
71
+ : clamp(1.12989086089 * pow(t - 60.0, -0.0755148492), 0.0, 1.0);
72
+ float b = t >= 66.0 ? 1.0 : (t <= 19.0 ? 0.0 : clamp(0.54320678911 * log(t - 10.0) - 1.19625408914, 0.0, 1.0));
73
+ return pow(vec3(r, g, b), vec3(2.2));
74
+ }
75
+ `;
76
+ /** Quality levels scale counts, raymarch samples and trail caps. Structural. */
77
+ const QUALITY_SCALE = { low: 0.5, medium: 1, high: 1.6 };
78
+ function qualityScale(q) { return QUALITY_SCALE[q] || 1; }
79
+ /** Mix the object's own base seed with the `seed` knob into a mulberry32 seed. */
80
+ function seedMix(base, seed) {
81
+ let h = (base ^ Math.imul((Math.round(seed) | 0) + 0x9e37, 0x85ebca6b)) >>> 0;
82
+ h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35) >>> 0;
83
+ return (h ^ (h >>> 16)) >>> 0;
84
+ }
85
+ /** Fixed-step clock: the same motion at 30, 60 or 144 Hz. `run(d, step)` advances d sim-seconds
86
+ * in steps of exactly H (at most maxSteps per call; a longer backlog is dropped, not caught up). */
87
+ function createFixedStep(H, maxSteps) {
88
+ let acc = 0;
89
+ return {
90
+ H,
91
+ run(d, step) {
92
+ acc += Math.max(d, 0);
93
+ let k = 0;
94
+ while (acc >= H && k < maxSteps) {
95
+ step(H);
96
+ acc -= H;
97
+ k++;
98
+ }
99
+ if (k === maxSteps)
100
+ acc = Math.min(acc, H);
101
+ return acc / H; // leftover fraction, for render interpolation
102
+ },
103
+ reset() { acc = 0; },
104
+ };
105
+ }
106
+ /** Selection bounds: an invisible sphere the editor can pick, frame and size (never rendered,
107
+ * still raycastable and counted by Box3.setFromObject). */
108
+ function createPickProxy(center, radius) {
109
+ const geo = new THREE.SphereGeometry(1, 16, 10);
110
+ const mat = new THREE.MeshBasicMaterial({ visible: false });
111
+ const mesh = new THREE.Mesh(geo, mat);
112
+ mesh.name = 'pickProxy';
113
+ mesh.position.set(center[0], center[1], center[2]);
114
+ mesh.scale.setScalar(radius);
115
+ return { mesh, dispose() { geo.dispose(); mat.dispose(); } };
116
+ }
117
+ const _frustum = new THREE.Frustum();
118
+ const _pm = new THREE.Matrix4();
119
+ const _sphere = new THREE.Sphere();
120
+ /** Off-screen test: false when the object's bounding sphere is outside the camera frustum. */
121
+ function inView(object, camera, center, radius, size) {
122
+ if (!camera || !camera.projectionMatrix)
123
+ return true;
124
+ object.updateWorldMatrix(true, false);
125
+ _sphere.center.set(center[0], center[1], center[2]).multiplyScalar(size).applyMatrix4(object.matrixWorld);
126
+ _sphere.radius = radius * size * object.matrixWorld.getMaxScaleOnAxis();
127
+ _pm.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
128
+ _frustum.setFromProjectionMatrix(_pm);
129
+ return _frustum.intersectsSphere(_sphere);
130
+ }
131
+ const POINTER_MODES = ['wheel', 'aim', 'sweep'];
132
+ const SLOWMO = 0.2; // physics seconds per shown second (a high-speed camera)
133
+ const H_STEP = 1 / 120; // shown-time step (physics step = H_STEP * SLOWMO)
134
+ const TR = 0.006; // physics seconds per trail unit (the shutter) for streaks
135
+ const TR_MODE = { streak: TR, comet: 0.03, echo: 0.04 }; // arcs and smoulder look back further
136
+ const C0 = [-0.8, 0.95, 0]; // contact point at rest (m)
137
+ const SPRAY_MID = [0.1, 0.6, 0];
138
+ const BOUNDS = { center: [0.2, 0.7, 0], radius: 2.3 };
139
+ const RHO_C = 7800 * 600; // steel density x heat capacity
140
+ const SIGMA = 5.67e-8, EPS = 0.8, T_AIR = 300;
141
+ const EL0 = -0.26; // rest elevation of the fan (rad)
142
+ const COOL = 0.22; // effective cooling scale (grains keep oxide shells hot)
143
+ const NK = 96; // cut heat-trail points
144
+ function createUniforms() {
145
+ return {
146
+ uTime: { value: 0 },
147
+ uSize: { value: 1 },
148
+ uViewH: { value: 800 },
149
+ uIntensity: { value: 1 },
150
+ uSpeed: { value: 1 },
151
+ uOpacity: { value: 0.9 },
152
+ uColor: colorUniform('#ffffff'),
153
+ uParticleCount: { value: 0 },
154
+ uSparkRate: { value: 1 },
155
+ uWheelSpeed: { value: 45 },
156
+ uSprayAngle: { value: 14 },
157
+ uCarbon: { value: 0.5 },
158
+ uTemperature: { value: 2000 },
159
+ uExposure: { value: 1 },
160
+ uFloorReflect: { value: 0.35 },
161
+ uEmberColor: colorUniform('#ffffff'),
162
+ uPointerMode: { value: 0 },
163
+ uPointerStrength: { value: 1 },
164
+ uPointerRadius: { value: 1 },
165
+ uPointerColor: colorUniform('#ffffff'),
166
+ uContact: { value: new THREE.Vector4(C0[0], C0[1], C0[2], 0) }, // xyz, glow
167
+ ...createPointerUniforms(),
168
+ ...createTrailUniforms(),
169
+ };
170
+ }
171
+ function applyConfig(u, c, size) {
172
+ u.uSize.value = size;
173
+ u.uIntensity.value = c.intensity;
174
+ u.uSpeed.value = c.speed;
175
+ u.uOpacity.value = c.opacity;
176
+ u.uColor.value.set(c.color);
177
+ u.uParticleCount.value = c.particleCount;
178
+ u.uSparkRate.value = c.sparkRate;
179
+ u.uWheelSpeed.value = c.wheelSpeed;
180
+ u.uSprayAngle.value = c.sprayAngle;
181
+ u.uCarbon.value = c.carbon;
182
+ u.uTemperature.value = c.temperature;
183
+ u.uExposure.value = c.exposure;
184
+ u.uFloorReflect.value = c.floorReflect;
185
+ u.uEmberColor.value.set(c.emberColor);
186
+ u.uPointerMode.value = POINTER_MODES.indexOf(c.pointerMode); // -1 = 'none' (off)
187
+ u.uPointerStrength.value = c.pointerStrength;
188
+ u.uPointerRadius.value = c.pointerRadius;
189
+ u.uPointerColor.value.set(c.pointerColor);
190
+ applyTrailConfig(u, c);
191
+ }
192
+ const BASE_U = 'uniform float uSize, uViewH, uIntensity, uOpacity;';
193
+ // Spark light: blackbody colour, visible-band radiance ~ exp(-c / T) normalised at 2000 K.
194
+ const HEAT_GLSL = /* glsl */ `
195
+ ${BLACKBODY_GLSL}
196
+ uniform vec3 uColor, uEmberColor;
197
+ uniform float uExposure;
198
+ vec3 heatLight(float T) {
199
+ if (T < 600.0) return vec3(0.0);
200
+ float b = exp(-5500.0 / uExposure * (1.0 / T - 1.0 / 2000.0));
201
+ vec3 c = blackbody(T);
202
+ c /= max(max(c.r, c.g), max(c.b, 1e-3));
203
+ c = mix(c, c * uColor, smoothstep(1700.0, 2300.0, T)); // hottest sparks take the tint
204
+ c = mix(c, uEmberColor, 0.55 * smoothstep(1350.0, 950.0, T)); // dull red embers take the ember tint
205
+ // film response: bright sparks saturate toward white like an overexposed photo
206
+ vec3 E = c * b * 2.4;
207
+ return (vec3(1.0) - exp(-E)) * (1.0 + 0.6 * smoothstep(1.2, 3.0, b));
208
+ }
209
+ `;
210
+ function sparkVertex(mirror) {
211
+ return /* glsl */ `
212
+ ${BASE_U}
213
+ ${HEAT_GLSL}
214
+ uniform float uFloorReflect;
215
+ attribute float aT;
216
+ attribute float aD;
217
+ varying vec3 vCol;
218
+ varying float vA;
219
+ void main() {
220
+ vec3 p = position;
221
+ ${mirror ? 'p.y = -p.y;' : ''}
222
+ vec3 L = heatLight(aT);
223
+ vec4 mv = modelViewMatrix * vec4(p * uSize, 1.0);
224
+ float lum = dot(L, vec3(0.3, 0.5, 0.2));
225
+ float px = uSize * uViewH * (0.0045 + 0.004 * aD) * (1.0 + 0.8 * min(lum, 2.0)) / -mv.z;
226
+ ${mirror ? 'px *= 1.6; float fade = uFloorReflect * 0.45 * exp(-position.y * 1.2) * smoothstep(3.0, 1.5, length(position.xz - vec2(0.4, 0.0)));' : 'float fade = 1.0;'}
227
+ vCol = L * uIntensity;
228
+ vA = uOpacity * fade * step(600.0, aT);
229
+ gl_PointSize = clamp(px, 1.0, 40.0) * step(0.001, vA);
230
+ gl_Position = projectionMatrix * mv;
231
+ }
232
+ `;
233
+ }
234
+ const sparkFragment = /* glsl */ `
235
+ varying vec3 vCol;
236
+ varying float vA;
237
+ void main() {
238
+ vec2 d = gl_PointCoord * 2.0 - 1.0;
239
+ float r2 = dot(d, d);
240
+ if (r2 > 1.0) discard;
241
+ float a = (exp(-r2 * 8.0) + 0.3 * exp(-r2 * 2.2)) * (1.0 - r2) * vA;
242
+ if (a < 0.002) discard;
243
+ gl_FragColor = vec4(vCol, a);
244
+ }
245
+ `;
246
+ // Floor: light from the contact point (Lambert, cos / d^2) — the pool the sparks rain into.
247
+ const floorVertex = /* glsl */ `
248
+ uniform float uSize;
249
+ varying vec3 vP;
250
+ void main() {
251
+ vP = position;
252
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position * uSize, 1.0);
253
+ }
254
+ `;
255
+ const floorFragment = /* glsl */ `
256
+ ${BASE_U}
257
+ ${BLACKBODY_GLSL}
258
+ uniform vec4 uContact;
259
+ uniform vec3 uColor;
260
+ varying vec3 vP;
261
+ void main() {
262
+ vec3 d = uContact.xyz - vP;
263
+ float r2 = dot(d, d);
264
+ float E = uContact.w * max(d.y, 0.0) / pow(r2 + 0.02, 1.5);
265
+ vec3 c = blackbody(2100.0); c /= max(max(c.r, c.g), c.b);
266
+ float edge = smoothstep(2.8, 1.8, length(vP.xz - vec2(0.3, 0.0)));
267
+ vec3 rgb = c * uColor * E * 0.14 * uIntensity * edge;
268
+ float I = max(max(rgb.r, rgb.g), rgb.b);
269
+ if (I < 0.001) discard;
270
+ I = min(I, 1.0);
271
+ gl_FragColor = vec4(rgb / max(I, 1e-4), I * uOpacity);
272
+ }
273
+ `;
274
+ // Glyphs: the contact glow and the cooling cut behind the wheel (nothing is drawn at the cursor).
275
+ const glyphVertex = /* glsl */ `
276
+ ${BASE_U}
277
+ ${HEAT_GLSL}
278
+ attribute vec3 aG; // kind, temperature, strength
279
+ varying vec3 vCol;
280
+ varying float vA;
281
+ varying float vKind;
282
+ void main() {
283
+ vec4 mv = modelViewMatrix * vec4(position * uSize, 1.0);
284
+ float kind = aG.x;
285
+ float s;
286
+ if (kind < 0.5) { s = 0.05 + 0.05 * aG.z; vCol = heatLight(2300.0); } // contact glow
287
+ else { s = 0.018; vCol = heatLight(aG.y); } // the cooling cut
288
+ vA = aG.z * uOpacity;
289
+ vKind = kind;
290
+ vCol *= uIntensity;
291
+ gl_PointSize = clamp(uSize * uViewH * s / -mv.z, 0.0, 200.0) * step(0.001, vA);
292
+ gl_Position = projectionMatrix * mv;
293
+ }
294
+ `;
295
+ const glyphFragment = /* glsl */ `
296
+ varying vec3 vCol;
297
+ varying float vA;
298
+ varying float vKind;
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 = vKind < 0.5 ? exp(-r2 * 40.0) * 1.5 + exp(-r2 * 5.0) * 0.25 : exp(-r2 * 6.0) * 0.9;
304
+ a *= (1.0 - r2) * vA;
305
+ if (a < 0.003) discard;
306
+ gl_FragColor = vec4(vCol, a);
307
+ }
308
+ `;
309
+ function build(u, c, size) {
310
+ const q = qualityScale(c.quality);
311
+ const n = Math.max(64, Math.round(c.particleCount * q));
312
+ const rng = mulberry32(seedMix(0x5a4c, c.seed));
313
+ const pos = new Float32Array(n * 3), vel = new Float32Array(n * 3);
314
+ let trail = null, trailKey = '', trailTR = TR;
315
+ const T = new Float32Array(n); // temperature (K); 0 = not glowing
316
+ const Dm = new Float32Array(n); // grain diameter (m)
317
+ const aD = new Float32Array(n); // size seed for rendering
318
+ const burn = new Float32Array(n); // remaining burn time (physics s)
319
+ const burst = new Float32Array(n); // physics age of the carbon burst (1e9 = none)
320
+ const age = new Float32Array(n);
321
+ const gen = new Uint8Array(n);
322
+ const st = new Uint8Array(n); // 0 free, 1 flying, 2 holding (dead)
323
+ const hold = new Float32Array(n);
324
+ const free = new Int32Array(n);
325
+ let nFree = n;
326
+ for (let i = 0; i < n; i++)
327
+ free[i] = n - 1 - i;
328
+ // contact point (moves in wheel mode), cut heat trail
329
+ const cp = new THREE.Vector3(...C0);
330
+ const KP = new Float32Array(NK * 3), KB = new Float32Array(NK);
331
+ for (let k = 0; k < NK; k++)
332
+ KB[k] = -1e9;
333
+ let kHead = 0, kClock = 0;
334
+ const kLast = new THREE.Vector3(...C0);
335
+ let emitAcc = 0, glow = 1, physT = 0, strokeRate = 0;
336
+ const hp = new THREE.Vector3(), hv = new THREE.Vector3();
337
+ let aimAz = 0, aimEl = EL0;
338
+ function spawn(i, x, y, z, vx, vy, vz, T0, d, g) {
339
+ st[i] = 1;
340
+ age[i] = 0;
341
+ gen[i] = g;
342
+ pos[i * 3] = x;
343
+ pos[i * 3 + 1] = y;
344
+ pos[i * 3 + 2] = z;
345
+ vel[i * 3] = vx;
346
+ vel[i * 3 + 1] = vy;
347
+ vel[i * 3 + 2] = vz;
348
+ T[i] = T0;
349
+ Dm[i] = d;
350
+ // burn time grows with grain size (diffusion-limited oxidation)
351
+ burn[i] = g === 0 ? 0.22 * Math.pow(d / 1.2e-4, 1.5) * (0.7 + 0.6 * rng()) : 0.012 + 0.03 * rng();
352
+ const pBurst = g === 0 ? 0.85 * u.uCarbon.value : g === 1 ? 0.35 * u.uCarbon.value : 0;
353
+ burst[i] = rng() < pBurst ? burn[i] * (0.35 + 0.5 * rng()) : 1e9;
354
+ aD[i] = Math.min(d / 2.5e-4, 1.5);
355
+ if (trail)
356
+ trail.reset(i);
357
+ }
358
+ function step(hShown) {
359
+ const h = hShown * SLOWMO; // physics seconds
360
+ physT += h;
361
+ const mode = u.uPointerMode.value;
362
+ const S = u.uPointerStrength.value, act = u.uPointerActive.value, Rr = u.uPointerRadius.value;
363
+ hp.copy(u.uPointer.value);
364
+ hv.copy(u.uPointerVel.value);
365
+ const useWheel = mode === 0 && S > 0.01;
366
+ const useAim = mode === 1 && S > 0.01;
367
+ const useSweep = mode === 2 && S > 0.01 && act > 0.01;
368
+ // ---- contact point and spark rate (Preston: removal ~ pressure x relative speed) ----
369
+ let target = C0;
370
+ if (useWheel && act > 0.05)
371
+ target = [hp.x, Math.min(Math.max(hp.y, 0.25), 1.6), hp.z];
372
+ const lagK = Math.min(1, hShown * 10);
373
+ cp.x += (target[0] - cp.x) * lagK;
374
+ cp.y += (target[1] - cp.y) * lagK;
375
+ cp.z += (target[2] - cp.z) * lagK;
376
+ const stroke = useWheel || useAim ? hv.length() * act : 0;
377
+ strokeRate += (stroke - strokeRate) * Math.min(1, hShown * 4);
378
+ const pressure = useWheel || useAim ? 0.6 + 0.5 * S : 0.8;
379
+ // aim: the fan direction swings toward the cursor (azimuth about y, elevation), back to rest when idle
380
+ let tEl = EL0, tAz = 0;
381
+ if (useAim && act > 0.05) {
382
+ const dx = hp.x - cp.x, dy = hp.y - cp.y, dz = hp.z - cp.z;
383
+ const dl = Math.hypot(dx, dy, dz);
384
+ if (dl > 0.05) {
385
+ tEl = Math.max(-1.2, Math.min(0.9, Math.asin(dy / dl)));
386
+ tAz = Math.atan2(dz, dx);
387
+ }
388
+ }
389
+ let dAz = tAz - aimAz;
390
+ while (dAz > Math.PI)
391
+ dAz -= 2 * Math.PI;
392
+ while (dAz < -Math.PI)
393
+ dAz += 2 * Math.PI;
394
+ const aimK = Math.min(1, hShown * 5);
395
+ aimAz += dAz * aimK;
396
+ aimEl += (tEl - aimEl) * aimK;
397
+ const rate = 5000 * u.uSparkRate.value * pressure * (1 + 1.2 * strokeRate / (strokeRate + 0.6));
398
+ glow += (rate / 5000 - glow) * Math.min(1, hShown * 6);
399
+ u.uContact.value.set(cp.x, cp.y, cp.z, glow);
400
+ // cut heat trail: a point every ~1 cm of contact travel
401
+ kClock += hShown;
402
+ if (cp.distanceTo(kLast) > 0.012 || kClock > 0.5) {
403
+ KP[kHead * 3] = cp.x;
404
+ KP[kHead * 3 + 1] = cp.y;
405
+ KP[kHead * 3 + 2] = cp.z;
406
+ KB[kHead] = physT;
407
+ kHead = (kHead + 1) % NK;
408
+ kLast.copy(cp);
409
+ kClock = 0;
410
+ }
411
+ // ---- emission: tangential fan off the rim (the wheel turns in the x-y plane) ----
412
+ emitAcc += rate * h;
413
+ const vRim = u.uWheelSpeed.value;
414
+ const spread = (u.uSprayAngle.value * Math.PI) / 180;
415
+ const T0 = u.uTemperature.value;
416
+ while (emitAcc >= 1 && nFree > 0) {
417
+ emitAcc -= 1;
418
+ const i = free[--nFree];
419
+ const ang = aimEl + spread * gauss(rng) * 0.6; // the fan spreads in the wheel plane
420
+ const yaw = aimAz + 0.05 * gauss(rng); // thin across it
421
+ const sp = vRim * (0.55 + 0.45 * rng());
422
+ const d = 1.2e-4 * Math.exp(0.45 * gauss(rng));
423
+ const ca = Math.cos(ang), sa = Math.sin(ang);
424
+ spawn(i, cp.x + 0.004 * gauss(rng), cp.y + 0.004 * gauss(rng), cp.z + 0.004 * gauss(rng), sp * ca * Math.cos(yaw), sp * sa, sp * ca * Math.sin(yaw), T0 * (0.96 + 0.08 * rng()), d, 0);
425
+ }
426
+ if (emitAcc > 1)
427
+ emitAcc = 1;
428
+ // ---- air sweep: the cursor is a moving body in the air (velocity in physics time) ----
429
+ const mx = hp.x, my = hp.y, mz = hp.z;
430
+ const RS = 0.2 * Rr;
431
+ const AIR = 2.2 * S * act / SLOWMO; // a fast sweep (a hand or a board fanning the air)
432
+ const bvx = hv.x * AIR, bvy = hv.y * AIR, bvz = hv.z * AIR;
433
+ const bsp = Math.hypot(bvx, bvy, bvz), bn = bsp > 1e-4 ? 1 / bsp : 0;
434
+ const sweepOn = useSweep && bsp > 0.05;
435
+ for (let i = 0; i < n; i++) {
436
+ const s = st[i];
437
+ if (s === 0)
438
+ continue;
439
+ if (s === 2) { // dead: park until the trails forget it
440
+ hold[i] -= h;
441
+ if (hold[i] <= 0) {
442
+ st[i] = 0;
443
+ free[nFree++] = i;
444
+ }
445
+ continue;
446
+ }
447
+ const j = i * 3;
448
+ const d = Dm[i];
449
+ let t = T[i];
450
+ age[i] += h;
451
+ let x = pos[j], y = pos[j + 1], z = pos[j + 2];
452
+ let vx = vel[j], vy = vel[j + 1], vz = vel[j + 2];
453
+ // air: still, unless the sweep stirs it (moving-sphere dipole outside, carried inside, plus a wake)
454
+ let ax = 0, ay = 0, az = 0, flare = 0;
455
+ if (sweepOn) {
456
+ const qx = x - mx, qy = y - my, qz = z - mz;
457
+ const r2 = qx * qx + qy * qy + qz * qz;
458
+ if (r2 < 64 * RS * RS) {
459
+ const r = Math.sqrt(r2) + 1e-6;
460
+ if (r > RS) {
461
+ const k3 = (RS * RS * RS) / (2 * r2 * r);
462
+ const vr = (bvx * qx + bvy * qy + bvz * qz) / r;
463
+ ax = k3 * (3 * vr * qx / r - bvx);
464
+ ay = k3 * (3 * vr * qy / r - bvy);
465
+ az = k3 * (3 * vr * qz / r - bvz);
466
+ }
467
+ else {
468
+ ax = bvx;
469
+ ay = bvy;
470
+ az = bvz;
471
+ }
472
+ const behind = -(qx * bvx + qy * bvy + qz * bvz) * bn;
473
+ if (behind > 0) {
474
+ const perp2 = r2 - behind * behind;
475
+ const wk = Math.exp(-perp2 / (RS * RS * 1.5) - behind / (8 * RS));
476
+ ax += bvx * wk * 0.7;
477
+ ay += bvy * wk * 0.7;
478
+ az += bvz * wk * 0.7;
479
+ flare = wk; // fresh air feeds the burning grains
480
+ }
481
+ }
482
+ }
483
+ // quadratic drag relative to the air, semi-implicit
484
+ const k = 1.8 / (31200 * d);
485
+ const rx = vx - ax, ry = vy - ay, rz = vz - az;
486
+ const rv = Math.sqrt(rx * rx + ry * ry + rz * rz);
487
+ const damp = 1 / (1 + k * rv * h);
488
+ vx = ax + rx * damp;
489
+ vy = ay + ry * damp - 9.81 * h;
490
+ vz = az + rz * damp;
491
+ x += vx * h;
492
+ y += vy * h;
493
+ z += vz * h;
494
+ // floor: bounce with restitution, skid friction; conduction cools grains that touch it
495
+ let cool = 1;
496
+ if (y < 0) {
497
+ y = -y * 0.25;
498
+ vy = -vy * 0.25;
499
+ vx *= 0.5;
500
+ vz *= 0.5;
501
+ vz += (rng() - 0.5) * Math.abs(vx) * 0.4;
502
+ cool = 8;
503
+ }
504
+ else if (y < 0.01)
505
+ cool = 8; // lying on the floor: conduction drains it fast
506
+ // temperature: oxidation holds it hot while burning; then radiation + convection
507
+ if (burn[i] > 0) {
508
+ burn[i] -= h;
509
+ t += (T0 * 1.03 + 260 * flare - t) * Math.min(1, h / 0.02);
510
+ if (flare > 0.2)
511
+ burn[i] += h * flare * 0.8; // oxygen keeps it burning longer
512
+ }
513
+ else {
514
+ const t2 = t * t;
515
+ t -= h * cool * COOL * (6 / (RHO_C * d)) * (EPS * SIGMA * (t2 * t2 - 8.1e9) + (0.052 / d) * (t - T_AIR));
516
+ }
517
+ // carbon burst: the grain forks into a star of sparklets
518
+ if (age[i] >= burst[i]) {
519
+ burst[i] = 1e9;
520
+ const kids = 3 + Math.floor(rng() * 4);
521
+ for (let q = 0; q < kids && nFree > 0; q++) {
522
+ const cI = free[--nFree];
523
+ let dx = gauss(rng), dy = gauss(rng), dz = gauss(rng);
524
+ const dl = Math.hypot(dx, dy, dz) || 1;
525
+ const sp = 2 + 4 * rng();
526
+ dx = dx / dl * sp;
527
+ dy = dy / dl * sp;
528
+ dz = dz / dl * sp;
529
+ spawn(cI, x, y, z, vx * 0.6 + dx, vy * 0.6 + dy, vz * 0.6 + dz, Math.max(t, T0) + 220, d * 0.45, gen[i] + 1);
530
+ }
531
+ Dm[i] = d * 0.75;
532
+ t += 120;
533
+ }
534
+ T[i] = t;
535
+ pos[j] = x;
536
+ pos[j + 1] = y;
537
+ pos[j + 2] = z;
538
+ vel[j] = vx;
539
+ vel[j + 1] = vy;
540
+ vel[j + 2] = vz;
541
+ if (t < 700 || age[i] > 6 || x > 3.5 || x < -2.5 || Math.abs(z) > 3 || y > 3.5) {
542
+ st[i] = 2;
543
+ hold[i] = 0.05;
544
+ T[i] = 0;
545
+ vel[j] = 0;
546
+ vel[j + 1] = 0;
547
+ vel[j + 2] = 0;
548
+ }
549
+ }
550
+ }
551
+ const stepper = createFixedStep(H_STEP, 6);
552
+ for (let s = 0; s < 240; s++)
553
+ step(H_STEP); // two shown seconds: the spray is established
554
+ // ---- geometry ----
555
+ const geo = new THREE.BufferGeometry();
556
+ const posAttr = new THREE.BufferAttribute(pos, 3).setUsage(THREE.DynamicDrawUsage);
557
+ const tAttr = new THREE.BufferAttribute(T, 1).setUsage(THREE.DynamicDrawUsage);
558
+ const dAttr = new THREE.BufferAttribute(aD, 1).setUsage(THREE.DynamicDrawUsage);
559
+ geo.setAttribute('position', posAttr);
560
+ geo.setAttribute('aT', tAttr);
561
+ geo.setAttribute('aD', dAttr);
562
+ const mkSpark = (mirror) => new THREE.ShaderMaterial({
563
+ uniforms: u, vertexShader: sparkVertex(mirror), fragmentShader: sparkFragment,
564
+ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
565
+ });
566
+ const mat = mkSpark(false), rmat = mkSpark(true);
567
+ const sparks = new THREE.Points(geo, mat);
568
+ sparks.frustumCulled = false;
569
+ attachViewH(sparks, u);
570
+ const mirror = new THREE.Points(geo, rmat);
571
+ mirror.frustumCulled = false;
572
+ const fgeo = new THREE.CircleGeometry(2.8, 64);
573
+ fgeo.rotateX(-Math.PI / 2);
574
+ fgeo.translate(0.3, 0.002, 0);
575
+ const fmat = new THREE.ShaderMaterial({
576
+ uniforms: u, vertexShader: floorVertex, fragmentShader: floorFragment,
577
+ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
578
+ });
579
+ const floor = new THREE.Mesh(fgeo, fmat);
580
+ floor.frustumCulled = false;
581
+ const NG = 1 + NK;
582
+ const gp = new Float32Array(NG * 3), ga = new Float32Array(NG * 3);
583
+ const gpAttr = new THREE.BufferAttribute(gp, 3).setUsage(THREE.DynamicDrawUsage);
584
+ const gaAttr = new THREE.BufferAttribute(ga, 3).setUsage(THREE.DynamicDrawUsage);
585
+ const ggeo = new THREE.BufferGeometry();
586
+ ggeo.setAttribute('position', gpAttr);
587
+ ggeo.setAttribute('aG', gaAttr);
588
+ const gmat = new THREE.ShaderMaterial({
589
+ uniforms: u, vertexShader: glyphVertex, fragmentShader: glyphFragment,
590
+ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
591
+ });
592
+ const glyphs = new THREE.Points(ggeo, gmat);
593
+ glyphs.frustumCulled = false;
594
+ const object = new THREE.Group();
595
+ const proxy = createPickProxy(BOUNDS.center, BOUNDS.radius);
596
+ object.add(floor, mirror, sparks, glyphs, proxy.mesh);
597
+ const anchorWheel = new THREE.Vector3(...C0), anchorSpray = new THREE.Vector3(...SPRAY_MID);
598
+ function upload() {
599
+ posAttr.needsUpdate = true;
600
+ tAttr.needsUpdate = true;
601
+ dAttr.needsUpdate = true;
602
+ const mode = u.uPointerMode.value;
603
+ gp[0] = cp.x;
604
+ gp[1] = cp.y;
605
+ gp[2] = cp.z;
606
+ ga[0] = 0;
607
+ ga[1] = 2300;
608
+ ga[2] = Math.min(glow, 2.5);
609
+ for (let k = 0; k < NK; k++) {
610
+ const g = 1 + k;
611
+ gp[g * 3] = KP[k * 3];
612
+ gp[g * 3 + 1] = KP[k * 3 + 1];
613
+ gp[g * 3 + 2] = KP[k * 3 + 2];
614
+ const a = physT - KB[k];
615
+ const tk = T_AIR + (1350 - T_AIR) * Math.exp(-a / 0.35); // the cut cools behind the wheel
616
+ ga[g * 3] = 1;
617
+ ga[g * 3 + 1] = tk;
618
+ ga[g * 3 + 2] = a < 4 && mode === 0 ? 1 : 0;
619
+ }
620
+ gpAttr.needsUpdate = true;
621
+ gaAttr.needsUpdate = true;
622
+ }
623
+ // ---- trails: coloured by the spark's temperature ----
624
+ const trailGLSL = /* glsl */ `
625
+ ${HEAT_GLSL}
626
+ vec3 trailShade(vec3 p, float f) { return heatLight(TRAIL_K) * 2.2; }
627
+ vec3 trailView(vec3 mv, out float mag) { mag = 1.0; return mv; }
628
+ `;
629
+ const materials = [mat, rmat, fmat, gmat];
630
+ trailKey = '';
631
+ trailTR = TR;
632
+ function buildTrails(mode, share) {
633
+ trailTR = TR_MODE[mode] || TR;
634
+ const key = mode + ':' + share;
635
+ if (key === trailKey)
636
+ return;
637
+ trailKey = key;
638
+ if (trail) {
639
+ object.remove(trail.object);
640
+ trail.dispose();
641
+ trail = null;
642
+ }
643
+ materials.length = 4;
644
+ trail = createCpuTrail(mode, { u, n, pos, vel, k: T, glsl: trailGLSL, sizePx: 0.0055, spacing: 0.004, sub: 12 });
645
+ if (!trail)
646
+ return;
647
+ object.add(trail.object);
648
+ materials.push(...trail.materials);
649
+ }
650
+ buildTrails(c.trailMode, c.trailShare);
651
+ upload();
652
+ return {
653
+ object,
654
+ materials,
655
+ apply(cfg) { if (cfg)
656
+ buildTrails(cfg.trailMode, cfg.trailShare); },
657
+ update(dt, camera, pointer) {
658
+ const rdt = Math.min(Math.max(dt, 0), 1 / 20);
659
+ if (camera && !inView(object, camera, BOUNDS.center, BOUNDS.radius, u.uSize.value))
660
+ return;
661
+ const d = rdt * u.uSpeed.value;
662
+ u.uTime.value += d;
663
+ if (camera && pointer) {
664
+ const anchor = u.uPointerMode.value === 0 ? anchorWheel : anchorSpray;
665
+ stepPointerField(u, pointer, camera, object, u.uSize.value, rdt, anchor);
666
+ }
667
+ u.uVelScale.value = trailTR; // trails cover trailTR x trailLength physics seconds
668
+ let simD = 0;
669
+ stepper.run(d, (hh) => { step(hh); simD += hh; });
670
+ if (simD > 0) {
671
+ upload();
672
+ if (trail)
673
+ trail.update((simD * SLOWMO) / trailTR);
674
+ }
675
+ },
676
+ seek(t) { for (let s = 0; s < Math.round(t / H_STEP); s++)
677
+ step(H_STEP); upload(); },
678
+ dispose() {
679
+ geo.dispose();
680
+ mat.dispose();
681
+ rmat.dispose();
682
+ fgeo.dispose();
683
+ fmat.dispose();
684
+ ggeo.dispose();
685
+ gmat.dispose();
686
+ proxy.dispose();
687
+ if (trail)
688
+ trail.dispose();
689
+ },
690
+ _state: { pos, vel, T },
691
+ };
692
+ }
693
+ /** Defaults for every knob, so an older saved config still renders (identical to the catalog entry). */
694
+ const DEFAULTS = {
695
+ intensity: 1,
696
+ speed: 1,
697
+ opacity: 0.9,
698
+ size: 1,
699
+ color: '#fff1d6',
700
+ particleCount: 12000,
701
+ sparkRate: 1,
702
+ wheelSpeed: 35,
703
+ sprayAngle: 14,
704
+ carbon: 0.5,
705
+ temperature: 2000,
706
+ exposure: 1,
707
+ floorReflect: 0.35,
708
+ emberColor: '#ff3a12',
709
+ seed: 1,
710
+ quality: 'medium',
711
+ pointerMode: 'wheel',
712
+ pointerStrength: 1,
713
+ pointerRadius: 1,
714
+ pointerColor: '#ffd9a0',
715
+ trailMode: 'comet',
716
+ trailLength: 0.9,
717
+ trailOpacity: 0.9,
718
+ trailColor: '#ff7a2e',
719
+ trailShare: 1,
720
+ };
721
+ const GrinderSparksParticles = ({ size, config, animations, onObjectReady, onObjectDispose }) => {
722
+ const rootRef = useRef(null);
723
+ const animCache = useRef({});
724
+ useObjectReady(rootRef, config, onObjectReady, onObjectDispose);
725
+ const s = size > 0 ? size : 1;
726
+ const cfg = useMemo(() => ({ ...DEFAULTS, ...config }), [config]);
727
+ // uniforms outlive the objects, so keyframe animations survive a rebuild
728
+ const uniforms = useMemo(() => createUniforms(), []);
729
+ // rebuilt only when a structural knob changes (trails rebuild inside apply())
730
+ const instance = useMemo(() => build(uniforms, cfg, s), [uniforms, cfg.particleCount, cfg.seed, cfg.quality]);
731
+ useEffect(() => () => instance.dispose(), [instance]);
732
+ useLayoutEffect(() => {
733
+ applyConfig(uniforms, cfg, s);
734
+ instance.apply(cfg);
735
+ }, [instance, uniforms, cfg, s]);
736
+ const pointer = usePointerField();
737
+ useFrame((state, delta) => {
738
+ instance.update(Math.min(delta, 0.1), state.camera, pointer);
739
+ if (animations?.length)
740
+ applyGenerativeAnimations(instance.materials, animations, animCache.current, Date.now(), false);
741
+ });
742
+ return (_jsx("group", { ref: rootRef, children: _jsx("primitive", { object: instance.object }) }));
743
+ };
744
+ export default GrinderSparksParticles;