@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
@@ -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';
@@ -737,7 +737,7 @@ const DEFAULTS = {
737
737
  const ANIMATABLE = new Set(GENERATIVE_PARTICLES_ANIMATABLE.brownianDust.map((p) => p.value));
738
738
  /** Knobs that rebuild the simulation: they change on commit, never through the live path. */
739
739
  const STRUCTURAL = ['particleCount'];
740
- const BrownianDustParticles = ({ id, size, config, animations, onObjectReady, onObjectDispose }) => {
740
+ const BrownianDustParticles = ({ id, size, config, animations, mouseMove, onObjectReady, onObjectDispose }) => {
741
741
  const rootRef = useRef(null);
742
742
  useObjectReady(rootRef, config, onObjectReady, onObjectDispose);
743
743
  const s = size > 0 ? size : 1;
@@ -769,8 +769,12 @@ const BrownianDustParticles = ({ id, size, config, animations, onObjectReady, on
769
769
  const pointer = usePointerField();
770
770
  useFrame((state, delta) => {
771
771
  instance.update(Math.min(delta, 0.1), state.camera, pointer);
772
- if (keyframes?.length) {
772
+ // Keyframes first, pointer on top: a knob bound to both follows the cursor.
773
+ const pointerBound = !!mouseMove?.enabled && !!mouseMove.properties?.length;
774
+ if (keyframes?.length || pointerBound) {
773
775
  applyConfigKeyframeAnimations(keyframes, animFrom.current, cfg, animOverride.current, performance.now() - animStart.current, ANIMATABLE);
776
+ if (pointerBound)
777
+ applyConfigMouseMove(mouseMove.properties, cfg, animOverride.current, state.pointer, ANIMATABLE);
774
778
  push(live.current);
775
779
  }
776
780
  });
@@ -1,7 +1,9 @@
1
1
  import React from 'react';
2
- import type { ObjectAnimations, ChladniSandParticlesConfig } from '@3driseai/3drise-core';
2
+ import type { ObjectAnimations, ChladniSandParticlesConfig, MouseMoveDomain } from '@3driseai/3drise-core';
3
3
  import type { ObjectReadyProps } from '../../types/objectReady';
4
4
  export interface ChladniSandParticlesProps 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":"ChladniSandParticles.d.ts","sourceRoot":"","sources":["../../../src/components/particles/ChladniSandParticles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAIxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AAM1F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,WAAW,yBAA0B,SAAQ,gBAAgB;IAC/D,wEAAwE;IACxE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,0BAA0B,CAAC;IACnC,sFAAsF;IACtF,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;AAsvBD,QAAA,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CAiD7D,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"ChladniSandParticles.d.ts","sourceRoot":"","sources":["../../../src/components/particles/ChladniSandParticles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAIxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAM3G,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,WAAW,yBAA0B,SAAQ,gBAAgB;IAC/D,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,0BAA0B,CAAC;IACnC,sFAAsF;IACtF,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;AAsvBD,QAAA,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CAqD7D,CAAC;AAEF,eAAe,oBAAoB,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';
@@ -870,7 +870,7 @@ const DEFAULTS = {
870
870
  const STRUCTURAL = ['particleCount', 'seed', 'quality'];
871
871
  /** Knobs an animation (or mouse move) may drive per frame — generated from applyConfig. */
872
872
  const ANIMATABLE = new Set(GENERATIVE_PARTICLES_ANIMATABLE.chladniSand.map((p) => p.value));
873
- const ChladniSandParticles = ({ id, size, config, animations, onObjectReady, onObjectDispose }) => {
873
+ const ChladniSandParticles = ({ id, size, config, animations, mouseMove, onObjectReady, onObjectDispose }) => {
874
874
  const rootRef = useRef(null);
875
875
  useObjectReady(rootRef, config, onObjectReady, onObjectDispose);
876
876
  const s = size > 0 ? size : 1;
@@ -902,8 +902,12 @@ const ChladniSandParticles = ({ id, size, config, animations, onObjectReady, onO
902
902
  const pointer = usePointerField();
903
903
  useFrame((state, delta) => {
904
904
  instance.update(Math.min(delta, 0.1), state.camera, pointer);
905
- if (keyframes?.length) {
905
+ // Keyframes first, pointer on top: a knob bound to both follows the cursor.
906
+ const pointerBound = !!mouseMove?.enabled && !!mouseMove.properties?.length;
907
+ if (keyframes?.length || pointerBound) {
906
908
  applyConfigKeyframeAnimations(keyframes, animFrom.current, cfg, animOverride.current, performance.now() - animStart.current, ANIMATABLE);
909
+ if (pointerBound)
910
+ applyConfigMouseMove(mouseMove.properties, cfg, animOverride.current, state.pointer, ANIMATABLE);
907
911
  push(live.current);
908
912
  }
909
913
  });
@@ -1,7 +1,9 @@
1
1
  import React from 'react';
2
- import type { ObjectAnimations, ElectronOrbitalsParticlesConfig } from '@3driseai/3drise-core';
2
+ import type { ObjectAnimations, ElectronOrbitalsParticlesConfig, MouseMoveDomain } from '@3driseai/3drise-core';
3
3
  import type { ObjectReadyProps } from '../../types/objectReady';
4
4
  export interface ElectronOrbitalsParticlesProps 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":"ElectronOrbitalsParticles.d.ts","sourceRoot":"","sources":["../../../src/components/particles/ElectronOrbitalsParticles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAIxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,+BAA+B,EAAE,MAAM,uBAAuB,CAAC;AAM/F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,WAAW,8BAA+B,SAAQ,gBAAgB;IACpE,wEAAwE;IACxE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,+BAA+B,CAAC;IACxC,sFAAsF;IACtF,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;AA0xBD,QAAA,MAAM,yBAAyB,EAAE,KAAK,CAAC,EAAE,CAAC,8BAA8B,CAiDvE,CAAC;AAEF,eAAe,yBAAyB,CAAC"}
1
+ {"version":3,"file":"ElectronOrbitalsParticles.d.ts","sourceRoot":"","sources":["../../../src/components/particles/ElectronOrbitalsParticles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAIxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,+BAA+B,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAMhH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,WAAW,8BAA+B,SAAQ,gBAAgB;IACpE,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,+BAA+B,CAAC;IACxC,sFAAsF;IACtF,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;AA0xBD,QAAA,MAAM,yBAAyB,EAAE,KAAK,CAAC,EAAE,CAAC,8BAA8B,CAqDvE,CAAC;AAEF,eAAe,yBAAyB,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';
@@ -924,7 +924,7 @@ const DEFAULTS = {
924
924
  const STRUCTURAL = ['particleCount', 'stateA', 'stateB', 'seed', 'quality'];
925
925
  /** Knobs an animation (or mouse move) may drive per frame — generated from applyConfig. */
926
926
  const ANIMATABLE = new Set(GENERATIVE_PARTICLES_ANIMATABLE.electronOrbitals.map((p) => p.value));
927
- const ElectronOrbitalsParticles = ({ id, size, config, animations, onObjectReady, onObjectDispose }) => {
927
+ const ElectronOrbitalsParticles = ({ id, size, config, animations, mouseMove, onObjectReady, onObjectDispose }) => {
928
928
  const rootRef = useRef(null);
929
929
  useObjectReady(rootRef, config, onObjectReady, onObjectDispose);
930
930
  const s = size > 0 ? size : 1;
@@ -956,8 +956,12 @@ const ElectronOrbitalsParticles = ({ id, size, config, animations, onObjectReady
956
956
  const pointer = usePointerField();
957
957
  useFrame((state, delta) => {
958
958
  instance.update(Math.min(delta, 0.1), state.camera, pointer);
959
- if (keyframes?.length) {
959
+ // Keyframes first, pointer on top: a knob bound to both follows the cursor.
960
+ const pointerBound = !!mouseMove?.enabled && !!mouseMove.properties?.length;
961
+ if (keyframes?.length || pointerBound) {
960
962
  applyConfigKeyframeAnimations(keyframes, animFrom.current, cfg, animOverride.current, performance.now() - animStart.current, ANIMATABLE);
963
+ if (pointerBound)
964
+ applyConfigMouseMove(mouseMove.properties, cfg, animOverride.current, state.pointer, ANIMATABLE);
961
965
  push(live.current);
962
966
  }
963
967
  });
@@ -1,7 +1,9 @@
1
1
  import React from 'react';
2
- import type { ObjectAnimations, FaradayPowderParticlesConfig } from '@3driseai/3drise-core';
2
+ import type { ObjectAnimations, FaradayPowderParticlesConfig, MouseMoveDomain } from '@3driseai/3drise-core';
3
3
  import type { ObjectReadyProps } from '../../types/objectReady';
4
4
  export interface FaradayPowderParticlesProps 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":"FaradayPowderParticles.d.ts","sourceRoot":"","sources":["../../../src/components/particles/FaradayPowderParticles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAIxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAM5F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,WAAW,2BAA4B,SAAQ,gBAAgB;IACjE,wEAAwE;IACxE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,4BAA4B,CAAC;IACrC,sFAAsF;IACtF,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;AAypBD,QAAA,MAAM,sBAAsB,EAAE,KAAK,CAAC,EAAE,CAAC,2BAA2B,CAiDjE,CAAC;AAEF,eAAe,sBAAsB,CAAC"}
1
+ {"version":3,"file":"FaradayPowderParticles.d.ts","sourceRoot":"","sources":["../../../src/components/particles/FaradayPowderParticles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAIxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAM7G,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,WAAW,2BAA4B,SAAQ,gBAAgB;IACjE,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,4BAA4B,CAAC;IACrC,sFAAsF;IACtF,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;AAypBD,QAAA,MAAM,sBAAsB,EAAE,KAAK,CAAC,EAAE,CAAC,2BAA2B,CAqDjE,CAAC;AAEF,eAAe,sBAAsB,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';
@@ -741,7 +741,7 @@ const DEFAULTS = {
741
741
  const STRUCTURAL = ['particleCount', 'seed', 'quality'];
742
742
  /** Knobs an animation (or mouse move) may drive per frame — generated from applyConfig. */
743
743
  const ANIMATABLE = new Set(GENERATIVE_PARTICLES_ANIMATABLE.faradayPowder.map((p) => p.value));
744
- const FaradayPowderParticles = ({ id, size, config, animations, onObjectReady, onObjectDispose }) => {
744
+ const FaradayPowderParticles = ({ id, size, config, animations, mouseMove, onObjectReady, onObjectDispose }) => {
745
745
  const rootRef = useRef(null);
746
746
  useObjectReady(rootRef, config, onObjectReady, onObjectDispose);
747
747
  const s = size > 0 ? size : 1;
@@ -773,8 +773,12 @@ const FaradayPowderParticles = ({ id, size, config, animations, onObjectReady, o
773
773
  const pointer = usePointerField();
774
774
  useFrame((state, delta) => {
775
775
  instance.update(Math.min(delta, 0.1), state.camera, pointer);
776
- if (keyframes?.length) {
776
+ // Keyframes first, pointer on top: a knob bound to both follows the cursor.
777
+ const pointerBound = !!mouseMove?.enabled && !!mouseMove.properties?.length;
778
+ if (keyframes?.length || pointerBound) {
777
779
  applyConfigKeyframeAnimations(keyframes, animFrom.current, cfg, animOverride.current, performance.now() - animStart.current, ANIMATABLE);
780
+ if (pointerBound)
781
+ applyConfigMouseMove(mouseMove.properties, cfg, animOverride.current, state.pointer, ANIMATABLE);
778
782
  push(live.current);
779
783
  }
780
784
  });
@@ -1,7 +1,9 @@
1
1
  import React from 'react';
2
- import type { ObjectAnimations, GrinderSparksParticlesConfig } from '@3driseai/3drise-core';
2
+ import type { ObjectAnimations, GrinderSparksParticlesConfig, MouseMoveDomain } from '@3driseai/3drise-core';
3
3
  import type { ObjectReadyProps } from '../../types/objectReady';
4
4
  export interface GrinderSparksParticlesProps 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":"GrinderSparksParticles.d.ts","sourceRoot":"","sources":["../../../src/components/particles/GrinderSparksParticles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAIxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAM5F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,WAAW,2BAA4B,SAAQ,gBAAgB;IACjE,wEAAwE;IACxE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,4BAA4B,CAAC;IACrC,sFAAsF;IACtF,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;AA8pBD,QAAA,MAAM,sBAAsB,EAAE,KAAK,CAAC,EAAE,CAAC,2BAA2B,CAiDjE,CAAC;AAEF,eAAe,sBAAsB,CAAC"}
1
+ {"version":3,"file":"GrinderSparksParticles.d.ts","sourceRoot":"","sources":["../../../src/components/particles/GrinderSparksParticles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAIxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAM7G,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,WAAW,2BAA4B,SAAQ,gBAAgB;IACjE,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,4BAA4B,CAAC;IACrC,sFAAsF;IACtF,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;AA8pBD,QAAA,MAAM,sBAAsB,EAAE,KAAK,CAAC,EAAE,CAAC,2BAA2B,CAqDjE,CAAC;AAEF,eAAe,sBAAsB,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';
@@ -726,7 +726,7 @@ const DEFAULTS = {
726
726
  const STRUCTURAL = ['particleCount', 'seed', 'quality'];
727
727
  /** Knobs an animation (or mouse move) may drive per frame — generated from applyConfig. */
728
728
  const ANIMATABLE = new Set(GENERATIVE_PARTICLES_ANIMATABLE.grinderSparks.map((p) => p.value));
729
- const GrinderSparksParticles = ({ id, size, config, animations, onObjectReady, onObjectDispose }) => {
729
+ const GrinderSparksParticles = ({ id, size, config, animations, mouseMove, onObjectReady, onObjectDispose }) => {
730
730
  const rootRef = useRef(null);
731
731
  useObjectReady(rootRef, config, onObjectReady, onObjectDispose);
732
732
  const s = size > 0 ? size : 1;
@@ -758,8 +758,12 @@ const GrinderSparksParticles = ({ id, size, config, animations, onObjectReady, o
758
758
  const pointer = usePointerField();
759
759
  useFrame((state, delta) => {
760
760
  instance.update(Math.min(delta, 0.1), state.camera, pointer);
761
- if (keyframes?.length) {
761
+ // Keyframes first, pointer on top: a knob bound to both follows the cursor.
762
+ const pointerBound = !!mouseMove?.enabled && !!mouseMove.properties?.length;
763
+ if (keyframes?.length || pointerBound) {
762
764
  applyConfigKeyframeAnimations(keyframes, animFrom.current, cfg, animOverride.current, performance.now() - animStart.current, ANIMATABLE);
765
+ if (pointerBound)
766
+ applyConfigMouseMove(mouseMove.properties, cfg, animOverride.current, state.pointer, ANIMATABLE);
763
767
  push(live.current);
764
768
  }
765
769
  });
@@ -1,7 +1,9 @@
1
1
  import React from 'react';
2
- import type { ObjectAnimations, NeonRainParticlesConfig } from '@3driseai/3drise-core';
2
+ import type { ObjectAnimations, NeonRainParticlesConfig, MouseMoveDomain } from '@3driseai/3drise-core';
3
3
  import type { ObjectReadyProps } from '../../types/objectReady';
4
4
  export interface NeonRainParticlesProps 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":"NeonRainParticles.d.ts","sourceRoot":"","sources":["../../../src/components/particles/NeonRainParticles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAIxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAMvF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,WAAW,sBAAuB,SAAQ,gBAAgB;IAC5D,wEAAwE;IACxE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,uBAAuB,CAAC;IAChC,sFAAsF;IACtF,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;AAinBD,QAAA,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAiDvD,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"NeonRainParticles.d.ts","sourceRoot":"","sources":["../../../src/components/particles/NeonRainParticles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAIxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAMxG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,WAAW,sBAAuB,SAAQ,gBAAgB;IAC5D,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,uBAAuB,CAAC;IAChC,sFAAsF;IACtF,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;AAinBD,QAAA,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAqDvD,CAAC;AAEF,eAAe,iBAAiB,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';
@@ -674,7 +674,7 @@ const DEFAULTS = {
674
674
  const STRUCTURAL = ['particleCount', 'seed', 'quality'];
675
675
  /** Knobs an animation (or mouse move) may drive per frame — generated from applyConfig. */
676
676
  const ANIMATABLE = new Set(GENERATIVE_PARTICLES_ANIMATABLE.neonRain.map((p) => p.value));
677
- const NeonRainParticles = ({ id, size, config, animations, onObjectReady, onObjectDispose }) => {
677
+ const NeonRainParticles = ({ id, size, config, animations, mouseMove, onObjectReady, onObjectDispose }) => {
678
678
  const rootRef = useRef(null);
679
679
  useObjectReady(rootRef, config, onObjectReady, onObjectDispose);
680
680
  const s = size > 0 ? size : 1;
@@ -706,8 +706,12 @@ const NeonRainParticles = ({ id, size, config, animations, onObjectReady, onObje
706
706
  const pointer = usePointerField();
707
707
  useFrame((state, delta) => {
708
708
  instance.update(Math.min(delta, 0.1), state.camera, pointer);
709
- if (keyframes?.length) {
709
+ // Keyframes first, pointer on top: a knob bound to both follows the cursor.
710
+ const pointerBound = !!mouseMove?.enabled && !!mouseMove.properties?.length;
711
+ if (keyframes?.length || pointerBound) {
710
712
  applyConfigKeyframeAnimations(keyframes, animFrom.current, cfg, animOverride.current, performance.now() - animStart.current, ANIMATABLE);
713
+ if (pointerBound)
714
+ applyConfigMouseMove(mouseMove.properties, cfg, animOverride.current, state.pointer, ANIMATABLE);
711
715
  push(live.current);
712
716
  }
713
717
  });
@@ -1,7 +1,9 @@
1
1
  import React from 'react';
2
- import type { ObjectAnimations, TidalStreamParticlesConfig } from '@3driseai/3drise-core';
2
+ import type { ObjectAnimations, TidalStreamParticlesConfig, MouseMoveDomain } from '@3driseai/3drise-core';
3
3
  import type { ObjectReadyProps } from '../../types/objectReady';
4
4
  export interface TidalStreamParticlesProps 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":"TidalStreamParticles.d.ts","sourceRoot":"","sources":["../../../src/components/particles/TidalStreamParticles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAIxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AAM1F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,WAAW,yBAA0B,SAAQ,gBAAgB;IAC/D,wEAAwE;IACxE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,0BAA0B,CAAC;IACnC,sFAAsF;IACtF,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;AAmtBD,QAAA,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CAiD7D,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"TidalStreamParticles.d.ts","sourceRoot":"","sources":["../../../src/components/particles/TidalStreamParticles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAIxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAM3G,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,WAAW,yBAA0B,SAAQ,gBAAgB;IAC/D,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,0BAA0B,CAAC;IACnC,sFAAsF;IACtF,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;AAmtBD,QAAA,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CAqD7D,CAAC;AAEF,eAAe,oBAAoB,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';
@@ -768,7 +768,7 @@ const DEFAULTS = {
768
768
  const ANIMATABLE = new Set(GENERATIVE_PARTICLES_ANIMATABLE.tidalStream.map((p) => p.value));
769
769
  /** Knobs that rebuild the simulation: they change on commit, never through the live path. */
770
770
  const STRUCTURAL = ['particleCount', 'satelliteCount', 'orbitEccentricity', 'streamAge', 'discStars'];
771
- const TidalStreamParticles = ({ id, size, config, animations, onObjectReady, onObjectDispose }) => {
771
+ const TidalStreamParticles = ({ id, size, config, animations, mouseMove, onObjectReady, onObjectDispose }) => {
772
772
  const rootRef = useRef(null);
773
773
  useObjectReady(rootRef, config, onObjectReady, onObjectDispose);
774
774
  const s = size > 0 ? size : 1;
@@ -800,8 +800,12 @@ const TidalStreamParticles = ({ id, size, config, animations, onObjectReady, onO
800
800
  const pointer = usePointerField();
801
801
  useFrame((state, delta) => {
802
802
  instance.update(Math.min(delta, 0.1), state.camera, pointer);
803
- if (keyframes?.length) {
803
+ // Keyframes first, pointer on top: a knob bound to both follows the cursor.
804
+ const pointerBound = !!mouseMove?.enabled && !!mouseMove.properties?.length;
805
+ if (keyframes?.length || pointerBound) {
804
806
  applyConfigKeyframeAnimations(keyframes, animFrom.current, cfg, animOverride.current, performance.now() - animStart.current, ANIMATABLE);
807
+ if (pointerBound)
808
+ applyConfigMouseMove(mouseMove.properties, cfg, animOverride.current, state.pointer, ANIMATABLE);
805
809
  push(live.current);
806
810
  }
807
811
  });
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import type { AnimationOptions, CreatedObjectSettings, MouseMoveDomain } from '@3driseai/3drise-core';
3
+ import type { ObjectReadyProps } from '../../types/objectReady';
4
+ export type SpotBeamProps = ObjectReadyProps & {
5
+ /** Scene object id: the light controller finds the live-edit handle by it. */
6
+ id?: string;
7
+ config: Record<string, any>;
8
+ animations?: AnimationOptions[];
9
+ mouseMove?: MouseMoveDomain;
10
+ settings: CreatedObjectSettings | Record<string, any>;
11
+ };
12
+ declare const SpotBeam: React.FC<SpotBeamProps>;
13
+ export default SpotBeam;
14
+ //# sourceMappingURL=SpotBeam.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SpotBeam.d.ts","sourceRoot":"","sources":["../../../src/components/spotBeam/SpotBeam.tsx"],"names":[],"mappings":"AAmBA,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;AAOhE,MAAM,MAAM,aAAa,GAAG,gBAAgB,GAAG;IAC3C,8EAA8E;IAC9E,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,UAAU,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAChC,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,QAAQ,EAAE,qBAAqB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CACzD,CAAC;AAuBF,QAAA,MAAM,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC,aAAa,CAwOrC,CAAC;AACF,eAAe,QAAQ,CAAC"}
@@ -0,0 +1,244 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // =============================================================================
3
+ // SpotBeam — a spot light you can see: pattern, beam and pool
4
+ // =============================================================================
5
+ //
6
+ // Rendered by LightObject for `config.type === 'spotBeam'`. This is a light, not
7
+ // a lamp: the geometry is a small emitter you can grab in the editor (and can
8
+ // switch off entirely), because the object is meant to be dropped inside whatever
9
+ // fixture the scene already has.
10
+ //
11
+ // The work is in the optics:
12
+ // - a procedural mask on SpotLight.map shapes the pool (slot, bars, blinds,
13
+ // window, leaves) — see beamPatterns.ts;
14
+ // - the beam samples that same mask, so the haze in the air and the light on
15
+ // the floor are the same shape;
16
+ // - the beam is nested cone shells in one InstancedMesh, dense on the axis.
17
+ //
18
+ // The object's origin is the light. It aims at `targetPosition` in world space,
19
+ // the same contract the plain spot light uses.
20
+ //
21
+ import { useEffect, useMemo, useRef } from 'react';
22
+ import { useFrame, useThree } from '@react-three/fiber';
23
+ import { CircleGeometry, Color, ConeGeometry, InstancedBufferAttribute, Matrix4, Object3D, SphereGeometry, Vector3 } from 'three';
24
+ import { SELF_OWNED_MATERIAL_KEY } from '@3driseai/3drise-core';
25
+ import { useObjectReady } from '../../hooks/useObjectReady.js';
26
+ import { useLightShadow } from '../../hooks/useLightShadow.js';
27
+ import { useLightConfig } from '../../hooks/useLightConfig.js';
28
+ import { createBeamPattern } from './beamPatterns.js';
29
+ import { createSpotBeamMaterials, disposeSpotBeamMaterials } from './spotBeamMaterials.js';
30
+ import { kelvinToColor } from '../lightBulb/kelvin.js';
31
+ /** Cone shells: more is smoother haze, and each one is a draw. */
32
+ const LAYERS = 10;
33
+ const num = (v, fallback) => (typeof v === 'number' ? v : fallback);
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 COLD = new Color(0.06, 0.02, 0.0);
43
+ const EMBER = new Color(1.0, 0.4, 0.08);
44
+ const WHITE = new Color(1, 1, 1);
45
+ const scratchVec = new Vector3();
46
+ const scratchScale = new Vector3();
47
+ const scratchMatrix = new Matrix4();
48
+ const SpotBeam = ({ id, config, animations, mouseMove, settings, onObjectReady, onObjectDispose }) => {
49
+ const scene = useThree((s) => s.scene);
50
+ const groupRef = useRef(null);
51
+ const lightRef = useRef(null);
52
+ const targetRef = useRef(new Object3D());
53
+ const beamRef = useRef(null);
54
+ const soloRef = useRef(null);
55
+ useObjectReady(groupRef, settings, onObjectReady, onObjectDispose);
56
+ useLightShadow(lightRef, config.shadow);
57
+ const { merged } = useLightConfig(groupRef, lightRef, id, config, animations, mouseMove);
58
+ // --- geometry: a unit cone for the beam, a small emitter ---
59
+ const geometry = useMemo(() => {
60
+ // apex at the origin, opening along +Y, rim radius == height so the
61
+ // shader can read x/y as the projector's coordinate
62
+ const cone = new ConeGeometry(1, 1, 56, 18, true);
63
+ cone.rotateX(Math.PI);
64
+ cone.translate(0, 0.5, 0);
65
+ const lens = new CircleGeometry(0.16, 40);
66
+ lens.rotateX(-Math.PI / 2);
67
+ const dot = new SphereGeometry(0.05, 20, 12);
68
+ return { cone, lens, dot };
69
+ }, []);
70
+ useEffect(() => () => {
71
+ geometry.cone.dispose();
72
+ geometry.lens.dispose();
73
+ geometry.dot.dispose();
74
+ }, [geometry]);
75
+ const mats = useMemo(() => createSpotBeamMaterials(), []);
76
+ useEffect(() => () => disposeSpotBeamMaterials(mats), [mats]);
77
+ // --- the projected mask: shapes the pool and the haze together ---
78
+ const pattern = (config.pattern ?? 'round');
79
+ const patternSoftness = Math.min(Math.max(num(config.patternSoftness, 0.25), 0), 1);
80
+ const patternRotation = num(config.patternRotation, 0);
81
+ useEffect(() => {
82
+ const texture = createBeamPattern(pattern, patternSoftness, patternRotation);
83
+ mats.layered.uniforms.uPattern.value = texture;
84
+ mats.volumetric.uniforms.uPattern.value = texture;
85
+ if (lightRef.current)
86
+ lightRef.current.map = texture;
87
+ return () => {
88
+ if (lightRef.current?.map === texture)
89
+ lightRef.current.map = null;
90
+ if (mats.layered.uniforms.uPattern.value === texture)
91
+ mats.layered.uniforms.uPattern.value = null;
92
+ if (mats.volumetric.uniforms.uPattern.value === texture)
93
+ mats.volumetric.uniforms.uPattern.value = null;
94
+ texture.dispose();
95
+ };
96
+ }, [mats, pattern, patternSoftness, patternRotation]);
97
+ // One shell per layer, fraction 0 (axis) → 1 (rim).
98
+ useEffect(() => {
99
+ const beam = beamRef.current;
100
+ if (!beam)
101
+ return;
102
+ const fracs = new Float32Array(LAYERS);
103
+ for (let i = 0; i < LAYERS; i++)
104
+ fracs[i] = (i + 1) / LAYERS;
105
+ beam.geometry.setAttribute('aFrac', new InstancedBufferAttribute(fracs, 1));
106
+ mats.layered.uniforms.uLayers.value = LAYERS;
107
+ }, [mats]);
108
+ const lightColor = useMemo(() => ((config.colorMode ?? 'temperature') === 'color'
109
+ ? new Color(config.color || '#ffffff')
110
+ : kelvinToColor(config.temperature ?? 3600)), [config.colorMode, config.color, config.temperature]);
111
+ // targetPosition is world space, like the plain spot light's.
112
+ const [tx, ty, tz] = (config.targetPosition ?? [0, 0, 0]);
113
+ useEffect(() => {
114
+ const light = lightRef.current;
115
+ if (!light)
116
+ return;
117
+ const target = targetRef.current;
118
+ target.position.set(tx, ty, tz);
119
+ target.updateMatrixWorld();
120
+ light.target = target;
121
+ scene.add(target);
122
+ return () => { scene.remove(target); };
123
+ }, [scene, tx, ty, tz]);
124
+ // The light owns its materials and its shadow flags.
125
+ useEffect(() => {
126
+ const group = groupRef.current;
127
+ if (!group)
128
+ return;
129
+ group.userData[SELF_OWNED_MATERIAL_KEY] = true;
130
+ return () => { delete group.userData[SELF_OWNED_MATERIAL_KEY]; };
131
+ }, []);
132
+ // Anything the user puts in front of this light is usually centimetres away.
133
+ const shadowNear = config.shadow?.cameraNear;
134
+ useEffect(() => {
135
+ const light = lightRef.current;
136
+ if (!light || shadowNear !== undefined)
137
+ return;
138
+ light.shadow.camera.near = 0.05;
139
+ light.shadow.camera.updateProjectionMatrix();
140
+ }, [shadowNear]);
141
+ const emitter = (config.emitter ?? 'lens');
142
+ // Which beam the user picked. Each mode is a different mesh, so this rebuilds
143
+ // rather than tweens: off, one shell, nested shells, or a raymarched volume.
144
+ const beamMode = (config.beamMode ?? 'soft');
145
+ const power = useRef({ level: (config.on ?? true) ? 1 : 0, base: config.intensity ?? 1, written: Number.NaN, seed: Math.random() * 1000 });
146
+ const scratch = useMemo(() => ({ heat: new Color(), glow: new Color() }), []);
147
+ useFrame(({ clock, camera }, delta) => {
148
+ const light = lightRef.current;
149
+ const group = groupRef.current;
150
+ if (!light || !group)
151
+ return;
152
+ const cfg = merged.current;
153
+ const p = power.current;
154
+ const on = cfg.on ?? true;
155
+ const warmup = Math.max(num(cfg.warmup, 0.3), 0);
156
+ const flicker = Math.min(Math.max(num(cfg.flicker, 0), 0), 1);
157
+ const beamAmount = Math.min(Math.max(num(cfg.beam, 1), 0), 3);
158
+ const haloAmount = Math.min(Math.max(num(cfg.halo, 1), 0), 3);
159
+ const baseIntensity = Math.max(num(config.intensity, 1), 1e-4);
160
+ if (light.intensity !== p.written)
161
+ p.base = light.intensity;
162
+ const target = on ? 1 : 0;
163
+ const tau = target > p.level ? warmup : warmup * 2.2;
164
+ p.level = tau <= 0 ? target : p.level + (target - p.level) * (1 - Math.exp(-Math.min(delta, 0.1) * 3 / tau));
165
+ if (Math.abs(target - p.level) < 1e-4)
166
+ p.level = target;
167
+ let flick = 1;
168
+ if (flicker > 0) {
169
+ const t = clock.elapsedTime + p.seed;
170
+ flick = 1 - flicker * (0.12 * noise1(t * 11) + 0.55 * Math.pow(noise1(t * 2.7), 4));
171
+ }
172
+ const output = Math.pow(p.level, 2.0) * flick;
173
+ light.intensity = p.base * output;
174
+ p.written = light.intensity;
175
+ const glowK = Math.min(output * (p.base / baseIntensity), 3);
176
+ const k = p.level * flick;
177
+ // Emitter: ember while it warms, then the light's own colour.
178
+ const heat = scratch.heat;
179
+ if (k < 0.5)
180
+ heat.copy(COLD).lerp(EMBER, k / 0.5);
181
+ else
182
+ heat.copy(EMBER).lerp(scratch.glow.copy(light.color).lerp(WHITE, 0.55), (k - 0.5) / 0.5);
183
+ mats.lens.color.copy(heat).multiplyScalar(0.05 + glowK * 2.2);
184
+ const glow = scratch.glow.copy(light.color);
185
+ mats.halo.uniforms.uColor.value.copy(glow);
186
+ mats.halo.uniforms.uStrength.value = haloAmount * glowK * 0.55;
187
+ const penumbra = num(cfg.penumbra, 0.3);
188
+ const noise = Math.min(Math.max(num(cfg.beamNoise, 0.35), 0), 1);
189
+ const active = beamMode === 'layered' ? mats.layered : beamMode === 'volumetric' ? mats.volumetric : mats.soft;
190
+ active.uniforms.uColor.value.copy(glow);
191
+ active.uniforms.uPenumbra.value = penumbra;
192
+ if (beamMode === 'layered') {
193
+ mats.layered.uniforms.uNoise.value = noise;
194
+ mats.layered.uniforms.uTime.value = clock.elapsedTime;
195
+ mats.layered.uniforms.uStrength.value = beamAmount * glowK * 0.45;
196
+ }
197
+ else if (beamMode === 'volumetric') {
198
+ mats.volumetric.uniforms.uNoise.value = noise;
199
+ mats.volumetric.uniforms.uTime.value = clock.elapsedTime;
200
+ mats.volumetric.uniforms.uSteps.value = num(cfg.beamSteps, 28);
201
+ mats.volumetric.uniforms.uStrength.value = beamAmount * glowK * 0.9;
202
+ }
203
+ else {
204
+ mats.soft.uniforms.uStrength.value = beamAmount * glowK * 0.5;
205
+ }
206
+ // Size and aim whichever beam mesh is mounted: as wide as the cone, as
207
+ // long as the throw, pointing down the light's own axis.
208
+ const mesh = beamMode === 'layered' ? beamRef.current : soloRef.current;
209
+ if (mesh) {
210
+ const reach = targetRef.current.position.distanceTo(group.getWorldPosition(scratchVec)) || 6;
211
+ const worldScale = group.getWorldScale(scratchScale).x || 1;
212
+ const length = Math.max((reach / worldScale) * num(cfg.beamLength, 1), 0.1);
213
+ const radius = Math.tan(Math.min(num(cfg.angle, Math.PI / 6), 1.45)) * length;
214
+ const visible = beamAmount > 0 && glowK > 0.01;
215
+ mesh.visible = visible;
216
+ group.worldToLocal(scratchVec.copy(targetRef.current.position));
217
+ mesh.quaternion.identity();
218
+ mesh.up.set(0, 0, 1);
219
+ mesh.lookAt(scratchVec);
220
+ mesh.rotateX(Math.PI / 2); // the cone opens along +Y, lookAt aims +Z
221
+ if (beamMode === 'layered') {
222
+ for (let i = 0; i < LAYERS; i++) {
223
+ scratchMatrix.makeScale(radius, length, radius);
224
+ mesh.setMatrixAt(i, scratchMatrix);
225
+ }
226
+ mesh.instanceMatrix.needsUpdate = true;
227
+ }
228
+ else {
229
+ mesh.scale.set(radius, length, radius);
230
+ }
231
+ if (beamMode === 'volumetric' && visible) {
232
+ // The raymarch runs in the cone's own space, so the camera goes there too.
233
+ mesh.updateMatrixWorld();
234
+ mats.volumetric.uniforms.uCamera.value.copy(camera.position);
235
+ mesh.worldToLocal(mats.volumetric.uniforms.uCamera.value);
236
+ }
237
+ }
238
+ });
239
+ return (_jsxs("group", { ref: groupRef, children: [_jsx("spotLight", { ref: lightRef,
240
+ // THREE.SpotLight's constructor parks itself at (0, 1, 0); left alone
241
+ // the light sits a unit away from the object it belongs to.
242
+ position: [0, 0, 0], color: lightColor, intensity: config.intensity ?? 1, angle: config.angle ?? Math.PI / 6, penumbra: config.penumbra ?? 0.3, decay: config.decay ?? 1, distance: config.distance ?? 0 }), emitter === 'lens' && _jsx("mesh", { geometry: geometry.lens, material: mats.lens, renderOrder: 2 }), emitter === 'dot' && _jsx("mesh", { geometry: geometry.dot, material: mats.lens, renderOrder: 2 }), beamMode === 'layered' && (_jsx("instancedMesh", { ref: beamRef, args: [geometry.cone, mats.layered, LAYERS], renderOrder: 3, frustumCulled: false })), (beamMode === 'soft' || beamMode === 'volumetric') && (_jsx("mesh", { ref: soloRef, geometry: geometry.cone, material: beamMode === 'soft' ? mats.soft : mats.volumetric, renderOrder: 3, frustumCulled: false })), _jsx("mesh", { material: mats.halo, renderOrder: 4, frustumCulled: false, children: _jsx("planeGeometry", { args: [2, 2] }) })] }));
243
+ };
244
+ export default SpotBeam;
@@ -0,0 +1,11 @@
1
+ import type { Texture } from 'three';
2
+ export type BeamPattern = 'round' | 'slot' | 'bars' | 'blinds' | 'window' | 'leaves';
3
+ export declare const BEAM_PATTERNS: BeamPattern[];
4
+ /**
5
+ * Build the mask. `rotation` is baked in rather than applied as a uv transform:
6
+ * three samples a spot light's map through the light frustum and ignores the
7
+ * texture matrix, so rotating it any other way would turn the haze without
8
+ * turning the pool.
9
+ */
10
+ export declare function createBeamPattern(pattern: BeamPattern, softness: number, rotationDeg: number): Texture;
11
+ //# sourceMappingURL=beamPatterns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"beamPatterns.d.ts","sourceRoot":"","sources":["../../../src/components/spotBeam/beamPatterns.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAErC,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAErF,eAAO,MAAM,aAAa,EAAE,WAAW,EAA4D,CAAC;AA+DpG;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAyBtG"}