ceiling system + reveal/hide

This commit is contained in:
wass08
2026-02-25 08:59:04 +09:00
parent 9b9398c5ff
commit c53f1f9679
5 changed files with 80 additions and 29 deletions
+11 -9
View File
@@ -2,26 +2,27 @@
import { initSpaceDetectionSync, initSpatialGridSync, useScene } from '@pascal-app/core'
import { Viewer } from '@pascal-app/viewer'
import { useKeyboard } from '@/hooks/use-keyboard'
import useEditor from '@/store/use-editor'
import { useProjectScene } from '@/features/community/lib/models/hooks'
import { useLocalProjectScene } from '@/features/community/lib/local-storage/hooks'
import { ThumbnailGenerator } from '@/app/viewer/[id]/thumbnail-generator'
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 { ToolManager } from '../tools/tool-manager'
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 { PanelManager } from '../ui/panels/panel-manager'
import { SidebarProvider } from '../ui/primitives/sidebar'
import { AppSidebar } from '../ui/sidebar/app-sidebar'
import { CustomCameraControls } from './custom-camera-controls'
import { ExportManager } from './export-manager'
import { Grid } from './grid'
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)
useScene.getState().loadScene()
@@ -74,6 +75,7 @@ export default function Editor({ projectId }: EditorProps) {
<ExportManager />
{/* Editor only system to toggle zone visibility */}
<ZoneSystem />
<CeilingSystem />
{/* <Stats /> */}
<Grid cellColor="#aaa" sectionColor="#ccc" fadeDistance={500} />
<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 { BufferGeometry, DoubleSide, type Line, type Mesh, Shape, Vector3 } from 'three'
import { sfxEmitter } from '@/lib/sfx-bus'
import useEditor from '@/store/use-editor'
import { CursorSphere } from '../shared/cursor-sphere'
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()
// Count existing ceilings for naming
@@ -62,6 +61,7 @@ const commitCeilingDrawing = (levelId: LevelNode['id'], points: Array<[number, n
createNode(ceiling, levelId)
sfxEmitter.emit('sfx:structure-build')
return ceiling.id
}
export const CeilingTool: React.FC = () => {
@@ -70,7 +70,7 @@ export const CeilingTool: React.FC = () => {
const mainLineRef = useRef<Line>(null!)
const closingLineRef = useRef<Line>(null!)
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 [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[1] - firstPoint[1]) < 0.25
) {
// Create the ceiling
commitCeilingDrawing(currentLevelId, points)
// Create the ceiling and select it
const ceilingId = commitCeilingDrawing(currentLevelId, points)
setSelection({ selectedIds: [ceilingId] })
setPoints([])
setTool(null)
} else {
// Add point to polygon
setPoints([...points, clickPoint])
@@ -148,9 +148,9 @@ export const CeilingTool: React.FC = () => {
// Need at least 3 points to form a polygon
if (points.length >= 3) {
commitCeilingDrawing(currentLevelId, points)
const ceilingId = commitCeilingDrawing(currentLevelId, points)
setSelection({ selectedIds: [ceilingId] })
setPoints([])
setTool(null)
}
}
@@ -180,7 +180,7 @@ export const CeilingTool: React.FC = () => {
emitter.off('grid:double-click', onGridDoubleClick)
emitter.off('tool:cancel', onCancel)
}
}, [currentLevelId, points, cursorPosition, setTool])
}, [currentLevelId, points, cursorPosition, setSelection])
// Update line geometries when points change
useEffect(() => {
@@ -42,6 +42,12 @@ function updateCeilingGeometry(node: CeilingNode, mesh: THREE.Mesh) {
mesh.geometry.dispose()
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
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 { useRef } from 'react'
import { faceDirection, float, mix, positionWorld, smoothstep, step } from 'three/tsl'
import { DoubleSide, type Mesh, MeshBasicNodeMaterial } from 'three/webgpu'
import { float, mix, positionWorld, smoothstep } from 'three/tsl'
import { BackSide, FrontSide, type Mesh, MeshBasicNodeMaterial } from 'three/webgpu'
import { useNodeEvents } from '../../../hooks/use-node-events'
import { NodeRenderer } from '../node-renderer'
// TSL material that renders differently based on face direction:
// - Back face (looking up at ceiling from below): solid
// - Front face (looking down at ceiling from above): 30% opacity
const ceilingMaterial = new MeshBasicNodeMaterial({
color: 0x999999,
const ceilingTopMaterial = new MeshBasicNodeMaterial({
color: 0xb5a78d,
transparent: true,
depthWrite: false,
side: DoubleSide,
alphaTestNode: float(0.4),
side: FrontSide,
// 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
@@ -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
const gridPattern = lineX.max(lineY)
// Grid lines at 0.5 opacity, spaces at 0 opacity
const gridOpacity = mix(float(0.0), float(0.5), gridPattern)
// Grid lines at 0.6 opacity, spaces at 0.2 opacity
const gridOpacity = mix(float(0.2), float(0.6), gridPattern)
// 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
ceilingMaterial.opacityNode = mix(float(1.0), gridOpacity, step(float(0.0), float(faceDirection)))
ceilingTopMaterial.opacityNode = gridOpacity
export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
const ref = useRef<Mesh>(null!)
@@ -45,9 +52,12 @@ export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
const handlers = useNodeEvents(node, 'ceiling')
return (
<mesh ref={ref} material={ceilingMaterial} {...handlers}>
<mesh ref={ref} material={ceilingBottomMaterial}>
{/* CeilingSystem will replace this geometry in the next frame */}
<boxGeometry args={[0, 0, 0]} />
<mesh name="ceiling-grid" material={ceilingTopMaterial} {...handlers} visible={false}>
<boxGeometry args={[0, 0, 0]} />
</mesh>
{node.children.map((childId) => (
<NodeRenderer key={childId} nodeId={childId} />
))}