feat(canvas): 框选 - 新增工具栏「框选」工具,空白处拖动框选后自动回到移动与选择 (#78)

Co-authored-by: sikongyue <3155157671@qq.com>
This commit is contained in:
爱笑的毛毛虫
2026-08-02 14:40:20 +08:00
committed by GitHub
parent f5c6c3a80f
commit 0317ac5bc7
6 changed files with 24 additions and 7 deletions

View File

@@ -218,6 +218,7 @@ export function CanvasTopBar({
<Shortcut keys={["滚轮"]} value="缩放画布" />
<Shortcut keys={["缩放滑杆"]} value="精确调整缩放" />
<Shortcut keys={["Shift / Ctrl / Cmd + 左键拖动"]} value="框选多个节点" />
<Shortcut keys={["工具栏「框选」", "左键拖动"]} value="框选多个节点,完成后自动回到「移动与选择」" />
<Shortcut keys={["Shift / Ctrl / Cmd", "点击"]} value="追加选择节点" />
<Shortcut keys={["Alt", "点击 / 框选"]} value="移除选择节点" />
<Shortcut keys={["Ctrl / Cmd", "1 / 2 / 3"]} value="100% / 适应全部 / 适应选择" />

View File

@@ -106,6 +106,7 @@ import {
type StoryboardShotDuration,
type CanvasWorkflowKind,
type CanvasWorkspaceMode,
type CanvasToolMode,
type ContextMenuState,
type Position,
type ViewportTransform,
@@ -204,6 +205,7 @@ function InfiniteCanvasPage() {
const [isMiniMapOpen, setIsMiniMapOpen] = useState(false);
const [backgroundMode, setBackgroundMode] = useState<CanvasBackgroundMode>("dots");
const [showImageInfo, setShowImageInfo] = useState(false);
const [canvasTool, setCanvasTool] = useState<CanvasToolMode>("move");
const [mediaPerformanceMode, setMediaPerformanceMode] = useState<CanvasMediaPerformanceMode>(readCanvasMediaPerformanceMode);
const [projectLoaded, setProjectLoaded] = useState(false);
const [workspaceMode, setWorkspaceMode] = useState<CanvasWorkspaceMode>(readCanvasWorkspaceMode);
@@ -700,6 +702,7 @@ function InfiniteCanvasPage() {
onNodeInteractionStart: handleNodeInteractionStart,
onNodeClick: handleSelectedNodeClick,
onDeselect: handleCanvasDeselect,
onSelectionBoxEnd: () => setCanvasTool((tool) => (tool === "box-select" ? "move" : tool)),
});
const keepNodeToolbar = useCallback(
@@ -1430,6 +1433,7 @@ function InfiniteCanvasPage() {
onViewportChange={handleViewportChange}
onViewportPreviewChange={handleViewportPreviewChange}
onCanvasMouseDown={handleCanvasMouseDown}
boxSelectEnabled={canvasTool === "box-select"}
onCanvasDoubleClick={handleCanvasDoubleClick}
onCanvasDeselect={deselectCanvas}
onContextMenu={handleCanvasContextMenu}
@@ -1568,6 +1572,8 @@ function InfiniteCanvasPage() {
<CanvasToolbar
selectedCount={selectedNodeIds.size}
workspaceMode={workspaceMode}
canvasTool={canvasTool}
onToolChange={setCanvasTool}
isProjectLinked={Boolean(currentProject?.projectId)}
canUndo={historyState.canUndo}
canRedo={historyState.canRedo}

View File

@@ -20,6 +20,7 @@ type UseCanvasSelectionControllerOptions = {
onNodeInteractionStart: (selectionModifier: boolean) => void;
onNodeClick: (node: CanvasNodeData) => void;
onDeselect: () => void;
onSelectionBoxEnd?: () => void;
};
type DragState = {
@@ -57,6 +58,7 @@ export function useCanvasSelectionController({
onNodeInteractionStart,
onNodeClick,
onDeselect,
onSelectionBoxEnd,
}: UseCanvasSelectionControllerOptions) {
const dragFrameRef = useRef<number | null>(null);
const pendingNodeDragRef = useRef<Position>({ x: 0, y: 0 });
@@ -296,7 +298,8 @@ export function useCanvasSelectionController({
const wasSelection = selectionActivatedRef.current;
cancelSelectionBox();
if (hadPendingSelection && !wasSelection) deselectCanvas();
}, [cancelSelectionBox, deselectCanvas]);
if (hadPendingSelection) onSelectionBoxEnd?.();
}, [cancelSelectionBox, deselectCanvas, onSelectionBoxEnd]);
useEffect(() => {
const handleMouseUp = (event: MouseEvent) => {