simplify poligon editor / boundaries editor
This commit is contained in:
@@ -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 { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
BufferGeometry,
|
BufferGeometry,
|
||||||
@@ -24,7 +25,8 @@ export interface PolygonEditorProps {
|
|||||||
color?: string
|
color?: string
|
||||||
onPolygonChange: (polygon: Array<[number, number]>) => void
|
onPolygonChange: (polygon: Array<[number, number]>) => void
|
||||||
minVertices?: number
|
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. */
|
/** Height of the surface being edited (e.g. slab elevation). Handles adapt to this. */
|
||||||
surfaceHeight?: number
|
surfaceHeight?: number
|
||||||
}
|
}
|
||||||
@@ -40,13 +42,17 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|||||||
color = '#3b82f6',
|
color = '#3b82f6',
|
||||||
onPolygonChange,
|
onPolygonChange,
|
||||||
minVertices = 3,
|
minVertices = 3,
|
||||||
levelY = 0,
|
levelId,
|
||||||
surfaceHeight = 0,
|
surfaceHeight = 0,
|
||||||
}) => {
|
}) => {
|
||||||
const { gl, camera } = useThree()
|
const { gl, camera } = useThree()
|
||||||
|
|
||||||
// Compute the editing plane height (level Y + small offset above floor)
|
// Get level node from registry if levelId is provided
|
||||||
const editY = levelY + Y_OFFSET
|
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
|
// Local state for dragging
|
||||||
const [dragState, setDragState] = useState<DragState | null>(null)
|
const [dragState, setDragState] = useState<DragState | null>(null)
|
||||||
@@ -222,7 +228,7 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|||||||
|
|
||||||
const canDelete = displayPolygon.length > minVertices
|
const canDelete = displayPolygon.length > minVertices
|
||||||
|
|
||||||
return (
|
const editorContent = (
|
||||||
<group>
|
<group>
|
||||||
{/* Border line */}
|
{/* Border line */}
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
@@ -337,4 +343,7 @@ export const PolygonEditor: React.FC<PolygonEditorProps> = ({
|
|||||||
})}
|
})}
|
||||||
</group>
|
</group>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Mount to level node if available, otherwise render at world origin
|
||||||
|
return levelNode ? createPortal(editorContent, levelNode) : editorContent
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { useViewer } from '@pascal-app/viewer'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { PolygonEditor } from '../shared/polygon-editor'
|
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
|
* Uses the generic PolygonEditor component
|
||||||
*/
|
*/
|
||||||
export const SlabBoundaryEditor: React.FC = () => {
|
export const SlabBoundaryEditor: React.FC<SlabBoundaryEditorProps> = ({ slabId }) => {
|
||||||
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
const slabNode = useScene((state) => state.nodes[slabId])
|
||||||
const levelId = useViewer((state) => state.selection.levelId)
|
|
||||||
const setSelection = useViewer((state) => state.setSelection)
|
|
||||||
const nodes = useScene((state) => state.nodes)
|
|
||||||
const updateNode = useScene((state) => state.updateNode)
|
const updateNode = useScene((state) => state.updateNode)
|
||||||
|
const setSelection = useViewer((state) => state.setSelection)
|
||||||
|
|
||||||
// Find the first selected slab
|
const slab = slabNode?.type === 'slab' ? (slabNode as SlabNode) : null
|
||||||
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 handlePolygonChange = useCallback(
|
const handlePolygonChange = useCallback(
|
||||||
(newPolygon: Array<[number, number]>) => {
|
(newPolygon: Array<[number, number]>) => {
|
||||||
if (selectedSlabId) {
|
updateNode(slabId, { polygon: newPolygon })
|
||||||
updateNode(selectedSlabId as SlabNode['id'], { polygon: newPolygon })
|
// Re-assert selection so the slab stays selected after the edit
|
||||||
// Re-assert selection so the slab stays selected after the edit
|
setSelection({ selectedIds: [slabId] })
|
||||||
setSelection({ selectedIds: [selectedSlabId] })
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[selectedSlabId, updateNode, setSelection],
|
[slabId, updateNode, setSelection],
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!slab || !slab.polygon || slab.polygon.length < 3) return null
|
if (!slab || !slab.polygon || slab.polygon.length < 3) return null
|
||||||
@@ -57,7 +35,7 @@ export const SlabBoundaryEditor: React.FC = () => {
|
|||||||
color="#a3a3a3"
|
color="#a3a3a3"
|
||||||
onPolygonChange={handlePolygonChange}
|
onPolygonChange={handlePolygonChange}
|
||||||
minVertices={3}
|
minVertices={3}
|
||||||
levelY={levelY}
|
levelId={resolveLevelId(slab, useScene.getState().nodes)}
|
||||||
surfaceHeight={slab.elevation ?? 0.05}
|
surfaceHeight={slab.elevation ?? 0.05}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import useEditor, { type Phase, type Tool } from "@/store/use-editor";
|
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 { useViewer } from "@pascal-app/viewer";
|
||||||
import { CeilingTool } from "./ceiling/ceiling-tool";
|
import { CeilingTool } from "./ceiling/ceiling-tool";
|
||||||
import { ItemTool } from "./item/item-tool";
|
import { ItemTool } from "./item/item-tool";
|
||||||
@@ -39,14 +39,14 @@ export const ToolManager: React.FC = () => {
|
|||||||
const nodes = useScene((state) => state.nodes);
|
const nodes = useScene((state) => state.nodes);
|
||||||
|
|
||||||
// Check if a slab is selected
|
// 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
|
// Show site boundary editor when in site phase and edit mode
|
||||||
const showSiteBoundaryEditor = phase === "site" && mode === "edit";
|
const showSiteBoundaryEditor = phase === "site" && mode === "edit";
|
||||||
|
|
||||||
// Show slab boundary editor when in structure/select mode with a slab selected
|
// Show slab boundary editor when in structure/select mode with a slab selected
|
||||||
const showSlabBoundaryEditor =
|
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
|
// Show zone boundary editor when in structure/select mode with a zone selected
|
||||||
// Hide when editing a slab to avoid overlapping handles
|
// Hide when editing a slab to avoid overlapping handles
|
||||||
@@ -61,8 +61,8 @@ export const ToolManager: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showSiteBoundaryEditor && <SiteBoundaryEditor />}
|
{showSiteBoundaryEditor && <SiteBoundaryEditor />}
|
||||||
{showZoneBoundaryEditor && <ZoneBoundaryEditor />}
|
{showZoneBoundaryEditor && selectedZoneId && <ZoneBoundaryEditor zoneId={selectedZoneId} />}
|
||||||
{showSlabBoundaryEditor && <SlabBoundaryEditor />}
|
{showSlabBoundaryEditor && selectedSlabId && <SlabBoundaryEditor slabId={selectedSlabId} />}
|
||||||
{movingNode && <MoveTool />}
|
{movingNode && <MoveTool />}
|
||||||
{!movingNode && BuildToolComponent && <BuildToolComponent />}
|
{!movingNode && BuildToolComponent && <BuildToolComponent />}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
import { useScene, type ZoneNode } from '@pascal-app/core'
|
import { resolveLevelId, useScene, type ZoneNode } from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { PolygonEditor } from '../shared/polygon-editor'
|
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
|
* Uses the generic PolygonEditor component
|
||||||
*/
|
*/
|
||||||
export const ZoneBoundaryEditor: React.FC = () => {
|
export const ZoneBoundaryEditor: React.FC<ZoneBoundaryEditorProps> = ({ zoneId }) => {
|
||||||
const selectedZoneId = useViewer((state) => state.selection.zoneId)
|
const zoneNode = useScene((state) => state.nodes[zoneId])
|
||||||
const zoneNode = useScene((state) => (selectedZoneId ? state.nodes[selectedZoneId] : null))
|
|
||||||
const zone = zoneNode?.type === 'zone' ? (zoneNode as ZoneNode) : null
|
|
||||||
const updateNode = useScene((state) => state.updateNode)
|
const updateNode = useScene((state) => state.updateNode)
|
||||||
|
|
||||||
|
const zone = zoneNode?.type === 'zone' ? (zoneNode as ZoneNode) : null
|
||||||
|
|
||||||
const handlePolygonChange = useCallback(
|
const handlePolygonChange = useCallback(
|
||||||
(newPolygon: Array<[number, number]>) => {
|
(newPolygon: Array<[number, number]>) => {
|
||||||
if (selectedZoneId) {
|
updateNode(zoneId, { polygon: newPolygon })
|
||||||
updateNode(selectedZoneId, { polygon: newPolygon })
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[selectedZoneId, updateNode],
|
[zoneId, updateNode],
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!zone || !zone.polygon || zone.polygon.length < 3) return null
|
if (!zone || !zone.polygon || zone.polygon.length < 3) return null
|
||||||
@@ -32,6 +33,7 @@ export const ZoneBoundaryEditor: React.FC = () => {
|
|||||||
color={zoneColor}
|
color={zoneColor}
|
||||||
onPolygonChange={handlePolygonChange}
|
onPolygonChange={handlePolygonChange}
|
||||||
minVertices={3}
|
minVertices={3}
|
||||||
|
levelId={resolveLevelId(zone, useScene.getState().nodes)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user