simplify poligon editor / boundaries editor

This commit is contained in:
wass08
2026-02-09 12:48:35 +09:00
parent 81f132611e
commit f2c1da1655
4 changed files with 48 additions and 59 deletions
@@ -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)}
/> />
) )
} }