chore: 本轮改造的共享契约与占位——ToolId/ShapeKind/LayerKind 扩充、Dropdown 壳、形状表、三个菜单与两个 composable 的桩

This commit is contained in:
2026-09-05 17:56:10 +08:00
parent 3a3798e37b
commit 19c8c604cb
17 changed files with 512 additions and 28 deletions
+63 -7
View File
@@ -1,6 +1,17 @@
import { Canvas, FabricImage, FabricObject, Rect, Textbox, type TPointerEvent } from "fabric";
import {
ActiveSelection,
Canvas,
Ellipse,
FabricImage,
FabricObject,
Line,
Polygon,
Rect,
Textbox,
type TPointerEvent,
} from "fabric";
import { ref, shallowRef } from "vue";
import type { CanvasSize, LayerKind, SelectionKind, ToolId } from "@/types";
import type { CanvasSize, LayerKind, SelectionKind, ShapeKind, ToolId } from "@/types";
import { DEFAULT_SIZE } from "./canvasSize";
/**
@@ -13,7 +24,7 @@ import { DEFAULT_SIZE } from "./canvasSize";
*/
/** 自定义属性要登记在这里,`canvas.toJSON()` 才会带上(fabric v7 的 customProperties 机制)。 */
FabricObject.customProperties = ["layerId", "layerName", "layerLocked"];
FabricObject.customProperties = ["layerId", "layerName", "layerLocked", "isBackground", "shapeKind"];
declare module "fabric" {
interface FabricObject {
@@ -23,6 +34,17 @@ declare module "fabric" {
layerName?: string;
/** 锁定:不可选中、不可移动。和 fabric 自带的 selectable/evented 联动。 */
layerLocked?: boolean;
/**
* 这是**那一个**专用背景图层。全画布最多一个,永远在最底层,
* 换背景是替换它而不是再加一层。归 `useBackground` 管,别处不要自己置位。
*/
isBackground?: boolean;
/**
* 形状工具画出来的形状是哪一种。fabric 的类推不出「箭头」——它就是一个
* Polygon/Path,和多边形同类;这个字段把用户的意图记下来,图层图标和
* 右栏属性都读它。
*/
shapeKind?: ShapeKind;
}
}
@@ -32,6 +54,8 @@ const selectionKind = ref<SelectionKind>("none");
const size = ref<CanvasSize>({ width: DEFAULT_SIZE.width, height: DEFAULT_SIZE.height });
/** 当前工具。`select` 是默认态。 */
const tool = ref<ToolId>("select");
/** 形状工具当前要画哪一种形状。工具栏形状按钮右边的下拉改它。 */
const shapeKind = ref<ShapeKind>("rect");
/**
* 画布内容的版本号。对象增删改、层序变化都会 +1。
* 图层面板、属性检查器 watch 它来重新推导自己的视图——
@@ -49,23 +73,54 @@ export function ensureLayerId(obj: FabricObject): string {
export function layerKindOf(obj: FabricObject | null | undefined): LayerKind {
if (!obj) return "other";
// 背景要先判:它本身是一张 FabricImage 或一个 Rect,光看类型分不出来。
if (obj.isBackground) return "background";
if (obj.shapeKind) return obj.shapeKind;
if (obj instanceof FabricImage) return "image";
if (obj instanceof Textbox) return "text";
if (obj instanceof Ellipse) return "ellipse";
if (obj instanceof Line) return "line";
if (obj instanceof Polygon) return "polygon";
if (obj instanceof Rect) return "rect";
return "other";
}
/** 这个种类算不算「形状」——形状共用同一组外观属性(填充、描边、圆角)。 */
export function isShapeKind(kind: LayerKind): boolean {
return kind === "rect" || kind === "line" || kind === "arrow" || kind === "ellipse" || kind === "polygon";
}
const KIND_NAME: Record<LayerKind, string> = {
background: "背景",
image: "图片",
text: "文字",
rect: "矩形",
line: "直线",
arrow: "箭头",
ellipse: "圆形",
polygon: "多边形",
other: "对象",
};
export function defaultLayerName(obj: FabricObject): string {
const kind = layerKindOf(obj);
if (kind === "text") return (obj as Textbox).text?.slice(0, 20) || "文字";
if (kind === "image") return "图片";
if (kind === "rect") return "矩形";
return "对象";
return KIND_NAME[kind];
}
/**
* 选中态:右栏据此决定显示哪几组属性。
*
* 五种形状塌成一个 `shape`——它们共用同一组外观属性,右栏没必要为每种各写一份。
* 背景图层归到 `image`:它就是一张图,滤镜之类的都适用。
*/
function kindOf(obj: FabricObject | null | undefined): SelectionKind {
if (!obj) return "none";
if (obj instanceof ActiveSelection) return "multi";
const k = layerKindOf(obj);
return k === "other" ? (obj ? "other" : "none") : k === "rect" ? "other" : k;
if (k === "background" || k === "image") return "image";
if (k === "text") return "text";
return isShapeKind(k) ? "shape" : "other";
}
function syncSelection() {
@@ -285,6 +340,7 @@ export function useEditor() {
selectionKind,
size,
tool,
shapeKind,
revision,
mount,
unmount,