feat(canvas): 画布工具 - 抓手框选改开关并精简多选整理
将底部抓手与区域选择收成胶囊开关,精简框选悬浮栏,并在右键提供按相对布局加大边距的自适应整理。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,6 +11,7 @@ type CanvasProjectContextMenuProps = {
|
||||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
canPaste: boolean;
|
||||
selectedCount: number;
|
||||
screenToCanvas: (clientX: number, clientY: number) => Position;
|
||||
onClose: () => void;
|
||||
onAddNode: (type: CanvasNodeTypeId, position: Position) => void;
|
||||
@@ -38,6 +39,9 @@ type CanvasProjectContextMenuProps = {
|
||||
onUploadToArkPrivateAsset: (node: CanvasNodeData) => void;
|
||||
onSetAssetCategory: (nodeId: string, category: CanvasAssetCategory) => void;
|
||||
onToggleFrame: (node: CanvasNodeData) => void;
|
||||
onSpreadSelection: () => void;
|
||||
onCopySelection: () => void;
|
||||
onDeleteSelection: () => void;
|
||||
};
|
||||
|
||||
export function CanvasProjectContextMenu({ menu, node, screenToCanvas, ...props }: CanvasProjectContextMenuProps) {
|
||||
@@ -52,6 +56,7 @@ export function CanvasProjectContextMenu({ menu, node, screenToCanvas, ...props
|
||||
canUndo={props.canUndo}
|
||||
canRedo={props.canRedo}
|
||||
canPaste={props.canPaste}
|
||||
selectedCount={props.selectedCount}
|
||||
onClose={props.onClose}
|
||||
onAddNode={(type) => {
|
||||
if (menu.type === "canvas") props.onAddNode(type, menu.position);
|
||||
@@ -106,6 +111,9 @@ export function CanvasProjectContextMenu({ menu, node, screenToCanvas, ...props
|
||||
onToggleFrame={() => {
|
||||
if (node?.type === CanvasNodeType.Frame) props.onToggleFrame(node);
|
||||
}}
|
||||
onSpreadSelection={props.onSpreadSelection}
|
||||
onCopySelection={props.onCopySelection}
|
||||
onDeleteSelection={props.onDeleteSelection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -947,6 +947,7 @@ function InfiniteCanvasPage() {
|
||||
alignSelectedNodes,
|
||||
autoArrangeCanvasNodes,
|
||||
arrangeSelectedNodes,
|
||||
spreadSelectedNodes,
|
||||
copyNodesToClipboard,
|
||||
copySelectedNodes,
|
||||
createFolder,
|
||||
@@ -1769,7 +1770,10 @@ function InfiniteCanvasPage() {
|
||||
(event: ReactMouseEvent, id: string) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setSelectedNodeIds(new Set([id]));
|
||||
setSelectedNodeIds((current) => {
|
||||
if (current.has(id) && current.size > 1) return current;
|
||||
return new Set([id]);
|
||||
});
|
||||
setSelectedConnectionId(null);
|
||||
closeConnectionCreateMenu();
|
||||
setToolbarNodeId(null);
|
||||
@@ -2660,6 +2664,7 @@ function InfiniteCanvasPage() {
|
||||
canUndo={historyState.canUndo}
|
||||
canRedo={historyState.canRedo}
|
||||
canPaste={hasCopiedNodes || Boolean(navigator.clipboard)}
|
||||
selectedCount={selectedNodeIds.size}
|
||||
screenToCanvas={screenToCanvas}
|
||||
onClose={() => setContextMenu(null)}
|
||||
onAddNode={(type, position) => createNode(type, position)}
|
||||
@@ -2693,6 +2698,9 @@ function InfiniteCanvasPage() {
|
||||
onUploadToArkPrivateAsset={confirmUploadNodeImageToArkPrivateAsset}
|
||||
onSetAssetCategory={(nodeId, assetCategory) => handleConfigNodeChange(nodeId, { assetCategory })}
|
||||
onToggleFrame={(node) => handleFrameToggle(node.id)}
|
||||
onSpreadSelection={spreadSelectedNodes}
|
||||
onCopySelection={copySelectedNodes}
|
||||
onDeleteSelection={() => deleteNodes(selectedNodeIds)}
|
||||
/>
|
||||
|
||||
<CanvasUploadModal open={uploadModalOpen} onClose={closeUploadModal} onUpload={handleUploadFiles} />
|
||||
|
||||
@@ -5,7 +5,7 @@ import { nanoid } from "nanoid";
|
||||
|
||||
import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
|
||||
import { FOLDER_COLLAPSED_HEIGHT, FOLDER_COLLAPSED_WIDTH, FRAME_HEADER_HEIGHT, getFrameChildIds, getFrameChildren, isFrameNode } from "@/lib/canvas/canvas-frame";
|
||||
import { alignCanvasNodes, layoutCanvasAuto, layoutCanvasFlow, layoutCanvasNodes, nextCanvasVersionLabel, type CanvasAlignmentMode } from "@/lib/canvas/canvas-layout";
|
||||
import { alignCanvasNodes, layoutCanvasAuto, layoutCanvasFlow, layoutCanvasNodes, nextCanvasVersionLabel, spreadCanvasNodes, type CanvasAlignmentMode } from "@/lib/canvas/canvas-layout";
|
||||
import { applyCanvasConnectionPromptSync } from "@/lib/canvas/canvas-resource-references";
|
||||
import { createCanvasNode, isHiddenBatchChild, removeCanvasNodes } from "@/lib/canvas/canvas-project-domain";
|
||||
import { isolateCopiedNodeMetadata, nextCopiedNodeTitle } from "@/lib/canvas/canvas-node-copy";
|
||||
@@ -206,6 +206,19 @@ export function useCanvasNodeOperations({
|
||||
message.success(hasSelection ? "已按媒体分类整理选中节点" : "已按媒体分类整理画布");
|
||||
}, [commitNodes, connectionsRef, message, nodesRef, selectedNodeIdsRef]);
|
||||
|
||||
const spreadSelectedNodes = useCallback(() => {
|
||||
const currentNodes = nodesRef.current;
|
||||
const selected = currentNodes.filter((node) => selectedNodeIdsRef.current.has(node.id) && !node.metadata?.locked && !isFrameNode(node) && !isHiddenBatchChild(node, currentNodes));
|
||||
if (selected.length < 2) {
|
||||
message.info("请至少选择两个可整理节点");
|
||||
return;
|
||||
}
|
||||
const positions = spreadCanvasNodes(selected);
|
||||
if (!positions.size) return;
|
||||
commitNodes(currentNodes.map((node) => positions.has(node.id) ? { ...node, position: positions.get(node.id)! } : node));
|
||||
message.success("已按相对布局加大间距");
|
||||
}, [commitNodes, message, nodesRef, selectedNodeIdsRef]);
|
||||
|
||||
const alignSelectedNodes = useCallback((mode: CanvasAlignmentMode) => {
|
||||
const selected = nodesRef.current.filter((node) => selectedNodeIdsRef.current.has(node.id) && !node.metadata?.locked && !isFrameNode(node));
|
||||
if (selected.length < 2 || ((mode === "distributeX" || mode === "distributeY") && selected.length < 3)) return;
|
||||
@@ -541,6 +554,7 @@ export function useCanvasNodeOperations({
|
||||
alignSelectedNodes,
|
||||
autoArrangeCanvasNodes,
|
||||
arrangeSelectedNodes,
|
||||
spreadSelectedNodes,
|
||||
copyNodesToClipboard,
|
||||
copySelectedNodes,
|
||||
createFolder,
|
||||
|
||||
Reference in New Issue
Block a user