fix: 背景默认锁定;补 ShapeMenu 的 close 绑定

背景铺满整个画板,不锁的话画布上就没有「空白处」了:想框选、想点空白取消选中,
实际都会抓住背景整个拖走(实测一次误拖就把底图挪出画板)。但也不能做成永久
不可选——用户对背景的核心需求正是「调整/缩放/裁剪一下」。锁定是可见且可撤销的:
图层面板那一行的锁图标点一下就解开,解开后拖拽缩放和普通图层完全一样。

走 layerLocked 而不是直接写 selectable:false,因为锁定态由 applyLocked 统一管,
自己写 selectable 会被 object:added 触发的 registerLayer 翻回去。

TopBar 没把 Dropdown 的 close 传给 ShapeMenu,选完形状下拉不收。
顺带删掉 useEditor.addRect:形状改成拖拽创建之后没有调用方了。
This commit is contained in:
2026-09-05 18:39:47 +08:00
parent 0c5bcc75e9
commit 0b7f84b071
3 changed files with 19 additions and 33 deletions
+2 -2
View File
@@ -168,8 +168,8 @@ async function scrollToExport() {
</button> </button>
</span> </span>
</template> </template>
<template #panel> <template #panel="{ close }">
<ShapeMenu /> <ShapeMenu :close="close" />
</template> </template>
</Dropdown> </Dropdown>
+17 -5
View File
@@ -15,11 +15,20 @@ import { capImageSize } from "./imageLimits";
* 关键实现约束(都是踩出来的,别再踩): * 关键实现约束(都是踩出来的,别再踩):
* - `current()` 每次现找,不要缓存引用——`loadFromJSON` 之后画布上是新对象, * - `current()` 每次现找,不要缓存引用——`loadFromJSON` 之后画布上是新对象,
* 缓存的引用会指向一个已经不在画布上的旧对象。 * 缓存的引用会指向一个已经不在画布上的旧对象。
* - **背景和普通图层一样可以点选、拖动、缩放。** 一开始做成了不可点选(防止点空白 * - **背景默认锁定(`layerLocked = true`)。**
* 处误选),但用户对背景的核心需求就是「调整 / 缩放 / 裁剪一下」,不可点选就没法 *
* 在画布上取景。要取消选中点画板外面的灰底就行——画布是无限工作区,画板外一大片 * 它铺满整个画板,不锁的话画布上就没有「空白处」了:想框选、想点空白取消选中,
* 都是空的。(顺带省掉一个坑:`selectable:false` 会被图层面板的解锁按钮 * 实际都会抓住背景把它整个拖走(2026-09-05 实测,一次误拖就把底图挪出画板)。
* `applyLocked(obj,false)` 翻回 true,得靠「最后再设」这种顺序技巧才压得住。) *
* 但也不能做成永久不可选——用户对背景的核心需求正是「调整 / 缩放 / 裁剪一下」。
* 锁定是**可见且可撤销**的:图层面板那一行的锁图标点一下就能解开,解开后拖拽
* 缩放和普通图层完全一样。
*
* 走 `layerLocked` 而不是直接写 `selectable: false`,是因为锁定态由
* `applyLocked()` 统一管(selectable / evented / lockMovement… 一整套),
* 而 `registerLayer()` 每次都会照 `layerLocked` 重设一遍。自己写 `selectable`
* 会被 `object:added` 触发的那次 `registerLayer` 翻回去,只能靠「最后再设」
* 这类顺序技巧压住,脆。
* - 铺满用 `width * scaleX` 而不是 `getScaledWidth()`:后者会把默认 strokeWidth * - 铺满用 `width * scaleX` 而不是 `getScaledWidth()`:后者会把默认 strokeWidth
* 也算进去(见 common 的 Fabric 陷阱 3)。 * 也算进去(见 common 的 Fabric 陷阱 3)。
*/ */
@@ -107,6 +116,8 @@ export function useBackground() {
const capped = capImageSize(img.getElement() as HTMLImageElement); const capped = capImageSize(img.getElement() as HTMLImageElement);
if (capped !== img.getElement()) img.setElement(capped); if (capped !== img.getElement()) img.setElement(capped);
img.isBackground = true; img.isBackground = true;
// 默认锁定,理由见文件头。registerLayer 会照这个标志跑一遍 applyLocked。
img.layerLocked = true;
fitToCanvas(img); fitToCanvas(img);
registerLayer(img); registerLayer(img);
c.add(img); c.add(img);
@@ -132,6 +143,7 @@ export function useBackground() {
fill: color, fill: color,
}); });
rect.isBackground = true; rect.isBackground = true;
rect.layerLocked = true;
registerLayer(rect); registerLayer(rect);
c.add(rect); c.add(rect);
c.sendObjectToBack(rect); c.sendObjectToBack(rect);
-26
View File
@@ -328,31 +328,6 @@ function addText(text = "标题"): Textbox {
return box; return box;
} }
/** 加一个矩形。圆角走 rx/ry,右栏的「圆角」改的就是它。 */
function addRect(at?: { x: number; y: number }): Rect {
const c = canvas.value;
if (!c) throw new Error("画布还没挂载");
const w = Math.round(size.value.width * 0.3);
const h = Math.round(size.value.height * 0.25);
const rect = new Rect({
left: at?.x ?? size.value.width / 2,
top: at?.y ?? size.value.height / 2,
originX: "center",
originY: "center",
width: w,
height: h,
fill: "#6ea8fe",
rx: 0,
ry: 0,
});
registerLayer(rect);
c.add(rect);
c.setActiveObject(rect);
c.requestRenderAll();
bumpRevision();
return rect;
}
function getCanvas(): Canvas { function getCanvas(): Canvas {
const c = canvas.value; const c = canvas.value;
if (!c) throw new Error("画布还没挂载"); if (!c) throw new Error("画布还没挂载");
@@ -380,7 +355,6 @@ export function useEditor() {
setSize, setSize,
addImage, addImage,
addText, addText,
addRect,
getCanvas, getCanvas,
selectedObjects, selectedObjects,
bumpRevision, bumpRevision,