From cba709cb8105c00fe1a6d9a8d93403f60aa1854f Mon Sep 17 00:00:00 2001 From: wass08 Date: Mon, 9 Feb 2026 16:11:50 +0900 Subject: [PATCH] bring back site edition + zindex fix --- .../tools/site/site-boundary-editor.tsx | 4 +- .../ui/sidebar/panels/site-panel/index.tsx | 245 +++++++++++++++--- .../renderers/site/site-renderer.tsx | 29 +++ .../renderers/zone/zone-renderer.tsx | 3 +- 4 files changed, 241 insertions(+), 40 deletions(-) diff --git a/apps/editor/components/tools/site/site-boundary-editor.tsx b/apps/editor/components/tools/site/site-boundary-editor.tsx index bf197fff..4783bd4d 100644 --- a/apps/editor/components/tools/site/site-boundary-editor.tsx +++ b/apps/editor/components/tools/site/site-boundary-editor.tsx @@ -1,4 +1,4 @@ -import { useScene, type SiteNode } from '@pascal-app/core' +import { type SiteNode, useScene } from '@pascal-app/core' import { useCallback } from 'react' import { PolygonEditor } from '../shared/polygon-editor' @@ -34,7 +34,7 @@ export const SiteBoundaryEditor: React.FC = () => { return ( 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 494dd895..67aa1d74 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx @@ -2,6 +2,7 @@ import { type BuildingNode, emitter, LevelNode, + type SiteNode, useScene, type ZoneNode, } from "@pascal-app/core"; @@ -11,7 +12,9 @@ import { Camera, ChevronDown, Layers, + MapPin, MoreHorizontal, + Pencil, Plus, Trash2, } from "lucide-react"; @@ -40,7 +43,186 @@ const PRESET_COLORS = [ ]; // ============================================================================ -// SITE PHASE VIEW - Simple building buttons +// PROPERTY LINE SECTION +// ============================================================================ + +function calculatePerimeter(points: Array<[number, number]>): number { + if (points.length < 2) return 0; + let perimeter = 0; + for (let i = 0; i < points.length; i++) { + const [x1, z1] = points[i]!; + const [x2, z2] = points[(i + 1) % points.length]!; + perimeter += Math.sqrt((x2 - x1) ** 2 + (z2 - z1) ** 2); + } + return perimeter; +} + +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 useSiteNode(): SiteNode | null { + const siteId = useScene((state) => { + for (const id of state.rootNodeIds) { + if (state.nodes[id]?.type === "site") return id; + } + return null; + }); + return useScene((state) => + siteId ? ((state.nodes[siteId] as SiteNode | undefined) ?? null) : null + ); +} + +function PropertyLineSection() { + const siteNode = useSiteNode(); + const updateNode = useScene((state) => state.updateNode); + const mode = useEditor((state) => state.mode); + const setMode = useEditor((state) => state.setMode); + + if (!siteNode) return null; + + const points = siteNode.polygon?.points ?? []; + const area = calculatePolygonArea(points); + const perimeter = calculatePerimeter(points); + const isEditing = mode === "edit"; + + const handleToggleEdit = () => { + setMode(isEditing ? "select" : "edit"); + }; + + const handlePointChange = (index: number, axis: 0 | 1, value: number) => { + const newPoints = [...points.map((p) => [...p] as [number, number])]; + newPoints[index]![axis] = value; + updateNode(siteNode.id, { + polygon: { type: "polygon" as const, points: newPoints }, + }); + }; + + const handleAddPoint = () => { + const lastPoint = points[points.length - 1]; + const firstPoint = points[0]; + if (!lastPoint || !firstPoint) return; + + const newPoint: [number, number] = [ + (lastPoint[0] + firstPoint[0]) / 2, + (lastPoint[1] + firstPoint[1]) / 2, + ]; + const newPoints = [...points, newPoint]; + updateNode(siteNode.id, { + polygon: { type: "polygon" as const, points: newPoints }, + }); + }; + + const handleDeletePoint = (index: number) => { + if (points.length <= 3) return; + const newPoints = points.filter((_, i) => i !== index); + updateNode(siteNode.id, { + polygon: { type: "polygon" as const, points: newPoints }, + }); + }; + + return ( +
+ {/* Header */} +
+
+ + Property Line +
+ +
+ + {/* Measurements */} +
+
+ Area: {area.toFixed(1)} m² +
+
+ Perimeter:{" "} + {perimeter.toFixed(1)} m +
+
+ + {/* Vertex list (shown when editing) */} + {isEditing && ( +
+
+ {points.map((point, index) => ( +
+ + {index + 1} + + + + handlePointChange(index, 0, parseFloat(e.target.value) || 0) + } + step={0.5} + className="w-16 bg-accent/50 rounded px-1.5 py-0.5 text-xs text-foreground border border-border/50 focus:outline-none focus:border-primary" + /> + + + handlePointChange(index, 1, parseFloat(e.target.value) || 0) + } + step={0.5} + className="w-16 bg-accent/50 rounded px-1.5 py-0.5 text-xs text-foreground border border-border/50 focus:outline-none focus:border-primary" + /> + +
+ ))} +
+ +
+ )} +
+ ); +} + +// ============================================================================ +// SITE PHASE VIEW - Property line + building buttons // ============================================================================ function SitePhaseView() { @@ -55,31 +237,32 @@ function SitePhaseView() { .map((child) => typeof child === 'string' ? nodes[child] : child) .filter((node): node is BuildingNode => node?.type === "building"); - if (buildings.length === 0) { - return ( -
- No buildings yet -
- ); - } - return ( -
- {buildings.map((building) => ( - - ))} +
+ + {buildings.length === 0 ? ( +
+ No buildings yet +
+ ) : ( +
+ {buildings.map((building) => ( + + ))} +
+ )}
); } @@ -367,18 +550,6 @@ 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 [cameraPopoverOpen, setCameraPopoverOpen] = useState(false); diff --git a/packages/viewer/src/components/renderers/site/site-renderer.tsx b/packages/viewer/src/components/renderers/site/site-renderer.tsx index 58e3d222..ce50a4bc 100644 --- a/packages/viewer/src/components/renderers/site/site-renderer.tsx +++ b/packages/viewer/src/components/renderers/site/site-renderer.tsx @@ -1,4 +1,5 @@ import { type SiteNode, useRegistry } from '@pascal-app/core' +import { Html } from '@react-three/drei' import { useMemo, useRef } from 'react' import { BufferGeometry, DoubleSide, Float32BufferAttribute, type Group, Shape } from 'three' import { useNodeEvents } from '../../../hooks/use-node-events' @@ -60,6 +61,19 @@ export const SiteRenderer = ({ node }: { node: SiteNode }) => { return createBoundaryLineGeometry(node.polygon.points) }, [node?.polygon?.points]) + // Edge distances for labels + const edges = useMemo(() => { + const polygon = node?.polygon?.points ?? [] + if (polygon.length < 2) return [] + return polygon.map(([x1, z1], i) => { + const [x2, z2] = polygon[(i + 1) % polygon.length]! + const midX = (x1! + x2) / 2 + const midZ = (z1! + z2) / 2 + const dist = Math.sqrt((x2 - x1!) ** 2 + (z2 - z1!) ** 2) + return { midX, midZ, dist } + }) + }, [node?.polygon?.points]) + const handlers = useNodeEvents(node, 'site') if (!node || !floorShape || !lineGeometry) { @@ -98,6 +112,21 @@ export const SiteRenderer = ({ node }: { node: SiteNode }) => { opacity={0.6} /> + + {/* Edge distance labels */} + {edges.map((edge, i) => ( + +
+ {edge.dist.toFixed(2)}m +
+ + ))} ) } diff --git a/packages/viewer/src/components/renderers/zone/zone-renderer.tsx b/packages/viewer/src/components/renderers/zone/zone-renderer.tsx index a8d89bcd..7fe2496a 100644 --- a/packages/viewer/src/components/renderers/zone/zone-renderer.tsx +++ b/packages/viewer/src/components/renderers/zone/zone-renderer.tsx @@ -180,7 +180,8 @@ export const ZoneRenderer = ({ node }: { node: ZoneNode }) => { + }} + zIndexRange={[10, 0]}>