Merge branch 't12-shapes'
This commit is contained in:
@@ -4,6 +4,7 @@ import { useEditor } from "@/editor/useEditor";
|
||||
import { useViewport } from "@/editor/useViewport";
|
||||
import { initHistory, commit } from "@/editor/useHistory";
|
||||
import { openContextMenu } from "@/editor/useContextMenu";
|
||||
import { attachShapeTool } from "@/editor/useShapeTool";
|
||||
|
||||
/**
|
||||
* 画布区。**画布元素的像素尺寸跟着容器走**(它是「视口」),
|
||||
@@ -16,7 +17,7 @@ import { openContextMenu } from "@/editor/useContextMenu";
|
||||
*
|
||||
* **这个文件由主控独占。**
|
||||
*/
|
||||
const { mount, unmount, getCanvas, tool, addText, addRect } = useEditor();
|
||||
const { mount, unmount, getCanvas, tool, addText } = useEditor();
|
||||
const { zoomAt, panBy, zoomToFit } = useViewport();
|
||||
|
||||
const canvasEl = ref<HTMLCanvasElement | null>(null);
|
||||
@@ -26,6 +27,7 @@ let spaceDown = false;
|
||||
let panning = false;
|
||||
let lastX = 0;
|
||||
let lastY = 0;
|
||||
let detachShapeTool: (() => void) | null = null;
|
||||
|
||||
/**
|
||||
* 视口尺寸跟着容器走。
|
||||
@@ -86,14 +88,12 @@ function onPointerDown(e: PointerEvent) {
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
return;
|
||||
}
|
||||
// 形状工具:拖拽创建由 useShapeTool 的 attachShapeTool 接管,这里完全让开。
|
||||
if (e.button === 0 && tool.value === "shape") return;
|
||||
|
||||
// 非选择工具:在点击处放一个新对象,然后回到选择工具(和 Figma 一致)
|
||||
if (e.button === 0 && tool.value !== "select") {
|
||||
const c = getCanvas();
|
||||
const p = c.getViewportPoint(e);
|
||||
const scene = c.getScenePoint(e);
|
||||
void p;
|
||||
if (tool.value === "text") addText();
|
||||
else if (tool.value === "shape") addRect({ x: scene.x, y: scene.y });
|
||||
if (tool.value !== "background") {
|
||||
tool.value = "select";
|
||||
commit();
|
||||
@@ -126,6 +126,8 @@ onMounted(() => {
|
||||
// 尺寸与首次适应交给 resize(),它会等到 stage 真的有尺寸了再做。
|
||||
resize();
|
||||
initHistory();
|
||||
// 形状工具的指针交互单独挂在画布上,这里只负责装/卸。
|
||||
detachShapeTool = attachShapeTool(c);
|
||||
// 拖拽 / 缩放 / 旋转结束都算一步操作。
|
||||
c.on("object:modified", () => commit());
|
||||
observer = new ResizeObserver(resize);
|
||||
@@ -136,6 +138,7 @@ onMounted(() => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
observer?.disconnect();
|
||||
detachShapeTool?.();
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
window.removeEventListener("keyup", onKeyUp);
|
||||
unmount();
|
||||
|
||||
@@ -1,7 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
/** 【占位】ShapeMenu 的内容由对应开发线实现。见任务书。 */
|
||||
import { useEditor } from "@/editor/useEditor";
|
||||
import { runCommand } from "@/editor/commands";
|
||||
import { SHAPES } from "@/editor/shapes";
|
||||
import { IconCheck } from "@/ui/icons";
|
||||
import type { ShapeKind } from "@/types";
|
||||
|
||||
/**
|
||||
* 形状下拉的面板内容。五项 `.menu-row`,图标 + 名字来自 `SHAPES`。
|
||||
* 当前形状高亮(`.on`)并在右侧显示对勾。
|
||||
*
|
||||
* 点一项:把 `shapeKind` 切过去,再 `runCommand("toolShape")` 切到形状工具,
|
||||
* 然后关掉下拉。组件本身**不**包 Dropdown——T11 在 TopBar 里包,这里只提供
|
||||
* 一列行,所以关闭通过 `close`(prop 或 `close` 事件,两种接法都兼容)交还给它。
|
||||
*/
|
||||
const { shapeKind } = useEditor();
|
||||
|
||||
const props = withDefaults(defineProps<{ close?: () => void }>(), { close: () => {} });
|
||||
const emit = defineEmits<{ close: [] }>();
|
||||
|
||||
function pick(kind: ShapeKind) {
|
||||
shapeKind.value = kind;
|
||||
runCommand("toolShape");
|
||||
props.close?.();
|
||||
emit("close");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="menu-block">(ShapeMenu 待实现)</div>
|
||||
<button
|
||||
v-for="s in SHAPES"
|
||||
:key="s.kind"
|
||||
type="button"
|
||||
class="menu-row"
|
||||
:class="{ on: shapeKind === s.kind }"
|
||||
@click="pick(s.kind)"
|
||||
>
|
||||
<component :is="s.icon" />
|
||||
<span>{{ s.label }}</span>
|
||||
<IconCheck v-if="shapeKind === s.kind" class="menu-aside" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
+395
-18
@@ -1,24 +1,401 @@
|
||||
import type { Canvas } from "fabric";
|
||||
import { Ellipse, Line, Path, Polygon, Rect, type Canvas, type FabricObject, type TSimplePathData } from "fabric";
|
||||
import { watch } from "vue";
|
||||
import { useEditor } from "./useEditor";
|
||||
import { commit } from "./useHistory";
|
||||
import type { ShapeKind, ToolId } from "@/types";
|
||||
|
||||
/**
|
||||
* 【T12 待实现】形状工具的拖拽创建。
|
||||
* 形状工具的拖拽创建。按住左键从起点拖到终点,五种形状共用一条路径。
|
||||
*
|
||||
* 现状(要被替换掉的):Workspace 里点一下画布就在点击处放一个固定大小的矩形。
|
||||
* 目标:按住左键从起点拖到终点松开,拖出多大就是多大,五种形状都走这一条路径。
|
||||
* 交互模型(和 Figma 一致):
|
||||
* - pointerdown:在起点放一个对象(此时尺寸为 0),登记成图层。
|
||||
* - pointermove:实时改对象的几何,`requestRenderAll()` + `bumpRevision()`,
|
||||
* **不**在这期间 `commit()`——一次拖拽只记一步历史。
|
||||
* - pointerup:距离 < 阈值当误触,改放一个默认尺寸的形状;否则选中新对象、
|
||||
* 回到 select 工具、`commit()`。
|
||||
*
|
||||
* 契约:
|
||||
* - `attachShapeTool(canvas)` 在画布挂载后调一次,返回解绑函数,卸载时调。
|
||||
* - 只在 `useEditor().tool === "shape"` 时接管指针事件;其余时间完全让开,
|
||||
* 不要影响选择/平移。
|
||||
* - 画什么看 `useEditor().shapeKind`。
|
||||
* - 拖拽过程中要有实时预览(把对象加进画布边拖边改,松手才 `commit()` 记一步历史)。
|
||||
* - **按住 Shift 约束成 1:1**(正方形 / 正圆 / 45° 倍数的直线和箭头)。
|
||||
* 注意这只管**新建**;已有对象缩放时的 Shift 语义保持现状,不要动。
|
||||
* - 松手后回到 `select` 工具并选中新对象(和 Figma 一致)。
|
||||
* - 拖拽距离小于阈值(约 4px)当作误触:放一个默认尺寸的形状,不要留下一个 0×0 的东西。
|
||||
* - 新对象要 `registerLayer()`,并把 `obj.shapeKind` 置成当前形状——
|
||||
* 箭头和多边形在 fabric 里都是 Polygon,光看类型分不出来。
|
||||
* Shift 的语义是「新建时约束比例」:矩形/圆/多边形约束到 1:1,直线/箭头
|
||||
* 吸附到 45° 的倍数。这只管**新建**;已有对象缩放时的 Shift 行为由 fabric
|
||||
* 自己决定,这里不动。
|
||||
*
|
||||
* 画布接管:形状工具激活期间把 `skipTargetFind` / `selection` 关掉,让 fabric
|
||||
* 的 pointerdown 不选对象、不起框选(否则在已有对象上起笔会先去拖动它)。
|
||||
* 切回其它工具时恢复原值。watch 用默认的 pre flush,工具一换下一帧就生效,
|
||||
* 足够赶在用户下一次点画布之前。
|
||||
*/
|
||||
export function attachShapeTool(_canvas: Canvas): () => void {
|
||||
return () => {};
|
||||
|
||||
const FILL = "#6ea8fe";
|
||||
const STROKE = "#6ea8fe";
|
||||
const STROKE_WIDTH = 4;
|
||||
/** 小于这个位移(屏幕像素)当误触,放默认尺寸而不是留下 0×0 的对象。 */
|
||||
const MIN_DRAG = 4;
|
||||
/** 箭头三角头的轴向长度 / 半宽。头大小固定,跟杆长无关;整体缩放时才一起变。 */
|
||||
const ARROW_HEAD_LEN = 12;
|
||||
const ARROW_HEAD_HALF = 6;
|
||||
|
||||
const ed = useEditor();
|
||||
|
||||
interface Pt {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface Box {
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
}
|
||||
|
||||
interface DragState {
|
||||
obj: FabricObject;
|
||||
kind: ShapeKind;
|
||||
startX: number;
|
||||
startY: number;
|
||||
/** 误触判定的起点用屏幕坐标:4px 指的是指针在屏幕上的位移,不是场景位移(受缩放影响)。 */
|
||||
startClientX: number;
|
||||
startClientY: number;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------ 几何:从两点推形状 */
|
||||
|
||||
/**
|
||||
* 矩形/圆/多边形的包围盒。Shift 约束成正方形:以起点为锚,边长取较长的那个分量,
|
||||
* 方向跟着拖拽走(`sign(0)` 归到正方向,纯竖直拖也会得到一个正方形)。
|
||||
*/
|
||||
function boxFrom(a: Pt, b: Pt, constrain: boolean): Box {
|
||||
let dx = b.x - a.x;
|
||||
let dy = b.y - a.y;
|
||||
if (constrain) {
|
||||
const side = Math.max(Math.abs(dx), Math.abs(dy));
|
||||
dx = (dx >= 0 ? 1 : -1) * side;
|
||||
dy = (dy >= 0 ? 1 : -1) * side;
|
||||
}
|
||||
return {
|
||||
minX: Math.min(a.x, a.x + dx),
|
||||
minY: Math.min(a.y, a.y + dy),
|
||||
maxX: Math.max(a.x, a.x + dx),
|
||||
maxY: Math.max(a.y, a.y + dy),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 直线/箭头的终点。Shift 时把方向角吸附到最近的 45° 倍数,长度保持拖拽距离。
|
||||
*/
|
||||
function snappedEnd(a: Pt, b: Pt, constrain: boolean): Pt {
|
||||
if (!constrain) return { x: b.x, y: b.y };
|
||||
const dx = b.x - a.x;
|
||||
const dy = b.y - a.y;
|
||||
const len = Math.hypot(dx, dy);
|
||||
if (len < 1e-6) return { x: a.x, y: a.y };
|
||||
const deg = (Math.atan2(dy, dx) * 180) / Math.PI;
|
||||
const snapped = Math.round(deg / 45) * 45;
|
||||
const rad = (snapped * Math.PI) / 180;
|
||||
return { x: a.x + len * Math.cos(rad), y: a.y + len * Math.sin(rad) };
|
||||
}
|
||||
|
||||
/* ------------------------------------------------ 五种形状的构建 / 更新 */
|
||||
|
||||
function makeRect(box: Box): Rect {
|
||||
// fabric 的默认原点是中心,显式写 left/top 原点,让 left/top 就是包围盒左上角。
|
||||
return new Rect({
|
||||
left: box.minX,
|
||||
top: box.minY,
|
||||
originX: "left",
|
||||
originY: "top",
|
||||
width: box.maxX - box.minX,
|
||||
height: box.maxY - box.minY,
|
||||
fill: FILL,
|
||||
rx: 0,
|
||||
ry: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function updateRect(obj: Rect, box: Box) {
|
||||
obj.set({
|
||||
left: box.minX,
|
||||
top: box.minY,
|
||||
width: box.maxX - box.minX,
|
||||
height: box.maxY - box.minY,
|
||||
});
|
||||
obj.setCoords();
|
||||
}
|
||||
|
||||
function makeEllipse(box: Box): Ellipse {
|
||||
// rx/ry 是半轴,宽高各取一半。
|
||||
return new Ellipse({
|
||||
left: box.minX,
|
||||
top: box.minY,
|
||||
originX: "left",
|
||||
originY: "top",
|
||||
rx: (box.maxX - box.minX) / 2,
|
||||
ry: (box.maxY - box.minY) / 2,
|
||||
fill: FILL,
|
||||
});
|
||||
}
|
||||
|
||||
function updateEllipse(obj: Ellipse, box: Box) {
|
||||
obj.set({
|
||||
left: box.minX,
|
||||
top: box.minY,
|
||||
rx: (box.maxX - box.minX) / 2,
|
||||
ry: (box.maxY - box.minY) / 2,
|
||||
});
|
||||
obj.setCoords();
|
||||
}
|
||||
|
||||
/**
|
||||
* 三角形(polygon,边数固定 3):外接于包围盒的等腰三角形,顶点在上、底边在下。
|
||||
* Polygon 的 points 是相对对象中心的局部坐标,left/top 是包围盒中心。
|
||||
*/
|
||||
function makeTriangle(box: Box): Polygon {
|
||||
const w = box.maxX - box.minX;
|
||||
const h = box.maxY - box.minY;
|
||||
return new Polygon(
|
||||
[
|
||||
{ x: 0, y: -h / 2 },
|
||||
{ x: -w / 2, y: h / 2 },
|
||||
{ x: w / 2, y: h / 2 },
|
||||
],
|
||||
{
|
||||
left: (box.minX + box.maxX) / 2,
|
||||
top: (box.minY + box.maxY) / 2,
|
||||
fill: FILL,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function updateTriangle(obj: Polygon, box: Box) {
|
||||
const w = box.maxX - box.minX;
|
||||
const h = box.maxY - box.minY;
|
||||
obj.set({
|
||||
left: (box.minX + box.maxX) / 2,
|
||||
top: (box.minY + box.maxY) / 2,
|
||||
points: [
|
||||
{ x: 0, y: -h / 2 },
|
||||
{ x: -w / 2, y: h / 2 },
|
||||
{ x: w / 2, y: h / 2 },
|
||||
],
|
||||
});
|
||||
// points 改动后 width/height/pathOffset 不会自动重算(除非 exactBoundingBox),手动补一次。
|
||||
obj.setDimensions();
|
||||
obj.setCoords();
|
||||
}
|
||||
|
||||
function makeLine(a: Pt, b: Pt): Line {
|
||||
// Line 的 left/top 由 x1/y1/x2/y2 自动居中,不用(也不该)手填。
|
||||
return new Line([a.x, a.y, b.x, b.y], {
|
||||
stroke: STROKE,
|
||||
strokeWidth: STROKE_WIDTH,
|
||||
strokeLineCap: "round",
|
||||
});
|
||||
}
|
||||
|
||||
function updateLine(obj: Line, a: Pt, b: Pt) {
|
||||
// Line 的 _set 会在坐标变化时重算宽高并重新居中,所以直接塞坐标即可。
|
||||
obj.set({ x1: a.x, y1: a.y, x2: b.x, y2: b.y });
|
||||
obj.setCoords();
|
||||
}
|
||||
|
||||
/**
|
||||
* 箭头用单个 Path 画:杆是一条开子路径(零面积,只有描边),头是一个闭合三角
|
||||
* (有面积,靠 fill 填色)。
|
||||
*
|
||||
* 选 Path 而不是 Group 的理由:
|
||||
* - 单对象 → 单包围盒 → 选中/旋转/缩放的把手都作用在这一个对象上,不用处理
|
||||
* Group 的子对象在局部坐标里的重新定位。
|
||||
* - `toJSON()` / `loadFromJSON()` 原样往返:它就是一个带 `shapeKind` 自定义属性的
|
||||
* Path,加载时不需要任何重建逻辑。
|
||||
* - 头的尺寸在**创建时**固定(和杆长无关,跟真实标注箭头一致);用户之后缩放
|
||||
* 整体时,头和杆按同一比例一起变,不会出现「头被单独拉伸」的走样。
|
||||
*/
|
||||
function makeArrow(a: Pt, b: Pt): Path {
|
||||
return new Path(arrowCommands(a, b), {
|
||||
fill: STROKE,
|
||||
stroke: STROKE,
|
||||
strokeWidth: STROKE_WIDTH,
|
||||
strokeLineCap: "round",
|
||||
strokeLineJoin: "round",
|
||||
});
|
||||
}
|
||||
|
||||
function updateArrow(obj: Path, a: Pt, b: Pt) {
|
||||
obj.set({ path: arrowCommands(a, b) });
|
||||
// Path 改 path 后要手动重算包围盒并重新定位到新中心。
|
||||
obj.setBoundingBox(true);
|
||||
obj.setCoords();
|
||||
}
|
||||
|
||||
/** 生成箭头的 SVG path 命令。坐标是场景坐标(Path 会按包围盒中心重新定位)。 */
|
||||
function arrowCommands(a: Pt, end: Pt): TSimplePathData {
|
||||
const dx = end.x - a.x;
|
||||
const dy = end.y - a.y;
|
||||
const len = Math.hypot(dx, dy);
|
||||
if (len < 1e-6) {
|
||||
// 零长度:先占住一个退化对象,移动时再补真路径。
|
||||
return [
|
||||
["M", a.x, a.y],
|
||||
["L", a.x, a.y],
|
||||
];
|
||||
}
|
||||
// 短到放不下头时把头长压进全长,别让头比杆还长。
|
||||
const hl = Math.min(ARROW_HEAD_LEN, len);
|
||||
const cos = dx / len;
|
||||
const sin = dy / len;
|
||||
const shaftX = a.x + (len - hl) * cos;
|
||||
const shaftY = a.y + (len - hl) * sin;
|
||||
// 三角底边两个顶点:底边中点在杆末端,沿杆的垂直方向各偏半个头宽。
|
||||
const base1X = shaftX - ARROW_HEAD_HALF * sin;
|
||||
const base1Y = shaftY + ARROW_HEAD_HALF * cos;
|
||||
const base2X = shaftX + ARROW_HEAD_HALF * sin;
|
||||
const base2Y = shaftY - ARROW_HEAD_HALF * cos;
|
||||
return [
|
||||
["M", a.x, a.y],
|
||||
["L", shaftX, shaftY],
|
||||
["M", base1X, base1Y],
|
||||
["L", end.x, end.y],
|
||||
["L", base2X, base2Y],
|
||||
["Z"],
|
||||
];
|
||||
}
|
||||
|
||||
/* ------------------------------------------------ 组装 */
|
||||
|
||||
function createShape(kind: ShapeKind, a: Pt, b: Pt, constrain: boolean): FabricObject {
|
||||
if (kind === "line") return makeLine(a, snappedEnd(a, b, constrain));
|
||||
if (kind === "arrow") return makeArrow(a, snappedEnd(a, b, constrain));
|
||||
const box = boxFrom(a, b, constrain);
|
||||
if (kind === "rect") return makeRect(box);
|
||||
if (kind === "ellipse") return makeEllipse(box);
|
||||
return makeTriangle(box);
|
||||
}
|
||||
|
||||
function updateShape(obj: FabricObject, kind: ShapeKind, a: Pt, b: Pt, constrain: boolean) {
|
||||
if (kind === "line") return updateLine(obj as Line, a, snappedEnd(a, b, constrain));
|
||||
if (kind === "arrow") return updateArrow(obj as Path, a, snappedEnd(a, b, constrain));
|
||||
const box = boxFrom(a, b, constrain);
|
||||
if (kind === "rect") return updateRect(obj as Rect, box);
|
||||
if (kind === "ellipse") return updateEllipse(obj as Ellipse, box);
|
||||
return updateTriangle(obj as Polygon, box);
|
||||
}
|
||||
|
||||
/**
|
||||
* 误触时放一个默认尺寸的形状(宽约画板的 30%),以点击处为中心。
|
||||
* 直线/箭头默认是一条水平线,其余是居中的矩形框。
|
||||
*/
|
||||
function placeDefault(canvas: Canvas, kind: ShapeKind, center: Pt) {
|
||||
const w = Math.round(ed.size.value.width * 0.3);
|
||||
const h = Math.round(ed.size.value.height * 0.25);
|
||||
let obj: FabricObject;
|
||||
if (kind === "line" || kind === "arrow") {
|
||||
obj = createShape(kind, { x: center.x - w / 2, y: center.y }, { x: center.x + w / 2, y: center.y }, false);
|
||||
} else {
|
||||
obj = createShape(
|
||||
kind,
|
||||
{ x: center.x - w / 2, y: center.y - h / 2 },
|
||||
{ x: center.x + w / 2, y: center.y + h / 2 },
|
||||
false,
|
||||
);
|
||||
}
|
||||
obj.shapeKind = kind;
|
||||
ed.registerLayer(obj);
|
||||
canvas.add(obj);
|
||||
canvas.setActiveObject(obj);
|
||||
canvas.requestRenderAll();
|
||||
ed.bumpRevision();
|
||||
commit();
|
||||
ed.tool.value = "select";
|
||||
}
|
||||
|
||||
/* ------------------------------------------------ 事件接管 */
|
||||
|
||||
export function attachShapeTool(canvas: Canvas): () => void {
|
||||
const el = canvas.upperCanvasEl;
|
||||
const doc = el.ownerDocument ?? document;
|
||||
let drag: DragState | null = null;
|
||||
|
||||
// 记下挂载时的交互开关,卸载 / 切回其它工具时原样恢复,别写死默认值。
|
||||
const origSkipTargetFind = canvas.skipTargetFind;
|
||||
const origSelection = canvas.selection;
|
||||
const origDefaultCursor = canvas.defaultCursor;
|
||||
|
||||
function scenePoint(e: PointerEvent): Pt {
|
||||
const p = canvas.getScenePoint(e);
|
||||
return { x: p.x, y: p.y };
|
||||
}
|
||||
|
||||
function onDown(e: PointerEvent) {
|
||||
if (ed.tool.value !== "shape" || e.button !== 0) return;
|
||||
const p = scenePoint(e);
|
||||
const kind = ed.shapeKind.value;
|
||||
const obj = createShape(kind, p, p, e.shiftKey);
|
||||
// 自定义属性要在加进画布前置好:registerLayer 用 defaultLayerName 起名,靠它分五种。
|
||||
obj.shapeKind = kind;
|
||||
ed.registerLayer(obj);
|
||||
canvas.add(obj);
|
||||
drag = { obj, kind, startX: p.x, startY: p.y, startClientX: e.clientX, startClientY: e.clientY };
|
||||
canvas.requestRenderAll();
|
||||
ed.bumpRevision();
|
||||
}
|
||||
|
||||
function onMove(e: PointerEvent) {
|
||||
if (!drag) return;
|
||||
const cur = scenePoint(e);
|
||||
updateShape(drag.obj, drag.kind, { x: drag.startX, y: drag.startY }, cur, e.shiftKey);
|
||||
canvas.requestRenderAll();
|
||||
ed.bumpRevision();
|
||||
}
|
||||
|
||||
function onUp(e: PointerEvent) {
|
||||
if (!drag) return;
|
||||
const d = drag;
|
||||
drag = null;
|
||||
const cur = scenePoint(e);
|
||||
const dist = Math.hypot(e.clientX - d.startClientX, e.clientY - d.startClientY);
|
||||
if (dist < MIN_DRAG) {
|
||||
canvas.remove(d.obj);
|
||||
placeDefault(canvas, d.kind, { x: d.startX, y: d.startY });
|
||||
return;
|
||||
}
|
||||
// 用松手点做最后一次定位,避免最后一个 move 事件比 up 慢半拍。
|
||||
updateShape(d.obj, d.kind, { x: d.startX, y: d.startY }, cur, e.shiftKey);
|
||||
canvas.setActiveObject(d.obj);
|
||||
canvas.requestRenderAll();
|
||||
ed.bumpRevision();
|
||||
commit();
|
||||
ed.tool.value = "select";
|
||||
}
|
||||
|
||||
function applyMode(tool: ToolId) {
|
||||
// 形状工具期间让 fabric 完全让开:不找目标、不框选;否则在已有对象上起笔会去拖动它。
|
||||
if (tool === "shape") {
|
||||
canvas.skipTargetFind = true;
|
||||
canvas.selection = false;
|
||||
canvas.defaultCursor = "crosshair";
|
||||
canvas.setCursor("crosshair");
|
||||
} else {
|
||||
canvas.skipTargetFind = origSkipTargetFind;
|
||||
canvas.selection = origSelection;
|
||||
canvas.defaultCursor = origDefaultCursor;
|
||||
canvas.setCursor("default");
|
||||
}
|
||||
}
|
||||
|
||||
el.addEventListener("pointerdown", onDown);
|
||||
// move/up 挂在 document 上:指针拖出画布元素也能收到,up 不丢。
|
||||
doc.addEventListener("pointermove", onMove);
|
||||
doc.addEventListener("pointerup", onUp);
|
||||
doc.addEventListener("pointercancel", onUp);
|
||||
|
||||
const stopWatch = watch(() => ed.tool.value, applyMode, { immediate: true });
|
||||
|
||||
return () => {
|
||||
el.removeEventListener("pointerdown", onDown);
|
||||
doc.removeEventListener("pointermove", onMove);
|
||||
doc.removeEventListener("pointerup", onUp);
|
||||
doc.removeEventListener("pointercancel", onUp);
|
||||
stopWatch();
|
||||
// 卸载时兜底恢复 fabric 默认交互,别把画布留在「不能选」的状态。
|
||||
applyMode("select");
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user