handling lights
This commit is contained in:
@@ -1229,6 +1229,24 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
rotation: [0, 0, 0],
|
rotation: [0, 0, 0],
|
||||||
dimensions: [1, 1, 1],
|
dimensions: [1, 1, 1],
|
||||||
attachTo: "ceiling",
|
attachTo: "ceiling",
|
||||||
|
interactive: {
|
||||||
|
controls: [
|
||||||
|
{
|
||||||
|
kind: 'toggle',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'slider', label: 'Intensity', min: 0, max: 100, unit: '%', displayMode: 'dial'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
effects: [
|
||||||
|
{
|
||||||
|
kind: 'light',
|
||||||
|
intensityRange: [0, 2],
|
||||||
|
color: '#ffffff',
|
||||||
|
offset: [0, -0.5, 0],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
type AnimationEffect,
|
type AnimationEffect,
|
||||||
type AnyNodeId,
|
type AnyNodeId,
|
||||||
|
type Interactive,
|
||||||
type ItemNode,
|
type ItemNode,
|
||||||
|
type LightEffect,
|
||||||
|
type SliderControl,
|
||||||
useInteractive,
|
useInteractive,
|
||||||
useRegistry,
|
useRegistry,
|
||||||
useScene,
|
useScene,
|
||||||
@@ -10,7 +13,9 @@ import { useAnimations } from '@react-three/drei'
|
|||||||
import { Clone } from '@react-three/drei/core/Clone'
|
import { Clone } from '@react-three/drei/core/Clone'
|
||||||
import { useGLTF } from '@react-three/drei/core/Gltf'
|
import { useGLTF } from '@react-three/drei/core/Gltf'
|
||||||
import { Suspense, useEffect, useMemo, useRef } from 'react'
|
import { Suspense, useEffect, useMemo, useRef } from 'react'
|
||||||
import type { Group, Material, Mesh } from 'three'
|
import { useFrame } from '@react-three/fiber'
|
||||||
|
import type { Group, Material, Mesh, PointLight } from 'three'
|
||||||
|
import { MathUtils } from 'three'
|
||||||
import { positionLocal, smoothstep, time } from 'three/tsl'
|
import { positionLocal, smoothstep, time } from 'three/tsl'
|
||||||
import { DoubleSide, MeshStandardNodeMaterial } from 'three/webgpu'
|
import { DoubleSide, MeshStandardNodeMaterial } from 'three/webgpu'
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
@@ -163,7 +168,11 @@ const ModelRenderer = ({ node }: { node: ItemNode }) => {
|
|||||||
})
|
})
|
||||||
}, [scene])
|
}, [scene])
|
||||||
|
|
||||||
|
const interactive = interactiveRef.current
|
||||||
|
const lightEffects = interactive?.effects.filter((e): e is LightEffect => e.kind === 'light') ?? []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Clone
|
<Clone
|
||||||
ref={ref}
|
ref={ref}
|
||||||
object={scene}
|
object={scene}
|
||||||
@@ -172,5 +181,56 @@ const ModelRenderer = ({ node }: { node: ItemNode }) => {
|
|||||||
rotation={node.asset.rotation}
|
rotation={node.asset.rotation}
|
||||||
{...handlers}
|
{...handlers}
|
||||||
/>
|
/>
|
||||||
|
{lightEffects.map((effect, i) => (
|
||||||
|
<ItemLight key={i} nodeId={node.id} effect={effect} interactive={interactive!} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ItemLight = ({
|
||||||
|
nodeId,
|
||||||
|
effect,
|
||||||
|
interactive,
|
||||||
|
}: {
|
||||||
|
nodeId: AnyNodeId
|
||||||
|
effect: LightEffect
|
||||||
|
interactive: Interactive
|
||||||
|
}) => {
|
||||||
|
const lightRef = useRef<PointLight>(null!)
|
||||||
|
// Precompute stable indices — interactive is frozen at mount
|
||||||
|
const toggleIndex = interactive.controls.findIndex((c) => c.kind === 'toggle')
|
||||||
|
const sliderIndex = interactive.controls.findIndex((c) => c.kind === 'slider')
|
||||||
|
const sliderControl = sliderIndex >= 0 ? (interactive.controls[sliderIndex] as SliderControl) : null
|
||||||
|
|
||||||
|
useFrame((_, delta) => {
|
||||||
|
if (!lightRef.current) return
|
||||||
|
const values = useInteractive.getState().items[nodeId]?.controlValues
|
||||||
|
|
||||||
|
const isOn = toggleIndex >= 0 ? Boolean(values?.[toggleIndex]) : true
|
||||||
|
|
||||||
|
// Normalize slider to 0-1 (default full intensity if no slider)
|
||||||
|
let t = 1
|
||||||
|
if (sliderControl) {
|
||||||
|
const raw = (values?.[sliderIndex] as number) ?? sliderControl.min
|
||||||
|
t = (raw - sliderControl.min) / (sliderControl.max - sliderControl.min)
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = isOn
|
||||||
|
? MathUtils.lerp(effect.intensityRange[0], effect.intensityRange[1], t)
|
||||||
|
: effect.intensityRange[0]
|
||||||
|
|
||||||
|
lightRef.current.intensity = MathUtils.lerp(lightRef.current.intensity, target, Math.min(delta * 12, 1))
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pointLight
|
||||||
|
ref={lightRef}
|
||||||
|
color={effect.color}
|
||||||
|
intensity={effect.intensityRange[0]}
|
||||||
|
distance={effect.distance ?? 0}
|
||||||
|
position={effect.offset}
|
||||||
|
castShadow={false}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ const ControlWidget = ({
|
|||||||
step={control.step}
|
step={control.step}
|
||||||
value={value as number}
|
value={value as number}
|
||||||
onChange={(e) => onChange(Number(e.target.value))}
|
onChange={(e) => onChange(Number(e.target.value))}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
)
|
)
|
||||||
@@ -159,6 +160,7 @@ const ControlWidget = ({
|
|||||||
step={1}
|
step={1}
|
||||||
value={value as number}
|
value={value as number}
|
||||||
onChange={(e) => onChange(Number(e.target.value))}
|
onChange={(e) => onChange(Number(e.target.value))}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user