working ceiling items
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
type CeilingEvent,
|
||||
type CeilingNode,
|
||||
emitter,
|
||||
type GridEvent,
|
||||
ItemNode,
|
||||
@@ -112,7 +113,7 @@ export const ItemTool: React.FC = () => {
|
||||
const draftItem = useRef<ItemNode | null>(null)
|
||||
const gridPosition = useRef(new Vector3(0, 0, 0))
|
||||
const selectedItem = useEditor((state) => state.selectedItem)
|
||||
const { canPlaceOnFloor, canPlaceOnWall } = useSpatialQuery()
|
||||
const { canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling } = useSpatialQuery()
|
||||
const isOnWall = useRef(false)
|
||||
const isOnCeiling = useRef(false)
|
||||
|
||||
@@ -129,7 +130,18 @@ export const ItemTool: React.FC = () => {
|
||||
if (currentLevelId && draftItem.current) {
|
||||
let placeable = true
|
||||
if (draftItem.current.asset.attachTo === 'ceiling') {
|
||||
placeable = isOnCeiling.current && !!currentCeilingId
|
||||
if (!isOnCeiling.current || !currentCeilingId) {
|
||||
placeable = false
|
||||
} else {
|
||||
const result = canPlaceOnCeiling(
|
||||
currentCeilingId as CeilingNode['id'],
|
||||
[gridPosition.current.x, gridPosition.current.y, gridPosition.current.z],
|
||||
draftItem.current.asset.dimensions,
|
||||
draftItem.current.rotation,
|
||||
[draftItem.current.id],
|
||||
)
|
||||
placeable = result.valid
|
||||
}
|
||||
} else if (draftItem.current.asset.attachTo) {
|
||||
if (!isOnWall.current || !currentWallId) {
|
||||
placeable = false
|
||||
@@ -541,7 +553,7 @@ export const ItemTool: React.FC = () => {
|
||||
emitter.off('ceiling:leave', onCeilingLeave)
|
||||
window.removeEventListener('keydown', onKeyDown)
|
||||
}
|
||||
}, [selectedItem, canPlaceOnFloor, canPlaceOnWall])
|
||||
}, [selectedItem, canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling])
|
||||
|
||||
useFrame((_, delta) => {
|
||||
if (draftItem.current && !isOnWall.current && !isOnCeiling.current) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { CeilingNode } from "@pascal-app/core";
|
||||
import { useViewer } from "@pascal-app/viewer";
|
||||
import { Square } from "lucide-react";
|
||||
import { TreeNodeWrapper } from "./tree-node";
|
||||
import { useState } from "react";
|
||||
import { TreeNode, TreeNodeWrapper } from "./tree-node";
|
||||
import { TreeNodeActions } from "./tree-node-actions";
|
||||
|
||||
interface CeilingTreeNodeProps {
|
||||
@@ -10,6 +11,7 @@ interface CeilingTreeNodeProps {
|
||||
}
|
||||
|
||||
export function CeilingTreeNode({ node, depth }: CeilingTreeNodeProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id));
|
||||
const isHovered = useViewer((state) => state.hoveredId === node.id);
|
||||
const setSelection = useViewer((state) => state.setSelection);
|
||||
@@ -35,16 +37,20 @@ export function CeilingTreeNode({ node, depth }: CeilingTreeNodeProps) {
|
||||
icon={<Square className="w-3.5 h-3.5" />}
|
||||
label={node.name || `Ceiling (${area}m²)`}
|
||||
depth={depth}
|
||||
hasChildren={false}
|
||||
expanded={false}
|
||||
onToggle={() => {}}
|
||||
hasChildren={node.children.length > 0}
|
||||
expanded={expanded}
|
||||
onToggle={() => setExpanded(!expanded)}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
isSelected={isSelected}
|
||||
isHovered={isHovered}
|
||||
actions={<TreeNodeActions node={node} />}
|
||||
/>
|
||||
>
|
||||
{node.children.map((childId) => (
|
||||
<TreeNode key={childId} nodeId={childId} depth={depth + 1} />
|
||||
))}
|
||||
</TreeNodeWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AnyNode, ItemNode, SlabNode, WallNode } from '../../schema'
|
||||
import type { AnyNode, CeilingNode, ItemNode, SlabNode, WallNode } from '../../schema'
|
||||
import { SpatialGrid } from './spatial-grid'
|
||||
import { WallSpatialGrid } from './wall-spatial-grid'
|
||||
|
||||
@@ -160,6 +160,9 @@ export class SpatialGridManager {
|
||||
private wallGrids = new Map<string, WallSpatialGrid>() // levelId -> wall grid
|
||||
private walls = new Map<string, WallNode>() // wallId -> wall data (for length calculations)
|
||||
private slabsByLevel = new Map<string, Map<string, SlabNode>>() // levelId -> (slabId -> slab)
|
||||
private ceilingGrids = new Map<string, SpatialGrid>() // ceilingId -> grid
|
||||
private ceilings = new Map<string, CeilingNode>() // ceilingId -> ceiling data
|
||||
private itemCeilingMap = new Map<string, string>() // itemId -> ceilingId (reverse lookup)
|
||||
|
||||
constructor(private cellSize = 0.5) {}
|
||||
|
||||
@@ -190,6 +193,13 @@ export class SpatialGridManager {
|
||||
return wall?.height ?? 2.5 // Default wall height
|
||||
}
|
||||
|
||||
private getCeilingGrid(ceilingId: string): SpatialGrid {
|
||||
if (!this.ceilingGrids.has(ceilingId)) {
|
||||
this.ceilingGrids.set(ceilingId, new SpatialGrid({ cellSize: this.cellSize }))
|
||||
}
|
||||
return this.ceilingGrids.get(ceilingId)!
|
||||
}
|
||||
|
||||
private getSlabMap(levelId: string): Map<string, SlabNode> {
|
||||
if (!this.slabsByLevel.has(levelId)) {
|
||||
this.slabsByLevel.set(levelId, new Map())
|
||||
@@ -201,6 +211,8 @@ export class SpatialGridManager {
|
||||
handleNodeCreated(node: AnyNode, levelId: string) {
|
||||
if (node.type === 'slab') {
|
||||
this.getSlabMap(levelId).set(node.id, node as SlabNode)
|
||||
} else if (node.type === 'ceiling') {
|
||||
this.ceilings.set(node.id, node as CeilingNode)
|
||||
} else if (node.type === 'wall') {
|
||||
const wall = node as WallNode
|
||||
this.walls.set(wall.id, wall)
|
||||
@@ -229,6 +241,13 @@ export class SpatialGridManager {
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (item.asset.attachTo === 'ceiling') {
|
||||
// Ceiling item - use parentId as the ceiling ID
|
||||
const ceilingId = item.parentId
|
||||
if (ceilingId && this.ceilings.has(ceilingId)) {
|
||||
this.getCeilingGrid(ceilingId).insert(item.id, item.position, item.asset.dimensions, item.rotation)
|
||||
this.itemCeilingMap.set(item.id, ceilingId)
|
||||
}
|
||||
} else if (!item.asset.attachTo) {
|
||||
// Floor item
|
||||
this.getFloorGrid(levelId).insert(
|
||||
@@ -244,6 +263,8 @@ export class SpatialGridManager {
|
||||
handleNodeUpdated(node: AnyNode, levelId: string) {
|
||||
if (node.type === 'slab') {
|
||||
this.getSlabMap(levelId).set(node.id, node as SlabNode)
|
||||
} else if (node.type === 'ceiling') {
|
||||
this.ceilings.set(node.id, node as CeilingNode)
|
||||
} else if (node.type === 'wall') {
|
||||
const wall = node as WallNode
|
||||
this.walls.set(wall.id, wall)
|
||||
@@ -273,6 +294,19 @@ export class SpatialGridManager {
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (item.asset.attachTo === 'ceiling') {
|
||||
// Remove from old ceiling grid
|
||||
const oldCeilingId = this.itemCeilingMap.get(item.id)
|
||||
if (oldCeilingId) {
|
||||
this.getCeilingGrid(oldCeilingId).remove(item.id)
|
||||
this.itemCeilingMap.delete(item.id)
|
||||
}
|
||||
// Insert into new ceiling grid
|
||||
const ceilingId = item.parentId
|
||||
if (ceilingId && this.ceilings.has(ceilingId)) {
|
||||
this.getCeilingGrid(ceilingId).insert(item.id, item.position, item.asset.dimensions, item.rotation)
|
||||
this.itemCeilingMap.set(item.id, ceilingId)
|
||||
}
|
||||
} else if (!item.asset.attachTo) {
|
||||
this.getFloorGrid(levelId).update(
|
||||
item.id,
|
||||
@@ -287,6 +321,9 @@ export class SpatialGridManager {
|
||||
handleNodeDeleted(nodeId: string, nodeType: string, levelId: string) {
|
||||
if (nodeType === 'slab') {
|
||||
this.getSlabMap(levelId).delete(nodeId)
|
||||
} else if (nodeType === 'ceiling') {
|
||||
this.ceilings.delete(nodeId)
|
||||
this.ceilingGrids.delete(nodeId)
|
||||
} else if (nodeType === 'wall') {
|
||||
this.walls.delete(nodeId)
|
||||
// Remove all items attached to this wall from the spatial grid
|
||||
@@ -295,6 +332,12 @@ export class SpatialGridManager {
|
||||
} else if (nodeType === 'item') {
|
||||
this.getFloorGrid(levelId).remove(nodeId)
|
||||
this.getWallGrid(levelId).removeByItemId(nodeId)
|
||||
// Also clean up ceiling grid
|
||||
const oldCeilingId = this.itemCeilingMap.get(nodeId)
|
||||
if (oldCeilingId) {
|
||||
this.getCeilingGrid(oldCeilingId).remove(nodeId)
|
||||
this.itemCeilingMap.delete(nodeId)
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
@@ -438,6 +481,34 @@ export class SpatialGridManager {
|
||||
return maxElevation
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an item can be placed on a ceiling.
|
||||
* Validates that the footprint is within the ceiling polygon and doesn't overlap other ceiling items.
|
||||
*/
|
||||
canPlaceOnCeiling(
|
||||
ceilingId: string,
|
||||
position: [number, number, number],
|
||||
dimensions: [number, number, number],
|
||||
rotation: [number, number, number],
|
||||
ignoreIds?: string[],
|
||||
): { valid: boolean; conflictIds: string[] } {
|
||||
const ceiling = this.ceilings.get(ceilingId)
|
||||
if (!ceiling || ceiling.polygon.length < 3) {
|
||||
return { valid: false, conflictIds: [] }
|
||||
}
|
||||
|
||||
// Check that the item footprint is entirely within the ceiling polygon
|
||||
const corners = getItemFootprint(position, dimensions, rotation)
|
||||
for (const [cx, cz] of corners) {
|
||||
if (!pointInPolygon(cx, cz, ceiling.polygon)) {
|
||||
return { valid: false, conflictIds: [] }
|
||||
}
|
||||
}
|
||||
|
||||
// Check for overlaps with other ceiling items
|
||||
return this.getCeilingGrid(ceilingId).canPlace(position, dimensions, rotation, ignoreIds)
|
||||
}
|
||||
|
||||
clearLevel(levelId: string) {
|
||||
this.floorGrids.delete(levelId)
|
||||
this.wallGrids.delete(levelId)
|
||||
@@ -449,6 +520,9 @@ export class SpatialGridManager {
|
||||
this.wallGrids.clear()
|
||||
this.walls.clear()
|
||||
this.slabsByLevel.clear()
|
||||
this.ceilingGrids.clear()
|
||||
this.ceilings.clear()
|
||||
this.itemCeilingMap.clear()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { LevelNode, WallNode } from '../../schema'
|
||||
import type { CeilingNode, LevelNode, WallNode } from '../../schema'
|
||||
import { spatialGridManager } from './spatial-grid-manager'
|
||||
|
||||
export function useSpatialQuery() {
|
||||
@@ -41,5 +41,18 @@ export function useSpatialQuery() {
|
||||
[],
|
||||
)
|
||||
|
||||
return { canPlaceOnFloor, canPlaceOnWall }
|
||||
const canPlaceOnCeiling = useCallback(
|
||||
(
|
||||
ceilingId: CeilingNode['id'],
|
||||
position: [number, number, number],
|
||||
dimensions: [number, number, number],
|
||||
rotation: [number, number, number],
|
||||
ignoreIds?: string[],
|
||||
) => {
|
||||
return spatialGridManager.canPlaceOnCeiling(ceilingId, position, dimensions, rotation, ignoreIds)
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
return { canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user