diff --git a/apps/editor/components/editor/selection-manager.tsx b/apps/editor/components/editor/selection-manager.tsx index 2f69c0c2..a0acf89a 100644 --- a/apps/editor/components/editor/selection-manager.tsx +++ b/apps/editor/components/editor/selection-manager.tsx @@ -20,7 +20,7 @@ const isNodeInCurrentLevel = (node: AnyNode): boolean => { return nodeLevelId === currentLevelId; }; -type SelectableNodeType = "wall" | "item" | "building" | "zone"; +type SelectableNodeType = "wall" | "item" | "building" | "zone" | 'slab'; interface SelectionStrategy { types: SelectableNodeType[]; @@ -44,7 +44,7 @@ const SELECTION_STRATEGIES: Record = { }, structure: { - types: ["wall", "item", "zone"], + types: ["wall", "item", "zone", "slab"], handleSelect: (node, isShift) => { const { selection, setSelection } = useViewer.getState(); if (node.type === 'zone') { @@ -73,7 +73,7 @@ const SELECTION_STRATEGIES: Record = { if (node.type === "zone") return true; return false; } else { - if (node.type === "wall") return true; + if (node.type === "wall" || node.type === "slab") return true; if (node.type === "item") { return ( (node as ItemNode).asset.category === "door" || diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx new file mode 100644 index 00000000..2a45243e --- /dev/null +++ b/apps/editor/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx @@ -0,0 +1,67 @@ +import { SlabNode } from "@pascal-app/core"; +import { useViewer } from "@pascal-app/viewer"; +import { RectangleHorizontal } from "lucide-react"; +import { TreeNodeWrapper } from "./tree-node"; +import { TreeNodeActions } from "./tree-node-actions"; + +interface SlabTreeNodeProps { + node: SlabNode; + depth: number; +} + +export function SlabTreeNode({ node, depth }: SlabTreeNodeProps) { + const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id)); + const isHovered = useViewer((state) => state.hoveredId === node.id); + const setSelection = useViewer((state) => state.setSelection); + const setHoveredId = useViewer((state) => state.setHoveredId); + + const handleClick = () => { + setSelection({ selectedIds: [node.id] }); + }; + + const handleMouseEnter = () => { + setHoveredId(node.id); + }; + + const handleMouseLeave = () => { + setHoveredId(null); + }; + + // Calculate approximate area from polygon + const area = calculatePolygonArea(node.polygon).toFixed(1); + + return ( + } + label={node.name || `Slab (${area}m²)`} + depth={depth} + hasChildren={false} + expanded={false} + onToggle={() => {}} + onClick={handleClick} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + isSelected={isSelected} + isHovered={isHovered} + actions={} + /> + ); +} + +/** + * Calculate the area of a polygon using the shoelace formula + */ +function calculatePolygonArea(polygon: Array<[number, number]>): number { + if (polygon.length < 3) return 0; + + let area = 0; + const n = polygon.length; + + for (let i = 0; i < n; i++) { + const j = (i + 1) % n; + area += polygon[i]![0] * polygon[j]![1]; + area -= polygon[j]![0] * polygon[i]![1]; + } + + return Math.abs(area) / 2; +} diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/tree-node.tsx index 14e152b4..4f12223b 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/tree-node.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/tree-node.tsx @@ -2,9 +2,10 @@ import { AnyNodeId, useScene } from "@pascal-app/core"; import { ChevronDown, ChevronRight } from "lucide-react"; import { cn } from "@/lib/utils"; import { BuildingTreeNode } from "./building-tree-node"; -import { LevelTreeNode } from "./level-tree-node"; -import { WallTreeNode } from "./wall-tree-node"; import { ItemTreeNode } from "./item-tree-node"; +import { LevelTreeNode } from "./level-tree-node"; +import { SlabTreeNode } from "./slab-tree-node"; +import { WallTreeNode } from "./wall-tree-node"; interface TreeNodeProps { nodeId: AnyNodeId; @@ -21,6 +22,8 @@ export function TreeNode({ nodeId, depth = 0 }: TreeNodeProps) { return ; case "level": return ; + case "slab": + return ; case "wall": return ; case "item": diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8dc560f9..9783ec1c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -7,6 +7,7 @@ export type { GridEvent, ItemEvent, NodeEvent, + SlabEvent, WallEvent, ZoneEvent, } from './events/bus' diff --git a/packages/viewer/src/components/renderers/slab/slab-renderer.tsx b/packages/viewer/src/components/renderers/slab/slab-renderer.tsx index 96390272..e7207eab 100644 --- a/packages/viewer/src/components/renderers/slab/slab-renderer.tsx +++ b/packages/viewer/src/components/renderers/slab/slab-renderer.tsx @@ -1,14 +1,17 @@ -import { useRegistry, type SlabNode } from '@pascal-app/core' +import { type SlabNode, useRegistry } from '@pascal-app/core' import { useRef } from 'react' import type { Mesh } from 'three' +import { useNodeEvents } from '../../../hooks/use-node-events' export const SlabRenderer = ({ node }: { node: SlabNode }) => { const ref = useRef(null!) useRegistry(node.id, 'slab', ref) + const handlers = useNodeEvents(node, 'slab') + return ( - + {/* SlabSystem will replace this geometry in the next frame */} diff --git a/packages/viewer/src/hooks/use-node-events.ts b/packages/viewer/src/hooks/use-node-events.ts index 18d2b561..999baf8b 100644 --- a/packages/viewer/src/hooks/use-node-events.ts +++ b/packages/viewer/src/hooks/use-node-events.ts @@ -5,6 +5,8 @@ import { emitter, type ItemEvent, type ItemNode, + type SlabEvent, + type SlabNode, type WallEvent, type WallNode, type ZoneEvent, @@ -17,6 +19,7 @@ type NodeConfig = { wall: { node: WallNode; event: WallEvent } building: { node: BuildingNode; event: BuildingEvent } zone: { node: ZoneNode; event: ZoneEvent } + slab: { node: SlabNode; event: SlabEvent } } type NodeType = keyof NodeConfig