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) => (
))}