From 30118f2980783ab4a47b5ee32465abde7a8f0708 Mon Sep 17 00:00:00 2001 From: yun Date: Sat, 4 Jul 2026 12:41:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=9A=E8=A7=92=E5=BA=A6=20=E6=8B=96?= =?UTF-8?q?=E6=8B=BD=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/globals.css | 6 + src/components/MultiAngleEditor.tsx | 117 +++++++++--------- src/components/WorkflowCanvas.tsx | 9 -- .../__tests__/FloatingNodeHeader.test.tsx | 19 +-- .../__tests__/MultiAngleEditor.test.tsx | 33 ++--- src/components/nodes/FloatingNodeHeader.tsx | 3 - 6 files changed, 88 insertions(+), 99 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 155d1b13..47bb11f0 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -181,6 +181,12 @@ body { .react-flow__node { font-family: inherit; + z-index: 2; +} + +.react-flow__node.selected, +.react-flow__node.dragging { + z-index: 10000; } /* Skip hit-testing on images inside nodes — parent containers handle all diff --git a/src/components/MultiAngleEditor.tsx b/src/components/MultiAngleEditor.tsx index 0aa0cdc8..33359f8f 100644 --- a/src/components/MultiAngleEditor.tsx +++ b/src/components/MultiAngleEditor.tsx @@ -2,7 +2,8 @@ import { useCallback, - type MouseEvent, + useRef, + useState, type PointerEvent, type ReactNode, } from "react"; @@ -41,6 +42,7 @@ export const MULTI_ANGLE_ZOOM_MAX = 10; export const FISHEYE_MULTI_ANGLE_PROMPT = "Ultra-close fisheye lens, strong ultra-wide perspective, circular frame edge, dark vignette, visible barrel distortion, enlarged subject center, stretched curved edges"; export const TILT_MULTI_ANGLE_PROMPT = "Dutch angle, tilted frame"; +const CUBE_DRAG_DEGREES_PER_PIXEL = 0.8; const MULTI_ANGLE_PRESET_LABEL_KEYS: Record = { custom: "multiAngle.preset.custom", @@ -124,6 +126,10 @@ function roundToStep(value: number, precision = 1): number { return Math.round(value * factor) / factor; } +function normalizeHorizontalDragAngle(value: number): number { + return Math.round(((value % 360) + 360) % 360); +} + function getNextMultiAngleVertical(current: number, direction: -1 | 1): number { return Math.round(clamp(current + direction, MULTI_ANGLE_VERTICAL_MIN, MULTI_ANGLE_VERTICAL_MAX)); } @@ -178,6 +184,14 @@ export function MultiAngleEditor({ actions?: ReactNode; }) { const { t } = useI18n(); + const [isCubeDragging, setIsCubeDragging] = useState(false); + const cubeDragRef = useRef<{ + pointerId: number; + startX: number; + startY: number; + startHorizontal: number; + startVertical: number; + } | null>(null); const previewScale = 0.58 + (settings.zoom / MULTI_ANGLE_ZOOM_MAX) * 0.64; const cubeRotateX = -settings.vertical; const cubeRotateY = settings.horizontal; @@ -198,72 +212,62 @@ export function MultiAngleEditor({ onChange({ zoom: roundToStep(clamp(value, MULTI_ANGLE_ZOOM_MIN, MULTI_ANGLE_ZOOM_MAX), 1) }); }; - const updateFromGlobePoint = useCallback( - (target: HTMLDivElement, clientX: number, clientY: number) => { - const rect = target.getBoundingClientRect(); - if (rect.width <= 0 || rect.height <= 0) return; - - const pointerY = clamp(1 - (clientY - rect.top) / rect.height, 0, 1); - const centerX = rect.left + rect.width / 2; - const centerY = rect.top + rect.height / 2; - const rawAngle = (Math.atan2(clientY - centerY, clientX - centerX) * 180) / Math.PI; - const normalizedAngle = (90 - rawAngle + 360) % 360; - const horizontal = Math.round(clamp(normalizedAngle, MULTI_ANGLE_HORIZONTAL_MIN, MULTI_ANGLE_HORIZONTAL_MAX)); - - onChange({ - horizontal, - vertical: Math.round(MULTI_ANGLE_VERTICAL_MIN + pointerY * (MULTI_ANGLE_VERTICAL_MAX - MULTI_ANGLE_VERTICAL_MIN)), - }); - }, - [onChange] - ); - const handleGlobePointerDown = useCallback( (event: PointerEvent) => { event.preventDefault(); event.stopPropagation(); event.currentTarget.setPointerCapture?.(event.pointerId); - updateFromGlobePoint(event.currentTarget, event.clientX, event.clientY); + cubeDragRef.current = { + pointerId: event.pointerId, + startX: event.clientX, + startY: event.clientY, + startHorizontal: settings.horizontal, + startVertical: settings.vertical, + }; + setIsCubeDragging(true); }, - [updateFromGlobePoint] + [settings.horizontal, settings.vertical] ); const handleGlobePointerMove = useCallback( (event: PointerEvent) => { - if ((event.buttons & 1) !== 1) return; + const dragState = cubeDragRef.current; + if (!dragState || dragState.pointerId !== event.pointerId) return; + if (event.pointerType === "mouse" && (event.buttons & 1) !== 1) return; event.preventDefault(); event.stopPropagation(); - updateFromGlobePoint(event.currentTarget, event.clientX, event.clientY); + const deltaX = event.clientX - dragState.startX; + const deltaY = event.clientY - dragState.startY; + + onChange({ + horizontal: normalizeHorizontalDragAngle(dragState.startHorizontal + deltaX * CUBE_DRAG_DEGREES_PER_PIXEL), + vertical: Math.round( + clamp( + dragState.startVertical - deltaY * CUBE_DRAG_DEGREES_PER_PIXEL, + MULTI_ANGLE_VERTICAL_MIN, + MULTI_ANGLE_VERTICAL_MAX + ) + ), + }); }, - [updateFromGlobePoint] + [onChange] ); const handleGlobePointerUp = useCallback((event: PointerEvent) => { event.stopPropagation(); event.currentTarget.releasePointerCapture?.(event.pointerId); + if (cubeDragRef.current?.pointerId === event.pointerId) { + cubeDragRef.current = null; + setIsCubeDragging(false); + } }, []); - const handleGlobeMouseDown = useCallback( - (event: MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); - updateFromGlobePoint(event.currentTarget, event.clientX, event.clientY); - }, - [updateFromGlobePoint] - ); - - const handleGlobeMouseMove = useCallback( - (event: MouseEvent) => { - if ((event.buttons & 1) !== 1) return; - event.preventDefault(); - event.stopPropagation(); - updateFromGlobePoint(event.currentTarget, event.clientX, event.clientY); - }, - [updateFromGlobePoint] - ); - - const handleGlobeMouseUp = useCallback((event: MouseEvent) => { + const handleGlobePointerCancel = useCallback((event: PointerEvent) => { event.stopPropagation(); + if (cubeDragRef.current?.pointerId === event.pointerId) { + cubeDragRef.current = null; + setIsCubeDragging(false); + } }, []); return ( @@ -303,9 +307,8 @@ export function MultiAngleEditor({ onPointerDown={handleGlobePointerDown} onPointerMove={handleGlobePointerMove} onPointerUp={handleGlobePointerUp} - onMouseDown={handleGlobeMouseDown} - onMouseMove={handleGlobeMouseMove} - onMouseUp={handleGlobeMouseUp} + onPointerCancel={handleGlobePointerCancel} + onLostPointerCapture={handleGlobePointerCancel} >