From 9501971c80099bdee226decde97c51d9bfb5a404 Mon Sep 17 00:00:00 2001 From: wass08 Date: Thu, 29 Jan 2026 13:25:16 +0900 Subject: [PATCH] fix drag zone issue --- .../tools/zone/zone-boundary-editor.tsx | 270 +++++++++--------- apps/editor/store/use-editor.tsx | 3 - .../renderers/zone/zone-renderer.tsx | 3 +- 3 files changed, 140 insertions(+), 136 deletions(-) diff --git a/apps/editor/components/tools/zone/zone-boundary-editor.tsx b/apps/editor/components/tools/zone/zone-boundary-editor.tsx index bcf01f38..8968f0a6 100644 --- a/apps/editor/components/tools/zone/zone-boundary-editor.tsx +++ b/apps/editor/components/tools/zone/zone-boundary-editor.tsx @@ -1,6 +1,7 @@ -import { useScene, type ZoneNode } from "@pascal-app/core"; -import { useViewer } from "@pascal-app/viewer"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useScene, type ZoneNode } from '@pascal-app/core' +import { useViewer } from '@pascal-app/viewer' +import { useThree } from '@react-three/fiber' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { BufferGeometry, Float32BufferAttribute, @@ -9,175 +10,180 @@ import { Raycaster, Vector2, Vector3, -} from "three"; -import { useThree } from "@react-three/fiber"; +} from 'three' -const Y_OFFSET = 0.02; +const Y_OFFSET = 0.02 type DragState = { - isDragging: boolean; - vertexIndex: number; - initialPosition: [number, number]; -}; + isDragging: boolean + vertexIndex: number + initialPosition: [number, number] + pointerId: number +} /** * Zone boundary editor - allows editing zone polygon vertices when a zone is selected * Uses the event emitter system for grid interactions */ export const ZoneBoundaryEditor: React.FC = () => { - const { gl, camera } = useThree(); - 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; - const updateNode = useScene((state) => state.updateNode); + const { gl, camera } = useThree() + 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 + const updateNode = useScene((state) => state.updateNode) // Local state for dragging - const [dragState, setDragState] = useState(null); - const [previewPolygon, setPreviewPolygon] = useState< - Array<[number, number]> | null - >(null); - const [hoveredVertex, setHoveredVertex] = useState(null); - const [hoveredMidpoint, setHoveredMidpoint] = useState(null); + const [dragState, setDragState] = useState(null) + const [previewPolygon, setPreviewPolygon] = useState | null>(null) + const [hoveredVertex, setHoveredVertex] = useState(null) + const [hoveredMidpoint, setHoveredMidpoint] = useState(null) // Refs for raycasting during drag - const dragPlane = useRef(new Plane(new Vector3(0, 1, 0), -Y_OFFSET)); - const raycaster = useRef(new Raycaster()); - const lineRef = useRef(null!); + const dragPlane = useRef(new Plane(new Vector3(0, 1, 0), -Y_OFFSET)) + const raycaster = useRef(new Raycaster()) + const lineRef = useRef(null!) // 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 const midpoints = useMemo(() => { - if (displayPolygon.length < 2) return []; + if (displayPolygon.length < 2) return [] return displayPolygon.map(([x1, z1], index) => { - const nextIndex = (index + 1) % displayPolygon.length; - const [x2, z2] = displayPolygon[nextIndex]!; - return [(x1! + x2) / 2, (z1! + z2) / 2] as [number, number]; - }); - }, [displayPolygon]); + const nextIndex = (index + 1) % displayPolygon.length + const [x2, z2] = displayPolygon[nextIndex]! + return [(x1! + x2) / 2, (z1! + z2) / 2] as [number, number] + }) + }, [displayPolygon]) // Handle vertex drag const handleVertexDrag = useCallback( (clientX: number, clientY: number, vertexIndex: number) => { - if (!zone) return; + if (!zone) return - const canvas = gl.domElement; - const rect = canvas.getBoundingClientRect(); - const x = ((clientX - rect.left) / rect.width) * 2 - 1; - const y = -((clientY - rect.top) / rect.height) * 2 + 1; + const canvas = gl.domElement + const rect = canvas.getBoundingClientRect() + const x = ((clientX - rect.left) / rect.width) * 2 - 1 + const y = -((clientY - rect.top) / rect.height) * 2 + 1 - raycaster.current.setFromCamera(new Vector2(x, y), camera); - const intersection = new Vector3(); - raycaster.current.ray.intersectPlane(dragPlane.current, intersection); + raycaster.current.setFromCamera(new Vector2(x, y), camera) + const intersection = new Vector3() + raycaster.current.ray.intersectPlane(dragPlane.current, intersection) if (intersection) { // Snap to 0.5 grid - const gridX = Math.round(intersection.x * 2) / 2; - const gridZ = Math.round(intersection.z * 2) / 2; + const gridX = Math.round(intersection.x * 2) / 2 + const gridZ = Math.round(intersection.z * 2) / 2 - const basePolygon = previewPolygon ?? zone.polygon; - const newPolygon = [...basePolygon]; - newPolygon[vertexIndex] = [gridX, gridZ]; - setPreviewPolygon(newPolygon); + const basePolygon = previewPolygon ?? zone.polygon + const newPolygon = [...basePolygon] + newPolygon[vertexIndex] = [gridX, gridZ] + setPreviewPolygon(newPolygon) } }, - [zone, gl, camera, previewPolygon] - ); + [zone, gl, camera, previewPolygon], + ) // Commit polygon changes const commitPolygonChange = useCallback(() => { if (previewPolygon && selectedZoneId) { - updateNode(selectedZoneId, { polygon: previewPolygon }); + updateNode(selectedZoneId, { polygon: previewPolygon }) } - setPreviewPolygon(null); - setDragState(null); - }, [previewPolygon, selectedZoneId, updateNode]); + setPreviewPolygon(null) + setDragState(null) + }, [previewPolygon, selectedZoneId, updateNode]) // Handle adding a new vertex at midpoint const handleAddVertex = useCallback( (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 = [ ...basePolygon.slice(0, afterIndex + 1), position, ...basePolygon.slice(afterIndex + 1), - ]; + ] - setPreviewPolygon(newPolygon); - return afterIndex + 1; // Return new vertex index + setPreviewPolygon(newPolygon) + return afterIndex + 1 // Return new vertex index }, - [zone, previewPolygon] - ); + [zone, previewPolygon], + ) // Handle deleting a vertex const handleDeleteVertex = useCallback( (index: number) => { - if (!zone || !selectedZoneId) return; + if (!zone || !selectedZoneId) return - const basePolygon = previewPolygon ?? zone.polygon; - if (basePolygon.length <= 3) return; // Need at least 3 points + const basePolygon = previewPolygon ?? zone.polygon + if (basePolygon.length <= 3) return // Need at least 3 points - const newPolygon = basePolygon.filter((_, i) => i !== index); - updateNode(selectedZoneId, { polygon: newPolygon }); - setPreviewPolygon(null); + const newPolygon = basePolygon.filter((_, i) => i !== index) + updateNode(selectedZoneId, { polygon: newPolygon }) + 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(() => { - 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) => { - handleVertexDrag(e.clientX, e.clientY, dragState.vertexIndex); - }; + handleVertexDrag(e.clientX, e.clientY, dragState.vertexIndex) + } - const handlePointerUp = () => { - commitPolygonChange(); - }; + const handlePointerUp = (e: PointerEvent) => { + // Release pointer capture + if (canvas.hasPointerCapture(e.pointerId)) { + canvas.releasePointerCapture(e.pointerId) + } + commitPolygonChange() + } - canvas.addEventListener("pointermove", handlePointerMove); - canvas.addEventListener("pointerup", handlePointerUp); + canvas.addEventListener('pointermove', handlePointerMove) + canvas.addEventListener('pointerup', handlePointerUp) return () => { - canvas.removeEventListener("pointermove", handlePointerMove); - canvas.removeEventListener("pointerup", handlePointerUp); - }; - }, [dragState, gl, handleVertexDrag, commitPolygonChange]); + // Release capture on cleanup + if (canvas.hasPointerCapture(pointerId)) { + canvas.releasePointerCapture(pointerId) + } + canvas.removeEventListener('pointermove', handlePointerMove) + canvas.removeEventListener('pointerup', handlePointerUp) + } + }, [dragState, gl, handleVertexDrag, commitPolygonChange]) // Update line geometry when polygon changes 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) { - positions.push(x!, Y_OFFSET + 0.01, z!); + positions.push(x!, Y_OFFSET + 0.01, z!) } // Close the loop - const first = displayPolygon[0]!; - positions.push(first[0]!, Y_OFFSET + 0.01, first[1]!); + const first = displayPolygon[0]! + positions.push(first[0]!, Y_OFFSET + 0.01, first[1]!) - const geometry = new BufferGeometry(); - geometry.setAttribute( - "position", - new Float32BufferAttribute(positions, 3) - ); + const geometry = new BufferGeometry() + geometry.setAttribute('position', new Float32BufferAttribute(positions, 3)) - lineRef.current.geometry.dispose(); - lineRef.current.geometry = geometry; - }, [displayPolygon]); + lineRef.current.geometry.dispose() + lineRef.current.geometry = geometry + }, [displayPolygon]) - if (!zone || displayPolygon.length < 3) return null; + if (!zone || displayPolygon.length < 3) return null - const canDelete = displayPolygon.length > 3; - const zoneColor = zone.color || "#3b82f6"; + const canDelete = displayPolygon.length > 3 + const zoneColor = zone.color || '#3b82f6' return ( @@ -197,97 +203,97 @@ export const ZoneBoundaryEditor: React.FC = () => { {/* Vertex handles */} {displayPolygon.map(([x, z], index) => { - const isHovered = hoveredVertex === index; - const isDragging = dragState?.vertexIndex === index; + const isHovered = hoveredVertex === index + const isDragging = dragState?.vertexIndex === index return ( { - e.stopPropagation(); - setHoveredVertex(index); + e.stopPropagation() + setHoveredVertex(index) }} onPointerLeave={(e) => { - e.stopPropagation(); - setHoveredVertex(null); + e.stopPropagation() + setHoveredVertex(null) }} onPointerDown={(e) => { - e.stopPropagation(); + e.stopPropagation() setDragState({ isDragging: true, vertexIndex: index, initialPosition: [x!, z!], - }); + pointerId: e.nativeEvent.pointerId, + }) + }} + onClick={(e) => { + e.stopPropagation() }} onDoubleClick={(e) => { - e.stopPropagation(); + e.stopPropagation() if (canDelete) { - handleDeleteVertex(index); + handleDeleteVertex(index) } }} > - + - ); + ) })} {/* Midpoint handles for adding vertices (hidden while dragging) */} {!dragState && midpoints.map(([x, z], index) => { - const isHovered = hoveredMidpoint === index; + const isHovered = hoveredMidpoint === index return ( { - e.stopPropagation(); - setHoveredMidpoint(index); + e.stopPropagation() + setHoveredMidpoint(index) }} onPointerLeave={(e) => { - e.stopPropagation(); - setHoveredMidpoint(null); + e.stopPropagation() + setHoveredMidpoint(null) }} onPointerDown={(e) => { - e.stopPropagation(); - const newVertexIndex = handleAddVertex(index, [x!, z!]); + e.stopPropagation() + const newVertexIndex = handleAddVertex(index, [x!, z!]) if (newVertexIndex >= 0) { setDragState({ isDragging: true, vertexIndex: newVertexIndex, initialPosition: [x!, z!], - }); - setHoveredMidpoint(null); + pointerId: e.nativeEvent.pointerId, + }) + setHoveredMidpoint(null) } }} + onClick={(e) => { + e.stopPropagation() + }} > - ); + ) })} - ); -}; + ) +} diff --git a/apps/editor/store/use-editor.tsx b/apps/editor/store/use-editor.tsx index 05a0384f..24685d0c 100644 --- a/apps/editor/store/use-editor.tsx +++ b/apps/editor/store/use-editor.tsx @@ -112,17 +112,14 @@ const useEditor = create()((set, get) => ({ case 'site': // In Site mode, we zoom out and deselect specific levels/buildings viewer.resetSelection() - viewer.setLevelMode('stacked') break case 'structure': selectBuildingAndLevel0() - viewer.setLevelMode('stacked') break case 'furnish': selectBuildingAndLevel0() - viewer.setLevelMode('solo') // Furnish mode only supports elements layer, not zones set({ structureLayer: 'elements' }) break diff --git a/packages/viewer/src/components/renderers/zone/zone-renderer.tsx b/packages/viewer/src/components/renderers/zone/zone-renderer.tsx index 8959acea..0802dcce 100644 --- a/packages/viewer/src/components/renderers/zone/zone-renderer.tsx +++ b/packages/viewer/src/components/renderers/zone/zone-renderer.tsx @@ -27,6 +27,7 @@ const createWallGradientMaterial = (zoneColor: string) => { opacityNode: opacity, side: DoubleSide, depthWrite: false, + depthTest: false, }) } @@ -144,7 +145,7 @@ export const ZoneRenderer = ({ node }: { node: ZoneNode }) => { return ( {/* Floor fill */} - +