diff --git a/apps/editor/components/editor/index.tsx b/apps/editor/components/editor/index.tsx index 33933f60..f28a3a29 100644 --- a/apps/editor/components/editor/index.tsx +++ b/apps/editor/components/editor/index.tsx @@ -29,9 +29,6 @@ export default function Editor() { return (
- {/* */} - - @@ -52,30 +49,6 @@ export default function Editor() { ) } -const TestUndo = () => { - const { undo, redo, futureStates, pastStates } = useScene.temporal.getState() - - return ( -
- - -
- ) -} const Grid = ({ cellSize = 0.5, diff --git a/apps/editor/components/ui/panels/roof-panel.tsx b/apps/editor/components/ui/panels/roof-panel.tsx index b32a8bbc..17c09c23 100644 --- a/apps/editor/components/ui/panels/roof-panel.tsx +++ b/apps/editor/components/ui/panels/roof-panel.tsx @@ -2,7 +2,8 @@ import { type AnyNode, type RoofNode, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' -import { Home, X } from 'lucide-react' +import { X } from 'lucide-react' +import Image from 'next/image' import { useCallback } from 'react' export function RoofPanel() { @@ -40,7 +41,7 @@ export function RoofPanel() { {/* Header */}
- +

{node.name || 'Gable Roof'}

diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx index 33b718b3..ea87fb74 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx @@ -1,7 +1,8 @@ import { CeilingNode } from "@pascal-app/core"; import { useViewer } from "@pascal-app/viewer"; -import { Square } from "lucide-react"; +import Image from "next/image"; import { useState } from "react"; +import { RenamePopover } from "./rename-popover"; import { TreeNode, TreeNodeWrapper } from "./tree-node"; import { TreeNodeActions } from "./tree-node-actions"; @@ -12,6 +13,7 @@ interface CeilingTreeNodeProps { export function CeilingTreeNode({ node, depth }: CeilingTreeNodeProps) { const [expanded, setExpanded] = useState(false); + const [renameOpen, setRenameOpen] = 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); @@ -21,6 +23,10 @@ export function CeilingTreeNode({ node, depth }: CeilingTreeNodeProps) { setSelection({ selectedIds: [node.id] }); }; + const handleDoubleClick = () => { + setRenameOpen(true); + }; + const handleMouseEnter = () => { setHoveredId(node.id); }; @@ -31,26 +37,35 @@ export function CeilingTreeNode({ node, depth }: CeilingTreeNodeProps) { // Calculate approximate area from polygon const area = calculatePolygonArea(node.polygon).toFixed(1); + const defaultName = `Ceiling (${area}m²)`; return ( - } - label={node.name || `Ceiling (${area}m²)`} - depth={depth} - hasChildren={node.children.length > 0} - expanded={expanded} - onToggle={() => setExpanded(!expanded)} - onClick={handleClick} - onMouseEnter={handleMouseEnter} - onMouseLeave={handleMouseLeave} - isSelected={isSelected} - isHovered={isHovered} - actions={} + - {node.children.map((childId) => ( - - ))} - + } + label={node.name || defaultName} + depth={depth} + hasChildren={node.children.length > 0} + expanded={expanded} + onToggle={() => setExpanded(!expanded)} + onClick={handleClick} + onDoubleClick={handleDoubleClick} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + isSelected={isSelected} + isHovered={isHovered} + actions={} + > + {node.children.map((childId) => ( + + ))} + + ); } diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx index 3e7bf9d5..b53da349 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx @@ -8,7 +8,6 @@ import { useViewer } from "@pascal-app/viewer"; import { Building2, ChevronDown, - Hexagon, Layers, MoreHorizontal, Plus, @@ -19,6 +18,7 @@ import { cn } from "@/lib/utils"; import useEditor from "@/store/use-editor"; import { TreeNode } from "./tree-node"; import { ReferencesDialog } from "./references-dialog"; +import { RenamePopover } from "./rename-popover"; import { Popover, PopoverContent, @@ -293,7 +293,20 @@ function LayerToggle() { ); } +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; +} + function ZoneItem({ zone }: { zone: ZoneNode }) { + const [renameOpen, setRenameOpen] = useState(false); const deleteNode = useScene((state) => state.deleteNode); const updateNode = useScene((state) => state.updateNode); const selectedZoneId = useViewer((state) => state.selection.zoneId); @@ -306,12 +319,19 @@ function ZoneItem({ zone }: { zone: ZoneNode }) { const isSelected = selectedZoneId === zone.id; const isHovered = hoveredId === zone.id; + const area = calculatePolygonArea(zone.polygon).toFixed(1); + const defaultName = `Zone (${area}m²)`; + const handleClick = () => { setSelection({ zoneId: zone.id }); setPhase("structure"); setMode("select"); }; + const handleDoubleClick = () => { + setRenameOpen(true); + }; + const handleDelete = (e: React.MouseEvent) => { e.stopPropagation(); deleteNode(zone.id); @@ -325,56 +345,63 @@ function ZoneItem({ zone }: { zone: ZoneNode }) { }; return ( -
setHoveredId(zone.id)} - onMouseLeave={() => setHoveredId(null)} + - - -
- - - - {zone.name} - -
+ + +
+ + + {zone.name || defaultName} + +
+ ); } @@ -432,8 +459,13 @@ function ContentSection() { ); } - // Show elements (walls, items, etc.) for this level - if (level.children.length === 0) { + // Show elements (walls, items, etc.) for this level - filter out zones + const elementChildren = level.children.filter((childId) => { + const childNode = nodes[childId]; + return childNode && childNode.type !== "zone"; + }); + + if (elementChildren.length === 0) { return (
No elements on this level @@ -443,7 +475,7 @@ function ContentSection() { return (
- {level.children.map((childId) => ( + {elementChildren.map((childId) => ( ))}
diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/item-tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/item-tree-node.tsx index 8a1757ff..d64b53e3 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/item-tree-node.tsx @@ -1,15 +1,29 @@ import { ItemNode } from "@pascal-app/core"; import { useViewer } from "@pascal-app/viewer"; -import { Box } from "lucide-react"; +import Image from "next/image"; +import { useState } from "react"; +import { RenamePopover } from "./rename-popover"; import { TreeNodeWrapper } from "./tree-node"; import { TreeNodeActions } from "./tree-node-actions"; +const CATEGORY_ICONS: Record = { + door: "/icons/door.png", + window: "/icons/window.png", + furniture: "/icons/couch.png", + appliance: "/icons/appliance.png", + kitchen: "/icons/kitchen.png", + bathroom: "/icons/bathroom.png", + outdoor: "/icons/tree.png", +}; + interface ItemTreeNodeProps { node: ItemNode; depth: number; } export function ItemTreeNode({ node, depth }: ItemTreeNodeProps) { + const [renameOpen, setRenameOpen] = useState(false); + const iconSrc = CATEGORY_ICONS[node.asset.category] || "/icons/couch.png"; const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id)); const isHovered = useViewer((state) => state.hoveredId === node.id); const setSelection = useViewer((state) => state.setSelection); @@ -19,6 +33,10 @@ export function ItemTreeNode({ node, depth }: ItemTreeNodeProps) { setSelection({ selectedIds: [node.id] }); }; + const handleDoubleClick = () => { + setRenameOpen(true); + }; + const handleMouseEnter = () => { setHoveredId(node.id); }; @@ -27,20 +45,30 @@ export function ItemTreeNode({ node, depth }: ItemTreeNodeProps) { setHoveredId(null); }; + const defaultName = node.asset.name || "Item"; + return ( - } - label={node.name || node.asset.name || "Item"} - depth={depth} - hasChildren={false} - expanded={false} - onToggle={() => {}} - onClick={handleClick} - onMouseEnter={handleMouseEnter} - onMouseLeave={handleMouseLeave} - isSelected={isSelected} - isHovered={isHovered} - actions={} - /> + + } + label={node.name || defaultName} + depth={depth} + hasChildren={false} + expanded={false} + onToggle={() => {}} + onClick={handleClick} + onDoubleClick={handleDoubleClick} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + isSelected={isSelected} + isHovered={isHovered} + actions={} + /> + ); } diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/level-tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/level-tree-node.tsx index 9b58b9db..43d67bf8 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/level-tree-node.tsx @@ -2,6 +2,7 @@ import { LevelNode } from "@pascal-app/core"; import { useViewer } from "@pascal-app/viewer"; import { Layers } from "lucide-react"; import { useState } from "react"; +import { RenamePopover } from "./rename-popover"; import { TreeNode, TreeNodeWrapper } from "./tree-node"; import { TreeNodeActions } from "./tree-node-actions"; @@ -12,6 +13,7 @@ interface LevelTreeNodeProps { export function LevelTreeNode({ node, depth }: LevelTreeNodeProps) { const [expanded, setExpanded] = useState(true); + const [renameOpen, setRenameOpen] = useState(false); const isSelected = useViewer((state) => state.selection.levelId === node.id); const isHovered = useViewer((state) => state.hoveredId === node.id); const setSelection = useViewer((state) => state.setSelection); @@ -20,22 +22,36 @@ export function LevelTreeNode({ node, depth }: LevelTreeNodeProps) { setSelection({ levelId: node.id }); }; + const handleDoubleClick = () => { + setRenameOpen(true); + }; + + const defaultName = `Level ${node.level}`; + return ( - } - label={node.name || `Level ${node.level}`} - depth={depth} - hasChildren={node.children.length > 0} - expanded={expanded} - onToggle={() => setExpanded(!expanded)} - onClick={handleClick} - isSelected={isSelected} - isHovered={isHovered} - actions={} + - {node.children.map((childId) => ( - - ))} - + } + label={node.name || defaultName} + depth={depth} + hasChildren={node.children.length > 0} + expanded={expanded} + onToggle={() => setExpanded(!expanded)} + onClick={handleClick} + onDoubleClick={handleDoubleClick} + isSelected={isSelected} + isHovered={isHovered} + actions={} + > + {node.children.map((childId) => ( + + ))} + + ); } diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/rename-popover.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/rename-popover.tsx new file mode 100644 index 00000000..762a75d6 --- /dev/null +++ b/apps/editor/components/ui/sidebar/panels/site-panel/rename-popover.tsx @@ -0,0 +1,94 @@ +import { useScene, type AnyNode } from "@pascal-app/core"; +import { Check, X } from "lucide-react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { Popover, PopoverAnchor, PopoverContent } from "@/components/ui/primitives/popover"; + +interface RenamePopoverProps { + node: AnyNode; + open: boolean; + onOpenChange: (open: boolean) => void; + children: React.ReactNode; + defaultName: string; +} + +export function RenamePopover({ + node, + open, + onOpenChange, + children, + defaultName, +}: RenamePopoverProps) { + const updateNode = useScene((s) => s.updateNode); + const [value, setValue] = useState(node.name || ""); + const inputRef = useRef(null); + + // Reset value when popover opens + useEffect(() => { + if (open) { + setValue(node.name || ""); + // Focus and select all text after a short delay + setTimeout(() => { + inputRef.current?.focus(); + inputRef.current?.select(); + }, 0); + } + }, [open, node.name]); + + const handleSave = useCallback(() => { + const trimmed = value.trim(); + // Only update if name actually changed + if (trimmed !== node.name) { + updateNode(node.id, { name: trimmed || undefined }); + } + onOpenChange(false); + }, [value, node.id, node.name, updateNode, onOpenChange]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + handleSave(); + } else if (e.key === "Escape") { + e.preventDefault(); + onOpenChange(false); + } + }; + + return ( + + {children} + e.preventDefault()} + > +
+ setValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={defaultName} + className="flex-1 rounded border border-input bg-background px-2 py-1 text-sm outline-none focus:border-primary" + /> + + +
+
+
+ ); +} diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx index 15b9cf26..26f2ac0b 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx @@ -1,6 +1,8 @@ import { RoofNode } from "@pascal-app/core"; import { useViewer } from "@pascal-app/viewer"; -import { Home } from "lucide-react"; +import Image from "next/image"; +import { useState } from "react"; +import { RenamePopover } from "./rename-popover"; import { TreeNodeWrapper } from "./tree-node"; import { TreeNodeActions } from "./tree-node-actions"; @@ -10,6 +12,7 @@ interface RoofTreeNodeProps { } export function RoofTreeNode({ node, depth }: RoofTreeNodeProps) { + const [renameOpen, setRenameOpen] = 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); @@ -19,6 +22,10 @@ export function RoofTreeNode({ node, depth }: RoofTreeNodeProps) { setSelection({ selectedIds: [node.id] }); }; + const handleDoubleClick = () => { + setRenameOpen(true); + }; + const handleMouseEnter = () => { setHoveredId(node.id); }; @@ -30,21 +37,30 @@ export function RoofTreeNode({ node, depth }: RoofTreeNodeProps) { // Calculate dimensions: length × total width (leftWidth + rightWidth) const totalWidth = node.leftWidth + node.rightWidth; const sizeLabel = `${node.length.toFixed(1)}×${totalWidth.toFixed(1)}m`; + const defaultName = `Roof (${sizeLabel})`; return ( - } - label={node.name || `Roof (${sizeLabel})`} - depth={depth} - hasChildren={false} - expanded={false} - onToggle={() => {}} - onClick={handleClick} - onMouseEnter={handleMouseEnter} - onMouseLeave={handleMouseLeave} - isSelected={isSelected} - isHovered={isHovered} - actions={} - /> + + } + label={node.name || defaultName} + depth={depth} + hasChildren={false} + expanded={false} + onToggle={() => {}} + onClick={handleClick} + onDoubleClick={handleDoubleClick} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + isSelected={isSelected} + isHovered={isHovered} + actions={} + /> + ); } 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 index 2a45243e..86c43860 100644 --- 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 @@ -1,6 +1,8 @@ import { SlabNode } from "@pascal-app/core"; import { useViewer } from "@pascal-app/viewer"; -import { RectangleHorizontal } from "lucide-react"; +import Image from "next/image"; +import { useState } from "react"; +import { RenamePopover } from "./rename-popover"; import { TreeNodeWrapper } from "./tree-node"; import { TreeNodeActions } from "./tree-node-actions"; @@ -10,6 +12,7 @@ interface SlabTreeNodeProps { } export function SlabTreeNode({ node, depth }: SlabTreeNodeProps) { + const [renameOpen, setRenameOpen] = 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); @@ -19,6 +22,10 @@ export function SlabTreeNode({ node, depth }: SlabTreeNodeProps) { setSelection({ selectedIds: [node.id] }); }; + const handleDoubleClick = () => { + setRenameOpen(true); + }; + const handleMouseEnter = () => { setHoveredId(node.id); }; @@ -29,22 +36,31 @@ export function SlabTreeNode({ node, depth }: SlabTreeNodeProps) { // Calculate approximate area from polygon const area = calculatePolygonArea(node.polygon).toFixed(1); + const defaultName = `Slab (${area}m²)`; 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={} - /> + + } + label={node.name || defaultName} + depth={depth} + hasChildren={false} + expanded={false} + onToggle={() => {}} + onClick={handleClick} + onDoubleClick={handleDoubleClick} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + isSelected={isSelected} + isHovered={isHovered} + actions={} + /> + ); } 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 553f4354..f5a447fc 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 @@ -1,5 +1,6 @@ import { AnyNodeId, useScene } from "@pascal-app/core"; import { ChevronDown, ChevronRight } from "lucide-react"; +import { forwardRef } from "react"; import { cn } from "@/lib/utils"; import { BuildingTreeNode } from "./building-tree-node"; import { CeilingTreeNode } from "./ceiling-tree-node"; @@ -8,6 +9,7 @@ import { LevelTreeNode } from "./level-tree-node"; import { RoofTreeNode } from "./roof-tree-node"; import { SlabTreeNode } from "./slab-tree-node"; import { WallTreeNode } from "./wall-tree-node"; +import { ZoneTreeNode } from "./zone-tree-node"; interface TreeNodeProps { nodeId: AnyNodeId; @@ -34,6 +36,8 @@ export function TreeNode({ nodeId, depth = 0 }: TreeNodeProps) { return ; case "item": return ; + case "zone": + return ; default: return null; } @@ -47,6 +51,7 @@ interface TreeNodeWrapperProps { expanded: boolean; onToggle: () => void; onClick: () => void; + onDoubleClick?: () => void; onMouseEnter?: () => void; onMouseLeave?: () => void; actions?: React.ReactNode; @@ -55,67 +60,74 @@ interface TreeNodeWrapperProps { isHovered?: boolean; } -export function TreeNodeWrapper({ - icon, - label, - depth, - hasChildren, - expanded, - onToggle, - onClick, - onMouseEnter, - onMouseLeave, - actions, - children, - isSelected, - isHovered, -}: TreeNodeWrapperProps) { - return ( -
-
- +export const TreeNodeWrapper = forwardRef( + function TreeNodeWrapper( + { + icon, + label, + depth, + hasChildren, + expanded, + onToggle, + onClick, + onDoubleClick, + onMouseEnter, + onMouseLeave, + actions, + children, + isSelected, + isHovered, + }, + ref + ) { + return ( +
- - {icon} - - {label} -
- {actions && ( -
- {actions} + +
+ + {icon} + + {label}
- )} + {actions && ( +
+ {actions} +
+ )} +
+ {expanded && children}
- {expanded && children} -
- ); -} + ); + } +); diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx index aab6f3a7..a5a4625e 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx @@ -1,7 +1,8 @@ import { WallNode } from "@pascal-app/core"; import { useViewer } from "@pascal-app/viewer"; -import { Square } from "lucide-react"; +import Image from "next/image"; import { useState } from "react"; +import { RenamePopover } from "./rename-popover"; import { TreeNode, TreeNodeWrapper } from "./tree-node"; import { TreeNodeActions } from "./tree-node-actions"; @@ -12,6 +13,7 @@ interface WallTreeNodeProps { export function WallTreeNode({ node, depth }: WallTreeNodeProps) { const [expanded, setExpanded] = useState(false); + const [renameOpen, setRenameOpen] = 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); @@ -21,6 +23,10 @@ export function WallTreeNode({ node, depth }: WallTreeNodeProps) { setSelection({ selectedIds: [node.id] }); }; + const handleDoubleClick = () => { + setRenameOpen(true); + }; + const handleMouseEnter = () => { setHoveredId(node.id); }; @@ -34,24 +40,34 @@ export function WallTreeNode({ node, depth }: WallTreeNodeProps) { Math.pow(node.end[1] - node.start[1], 2), ).toFixed(1); + const defaultName = `Wall (${wallLength}m/${node.height || 2.5}m)`; + return ( - } - label={node.name || `Wall (${wallLength}m/${node.height || 2.5}m)`} - depth={depth} - hasChildren={node.children.length > 0} - expanded={expanded} - onToggle={() => setExpanded(!expanded)} - onClick={handleClick} - onMouseEnter={handleMouseEnter} - onMouseLeave={handleMouseLeave} - isSelected={isSelected} - isHovered={isHovered} - actions={} + - {node.children.map((childId) => ( - - ))} - + } + label={node.name || defaultName} + depth={depth} + hasChildren={node.children.length > 0} + expanded={expanded} + onToggle={() => setExpanded(!expanded)} + onClick={handleClick} + onDoubleClick={handleDoubleClick} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + isSelected={isSelected} + isHovered={isHovered} + actions={} + > + {node.children.map((childId) => ( + + ))} + + ); } diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx new file mode 100644 index 00000000..4a99abdd --- /dev/null +++ b/apps/editor/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx @@ -0,0 +1,87 @@ +import { ZoneNode } from "@pascal-app/core"; +import { useViewer } from "@pascal-app/viewer"; +import { useState } from "react"; +import { RenamePopover } from "./rename-popover"; +import { TreeNodeWrapper } from "./tree-node"; +import { TreeNodeActions } from "./tree-node-actions"; + +interface ZoneTreeNodeProps { + node: ZoneNode; + depth: number; +} + +export function ZoneTreeNode({ node, depth }: ZoneTreeNodeProps) { + const [renameOpen, setRenameOpen] = useState(false); + const isSelected = useViewer((state) => state.selection.zoneId === 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({ zoneId: node.id }); + }; + + const handleDoubleClick = () => { + setRenameOpen(true); + }; + + const handleMouseEnter = () => { + setHoveredId(node.id); + }; + + const handleMouseLeave = () => { + setHoveredId(null); + }; + + // Calculate approximate area from polygon + const area = calculatePolygonArea(node.polygon).toFixed(1); + const defaultName = `Zone (${area}m²)`; + + return ( + + + } + label={node.name || defaultName} + depth={depth} + hasChildren={false} + expanded={false} + onToggle={() => {}} + onClick={handleClick} + onDoubleClick={handleDoubleClick} + 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/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts b/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts index e266179b..8e8e75ad 100644 --- a/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts +++ b/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts @@ -210,7 +210,6 @@ export class SpatialGridManager { // Called when nodes change handleNodeCreated(node: AnyNode, levelId: string) { if (node.type === 'slab') { - console.log(`[SpatialGrid] Adding slab ${node.id} to level ${levelId}`, (node as SlabNode).polygon) this.getSlabMap(levelId).set(node.id, node as SlabNode) } else if (node.type === 'ceiling') { this.ceilings.set(node.id, node as CeilingNode) @@ -459,7 +458,6 @@ export class SpatialGridManager { end: [number, number], ): number { const slabMap = this.slabsByLevel.get(levelId) - console.log(`[SpatialGrid] getSlabElevationForWall | levelId: ${levelId} | slabMap size: ${slabMap?.size ?? 0} | slabs:`, slabMap ? Array.from(slabMap.keys()) : []) if (!slabMap) return 0 let maxElevation = 0 diff --git a/packages/core/src/systems/wall/wall-system.tsx b/packages/core/src/systems/wall/wall-system.tsx index c9d9b8e9..a40544ee 100644 --- a/packages/core/src/systems/wall/wall-system.tsx +++ b/packages/core/src/systems/wall/wall-system.tsx @@ -107,7 +107,6 @@ function updateWallGeometry(wallId: string, miterData: WallMiterData) { const levelId = resolveLevelId(node, nodes) const slabElevation = spatialGridManager.getSlabElevationForWall(levelId, node.start, node.end) - console.log(`[WallSystem] Wall ${node.name || node.id} | levelId: ${levelId} | start: [${node.start}] | end: [${node.end}] | slabElevation: ${slabElevation}`) const childrenIds = node.children || [] const childrenNodes = childrenIds