ceiling system + reveal/hide
This commit is contained in:
@@ -2,26 +2,27 @@
|
|||||||
|
|
||||||
import { initSpaceDetectionSync, initSpatialGridSync, useScene } from '@pascal-app/core'
|
import { initSpaceDetectionSync, initSpatialGridSync, useScene } from '@pascal-app/core'
|
||||||
import { Viewer } from '@pascal-app/viewer'
|
import { Viewer } from '@pascal-app/viewer'
|
||||||
import { useKeyboard } from '@/hooks/use-keyboard'
|
import { ThumbnailGenerator } from '@/app/viewer/[id]/thumbnail-generator'
|
||||||
import useEditor from '@/store/use-editor'
|
|
||||||
import { useProjectScene } from '@/features/community/lib/models/hooks'
|
|
||||||
import { useLocalProjectScene } from '@/features/community/lib/local-storage/hooks'
|
|
||||||
import { useAuth } from '@/features/community/lib/auth/hooks'
|
import { useAuth } from '@/features/community/lib/auth/hooks'
|
||||||
|
import { useLocalProjectScene } from '@/features/community/lib/local-storage/hooks'
|
||||||
|
import { useProjectScene } from '@/features/community/lib/models/hooks'
|
||||||
|
import { useKeyboard } from '@/hooks/use-keyboard'
|
||||||
|
import { initSFXBus } from '@/lib/sfx-bus'
|
||||||
|
import useEditor from '@/store/use-editor'
|
||||||
|
import { FeedbackDialog } from '../feedback-dialog'
|
||||||
|
import { PascalRadio } from '../pascal-radio'
|
||||||
|
import { CeilingSystem } from '../systems/ceiling/ceiling-system'
|
||||||
import { ZoneSystem } from '../systems/zone/zone-system'
|
import { ZoneSystem } from '../systems/zone/zone-system'
|
||||||
import { ToolManager } from '../tools/tool-manager'
|
import { ToolManager } from '../tools/tool-manager'
|
||||||
import { ActionMenu } from '../ui/action-menu'
|
import { ActionMenu } from '../ui/action-menu'
|
||||||
import { FeedbackDialog } from '../feedback-dialog'
|
|
||||||
import { PascalRadio } from '../pascal-radio'
|
|
||||||
import { PanelManager } from '../ui/panels/panel-manager'
|
|
||||||
import { HelperManager } from '../ui/helpers/helper-manager'
|
import { HelperManager } from '../ui/helpers/helper-manager'
|
||||||
|
import { PanelManager } from '../ui/panels/panel-manager'
|
||||||
import { SidebarProvider } from '../ui/primitives/sidebar'
|
import { SidebarProvider } from '../ui/primitives/sidebar'
|
||||||
import { AppSidebar } from '../ui/sidebar/app-sidebar'
|
import { AppSidebar } from '../ui/sidebar/app-sidebar'
|
||||||
import { CustomCameraControls } from './custom-camera-controls'
|
import { CustomCameraControls } from './custom-camera-controls'
|
||||||
import { ExportManager } from './export-manager'
|
import { ExportManager } from './export-manager'
|
||||||
import { Grid } from './grid'
|
import { Grid } from './grid'
|
||||||
import { SelectionManager } from './selection-manager'
|
import { SelectionManager } from './selection-manager'
|
||||||
import { initSFXBus } from '@/lib/sfx-bus'
|
|
||||||
import { ThumbnailGenerator } from '@/app/viewer/[id]/thumbnail-generator'
|
|
||||||
|
|
||||||
// Load default scene initially (will be replaced when project loads)
|
// Load default scene initially (will be replaced when project loads)
|
||||||
useScene.getState().loadScene()
|
useScene.getState().loadScene()
|
||||||
@@ -74,6 +75,7 @@ export default function Editor({ projectId }: EditorProps) {
|
|||||||
<ExportManager />
|
<ExportManager />
|
||||||
{/* Editor only system to toggle zone visibility */}
|
{/* Editor only system to toggle zone visibility */}
|
||||||
<ZoneSystem />
|
<ZoneSystem />
|
||||||
|
<CeilingSystem />
|
||||||
{/* <Stats /> */}
|
{/* <Stats /> */}
|
||||||
<Grid cellColor="#aaa" sectionColor="#ccc" fadeDistance={500} />
|
<Grid cellColor="#aaa" sectionColor="#ccc" fadeDistance={500} />
|
||||||
<ToolManager />
|
<ToolManager />
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { type AnyNodeId, sceneRegistry, useScene } from '@pascal-app/core'
|
||||||
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import useEditor from '@/store/use-editor'
|
||||||
|
|
||||||
|
export const CeilingSystem = () => {
|
||||||
|
const tool = useEditor((state) => state.tool)
|
||||||
|
const selectedItem = useEditor((state) => state.selectedItem)
|
||||||
|
const movingNode = useEditor((state) => state.movingNode)
|
||||||
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
||||||
|
useEffect(() => {
|
||||||
|
const shouldShowGrid =
|
||||||
|
tool === 'ceiling' ||
|
||||||
|
selectedItem?.attachTo === 'ceiling' ||
|
||||||
|
(movingNode?.type === 'item' && movingNode?.asset?.attachTo === 'ceiling') ||
|
||||||
|
selectedIds.some((id) => {
|
||||||
|
const node = useScene.getState().nodes[id as AnyNodeId]
|
||||||
|
return node?.type === 'ceiling'
|
||||||
|
})
|
||||||
|
|
||||||
|
const ceilings = sceneRegistry.byType.ceiling
|
||||||
|
ceilings.forEach((ceiling) => {
|
||||||
|
const mesh = sceneRegistry.nodes.get(ceiling)
|
||||||
|
if (mesh) {
|
||||||
|
const ceilingGrid = mesh.getObjectByName('ceiling-grid')
|
||||||
|
if (ceilingGrid) {
|
||||||
|
ceilingGrid.visible = shouldShowGrid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [tool, selectedItem, movingNode, selectedIds])
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ import { useViewer } from '@pascal-app/viewer'
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { BufferGeometry, DoubleSide, type Line, type Mesh, Shape, Vector3 } from 'three'
|
import { BufferGeometry, DoubleSide, type Line, type Mesh, Shape, Vector3 } from 'three'
|
||||||
import { sfxEmitter } from '@/lib/sfx-bus'
|
import { sfxEmitter } from '@/lib/sfx-bus'
|
||||||
import useEditor from '@/store/use-editor'
|
|
||||||
import { CursorSphere } from '../shared/cursor-sphere'
|
import { CursorSphere } from '../shared/cursor-sphere'
|
||||||
|
|
||||||
const CEILING_HEIGHT = 2.52
|
const CEILING_HEIGHT = 2.52
|
||||||
@@ -46,9 +45,9 @@ const calculateSnapPoint = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a ceiling with the given polygon points
|
* Creates a ceiling with the given polygon points and returns its ID
|
||||||
*/
|
*/
|
||||||
const commitCeilingDrawing = (levelId: LevelNode['id'], points: Array<[number, number]>) => {
|
const commitCeilingDrawing = (levelId: LevelNode['id'], points: Array<[number, number]>): string => {
|
||||||
const { createNode, nodes } = useScene.getState()
|
const { createNode, nodes } = useScene.getState()
|
||||||
|
|
||||||
// Count existing ceilings for naming
|
// Count existing ceilings for naming
|
||||||
@@ -62,6 +61,7 @@ const commitCeilingDrawing = (levelId: LevelNode['id'], points: Array<[number, n
|
|||||||
|
|
||||||
createNode(ceiling, levelId)
|
createNode(ceiling, levelId)
|
||||||
sfxEmitter.emit('sfx:structure-build')
|
sfxEmitter.emit('sfx:structure-build')
|
||||||
|
return ceiling.id
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CeilingTool: React.FC = () => {
|
export const CeilingTool: React.FC = () => {
|
||||||
@@ -70,7 +70,7 @@ export const CeilingTool: React.FC = () => {
|
|||||||
const mainLineRef = useRef<Line>(null!)
|
const mainLineRef = useRef<Line>(null!)
|
||||||
const closingLineRef = useRef<Line>(null!)
|
const closingLineRef = useRef<Line>(null!)
|
||||||
const currentLevelId = useViewer((state) => state.selection.levelId)
|
const currentLevelId = useViewer((state) => state.selection.levelId)
|
||||||
const setTool = useEditor((state) => state.setTool)
|
const setSelection = useViewer((state) => state.setSelection)
|
||||||
|
|
||||||
const [points, setPoints] = useState<Array<[number, number]>>([])
|
const [points, setPoints] = useState<Array<[number, number]>>([])
|
||||||
const [cursorPosition, setCursorPosition] = useState<[number, number]>([0, 0])
|
const [cursorPosition, setCursorPosition] = useState<[number, number]>([0, 0])
|
||||||
@@ -133,10 +133,10 @@ export const CeilingTool: React.FC = () => {
|
|||||||
Math.abs(clickPoint[0] - firstPoint[0]) < 0.25 &&
|
Math.abs(clickPoint[0] - firstPoint[0]) < 0.25 &&
|
||||||
Math.abs(clickPoint[1] - firstPoint[1]) < 0.25
|
Math.abs(clickPoint[1] - firstPoint[1]) < 0.25
|
||||||
) {
|
) {
|
||||||
// Create the ceiling
|
// Create the ceiling and select it
|
||||||
commitCeilingDrawing(currentLevelId, points)
|
const ceilingId = commitCeilingDrawing(currentLevelId, points)
|
||||||
|
setSelection({ selectedIds: [ceilingId] })
|
||||||
setPoints([])
|
setPoints([])
|
||||||
setTool(null)
|
|
||||||
} else {
|
} else {
|
||||||
// Add point to polygon
|
// Add point to polygon
|
||||||
setPoints([...points, clickPoint])
|
setPoints([...points, clickPoint])
|
||||||
@@ -148,9 +148,9 @@ export const CeilingTool: React.FC = () => {
|
|||||||
|
|
||||||
// Need at least 3 points to form a polygon
|
// Need at least 3 points to form a polygon
|
||||||
if (points.length >= 3) {
|
if (points.length >= 3) {
|
||||||
commitCeilingDrawing(currentLevelId, points)
|
const ceilingId = commitCeilingDrawing(currentLevelId, points)
|
||||||
|
setSelection({ selectedIds: [ceilingId] })
|
||||||
setPoints([])
|
setPoints([])
|
||||||
setTool(null)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ export const CeilingTool: React.FC = () => {
|
|||||||
emitter.off('grid:double-click', onGridDoubleClick)
|
emitter.off('grid:double-click', onGridDoubleClick)
|
||||||
emitter.off('tool:cancel', onCancel)
|
emitter.off('tool:cancel', onCancel)
|
||||||
}
|
}
|
||||||
}, [currentLevelId, points, cursorPosition, setTool])
|
}, [currentLevelId, points, cursorPosition, setSelection])
|
||||||
|
|
||||||
// Update line geometries when points change
|
// Update line geometries when points change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ function updateCeilingGeometry(node: CeilingNode, mesh: THREE.Mesh) {
|
|||||||
mesh.geometry.dispose()
|
mesh.geometry.dispose()
|
||||||
mesh.geometry = newGeo
|
mesh.geometry = newGeo
|
||||||
|
|
||||||
|
const gridMesh = mesh.getObjectByName('ceiling-grid') as THREE.Mesh
|
||||||
|
if (gridMesh) {
|
||||||
|
gridMesh.geometry.dispose()
|
||||||
|
gridMesh.geometry = newGeo
|
||||||
|
}
|
||||||
|
|
||||||
// Position at the ceiling height
|
// Position at the ceiling height
|
||||||
mesh.position.y = (node.height ?? 2.5) - 0.01 // Slight offset to avoid z-fighting with upper-level slabs
|
mesh.position.y = (node.height ?? 2.5) - 0.01 // Slight offset to avoid z-fighting with upper-level slabs
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,26 @@
|
|||||||
import { type CeilingNode, useRegistry } from '@pascal-app/core'
|
import { type CeilingNode, useRegistry } from '@pascal-app/core'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import { faceDirection, float, mix, positionWorld, smoothstep, step } from 'three/tsl'
|
import { float, mix, positionWorld, smoothstep } from 'three/tsl'
|
||||||
import { DoubleSide, type Mesh, MeshBasicNodeMaterial } from 'three/webgpu'
|
import { BackSide, FrontSide, type Mesh, MeshBasicNodeMaterial } from 'three/webgpu'
|
||||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||||
import { NodeRenderer } from '../node-renderer'
|
import { NodeRenderer } from '../node-renderer'
|
||||||
|
|
||||||
// TSL material that renders differently based on face direction:
|
// TSL material that renders differently based on face direction:
|
||||||
// - Back face (looking up at ceiling from below): solid
|
// - Back face (looking up at ceiling from below): solid
|
||||||
// - Front face (looking down at ceiling from above): 30% opacity
|
// - Front face (looking down at ceiling from above): 30% opacity
|
||||||
const ceilingMaterial = new MeshBasicNodeMaterial({
|
const ceilingTopMaterial = new MeshBasicNodeMaterial({
|
||||||
color: 0x999999,
|
color: 0xb5a78d,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
side: DoubleSide,
|
side: FrontSide,
|
||||||
alphaTestNode: float(0.4),
|
// Disabled as we only show ceiling grid when needed
|
||||||
|
// alphaTestNode: float(0.4), // Discard pixels with alpha below 0.4 to create grid lines and not affect depth buffer
|
||||||
|
})
|
||||||
|
|
||||||
|
const ceilingBottomMaterial = new MeshBasicNodeMaterial({
|
||||||
|
color: 0x999999,
|
||||||
|
transparent: true,
|
||||||
|
side: BackSide,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create grid pattern based on local position
|
// Create grid pattern based on local position
|
||||||
@@ -31,12 +38,12 @@ const lineY = smoothstep(lineWidth, 0, gridY).add(smoothstep(1.0 - lineWidth, 1.
|
|||||||
// Combine: if either X or Y is a line, show the line
|
// Combine: if either X or Y is a line, show the line
|
||||||
const gridPattern = lineX.max(lineY)
|
const gridPattern = lineX.max(lineY)
|
||||||
|
|
||||||
// Grid lines at 0.5 opacity, spaces at 0 opacity
|
// Grid lines at 0.6 opacity, spaces at 0.2 opacity
|
||||||
const gridOpacity = mix(float(0.0), float(0.5), gridPattern)
|
const gridOpacity = mix(float(0.2), float(0.6), gridPattern)
|
||||||
|
|
||||||
// faceDirection is 1.0 for front face, -1.0 for back face
|
// faceDirection is 1.0 for front face, -1.0 for back face
|
||||||
// Front face (top, looking down): grid pattern, Back face (bottom, looking up): solid
|
// Front face (top, looking down): grid pattern, Back face (bottom, looking up): solid
|
||||||
ceilingMaterial.opacityNode = mix(float(1.0), gridOpacity, step(float(0.0), float(faceDirection)))
|
ceilingTopMaterial.opacityNode = gridOpacity
|
||||||
|
|
||||||
export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
|
export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
|
||||||
const ref = useRef<Mesh>(null!)
|
const ref = useRef<Mesh>(null!)
|
||||||
@@ -45,9 +52,12 @@ export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
|
|||||||
const handlers = useNodeEvents(node, 'ceiling')
|
const handlers = useNodeEvents(node, 'ceiling')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh ref={ref} material={ceilingMaterial} {...handlers}>
|
<mesh ref={ref} material={ceilingBottomMaterial}>
|
||||||
{/* CeilingSystem will replace this geometry in the next frame */}
|
{/* CeilingSystem will replace this geometry in the next frame */}
|
||||||
<boxGeometry args={[0, 0, 0]} />
|
<boxGeometry args={[0, 0, 0]} />
|
||||||
|
<mesh name="ceiling-grid" material={ceilingTopMaterial} {...handlers} visible={false}>
|
||||||
|
<boxGeometry args={[0, 0, 0]} />
|
||||||
|
</mesh>
|
||||||
{node.children.map((childId) => (
|
{node.children.map((childId) => (
|
||||||
<NodeRenderer key={childId} nodeId={childId} />
|
<NodeRenderer key={childId} nodeId={childId} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user