rename items
This commit is contained in:
@@ -29,9 +29,6 @@ export default function Editor() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<div className="w-full h-full">
|
||||||
{/* <LevelModeSwitcher /> */}
|
|
||||||
|
|
||||||
<TestUndo />
|
|
||||||
<ActionMenu />
|
<ActionMenu />
|
||||||
<ReferencePanel />
|
<ReferencePanel />
|
||||||
<RoofPanel />
|
<RoofPanel />
|
||||||
@@ -52,30 +49,6 @@ export default function Editor() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const TestUndo = () => {
|
|
||||||
const { undo, redo, futureStates, pastStates } = useScene.temporal.getState()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="absolute top-4 right-4 z-10 flex gap-2">
|
|
||||||
<button
|
|
||||||
className="px-4 py-2 rounded bg-white"
|
|
||||||
onClick={() => {
|
|
||||||
undo()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Undo
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="px-4 py-2 rounded bg-white"
|
|
||||||
onClick={() => {
|
|
||||||
redo()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Redo
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Grid = ({
|
const Grid = ({
|
||||||
cellSize = 0.5,
|
cellSize = 0.5,
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import { type AnyNode, type RoofNode, useScene } from '@pascal-app/core'
|
import { type AnyNode, type RoofNode, useScene } from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
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'
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
export function RoofPanel() {
|
export function RoofPanel() {
|
||||||
@@ -40,7 +41,7 @@ export function RoofPanel() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between gap-2 border-b p-3">
|
<div className="flex items-center justify-between gap-2 border-b p-3">
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<Home className="h-4 w-4 shrink-0 text-muted-foreground" />
|
<Image src="/icons/roof.png" alt="" width={16} height={16} className="shrink-0 object-contain" />
|
||||||
<h2 className="font-semibold text-foreground text-sm truncate">
|
<h2 className="font-semibold text-foreground text-sm truncate">
|
||||||
{node.name || 'Gable Roof'}
|
{node.name || 'Gable Roof'}
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { CeilingNode } from "@pascal-app/core";
|
import { CeilingNode } from "@pascal-app/core";
|
||||||
import { useViewer } from "@pascal-app/viewer";
|
import { useViewer } from "@pascal-app/viewer";
|
||||||
import { Square } from "lucide-react";
|
import Image from "next/image";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { RenamePopover } from "./rename-popover";
|
||||||
import { TreeNode, TreeNodeWrapper } from "./tree-node";
|
import { TreeNode, TreeNodeWrapper } from "./tree-node";
|
||||||
import { TreeNodeActions } from "./tree-node-actions";
|
import { TreeNodeActions } from "./tree-node-actions";
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ interface CeilingTreeNodeProps {
|
|||||||
|
|
||||||
export function CeilingTreeNode({ node, depth }: CeilingTreeNodeProps) {
|
export function CeilingTreeNode({ node, depth }: CeilingTreeNodeProps) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const [renameOpen, setRenameOpen] = useState(false);
|
||||||
const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id));
|
const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id));
|
||||||
const isHovered = useViewer((state) => state.hoveredId === node.id);
|
const isHovered = useViewer((state) => state.hoveredId === node.id);
|
||||||
const setSelection = useViewer((state) => state.setSelection);
|
const setSelection = useViewer((state) => state.setSelection);
|
||||||
@@ -21,6 +23,10 @@ export function CeilingTreeNode({ node, depth }: CeilingTreeNodeProps) {
|
|||||||
setSelection({ selectedIds: [node.id] });
|
setSelection({ selectedIds: [node.id] });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDoubleClick = () => {
|
||||||
|
setRenameOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
setHoveredId(node.id);
|
setHoveredId(node.id);
|
||||||
};
|
};
|
||||||
@@ -31,16 +37,24 @@ export function CeilingTreeNode({ node, depth }: CeilingTreeNodeProps) {
|
|||||||
|
|
||||||
// Calculate approximate area from polygon
|
// Calculate approximate area from polygon
|
||||||
const area = calculatePolygonArea(node.polygon).toFixed(1);
|
const area = calculatePolygonArea(node.polygon).toFixed(1);
|
||||||
|
const defaultName = `Ceiling (${area}m²)`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<RenamePopover
|
||||||
|
node={node}
|
||||||
|
open={renameOpen}
|
||||||
|
onOpenChange={setRenameOpen}
|
||||||
|
defaultName={defaultName}
|
||||||
|
>
|
||||||
<TreeNodeWrapper
|
<TreeNodeWrapper
|
||||||
icon={<Square className="w-3.5 h-3.5" />}
|
icon={<Image src="/icons/ceiling.png" alt="" width={14} height={14} className="object-contain" />}
|
||||||
label={node.name || `Ceiling (${area}m²)`}
|
label={node.name || defaultName}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
hasChildren={node.children.length > 0}
|
hasChildren={node.children.length > 0}
|
||||||
expanded={expanded}
|
expanded={expanded}
|
||||||
onToggle={() => setExpanded(!expanded)}
|
onToggle={() => setExpanded(!expanded)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onDoubleClick={handleDoubleClick}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
@@ -51,6 +65,7 @@ export function CeilingTreeNode({ node, depth }: CeilingTreeNodeProps) {
|
|||||||
<TreeNode key={childId} nodeId={childId} depth={depth + 1} />
|
<TreeNode key={childId} nodeId={childId} depth={depth + 1} />
|
||||||
))}
|
))}
|
||||||
</TreeNodeWrapper>
|
</TreeNodeWrapper>
|
||||||
|
</RenamePopover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { useViewer } from "@pascal-app/viewer";
|
|||||||
import {
|
import {
|
||||||
Building2,
|
Building2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Hexagon,
|
|
||||||
Layers,
|
Layers,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
Plus,
|
Plus,
|
||||||
@@ -19,6 +18,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import useEditor from "@/store/use-editor";
|
import useEditor from "@/store/use-editor";
|
||||||
import { TreeNode } from "./tree-node";
|
import { TreeNode } from "./tree-node";
|
||||||
import { ReferencesDialog } from "./references-dialog";
|
import { ReferencesDialog } from "./references-dialog";
|
||||||
|
import { RenamePopover } from "./rename-popover";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
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 }) {
|
function ZoneItem({ zone }: { zone: ZoneNode }) {
|
||||||
|
const [renameOpen, setRenameOpen] = useState(false);
|
||||||
const deleteNode = useScene((state) => state.deleteNode);
|
const deleteNode = useScene((state) => state.deleteNode);
|
||||||
const updateNode = useScene((state) => state.updateNode);
|
const updateNode = useScene((state) => state.updateNode);
|
||||||
const selectedZoneId = useViewer((state) => state.selection.zoneId);
|
const selectedZoneId = useViewer((state) => state.selection.zoneId);
|
||||||
@@ -306,12 +319,19 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
|
|||||||
const isSelected = selectedZoneId === zone.id;
|
const isSelected = selectedZoneId === zone.id;
|
||||||
const isHovered = hoveredId === zone.id;
|
const isHovered = hoveredId === zone.id;
|
||||||
|
|
||||||
|
const area = calculatePolygonArea(zone.polygon).toFixed(1);
|
||||||
|
const defaultName = `Zone (${area}m²)`;
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
setSelection({ zoneId: zone.id });
|
setSelection({ zoneId: zone.id });
|
||||||
setPhase("structure");
|
setPhase("structure");
|
||||||
setMode("select");
|
setMode("select");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDoubleClick = () => {
|
||||||
|
setRenameOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleDelete = (e: React.MouseEvent) => {
|
const handleDelete = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
deleteNode(zone.id);
|
deleteNode(zone.id);
|
||||||
@@ -325,9 +345,15 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<RenamePopover
|
||||||
|
node={zone}
|
||||||
|
open={renameOpen}
|
||||||
|
onOpenChange={setRenameOpen}
|
||||||
|
defaultName={defaultName}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center h-7 cursor-pointer group/row text-sm px-3",
|
"flex items-center h-7 cursor-pointer group/row text-sm px-3 select-none",
|
||||||
isSelected
|
isSelected
|
||||||
? "text-primary-foreground bg-primary/80 hover:bg-primary/90"
|
? "text-primary-foreground bg-primary/80 hover:bg-primary/90"
|
||||||
: isHovered
|
: isHovered
|
||||||
@@ -335,6 +361,7 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
|
|||||||
: "text-muted-foreground hover:bg-accent/50"
|
: "text-muted-foreground hover:bg-accent/50"
|
||||||
)}
|
)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onDoubleClick={handleDoubleClick}
|
||||||
onMouseEnter={() => setHoveredId(zone.id)}
|
onMouseEnter={() => setHoveredId(zone.id)}
|
||||||
onMouseLeave={() => setHoveredId(null)}
|
onMouseLeave={() => setHoveredId(null)}
|
||||||
>
|
>
|
||||||
@@ -366,8 +393,7 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
|
|||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Hexagon className="w-3.5 h-3.5 mr-1.5 shrink-0" />
|
<span className="truncate flex-1">{zone.name || defaultName}</span>
|
||||||
<span className="truncate flex-1">{zone.name}</span>
|
|
||||||
<button
|
<button
|
||||||
className="opacity-0 group-hover/row:opacity-100 w-5 h-5 flex items-center justify-center rounded cursor-pointer hover:bg-primary-foreground/20"
|
className="opacity-0 group-hover/row:opacity-100 w-5 h-5 flex items-center justify-center rounded cursor-pointer hover:bg-primary-foreground/20"
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
@@ -375,6 +401,7 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
|
|||||||
<Trash2 className="w-3 h-3" />
|
<Trash2 className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</RenamePopover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,8 +459,13 @@ function ContentSection() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show elements (walls, items, etc.) for this level
|
// Show elements (walls, items, etc.) for this level - filter out zones
|
||||||
if (level.children.length === 0) {
|
const elementChildren = level.children.filter((childId) => {
|
||||||
|
const childNode = nodes[childId];
|
||||||
|
return childNode && childNode.type !== "zone";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (elementChildren.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="px-3 py-4 text-sm text-muted-foreground">
|
<div className="px-3 py-4 text-sm text-muted-foreground">
|
||||||
No elements on this level
|
No elements on this level
|
||||||
@@ -443,7 +475,7 @@ function ContentSection() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
{level.children.map((childId) => (
|
{elementChildren.map((childId) => (
|
||||||
<TreeNode key={childId} nodeId={childId} depth={0} />
|
<TreeNode key={childId} nodeId={childId} depth={0} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,29 @@
|
|||||||
import { ItemNode } from "@pascal-app/core";
|
import { ItemNode } from "@pascal-app/core";
|
||||||
import { useViewer } from "@pascal-app/viewer";
|
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 { TreeNodeWrapper } from "./tree-node";
|
||||||
import { TreeNodeActions } from "./tree-node-actions";
|
import { TreeNodeActions } from "./tree-node-actions";
|
||||||
|
|
||||||
|
const CATEGORY_ICONS: Record<string, string> = {
|
||||||
|
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 {
|
interface ItemTreeNodeProps {
|
||||||
node: ItemNode;
|
node: ItemNode;
|
||||||
depth: number;
|
depth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ItemTreeNode({ node, depth }: ItemTreeNodeProps) {
|
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 isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id));
|
||||||
const isHovered = useViewer((state) => state.hoveredId === node.id);
|
const isHovered = useViewer((state) => state.hoveredId === node.id);
|
||||||
const setSelection = useViewer((state) => state.setSelection);
|
const setSelection = useViewer((state) => state.setSelection);
|
||||||
@@ -19,6 +33,10 @@ export function ItemTreeNode({ node, depth }: ItemTreeNodeProps) {
|
|||||||
setSelection({ selectedIds: [node.id] });
|
setSelection({ selectedIds: [node.id] });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDoubleClick = () => {
|
||||||
|
setRenameOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
setHoveredId(node.id);
|
setHoveredId(node.id);
|
||||||
};
|
};
|
||||||
@@ -27,20 +45,30 @@ export function ItemTreeNode({ node, depth }: ItemTreeNodeProps) {
|
|||||||
setHoveredId(null);
|
setHoveredId(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultName = node.asset.name || "Item";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<RenamePopover
|
||||||
|
node={node}
|
||||||
|
open={renameOpen}
|
||||||
|
onOpenChange={setRenameOpen}
|
||||||
|
defaultName={defaultName}
|
||||||
|
>
|
||||||
<TreeNodeWrapper
|
<TreeNodeWrapper
|
||||||
icon={<Box className="w-3.5 h-3.5" />}
|
icon={<Image src={iconSrc} alt="" width={14} height={14} className="object-contain" />}
|
||||||
label={node.name || node.asset.name || "Item"}
|
label={node.name || defaultName}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
hasChildren={false}
|
hasChildren={false}
|
||||||
expanded={false}
|
expanded={false}
|
||||||
onToggle={() => {}}
|
onToggle={() => {}}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onDoubleClick={handleDoubleClick}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
isHovered={isHovered}
|
isHovered={isHovered}
|
||||||
actions={<TreeNodeActions node={node} />}
|
actions={<TreeNodeActions node={node} />}
|
||||||
/>
|
/>
|
||||||
|
</RenamePopover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { LevelNode } from "@pascal-app/core";
|
|||||||
import { useViewer } from "@pascal-app/viewer";
|
import { useViewer } from "@pascal-app/viewer";
|
||||||
import { Layers } from "lucide-react";
|
import { Layers } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { RenamePopover } from "./rename-popover";
|
||||||
import { TreeNode, TreeNodeWrapper } from "./tree-node";
|
import { TreeNode, TreeNodeWrapper } from "./tree-node";
|
||||||
import { TreeNodeActions } from "./tree-node-actions";
|
import { TreeNodeActions } from "./tree-node-actions";
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ interface LevelTreeNodeProps {
|
|||||||
|
|
||||||
export function LevelTreeNode({ node, depth }: LevelTreeNodeProps) {
|
export function LevelTreeNode({ node, depth }: LevelTreeNodeProps) {
|
||||||
const [expanded, setExpanded] = useState(true);
|
const [expanded, setExpanded] = useState(true);
|
||||||
|
const [renameOpen, setRenameOpen] = useState(false);
|
||||||
const isSelected = useViewer((state) => state.selection.levelId === node.id);
|
const isSelected = useViewer((state) => state.selection.levelId === node.id);
|
||||||
const isHovered = useViewer((state) => state.hoveredId === node.id);
|
const isHovered = useViewer((state) => state.hoveredId === node.id);
|
||||||
const setSelection = useViewer((state) => state.setSelection);
|
const setSelection = useViewer((state) => state.setSelection);
|
||||||
@@ -20,15 +22,28 @@ export function LevelTreeNode({ node, depth }: LevelTreeNodeProps) {
|
|||||||
setSelection({ levelId: node.id });
|
setSelection({ levelId: node.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDoubleClick = () => {
|
||||||
|
setRenameOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultName = `Level ${node.level}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<RenamePopover
|
||||||
|
node={node}
|
||||||
|
open={renameOpen}
|
||||||
|
onOpenChange={setRenameOpen}
|
||||||
|
defaultName={defaultName}
|
||||||
|
>
|
||||||
<TreeNodeWrapper
|
<TreeNodeWrapper
|
||||||
icon={<Layers className="w-3.5 h-3.5" />}
|
icon={<Layers className="w-3.5 h-3.5" />}
|
||||||
label={node.name || `Level ${node.level}`}
|
label={node.name || defaultName}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
hasChildren={node.children.length > 0}
|
hasChildren={node.children.length > 0}
|
||||||
expanded={expanded}
|
expanded={expanded}
|
||||||
onToggle={() => setExpanded(!expanded)}
|
onToggle={() => setExpanded(!expanded)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onDoubleClick={handleDoubleClick}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
isHovered={isHovered}
|
isHovered={isHovered}
|
||||||
actions={<TreeNodeActions node={node} />}
|
actions={<TreeNodeActions node={node} />}
|
||||||
@@ -37,5 +52,6 @@ export function LevelTreeNode({ node, depth }: LevelTreeNodeProps) {
|
|||||||
<TreeNode key={childId} nodeId={childId} depth={depth + 1} />
|
<TreeNode key={childId} nodeId={childId} depth={depth + 1} />
|
||||||
))}
|
))}
|
||||||
</TreeNodeWrapper>
|
</TreeNodeWrapper>
|
||||||
|
</RenamePopover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<HTMLInputElement>(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 (
|
||||||
|
<Popover open={open} onOpenChange={onOpenChange}>
|
||||||
|
<PopoverAnchor asChild>{children}</PopoverAnchor>
|
||||||
|
<PopoverContent
|
||||||
|
className="w-64 p-2 z-50"
|
||||||
|
align="start"
|
||||||
|
side="right"
|
||||||
|
sideOffset={8}
|
||||||
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-7 h-7 flex items-center justify-center rounded hover:bg-accent text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-7 h-7 flex items-center justify-center rounded hover:bg-accent text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { RoofNode } from "@pascal-app/core";
|
import { RoofNode } from "@pascal-app/core";
|
||||||
import { useViewer } from "@pascal-app/viewer";
|
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 { TreeNodeWrapper } from "./tree-node";
|
||||||
import { TreeNodeActions } from "./tree-node-actions";
|
import { TreeNodeActions } from "./tree-node-actions";
|
||||||
|
|
||||||
@@ -10,6 +12,7 @@ interface RoofTreeNodeProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function RoofTreeNode({ node, depth }: RoofTreeNodeProps) {
|
export function RoofTreeNode({ node, depth }: RoofTreeNodeProps) {
|
||||||
|
const [renameOpen, setRenameOpen] = useState(false);
|
||||||
const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id));
|
const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id));
|
||||||
const isHovered = useViewer((state) => state.hoveredId === node.id);
|
const isHovered = useViewer((state) => state.hoveredId === node.id);
|
||||||
const setSelection = useViewer((state) => state.setSelection);
|
const setSelection = useViewer((state) => state.setSelection);
|
||||||
@@ -19,6 +22,10 @@ export function RoofTreeNode({ node, depth }: RoofTreeNodeProps) {
|
|||||||
setSelection({ selectedIds: [node.id] });
|
setSelection({ selectedIds: [node.id] });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDoubleClick = () => {
|
||||||
|
setRenameOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
setHoveredId(node.id);
|
setHoveredId(node.id);
|
||||||
};
|
};
|
||||||
@@ -30,21 +37,30 @@ export function RoofTreeNode({ node, depth }: RoofTreeNodeProps) {
|
|||||||
// Calculate dimensions: length × total width (leftWidth + rightWidth)
|
// Calculate dimensions: length × total width (leftWidth + rightWidth)
|
||||||
const totalWidth = node.leftWidth + node.rightWidth;
|
const totalWidth = node.leftWidth + node.rightWidth;
|
||||||
const sizeLabel = `${node.length.toFixed(1)}×${totalWidth.toFixed(1)}m`;
|
const sizeLabel = `${node.length.toFixed(1)}×${totalWidth.toFixed(1)}m`;
|
||||||
|
const defaultName = `Roof (${sizeLabel})`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<RenamePopover
|
||||||
|
node={node}
|
||||||
|
open={renameOpen}
|
||||||
|
onOpenChange={setRenameOpen}
|
||||||
|
defaultName={defaultName}
|
||||||
|
>
|
||||||
<TreeNodeWrapper
|
<TreeNodeWrapper
|
||||||
icon={<Home className="w-3.5 h-3.5" />}
|
icon={<Image src="/icons/roof.png" alt="" width={14} height={14} className="object-contain" />}
|
||||||
label={node.name || `Roof (${sizeLabel})`}
|
label={node.name || defaultName}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
hasChildren={false}
|
hasChildren={false}
|
||||||
expanded={false}
|
expanded={false}
|
||||||
onToggle={() => {}}
|
onToggle={() => {}}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onDoubleClick={handleDoubleClick}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
isHovered={isHovered}
|
isHovered={isHovered}
|
||||||
actions={<TreeNodeActions node={node} />}
|
actions={<TreeNodeActions node={node} />}
|
||||||
/>
|
/>
|
||||||
|
</RenamePopover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { SlabNode } from "@pascal-app/core";
|
import { SlabNode } from "@pascal-app/core";
|
||||||
import { useViewer } from "@pascal-app/viewer";
|
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 { TreeNodeWrapper } from "./tree-node";
|
||||||
import { TreeNodeActions } from "./tree-node-actions";
|
import { TreeNodeActions } from "./tree-node-actions";
|
||||||
|
|
||||||
@@ -10,6 +12,7 @@ interface SlabTreeNodeProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SlabTreeNode({ node, depth }: SlabTreeNodeProps) {
|
export function SlabTreeNode({ node, depth }: SlabTreeNodeProps) {
|
||||||
|
const [renameOpen, setRenameOpen] = useState(false);
|
||||||
const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id));
|
const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id));
|
||||||
const isHovered = useViewer((state) => state.hoveredId === node.id);
|
const isHovered = useViewer((state) => state.hoveredId === node.id);
|
||||||
const setSelection = useViewer((state) => state.setSelection);
|
const setSelection = useViewer((state) => state.setSelection);
|
||||||
@@ -19,6 +22,10 @@ export function SlabTreeNode({ node, depth }: SlabTreeNodeProps) {
|
|||||||
setSelection({ selectedIds: [node.id] });
|
setSelection({ selectedIds: [node.id] });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDoubleClick = () => {
|
||||||
|
setRenameOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
setHoveredId(node.id);
|
setHoveredId(node.id);
|
||||||
};
|
};
|
||||||
@@ -29,22 +36,31 @@ export function SlabTreeNode({ node, depth }: SlabTreeNodeProps) {
|
|||||||
|
|
||||||
// Calculate approximate area from polygon
|
// Calculate approximate area from polygon
|
||||||
const area = calculatePolygonArea(node.polygon).toFixed(1);
|
const area = calculatePolygonArea(node.polygon).toFixed(1);
|
||||||
|
const defaultName = `Slab (${area}m²)`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<RenamePopover
|
||||||
|
node={node}
|
||||||
|
open={renameOpen}
|
||||||
|
onOpenChange={setRenameOpen}
|
||||||
|
defaultName={defaultName}
|
||||||
|
>
|
||||||
<TreeNodeWrapper
|
<TreeNodeWrapper
|
||||||
icon={<RectangleHorizontal className="w-3.5 h-3.5" />}
|
icon={<Image src="/icons/floor.png" alt="" width={14} height={14} className="object-contain" />}
|
||||||
label={node.name || `Slab (${area}m²)`}
|
label={node.name || defaultName}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
hasChildren={false}
|
hasChildren={false}
|
||||||
expanded={false}
|
expanded={false}
|
||||||
onToggle={() => {}}
|
onToggle={() => {}}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onDoubleClick={handleDoubleClick}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
isHovered={isHovered}
|
isHovered={isHovered}
|
||||||
actions={<TreeNodeActions node={node} />}
|
actions={<TreeNodeActions node={node} />}
|
||||||
/>
|
/>
|
||||||
|
</RenamePopover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { AnyNodeId, useScene } from "@pascal-app/core";
|
import { AnyNodeId, useScene } from "@pascal-app/core";
|
||||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||||
|
import { forwardRef } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { BuildingTreeNode } from "./building-tree-node";
|
import { BuildingTreeNode } from "./building-tree-node";
|
||||||
import { CeilingTreeNode } from "./ceiling-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 { RoofTreeNode } from "./roof-tree-node";
|
||||||
import { SlabTreeNode } from "./slab-tree-node";
|
import { SlabTreeNode } from "./slab-tree-node";
|
||||||
import { WallTreeNode } from "./wall-tree-node";
|
import { WallTreeNode } from "./wall-tree-node";
|
||||||
|
import { ZoneTreeNode } from "./zone-tree-node";
|
||||||
|
|
||||||
interface TreeNodeProps {
|
interface TreeNodeProps {
|
||||||
nodeId: AnyNodeId;
|
nodeId: AnyNodeId;
|
||||||
@@ -34,6 +36,8 @@ export function TreeNode({ nodeId, depth = 0 }: TreeNodeProps) {
|
|||||||
return <RoofTreeNode node={node} depth={depth} />;
|
return <RoofTreeNode node={node} depth={depth} />;
|
||||||
case "item":
|
case "item":
|
||||||
return <ItemTreeNode node={node} depth={depth} />;
|
return <ItemTreeNode node={node} depth={depth} />;
|
||||||
|
case "zone":
|
||||||
|
return <ZoneTreeNode node={node} depth={depth} />;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -47,6 +51,7 @@ interface TreeNodeWrapperProps {
|
|||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
onDoubleClick?: () => void;
|
||||||
onMouseEnter?: () => void;
|
onMouseEnter?: () => void;
|
||||||
onMouseLeave?: () => void;
|
onMouseLeave?: () => void;
|
||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
@@ -55,7 +60,9 @@ interface TreeNodeWrapperProps {
|
|||||||
isHovered?: boolean;
|
isHovered?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TreeNodeWrapper({
|
export const TreeNodeWrapper = forwardRef<HTMLDivElement, TreeNodeWrapperProps>(
|
||||||
|
function TreeNodeWrapper(
|
||||||
|
{
|
||||||
icon,
|
icon,
|
||||||
label,
|
label,
|
||||||
depth,
|
depth,
|
||||||
@@ -63,18 +70,21 @@ export function TreeNodeWrapper({
|
|||||||
expanded,
|
expanded,
|
||||||
onToggle,
|
onToggle,
|
||||||
onClick,
|
onClick,
|
||||||
|
onDoubleClick,
|
||||||
onMouseEnter,
|
onMouseEnter,
|
||||||
onMouseLeave,
|
onMouseLeave,
|
||||||
actions,
|
actions,
|
||||||
children,
|
children,
|
||||||
isSelected,
|
isSelected,
|
||||||
isHovered,
|
isHovered,
|
||||||
}: TreeNodeWrapperProps) {
|
},
|
||||||
|
ref
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div ref={ref}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center h-7 cursor-pointer group/row text-sm",
|
"flex items-center h-7 cursor-pointer group/row text-sm select-none",
|
||||||
isSelected
|
isSelected
|
||||||
? "text-primary-foreground bg-primary/80 hover:bg-primary/90"
|
? "text-primary-foreground bg-primary/80 hover:bg-primary/90"
|
||||||
: isHovered
|
: isHovered
|
||||||
@@ -103,6 +113,7 @@ export function TreeNodeWrapper({
|
|||||||
<div
|
<div
|
||||||
className="flex items-center gap-1.5 flex-1 min-w-0"
|
className="flex items-center gap-1.5 flex-1 min-w-0"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
onDoubleClick={onDoubleClick}
|
||||||
>
|
>
|
||||||
<span className="w-4 h-4 flex items-center justify-center shrink-0">
|
<span className="w-4 h-4 flex items-center justify-center shrink-0">
|
||||||
{icon}
|
{icon}
|
||||||
@@ -119,3 +130,4 @@ export function TreeNodeWrapper({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { WallNode } from "@pascal-app/core";
|
import { WallNode } from "@pascal-app/core";
|
||||||
import { useViewer } from "@pascal-app/viewer";
|
import { useViewer } from "@pascal-app/viewer";
|
||||||
import { Square } from "lucide-react";
|
import Image from "next/image";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { RenamePopover } from "./rename-popover";
|
||||||
import { TreeNode, TreeNodeWrapper } from "./tree-node";
|
import { TreeNode, TreeNodeWrapper } from "./tree-node";
|
||||||
import { TreeNodeActions } from "./tree-node-actions";
|
import { TreeNodeActions } from "./tree-node-actions";
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ interface WallTreeNodeProps {
|
|||||||
|
|
||||||
export function WallTreeNode({ node, depth }: WallTreeNodeProps) {
|
export function WallTreeNode({ node, depth }: WallTreeNodeProps) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const [renameOpen, setRenameOpen] = useState(false);
|
||||||
const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id));
|
const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id));
|
||||||
const isHovered = useViewer((state) => state.hoveredId === node.id);
|
const isHovered = useViewer((state) => state.hoveredId === node.id);
|
||||||
const setSelection = useViewer((state) => state.setSelection);
|
const setSelection = useViewer((state) => state.setSelection);
|
||||||
@@ -21,6 +23,10 @@ export function WallTreeNode({ node, depth }: WallTreeNodeProps) {
|
|||||||
setSelection({ selectedIds: [node.id] });
|
setSelection({ selectedIds: [node.id] });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDoubleClick = () => {
|
||||||
|
setRenameOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
setHoveredId(node.id);
|
setHoveredId(node.id);
|
||||||
};
|
};
|
||||||
@@ -34,15 +40,24 @@ export function WallTreeNode({ node, depth }: WallTreeNodeProps) {
|
|||||||
Math.pow(node.end[1] - node.start[1], 2),
|
Math.pow(node.end[1] - node.start[1], 2),
|
||||||
).toFixed(1);
|
).toFixed(1);
|
||||||
|
|
||||||
|
const defaultName = `Wall (${wallLength}m/${node.height || 2.5}m)`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<RenamePopover
|
||||||
|
node={node}
|
||||||
|
open={renameOpen}
|
||||||
|
onOpenChange={setRenameOpen}
|
||||||
|
defaultName={defaultName}
|
||||||
|
>
|
||||||
<TreeNodeWrapper
|
<TreeNodeWrapper
|
||||||
icon={<Square className="w-3.5 h-3.5" />}
|
icon={<Image src="/icons/wall.png" alt="" width={14} height={14} className="object-contain" />}
|
||||||
label={node.name || `Wall (${wallLength}m/${node.height || 2.5}m)`}
|
label={node.name || defaultName}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
hasChildren={node.children.length > 0}
|
hasChildren={node.children.length > 0}
|
||||||
expanded={expanded}
|
expanded={expanded}
|
||||||
onToggle={() => setExpanded(!expanded)}
|
onToggle={() => setExpanded(!expanded)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onDoubleClick={handleDoubleClick}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
@@ -53,5 +68,6 @@ export function WallTreeNode({ node, depth }: WallTreeNodeProps) {
|
|||||||
<TreeNode key={childId} nodeId={childId} depth={depth + 1} />
|
<TreeNode key={childId} nodeId={childId} depth={depth + 1} />
|
||||||
))}
|
))}
|
||||||
</TreeNodeWrapper>
|
</TreeNodeWrapper>
|
||||||
|
</RenamePopover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<RenamePopover
|
||||||
|
node={node}
|
||||||
|
open={renameOpen}
|
||||||
|
onOpenChange={setRenameOpen}
|
||||||
|
defaultName={defaultName}
|
||||||
|
>
|
||||||
|
<TreeNodeWrapper
|
||||||
|
icon={
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-sm border border-border/50"
|
||||||
|
style={{ backgroundColor: node.color }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={node.name || defaultName}
|
||||||
|
depth={depth}
|
||||||
|
hasChildren={false}
|
||||||
|
expanded={false}
|
||||||
|
onToggle={() => {}}
|
||||||
|
onClick={handleClick}
|
||||||
|
onDoubleClick={handleDoubleClick}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
isSelected={isSelected}
|
||||||
|
isHovered={isHovered}
|
||||||
|
actions={<TreeNodeActions node={node} />}
|
||||||
|
/>
|
||||||
|
</RenamePopover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
@@ -210,7 +210,6 @@ export class SpatialGridManager {
|
|||||||
// Called when nodes change
|
// Called when nodes change
|
||||||
handleNodeCreated(node: AnyNode, levelId: string) {
|
handleNodeCreated(node: AnyNode, levelId: string) {
|
||||||
if (node.type === 'slab') {
|
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)
|
this.getSlabMap(levelId).set(node.id, node as SlabNode)
|
||||||
} else if (node.type === 'ceiling') {
|
} else if (node.type === 'ceiling') {
|
||||||
this.ceilings.set(node.id, node as CeilingNode)
|
this.ceilings.set(node.id, node as CeilingNode)
|
||||||
@@ -459,7 +458,6 @@ export class SpatialGridManager {
|
|||||||
end: [number, number],
|
end: [number, number],
|
||||||
): number {
|
): number {
|
||||||
const slabMap = this.slabsByLevel.get(levelId)
|
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
|
if (!slabMap) return 0
|
||||||
|
|
||||||
let maxElevation = 0
|
let maxElevation = 0
|
||||||
|
|||||||
@@ -107,7 +107,6 @@ function updateWallGeometry(wallId: string, miterData: WallMiterData) {
|
|||||||
|
|
||||||
const levelId = resolveLevelId(node, nodes)
|
const levelId = resolveLevelId(node, nodes)
|
||||||
const slabElevation = spatialGridManager.getSlabElevationForWall(levelId, node.start, node.end)
|
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 childrenIds = node.children || []
|
||||||
const childrenNodes = childrenIds
|
const childrenNodes = childrenIds
|
||||||
|
|||||||
Reference in New Issue
Block a user