remove community

This commit is contained in:
wass08
2026-03-13 09:07:46 +01:00
parent 3eeba322a8
commit f2501be2bf
693 changed files with 8264 additions and 35562 deletions
@@ -9,7 +9,7 @@ import { NodeRenderer } from '../node-renderer'
// - Back face (looking up at ceiling from below): solid
// - Front face (looking down at ceiling from above): 30% opacity
const ceilingTopMaterial = new MeshBasicNodeMaterial({
color: 0xb5a78d,
color: 0xb5_a7_8d,
transparent: true,
depthWrite: false,
side: FrontSide,
@@ -18,7 +18,7 @@ const ceilingTopMaterial = new MeshBasicNodeMaterial({
})
const ceilingBottomMaterial = new MeshBasicNodeMaterial({
color: 0x999999,
color: 0x99_99_99,
transparent: true,
side: BackSide,
})
@@ -52,10 +52,16 @@ export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
const handlers = useNodeEvents(node, 'ceiling')
return (
<mesh ref={ref} material={ceilingBottomMaterial}>
<mesh material={ceilingBottomMaterial} ref={ref}>
{/* CeilingSystem will replace this geometry in the next frame */}
<boxGeometry args={[0, 0, 0]} />
<mesh name="ceiling-grid" material={ceilingTopMaterial} {...handlers} visible={false} scale={0}>
<mesh
material={ceilingTopMaterial}
name="ceiling-grid"
{...handlers}
scale={0}
visible={false}
>
<boxGeometry args={[0, 0, 0]} />
</mesh>
{node.children.map((childId) => (
@@ -1,4 +1,4 @@
import { useRegistry, type DoorNode } from '@pascal-app/core'
import { type DoorNode, useRegistry } from '@pascal-app/core'
import { useRef } from 'react'
import type { Mesh } from 'three'
import { useNodeEvents } from '../../../hooks/use-node-events'
@@ -12,12 +12,12 @@ export const DoorRenderer = ({ node }: { node: DoorNode }) => {
return (
<mesh
ref={ref}
castShadow
receiveShadow
visible={node.visible}
position={node.position}
receiveShadow
ref={ref}
rotation={node.rotation}
visible={node.visible}
{...(isTransient ? {} : handlers)}
>
{/* DoorSystem replaces this geometry each time the node is dirty */}
@@ -15,10 +15,15 @@ export const GuideRenderer = ({ node }: { node: GuideNode }) => {
const resolvedUrl = useAssetUrl(node.url)
return (
<group ref={ref} visible={showGuides} position={node.position} rotation={[0, node.rotation[1], 0]}>
<group
position={node.position}
ref={ref}
rotation={[0, node.rotation[1], 0]}
visible={showGuides}
>
{resolvedUrl && (
<Suspense>
<GuidePlane url={resolvedUrl} scale={node.scale} opacity={node.opacity} />
<GuidePlane opacity={node.opacity} scale={node.scale} url={resolvedUrl} />
</Suspense>
)}
</group>
@@ -53,10 +58,10 @@ const GuidePlane = ({ url, scale, opacity }: { url: string; scale: number; opaci
return (
<mesh
rotation={[-Math.PI / 2, 0, 0]}
frustumCulled={false}
material={material}
raycast={() => {}}
frustumCulled={false}
rotation={[-Math.PI / 2, 0, 0]}
>
<planeGeometry args={[width, height]} boundingBox={null} boundingSphere={null} />
</mesh>
@@ -24,7 +24,7 @@ import { NodeRenderer } from '../node-renderer'
// Shared materials to avoid creating new instances for every mesh
const defaultMaterial = new MeshStandardNodeMaterial({
color: 0xffffff,
color: 0xff_ff_ff,
roughness: 1,
metalness: 0,
})
@@ -53,7 +53,7 @@ export const ItemRenderer = ({ node }: { node: ItemNode }) => {
useRegistry(node.id, node.type, ref)
return (
<group position={node.position} rotation={node.rotation} ref={ref} visible={node.visible}>
<group position={node.position} ref={ref} rotation={node.rotation} visible={node.visible}>
<Suspense fallback={<PreviewModel node={node} />}>
<ModelRenderer node={node} />
</Suspense>
@@ -78,7 +78,7 @@ previewMaterial.transparent = true
const PreviewModel = ({ node }: { node: ItemNode }) => {
return (
<mesh position-y={node.asset.dimensions[1] / 2} material={previewMaterial}>
<mesh material={previewMaterial} position-y={node.asset.dimensions[1] / 2}>
<boxGeometry
args={[node.asset.dimensions[0], node.asset.dimensions[1], node.asset.dimensions[2]]}
/>
@@ -150,24 +150,24 @@ const ModelRenderer = ({ node }: { node: ItemNode }) => {
return (
<>
<Clone
ref={ref}
object={scene}
scale={multiplyScales(node.asset.scale || [1, 1, 1], node.scale || [1, 1, 1])}
position={node.asset.offset}
ref={ref}
rotation={node.asset.rotation}
scale={multiplyScales(node.asset.scale || [1, 1, 1], node.scale || [1, 1, 1])}
{...handlers}
/>
{animations.length > 0 && (
<ItemAnimation
nodeId={node.id}
animEffect={animEffect}
interactive={interactive ?? null}
actions={actions}
animations={animations}
animEffect={animEffect}
interactive={interactive ?? null}
nodeId={node.id}
/>
)}
{lightEffects.map((effect, i) => (
<ItemLight key={i} nodeId={node.id} effect={effect} interactive={interactive!} />
<ItemLight effect={effect} interactive={interactive!} key={i} nodeId={node.id} />
))}
</>
)
@@ -287,12 +287,12 @@ const ItemLight = ({
return (
<pointLight
ref={lightRef}
color={effect.color}
intensity={effect.intensityRange[0]}
distance={effect.distance ?? 0}
position={effect.offset}
castShadow={false}
color={effect.color}
distance={effect.distance ?? 0}
intensity={effect.intensityRange[0]}
position={effect.offset}
ref={lightRef}
/>
)
}
@@ -12,10 +12,10 @@ export const RoofRenderer = ({ node }: { node: RoofNode }) => {
return (
<mesh
ref={ref}
castShadow
receiveShadow
position={node.position}
receiveShadow
ref={ref}
rotation-y={node.rotation}
visible={node.visible}
{...handlers}
@@ -14,15 +14,15 @@ export const ScanRenderer = ({ node }: { node: ScanNode }) => {
return (
<group
ref={ref}
visible={showScans}
position={node.position}
ref={ref}
rotation={node.rotation}
scale={[node.scale, node.scale, node.scale]}
visible={showScans}
>
{resolvedUrl && (
<Suspense>
<ScanModel url={resolvedUrl} opacity={node.opacity} />
<ScanModel opacity={node.opacity} url={resolvedUrl} />
</Suspense>
)}
</group>
@@ -41,11 +41,11 @@ const ScanModel = ({ url, opacity }: { url: string; opacity: number }) => {
if (isTransparent) {
material.transparent = true
material.opacity = normalizedOpacity
material.depthWrite = false;
material.depthWrite = false
} else {
material.transparent = false
material.opacity = 1
material.depthWrite = true;
material.depthWrite = true
}
material.needsUpdate = true
}
@@ -1,10 +1,10 @@
"use client";
'use client'
import { useScene } from "@pascal-app/core";
import { NodeRenderer } from "./node-renderer";
import { useScene } from '@pascal-app/core'
import { NodeRenderer } from './node-renderer'
export const SceneRenderer = () => {
const rootNodes = useScene((state) => state.rootNodeIds);
const rootNodes = useScene((state) => state.rootNodeIds)
return (
<group name="scene-renderer">
@@ -12,5 +12,5 @@ export const SceneRenderer = () => {
<NodeRenderer key={nodeId} nodeId={nodeId} />
))}
</group>
);
};
)
}
@@ -61,7 +61,7 @@ export const SiteRenderer = ({ node }: { node: SiteNode }) => {
const handlers = useNodeEvents(node, 'site')
if (!node || !floorShape || !lineGeometry) {
if (!(node && floorShape && lineGeometry)) {
return null
}
@@ -76,17 +76,16 @@ export const SiteRenderer = ({ node }: { node: SiteNode }) => {
))}
{/* Transparent floor fill */}
<mesh position={[0, Y_OFFSET - 0.005, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
<mesh position={[0, Y_OFFSET - 0.005, 0]} receiveShadow rotation={[-Math.PI / 2, 0, 0]}>
<shapeGeometry args={[floorShape]} />
<shadowMaterial transparent opacity={0.75} />
<shadowMaterial opacity={0.75} transparent />
</mesh>
{/* Simple boundary line */}
{/* @ts-ignore */}
<line geometry={lineGeometry} frustumCulled={false} renderOrder={9}>
<lineBasicMaterial color="#f59e0b" linewidth={2} transparent opacity={0.6} />
<line frustumCulled={false} geometry={lineGeometry} renderOrder={9}>
<lineBasicMaterial color="#f59e0b" linewidth={2} opacity={0.6} transparent />
</line>
</group>
)
}
@@ -11,7 +11,7 @@ export const SlabRenderer = ({ node }: { node: SlabNode }) => {
const handlers = useNodeEvents(node, 'slab')
return (
<mesh ref={ref} castShadow receiveShadow {...handlers} visible={node.visible}>
<mesh castShadow receiveShadow ref={ref} {...handlers} visible={node.visible}>
{/* SlabSystem will replace this geometry in the next frame */}
<boxGeometry args={[0, 0, 0]} />
<meshStandardMaterial color="#e5e5e5" />
@@ -12,7 +12,7 @@ export const WallRenderer = ({ node }: { node: WallNode }) => {
const handlers = useNodeEvents(node, 'wall')
return (
<mesh ref={ref} castShadow receiveShadow visible={node.visible}>
<mesh castShadow receiveShadow ref={ref} visible={node.visible}>
{/* WallSystem will replace this geometry in the next frame */}
<boxGeometry args={[0, 0, 0]} />
{/* Collision mesh: full-wall geometry (no cutouts) for pointer events */}
@@ -12,12 +12,12 @@ export const WindowRenderer = ({ node }: { node: WindowNode }) => {
return (
<mesh
ref={ref}
castShadow
receiveShadow
visible={node.visible}
position={node.position}
receiveShadow
ref={ref}
rotation={node.rotation}
visible={node.visible}
{...(isTransient ? {} : handlers)}
>
{/* WindowSystem replaces this geometry each time the node is dirty */}
@@ -173,80 +173,83 @@ export const ZoneRenderer = ({ node }: { node: ZoneNode }) => {
const handlers = useNodeEvents(node, 'zone')
if (!node || !floorShape || !wallGeometry || !floorMaterial || !wallMaterial) {
if (!(node && floorShape && wallGeometry && floorMaterial && wallMaterial)) {
return null
}
return (
<group ref={ref} {...handlers} userData={{ labelPosition: [centroid[0], 1, centroid[1]] }}>
<Html
name="label"
position={[centroid[0], 1, centroid[1]]}
style={{ pointerEvents: 'none', }}
style={{ pointerEvents: 'none' }}
zIndexRange={[10, 0]}
>
<div id={`${node.id}-label`} style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
transform: 'translate3d(-50%, -50%, 0)',
opacity: 0,
transition: 'opacity 0.3s ease-in-out',
}}>
<div
style={{
width: 'max-content',
color: 'white',
textShadow: `-1px -1px 0 ${node.color}, 1px -1px 0 ${node.color}, -1px 1px 0 ${node.color}, 1px 1px 0 ${node.color}`,
textAlign: 'center',
}}
>
<span>{node.name}</span>
</div>
<div
className="label-pin"
id={`${node.id}-label`}
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
marginTop: '2px',
transform: 'translate3d(-50%, -50%, 0)',
opacity: 0,
transition: 'opacity 0.5s ease-in-out',
transition: 'opacity 0.3s ease-in-out',
}}
>
<div
style={{
width: '2px',
height: '40px',
backgroundColor: node.color,
}}/>
>
<div
style={{
width: '10px',
height: '10px',
borderRadius: '50%',
backgroundColor: node.color,
border: `1px solid white`,
width: 'max-content',
color: 'white',
textShadow: `-1px -1px 0 ${node.color}, 1px -1px 0 ${node.color}, -1px 1px 0 ${node.color}, 1px 1px 0 ${node.color}`,
textAlign: 'center',
}}
/>
>
<span>{node.name}</span>
</div>
<div
className="label-pin"
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
marginTop: '2px',
opacity: 0,
transition: 'opacity 0.5s ease-in-out',
}}
>
<div
style={{
width: '2px',
height: '40px',
backgroundColor: node.color,
}}
/>
<div
style={{
width: '10px',
height: '10px',
borderRadius: '50%',
backgroundColor: node.color,
border: '1px solid white',
}}
/>
</div>
</div>
</Html>
{/* Floor fill */}
<mesh
position={[0, Y_OFFSET, 0]}
rotation={[-Math.PI / 2, 0, 0]}
layers={ZONE_LAYER}
material={floorMaterial}
name="floor"
layers={ZONE_LAYER}
position={[0, Y_OFFSET, 0]}
rotation={[-Math.PI / 2, 0, 0]}
>
<shapeGeometry args={[floorShape]} />
</mesh>
{/* Wall borders with gradient */}
<mesh geometry={wallGeometry} material={wallMaterial} name="walls" layers={ZONE_LAYER} />
<mesh geometry={wallGeometry} layers={ZONE_LAYER} material={wallMaterial} name="walls" />
</group>
)
}
@@ -62,7 +62,7 @@ export const GroundOccluder = () => {
}, [nodes])
return (
<mesh rotation-x={-Math.PI / 2} position-y={-0.05}>
<mesh position-y={-0.05} rotation-x={-Math.PI / 2}>
<shapeGeometry args={[shape]} />
<meshBasicMaterial
color={bgColor}
@@ -34,7 +34,7 @@ function AnimatedBackground({ isDark }: { isDark: boolean }) {
const dt = Math.min(delta, 0.1) * 4
const targetHex = isDark ? '#1f2433' : '#ffffff'
if (!scene.background || !(scene.background instanceof THREE.Color)) {
if (!(scene.background && scene.background instanceof THREE.Color)) {
scene.background = new THREE.Color(targetHex)
initialized.current = true
return
@@ -69,8 +69,9 @@ const Viewer: React.FC<ViewerProps> = ({ children, selectionManager = 'default'
return (
<Canvas
dpr={[1, 1.5]}
camera={{ position: [50, 50, 50], fov: 50 }}
className={`transition-colors duration-700 ${theme === 'dark' ? 'bg-[#1f2433]' : 'bg-[#fafafa]'}`}
dpr={[1, 1.5]}
gl={async (props) => {
const renderer = new THREE.WebGPURenderer(props as any)
renderer.toneMapping = THREE.ACESFilmicToneMapping
@@ -82,7 +83,6 @@ const Viewer: React.FC<ViewerProps> = ({ children, selectionManager = 'default'
type: THREE.PCFShadowMap,
enabled: true,
}}
camera={{ position: [50, 50, 50], fov: 50 }}
>
{/* <AnimatedBackground isDark={theme === 'dark'} /> */}
<GroundOccluder />
@@ -1,7 +1,7 @@
import { useRef, useMemo } from 'react'
import { useFrame } from '@react-three/fiber'
import { useMemo, useRef } from 'react'
import type { AmbientLight, DirectionalLight, OrthographicCamera } from 'three/webgpu'
import * as THREE from 'three/webgpu'
import type { DirectionalLight, OrthographicCamera, AmbientLight } from 'three/webgpu'
import useViewer from '../../store/use-viewer'
export function Lights() {
@@ -17,13 +17,16 @@ export function Lights() {
const ambientRef = useRef<AmbientLight>(null)
const initialized = useRef(false)
const targets = useMemo(() => ({
l1Color: new THREE.Color(),
l2Color: new THREE.Color(),
l3Color: new THREE.Color(),
ambColor: new THREE.Color(),
}), [])
const targets = useMemo(
() => ({
l1Color: new THREE.Color(),
l2Color: new THREE.Color(),
l3Color: new THREE.Color(),
ambColor: new THREE.Color(),
}),
[],
)
useFrame((_, delta) => {
// clamp delta to avoid huge jumps on tab switch
@@ -33,7 +36,7 @@ export function Lights() {
if (light1Ref.current) {
light1Ref.current.intensity = isDark ? 0.8 : 4
light1Ref.current.color.set(isDark ? '#e0e5ff' : '#ffffff')
// @ts-ignore
if (light1Ref.current.shadow) light1Ref.current.shadow.intensity = isDark ? 0.8 : 0.4
}
if (light2Ref.current) {
@@ -53,33 +56,51 @@ export function Lights() {
}
if (light1Ref.current) {
light1Ref.current.intensity = THREE.MathUtils.lerp(light1Ref.current.intensity, isDark ? 0.8 : 4, dt)
light1Ref.current.intensity = THREE.MathUtils.lerp(
light1Ref.current.intensity,
isDark ? 0.8 : 4,
dt,
)
targets.l1Color.set(isDark ? '#e0e5ff' : '#ffffff')
light1Ref.current.color.lerp(targets.l1Color, dt)
if (light1Ref.current.shadow) {
// @ts-ignore
if (light1Ref.current.shadow.intensity !== undefined) {
// @ts-ignore
light1Ref.current.shadow.intensity = THREE.MathUtils.lerp(light1Ref.current.shadow.intensity, isDark ? 0.8 : 0.4, dt)
light1Ref.current.shadow.intensity = THREE.MathUtils.lerp(
light1Ref.current.shadow.intensity,
isDark ? 0.8 : 0.4,
dt,
)
}
}
}
if (light2Ref.current) {
light2Ref.current.intensity = THREE.MathUtils.lerp(light2Ref.current.intensity, isDark ? 0.2 : 0.75, dt)
light2Ref.current.intensity = THREE.MathUtils.lerp(
light2Ref.current.intensity,
isDark ? 0.2 : 0.75,
dt,
)
targets.l2Color.set(isDark ? '#8090ff' : '#ffffff')
light2Ref.current.color.lerp(targets.l2Color, dt)
}
if (light3Ref.current) {
light3Ref.current.intensity = THREE.MathUtils.lerp(light3Ref.current.intensity, isDark ? 0.3 : 1, dt)
light3Ref.current.intensity = THREE.MathUtils.lerp(
light3Ref.current.intensity,
isDark ? 0.3 : 1,
dt,
)
targets.l3Color.set(isDark ? '#a0b0ff' : '#ffffff')
light3Ref.current.color.lerp(targets.l3Color, dt)
}
if (ambientRef.current) {
ambientRef.current.intensity = THREE.MathUtils.lerp(ambientRef.current.intensity, isDark ? 0.15 : 0.5, dt)
ambientRef.current.intensity = THREE.MathUtils.lerp(
ambientRef.current.intensity,
isDark ? 0.15 : 0.5,
dt,
)
targets.ambColor.set(isDark ? '#a0b0ff' : '#ffffff')
ambientRef.current.color.lerp(targets.ambColor, dt)
}
@@ -88,35 +109,29 @@ export function Lights() {
return (
<>
<directionalLight
ref={light1Ref}
position={[10, 10, 10]}
castShadow
position={[10, 10, 10]}
ref={light1Ref}
shadow-bias={-0.002}
shadow-normalBias={0.3}
shadow-mapSize={[1024, 1024]}
shadow-normalBias={0.3}
shadow-radius={3}
>
<orthographicCamera
ref={shadowCamera}
attach="shadow-camera"
near={1}
bottom={-shadowCameraSize}
far={100}
left={-shadowCameraSize}
near={1}
ref={shadowCamera}
right={shadowCameraSize}
top={shadowCameraSize}
bottom={-shadowCameraSize}
/>
</directionalLight>
<directionalLight
ref={light2Ref}
position={[-10, 10, -10]}
/>
<directionalLight position={[-10, 10, -10]} ref={light2Ref} />
<directionalLight
ref={light3Ref}
position={[-10, 10, 10]}
/>
<directionalLight position={[-10, 10, 10]} ref={light3Ref} />
<ambientLight ref={ambientRef} />
</>
@@ -24,8 +24,8 @@ import {
} from 'three/tsl'
import { RenderPipeline, type WebGPURenderer } from 'three/webgpu'
import useViewer from '../../store/use-viewer'
import { SCENE_LAYER, ZONE_LAYER } from '../../lib/layers'
import useViewer from '../../store/use-viewer'
// SSGI Parameters - adjust these to fine-tune global illumination and ambient occlusion
export const SSGI_PARAMS = {
@@ -94,7 +94,7 @@ const PostProcessingPasses = () => {
}, [renderer])
useEffect(() => {
if (!renderer || !scene || !camera || !isInitialized) {
if (!(renderer && scene && camera && isInitialized)) {
return
}
@@ -105,10 +105,10 @@ const PostProcessingPasses = () => {
const scenePass = pass(scene, camera)
scenePass.setMRT(
mrt({
output: output,
diffuseColor: diffuseColor,
output,
diffuseColor,
normal: directionToColor(normalView),
velocity: velocity,
velocity,
}),
)
@@ -169,8 +169,8 @@ const PostProcessingPasses = () => {
const edgeStrength = uniform(3)
const edgeGlow = uniform(0)
const edgeThickness = uniform(1)
const visibleEdgeColor = uniform(new Color(0xffffff))
const hiddenEdgeColor = uniform(new Color(0xf3ff47))
const visibleEdgeColor = uniform(new Color(0xff_ff_ff))
const hiddenEdgeColor = uniform(new Color(0xf3_ff_47))
const outlinePass = outline(scene, camera, {
selectedObjects: useViewer.getState().outliner.selectedObjects,
@@ -192,8 +192,8 @@ const PostProcessingPasses = () => {
const edgeGlow = uniform(0.5)
const edgeThickness = uniform(1.5)
const pulsePeriod = uniform(3)
const visibleEdgeColor = uniform(new Color(0x00aaff))
const hiddenEdgeColor = uniform(new Color(0xf3ff47))
const visibleEdgeColor = uniform(new Color(0x00_aa_ff))
const hiddenEdgeColor = uniform(new Color(0xf3_ff_47))
const outlinePass = outline(scene, camera, {
selectedObjects: useViewer.getState().outliner.hoveredObjects,
@@ -230,10 +230,7 @@ const PostProcessingPasses = () => {
// (zones over background, pure background) use compositePass.rgb directly.
const traaRgb = (traaOutput as any).rgb
const colorSource = mix(compositePass.rgb, traaRgb, hasGeometry)
const finalOutput = vec4(
mix(bgUniform.current, colorSource, contentAlpha),
float(1),
)
const finalOutput = vec4(mix(bgUniform.current, colorSource, contentAlpha), float(1))
const renderPipeline = new RenderPipeline(renderer as unknown as WebGPURenderer)
renderPipeline.outputNode = finalOutput
@@ -95,7 +95,9 @@ const isNodeOnLevel = (node: AnyNode, levelId: string): boolean => {
}
// Ceiling/slab/roof-attached items: check if parent structure is on the level
if (
(parentNode?.type === 'ceiling' || parentNode?.type === 'slab' || parentNode?.type === 'roof') &&
(parentNode?.type === 'ceiling' ||
parentNode?.type === 'slab' ||
parentNode?.type === 'roof') &&
parentNode.parentId === levelId
) {
return true
@@ -160,19 +162,18 @@ const getStrategy = (): SelectionStrategy | null => {
const { buildingId, levelId, zoneId } = useViewer.getState().selection
const computeNextIds = (node: AnyNode, selectedIds: string[], event?: any): string[] => {
const isMeta = event?.metaKey || event?.nativeEvent?.metaKey;
const isCtrl = event?.ctrlKey || event?.nativeEvent?.ctrlKey;
const isMeta = event?.metaKey || event?.nativeEvent?.metaKey
const isCtrl = event?.ctrlKey || event?.nativeEvent?.ctrlKey
if (isMeta || isCtrl) {
if (selectedIds.includes(node.id)) {
return selectedIds.filter((id) => id !== node.id);
} else {
return [...selectedIds, node.id];
return selectedIds.filter((id) => id !== node.id)
}
return [...selectedIds, node.id]
}
return [node.id];
};
return [node.id]
}
// No building selected -> can select buildings
if (!buildingId) {
@@ -221,7 +222,9 @@ const getStrategy = (): SelectionStrategy | null => {
types: ['wall', 'item', 'slab', 'ceiling', 'roof', 'window', 'door'],
handleClick: (node, nativeEvent) => {
const { selectedIds } = useViewer.getState().selection
useViewer.getState().setSelection({ selectedIds: computeNextIds(node, selectedIds, nativeEvent) })
useViewer
.getState()
.setSelection({ selectedIds: computeNextIds(node, selectedIds, nativeEvent) })
},
handleDeselect: () => {
const { selectedIds } = useViewer.getState().selection