From 238674785b4f631588adcc9b9eb0e938695be4ea Mon Sep 17 00:00:00 2001 From: yun Date: Sat, 4 Jul 2026 15:35:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=9A=E4=BA=A4=E4=BA=92=20=E4=BA=A4?= =?UTF-8?q?=E4=BA=92=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/MultiAngleEditor.tsx | 6 +- src/components/WorkflowCanvas.tsx | 65 ++++++++++++++----- .../__tests__/MultiAngleEditor.test.tsx | 3 +- 3 files changed, 54 insertions(+), 20 deletions(-) diff --git a/src/components/MultiAngleEditor.tsx b/src/components/MultiAngleEditor.tsx index 6a9c3455..c836bcd2 100644 --- a/src/components/MultiAngleEditor.tsx +++ b/src/components/MultiAngleEditor.tsx @@ -243,7 +243,7 @@ export function MultiAngleEditor({ horizontal: normalizeHorizontalDragAngle(dragState.startHorizontal + deltaX * CUBE_DRAG_DEGREES_PER_PIXEL), vertical: Math.round( clamp( - dragState.startVertical - deltaY * CUBE_DRAG_DEGREES_PER_PIXEL, + dragState.startVertical + deltaY * CUBE_DRAG_DEGREES_PER_PIXEL, MULTI_ANGLE_VERTICAL_MIN, MULTI_ANGLE_VERTICAL_MAX ) @@ -315,7 +315,7 @@ export function MultiAngleEditor({ aria-label={t("multiAngle.adjustVerticalUp")} onPointerDown={(event) => event.stopPropagation()} onMouseDown={(event) => event.stopPropagation()} - onClick={() => updateVertical(getNextMultiAngleVertical(settings.vertical, 1))} + onClick={() => updateVertical(getNextMultiAngleVertical(settings.vertical, -1))} className="absolute left-1/2 top-3 z-40 flex h-7 w-7 -translate-x-1/2 items-center justify-center rounded-md text-[var(--text-muted)] transition-colors hover:bg-[var(--surface-hover)] hover:text-[var(--text-primary)]" > @@ -325,7 +325,7 @@ export function MultiAngleEditor({ aria-label={t("multiAngle.adjustVerticalDown")} onPointerDown={(event) => event.stopPropagation()} onMouseDown={(event) => event.stopPropagation()} - onClick={() => updateVertical(getNextMultiAngleVertical(settings.vertical, -1))} + onClick={() => updateVertical(getNextMultiAngleVertical(settings.vertical, 1))} className="absolute bottom-3 left-1/2 z-40 flex h-7 w-7 -translate-x-1/2 items-center justify-center rounded-md text-[var(--text-muted)] transition-colors hover:bg-[var(--surface-hover)] hover:text-[var(--text-primary)]" > diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index e9e4f5ee..3997177b 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -98,10 +98,7 @@ const BOX_SELECTION_EXPAND_MIN_DISTANCE = 4; const MULTI_SELECTION_BOUNDS_PADDING = 40; const GROUP_NODE_Z_INDEX = 0; const DEFAULT_NODE_Z_INDEX = 1; -const SELECTED_NODE_Z_INDEX = 10000; -const DRAGGING_GROUP_Z_INDEX = 9999; -const DRAGGING_GROUP_MEMBER_Z_INDEX = 10001; -const DRAGGING_NODE_Z_INDEX = 10002; +const INITIAL_ACTIVE_NODE_Z_INDEX = 100; type ScreenPoint = { x: number; @@ -575,6 +572,10 @@ export function WorkflowCanvas() { const [isMiniMapOpen, setIsMiniMapOpen] = useState(false); const [zoomPercent, setZoomPercent] = useState(100); const [draggingGroupId, setDraggingGroupId] = useState(null); + const [activeNodeLayer, setActiveNodeLayer] = useState<{ nodeId: string | null; zIndex: number }>({ + nodeId: null, + zIndex: INITIAL_ACTIVE_NODE_Z_INDEX, + }); const canvasGridColor = "var(--canvas-grid)"; const miniMapMaskColor = "var(--surface-overlay)"; const miniMapNodeColor = "var(--text-disabled)"; @@ -587,6 +588,20 @@ export function WorkflowCanvas() { const gestureZoomStartRef = useRef<{ zoom: number } | null>(null); const [boxSelectionRect, setBoxSelectionRect] = useState(null); + const bringNodeToFront = useCallback((nodeId: string) => { + setActiveNodeLayer((current) => ({ + nodeId, + zIndex: current.zIndex + 1, + })); + }, []); + + const resetActiveNodeLayer = useCallback(() => { + setActiveNodeLayer((current) => ({ + ...current, + nodeId: null, + })); + }, []); + const onNodesChange = useCallback((changes: NodeChange[]) => { if (!boxSelectionActiveRef.current) { storeOnNodesChange(changes); @@ -878,6 +893,13 @@ export function WorkflowCanvas() { // Apply visual state that React Flow can turn into DOM classes and z-index. const allNodes = useMemo(() => { const draggingGroupNodeId = draggingGroupId ? `group-node-${draggingGroupId}` : null; + const activeNode = activeNodeLayer.nodeId + ? nodes.find((node) => node.id === activeNodeLayer.nodeId) + : undefined; + const activeGroupId = activeNode?.type === "group" + ? (activeNode.data as { groupId?: string }).groupId + : undefined; + const activeGroupNodeId = activeGroupId ? `group-node-${activeGroupId}` : null; return nodes.map((node) => { // Never dim Switch or ConditionalSwitch nodes themselves @@ -897,23 +919,29 @@ export function WorkflowCanvas() { // Preserve existing className if any, add/remove dimmed/skipped classes const baseClass = (node.className || "").replace(/\bswitch-dimmed\b/g, "").replace(/\bnode-skipped\b/g, "").trim(); const newClass = extraClasses ? `${baseClass} ${extraClasses}`.trim() : baseClass; - const zIndex = node.type === "group" - ? isDraggingGroupShell - ? DRAGGING_GROUP_Z_INDEX - : GROUP_NODE_Z_INDEX - : isDraggingGroupMember - ? DRAGGING_GROUP_MEMBER_Z_INDEX - : node.dragging - ? DRAGGING_NODE_Z_INDEX - : node.selected - ? SELECTED_NODE_Z_INDEX - : DEFAULT_NODE_Z_INDEX; + let zIndex = node.type === "group" ? GROUP_NODE_Z_INDEX : DEFAULT_NODE_Z_INDEX; + + if (activeGroupId && node.id === activeGroupNodeId) { + zIndex = activeNodeLayer.zIndex; + } else if (activeGroupId && node.type !== "group" && (node.groupId === activeGroupId || node.parentId === activeGroupNodeId)) { + zIndex = activeNodeLayer.zIndex + 1; + } else if (!activeGroupId && node.id === activeNodeLayer.nodeId) { + zIndex = activeNodeLayer.zIndex; + } + + if (isDraggingGroupShell) { + zIndex = Math.max(zIndex, activeNodeLayer.zIndex); + } else if (isDraggingGroupMember) { + zIndex = Math.max(zIndex, activeNodeLayer.zIndex + 1); + } else if (node.dragging) { + zIndex = Math.max(zIndex, activeNodeLayer.zIndex + 1); + } // Only create new node object if className changed if (node.className === newClass && node.zIndex === zIndex) return node; return { ...node, className: newClass, zIndex }; }); - }, [nodes, dimmedNodeIds, draggingGroupId, skippedNodeIds]); + }, [nodes, activeNodeLayer, dimmedNodeIds, draggingGroupId, skippedNodeIds]); const absoluteNodePositions = useMemo(() => { const byId = new Map(allNodes.map((node) => [node.id, node])); @@ -3049,9 +3077,13 @@ export function WorkflowCanvas() { onConnectEnd={handleConnectEnd} onMoveStart={() => { isPanningRef.current = true; setHoveredNodeId(null); document.documentElement.classList.add("canvas-interacting"); }} onMoveEnd={() => { isPanningRef.current = false; document.documentElement.classList.remove("canvas-interacting"); syncZoomPercent(); }} + onNodeClick={(_event, node) => { + bringNodeToFront(node.id); + }} onNodeDragStart={(_event, node) => { isDraggingNodeRef.current = true; document.documentElement.classList.add("canvas-interacting"); + bringNodeToFront(node.id); setDraggingGroupId(node.type === "group" ? (node.data as { groupId?: string }).groupId ?? null : null); }} onNodeDragStop={(event, node) => { @@ -3060,6 +3092,7 @@ export function WorkflowCanvas() { setDraggingGroupId(null); handleNodeDragStop(event, node); }} + onPaneClick={resetActiveNodeLayer} onPaneContextMenu={openCanvasContextMenu} onNodeContextMenu={openNodeContextMenu} onSelectionStart={handleSelectionStart} diff --git a/src/components/__tests__/MultiAngleEditor.test.tsx b/src/components/__tests__/MultiAngleEditor.test.tsx index 5a6db0b7..2ddbdb71 100644 --- a/src/components/__tests__/MultiAngleEditor.test.tsx +++ b/src/components/__tests__/MultiAngleEditor.test.tsx @@ -99,12 +99,13 @@ describe("MultiAngleEditor", () => { render(); const globe = screen.getByRole("application", { name: "Multi-angle sight control" }); + fireEvent.change(screen.getByLabelText("Vertical"), { target: { value: "0" } }); fireEvent.pointerDown(globe, { pointerId: 1, pointerType: "mouse", clientX: 100, clientY: 100, buttons: 1 }); fireEvent.pointerMove(globe, { pointerId: 1, pointerType: "mouse", clientX: 150, clientY: 80, buttons: 1 }); await waitFor(() => expect(screen.getByLabelText("Horizontal")).toHaveValue("40")); - expect(screen.getByLabelText("Vertical")).toHaveValue("-14"); + expect(screen.getByLabelText("Vertical")).toHaveValue("-16"); fireEvent.pointerUp(globe, { pointerId: 1, pointerType: "mouse" });