feat(canvas): 框选 - 新增工具栏「框选」工具,空白处拖动框选后自动回到移动与选择 (#78)
Co-authored-by: sikongyue <3155157671@qq.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useEffect, useRef, useState, type MouseEvent as ReactMouseEvent, type ReactNode } from "react";
|
||||
import { Segmented, Switch } from "antd";
|
||||
import { CircleDot, Clapperboard, Eraser, FolderOpen, Grid2x2, Hand, Image as ImageIcon, Info, Layers3, Moon, Music2, Palette, PanelTop, Pencil, Plus, Redo2, Square, Sun, Trash2, Type, Undo2, UploadCloud, UserRound, Video, X } from "lucide-react";
|
||||
import { CircleDot, Clapperboard, Eraser, FolderOpen, Grid2x2, Hand, Image as ImageIcon, Info, Layers3, Moon, Music2, Palette, PanelTop, Pencil, Plus, Redo2, Square, SquareDashedMousePointer, Sun, Trash2, Type, Undo2, UploadCloud, UserRound, Video, X } from "lucide-react";
|
||||
|
||||
import { AnimatedThemeToggler } from "@/components/ui/animated-theme-toggler";
|
||||
import { FloatingDock, type FloatingDockEntry } from "@/components/ui/aceternity/floating-dock";
|
||||
@@ -11,11 +11,13 @@ import { aceternityMotion } from "@/lib/aceternity-motion";
|
||||
import { canvasDockStyle } from "@/lib/canvas/canvas-aceternity-style";
|
||||
import { canvasThemes, type CanvasBackgroundMode, type CanvasColorTheme, type CanvasTheme } from "@/lib/canvas-theme";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import type { CanvasWorkspaceMode } from "@/types/canvas";
|
||||
import type { CanvasToolMode, CanvasWorkspaceMode } from "@/types/canvas";
|
||||
|
||||
export function CanvasToolbar({
|
||||
selectedCount,
|
||||
workspaceMode,
|
||||
canvasTool,
|
||||
onToolChange,
|
||||
isProjectLinked,
|
||||
canUndo,
|
||||
canRedo,
|
||||
@@ -43,6 +45,8 @@ export function CanvasToolbar({
|
||||
}: {
|
||||
selectedCount: number;
|
||||
workspaceMode: CanvasWorkspaceMode;
|
||||
canvasTool: CanvasToolMode;
|
||||
onToolChange: (tool: CanvasToolMode) => void;
|
||||
isProjectLinked: boolean;
|
||||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
@@ -96,7 +100,8 @@ export function CanvasToolbar({
|
||||
}, [addOpen, appearanceOpen]);
|
||||
|
||||
const items: FloatingDockEntry[] = [
|
||||
{ id: selectedCount ? "tool-close-selection" : "tool-hand", label: selectedCount ? `取消选择${selectedCount > 1 ? ` ${selectedCount} 个节点` : ""}` : "移动与选择", icon: selectedCount ? <X /> : <Hand />, active: !selectedCount, onClick: () => onDeselect() },
|
||||
{ id: canvasTool === "box-select" ? "tool-move" : selectedCount ? "tool-close-selection" : "tool-move", label: canvasTool === "box-select" ? "移动与选择" : selectedCount ? `取消选择${selectedCount > 1 ? ` ${selectedCount} 个节点` : ""}` : "移动与选择", icon: canvasTool === "box-select" ? <Hand /> : selectedCount ? <X /> : <Hand />, active: canvasTool === "move", onClick: () => { if (canvasTool !== "move") onToolChange("move"); else onDeselect(); } },
|
||||
{ id: "tool-box-select", label: "框选", icon: <SquareDashedMousePointer />, active: canvasTool === "box-select", onClick: () => onToolChange(canvasTool === "box-select" ? "move" : "box-select") },
|
||||
{ kind: "separator", id: "history-separator" },
|
||||
{ id: "tool-undo", label: "撤销", icon: <Undo2 />, disabled: !canUndo, onClick: () => onUndo() },
|
||||
{ id: "tool-redo", label: "重做", icon: <Redo2 />, disabled: !canRedo, onClick: () => onRedo() },
|
||||
|
||||
@@ -12,6 +12,7 @@ type InfiniteCanvasProps = {
|
||||
onViewportChange: (viewport: ViewportTransform) => void;
|
||||
onViewportPreviewChange?: (viewport: ViewportTransform) => void;
|
||||
onCanvasMouseDown?: (event: React.PointerEvent<HTMLDivElement>) => void;
|
||||
boxSelectEnabled?: boolean;
|
||||
onCanvasDoubleClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
onCanvasDeselect?: () => void;
|
||||
onContextMenu?: (event: React.MouseEvent) => void;
|
||||
@@ -38,7 +39,7 @@ type PinchState = {
|
||||
initialScale: number;
|
||||
};
|
||||
|
||||
export function InfiniteCanvas({ containerRef, viewport, backgroundMode = "dots", onViewportChange, onViewportPreviewChange, onCanvasMouseDown, onCanvasDoubleClick, onCanvasDeselect, onContextMenu, onDrop, onFileDragEnter, onFileDragLeave, onFileDragOver, graphicsLayer, children }: InfiniteCanvasProps) {
|
||||
export function InfiniteCanvas({ containerRef, viewport, backgroundMode = "dots", onViewportChange, onViewportPreviewChange, onCanvasMouseDown, boxSelectEnabled = false, onCanvasDoubleClick, onCanvasDeselect, onContextMenu, onDrop, onFileDragEnter, onFileDragLeave, onFileDragOver, graphicsLayer, children }: InfiniteCanvasProps) {
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
const panState = useRef({
|
||||
isPanning: false,
|
||||
@@ -196,7 +197,7 @@ export function InfiniteCanvas({ containerRef, viewport, backgroundMode = "dots"
|
||||
const isTouch = event.pointerType === "touch";
|
||||
|
||||
const hasSelectionModifier = event.shiftKey || event.ctrlKey || event.metaKey || event.altKey;
|
||||
if (event.button === 0 && !isSpacePressed && !isTouch && isBackgroundClick && hasSelectionModifier) {
|
||||
if (event.button === 0 && !isSpacePressed && !isTouch && isBackgroundClick && (hasSelectionModifier || boxSelectEnabled)) {
|
||||
event.preventDefault();
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
onCanvasMouseDown?.(event);
|
||||
@@ -368,7 +369,7 @@ export function InfiniteCanvas({ containerRef, viewport, backgroundMode = "dots"
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`relative h-full w-full select-none overflow-hidden touch-none ${isPanning ? "cursor-grabbing" : "cursor-grab"}`}
|
||||
className={`relative h-full w-full select-none overflow-hidden touch-none ${isPanning ? "cursor-grabbing" : boxSelectEnabled ? "cursor-crosshair" : "cursor-grab"}`}
|
||||
style={{
|
||||
background: theme.canvas.background,
|
||||
overscrollBehavior: "none",
|
||||
|
||||
@@ -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% / 适应全部 / 适应选择" />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -26,6 +26,7 @@ export enum CanvasNodeType {
|
||||
export type CanvasNodeStatus = "idle" | "success" | "loading" | "error";
|
||||
export type CanvasMediaPerformanceMode = "auto" | "quality" | "performance";
|
||||
export type CanvasWorkspaceMode = "simple" | "professional";
|
||||
export type CanvasToolMode = "move" | "box-select";
|
||||
export type StoryboardShotDuration = "auto" | "5" | "10" | "15" | "30";
|
||||
export type StoryboardShotCount = "auto" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10";
|
||||
export type CanvasGenerationMode = "text" | "image" | "video" | "audio";
|
||||
|
||||
Reference in New Issue
Block a user