diff --git a/apps/editor/components/tools/shared/polygon-editor.tsx b/apps/editor/components/tools/shared/polygon-editor.tsx index 670404c8..73c06b68 100644 --- a/apps/editor/components/tools/shared/polygon-editor.tsx +++ b/apps/editor/components/tools/shared/polygon-editor.tsx @@ -1,4 +1,5 @@ -import { useThree } from '@react-three/fiber' +import { createPortal, useThree } from '@react-three/fiber' +import { sceneRegistry } from '@pascal-app/core' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { BufferGeometry, @@ -24,7 +25,8 @@ export interface PolygonEditorProps { color?: string onPolygonChange: (polygon: Array<[number, number]>) => void minVertices?: number - levelY?: number + /** Level ID to mount the editor to. If provided, uses createPortal for automatic level animation following. */ + levelId?: string /** Height of the surface being edited (e.g. slab elevation). Handles adapt to this. */ surfaceHeight?: number } @@ -40,13 +42,17 @@ export const PolygonEditor: React.FC = ({ color = '#3b82f6', onPolygonChange, minVertices = 3, - levelY = 0, + levelId, surfaceHeight = 0, }) => { const { gl, camera } = useThree() - // Compute the editing plane height (level Y + small offset above floor) - const editY = levelY + Y_OFFSET + // Get level node from registry if levelId is provided + const levelNode = levelId ? sceneRegistry.nodes.get(levelId) : null + + // When using portal, edit at Y_OFFSET (local to level) + // When not using portal, edit at world origin + const editY = levelNode ? Y_OFFSET : 0 // Local state for dragging const [dragState, setDragState] = useState(null) @@ -222,7 +228,7 @@ export const PolygonEditor: React.FC = ({ const canDelete = displayPolygon.length > minVertices - return ( + const editorContent = ( {/* Border line */} {/* @ts-ignore */} @@ -337,4 +343,7 @@ export const PolygonEditor: React.FC = ({ })} ) + + // Mount to level node if available, otherwise render at world origin + return levelNode ? createPortal(editorContent, levelNode) : editorContent } diff --git a/apps/editor/components/tools/slab/slab-boundary-editor.tsx b/apps/editor/components/tools/slab/slab-boundary-editor.tsx index 24f0e3e5..2fdd7f02 100644 --- a/apps/editor/components/tools/slab/slab-boundary-editor.tsx +++ b/apps/editor/components/tools/slab/slab-boundary-editor.tsx @@ -1,52 +1,30 @@ -import { sceneRegistry, useScene, type AnyNodeId, type SlabNode } from '@pascal-app/core' +import { resolveLevelId, type SlabNode, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { useCallback } from 'react' import { PolygonEditor } from '../shared/polygon-editor' +interface SlabBoundaryEditorProps { + slabId: SlabNode['id'] +} + /** - * Slab boundary editor - allows editing slab polygon vertices when a slab is selected + * Slab boundary editor - allows editing slab polygon vertices for a specific slab * Uses the generic PolygonEditor component */ -export const SlabBoundaryEditor: React.FC = () => { - const selectedIds = useViewer((state) => state.selection.selectedIds) - const levelId = useViewer((state) => state.selection.levelId) - const setSelection = useViewer((state) => state.setSelection) - const nodes = useScene((state) => state.nodes) +export const SlabBoundaryEditor: React.FC = ({ slabId }) => { + const slabNode = useScene((state) => state.nodes[slabId]) const updateNode = useScene((state) => state.updateNode) + const setSelection = useViewer((state) => state.setSelection) - // Find the first selected slab - const selectedSlabId = - selectedIds.find((id) => nodes[id as AnyNodeId]?.type === 'slab') ?? null - const slab = selectedSlabId ? (nodes[selectedSlabId as AnyNodeId] as SlabNode) : null - - // Get level Y position for the editing plane - let levelY = 0 - if (levelId) { - const levelMesh = sceneRegistry.nodes.get(levelId) - if (levelMesh) { - levelY = levelMesh.position.y - } else { - const levelNode = nodes[levelId] - if (levelNode && 'level' in levelNode) { - const levelMode = useViewer.getState().levelMode - const LEVEL_HEIGHT = 2.5 - const EXPLODED_GAP = 5 - levelY = - ((levelNode as any).level || 0) * - (LEVEL_HEIGHT + (levelMode === 'exploded' ? EXPLODED_GAP : 0)) - } - } - } + const slab = slabNode?.type === 'slab' ? (slabNode as SlabNode) : null const handlePolygonChange = useCallback( (newPolygon: Array<[number, number]>) => { - if (selectedSlabId) { - updateNode(selectedSlabId as SlabNode['id'], { polygon: newPolygon }) - // Re-assert selection so the slab stays selected after the edit - setSelection({ selectedIds: [selectedSlabId] }) - } + updateNode(slabId, { polygon: newPolygon }) + // Re-assert selection so the slab stays selected after the edit + setSelection({ selectedIds: [slabId] }) }, - [selectedSlabId, updateNode, setSelection], + [slabId, updateNode, setSelection], ) if (!slab || !slab.polygon || slab.polygon.length < 3) return null @@ -57,7 +35,7 @@ export const SlabBoundaryEditor: React.FC = () => { color="#a3a3a3" onPolygonChange={handlePolygonChange} minVertices={3} - levelY={levelY} + levelId={resolveLevelId(slab, useScene.getState().nodes)} surfaceHeight={slab.elevation ?? 0.05} /> ) diff --git a/apps/editor/components/tools/tool-manager.tsx b/apps/editor/components/tools/tool-manager.tsx index 43ea883b..1cb381d8 100644 --- a/apps/editor/components/tools/tool-manager.tsx +++ b/apps/editor/components/tools/tool-manager.tsx @@ -1,5 +1,5 @@ import useEditor, { type Phase, type Tool } from "@/store/use-editor"; -import { useScene, type AnyNodeId } from "@pascal-app/core"; +import { useScene, type AnyNodeId, type SlabNode } from "@pascal-app/core"; import { useViewer } from "@pascal-app/viewer"; import { CeilingTool } from "./ceiling/ceiling-tool"; import { ItemTool } from "./item/item-tool"; @@ -39,14 +39,14 @@ export const ToolManager: React.FC = () => { const nodes = useScene((state) => state.nodes); // Check if a slab is selected - const selectedSlabId = selectedIds.find((id) => nodes[id as AnyNodeId]?.type === "slab") ?? null; + const selectedSlabId = selectedIds.find((id) => nodes[id as AnyNodeId]?.type === "slab") as SlabNode['id'] | undefined; // Show site boundary editor when in site phase and edit mode const showSiteBoundaryEditor = phase === "site" && mode === "edit"; // Show slab boundary editor when in structure/select mode with a slab selected const showSlabBoundaryEditor = - phase === "structure" && mode === "select" && selectedSlabId !== null; + phase === "structure" && mode === "select" && selectedSlabId !== undefined; // Show zone boundary editor when in structure/select mode with a zone selected // Hide when editing a slab to avoid overlapping handles @@ -61,8 +61,8 @@ export const ToolManager: React.FC = () => { return ( <> {showSiteBoundaryEditor && } - {showZoneBoundaryEditor && } - {showSlabBoundaryEditor && } + {showZoneBoundaryEditor && selectedZoneId && } + {showSlabBoundaryEditor && selectedSlabId && } {movingNode && } {!movingNode && BuildToolComponent && } diff --git a/apps/editor/components/tools/zone/zone-boundary-editor.tsx b/apps/editor/components/tools/zone/zone-boundary-editor.tsx index 2e66eef9..9dc26d9a 100644 --- a/apps/editor/components/tools/zone/zone-boundary-editor.tsx +++ b/apps/editor/components/tools/zone/zone-boundary-editor.tsx @@ -1,25 +1,26 @@ -import { useScene, type ZoneNode } from '@pascal-app/core' -import { useViewer } from '@pascal-app/viewer' +import { resolveLevelId, useScene, type ZoneNode } from '@pascal-app/core' import { useCallback } from 'react' import { PolygonEditor } from '../shared/polygon-editor' +interface ZoneBoundaryEditorProps { + zoneId: ZoneNode['id'] +} + /** - * Zone boundary editor - allows editing zone polygon vertices when a zone is selected + * Zone boundary editor - allows editing zone polygon vertices for a specific zone * Uses the generic PolygonEditor component */ -export const ZoneBoundaryEditor: React.FC = () => { - const selectedZoneId = useViewer((state) => state.selection.zoneId) - const zoneNode = useScene((state) => (selectedZoneId ? state.nodes[selectedZoneId] : null)) - const zone = zoneNode?.type === 'zone' ? (zoneNode as ZoneNode) : null +export const ZoneBoundaryEditor: React.FC = ({ zoneId }) => { + const zoneNode = useScene((state) => state.nodes[zoneId]) const updateNode = useScene((state) => state.updateNode) + const zone = zoneNode?.type === 'zone' ? (zoneNode as ZoneNode) : null + const handlePolygonChange = useCallback( (newPolygon: Array<[number, number]>) => { - if (selectedZoneId) { - updateNode(selectedZoneId, { polygon: newPolygon }) - } + updateNode(zoneId, { polygon: newPolygon }) }, - [selectedZoneId, updateNode], + [zoneId, updateNode], ) if (!zone || !zone.polygon || zone.polygon.length < 3) return null @@ -32,6 +33,7 @@ export const ZoneBoundaryEditor: React.FC = () => { color={zoneColor} onPolygonChange={handlePolygonChange} minVertices={3} + levelId={resolveLevelId(zone, useScene.getState().nodes)} /> ) }