Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/src/app/theater/[id]/hooks/useCanvasSnapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function useCanvasSnapping(snapToGuides: boolean) {
horizontal: number | null;
}>({ vertical: null, horizontal: null });

const onNodeDrag = useCallback((_: React.MouseEvent, node: Node) => {
const onNodeDrag = useCallback((_: MouseEvent | TouchEvent, node: Node) => {
if (!snapToGuides) {
setAlignmentLines({ vertical: null, horizontal: null });
return;
Expand Down
27 changes: 22 additions & 5 deletions frontend/src/app/theater/[id]/hooks/useNodeDragToAI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@ function isPointInRect(x: number, y: number, rect: DOMRect): boolean {
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
}

/**
* 归一化 xyflow 拖拽事件(原生 MouseEvent | TouchEvent)
* TouchEvent 没有 clientX/clientY/ctrlKey,需从 changedTouches 提取
*/
function getDragEventPoint(event: MouseEvent | TouchEvent): { clientX: number; clientY: number; ctrlKey: boolean; metaKey: boolean } {
const touch = 'touches' in event ? (event.changedTouches[0] ?? event.touches[0]) : undefined;
const source = (touch ?? event) as MouseEvent;
return {
clientX: source.clientX,
clientY: source.clientY,
ctrlKey: 'ctrlKey' in event ? event.ctrlKey : false,
metaKey: 'metaKey' in event ? event.metaKey : false,
};
}

/**
* 画布节点拖拽到 AI 面板的检测 hook
* 支持多选节点拖拽(最多5个图像节点)
Expand All @@ -29,9 +44,10 @@ export function useNodeDragToAI() {
// 当前拖拽的节点ID列表
const draggedNodeIdsRef = useRef<string[]>([]);

const onNodeDragStart = useCallback((event: React.MouseEvent, node: Node, nodes: Node[]) => {
const onNodeDragStart = useCallback((event: MouseEvent | TouchEvent, node: Node, nodes: Node[]) => {
// 判断是否多选:按住 Ctrl 或节点已被选中且选中有多个节点
const isMultiSelect = event.ctrlKey || event.metaKey ||
const { ctrlKey, metaKey } = getDragEventPoint(event);
const isMultiSelect = ctrlKey || metaKey ||
(node.selected && nodes.filter(n => n.selected).length > 1);

// 获取所有正在拖拽的节点
Expand All @@ -51,22 +67,23 @@ export function useNodeDragToAI() {
isOverPanelRef.current = false;
}, []);

const onNodeDrag = useCallback((event: React.MouseEvent, _node: Node) => {
const onNodeDrag = useCallback((event: MouseEvent | TouchEvent, _node: Node) => {
const rect = panelRectRef.current;
// 面板不存在(未打开)时,尝试检测关闭态按钮区域
const panelEl = document.querySelector(AI_PANEL_SELECTOR);
const currentRect = panelEl?.getBoundingClientRect() ?? rect;
currentRect && (panelRectRef.current = currentRect);

const isOver = !!currentRect && isPointInRect(event.clientX, event.clientY, currentRect);
const { clientX, clientY } = getDragEventPoint(event);
const isOver = !!currentRect && isPointInRect(clientX, clientY, currentRect);

// 状态变化时才更新 store,减少渲染
const prev = isOverPanelRef.current;
isOverPanelRef.current = isOver;
(isOver !== prev) && useAIAssistantStore.getState().setIsDragOverPanel(isOver);
}, []);

const onNodeDragStop = useCallback((_: React.MouseEvent, node: Node, nodes: Node[]) => {
const onNodeDragStop = useCallback((_: MouseEvent | TouchEvent, node: Node, nodes: Node[]) => {
const wasOverPanel = isOverPanelRef.current;

// 重置拖拽悬停状态
Expand Down
16 changes: 9 additions & 7 deletions frontend/src/app/theater/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import {
ConnectionMode,
BackgroundVariant,
NodeTypes,
type OnNodeDrag,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { ChevronRight } from 'lucide-react';

import { useCanvasStore } from '@/store/useCanvasStore';
import { useCanvasStore, type CanvasNode } from '@/store/useCanvasStore';
import { Sidebar } from '@/components/canvas/Sidebar';
import { ZoomControls } from '@/components/canvas/ZoomControls';
import ScriptNode from '@/components/canvas/TextNode';
Expand Down Expand Up @@ -129,21 +130,22 @@ function InfiniteCanvas() {
const { onNodeDragStart: onAIDragStart, onNodeDrag: onAIDrag, onNodeDragStop: onAIDragStop } = useNodeDragToAI();

// --- Composed drag callbacks: snapping + AI panel detection ---
const composedOnNodeDragStart = useCallback(
(event: React.MouseEvent, node: any, nodes: any[]) => {
// xyflow 拖拽事件为原生 MouseEvent | TouchEvent,显式标注 OnNodeDrag 防止签名漂移
const composedOnNodeDragStart = useCallback<OnNodeDrag<CanvasNode>>(
(event, node, nodes) => {
onAIDragStart(event, node, nodes);
},
[onAIDragStart]
);
const composedOnNodeDrag = useCallback(
(event: React.MouseEvent, node: any, nodes: any[]) => {
const composedOnNodeDrag = useCallback<OnNodeDrag<CanvasNode>>(
(event, node) => {
onSnappingDrag(event, node);
onAIDrag(event, node);
},
[onSnappingDrag, onAIDrag]
);
const composedOnNodeDragStop = useCallback(
(event: React.MouseEvent, node: any, nodes: any[]) => {
const composedOnNodeDragStop = useCallback<OnNodeDrag<CanvasNode>>(
(event, node, nodes) => {
onSnappingDragStop();
onAIDragStop(event, node, nodes);
},
Expand Down
Loading