Refine wall move previews and expose auto-slab planning
This commit is contained in:
@@ -45,6 +45,8 @@ export { getRenderableSlabPolygon } from './lib/slab-polygon'
|
|||||||
export {
|
export {
|
||||||
detectSpacesForLevel,
|
detectSpacesForLevel,
|
||||||
initSpaceDetectionSync,
|
initSpaceDetectionSync,
|
||||||
|
planAutoSlabsForLevel,
|
||||||
|
type AutoSlabSyncPlan,
|
||||||
type Space,
|
type Space,
|
||||||
wallTouchesOthers,
|
wallTouchesOthers,
|
||||||
} from './lib/space-detection'
|
} from './lib/space-detection'
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ type DetectedRoom = {
|
|||||||
bbox: ReturnType<typeof bboxOf>
|
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_SLAB_ELEVATION = 0.05
|
||||||
const DEFAULT_AUTO_CEILING_HEIGHT = 2.5
|
const DEFAULT_AUTO_CEILING_HEIGHT = 2.5
|
||||||
const ROOM_CURVE_TOLERANCE = 0.04
|
const ROOM_CURVE_TOLERANCE = 0.04
|
||||||
@@ -488,12 +494,10 @@ function buildSpace(levelId: string, polygon: Point2D[]): Space {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncAutoSlabsForLevel(
|
export function planAutoSlabsForLevel(
|
||||||
levelId: string,
|
|
||||||
roomPolygons: Point2D[][],
|
roomPolygons: Point2D[][],
|
||||||
existingSlabs: SlabNodeType[],
|
existingSlabs: SlabNodeType[],
|
||||||
sceneStore: any,
|
): AutoSlabSyncPlan {
|
||||||
) {
|
|
||||||
const manualSlabs = existingSlabs.filter((slab) => !slab.autoFromWalls)
|
const manualSlabs = existingSlabs.filter((slab) => !slab.autoFromWalls)
|
||||||
const manualSignatures = new Set(
|
const manualSignatures = new Set(
|
||||||
manualSlabs.map((slab) => polygonSignature(slab.polygon.map(pointFromTuple))),
|
manualSlabs.map((slab) => polygonSignature(slab.polygon.map(pointFromTuple))),
|
||||||
@@ -618,16 +622,31 @@ function syncAutoSlabsForLevel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slabsToDelete.length > 0) {
|
return {
|
||||||
sceneStore.getState().deleteNodes(slabsToDelete)
|
create: slabsToCreate,
|
||||||
|
update: slabsToUpdate,
|
||||||
|
delete: slabsToDelete,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slabsToUpdate.length > 0) {
|
function syncAutoSlabsForLevel(
|
||||||
sceneStore.getState().updateNodes(slabsToUpdate)
|
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) {
|
if (plan.update.length > 0) {
|
||||||
sceneStore.getState().createNodes(slabsToCreate.map((node) => ({ node, parentId: levelId })))
|
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
|
onDelete: FloorplanActionMenuHandler
|
||||||
onMove: FloorplanActionMenuHandler
|
onMove: FloorplanActionMenuHandler
|
||||||
onAddHole?: FloorplanActionMenuHandler
|
onAddHole?: FloorplanActionMenuHandler
|
||||||
|
onCurve?: FloorplanActionMenuHandler
|
||||||
onDuplicate?: FloorplanActionMenuHandler
|
onDuplicate?: FloorplanActionMenuHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +82,7 @@ export const FloorplanActionMenuLayer = memo(function FloorplanActionMenuLayer({
|
|||||||
>
|
>
|
||||||
<NodeActionMenu
|
<NodeActionMenu
|
||||||
onAddHole={entry.onAddHole}
|
onAddHole={entry.onAddHole}
|
||||||
|
onCurve={entry.onCurve}
|
||||||
onDelete={entry.onDelete}
|
onDelete={entry.onDelete}
|
||||||
onDuplicate={entry.onDuplicate}
|
onDuplicate={entry.onDuplicate}
|
||||||
onMove={entry.onMove}
|
onMove={entry.onMove}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
sceneRegistry,
|
sceneRegistry,
|
||||||
useLiveTransforms,
|
useLiveTransforms,
|
||||||
useScene,
|
useScene,
|
||||||
|
WallNode as WallNodeSchema,
|
||||||
type WallNode,
|
type WallNode,
|
||||||
WindowNode,
|
WindowNode,
|
||||||
ZoneNode as ZoneNodeSchema,
|
ZoneNode as ZoneNodeSchema,
|
||||||
@@ -7554,6 +7555,7 @@ export function FloorplanPanel() {
|
|||||||
const setPhase = useEditor((state) => state.setPhase)
|
const setPhase = useEditor((state) => state.setPhase)
|
||||||
const setMovingFenceEndpoint = useEditor((state) => state.setMovingFenceEndpoint)
|
const setMovingFenceEndpoint = useEditor((state) => state.setMovingFenceEndpoint)
|
||||||
const setMovingNode = useEditor((state) => state.setMovingNode)
|
const setMovingNode = useEditor((state) => state.setMovingNode)
|
||||||
|
const setCurvingWall = useEditor((state) => state.setCurvingWall)
|
||||||
const movingFenceEndpoint = useEditor((state) => state.movingFenceEndpoint)
|
const movingFenceEndpoint = useEditor((state) => state.movingFenceEndpoint)
|
||||||
const structureLayer = useEditor((state) => state.structureLayer)
|
const structureLayer = useEditor((state) => state.structureLayer)
|
||||||
const setStructureLayer = useEditor((state) => state.setStructureLayer)
|
const setStructureLayer = useEditor((state) => state.setStructureLayer)
|
||||||
@@ -9324,6 +9326,7 @@ export function FloorplanPanel() {
|
|||||||
selectedWallEntry,
|
selectedWallEntry,
|
||||||
wallCurveDraft,
|
wallCurveDraft,
|
||||||
])
|
])
|
||||||
|
const canCurveSelectedWall = wallCurveHandles.length > 0
|
||||||
const slabVertexHandles = useMemo(() => {
|
const slabVertexHandles = useMemo(() => {
|
||||||
if (!shouldShowSlabBoundaryHandles) {
|
if (!shouldShowSlabBoundaryHandles) {
|
||||||
return []
|
return []
|
||||||
@@ -14066,6 +14069,57 @@ export function FloorplanPanel() {
|
|||||||
},
|
},
|
||||||
[selectedWallEntry, setMovingNode, setSelection],
|
[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(
|
const handleSelectedWallDelete = useCallback(
|
||||||
(event: ReactMouseEvent<HTMLButtonElement>) => {
|
(event: ReactMouseEvent<HTMLButtonElement>) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
@@ -16105,9 +16159,17 @@ export function FloorplanPanel() {
|
|||||||
site,
|
site,
|
||||||
])
|
])
|
||||||
const hasDuplicatableFloorplanSelection = Boolean(
|
const hasDuplicatableFloorplanSelection = Boolean(
|
||||||
selectedItemEntry || selectedOpeningEntry || selectedStairEntry || selectedRoofEntry,
|
selectedItemEntry ||
|
||||||
|
selectedOpeningEntry ||
|
||||||
|
selectedStairEntry ||
|
||||||
|
selectedRoofEntry ||
|
||||||
|
selectedWallEntry,
|
||||||
)
|
)
|
||||||
const handleDuplicateFloorplanSelection = useCallback(() => {
|
const handleDuplicateFloorplanSelection = useCallback(() => {
|
||||||
|
if (selectedWallEntry) {
|
||||||
|
duplicateSelectedWall()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (selectedOpeningEntry) {
|
if (selectedOpeningEntry) {
|
||||||
duplicateSelectedOpening()
|
duplicateSelectedOpening()
|
||||||
return
|
return
|
||||||
@@ -16124,6 +16186,7 @@ export function FloorplanPanel() {
|
|||||||
duplicateSelectedRoof()
|
duplicateSelectedRoof()
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
duplicateSelectedWall,
|
||||||
duplicateSelectedItem,
|
duplicateSelectedItem,
|
||||||
duplicateSelectedOpening,
|
duplicateSelectedOpening,
|
||||||
duplicateSelectedRoof,
|
duplicateSelectedRoof,
|
||||||
@@ -16132,6 +16195,7 @@ export function FloorplanPanel() {
|
|||||||
selectedOpeningEntry,
|
selectedOpeningEntry,
|
||||||
selectedRoofEntry,
|
selectedRoofEntry,
|
||||||
selectedStairEntry,
|
selectedStairEntry,
|
||||||
|
selectedWallEntry,
|
||||||
])
|
])
|
||||||
const activeDraftAnchorPoint =
|
const activeDraftAnchorPoint =
|
||||||
referenceScaleDraft?.start ??
|
referenceScaleDraft?.start ??
|
||||||
@@ -16258,7 +16322,9 @@ export function FloorplanPanel() {
|
|||||||
}}
|
}}
|
||||||
wall={{
|
wall={{
|
||||||
position: selectedWallActionMenuPosition,
|
position: selectedWallActionMenuPosition,
|
||||||
|
onCurve: canCurveSelectedWall ? handleSelectedWallCurve : undefined,
|
||||||
onDelete: handleSelectedWallDelete,
|
onDelete: handleSelectedWallDelete,
|
||||||
|
onDuplicate: handleSelectedWallDuplicate,
|
||||||
onMove: handleSelectedWallMove,
|
onMove: handleSelectedWallMove,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,13 +9,12 @@ import {
|
|||||||
type GridEvent,
|
type GridEvent,
|
||||||
getMaterialPresetByRef,
|
getMaterialPresetByRef,
|
||||||
getPerpendicularWallMoveAxis,
|
getPerpendicularWallMoveAxis,
|
||||||
getRenderableSlabPolygon,
|
|
||||||
pauseSceneHistory,
|
pauseSceneHistory,
|
||||||
|
planAutoSlabsForLevel,
|
||||||
planWallMoveJunctions,
|
planWallMoveJunctions,
|
||||||
resolveMaterial,
|
resolveMaterial,
|
||||||
resumeSceneHistory,
|
resumeSceneHistory,
|
||||||
type SlabNode,
|
type SlabNode,
|
||||||
SlabNode as SlabSchema,
|
|
||||||
useScene,
|
useScene,
|
||||||
type WallMoveAxis,
|
type WallMoveAxis,
|
||||||
type WallMoveBridgePlan,
|
type WallMoveBridgePlan,
|
||||||
@@ -25,7 +24,7 @@ import {
|
|||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
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 { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
||||||
import { EDITOR_LAYER } from '../../../lib/constants'
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
||||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||||
@@ -33,9 +32,6 @@ import useEditor from '../../../store/use-editor'
|
|||||||
import { CursorSphere } from '../shared/cursor-sphere'
|
import { CursorSphere } from '../shared/cursor-sphere'
|
||||||
import { getWallGridStep, isWallLongEnough, snapScalarToGrid } from './wall-drafting'
|
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] {
|
function rotateVector([x, z]: [number, number], angle: number): [number, number] {
|
||||||
const cos = Math.cos(angle)
|
const cos = Math.cos(angle)
|
||||||
const sin = Math.sin(angle)
|
const sin = Math.sin(angle)
|
||||||
@@ -70,11 +66,6 @@ type GhostWallPreview = {
|
|||||||
height: number
|
height: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type GhostSlabPreview = {
|
|
||||||
id: string
|
|
||||||
polygon: Array<[number, number]>
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLinkedWallSnapshots(args: {
|
function getLinkedWallSnapshots(args: {
|
||||||
wallId: WallNode['id']
|
wallId: WallNode['id']
|
||||||
wallParentId: string | null
|
wallParentId: string | null
|
||||||
@@ -222,27 +213,6 @@ function getWallGhostColor(wall: WallNode) {
|
|||||||
return resolveMaterial(wall.material ?? wall.interiorMaterial ?? wall.exteriorMaterial).color
|
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(
|
function getWallsAfterUpdates(
|
||||||
nodes: ReturnType<typeof useScene.getState>['nodes'],
|
nodes: ReturnType<typeof useScene.getState>['nodes'],
|
||||||
updates: Array<{ id: AnyNodeId; data: Partial<WallNode> }>,
|
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: {
|
function buildBridgeWallCreates(args: {
|
||||||
bridgePlans: Array<WallMoveBridgePlan<LinkedWallSnapshot>>
|
bridgePlans: Array<WallMoveBridgePlan<LinkedWallSnapshot>>
|
||||||
nextStart: [number, number]
|
nextStart: [number, number]
|
||||||
@@ -343,68 +339,6 @@ function buildBridgeWallPreviews(args: {
|
|||||||
return previews
|
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(
|
function setPreviewGeometryAttributes(
|
||||||
geometry: BufferGeometry,
|
geometry: BufferGeometry,
|
||||||
positions: number[],
|
positions: number[],
|
||||||
@@ -430,48 +364,6 @@ function createWallPreviewGeometry(length: number, height: number) {
|
|||||||
return geometry
|
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 }) {
|
function GhostWallPreviewMesh({ preview }: { preview: GhostWallPreview }) {
|
||||||
const dx = preview.end[0] - preview.start[0]
|
const dx = preview.end[0] - preview.start[0]
|
||||||
const dz = preview.end[1] - preview.start[1]
|
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 }) => {
|
export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
|
||||||
const meta =
|
const meta =
|
||||||
typeof node.metadata === 'object' && node.metadata !== null && !Array.isArray(node.metadata)
|
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,
|
originalEnd: node.end,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
const originalAutoSlabsRef = useRef<SlabNode[]>(
|
||||||
|
node.parentId ? getLevelAutoSlabSnapshots(node.parentId) : [],
|
||||||
|
)
|
||||||
const dragAnchorRef = useRef<[number, number] | null>(null)
|
const dragAnchorRef = useRef<[number, number] | null>(null)
|
||||||
const nodeIdRef = useRef(node.id)
|
const nodeIdRef = useRef(node.id)
|
||||||
const previewRef = useRef<{ start: [number, number]; end: [number, number] } | null>(null)
|
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]
|
return [centerX, 0, centerZ]
|
||||||
})
|
})
|
||||||
const [ghostWallPreviews, setGhostWallPreviews] = useState<GhostWallPreview[]>([])
|
const [ghostWallPreviews, setGhostWallPreviews] = useState<GhostWallPreview[]>([])
|
||||||
const [ghostSlabPreviews, setGhostSlabPreviews] = useState<GhostSlabPreview[]>([])
|
|
||||||
|
|
||||||
const exitMoveMode = useCallback(() => {
|
const exitMoveMode = useCallback(() => {
|
||||||
useEditor.getState().setMovingNode(null)
|
useEditor.getState().setMovingNode(null)
|
||||||
@@ -588,9 +453,11 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
|
|||||||
const originalEnd = originalEndRef.current
|
const originalEnd = originalEndRef.current
|
||||||
const originalCenter = originalCenterRef.current
|
const originalCenter = originalCenterRef.current
|
||||||
const originalHalfVector = originalHalfVectorRef.current
|
const originalHalfVector = originalHalfVectorRef.current
|
||||||
|
const levelId = node.parentId ?? null
|
||||||
|
const originalAutoSlabs = originalAutoSlabsRef.current
|
||||||
|
|
||||||
pauseSceneHistory(useScene)
|
pauseSceneHistory(useScene)
|
||||||
let wasCommitted = false
|
let shouldRestoreOnCleanup = true
|
||||||
|
|
||||||
const applyNodePreview = (
|
const applyNodePreview = (
|
||||||
updates: Array<{ id: WallNode['id']; start: [number, number]; end: [number, number] }>,
|
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 buildWallFromCenter = (center: [number, number]) => {
|
||||||
const rotatedHalf = rotateVector(originalHalfVector, pendingRotationRef.current)
|
const rotatedHalf = rotateVector(originalHalfVector, pendingRotationRef.current)
|
||||||
const nextStart: [number, number] = [center[0] - rotatedHalf[0], center[1] - rotatedHalf[1]]
|
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 nextGhostWalls = bridgePreviews.map((preview) => preview.ghost)
|
||||||
const virtualBridgeWalls = bridgePreviews.map((preview) => preview.wall)
|
const virtualBridgeWalls = bridgePreviews.map((preview) => preview.wall)
|
||||||
const sceneNodes = useScene.getState().nodes
|
|
||||||
const levelId = node.parentId ?? null
|
|
||||||
setGhostWallPreviews(nextGhostWalls)
|
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)
|
applyNodePreview(previewUpdates)
|
||||||
|
applyLiveAutoSlabPreview([...previewSceneWalls, ...virtualBridgeWalls])
|
||||||
}
|
}
|
||||||
|
|
||||||
const restoreOriginal = () => {
|
const restoreOriginal = () => {
|
||||||
setGhostWallPreviews([])
|
setGhostWallPreviews([])
|
||||||
setGhostSlabPreviews([])
|
|
||||||
applyNodePreview([
|
applyNodePreview([
|
||||||
{ id: nodeId, start: originalStart, end: originalEnd },
|
{ id: nodeId, start: originalStart, end: originalEnd },
|
||||||
...linkedOriginalsRef.current,
|
...linkedOriginalsRef.current,
|
||||||
])
|
])
|
||||||
|
restoreAutoSlabPreview()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onGridMove = (event: GridEvent) => {
|
const onGridMove = (event: GridEvent) => {
|
||||||
@@ -738,16 +658,16 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
|
|||||||
|
|
||||||
const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
|
const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
|
||||||
|
|
||||||
wasCommitted = true
|
shouldRestoreOnCleanup = false
|
||||||
|
|
||||||
// Restore original baseline while paused so the next resume+update
|
// Restore original baseline while paused so the next resume+update
|
||||||
// registers as a single tracked change (undo reverts to original).
|
// registers as a single tracked change (undo reverts to original).
|
||||||
setGhostWallPreviews([])
|
setGhostWallPreviews([])
|
||||||
setGhostSlabPreviews([])
|
|
||||||
applyNodePreview([
|
applyNodePreview([
|
||||||
{ id: nodeId, start: originalStart, end: originalEnd },
|
{ id: nodeId, start: originalStart, end: originalEnd },
|
||||||
...linkedOriginalsRef.current,
|
...linkedOriginalsRef.current,
|
||||||
])
|
])
|
||||||
|
restoreAutoSlabPreview()
|
||||||
|
|
||||||
resumeSceneHistory(useScene)
|
resumeSceneHistory(useScene)
|
||||||
const commitPlan = getMovePlan(preview.start, preview.end)
|
const commitPlan = getMovePlan(preview.start, preview.end)
|
||||||
@@ -848,6 +768,7 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
|
shouldRestoreOnCleanup = false
|
||||||
restoreOriginal()
|
restoreOriginal()
|
||||||
useViewer.getState().setSelection({ selectedIds: [nodeId] })
|
useViewer.getState().setSelection({ selectedIds: [nodeId] })
|
||||||
resumeSceneHistory(useScene)
|
resumeSceneHistory(useScene)
|
||||||
@@ -862,7 +783,7 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
|
|||||||
window.addEventListener('keyup', onKeyUp)
|
window.addEventListener('keyup', onKeyUp)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (!wasCommitted) {
|
if (shouldRestoreOnCleanup) {
|
||||||
restoreOriginal()
|
restoreOriginal()
|
||||||
}
|
}
|
||||||
shiftPressedRef.current = false
|
shiftPressedRef.current = false
|
||||||
@@ -873,14 +794,11 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
|
|||||||
window.removeEventListener('keydown', onKeyDown)
|
window.removeEventListener('keydown', onKeyDown)
|
||||||
window.removeEventListener('keyup', onKeyUp)
|
window.removeEventListener('keyup', onKeyUp)
|
||||||
}
|
}
|
||||||
}, [exitMoveMode, isNew, node.metadata])
|
}, [exitMoveMode, isNew, node.metadata, node.parentId])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
<CursorSphere position={cursorLocalPos} showTooltip={false} />
|
<CursorSphere position={cursorLocalPos} showTooltip={false} />
|
||||||
{ghostSlabPreviews.map((preview) => (
|
|
||||||
<GhostSlabPreviewMesh key={preview.id} preview={preview} />
|
|
||||||
))}
|
|
||||||
{ghostWallPreviews.map((preview) => (
|
{ghostWallPreviews.map((preview) => (
|
||||||
<GhostWallPreviewMesh key={preview.id} preview={preview} />
|
<GhostWallPreviewMesh key={preview.id} preview={preview} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -4,12 +4,19 @@ import {
|
|||||||
resolveMaterial,
|
resolveMaterial,
|
||||||
useRegistry,
|
useRegistry,
|
||||||
} from '@pascal-app/core'
|
} 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 { float, mix, positionWorld, smoothstep } from 'three/tsl'
|
||||||
import { BackSide, FrontSide, type Mesh, MeshBasicNodeMaterial } from 'three/webgpu'
|
import { BackSide, FrontSide, type Mesh, MeshBasicNodeMaterial } from 'three/webgpu'
|
||||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||||
import { NodeRenderer } from '../node-renderer'
|
import { NodeRenderer } from '../node-renderer'
|
||||||
|
|
||||||
|
function createEmptyGeometry() {
|
||||||
|
const geometry = new BufferGeometry()
|
||||||
|
geometry.setAttribute('position', new Float32BufferAttribute([], 3))
|
||||||
|
return geometry
|
||||||
|
}
|
||||||
|
|
||||||
const gridScale = 5
|
const gridScale = 5
|
||||||
const gridX = positionWorld.x.mul(gridScale).fract()
|
const gridX = positionWorld.x.mul(gridScale).fract()
|
||||||
const gridY = positionWorld.z.mul(gridScale).fract()
|
const gridY = positionWorld.z.mul(gridScale).fract()
|
||||||
@@ -51,10 +58,20 @@ function getCeilingMaterials(color = '#999999') {
|
|||||||
|
|
||||||
export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
|
export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
|
||||||
const ref = useRef<Mesh>(null!)
|
const ref = useRef<Mesh>(null!)
|
||||||
|
const placeholderGeometry = useMemo(createEmptyGeometry, [])
|
||||||
|
const gridPlaceholderGeometry = useMemo(createEmptyGeometry, [])
|
||||||
|
|
||||||
useRegistry(node.id, 'ceiling', ref)
|
useRegistry(node.id, 'ceiling', ref)
|
||||||
const handlers = useNodeEvents(node, 'ceiling')
|
const handlers = useNodeEvents(node, 'ceiling')
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
placeholderGeometry.dispose()
|
||||||
|
gridPlaceholderGeometry.dispose()
|
||||||
|
},
|
||||||
|
[gridPlaceholderGeometry, placeholderGeometry],
|
||||||
|
)
|
||||||
|
|
||||||
const materials = useMemo(() => {
|
const materials = useMemo(() => {
|
||||||
const preset = getMaterialPresetByRef(node.materialPreset)
|
const preset = getMaterialPresetByRef(node.materialPreset)
|
||||||
const props = preset?.mapProperties ?? resolveMaterial(node.material)
|
const props = preset?.mapProperties ?? resolveMaterial(node.material)
|
||||||
@@ -69,17 +86,15 @@ export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh material={materials.bottomMaterial} ref={ref}>
|
<mesh geometry={placeholderGeometry} material={materials.bottomMaterial} ref={ref}>
|
||||||
<boxGeometry args={[0, 0, 0]} />
|
|
||||||
<mesh
|
<mesh
|
||||||
|
geometry={gridPlaceholderGeometry}
|
||||||
material={materials.topMaterial}
|
material={materials.topMaterial}
|
||||||
name="ceiling-grid"
|
name="ceiling-grid"
|
||||||
{...handlers}
|
{...handlers}
|
||||||
scale={0}
|
scale={0}
|
||||||
visible={false}
|
visible={false}
|
||||||
>
|
/>
|
||||||
<boxGeometry args={[0, 0, 0]} />
|
|
||||||
</mesh>
|
|
||||||
{node.children.map((childId) => (
|
{node.children.map((childId) => (
|
||||||
<NodeRenderer key={childId} nodeId={childId} />
|
<NodeRenderer key={childId} nodeId={childId} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ import {
|
|||||||
|
|
||||||
const slabMaterialCache = new Map<string, THREE.MeshStandardMaterial>()
|
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(
|
function getSlabMaterial(
|
||||||
cacheKey: string,
|
cacheKey: string,
|
||||||
params: { material?: SlabNode['material']; materialPreset?: string },
|
params: { material?: SlabNode['material']; materialPreset?: string },
|
||||||
@@ -47,11 +53,14 @@ function getSlabMaterial(
|
|||||||
|
|
||||||
export const SlabRenderer = ({ node }: { node: SlabNode }) => {
|
export const SlabRenderer = ({ node }: { node: SlabNode }) => {
|
||||||
const ref = useRef<Mesh>(null!)
|
const ref = useRef<Mesh>(null!)
|
||||||
|
const placeholderGeometry = useMemo(createEmptyGeometry, [])
|
||||||
|
|
||||||
useRegistry(node.id, 'slab', ref)
|
useRegistry(node.id, 'slab', ref)
|
||||||
|
|
||||||
const handlers = useNodeEvents(node, 'slab')
|
const handlers = useNodeEvents(node, 'slab')
|
||||||
|
|
||||||
|
useEffect(() => () => placeholderGeometry.dispose(), [placeholderGeometry])
|
||||||
|
|
||||||
const material = useMemo(() => {
|
const material = useMemo(() => {
|
||||||
const resolvedMaterial = node.material
|
const resolvedMaterial = node.material
|
||||||
const resolvedMaterialPreset = node.materialPreset
|
const resolvedMaterialPreset = node.materialPreset
|
||||||
@@ -75,13 +84,12 @@ export const SlabRenderer = ({ node }: { node: SlabNode }) => {
|
|||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
castShadow
|
castShadow
|
||||||
|
geometry={placeholderGeometry}
|
||||||
receiveShadow
|
receiveShadow
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...handlers}
|
{...handlers}
|
||||||
material={material}
|
material={material}
|
||||||
visible={node.visible}
|
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
|
const gridMesh = mesh.getObjectByName('ceiling-grid') as THREE.Mesh
|
||||||
if (gridMesh) {
|
if (gridMesh) {
|
||||||
gridMesh.geometry.dispose()
|
gridMesh.geometry.dispose()
|
||||||
gridMesh.geometry = newGeo
|
gridMesh.geometry = newGeo.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Position at the ceiling height
|
// Position at the ceiling height
|
||||||
|
|||||||
Reference in New Issue
Block a user