@3driseai/3drise-viewer 0.1.22 → 0.1.24

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 (102) hide show
  1. package/dist/components/CreatedObject.d.ts.map +1 -1
  2. package/dist/components/CreatedObject.js +6 -1
  3. package/dist/components/LightObject.d.ts.map +1 -1
  4. package/dist/components/LightObject.js +24 -13
  5. package/dist/components/ModelPrimitive.d.ts.map +1 -1
  6. package/dist/components/ModelPrimitive.js +4 -6
  7. package/dist/components/ObjectNode.js +1 -1
  8. package/dist/components/ParticlesGenerator.d.ts +3 -1
  9. package/dist/components/ParticlesGenerator.d.ts.map +1 -1
  10. package/dist/components/ParticlesGenerator.js +2 -2
  11. package/dist/components/ProjectLoader.js +1 -1
  12. package/dist/components/SceneBuilder.d.ts.map +1 -1
  13. package/dist/components/SceneBuilder.js +5 -1
  14. package/dist/components/candle/Candle.d.ts +14 -0
  15. package/dist/components/candle/Candle.d.ts.map +1 -0
  16. package/dist/components/candle/Candle.js +249 -0
  17. package/dist/components/candle/candleGeometry.d.ts +23 -0
  18. package/dist/components/candle/candleGeometry.d.ts.map +1 -0
  19. package/dist/components/candle/candleGeometry.js +73 -0
  20. package/dist/components/candle/candleMaterials.d.ts +24 -0
  21. package/dist/components/candle/candleMaterials.d.ts.map +1 -0
  22. package/dist/components/candle/candleMaterials.js +433 -0
  23. package/dist/components/candle/flameMotion.d.ts +64 -0
  24. package/dist/components/candle/flameMotion.d.ts.map +1 -0
  25. package/dist/components/candle/flameMotion.js +138 -0
  26. package/dist/components/candle/index.d.ts +9 -0
  27. package/dist/components/candle/index.d.ts.map +1 -0
  28. package/dist/components/candle/index.js +6 -0
  29. package/dist/components/fluorescentTube/FluorescentTube.d.ts +14 -0
  30. package/dist/components/fluorescentTube/FluorescentTube.d.ts.map +1 -0
  31. package/dist/components/fluorescentTube/FluorescentTube.js +262 -0
  32. package/dist/components/fluorescentTube/index.d.ts +5 -0
  33. package/dist/components/fluorescentTube/index.d.ts.map +1 -0
  34. package/dist/components/fluorescentTube/index.js +3 -0
  35. package/dist/components/fluorescentTube/tubeMaterials.d.ts +34 -0
  36. package/dist/components/fluorescentTube/tubeMaterials.d.ts.map +1 -0
  37. package/dist/components/fluorescentTube/tubeMaterials.js +185 -0
  38. package/dist/components/index.d.ts +4 -0
  39. package/dist/components/index.d.ts.map +1 -1
  40. package/dist/components/index.js +4 -0
  41. package/dist/components/ledArray/LedArray.d.ts +14 -0
  42. package/dist/components/ledArray/LedArray.d.ts.map +1 -0
  43. package/dist/components/ledArray/LedArray.js +243 -0
  44. package/dist/components/ledArray/index.d.ts +7 -0
  45. package/dist/components/ledArray/index.d.ts.map +1 -0
  46. package/dist/components/ledArray/index.js +4 -0
  47. package/dist/components/ledArray/ledLayout.d.ts +22 -0
  48. package/dist/components/ledArray/ledLayout.d.ts.map +1 -0
  49. package/dist/components/ledArray/ledLayout.js +59 -0
  50. package/dist/components/ledArray/ledMaterials.d.ts +14 -0
  51. package/dist/components/ledArray/ledMaterials.d.ts.map +1 -0
  52. package/dist/components/ledArray/ledMaterials.js +151 -0
  53. package/dist/components/lightBulb/LightBulb.d.ts.map +1 -1
  54. package/dist/components/lightBulb/LightBulb.js +10 -13
  55. package/dist/components/particles/BioluminescentWakeParticles.d.ts +3 -1
  56. package/dist/components/particles/BioluminescentWakeParticles.d.ts.map +1 -1
  57. package/dist/components/particles/BioluminescentWakeParticles.js +7 -3
  58. package/dist/components/particles/BrownianDustParticles.d.ts +3 -1
  59. package/dist/components/particles/BrownianDustParticles.d.ts.map +1 -1
  60. package/dist/components/particles/BrownianDustParticles.js +7 -3
  61. package/dist/components/particles/ChladniSandParticles.d.ts +3 -1
  62. package/dist/components/particles/ChladniSandParticles.d.ts.map +1 -1
  63. package/dist/components/particles/ChladniSandParticles.js +7 -3
  64. package/dist/components/particles/ElectronOrbitalsParticles.d.ts +3 -1
  65. package/dist/components/particles/ElectronOrbitalsParticles.d.ts.map +1 -1
  66. package/dist/components/particles/ElectronOrbitalsParticles.js +7 -3
  67. package/dist/components/particles/FaradayPowderParticles.d.ts +3 -1
  68. package/dist/components/particles/FaradayPowderParticles.d.ts.map +1 -1
  69. package/dist/components/particles/FaradayPowderParticles.js +7 -3
  70. package/dist/components/particles/GrinderSparksParticles.d.ts +3 -1
  71. package/dist/components/particles/GrinderSparksParticles.d.ts.map +1 -1
  72. package/dist/components/particles/GrinderSparksParticles.js +7 -3
  73. package/dist/components/particles/NeonRainParticles.d.ts +3 -1
  74. package/dist/components/particles/NeonRainParticles.d.ts.map +1 -1
  75. package/dist/components/particles/NeonRainParticles.js +7 -3
  76. package/dist/components/particles/TidalStreamParticles.d.ts +3 -1
  77. package/dist/components/particles/TidalStreamParticles.d.ts.map +1 -1
  78. package/dist/components/particles/TidalStreamParticles.js +7 -3
  79. package/dist/components/spotBeam/SpotBeam.d.ts +14 -0
  80. package/dist/components/spotBeam/SpotBeam.d.ts.map +1 -0
  81. package/dist/components/spotBeam/SpotBeam.js +244 -0
  82. package/dist/components/spotBeam/beamPatterns.d.ts +11 -0
  83. package/dist/components/spotBeam/beamPatterns.d.ts.map +1 -0
  84. package/dist/components/spotBeam/beamPatterns.js +106 -0
  85. package/dist/components/spotBeam/index.d.ts +7 -0
  86. package/dist/components/spotBeam/index.d.ts.map +1 -0
  87. package/dist/components/spotBeam/index.js +4 -0
  88. package/dist/components/spotBeam/spotBeamMaterials.d.ts +16 -0
  89. package/dist/components/spotBeam/spotBeamMaterials.d.ts.map +1 -0
  90. package/dist/components/spotBeam/spotBeamMaterials.js +307 -0
  91. package/dist/hooks/index.d.ts +2 -0
  92. package/dist/hooks/index.d.ts.map +1 -1
  93. package/dist/hooks/index.js +1 -0
  94. package/dist/hooks/useLightConfig.d.ts +25 -0
  95. package/dist/hooks/useLightConfig.d.ts.map +1 -0
  96. package/dist/hooks/useLightConfig.js +125 -0
  97. package/dist/hooks/useLightLive.d.ts.map +1 -1
  98. package/dist/hooks/useLightLive.js +12 -2
  99. package/dist/hooks/useShadowApplication.d.ts +4 -4
  100. package/dist/hooks/useShadowApplication.d.ts.map +1 -1
  101. package/dist/hooks/useShadowApplication.js +25 -10
  102. package/package.json +2 -2
