ceiling system + reveal/hide
This commit is contained in:
@@ -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} />
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user