fix drag zone issue

This commit is contained in:
wass08
2026-01-29 13:25:16 +09:00
parent 24a94c8ac1
commit 9501971c80
3 changed files with 140 additions and 136 deletions
@@ -1,6 +1,7 @@
import { useScene, type ZoneNode } from "@pascal-app/core"; import { useScene, type ZoneNode } from '@pascal-app/core'
import { useViewer } from "@pascal-app/viewer"; import { useViewer } from '@pascal-app/viewer'
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useThree } from '@react-three/fiber'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { import {
BufferGeometry, BufferGeometry,
Float32BufferAttribute, Float32BufferAttribute,
@@ -9,175 +10,180 @@ import {
Raycaster, Raycaster,
Vector2, Vector2,
Vector3, Vector3,
} from "three"; } from 'three'
import { useThree } from "@react-three/fiber";
const Y_OFFSET = 0.02; const Y_OFFSET = 0.02
type DragState = { type DragState = {
isDragging: boolean; isDragging: boolean
vertexIndex: number; vertexIndex: number
initialPosition: [number, number]; initialPosition: [number, number]
}; pointerId: number
}
/** /**
* Zone boundary editor - allows editing zone polygon vertices when a zone is selected * Zone boundary editor - allows editing zone polygon vertices when a zone is selected
* Uses the event emitter system for grid interactions * Uses the event emitter system for grid interactions
*/ */
export const ZoneBoundaryEditor: React.FC = () => { export const ZoneBoundaryEditor: React.FC = () => {
const { gl, camera } = useThree(); const { gl, camera } = useThree()
const selectedZoneId = useViewer((state) => state.selection.zoneId); const selectedZoneId = useViewer((state) => state.selection.zoneId)
const zoneNode = useScene((state) => const zoneNode = useScene((state) => (selectedZoneId ? state.nodes[selectedZoneId] : null))
selectedZoneId ? state.nodes[selectedZoneId] : null const zone = zoneNode?.type === 'zone' ? (zoneNode as ZoneNode) : null
); const updateNode = useScene((state) => state.updateNode)
const zone = zoneNode?.type === "zone" ? (zoneNode as ZoneNode) : null;
const updateNode = useScene((state) => state.updateNode);
// Local state for dragging // Local state for dragging
const [dragState, setDragState] = useState<DragState | null>(null); const [dragState, setDragState] = useState<DragState | null>(null)
const [previewPolygon, setPreviewPolygon] = useState< const [previewPolygon, setPreviewPolygon] = useState<Array<[number, number]> | null>(null)
Array<[number, number]> | null const [hoveredVertex, setHoveredVertex] = useState<number | null>(null)
>(null); const [hoveredMidpoint, setHoveredMidpoint] = useState<number | null>(null)
const [hoveredVertex, setHoveredVertex] = useState<number | null>(null);
const [hoveredMidpoint, setHoveredMidpoint] = useState<number | null>(null);
// Refs for raycasting during drag // Refs for raycasting during drag
const dragPlane = useRef(new Plane(new Vector3(0, 1, 0), -Y_OFFSET)); const dragPlane = useRef(new Plane(new Vector3(0, 1, 0), -Y_OFFSET))
const raycaster = useRef(new Raycaster()); const raycaster = useRef(new Raycaster())
const lineRef = useRef<Mesh>(null!); const lineRef = useRef<Mesh>(null!)
// The polygon to display (preview during drag, or actual zone polygon) // The polygon to display (preview during drag, or actual zone polygon)
const displayPolygon = previewPolygon ?? zone?.polygon ?? []; const displayPolygon = previewPolygon ?? zone?.polygon ?? []
// Calculate midpoints for adding new vertices // Calculate midpoints for adding new vertices
const midpoints = useMemo(() => { const midpoints = useMemo(() => {
if (displayPolygon.length < 2) return []; if (displayPolygon.length < 2) return []
return displayPolygon.map(([x1, z1], index) => { return displayPolygon.map(([x1, z1], index) => {
const nextIndex = (index + 1) % displayPolygon.length; const nextIndex = (index + 1) % displayPolygon.length
const [x2, z2] = displayPolygon[nextIndex]!; const [x2, z2] = displayPolygon[nextIndex]!
return [(x1! + x2) / 2, (z1! + z2) / 2] as [number, number]; return [(x1! + x2) / 2, (z1! + z2) / 2] as [number, number]
}); })
}, [displayPolygon]); }, [displayPolygon])
// Handle vertex drag // Handle vertex drag
const handleVertexDrag = useCallback( const handleVertexDrag = useCallback(
(clientX: number, clientY: number, vertexIndex: number) => { (clientX: number, clientY: number, vertexIndex: number) => {
if (!zone) return; if (!zone) return
const canvas = gl.domElement; const canvas = gl.domElement
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect()
const x = ((clientX - rect.left) / rect.width) * 2 - 1; const x = ((clientX - rect.left) / rect.width) * 2 - 1
const y = -((clientY - rect.top) / rect.height) * 2 + 1; const y = -((clientY - rect.top) / rect.height) * 2 + 1
raycaster.current.setFromCamera(new Vector2(x, y), camera); raycaster.current.setFromCamera(new Vector2(x, y), camera)
const intersection = new Vector3(); const intersection = new Vector3()
raycaster.current.ray.intersectPlane(dragPlane.current, intersection); raycaster.current.ray.intersectPlane(dragPlane.current, intersection)
if (intersection) { if (intersection) {
// Snap to 0.5 grid // Snap to 0.5 grid
const gridX = Math.round(intersection.x * 2) / 2; const gridX = Math.round(intersection.x * 2) / 2
const gridZ = Math.round(intersection.z * 2) / 2; const gridZ = Math.round(intersection.z * 2) / 2
const basePolygon = previewPolygon ?? zone.polygon; const basePolygon = previewPolygon ?? zone.polygon
const newPolygon = [...basePolygon]; const newPolygon = [...basePolygon]
newPolygon[vertexIndex] = [gridX, gridZ]; newPolygon[vertexIndex] = [gridX, gridZ]
setPreviewPolygon(newPolygon); setPreviewPolygon(newPolygon)
} }
}, },
[zone, gl, camera, previewPolygon] [zone, gl, camera, previewPolygon],
); )
// Commit polygon changes // Commit polygon changes
const commitPolygonChange = useCallback(() => { const commitPolygonChange = useCallback(() => {
if (previewPolygon && selectedZoneId) { if (previewPolygon && selectedZoneId) {
updateNode(selectedZoneId, { polygon: previewPolygon }); updateNode(selectedZoneId, { polygon: previewPolygon })
} }
setPreviewPolygon(null); setPreviewPolygon(null)
setDragState(null); setDragState(null)
}, [previewPolygon, selectedZoneId, updateNode]); }, [previewPolygon, selectedZoneId, updateNode])
// Handle adding a new vertex at midpoint // Handle adding a new vertex at midpoint
const handleAddVertex = useCallback( const handleAddVertex = useCallback(
(afterIndex: number, position: [number, number]) => { (afterIndex: number, position: [number, number]) => {
if (!zone) return -1; if (!zone) return -1
const basePolygon = previewPolygon ?? zone.polygon; const basePolygon = previewPolygon ?? zone.polygon
const newPolygon = [ const newPolygon = [
...basePolygon.slice(0, afterIndex + 1), ...basePolygon.slice(0, afterIndex + 1),
position, position,
...basePolygon.slice(afterIndex + 1), ...basePolygon.slice(afterIndex + 1),
]; ]
setPreviewPolygon(newPolygon); setPreviewPolygon(newPolygon)
return afterIndex + 1; // Return new vertex index return afterIndex + 1 // Return new vertex index
}, },
[zone, previewPolygon] [zone, previewPolygon],
); )
// Handle deleting a vertex // Handle deleting a vertex
const handleDeleteVertex = useCallback( const handleDeleteVertex = useCallback(
(index: number) => { (index: number) => {
if (!zone || !selectedZoneId) return; if (!zone || !selectedZoneId) return
const basePolygon = previewPolygon ?? zone.polygon; const basePolygon = previewPolygon ?? zone.polygon
if (basePolygon.length <= 3) return; // Need at least 3 points if (basePolygon.length <= 3) return // Need at least 3 points
const newPolygon = basePolygon.filter((_, i) => i !== index); const newPolygon = basePolygon.filter((_, i) => i !== index)
updateNode(selectedZoneId, { polygon: newPolygon }); updateNode(selectedZoneId, { polygon: newPolygon })
setPreviewPolygon(null); setPreviewPolygon(null)
}, },
[zone, selectedZoneId, previewPolygon, updateNode] [zone, selectedZoneId, previewPolygon, updateNode],
); )
// Set up pointer move/up listeners for dragging // Set up pointer move/up listeners for dragging with pointer capture
useEffect(() => { useEffect(() => {
if (!dragState?.isDragging) return; if (!dragState?.isDragging) return
const canvas = gl.domElement; const canvas = gl.domElement
const pointerId = dragState.pointerId
// Capture pointer to prevent R3F events from firing on other objects (like the grid)
canvas.setPointerCapture(pointerId)
const handlePointerMove = (e: PointerEvent) => { const handlePointerMove = (e: PointerEvent) => {
handleVertexDrag(e.clientX, e.clientY, dragState.vertexIndex); handleVertexDrag(e.clientX, e.clientY, dragState.vertexIndex)
}; }
const handlePointerUp = () => { const handlePointerUp = (e: PointerEvent) => {
commitPolygonChange(); // Release pointer capture
}; if (canvas.hasPointerCapture(e.pointerId)) {
canvas.releasePointerCapture(e.pointerId)
}
commitPolygonChange()
}
canvas.addEventListener("pointermove", handlePointerMove); canvas.addEventListener('pointermove', handlePointerMove)
canvas.addEventListener("pointerup", handlePointerUp); canvas.addEventListener('pointerup', handlePointerUp)
return () => { return () => {
canvas.removeEventListener("pointermove", handlePointerMove); // Release capture on cleanup
canvas.removeEventListener("pointerup", handlePointerUp); if (canvas.hasPointerCapture(pointerId)) {
}; canvas.releasePointerCapture(pointerId)
}, [dragState, gl, handleVertexDrag, commitPolygonChange]); }
canvas.removeEventListener('pointermove', handlePointerMove)
canvas.removeEventListener('pointerup', handlePointerUp)
}
}, [dragState, gl, handleVertexDrag, commitPolygonChange])
// Update line geometry when polygon changes // Update line geometry when polygon changes
useEffect(() => { useEffect(() => {
if (!lineRef.current || displayPolygon.length < 2) return; if (!lineRef.current || displayPolygon.length < 2) return
const positions: number[] = []; const positions: number[] = []
for (const [x, z] of displayPolygon) { for (const [x, z] of displayPolygon) {
positions.push(x!, Y_OFFSET + 0.01, z!); positions.push(x!, Y_OFFSET + 0.01, z!)
} }
// Close the loop // Close the loop
const first = displayPolygon[0]!; const first = displayPolygon[0]!
positions.push(first[0]!, Y_OFFSET + 0.01, first[1]!); positions.push(first[0]!, Y_OFFSET + 0.01, first[1]!)
const geometry = new BufferGeometry(); const geometry = new BufferGeometry()
geometry.setAttribute( geometry.setAttribute('position', new Float32BufferAttribute(positions, 3))
"position",
new Float32BufferAttribute(positions, 3)
);
lineRef.current.geometry.dispose(); lineRef.current.geometry.dispose()
lineRef.current.geometry = geometry; lineRef.current.geometry = geometry
}, [displayPolygon]); }, [displayPolygon])
if (!zone || displayPolygon.length < 3) return null; if (!zone || displayPolygon.length < 3) return null
const canDelete = displayPolygon.length > 3; const canDelete = displayPolygon.length > 3
const zoneColor = zone.color || "#3b82f6"; const zoneColor = zone.color || '#3b82f6'
return ( return (
<group> <group>
@@ -197,97 +203,97 @@ export const ZoneBoundaryEditor: React.FC = () => {
{/* Vertex handles */} {/* Vertex handles */}
{displayPolygon.map(([x, z], index) => { {displayPolygon.map(([x, z], index) => {
const isHovered = hoveredVertex === index; const isHovered = hoveredVertex === index
const isDragging = dragState?.vertexIndex === index; const isDragging = dragState?.vertexIndex === index
return ( return (
<mesh <mesh
key={`vertex-${index}`} key={`vertex-${index}`}
position={[x!, Y_OFFSET, z!]} position={[x!, Y_OFFSET, z!]}
onPointerEnter={(e) => { onPointerEnter={(e) => {
e.stopPropagation(); e.stopPropagation()
setHoveredVertex(index); setHoveredVertex(index)
}} }}
onPointerLeave={(e) => { onPointerLeave={(e) => {
e.stopPropagation(); e.stopPropagation()
setHoveredVertex(null); setHoveredVertex(null)
}} }}
onPointerDown={(e) => { onPointerDown={(e) => {
e.stopPropagation(); e.stopPropagation()
setDragState({ setDragState({
isDragging: true, isDragging: true,
vertexIndex: index, vertexIndex: index,
initialPosition: [x!, z!], initialPosition: [x!, z!],
}); pointerId: e.nativeEvent.pointerId,
})
}}
onClick={(e) => {
e.stopPropagation()
}} }}
onDoubleClick={(e) => { onDoubleClick={(e) => {
e.stopPropagation(); e.stopPropagation()
if (canDelete) { if (canDelete) {
handleDeleteVertex(index); handleDeleteVertex(index)
} }
}} }}
> >
<sphereGeometry <sphereGeometry args={[isHovered || isDragging ? 0.2 : 0.15, 16, 16]} />
args={[isHovered || isDragging ? 0.2 : 0.15, 16, 16]}
/>
<meshBasicMaterial <meshBasicMaterial
color={ color={
isDragging isDragging ? '#22c55e' : isHovered ? (canDelete ? '#ef4444' : '#ffffff') : zoneColor
? "#22c55e"
: isHovered
? canDelete
? "#ef4444"
: "#ffffff"
: zoneColor
} }
depthTest={false} depthTest={false}
depthWrite={false} depthWrite={false}
/> />
</mesh> </mesh>
); )
})} })}
{/* Midpoint handles for adding vertices (hidden while dragging) */} {/* Midpoint handles for adding vertices (hidden while dragging) */}
{!dragState && {!dragState &&
midpoints.map(([x, z], index) => { midpoints.map(([x, z], index) => {
const isHovered = hoveredMidpoint === index; const isHovered = hoveredMidpoint === index
return ( return (
<mesh <mesh
key={`midpoint-${index}`} key={`midpoint-${index}`}
position={[x!, Y_OFFSET, z!]} position={[x!, Y_OFFSET, z!]}
onPointerEnter={(e) => { onPointerEnter={(e) => {
e.stopPropagation(); e.stopPropagation()
setHoveredMidpoint(index); setHoveredMidpoint(index)
}} }}
onPointerLeave={(e) => { onPointerLeave={(e) => {
e.stopPropagation(); e.stopPropagation()
setHoveredMidpoint(null); setHoveredMidpoint(null)
}} }}
onPointerDown={(e) => { onPointerDown={(e) => {
e.stopPropagation(); e.stopPropagation()
const newVertexIndex = handleAddVertex(index, [x!, z!]); const newVertexIndex = handleAddVertex(index, [x!, z!])
if (newVertexIndex >= 0) { if (newVertexIndex >= 0) {
setDragState({ setDragState({
isDragging: true, isDragging: true,
vertexIndex: newVertexIndex, vertexIndex: newVertexIndex,
initialPosition: [x!, z!], initialPosition: [x!, z!],
}); pointerId: e.nativeEvent.pointerId,
setHoveredMidpoint(null); })
setHoveredMidpoint(null)
} }
}} }}
onClick={(e) => {
e.stopPropagation()
}}
> >
<sphereGeometry args={[isHovered ? 0.12 : 0.08, 16, 16]} /> <sphereGeometry args={[isHovered ? 0.12 : 0.08, 16, 16]} />
<meshBasicMaterial <meshBasicMaterial
color={isHovered ? "#22c55e" : zoneColor} color={isHovered ? '#22c55e' : zoneColor}
depthTest={false} depthTest={false}
depthWrite={false} depthWrite={false}
transparent transparent
opacity={isHovered ? 1 : 0.4} opacity={isHovered ? 1 : 0.4}
/> />
</mesh> </mesh>
); )
})} })}
</group> </group>
); )
}; }
-3
View File
@@ -112,17 +112,14 @@ const useEditor = create<EditorState>()((set, get) => ({
case 'site': case 'site':
// In Site mode, we zoom out and deselect specific levels/buildings // In Site mode, we zoom out and deselect specific levels/buildings
viewer.resetSelection() viewer.resetSelection()
viewer.setLevelMode('stacked')
break break
case 'structure': case 'structure':
selectBuildingAndLevel0() selectBuildingAndLevel0()
viewer.setLevelMode('stacked')
break break
case 'furnish': case 'furnish':
selectBuildingAndLevel0() selectBuildingAndLevel0()
viewer.setLevelMode('solo')
// Furnish mode only supports elements layer, not zones // Furnish mode only supports elements layer, not zones
set({ structureLayer: 'elements' }) set({ structureLayer: 'elements' })
break break
@@ -27,6 +27,7 @@ const createWallGradientMaterial = (zoneColor: string) => {
opacityNode: opacity, opacityNode: opacity,
side: DoubleSide, side: DoubleSide,
depthWrite: false, depthWrite: false,
depthTest: false,
}) })
} }