@@ -0,0 +1,243 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // =============================================================================
3
+ // LedArray — a point light drawn as the dies that make it
4
+ // =============================================================================
5
+ //
6
+ // Rendered by LightObject for `config.type === 'ledArray'`. Like spotBeam, this
7
+ // is a light rather than a lamp: the geometry is the emitters themselves (and
8
+ // can be switched off), because the object is meant to be dropped into whatever
9
+ // housing the scene already has.
10
+ //
11
+ // What makes it read as LED rather than a small bulb:
12
+ // - it comes on instantly — no thermal lag, the opposite of a filament;
13
+ // - `pwm` puts back the high-frequency chop a dimmed LED runs on, which a
14
+ // camera (and a 60 Hz render) sees as a slow strobe rather than a flat dim;
15
+ // - `diffuser` both fades the dies into one sheet and widens the light's own
16
+ // shadow radius, because those are the same fact: a bigger apparent source
17
+ // casts a softer shadow;
18
+ // - 'rgb' colour mode runs a hue across the array and can chase along it,
19
+ // which is what an addressable strip actually is. The light itself takes the
20
+ // average of the dies, the way the room would.
21
+ //
22
+ import { useEffect, useMemo, useRef } from 'react';
23
+ import { useFrame } from '@react-three/fiber';
24
+ import { BoxGeometry, Color, InstancedBufferAttribute, Matrix4, PlaneGeometry } from 'three';
25
+ import { SELF_OWNED_MATERIAL_KEY } from '@3driseai/3drise-core';
26
+ import { useObjectReady } from '../../hooks/useObjectReady.js';
27
+ import { useLightShadow } from '../../hooks/useLightShadow.js';
28
+ import { useLightConfig } from '../../hooks/useLightConfig.js';
29
+ import { kelvinToColor } from '../lightBulb/kelvin.js';
30
+ import { createLedArrayMaterials, disposeLedArrayMaterials } from './ledMaterials.js';
31
+ import { placeDies } from './ledLayout.js';
32
+ const num = (v, fallback) => (typeof v === 'number' && Number.isFinite(v) ? v : fallback);
33
+ const clamp01 = (v) => Math.min(Math.max(v, 0), 1);
34
+ const hash = (n) => {
35
+ const s = Math.sin(n * 127.1) * 43758.5453;
36
+ return s - Math.floor(s);
37
+ };
38
+ const noise1 = (x) => {
39
+ const i = Math.floor(x), f = x - i, u = f * f * (3 - 2 * f);
40
+ return hash(i) * (1 - u) + hash(i + 1) * u;
41
+ };
42
+ const scratchMatrix = new Matrix4();
43
+ const LedArray = ({ id, config, animations, mouseMove, settings, onObjectReady, onObjectDispose }) => {
44
+ const groupRef = useRef(null);
45
+ const lightRef = useRef(null);
46
+ const diesRef = useRef(null);
47
+ const glowRef = useRef(null);
48
+ const diffuserRef = useRef(null);
49
+ useObjectReady(groupRef, settings, onObjectReady, onObjectDispose);
50
+ useLightShadow(lightRef, config.shadow);
51
+ const { merged } = useLightConfig(groupRef, lightRef, id, config, animations, mouseMove);
52
+ const layout = (config.layout ?? 'panel');
53
+ const spacing = Math.max(num(config.spacing, 0.06), 0.005);
54
+ const dieSize = Math.max(num(config.dieSize, 0.022), 0.002);
55
+ const emitter = (config.emitter ?? 'dies');
56
+ // --- where the dies sit; rebuilt only when the array's shape changes ---
57
+ const place = useMemo(() => placeDies(layout, config.count, config.rows, spacing), [layout, config.count, config.rows, spacing]);
58
+ const geometry = useMemo(() => ({
59
+ // a flat chip, so a die reads as a surface rather than a cube
60
+ die: new BoxGeometry(1, 0.35, 1),
61
+ glow: new PlaneGeometry(2, 2),
62
+ sheet: new PlaneGeometry(1, 1),
63
+ }), []);
64
+ useEffect(() => () => {
65
+ geometry.die.dispose();
66
+ geometry.glow.dispose();
67
+ geometry.sheet.dispose();
68
+ }, [geometry]);
69
+ const mats = useMemo(() => createLedArrayMaterials(), []);
70
+ useEffect(() => () => disposeLedArrayMaterials(mats), [mats]);
71
+ /** One colour per die, reused every frame instead of reallocated. */
72
+ const dieColors = useMemo(() => {
73
+ const list = [];
74
+ for (let i = 0; i < place.count; i++)
75
+ list.push(new Color());
76
+ return list;
77
+ }, [place]);
78
+ // Instance transforms + the per-die colour attribute the glow shader reads.
79
+ useEffect(() => {
80
+ const dies = diesRef.current;
81
+ const glow = glowRef.current;
82
+ const { positions, count } = place;
83
+ const colors = new Float32Array(count * 3).fill(1);
84
+ for (let i = 0; i < count; i++) {
85
+ const x = positions[i * 3], z = positions[i * 3 + 2];
86
+ scratchMatrix.makeScale(dieSize, dieSize, dieSize);
87
+ scratchMatrix.setPosition(x, 0, z);
88
+ dies?.setMatrixAt(i, scratchMatrix);
89
+ glow?.setMatrixAt(i, scratchMatrix);
90
+ }
91
+ if (dies) {
92
+ dies.count = count;
93
+ dies.instanceMatrix.needsUpdate = true;
94
+ }
95
+ if (glow) {
96
+ glow.count = count;
97
+ glow.instanceMatrix.needsUpdate = true;
98
+ glow.geometry.setAttribute('aColor', new InstancedBufferAttribute(colors, 3));
99
+ }
100
+ mats.diffuser.uniforms.uPitch.value = Math.max(place.pitchX, 1);
101
+ }, [place, dieSize, mats]);
102
+ // The light owns its materials and its shadow flags.
103
+ useEffect(() => {
104
+ const group = groupRef.current;
105
+ if (!group)
106
+ return;
107
+ group.userData[SELF_OWNED_MATERIAL_KEY] = true;
108
+ return () => { delete group.userData[SELF_OWNED_MATERIAL_KEY]; };
109
+ }, []);
110
+ // The dies sit millimetres from the light; the point shadow camera's default
111
+ // near of 0.5 would clip the whole array out of its own shadow map.
112
+ const shadowNear = config.shadow?.cameraNear;
113
+ useEffect(() => {
114
+ const light = lightRef.current;
115
+ if (!light || shadowNear !== undefined)
116
+ return;
117
+ light.shadow.camera.near = 0.02;
118
+ light.shadow.camera.updateProjectionMatrix();
119
+ }, [shadowNear]);
120
+ const colorMode = (config.colorMode ?? 'temperature');
121
+ const lightColor = useMemo(() => (colorMode === 'temperature'
122
+ ? kelvinToColor(config.temperature ?? 5000)
123
+ : new Color(config.color || '#ffffff')), [colorMode, config.color, config.temperature]);
124
+ const power = useRef({
125
+ level: (config.on ?? true) ? 1 : 0,
126
+ base: config.intensity ?? 1,
127
+ written: Number.NaN,
128
+ seed: Math.random() * 1000,
129
+ });
130
+ const scratch = useMemo(() => ({ mean: new Color(), glow: new Color(), tmp: new Color() }), []);
131
+ useFrame(({ clock }, delta) => {
132
+ const light = lightRef.current;
133
+ if (!light)
134
+ return;
135
+ const cfg = merged.current;
136
+ const p = power.current;
137
+ // Adopt any intensity we did not write ourselves as the new base, so the
138
+ // multiplier below never compounds and animations stay in charge.
139
+ if (light.intensity !== p.written)
140
+ p.base = light.intensity;
141
+ const on = cfg.on ?? true;
142
+ // An LED has no thermal lag: the default is a hard switch, and `warmup`
143
+ // is there only for someone who wants a fade.
144
+ const warmup = Math.max(num(cfg.warmup, 0), 0);
145
+ const target = on ? 1 : 0;
146
+ p.level = warmup <= 0
147
+ ? target
148
+ : p.level + (target - p.level) * (1 - Math.exp(-Math.min(delta, 0.1) * 3 / warmup));
149
+ if (Math.abs(target - p.level) < 1e-4)
150
+ p.level = target;
151
+ const t = clock.elapsedTime;
152
+ // PWM: a real dimmed LED is fully on for part of each cycle and fully off
153
+ // for the rest. At ~118 Hz against a 60 Hz frame that beats slowly, which
154
+ // is exactly the strobe a camera picks up.
155
+ const pwm = clamp01(num(cfg.pwm, 0));
156
+ const chop = pwm > 0 ? 1 - pwm * 0.5 * (0.5 + 0.5 * Math.sin(t * Math.PI * 2 * 118)) : 1;
157
+ const flicker = clamp01(num(cfg.flicker, 0));
158
+ let flick = 1;
159
+ if (flicker > 0) {
160
+ const ft = t + p.seed;
161
+ flick = 1 - flicker * (0.1 * noise1(ft * 13) + 0.6 * Math.pow(noise1(ft * 3.1), 4));
162
+ }
163
+ const output = p.level * chop * flick;
164
+ light.intensity = p.base * output;
165
+ p.written = light.intensity;
166
+ const baseIntensity = Math.max(num(config.intensity, 1), 1e-4);
167
+ const glowK = Math.min(output * (p.base / baseIntensity), 3);
168
+ // --- colour: one per die, and the light takes their mean ---
169
+ const rgbSpread = num(cfg.rgbSpread, 1);
170
+ const rgbSpeed = num(cfg.rgbSpeed, 0.2);
171
+ const mean = scratch.mean.setRGB(0, 0, 0);
172
+ const n = dieColors.length || 1;
173
+ for (let i = 0; i < dieColors.length; i++) {
174
+ const c = dieColors[i];
175
+ if (colorMode === 'rgb')
176
+ c.setHSL(((i / n) * rgbSpread + t * rgbSpeed) % 1, 1, 0.5);
177
+ else
178
+ c.copy(light.color);
179
+ mean.r += c.r / n;
180
+ mean.g += c.g / n;
181
+ mean.b += c.b / n;
182
+ }
183
+ // In RGB mode the light is the mix of the dies, not one of them. This runs
184
+ // after useLightConfig's own write, which is the point: the array owns its
185
+ // colour the way the bulb's filament owns its heat.
186
+ if (colorMode === 'rgb' && dieColors.length)
187
+ light.color.copy(mean);
188
+ const glowColor = scratch.glow.copy(colorMode === 'rgb' ? mean : light.color);
189
+ // --- diffuser: fades the dies out and softens the shadow with them ---
190
+ const diffuser = clamp01(num(cfg.diffuser, 0));
191
+ const dieVisible = emitter === 'dies' && glowK > 0.005;
192
+ const dieK = glowK * (1 - diffuser * 0.85);
193
+ const dies = diesRef.current;
194
+ const glow = glowRef.current;
195
+ if (dies) {
196
+ dies.visible = dieVisible;
197
+ if (dieVisible) {
198
+ for (let i = 0; i < dieColors.length; i++)
199
+ dies.setColorAt(i, scratch.tmp.copy(dieColors[i]).multiplyScalar(Math.min(0.06 + dieK * 1.8, 4)));
200
+ if (dies.instanceColor)
201
+ dies.instanceColor.needsUpdate = true;
202
+ }
203
+ }
204
+ if (glow) {
205
+ glow.visible = dieVisible;
206
+ const attr = glow.geometry.getAttribute('aColor');
207
+ if (dieVisible && attr) {
208
+ const arr = attr.array;
209
+ for (let i = 0; i < dieColors.length; i++) {
210
+ const c = dieColors[i];
211
+ arr[i * 3] = c.r;
212
+ arr[i * 3 + 1] = c.g;
213
+ arr[i * 3 + 2] = c.b;
214
+ }
215
+ attr.needsUpdate = true;
216
+ }
217
+ // Every die adds to the same pixels, so the per-die glow has to stay
218
+ // small and faint or a panel fuses into one white slab.
219
+ mats.dieGlow.uniforms.uStrength.value = dieK * 0.34;
220
+ mats.dieGlow.uniforms.uSize.value = Math.min(dieSize * 3.6, spacing * 0.85);
221
+ }
222
+ const sheet = diffuserRef.current;
223
+ if (sheet) {
224
+ sheet.visible = diffuser > 0.01 && glowK > 0.005;
225
+ mats.diffuser.uniforms.uColor.value.copy(glowColor);
226
+ mats.diffuser.uniforms.uStrength.value = diffuser * glowK * 0.85;
227
+ mats.diffuser.uniforms.uBlend.value = diffuser;
228
+ }
229
+ // A diffuser is a physically bigger source, so it blurs the shadow. Only
230
+ // when the config has not set a radius of its own.
231
+ if (config.shadow?.radius === undefined)
232
+ light.shadow.radius = 1 + diffuser * 11;
233
+ const halo = Math.max(num(cfg.halo, 1), 0);
234
+ mats.halo.uniforms.uColor.value.copy(glowColor);
235
+ mats.halo.uniforms.uStrength.value = halo * glowK * (0.12 + diffuser * 0.2);
236
+ // Sized to the array, not the world: a strip and a panel glow differently.
237
+ mats.halo.uniforms.uRadius.value = Math.max(place.width, place.depth) * 1.5;
238
+ });
239
+ // The diffuser sits just in front of the dies, facing down with them.
240
+ const sheetSize = [place.width * 1.12, place.depth * 1.12];
241
+ return (_jsxs("group", { ref: groupRef, children: [_jsx("pointLight", { ref: lightRef, color: lightColor, intensity: config.intensity ?? 1, decay: config.decay ?? 1, distance: config.distance ?? 0 }), _jsx("instancedMesh", { ref: diesRef, args: [geometry.die, mats.die, place.count], renderOrder: 2, frustumCulled: false }), _jsx("instancedMesh", { ref: glowRef, args: [geometry.glow, mats.dieGlow, place.count], renderOrder: 3, frustumCulled: false }), _jsx("mesh", { ref: diffuserRef, material: mats.diffuser, position: [0, -Math.max(dieSize * 1.4, 0.012), 0], rotation: [-Math.PI / 2, 0, 0], scale: [sheetSize[0], sheetSize[1], 1], geometry: geometry.sheet, renderOrder: 4, frustumCulled: false }), _jsx("mesh", { material: mats.halo, geometry: geometry.glow, renderOrder: 5, frustumCulled: false })] }));
242
+ };
243
+ export default LedArray;
@@ -0,0 +1,7 @@
1
+ export { default as LedArray } from './LedArray';
2
+ export type { LedArrayProps } from './LedArray';
3
+ export { createLedArrayMaterials, disposeLedArrayMaterials } from './ledMaterials';
4
+ export type { LedArrayMaterials } from './ledMaterials';
5
+ export { LED_LAYOUTS, MAX_DIES, placeDies } from './ledLayout';
6
+ export type { LedLayout, LedPlacement } from './ledLayout';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/ledArray/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAC;AACjD,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AACnF,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC/D,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,4 @@
1
+ // An array of LED dies with a real point light (config.type 'ledArray').
2
+ export { default as LedArray } from './LedArray.js';
3
+ export { createLedArrayMaterials, disposeLedArrayMaterials } from './ledMaterials.js';
4
+ export { LED_LAYOUTS, MAX_DIES, placeDies } from './ledLayout.js';
@@ -0,0 +1,22 @@
1
+ export type LedLayout = 'single' | 'strip' | 'panel' | 'ring';
2
+ export declare const LED_LAYOUTS: LedLayout[];
3
+ /** Instances are cheap but not free, and a 64x12 panel is already 768 dies. */
4
+ export declare const MAX_DIES = 512;
5
+ export interface LedPlacement {
6
+ /** x, y, z per die, flat. */
7
+ positions: Float32Array;
8
+ /** How many dies were actually placed (after the cap). */
9
+ count: number;
10
+ /** Footprint in object space, for the diffuser and the halo. */
11
+ width: number;
12
+ depth: number;
13
+ /** Hot spots across the diffuser's width, for its ripple. */
14
+ pitchX: number;
15
+ pitchZ: number;
16
+ }
17
+ /**
18
+ * Lay the dies out for a layout. `count` is the number along the main axis;
19
+ * `rows` only means anything for a panel.
20
+ */
21
+ export declare function placeDies(layout: LedLayout, countRaw: unknown, rowsRaw: unknown, spacing: number): LedPlacement;
22
+ //# sourceMappingURL=ledLayout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledLayout.d.ts","sourceRoot":"","sources":["../../../src/components/ledArray/ledLayout.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;AAE9D,eAAO,MAAM,WAAW,EAAE,SAAS,EAAyC,CAAC;AAE7E,+EAA+E;AAC/E,eAAO,MAAM,QAAQ,MAAM,CAAC;AAE5B,MAAM,WAAW,YAAY;IACzB,6BAA6B;IAC7B,SAAS,EAAE,YAAY,CAAC;IACxB,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAClB;AAOD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,YAAY,CA8C/G"}
@@ -0,0 +1,59 @@
1
+ export const LED_LAYOUTS = ['single', 'strip', 'panel', 'ring'];
2
+ /** Instances are cheap but not free, and a 64x12 panel is already 768 dies. */
3
+ export const MAX_DIES = 512;
4
+ const clampInt = (v, lo, hi, fallback) => {
5
+ const n = typeof v === 'number' && Number.isFinite(v) ? Math.round(v) : fallback;
6
+ return Math.min(Math.max(n, lo), hi);
7
+ };
8
+ /**
9
+ * Lay the dies out for a layout. `count` is the number along the main axis;
10
+ * `rows` only means anything for a panel.
11
+ */
12
+ export function placeDies(layout, countRaw, rowsRaw, spacing) {
13
+ const pitch = Math.max(spacing, 0.005);
14
+ let count = clampInt(countRaw, 1, 64, 8);
15
+ let rows = clampInt(rowsRaw, 1, 12, 4);
16
+ if (layout === 'single') {
17
+ count = 1;
18
+ rows = 1;
19
+ }
20
+ else if (layout !== 'panel')
21
+ rows = 1;
22
+ // Keep the instance count sane whatever the knobs say.
23
+ while (count * rows > MAX_DIES) {
24
+ if (rows > 1)
25
+ rows -= 1;
26
+ else
27
+ count -= 1;
28
+ }
29
+ const total = count * rows;
30
+ const positions = new Float32Array(total * 3);
31
+ if (layout === 'ring') {
32
+ // A ring of `count`, sized so neighbours sit one pitch apart.
33
+ const radius = count > 1 ? (pitch * count) / (2 * Math.PI) : 0;
34
+ for (let i = 0; i < count; i++) {
35
+ const a = (i / count) * Math.PI * 2;
36
+ positions[i * 3] = Math.cos(a) * radius;
37
+ positions[i * 3 + 2] = Math.sin(a) * radius;
38
+ }
39
+ const span = radius * 2 + pitch;
40
+ return { positions, count, width: span, depth: span, pitchX: Math.max(count / 4, 1), pitchZ: Math.max(count / 4, 1) };
41
+ }
42
+ const spanX = (count - 1) * pitch;
43
+ const spanZ = (rows - 1) * pitch;
44
+ let i = 0;
45
+ for (let r = 0; r < rows; r++) {
46
+ for (let c = 0; c < count; c++, i++) {
47
+ positions[i * 3] = c * pitch - spanX / 2;
48
+ positions[i * 3 + 2] = r * pitch - spanZ / 2;
49
+ }
50
+ }
51
+ return {
52
+ positions,
53
+ count: total,
54
+ width: spanX + pitch,
55
+ depth: spanZ + pitch,
56
+ pitchX: Math.max(count / 2, 1),
57
+ pitchZ: Math.max(rows / 2, 1),
58
+ };
59
+ }
@@ -0,0 +1,14 @@
1
+ import { MeshBasicMaterial, ShaderMaterial } from 'three';
2
+ export interface LedArrayMaterials {
3
+ /** The dies themselves: emissive squares, per-instance colour. */
4
+ die: MeshBasicMaterial;
5
+ /** One additive billboard per die. */
6
+ dieGlow: ShaderMaterial;
7
+ /** The sheet in front of the dies, lit from behind. */
8
+ diffuser: ShaderMaterial;
9
+ /** One big camera-facing halo for the array as a whole. */
10
+ halo: ShaderMaterial;
11
+ }
12
+ export declare function createLedArrayMaterials(): LedArrayMaterials;
13
+ export declare function disposeLedArrayMaterials(m: LedArrayMaterials): void;
14
+ //# sourceMappingURL=ledMaterials.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledMaterials.d.ts","sourceRoot":"","sources":["../../../src/components/ledArray/ledMaterials.ts"],"names":[],"mappings":"AAeA,OAAO,EAAuC,iBAAiB,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAE/F,MAAM,WAAW,iBAAiB;IAC9B,kEAAkE;IAClE,GAAG,EAAE,iBAAiB,CAAC;IACvB,sCAAsC;IACtC,OAAO,EAAE,cAAc,CAAC;IACxB,uDAAuD;IACvD,QAAQ,EAAE,cAAc,CAAC;IACzB,2DAA2D;IAC3D,IAAI,EAAE,cAAc,CAAC;CACxB;AA+HD,wBAAgB,uBAAuB,IAAI,iBAAiB,CAO3D;AAED,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAKnE"}
@@ -0,0 +1,151 @@
1
+ // =============================================================================
2
+ // ledMaterials — the dies, their per-die glow, the diffuser and the halo
3
+ // =============================================================================
4
+ //
5
+ // The viewer has no tone mapping and no bloom, so every bright thing here is
6
+ // drawn by hand. An LED array is a set of very small, very bright sources, so
7
+ // the glow is per die rather than one blob: a billboard quad per instance,
8
+ // coloured from an instanced attribute so RGB mode can run a different hue down
9
+ // the array.
10
+ //
11
+ // The diffuser is the other half of the look. A bare die is a point source with
12
+ // a hard-edged shadow; a diffuser turns the whole array into one large soft
13
+ // source. The slab drawn here is that surface — and the component couples the
14
+ // same knob to the light's shadow radius, because the two are the same physics.
15
+ //
16
+ import { AdditiveBlending, Color, DoubleSide, MeshBasicMaterial, ShaderMaterial } from 'three';
17
+ const OUTPUT = /* glsl */ `
18
+ #include <tonemapping_fragment>
19
+ #include <colorspace_fragment>
20
+ `;
21
+ /**
22
+ * A billboard per instance. The quad is built in view space from the instance's
23
+ * own origin, so the dies always face the camera however the array is turned.
24
+ */
25
+ function dieGlowMaterial() {
26
+ return new ShaderMaterial({
27
+ uniforms: {
28
+ uStrength: { value: 0 },
29
+ uSize: { value: 0.09 },
30
+ },
31
+ vertexShader: /* glsl */ `
32
+ attribute vec3 aColor;
33
+ uniform float uSize;
34
+ varying vec2 vUv;
35
+ varying vec3 vColor;
36
+ void main() {
37
+ vColor = aColor;
38
+ vUv = position.xy;
39
+ // instance origin → view space, then the quad is laid out there
40
+ vec4 mv = modelViewMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0);
41
+ float s = length(modelMatrix[0].xyz) * uSize;
42
+ mv.xy += position.xy * s;
43
+ gl_Position = projectionMatrix * mv;
44
+ }`,
45
+ fragmentShader: /* glsl */ `
46
+ uniform float uStrength;
47
+ varying vec2 vUv;
48
+ varying vec3 vColor;
49
+ void main() {
50
+ float d = length(vUv);
51
+ // a tight core with a wide, faint skirt — how a small very bright
52
+ // source reads through a lens
53
+ float a = exp(-d * 11.0) + exp(-d * d * 6.0) * 0.35;
54
+ a *= 1.0 - smoothstep(0.7, 1.0, d);
55
+ gl_FragColor = vec4(vColor * uStrength * a, 1.0);
56
+ ${OUTPUT}
57
+ }`,
58
+ transparent: true,
59
+ depthWrite: false,
60
+ blending: AdditiveBlending,
61
+ });
62
+ }
63
+ /**
64
+ * The diffuser sheet. Brightest where the dies are (a soft periodic ripple that
65
+ * fades out as the sheet thickens), dimmer towards the rim where light escapes.
66
+ */
67
+ function diffuserMaterial() {
68
+ return new ShaderMaterial({
69
+ uniforms: {
70
+ uColor: { value: new Color(1, 1, 1) },
71
+ uStrength: { value: 0 },
72
+ /** 0 = the dies still show through, 1 = a single even sheet. */
73
+ uBlend: { value: 0.5 },
74
+ /** How many die pitches across the sheet, for the hot-spot ripple. */
75
+ uPitch: { value: 4 },
76
+ },
77
+ vertexShader: /* glsl */ `
78
+ varying vec2 vUv;
79
+ void main() {
80
+ vUv = uv;
81
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
82
+ }`,
83
+ fragmentShader: /* glsl */ `
84
+ uniform vec3 uColor;
85
+ uniform float uStrength;
86
+ uniform float uBlend;
87
+ uniform float uPitch;
88
+ varying vec2 vUv;
89
+ void main() {
90
+ vec2 p = vUv * 2.0 - 1.0;
91
+ // light leaks away at the rim of any real diffuser
92
+ float edge = (1.0 - smoothstep(0.72, 1.0, abs(p.x))) * (1.0 - smoothstep(0.72, 1.0, abs(p.y)));
93
+ // hot spots over each die, washed out as the sheet thickens
94
+ vec2 c = cos(vUv * uPitch * 6.2831853);
95
+ float dots = (c.x * c.y) * 0.5 + 0.5;
96
+ float lit = mix(0.45 + dots * 0.75, 1.0, uBlend);
97
+ gl_FragColor = vec4(uColor * uStrength * lit * edge, 1.0);
98
+ ${OUTPUT}
99
+ }`,
100
+ transparent: true,
101
+ depthWrite: false,
102
+ side: DoubleSide,
103
+ blending: AdditiveBlending,
104
+ });
105
+ }
106
+ function haloMaterial() {
107
+ return new ShaderMaterial({
108
+ uniforms: {
109
+ uColor: { value: new Color(1, 1, 1) },
110
+ uStrength: { value: 0 },
111
+ uRadius: { value: 0.55 },
112
+ },
113
+ vertexShader: /* glsl */ `
114
+ uniform float uRadius;
115
+ varying vec2 vUv;
116
+ void main() {
117
+ vec4 mv = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
118
+ float s = length(modelMatrix[0].xyz) * uRadius;
119
+ mv.xy += position.xy * s;
120
+ vUv = position.xy;
121
+ gl_Position = projectionMatrix * mv;
122
+ }`,
123
+ fragmentShader: /* glsl */ `
124
+ uniform vec3 uColor;
125
+ uniform float uStrength;
126
+ varying vec2 vUv;
127
+ void main() {
128
+ float d = length(vUv);
129
+ float a = (exp(-d * 8.0) + exp(-d * d * 4.0) * 0.3) * (1.0 - smoothstep(0.65, 1.0, d));
130
+ gl_FragColor = vec4(uColor * uStrength * a, 1.0);
131
+ ${OUTPUT}
132
+ }`,
133
+ transparent: true,
134
+ depthWrite: false,
135
+ blending: AdditiveBlending,
136
+ });
137
+ }
138
+ export function createLedArrayMaterials() {
139
+ return {
140
+ die: new MeshBasicMaterial({ color: 0xffffff, toneMapped: false }),
141
+ dieGlow: dieGlowMaterial(),
142
+ diffuser: diffuserMaterial(),
143
+ halo: haloMaterial(),
144
+ };
145
+ }
146
+ export function disposeLedArrayMaterials(m) {
147
+ m.die.dispose();
148
+ m.dieGlow.dispose();
149
+ m.diffuser.dispose();
150
+ m.halo.dispose();
151
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"LightBulb.d.ts","sourceRoot":"","sources":["../../../src/components/lightBulb/LightBulb.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAqC,MAAM,OAAO,CAAC;AAK1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAMtG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAIhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,MAAM,cAAc,GAAG,gBAAgB,GAAG;IAC5C,8EAA8E;IAC9E,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,eAAe,CAAC;IACxB,UAAU,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAChC,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,yEAAyE;IACzE,QAAQ,EAAE,qBAAqB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CACzD,CAAC;AAsBF,QAAA,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CAsHvC,CAAC;AACF,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"LightBulb.d.ts","sourceRoot":"","sources":["../../../src/components/lightBulb/LightBulb.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAqC,MAAM,OAAO,CAAC;AAK1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAItG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAIhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,MAAM,cAAc,GAAG,gBAAgB,GAAG;IAC5C,8EAA8E;IAC9E,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,eAAe,CAAC;IACxB,UAAU,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAChC,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,yEAAyE;IACzE,QAAQ,EAAE,qBAAqB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CACzD,CAAC;AAuBF,QAAA,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CAqHvC,CAAC;AACF,eAAe,SAAS,CAAC"}
@@ -7,8 +7,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
7
  // is the filament centre, which is where the point light sits; the bulb stands
8
8
  // upright (base down, -Y) — rotate the object 180° for a pendant.
9
9
  //
10
- // Intensity ownership: useLightAnimation / useLightMouseMove (and R3F's
11
- // `intensity` prop) write `light.intensity` as usual. This component then
10
+ // Intensity ownership: useLightConfig (keyframes + pointer bindings) and R3F's
11
+ // `intensity` prop write `light.intensity` as usual. This component then
12
12
  // multiplies in the power level (on/off, warm-up, flicker) each frame. Any
13
13
  // intensity it did not write itself is adopted as the new base, so the
14
14
  // multiplier never compounds and external writers stay in charge of the base.
@@ -26,9 +26,7 @@ import { Color } from 'three';
26
26
  import { SELF_OWNED_MATERIAL_KEY } from '@3driseai/3drise-core';
27
27
  import { useObjectReady } from '../../hooks/useObjectReady.js';
28
28
  import { useLightShadow } from '../../hooks/useLightShadow.js';
29
- import { useLightAnimation } from '../../hooks/useLightAnimation.js';
30
- import { useLightMouseMove } from '../../hooks/useLightMouseMove.js';
31
- import { useLightLive } from '../../hooks/useLightLive.js';
29
+ import { useLightConfig } from '../../hooks/useLightConfig.js';
32
30
  import { buildBulbShape, disposeBulbShape } from './bulbShapes.js';
33
31
  import { applyGlass, createBulbMaterials, disposeBulbMaterials, GLASS_RESPONSE } from './bulbMaterials.js';
34
32
  import { kelvinToColor } from './kelvin.js';
@@ -53,10 +51,9 @@ const LightBulb = ({ id, config, animations, mouseMove, settings, onObjectReady,
53
51
  const lightRef = useRef(null);
54
52
  useObjectReady(groupRef, settings, onObjectReady, onObjectDispose);
55
53
  useLightShadow(lightRef, config.shadow);
56
- useLightAnimation(lightRef, animations);
57
- useLightMouseMove(lightRef, mouseMove, { lightConfig: config });
58
- // Slider drags: light props land on the light; flicker / halo / warmup are read per frame below.
59
- const live = useLightLive(groupRef, lightRef, id, config);
54
+ // Keyframes, pointer bindings and slider drags: the light's own knobs land on
55
+ // the light, and the merged config is what the filament, glow and halo read.
56
+ const { merged } = useLightConfig(groupRef, lightRef, id, config, animations, mouseMove);
60
57
  const glass = config.glass ?? 'clear';
61
58
  const glassTint = config.glassTint ?? '#ffb070';
62
59
  const on = config.on ?? true;
@@ -99,10 +96,10 @@ const LightBulb = ({ id, config, animations, mouseMove, settings, onObjectReady,
99
96
  if (!light)
100
97
  return;
101
98
  const p = power.current;
102
- const lv = live.current;
103
- const warmup = Math.max(num(lv.warmup, config.warmup ?? 0.25), 0);
104
- const flicker = Math.min(Math.max(num(lv.flicker, config.flicker ?? 0), 0), 1);
105
- const haloAmount = Math.min(Math.max(num(lv.halo, config.halo ?? 1), 0), 2);
99
+ const cfg = merged.current;
100
+ const warmup = Math.max(num(cfg.warmup, 0.25), 0);
101
+ const flicker = Math.min(Math.max(num(cfg.flicker, 0), 0), 1);
102
+ const haloAmount = Math.min(Math.max(num(cfg.halo, 1), 0), 2);
106
103
  if (light.intensity !== p.written)
107
104
  p.base = light.intensity;
108
105
  const target = on ? 1 : 0;
@@ -1,7 +1,9 @@
1
1
  import React from 'react';
2
- import type { ObjectAnimations, BioluminescentWakeParticlesConfig } from '@3driseai/3drise-core';
2
+ import type { ObjectAnimations, BioluminescentWakeParticlesConfig, MouseMoveDomain } from '@3driseai/3drise-core';
3
3
  import type { ObjectReadyProps } from '../../types/objectReady';
4
4
  export interface BioluminescentWakeParticlesProps extends ObjectReadyProps {
5
+ /** Pointer bindings for this object's config knobs (mouseMove.particles). */
6
+ mouseMove?: MouseMoveDomain;
5
7
  /** Scene object id: the controller finds the live-edit handle by it. */
6
8
  id?: string;
7
9
  /** World size of the object (units); the object's transform scales it further. */
@@ -1 +1 @@
1
- {"version":3,"file":"BioluminescentWakeParticles.d.ts","sourceRoot":"","sources":["../../../src/components/particles/BioluminescentWakeParticles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAIxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,iCAAiC,EAAE,MAAM,uBAAuB,CAAC;AAMjG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,WAAW,gCAAiC,SAAQ,gBAAgB;IACtE,wEAAwE;IACxE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,iCAAiC,CAAC;IAC1C,sFAAsF;IACtF,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;AA09BD,QAAA,MAAM,2BAA2B,EAAE,KAAK,CAAC,EAAE,CAAC,gCAAgC,CAiD3E,CAAC;AAEF,eAAe,2BAA2B,CAAC"}
1
+ {"version":3,"file":"BioluminescentWakeParticles.d.ts","sourceRoot":"","sources":["../../../src/components/particles/BioluminescentWakeParticles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAIxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,iCAAiC,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAMlH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,WAAW,gCAAiC,SAAQ,gBAAgB;IACtE,6EAA6E;IAC7E,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,wEAAwE;IACxE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,iCAAiC,CAAC;IAC1C,sFAAsF;IACtF,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;AA09BD,QAAA,MAAM,2BAA2B,EAAE,KAAK,CAAC,EAAE,CAAC,gCAAgC,CAqD3E,CAAC;AAEF,eAAe,2BAA2B,CAAC"}
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react'
3
3
  import * as THREE from 'three';
4
4
  import { useFrame } from '@react-three/fiber';
5
5
  import { GENERATIVE_PARTICLES_ANIMATABLE } from '@3driseai/3drise-core';
6
- import { applyConfigKeyframeAnimations } from '../../utils/configAnimations.js';
6
+ import { applyConfigKeyframeAnimations, applyConfigMouseMove } from '../../utils/configAnimations.js';
7
7
  import { createPointerUniforms, stepPointerField, usePointerField } from '../../utils/pointerField.js';
8
8
  import { createTrailUniforms, applyTrailConfig, createCpuTrail } from '../../utils/particleTrails.js';
9
9
  import { useObjectReady } from '../../hooks/useObjectReady.js';
@@ -1103,7 +1103,7 @@ const DEFAULTS = {
1103
1103
  const STRUCTURAL = ['particleCount', 'seed', 'quality'];
1104
1104
  /** Knobs an animation (or mouse move) may drive per frame — generated from applyConfig. */
1105
1105
  const ANIMATABLE = new Set(GENERATIVE_PARTICLES_ANIMATABLE.bioluminescentWake.map((p) => p.value));
1106
- const BioluminescentWakeParticles = ({ id, size, config, animations, onObjectReady, onObjectDispose }) => {
1106
+ const BioluminescentWakeParticles = ({ id, size, config, animations, mouseMove, onObjectReady, onObjectDispose }) => {
1107
1107
  const rootRef = useRef(null);
1108
1108
  useObjectReady(rootRef, config, onObjectReady, onObjectDispose);
1109
1109
  const s = size > 0 ? size : 1;
@@ -1135,8 +1135,12 @@ const BioluminescentWakeParticles = ({ id, size, config, animations, onObjectRea
1135
1135
  const pointer = usePointerField();
1136
1136
  useFrame((state, delta) => {
1137
1137
  instance.update(Math.min(delta, 0.1), state.camera, pointer);
1138
- if (keyframes?.length) {
1138
+ // Keyframes first, pointer on top: a knob bound to both follows the cursor.
1139
+ const pointerBound = !!mouseMove?.enabled && !!mouseMove.properties?.length;
1140
+ if (keyframes?.length || pointerBound) {
1139
1141
  applyConfigKeyframeAnimations(keyframes, animFrom.current, cfg, animOverride.current, performance.now() - animStart.current, ANIMATABLE);
1142
+ if (pointerBound)
1143
+ applyConfigMouseMove(mouseMove.properties, cfg, animOverride.current, state.pointer, ANIMATABLE);
1140
1144
  push(live.current);
1141
1145
  }
1142
1146
  });
@@ -1,7 +1,9 @@
1
1
  import React from 'react';
2
- import type { ObjectAnimations, BrownianDustParticlesConfig } from '@3driseai/3drise-core';
2
+ import type { ObjectAnimations, BrownianDustParticlesConfig, MouseMoveDomain } from '@3driseai/3drise-core';
3
3
  import type { ObjectReadyProps } from '../../types/objectReady';
4
4
  export interface BrownianDustParticlesProps extends ObjectReadyProps {
5
+ /** Pointer bindings for this object's config knobs (mouseMove.particles). */
6
+ mouseMove?: MouseMoveDomain;
5
7
  /** Scene object id: the controller finds the live-edit handle by it. */
6
8
  id?: string;
7
9
  /** World size of the object (units); the object's transform scales it further. */
@@ -1 +1 @@
1
- {"version":3,"file":"BrownianDustParticles.d.ts","sourceRoot":"","sources":["../../../src/components/particles/BrownianDustParticles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAIxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,2BAA2B,EAAE,MAAM,uBAAuB,CAAC;AAM3F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,WAAW,0BAA2B,SAAQ,gBAAgB;IAChE,wEAAwE;IACxE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,2BAA2B,CAAC;IACpC,sFAAsF;IACtF,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;AA4qBD,QAAA,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CAiD/D,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"BrownianDustParticles.d.ts","sourceRoot":"","sources":["../../../src/components/particles/BrownianDustParticles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAIxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,2BAA2B,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAM5G,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,WAAW,0BAA2B,SAAQ,gBAAgB;IAChE,6EAA6E;IAC7E,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,wEAAwE;IACxE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,2BAA2B,CAAC;IACpC,sFAAsF;IACtF,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;AA4qBD,QAAA,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CAqD/D,CAAC;AAEF,eAAe,qBAAqB,CAAC"}