remove community
This commit is contained in:
@@ -33,7 +33,7 @@
|
||||
"zustand": "^5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/typescript-config": "*",
|
||||
"@pascal/typescript-config": "*",
|
||||
"@types/react": "^19.2.2",
|
||||
"typescript": "5.9.3",
|
||||
"@types/three": "^0.183.0"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export { default as Viewer } from './components/viewer'
|
||||
export { default as useViewer } from './store/use-viewer'
|
||||
export { ASSETS_CDN_URL, resolveAssetUrl, resolveCdnUrl } from './lib/asset-url'
|
||||
export { SCENE_LAYER, ZONE_LAYER } from './lib/layers'
|
||||
export { default as useViewer } from './store/use-viewer'
|
||||
export { InteractiveSystem } from './systems/interactive/interactive-system'
|
||||
export { snapLevelsToTruePositions } from './systems/level/level-utils'
|
||||
export { snapLevelsToTruePositions } from './systems/level/level-utils'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { loadAssetUrl } from '@pascal-app/core'
|
||||
|
||||
// @ts-expect-error
|
||||
export const ASSETS_CDN_URL = process.env.NEXT_PUBLIC_ASSETS_CDN_URL || 'https://editor.pascal.app'
|
||||
|
||||
/**
|
||||
|
||||
+32
-32
@@ -1,35 +1,35 @@
|
||||
import type { AnyNode, BaseNode, BuildingNode, LevelNode, ZoneNode } from "@pascal-app/core";
|
||||
import type { Object3D } from "three";
|
||||
import type { AnyNode, BaseNode, BuildingNode, LevelNode, ZoneNode } from '@pascal-app/core'
|
||||
import type { Object3D } from 'three'
|
||||
type SelectionPath = {
|
||||
buildingId: BuildingNode["id"] | null;
|
||||
levelId: LevelNode["id"] | null;
|
||||
zoneId: ZoneNode["id"] | null;
|
||||
selectedIds: BaseNode["id"][];
|
||||
};
|
||||
buildingId: BuildingNode['id'] | null
|
||||
levelId: LevelNode['id'] | null
|
||||
zoneId: ZoneNode['id'] | null
|
||||
selectedIds: BaseNode['id'][]
|
||||
}
|
||||
type Outliner = {
|
||||
selectedObjects: Object3D[];
|
||||
hoveredObjects: Object3D[];
|
||||
};
|
||||
selectedObjects: Object3D[]
|
||||
hoveredObjects: Object3D[]
|
||||
}
|
||||
type ViewerState = {
|
||||
selection: SelectionPath;
|
||||
hoveredId: AnyNode['id'] | ZoneNode['id'] | null;
|
||||
setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void;
|
||||
cameraMode: 'perspective' | 'orthographic';
|
||||
setCameraMode: (mode: 'perspective' | 'orthographic') => void;
|
||||
levelMode: 'stacked' | 'exploded' | 'solo' | 'manual';
|
||||
setLevelMode: (mode: 'stacked' | 'exploded' | 'solo' | 'manual') => void;
|
||||
wallMode: 'up' | 'cutaway' | 'down';
|
||||
setWallMode: (mode: 'up' | 'cutaway' | 'down') => void;
|
||||
showScans: boolean;
|
||||
setShowScans: (show: boolean) => void;
|
||||
showGuides: boolean;
|
||||
setShowGuides: (show: boolean) => void;
|
||||
setSelection: (updates: Partial<SelectionPath>) => void;
|
||||
resetSelection: () => void;
|
||||
outliner: Outliner;
|
||||
exportScene: (() => Promise<void>) | null;
|
||||
setExportScene: (fn: (() => Promise<void>) | null) => void;
|
||||
};
|
||||
declare const useViewer: import("zustand").UseBoundStore<import("zustand").StoreApi<ViewerState>>;
|
||||
export default useViewer;
|
||||
//# sourceMappingURL=use-viewer.d.ts.map
|
||||
selection: SelectionPath
|
||||
hoveredId: AnyNode['id'] | ZoneNode['id'] | null
|
||||
setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void
|
||||
cameraMode: 'perspective' | 'orthographic'
|
||||
setCameraMode: (mode: 'perspective' | 'orthographic') => void
|
||||
levelMode: 'stacked' | 'exploded' | 'solo' | 'manual'
|
||||
setLevelMode: (mode: 'stacked' | 'exploded' | 'solo' | 'manual') => void
|
||||
wallMode: 'up' | 'cutaway' | 'down'
|
||||
setWallMode: (mode: 'up' | 'cutaway' | 'down') => void
|
||||
showScans: boolean
|
||||
setShowScans: (show: boolean) => void
|
||||
showGuides: boolean
|
||||
setShowGuides: (show: boolean) => void
|
||||
setSelection: (updates: Partial<SelectionPath>) => void
|
||||
resetSelection: () => void
|
||||
outliner: Outliner
|
||||
exportScene: (() => Promise<void>) | null
|
||||
setExportScene: (fn: (() => Promise<void>) | null) => void
|
||||
}
|
||||
declare const useViewer: import('zustand').UseBoundStore<import('zustand').StoreApi<ViewerState>>
|
||||
export default useViewer
|
||||
//# sourceMappingURL=use-viewer.d.ts.map
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import type {
|
||||
AnyNode,
|
||||
BaseNode,
|
||||
BuildingNode,
|
||||
LevelNode,
|
||||
ZoneNode,
|
||||
} from "@pascal-app/core";
|
||||
import type { Object3D } from "three";
|
||||
import type { AnyNode, BaseNode, BuildingNode, LevelNode, ZoneNode } from '@pascal-app/core'
|
||||
import type { Object3D } from 'three'
|
||||
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
|
||||
type SelectionPath = {
|
||||
buildingId: BuildingNode["id"] | null;
|
||||
levelId: LevelNode["id"] | null;
|
||||
zoneId: ZoneNode["id"] | null;
|
||||
selectedIds: BaseNode["id"][]; // For items/assets (multi-select)
|
||||
};
|
||||
buildingId: BuildingNode['id'] | null
|
||||
levelId: LevelNode['id'] | null
|
||||
zoneId: ZoneNode['id'] | null
|
||||
selectedIds: BaseNode['id'][] // For items/assets (multi-select)
|
||||
}
|
||||
|
||||
type Outliner = {
|
||||
selectedObjects: Object3D[];
|
||||
hoveredObjects: Object3D[];
|
||||
};
|
||||
selectedObjects: Object3D[]
|
||||
hoveredObjects: Object3D[]
|
||||
}
|
||||
|
||||
type ViewerState = {
|
||||
selection: SelectionPath
|
||||
@@ -52,7 +46,10 @@ type ViewerState = {
|
||||
|
||||
projectId: string | null
|
||||
setProjectId: (id: string | null) => void
|
||||
projectPreferences: Record<string, { showScans?: boolean, showGuides?: boolean, showGrid?: boolean }>
|
||||
projectPreferences: Record<
|
||||
string,
|
||||
{ showScans?: boolean; showGuides?: boolean; showGrid?: boolean }
|
||||
>
|
||||
|
||||
// Smart selection update
|
||||
setSelection: (updates: Partial<SelectionPath>) => void
|
||||
@@ -75,13 +72,13 @@ const useViewer = create<ViewerState>()(
|
||||
hoveredId: null,
|
||||
setHoveredId: (id) => set({ hoveredId: id }),
|
||||
|
||||
cameraMode: "perspective",
|
||||
cameraMode: 'perspective',
|
||||
setCameraMode: (mode) => set({ cameraMode: mode }),
|
||||
|
||||
theme: "light",
|
||||
theme: 'light',
|
||||
setTheme: (theme) => set({ theme }),
|
||||
|
||||
levelMode: "stacked",
|
||||
levelMode: 'stacked',
|
||||
setLevelMode: (mode) => set({ levelMode: mode }),
|
||||
|
||||
wallMode: 'up',
|
||||
@@ -90,75 +87,75 @@ const useViewer = create<ViewerState>()(
|
||||
showScans: true,
|
||||
setShowScans: (show) =>
|
||||
set((state) => {
|
||||
const projectPreferences = { ...(state.projectPreferences || {}) };
|
||||
const projectPreferences = { ...(state.projectPreferences || {}) }
|
||||
if (state.projectId) {
|
||||
projectPreferences[state.projectId] = {
|
||||
...(projectPreferences[state.projectId] || {}),
|
||||
showScans: show,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { showScans: show, projectPreferences };
|
||||
return { showScans: show, projectPreferences }
|
||||
}),
|
||||
|
||||
showGuides: true,
|
||||
setShowGuides: (show) =>
|
||||
set((state) => {
|
||||
const projectPreferences = { ...(state.projectPreferences || {}) };
|
||||
const projectPreferences = { ...(state.projectPreferences || {}) }
|
||||
if (state.projectId) {
|
||||
projectPreferences[state.projectId] = {
|
||||
...(projectPreferences[state.projectId] || {}),
|
||||
showGuides: show,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { showGuides: show, projectPreferences };
|
||||
return { showGuides: show, projectPreferences }
|
||||
}),
|
||||
|
||||
showGrid: true,
|
||||
setShowGrid: (show) =>
|
||||
set((state) => {
|
||||
const projectPreferences = { ...(state.projectPreferences || {}) };
|
||||
const projectPreferences = { ...(state.projectPreferences || {}) }
|
||||
if (state.projectId) {
|
||||
projectPreferences[state.projectId] = {
|
||||
...(projectPreferences[state.projectId] || {}),
|
||||
showGrid: show,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { showGrid: show, projectPreferences };
|
||||
return { showGrid: show, projectPreferences }
|
||||
}),
|
||||
|
||||
projectId: null,
|
||||
setProjectId: (id) =>
|
||||
set((state) => {
|
||||
if (!id) return { projectId: id };
|
||||
const prefs = state.projectPreferences?.[id] || {};
|
||||
if (!id) return { projectId: id }
|
||||
const prefs = state.projectPreferences?.[id] || {}
|
||||
return {
|
||||
projectId: id,
|
||||
showScans: prefs.showScans ?? true,
|
||||
showGuides: prefs.showGuides ?? true,
|
||||
showGrid: prefs.showGrid ?? true,
|
||||
};
|
||||
}
|
||||
}),
|
||||
projectPreferences: {},
|
||||
|
||||
setSelection: (updates) =>
|
||||
set((state) => {
|
||||
const newSelection = { ...state.selection, ...updates };
|
||||
const newSelection = { ...state.selection, ...updates }
|
||||
|
||||
// Hierarchy Guard: If we change a high-level parent, reset the children unless explicitly provided
|
||||
if (updates.buildingId !== undefined) {
|
||||
if (updates.levelId === undefined) newSelection.levelId = null;
|
||||
if (updates.zoneId === undefined) newSelection.zoneId = null;
|
||||
if (updates.selectedIds === undefined) newSelection.selectedIds = [];
|
||||
if (updates.levelId === undefined) newSelection.levelId = null
|
||||
if (updates.zoneId === undefined) newSelection.zoneId = null
|
||||
if (updates.selectedIds === undefined) newSelection.selectedIds = []
|
||||
}
|
||||
if (updates.levelId !== undefined) {
|
||||
if (updates.zoneId === undefined) newSelection.zoneId = null;
|
||||
if (updates.selectedIds === undefined) newSelection.selectedIds = [];
|
||||
if (updates.zoneId === undefined) newSelection.zoneId = null
|
||||
if (updates.selectedIds === undefined) newSelection.selectedIds = []
|
||||
}
|
||||
if (updates.zoneId !== undefined) {
|
||||
if (updates.selectedIds === undefined) newSelection.selectedIds = [];
|
||||
if (updates.selectedIds === undefined) newSelection.selectedIds = []
|
||||
}
|
||||
|
||||
return { selection: newSelection };
|
||||
return { selection: newSelection }
|
||||
}),
|
||||
|
||||
resetSelection: () =>
|
||||
@@ -190,6 +187,6 @@ const useViewer = create<ViewerState>()(
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
)
|
||||
|
||||
export default useViewer;
|
||||
export default useViewer
|
||||
|
||||
@@ -14,5 +14,5 @@ export const GuideSystem = () => {
|
||||
}
|
||||
})
|
||||
}, [showGuides])
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ const ItemControlsOverlay = ({ nodeId }: { nodeId: AnyNodeId }) => {
|
||||
return z?.polygon ?? null
|
||||
})
|
||||
|
||||
if (!itemObj || !controlValues || !node?.asset.interactive) return null
|
||||
if (!(itemObj && controlValues && node?.asset.interactive)) return null
|
||||
|
||||
const { controls } = node.asset.interactive
|
||||
const [, height] = node.asset.dimensions
|
||||
@@ -77,7 +77,7 @@ const ItemControlsOverlay = ({ nodeId }: { nodeId: AnyNodeId }) => {
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<Html center position={[0, height + 0.3, 0]} zIndexRange={[20, 0]} occlude distanceFactor={8}>
|
||||
<Html center distanceFactor={8} occlude position={[0, height + 0.3, 0]} zIndexRange={[20, 0]}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -96,10 +96,10 @@ const ItemControlsOverlay = ({ nodeId }: { nodeId: AnyNodeId }) => {
|
||||
>
|
||||
{controls.map((control, i) => (
|
||||
<ControlWidget
|
||||
key={i}
|
||||
control={control}
|
||||
value={controlValues[i] ?? false}
|
||||
key={i}
|
||||
onChange={(v) => setControlValue(nodeId, i, v)}
|
||||
value={controlValues[i] ?? false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -157,13 +157,13 @@ const ControlWidget = ({
|
||||
{control.unit ? ` ${control.unit}` : ''}
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min={control.min}
|
||||
max={control.max}
|
||||
step={control.step}
|
||||
value={value as number}
|
||||
min={control.min}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
step={control.step}
|
||||
type="range"
|
||||
value={value as number}
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
@@ -176,13 +176,13 @@ const ControlWidget = ({
|
||||
{control.label}: {value}°{control.unit}
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min={control.min}
|
||||
max={control.max}
|
||||
step={1}
|
||||
value={value as number}
|
||||
min={control.min}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
step={1}
|
||||
type="range"
|
||||
value={value as number}
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
export declare const LevelSystem: () => null;
|
||||
//# sourceMappingURL=level-system.d.ts.map
|
||||
export declare const LevelSystem: () => null
|
||||
//# sourceMappingURL=level-system.d.ts.map
|
||||
|
||||
@@ -14,7 +14,11 @@ export const LevelSystem = () => {
|
||||
|
||||
// Collect and sort levels by floor index so we can compute cumulative offsets.
|
||||
// Level 0 → Y=0, Level 1 → Y=height(0), Level 2 → Y=height(0)+height(1), etc.
|
||||
type LevelEntry = { levelId: string; index: number; obj: NonNullable<ReturnType<typeof sceneRegistry.nodes.get>> }
|
||||
type LevelEntry = {
|
||||
levelId: string
|
||||
index: number
|
||||
obj: NonNullable<ReturnType<typeof sceneRegistry.nodes.get>>
|
||||
}
|
||||
const entries: LevelEntry[] = []
|
||||
sceneRegistry.byType.level.forEach((levelId) => {
|
||||
const obj = sceneRegistry.nodes.get(levelId)
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { type CeilingNode, type LevelNode, sceneRegistry, useScene, type WallNode } from '@pascal-app/core'
|
||||
import {
|
||||
type CeilingNode,
|
||||
type LevelNode,
|
||||
sceneRegistry,
|
||||
useScene,
|
||||
type WallNode,
|
||||
} from '@pascal-app/core'
|
||||
|
||||
export const DEFAULT_LEVEL_HEIGHT = 2.5
|
||||
|
||||
@@ -75,7 +81,9 @@ export function snapLevelsToTruePositions(): () => void {
|
||||
entries.sort((a, b) => a.index - b.index)
|
||||
|
||||
// Snapshot current Y and visibility so we can restore them after the render
|
||||
const snapshot = new Map(entries.map(({ levelId, obj }) => [levelId, { y: obj.position.y, visible: obj.visible }]))
|
||||
const snapshot = new Map(
|
||||
entries.map(({ levelId, obj }) => [levelId, { y: obj.position.y, visible: obj.visible }]),
|
||||
)
|
||||
|
||||
// Snap to true stacked positions and make all levels visible
|
||||
let cumulativeY = 0
|
||||
|
||||
@@ -14,5 +14,5 @@ export const ScanSystem = () => {
|
||||
}
|
||||
})
|
||||
}, [showScans])
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ export const WallCutout = () => {
|
||||
} else if (wallMode === 'down') {
|
||||
hideWall = true
|
||||
} else {
|
||||
wallMesh.getWorldDirection(v)
|
||||
wallMesh.getWorldDirection(v)
|
||||
if (v.dot(u) < 0) {
|
||||
// Front side
|
||||
if (wallNode.frontSide === 'exterior' && wallNode.backSide !== 'exterior') {
|
||||
|
||||
@@ -16,7 +16,7 @@ export const ZoneSystem = () => {
|
||||
const pendingZoneRef = useRef<string | null>(null)
|
||||
const pendingZoneSinceRef = useRef(0)
|
||||
|
||||
useFrame(({clock}, delta) => {
|
||||
useFrame(({ clock }, delta) => {
|
||||
const hoveredId = useViewer.getState().hoveredId
|
||||
let rawZone: string | null = null
|
||||
|
||||
@@ -35,9 +35,8 @@ export const ZoneSystem = () => {
|
||||
|
||||
// Apply non-null immediately; debounce null to filter out brief exits
|
||||
const age = clock.elapsedTime * 1000 - pendingZoneSinceRef.current
|
||||
const highlightedZone = rawZone !== null
|
||||
? rawZone
|
||||
: age >= EXIT_DEBOUNCE_MS ? null : lastHighlightedZoneRef.current
|
||||
const highlightedZone =
|
||||
rawZone !== null ? rawZone : age >= EXIT_DEBOUNCE_MS ? null : lastHighlightedZoneRef.current
|
||||
|
||||
// Detect stable zone change
|
||||
if (highlightedZone !== lastHighlightedZoneRef.current) {
|
||||
@@ -45,7 +44,7 @@ export const ZoneSystem = () => {
|
||||
if (lastHighlightedZoneRef.current) {
|
||||
const prevLabel = document.getElementById(`${lastHighlightedZoneRef.current}-label`)
|
||||
const pin = prevLabel?.querySelector('.label-pin') as HTMLElement | null
|
||||
if (pin) pin.style.opacity = '0'
|
||||
if (pin) pin.style.opacity = '0'
|
||||
}
|
||||
// Fade in new zone label-pin
|
||||
if (highlightedZone) {
|
||||
@@ -92,7 +91,6 @@ export const ZoneSystem = () => {
|
||||
const currentOpacity = material.userData.uOpacity.value
|
||||
material.userData.uOpacity.value = MathUtils.lerp(currentOpacity, targetOpacity, lerpSpeed)
|
||||
}
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
{
|
||||
"extends": "@repo/typescript-config/react-library.json",
|
||||
"extends": "@pascal/typescript-config/react-library.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"noEmit": false,
|
||||
"composite": true,
|
||||
"incremental": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"references": [
|
||||
{ "path": "../core" }
|
||||
]
|
||||
"references": [{ "path": "../core" }]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user