Refine wall move previews and expose auto-slab planning

This commit is contained in:
sudhir
2026-05-12 12:15:45 +05:30
parent f3444d4275
commit 70c85d7760
8 changed files with 240 additions and 210 deletions
+2
View File
@@ -45,6 +45,8 @@ export { getRenderableSlabPolygon } from './lib/slab-polygon'
export {
detectSpacesForLevel,
initSpaceDetectionSync,
planAutoSlabsForLevel,
type AutoSlabSyncPlan,
type Space,
wallTouchesOthers,
} from './lib/space-detection'
+29 -10
View File
@@ -41,6 +41,12 @@ type DetectedRoom = {
bbox: ReturnType<typeof bboxOf>
}
export type AutoSlabSyncPlan = {
create: SlabNodeType[]
update: Array<{ id: SlabNodeType['id']; data: Partial<SlabNodeType> }>
delete: Array<SlabNodeType['id']>
}
const DEFAULT_AUTO_SLAB_ELEVATION = 0.05
const DEFAULT_AUTO_CEILING_HEIGHT = 2.5
const ROOM_CURVE_TOLERANCE = 0.04
@@ -488,12 +494,10 @@ function buildSpace(levelId: string, polygon: Point2D[]): Space {
}
}
function syncAutoSlabsForLevel(
levelId: string,
export function planAutoSlabsForLevel(
roomPolygons: Point2D[][],
existingSlabs: SlabNodeType[],
sceneStore: any,
) {
): AutoSlabSyncPlan {
const manualSlabs = existingSlabs.filter((slab) => !slab.autoFromWalls)
const manualSignatures = new Set(
manualSlabs.map((slab) => polygonSignature(slab.polygon.map(pointFromTuple))),
@@ -618,16 +622,31 @@ function syncAutoSlabsForLevel(
)
}
if (slabsToDelete.length > 0) {
sceneStore.getState().deleteNodes(slabsToDelete)
return {
create: slabsToCreate,
update: slabsToUpdate,
delete: slabsToDelete,
}
}
if (slabsToUpdate.length > 0) {
sceneStore.getState().updateNodes(slabsToUpdate)
function syncAutoSlabsForLevel(
levelId: string,
roomPolygons: Point2D[][],
existingSlabs: SlabNodeType[],
sceneStore: any,
) {
const plan = planAutoSlabsForLevel(roomPolygons, existingSlabs)
if (plan.delete.length > 0) {
sceneStore.getState().deleteNodes(plan.delete)
}
if (slabsToCreate.length > 0) {
sceneStore.getState().createNodes(slabsToCreate.map((node) => ({ node, parentId: levelId })))
if (plan.update.length > 0) {
sceneStore.getState().updateNodes(plan.update)
}
if (plan.create.length > 0) {
sceneStore.getState().createNodes(plan.create.map((node) => ({ node, parentId: levelId })))
}
}
@@ -16,6 +16,7 @@ export type FloorplanActionMenuEntry = {
onDelete: FloorplanActionMenuHandler
onMove: FloorplanActionMenuHandler
onAddHole?: FloorplanActionMenuHandler
onCurve?: FloorplanActionMenuHandler
onDuplicate?: FloorplanActionMenuHandler
}
@@ -81,6 +82,7 @@ export const FloorplanActionMenuLayer = memo(function FloorplanActionMenuLayer({
>
<NodeActionMenu
onAddHole={entry.onAddHole}
onCurve={entry.onCurve}
onDelete={entry.onDelete}
onDuplicate={entry.onDuplicate}
onMove={entry.onMove}
@@ -39,6 +39,7 @@ import {
sceneRegistry,
useLiveTransforms,
useScene,
WallNode as WallNodeSchema,
type WallNode,
WindowNode,
ZoneNode as ZoneNodeSchema,
@@ -7554,6 +7555,7 @@ export function FloorplanPanel() {
const setPhase = useEditor((state) => state.setPhase)
const setMovingFenceEndpoint = useEditor((state) => state.setMovingFenceEndpoint)
const setMovingNode = useEditor((state) => state.setMovingNode)
const setCurvingWall = useEditor((state) => state.setCurvingWall)
const movingFenceEndpoint = useEditor((state) => state.movingFenceEndpoint)
const structureLayer = useEditor((state) => state.structureLayer)
const setStructureLayer = useEditor((state) => state.setStructureLayer)
@@ -9324,6 +9326,7 @@ export function FloorplanPanel() {
selectedWallEntry,
wallCurveDraft,
])
const canCurveSelectedWall = wallCurveHandles.length > 0
const slabVertexHandles = useMemo(() => {
if (!shouldShowSlabBoundaryHandles) {
return []
@@ -14066,6 +14069,57 @@ export function FloorplanPanel() {
},
[selectedWallEntry, setMovingNode, setSelection],
)
const duplicateSelectedWall = useCallback(() => {
const wall = selectedWallEntry?.wall
if (!wall?.parentId) {
return
}
sfxEmitter.emit('sfx:item-pick')
const cloned = structuredClone(wall) as Record<string, unknown>
delete cloned.id
cloned.children = []
cloned.metadata = {
...(typeof cloned.metadata === 'object' && cloned.metadata !== null ? cloned.metadata : {}),
isNew: true,
}
const temporal = useScene.temporal.getState()
temporal.pause()
try {
const duplicate = WallNodeSchema.parse(cloned)
useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
setMovingNode(duplicate)
setSelection({ selectedIds: [] })
} catch (error) {
console.error('Failed to duplicate wall', error)
} finally {
temporal.resume()
}
}, [selectedWallEntry, setMovingNode, setSelection])
const handleSelectedWallDuplicate = useCallback(
(event: ReactMouseEvent<HTMLButtonElement>) => {
event.stopPropagation()
duplicateSelectedWall()
},
[duplicateSelectedWall],
)
const handleSelectedWallCurve = useCallback(
(event: ReactMouseEvent<HTMLButtonElement>) => {
event.stopPropagation()
const wall = selectedWallEntry?.wall
if (!(wall && canCurveSelectedWall)) {
return
}
sfxEmitter.emit('sfx:item-pick')
setCurvingWall(wall)
setSelection({ selectedIds: [] })
},
[canCurveSelectedWall, selectedWallEntry, setCurvingWall, setSelection],
)
const handleSelectedWallDelete = useCallback(
(event: ReactMouseEvent<HTMLButtonElement>) => {
event.stopPropagation()
@@ -16105,9 +16159,17 @@ export function FloorplanPanel() {
site,
])
const hasDuplicatableFloorplanSelection = Boolean(
selectedItemEntry || selectedOpeningEntry || selectedStairEntry || selectedRoofEntry,
selectedItemEntry ||
selectedOpeningEntry ||
selectedStairEntry ||
selectedRoofEntry ||
selectedWallEntry,
)
const handleDuplicateFloorplanSelection = useCallback(() => {
if (selectedWallEntry) {
duplicateSelectedWall()
return
}
if (selectedOpeningEntry) {
duplicateSelectedOpening()
return
@@ -16124,6 +16186,7 @@ export function FloorplanPanel() {
duplicateSelectedRoof()
}
}, [
duplicateSelectedWall,
duplicateSelectedItem,
duplicateSelectedOpening,
duplicateSelectedRoof,
@@ -16132,6 +16195,7 @@ export function FloorplanPanel() {
selectedOpeningEntry,
selectedRoofEntry,
selectedStairEntry,
selectedWallEntry,
])
const activeDraftAnchorPoint =
referenceScaleDraft?.start ??
@@ -16258,7 +16322,9 @@ export function FloorplanPanel() {
}}
wall={{
position: selectedWallActionMenuPosition,
onCurve: canCurveSelectedWall ? handleSelectedWallCurve : undefined,
onDelete: handleSelectedWallDelete,
onDuplicate: handleSelectedWallDuplicate,
onMove: handleSelectedWallMove,
}}
/>
@@ -9,13 +9,12 @@ import {
type GridEvent,
getMaterialPresetByRef,
getPerpendicularWallMoveAxis,
getRenderableSlabPolygon,
pauseSceneHistory,
planAutoSlabsForLevel,
planWallMoveJunctions,
resolveMaterial,
resumeSceneHistory,
type SlabNode,
SlabNode as SlabSchema,
useScene,
type WallMoveAxis,
type WallMoveBridgePlan,
@@ -25,7 +24,7 @@ import {
} from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { BufferGeometry, DoubleSide, Float32BufferAttribute, ShapeUtils, Vector2 } from 'three'
import { BufferGeometry, DoubleSide, Float32BufferAttribute } from 'three'
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
import { EDITOR_LAYER } from '../../../lib/constants'
import { sfxEmitter } from '../../../lib/sfx-bus'
@@ -33,9 +32,6 @@ import useEditor from '../../../store/use-editor'
import { CursorSphere } from '../shared/cursor-sphere'
import { getWallGridStep, isWallLongEnough, snapScalarToGrid } from './wall-drafting'
const AUTO_SLAB_PREVIEW_ELEVATION = 0.05
const AUTO_SLAB_PREVIEW_Y = AUTO_SLAB_PREVIEW_ELEVATION + 0.025
function rotateVector([x, z]: [number, number], angle: number): [number, number] {
const cos = Math.cos(angle)
const sin = Math.sin(angle)
@@ -70,11 +66,6 @@ type GhostWallPreview = {
height: number
}
type GhostSlabPreview = {
id: string
polygon: Array<[number, number]>
}
function getLinkedWallSnapshots(args: {
wallId: WallNode['id']
wallParentId: string | null
@@ -222,27 +213,6 @@ function getWallGhostColor(wall: WallNode) {
return resolveMaterial(wall.material ?? wall.interiorMaterial ?? wall.exteriorMaterial).color
}
function getMinRotatedKey(values: string[]) {
if (values.length === 0) return ''
let best = ''
for (let index = 0; index < values.length; index += 1) {
const value = [...values.slice(index), ...values.slice(0, index)].join('|')
if (!best || value < best) {
best = value
}
}
return best
}
function getPolygonPreviewKey(polygon: Array<[number, number]>) {
const values = polygon.map(([x, z]) => `${x.toFixed(3)}:${z.toFixed(3)}`)
const forward = getMinRotatedKey(values)
const reversed = getMinRotatedKey([...values].reverse())
return forward < reversed ? forward : reversed
}
function getWallsAfterUpdates(
nodes: ReturnType<typeof useScene.getState>['nodes'],
updates: Array<{ id: AnyNodeId; data: Partial<WallNode> }>,
@@ -257,6 +227,32 @@ function getWallsAfterUpdates(
})
}
function cloneSlabSnapshot(slab: SlabNode): SlabNode {
return {
...slab,
polygon: slab.polygon.map(([x, z]) => [x, z] as [number, number]),
holes: slab.holes.map((hole) => hole.map(([x, z]) => [x, z] as [number, number])),
holeMetadata: slab.holeMetadata.map((metadata) => ({ ...metadata })),
}
}
function getLevelSlabs(levelId: string, nodes: ReturnType<typeof useScene.getState>['nodes']) {
return Object.values(nodes).filter(
(entry): entry is SlabNode => entry?.type === 'slab' && (entry.parentId ?? null) === levelId,
)
}
function getLevelAutoSlabs(
levelId: string,
nodes: ReturnType<typeof useScene.getState>['nodes'],
) {
return getLevelSlabs(levelId, nodes).filter((slab) => slab.autoFromWalls)
}
function getLevelAutoSlabSnapshots(levelId: string) {
return getLevelAutoSlabs(levelId, useScene.getState().nodes).map(cloneSlabSnapshot)
}
function buildBridgeWallCreates(args: {
bridgePlans: Array<WallMoveBridgePlan<LinkedWallSnapshot>>
nextStart: [number, number]
@@ -343,68 +339,6 @@ function buildBridgeWallPreviews(args: {
return previews
}
function buildAutoSlabGhostPreviews(args: {
levelId: string
walls: WallNode[]
existingSlabs: SlabNode[]
}): GhostSlabPreview[] {
const { levelId, walls, existingSlabs } = args
const levelWalls = walls.filter((wall) => (wall.parentId ?? null) === levelId)
if (levelWalls.length < 3) {
return []
}
const manualSlabKeys = new Set(
existingSlabs
.filter((slab) => !slab.autoFromWalls)
.map((slab) => getPolygonPreviewKey(slab.polygon)),
)
const existingAutoKeys = new Set(
existingSlabs
.filter((slab) => slab.autoFromWalls)
.map((slab) => getPolygonPreviewKey(getRenderableSlabPolygon(slab))),
)
const seenPreviewKeys = new Set<string>()
const { roomPolygons } = detectSpacesForLevel(levelId, levelWalls)
const previews: GhostSlabPreview[] = []
for (let index = 0; index < roomPolygons.length; index += 1) {
const polygon = roomPolygons[index]
if (!polygon || polygon.length < 3) continue
const rawPolygon = polygon.map((point) => [point.x, point.y] as [number, number])
if (manualSlabKeys.has(getPolygonPreviewKey(rawPolygon))) {
continue
}
const previewSlab = SlabSchema.parse({
polygon: rawPolygon,
holes: [],
elevation: AUTO_SLAB_PREVIEW_ELEVATION,
autoFromWalls: true,
})
const renderablePolygon = getRenderableSlabPolygon(previewSlab)
const previewKey = getPolygonPreviewKey(renderablePolygon)
if (
renderablePolygon.length < 3 ||
existingAutoKeys.has(previewKey) ||
seenPreviewKeys.has(previewKey)
) {
continue
}
seenPreviewKeys.add(previewKey)
previews.push({
id: `auto-slab:${index}:${previewKey}`,
polygon: renderablePolygon,
})
}
return previews
}
function setPreviewGeometryAttributes(
geometry: BufferGeometry,
positions: number[],
@@ -430,48 +364,6 @@ function createWallPreviewGeometry(length: number, height: number) {
return geometry
}
function createSlabPreviewGeometry(polygon: Array<[number, number]>) {
if (polygon.length < 3) {
return null
}
const contour = polygon.map(([x, z]) => new Vector2(x, z))
const triangles = ShapeUtils.triangulateShape(contour, [])
if (triangles.length === 0) {
return null
}
let minX = Number.POSITIVE_INFINITY
let minZ = Number.POSITIVE_INFINITY
let maxX = Number.NEGATIVE_INFINITY
let maxZ = Number.NEGATIVE_INFINITY
for (const [x, z] of polygon) {
minX = Math.min(minX, x)
minZ = Math.min(minZ, z)
maxX = Math.max(maxX, x)
maxZ = Math.max(maxZ, z)
}
const width = Math.max(maxX - minX, 0.001)
const depth = Math.max(maxZ - minZ, 0.001)
const positions: number[] = []
const normals: number[] = []
const uvs: number[] = []
for (const [x, z] of polygon) {
positions.push(x, 0, z)
normals.push(0, 1, 0)
uvs.push((x - minX) / width, (z - minZ) / depth)
}
const geometry = new BufferGeometry()
setPreviewGeometryAttributes(geometry, positions, normals, uvs)
geometry.setIndex(triangles.flat())
geometry.computeBoundingSphere()
return geometry
}
function GhostWallPreviewMesh({ preview }: { preview: GhostWallPreview }) {
const dx = preview.end[0] - preview.start[0]
const dz = preview.end[1] - preview.start[1]
@@ -504,35 +396,6 @@ function GhostWallPreviewMesh({ preview }: { preview: GhostWallPreview }) {
)
}
function GhostSlabPreviewMesh({ preview }: { preview: GhostSlabPreview }) {
const geometry = useMemo(() => createSlabPreviewGeometry(preview.polygon), [preview.polygon])
useEffect(() => () => geometry?.dispose(), [geometry])
if (!geometry) {
return null
}
return (
<mesh
frustumCulled={false}
layers={EDITOR_LAYER}
position={[0, AUTO_SLAB_PREVIEW_Y, 0]}
renderOrder={1}
>
<primitive attach="geometry" object={geometry} />
<meshBasicMaterial
color="#38bdf8"
depthTest={false}
depthWrite={false}
opacity={0.2}
side={DoubleSide}
transparent
/>
</mesh>
)
}
export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
const meta =
typeof node.metadata === 'object' && node.metadata !== null && !Array.isArray(node.metadata)
@@ -564,6 +427,9 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
originalEnd: node.end,
}),
)
const originalAutoSlabsRef = useRef<SlabNode[]>(
node.parentId ? getLevelAutoSlabSnapshots(node.parentId) : [],
)
const dragAnchorRef = useRef<[number, number] | null>(null)
const nodeIdRef = useRef(node.id)
const previewRef = useRef<{ start: [number, number]; end: [number, number] } | null>(null)
@@ -576,7 +442,6 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
return [centerX, 0, centerZ]
})
const [ghostWallPreviews, setGhostWallPreviews] = useState<GhostWallPreview[]>([])
const [ghostSlabPreviews, setGhostSlabPreviews] = useState<GhostSlabPreview[]>([])
const exitMoveMode = useCallback(() => {
useEditor.getState().setMovingNode(null)
@@ -588,9 +453,11 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
const originalEnd = originalEndRef.current
const originalCenter = originalCenterRef.current
const originalHalfVector = originalHalfVectorRef.current
const levelId = node.parentId ?? null
const originalAutoSlabs = originalAutoSlabsRef.current
pauseSceneHistory(useScene)
let wasCommitted = false
let shouldRestoreOnCleanup = true
const applyNodePreview = (
updates: Array<{ id: WallNode['id']; start: [number, number]; end: [number, number] }>,
@@ -606,6 +473,72 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
}
}
const applyLiveAutoSlabPreview = (walls: WallNode[]) => {
if (!levelId) {
return
}
const levelWalls = walls.filter((wall) => (wall.parentId ?? null) === levelId)
const sceneState = useScene.getState()
const { roomPolygons } = detectSpacesForLevel(levelId, levelWalls)
const slabPlan = planAutoSlabsForLevel(roomPolygons, getLevelSlabs(levelId, sceneState.nodes))
if (
slabPlan.create.length === 0 &&
slabPlan.update.length === 0 &&
slabPlan.delete.length === 0
) {
return
}
sceneState.applyNodeChanges({
update: slabPlan.update.map((entry) => ({
id: entry.id as AnyNodeId,
data: entry.data,
})),
create: slabPlan.create.map((slab) => ({
node: slab,
parentId: levelId as AnyNodeId,
})),
delete: slabPlan.delete.map((id) => id as AnyNodeId),
})
}
const restoreAutoSlabPreview = () => {
if (!levelId) {
return
}
const sceneState = useScene.getState()
const originalIds = new Set(originalAutoSlabs.map((slab) => slab.id))
const currentAutoSlabs = getLevelAutoSlabs(levelId, sceneState.nodes)
const update = originalAutoSlabs
.filter((slab) => sceneState.nodes[slab.id as AnyNodeId])
.map((slab) => ({
id: slab.id as AnyNodeId,
data: cloneSlabSnapshot(slab),
}))
const create = originalAutoSlabs
.filter((slab) => !sceneState.nodes[slab.id as AnyNodeId])
.map((slab) => ({
node: cloneSlabSnapshot(slab),
parentId: levelId as AnyNodeId,
}))
const deleteIds = currentAutoSlabs
.filter((slab) => !originalIds.has(slab.id))
.map((slab) => slab.id as AnyNodeId)
if (update.length === 0 && create.length === 0 && deleteIds.length === 0) {
return
}
sceneState.applyNodeChanges({
update,
create,
delete: deleteIds,
})
}
const buildWallFromCenter = (center: [number, number]) => {
const rotatedHalf = rotateVector(originalHalfVector, pendingRotationRef.current)
const nextStart: [number, number] = [center[0] - rotatedHalf[0], center[1] - rotatedHalf[1]]
@@ -672,31 +605,18 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
})
const nextGhostWalls = bridgePreviews.map((preview) => preview.ghost)
const virtualBridgeWalls = bridgePreviews.map((preview) => preview.wall)
const sceneNodes = useScene.getState().nodes
const levelId = node.parentId ?? null
setGhostWallPreviews(nextGhostWalls)
setGhostSlabPreviews(
levelId
? buildAutoSlabGhostPreviews({
levelId,
walls: [...previewSceneWalls, ...virtualBridgeWalls],
existingSlabs: Object.values(sceneNodes).filter(
(entry): entry is SlabNode =>
entry?.type === 'slab' && (entry.parentId ?? null) === levelId,
),
})
: [],
)
applyNodePreview(previewUpdates)
applyLiveAutoSlabPreview([...previewSceneWalls, ...virtualBridgeWalls])
}
const restoreOriginal = () => {
setGhostWallPreviews([])
setGhostSlabPreviews([])
applyNodePreview([
{ id: nodeId, start: originalStart, end: originalEnd },
...linkedOriginalsRef.current,
])
restoreAutoSlabPreview()
}
const onGridMove = (event: GridEvent) => {
@@ -738,16 +658,16 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
wasCommitted = true
shouldRestoreOnCleanup = false
// Restore original baseline while paused so the next resume+update
// registers as a single tracked change (undo reverts to original).
setGhostWallPreviews([])
setGhostSlabPreviews([])
applyNodePreview([
{ id: nodeId, start: originalStart, end: originalEnd },
...linkedOriginalsRef.current,
])
restoreAutoSlabPreview()
resumeSceneHistory(useScene)
const commitPlan = getMovePlan(preview.start, preview.end)
@@ -848,6 +768,7 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
}
const onCancel = () => {
shouldRestoreOnCleanup = false
restoreOriginal()
useViewer.getState().setSelection({ selectedIds: [nodeId] })
resumeSceneHistory(useScene)
@@ -862,7 +783,7 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
window.addEventListener('keyup', onKeyUp)
return () => {
if (!wasCommitted) {
if (shouldRestoreOnCleanup) {
restoreOriginal()
}
shiftPressedRef.current = false
@@ -873,14 +794,11 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
window.removeEventListener('keydown', onKeyDown)
window.removeEventListener('keyup', onKeyUp)
}
}, [exitMoveMode, isNew, node.metadata])
}, [exitMoveMode, isNew, node.metadata, node.parentId])
return (
<group>
<CursorSphere position={cursorLocalPos} showTooltip={false} />
{ghostSlabPreviews.map((preview) => (
<GhostSlabPreviewMesh key={preview.id} preview={preview} />
))}
{ghostWallPreviews.map((preview) => (
<GhostWallPreviewMesh key={preview.id} preview={preview} />
))}
@@ -4,12 +4,19 @@ import {
resolveMaterial,
useRegistry,
} from '@pascal-app/core'
import { useMemo, useRef } from 'react'
import { useEffect, useMemo, useRef } from 'react'
import { BufferGeometry, Float32BufferAttribute } from 'three'
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'
function createEmptyGeometry() {
const geometry = new BufferGeometry()
geometry.setAttribute('position', new Float32BufferAttribute([], 3))
return geometry
}
const gridScale = 5
const gridX = positionWorld.x.mul(gridScale).fract()
const gridY = positionWorld.z.mul(gridScale).fract()
@@ -51,10 +58,20 @@ function getCeilingMaterials(color = '#999999') {
export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
const ref = useRef<Mesh>(null!)
const placeholderGeometry = useMemo(createEmptyGeometry, [])
const gridPlaceholderGeometry = useMemo(createEmptyGeometry, [])
useRegistry(node.id, 'ceiling', ref)
const handlers = useNodeEvents(node, 'ceiling')
useEffect(
() => () => {
placeholderGeometry.dispose()
gridPlaceholderGeometry.dispose()
},
[gridPlaceholderGeometry, placeholderGeometry],
)
const materials = useMemo(() => {
const preset = getMaterialPresetByRef(node.materialPreset)
const props = preset?.mapProperties ?? resolveMaterial(node.material)
@@ -69,17 +86,15 @@ export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
])
return (
<mesh material={materials.bottomMaterial} ref={ref}>
<boxGeometry args={[0, 0, 0]} />
<mesh geometry={placeholderGeometry} material={materials.bottomMaterial} ref={ref}>
<mesh
geometry={gridPlaceholderGeometry}
material={materials.topMaterial}
name="ceiling-grid"
{...handlers}
scale={0}
visible={false}
>
<boxGeometry args={[0, 0, 0]} />
</mesh>
/>
{node.children.map((childId) => (
<NodeRenderer key={childId} nodeId={childId} />
))}
@@ -11,6 +11,12 @@ import {
const slabMaterialCache = new Map<string, THREE.MeshStandardMaterial>()
function createEmptyGeometry() {
const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', new THREE.Float32BufferAttribute([], 3))
return geometry
}
function getSlabMaterial(
cacheKey: string,
params: { material?: SlabNode['material']; materialPreset?: string },
@@ -47,11 +53,14 @@ function getSlabMaterial(
export const SlabRenderer = ({ node }: { node: SlabNode }) => {
const ref = useRef<Mesh>(null!)
const placeholderGeometry = useMemo(createEmptyGeometry, [])
useRegistry(node.id, 'slab', ref)
const handlers = useNodeEvents(node, 'slab')
useEffect(() => () => placeholderGeometry.dispose(), [placeholderGeometry])
const material = useMemo(() => {
const resolvedMaterial = node.material
const resolvedMaterialPreset = node.materialPreset
@@ -75,13 +84,12 @@ export const SlabRenderer = ({ node }: { node: SlabNode }) => {
return (
<mesh
castShadow
geometry={placeholderGeometry}
receiveShadow
ref={ref}
{...handlers}
material={material}
visible={node.visible}
>
<boxGeometry args={[0, 0, 0]} />
</mesh>
/>
)
}
@@ -50,7 +50,7 @@ function updateCeilingGeometry(node: CeilingNode, mesh: THREE.Mesh) {
const gridMesh = mesh.getObjectByName('ceiling-grid') as THREE.Mesh
if (gridMesh) {
gridMesh.geometry.dispose()
gridMesh.geometry = newGeo
gridMesh.geometry = newGeo.clone()
}
// Position at the ceiling height