mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-07 01:54:24 +08:00
feat(canvas): update canvas tool behavior to allow temporary switching between Select and Move modes using Control or Space
This commit is contained in:
+2
-1
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
+ [优化] 画布默认改为拖动疏朗虚线框选,虚线显示不受画布缩放影响,左下工具可切换选择与移动模式,并支持按住 Control 或空格临时平移画布。
|
+ [修复] 画布将空格键专用于临时切换工具,避免按钮保留焦点后按空格被再次触发。
|
||||||
|
+ [优化] 画布默认改为拖动轻量虚线框选,虚线显示不受画布缩放影响,左下工具可切换选择与移动模式,并支持按住 Control 或空格临时反转当前模式。
|
||||||
+ [优化] 画布多图生成结果合并为单个图片节点,生成期间可展开查看每张图片的实时状态,优先展示最先成功的结果,并以紧凑堆叠展示已有图片。
|
+ [优化] 画布多图生成结果合并为单个图片节点,生成期间可展开查看每张图片的实时状态,优先展示最先成功的结果,并以紧凑堆叠展示已有图片。
|
||||||
+ [优化] 从左侧元素列表定位画布节点时,自动缩放上限调整为 100%。
|
+ [优化] 从左侧元素列表定位画布节点时,自动缩放上限调整为 100%。
|
||||||
+ [修复] 画布多图切换主图时保持节点当前视觉尺度和中心位置,避免节点突然放大造成视口缩放错觉。
|
+ [修复] 画布多图切换主图时保持节点当前视觉尺度和中心位置,避免节点突然放大造成视口缩放错觉。
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ description: Implemented changes that still need manual verification
|
|||||||
|
|
||||||
The current release needs manual verification in these areas:
|
The current release needs manual verification in these areas:
|
||||||
|
|
||||||
- Canvas navigation: the bottom toolbar should switch between Select and Move modes and reflect the active mode with its icon; Select should draw a clearly spaced dashed range whose dash length, gap, and stroke width remain visually constant at every zoom level, while Move should pan from either the background or a node. Holding Control or Space should temporarily pan without changing the selected toolbar mode; middle-button panning and additive node selection with Shift/Cmd should continue to work.
|
- Canvas navigation: the bottom toolbar should switch between Select and Move modes and reflect the active mode with its icon; Select should draw a clearly spaced dashed range whose dash length, gap, and stroke width remain visually constant at every zoom level, while Move should pan from either the background or a node. Holding Control or Space should temporarily invert the active tool without changing the toolbar mode: Select becomes Move, and Move becomes range selection. After clicking any canvas-page button, pressing Space should only invert the tool and must not trigger that button again. Middle-button panning and additive node selection with Shift/Cmd should continue to work, while Space remains available inside text inputs and editable content.
|
||||||
- English and Simplified Chinese switching across navigation, settings, titles, descriptions, Ant Design components, and persisted preferences.
|
- English and Simplified Chinese switching across navigation, settings, titles, descriptions, Ant Design components, and persisted preferences.
|
||||||
- Global Dropdown, Menu, Select, Cascader, and TreeSelect popup backgrounds, hover states, and selected states in both light and dark themes.
|
- Global Dropdown, Menu, Select, Cascader, and TreeSelect popup backgrounds, hover states, and selected states in both light and dark themes.
|
||||||
- Canvas node resize stability, prompt scrolling, generated-prompt restoration, image editing, drag-and-drop references, and generation configuration.
|
- Canvas node resize stability, prompt scrolling, generated-prompt restoration, image editing, drag-and-drop references, and generation configuration.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ description: 当前版本已实现但仍需人工验证的变更项
|
|||||||
|
|
||||||
# 待测试
|
# 待测试
|
||||||
|
|
||||||
- 画布选择与平移:左下工具栏应能切换「选择」与「移动」模式并用图标显示当前模式;选择模式拖动空白区域应显示疏朗清晰的虚线框,虚线长度、间距和粗细在任意缩放比例下保持一致;移动模式应能从空白区域或节点上拖动画布。按住 Control 或空格应临时平移且不改变工具栏模式,中键平移以及 Shift/Cmd 追加选择节点应继续可用。
|
- 画布选择与平移:左下工具栏应能切换「选择」与「移动」模式并用图标显示当前模式;选择模式拖动空白区域应显示疏朗清晰的虚线框,虚线长度、间距和粗细在任意缩放比例下保持一致;移动模式应能从空白区域或节点上拖动画布。按住 Control 或空格应临时反转当前工具且不改变工具栏模式,即选择变移动、移动变范围选择;点击画布页任意按钮后按空格应只临时切换工具,不得再次触发该按钮;输入框和可编辑文本内仍可正常输入空格。中键平移以及 Shift/Cmd 追加选择节点应继续可用。
|
||||||
- 文档站国际化:访问文档站首页及 `/docs` 应默认显示英文,切换简体中文后 URL 应带 `/zh-CN` 且首页、顶部导航、侧边栏、正文、搜索框和页面操作文案均显示中文;刷新、站内跳转和再次切回 English 后语言应保持正确,搜索结果与 Markdown/LLM 文档内容不得混入另一种语言。
|
- 文档站国际化:访问文档站首页及 `/docs` 应默认显示英文,切换简体中文后 URL 应带 `/zh-CN` 且首页、顶部导航、侧边栏、正文、搜索框和页面操作文案均显示中文;刷新、站内跳转和再次切回 English 后语言应保持正确,搜索结果与 Markdown/LLM 文档内容不得混入另一种语言。
|
||||||
- 国际化基础框架:从页面右上角切换简体中文或 English 后,首页、顶部及移动端导航、版本弹窗、配置主面板、渠道编辑、模型选择、页面标题、页面描述和 Ant Design 内置文案应立即切换,刷新页面后保持所选语言;偏好设置中不应再显示语言选项,WebDAV 同步错误、提示词来源及其他尚未接入翻译的业务模块暂时仍可能显示中文。
|
- 国际化基础框架:从页面右上角切换简体中文或 English 后,首页、顶部及移动端导航、版本弹窗、配置主面板、渠道编辑、模型选择、页面标题、页面描述和 Ant Design 内置文案应立即切换,刷新页面后保持所选语言;偏好设置中不应再显示语言选项,WebDAV 同步错误、提示词来源及其他尚未接入翻译的业务模块暂时仍可能显示中文。
|
||||||
- 全局弹层主题:全站 Dropdown、Menu、Select、Cascader、TreeSelect 等弹层在浅色主题下应使用白色背景、轻灰悬停和选中态,在深色主题下应使用深色背景与低对比度浅色交互态,不应再出现默认黑灰色块、文字对比不足或单个页面配色不一致。
|
- 全局弹层主题:全站 Dropdown、Menu、Select、Cascader、TreeSelect 等弹层在浅色主题下应使用白色背景、轻灰悬停和选中态,在深色主题下应使用深色背景与低对比度浅色交互态,不应再出现默认黑灰色块、文字对比不足或单个页面配色不一致。
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export function CanvasTopBar({
|
|||||||
</div>
|
</div>
|
||||||
<Modal title={t("canvas.shortcuts")} open={shortcutsOpen} onCancel={() => setShortcutsOpen(false)} footer={null} centered>
|
<Modal title={t("canvas.shortcuts")} open={shortcutsOpen} onCancel={() => setShortcutsOpen(false)} footer={null} centered>
|
||||||
<div className="space-y-2 border-t pt-4 text-sm" style={{ borderColor: theme.node.stroke }}>
|
<div className="space-y-2 border-t pt-4 text-sm" style={{ borderColor: theme.node.stroke }}>
|
||||||
<Shortcut keys={["Ctrl / Space", t("canvas.shortcut.drag")]} value={t("canvas.shortcut.pan")} />
|
<Shortcut keys={["Ctrl / Space", t("canvas.shortcut.drag")]} value={t("canvas.shortcut.toggleTool")} />
|
||||||
<Shortcut keys={[t("canvas.shortcut.wheel")]} value={t("canvas.shortcut.zoom")} />
|
<Shortcut keys={[t("canvas.shortcut.wheel")]} value={t("canvas.shortcut.zoom")} />
|
||||||
<Shortcut keys={[t("canvas.shortcut.zoomSlider")]} value={t("canvas.shortcut.preciseZoom")} />
|
<Shortcut keys={[t("canvas.shortcut.zoomSlider")]} value={t("canvas.shortcut.preciseZoom")} />
|
||||||
<Shortcut keys={[t("canvas.shortcut.drag")]} value={t("canvas.shortcut.boxSelect")} />
|
<Shortcut keys={[t("canvas.shortcut.drag")]} value={t("canvas.shortcut.boxSelect")} />
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function CanvasZoomControls({ scale, onScaleChange, onReset, isMiniMapOpe
|
|||||||
</div>
|
</div>
|
||||||
<Modal title={t("canvas.shortcuts")} open={shortcutsOpen} onCancel={() => setShortcutsOpen(false)} footer={null} centered>
|
<Modal title={t("canvas.shortcuts")} open={shortcutsOpen} onCancel={() => setShortcutsOpen(false)} footer={null} centered>
|
||||||
<div className="space-y-3 border-t pt-4 text-sm" style={{ borderColor: theme.node.stroke }}>
|
<div className="space-y-3 border-t pt-4 text-sm" style={{ borderColor: theme.node.stroke }}>
|
||||||
<Shortcut label={`Ctrl / Space + ${t("canvas.shortcut.drag")}`} value={t("canvas.shortcut.pan")} />
|
<Shortcut label={`Ctrl / Space + ${t("canvas.shortcut.drag")}`} value={t("canvas.shortcut.toggleTool")} />
|
||||||
<Shortcut label={t("canvas.shortcut.wheel")} value={t("canvas.shortcut.zoom")} />
|
<Shortcut label={t("canvas.shortcut.wheel")} value={t("canvas.shortcut.zoom")} />
|
||||||
<Shortcut label={t("canvas.shortcut.drag")} value={t("canvas.shortcut.boxSelect")} />
|
<Shortcut label={t("canvas.shortcut.drag")} value={t("canvas.shortcut.boxSelect")} />
|
||||||
<Shortcut label={`Shift / Cmd + ${t("canvas.shortcut.click")}`} value={t("canvas.shortcut.addSelection")} />
|
<Shortcut label={`Shift / Cmd + ${t("canvas.shortcut.click")}`} value={t("canvas.shortcut.addSelection")} />
|
||||||
|
|||||||
@@ -50,11 +50,19 @@ export function InfiniteCanvas({ containerRef, viewport, tool, backgroundMode =
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === "Control") setIsControlPressed(true);
|
if (event.key === "Control") setIsControlPressed(true);
|
||||||
if (event.code === "Space" && !(event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement)) setIsSpacePressed(true);
|
if (event.code !== "Space") return;
|
||||||
|
const target = event.target instanceof Element ? event.target : null;
|
||||||
|
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement || event.target instanceof HTMLSelectElement || target?.closest("[contenteditable='true']")) return;
|
||||||
|
event.preventDefault();
|
||||||
|
setIsSpacePressed(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyUp = (event: KeyboardEvent) => {
|
const handleKeyUp = (event: KeyboardEvent) => {
|
||||||
if (event.code === "Space") setIsSpacePressed(false);
|
if (event.code === "Space") {
|
||||||
|
const target = event.target instanceof Element ? event.target : null;
|
||||||
|
if (!(event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement || event.target instanceof HTMLSelectElement || target?.closest("[contenteditable='true']"))) event.preventDefault();
|
||||||
|
setIsSpacePressed(false);
|
||||||
|
}
|
||||||
if (event.key === "Control") setIsControlPressed(false);
|
if (event.key === "Control") setIsControlPressed(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,7 +111,9 @@ export function InfiniteCanvas({ containerRef, viewport, tool, backgroundMode =
|
|||||||
if (target?.closest("[data-canvas-no-zoom]")) return;
|
if (target?.closest("[data-canvas-no-zoom]")) return;
|
||||||
if (target?.closest("[data-connection-create-menu]")) return;
|
if (target?.closest("[data-connection-create-menu]")) return;
|
||||||
const isBackgroundClick = !target?.closest("[data-node-id],[data-connection-id]");
|
const isBackgroundClick = !target?.closest("[data-node-id],[data-connection-id]");
|
||||||
const shouldPan = event.button === 1 || (event.button === 0 && (tool === "pan" || event.ctrlKey || isSpacePressed));
|
const temporaryTool = event.ctrlKey || isSpacePressed;
|
||||||
|
const activeTool = temporaryTool ? (tool === "select" ? "pan" : "select") : tool;
|
||||||
|
const shouldPan = event.button === 1 || (event.button === 0 && activeTool === "pan");
|
||||||
|
|
||||||
if (shouldPan) {
|
if (shouldPan) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -193,7 +203,9 @@ export function InfiniteCanvas({ containerRef, viewport, tool, backgroundMode =
|
|||||||
return () => container.removeEventListener("wheel", preventWheelScroll);
|
return () => container.removeEventListener("wheel", preventWheelScroll);
|
||||||
}, [containerRef]);
|
}, [containerRef]);
|
||||||
|
|
||||||
const cursor = isPanning ? "grabbing" : tool === "pan" || isControlPressed || isSpacePressed ? "grab" : undefined;
|
const temporaryTool = isControlPressed || isSpacePressed;
|
||||||
|
const activeTool = temporaryTool ? (tool === "select" ? "pan" : "select") : tool;
|
||||||
|
const cursor = isPanning ? "grabbing" : activeTool === "pan" ? "grab" : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ export default {
|
|||||||
shortcut: {
|
shortcut: {
|
||||||
dragCanvas: "Drag canvas",
|
dragCanvas: "Drag canvas",
|
||||||
pan: "Pan view",
|
pan: "Pan view",
|
||||||
|
toggleTool: "Temporarily switch Select / Move",
|
||||||
wheel: "Mouse wheel",
|
wheel: "Mouse wheel",
|
||||||
zoom: "Zoom canvas",
|
zoom: "Zoom canvas",
|
||||||
zoomSlider: "Zoom slider",
|
zoomSlider: "Zoom slider",
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ export default {
|
|||||||
shortcut: {
|
shortcut: {
|
||||||
dragCanvas: "拖动画布",
|
dragCanvas: "拖动画布",
|
||||||
pan: "平移视图",
|
pan: "平移视图",
|
||||||
|
toggleTool: "临时切换选择 / 移动",
|
||||||
wheel: "滚轮",
|
wheel: "滚轮",
|
||||||
zoom: "缩放画布",
|
zoom: "缩放画布",
|
||||||
zoomSlider: "缩放滑杆",
|
zoomSlider: "缩放滑杆",
|
||||||
|
|||||||
@@ -2867,7 +2867,7 @@ function InfiniteCanvasPage() {
|
|||||||
height: Math.abs(selectionBox.currentWorldY - selectionBox.startWorldY),
|
height: Math.abs(selectionBox.currentWorldY - selectionBox.startWorldY),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<rect width="100%" height="100%" fill={theme.canvas.selectionFill} stroke={theme.canvas.selectionStroke} strokeWidth={1.5 / viewport.k} strokeDasharray={`${10 / viewport.k} ${6 / viewport.k}`} />
|
<rect width="100%" height="100%" fill={theme.canvas.selectionFill} stroke={theme.canvas.selectionStroke} strokeOpacity={0.55} strokeWidth={1 / viewport.k} strokeDasharray={`${6 / viewport.k} ${4 / viewport.k}`} />
|
||||||
</svg>
|
</svg>
|
||||||
) : null}
|
) : null}
|
||||||
{pendingConnectionCreate ? <ConnectionCreateMenu pending={pendingConnectionCreate} onCreate={(type) => createConnectedNode(type, pendingConnectionCreate)} onClose={cancelPendingConnectionCreate} /> : null}
|
{pendingConnectionCreate ? <ConnectionCreateMenu pending={pendingConnectionCreate} onCreate={(type) => createConnectedNode(type, pendingConnectionCreate)} onClose={cancelPendingConnectionCreate} /> : null}
|
||||||
|
|||||||
Reference in New Issue
Block a user