diff --git a/apps/editor/components/editor/index.tsx b/apps/editor/components/editor/index.tsx index 08b5e0dc..391b47a4 100644 --- a/apps/editor/components/editor/index.tsx +++ b/apps/editor/components/editor/index.tsx @@ -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) { {/* Editor only system to toggle zone visibility */} + {/* */} diff --git a/apps/editor/components/systems/ceiling/ceiling-system.tsx b/apps/editor/components/systems/ceiling/ceiling-system.tsx new file mode 100644 index 00000000..0ec6658c --- /dev/null +++ b/apps/editor/components/systems/ceiling/ceiling-system.tsx @@ -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 +} diff --git a/apps/editor/components/tools/ceiling/ceiling-tool.tsx b/apps/editor/components/tools/ceiling/ceiling-tool.tsx index 89567f85..9736185b 100644 --- a/apps/editor/components/tools/ceiling/ceiling-tool.tsx +++ b/apps/editor/components/tools/ceiling/ceiling-tool.tsx @@ -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(null!) const closingLineRef = useRef(null!) const currentLevelId = useViewer((state) => state.selection.levelId) - const setTool = useEditor((state) => state.setTool) + const setSelection = useViewer((state) => state.setSelection) const [points, setPoints] = useState>([]) 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(() => { diff --git a/packages/core/src/systems/ceiling/ceiling-system.tsx b/packages/core/src/systems/ceiling/ceiling-system.tsx index 85cb598a..6787864a 100644 --- a/packages/core/src/systems/ceiling/ceiling-system.tsx +++ b/packages/core/src/systems/ceiling/ceiling-system.tsx @@ -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 } diff --git a/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx b/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx index 19c23e26..4c044bb1 100644 --- a/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx +++ b/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx @@ -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(null!) @@ -45,9 +52,12 @@ export const CeilingRenderer = ({ node }: { node: CeilingNode }) => { const handlers = useNodeEvents(node, 'ceiling') return ( - + {/* CeilingSystem will replace this geometry in the next frame */} + + + {node.children.map((childId) => ( ))}