Merge branch 't12-shapes'

This commit is contained in:
2026-09-05 18:28:59 +08:00
3 changed files with 441 additions and 26 deletions
+9 -6
View File
@@ -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();
+37 -2
View File
@@ -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>