From b050af02670d5560edba011fac881ca9cf25ad30 Mon Sep 17 00:00:00 2001 From: shay7sev Date: Sat, 18 Apr 2026 21:13:17 +0800 Subject: [PATCH] fix(label): selected path add point --- 5168f53_polygon_brush_point_edit_fix.md | 91 ++++++++++++++++++++++ components/label/usePaperStore.ts | 59 +++++++++++--- components/label/utils/objectVisibility.ts | 33 +++++--- 3 files changed, 162 insertions(+), 21 deletions(-) create mode 100644 5168f53_polygon_brush_point_edit_fix.md diff --git a/5168f53_polygon_brush_point_edit_fix.md b/5168f53_polygon_brush_point_edit_fix.md new file mode 100644 index 0000000..c8fdf90 --- /dev/null +++ b/5168f53_polygon_brush_point_edit_fix.md @@ -0,0 +1,91 @@ +# 5168f53 回归分析与修复记录 + +## 问题描述 +- 提交:`5168f539e0dbf84d6a1625d10ef98149fb21ffe6` +- 现象:`polygon` / `brush` / `point` 类型对象在被选中后,无法通过鼠标悬浮边触发边高亮,也无法通过双击边新增点。 + +## usePaperStore 关键链路梳理 +1. 选中对象 + - 入口主要在 `components/label/usePaperStore.ts` 的 `initPanTool -> panTool.onMouseDown` + - 通过 `resolvePanHitResult()` 命中对象、辅助边(`pathBuff`)或辅助点(`pathCircle`) + - 选中后会调用: + - `usePaperSupportStore.handleBufferPaths()` 生成可 hover / 可双击的辅助边 + - `usePaperSupportStore.handleCircles()` 生成可拖拽节点 + - `usePaperSupportStore.handleMask()` / `handleText()` 生成刷子/点类型附加辅助层 + +2. 悬浮边高亮 + - `usePaperSupportStore.handleBufferPaths()` 会遍历 path 的 segment,调用 `usePaperStore.getBuffPath()` 为每条边创建 `pathBuff` + - `getBuffPath()` 中给 `bufferPath` 绑定 `onMouseEnter / onMouseLeave` + - 悬浮时通过额外创建 `pathBuffHover` 白色描边来实现高亮 + - 同时把当前 hover 信息记入 `hoveredBufferPathInfo` + +3. 双击边新增点 + - 仍然在 `panTool.onMouseDown` + - 双击判定由 `isPaperDoubleClick()` 完成 + - 命中 `pathBuff` 或存在 `hoveredBufferPathInfo` 时,会走 `stroke` 分支或 `hoveredBufferInfo` 分支 + - 最终通过 `path.insert(hitIndex + 1, point)` 新增节点 + +4. 右侧/外部选择对象后的恢复链路 + - `components/label/utils/objectVisibility.ts` 的 `syncSelectedObjectsVisualState()` 负责根据 `selectedPath` 重新恢复画布上的选中态 + - 该链路也必须正确恢复: + - 选中的实际可编辑 path + - 辅助边 `pathBuff` + - 辅助点 `pathCircle` + - 否则对象虽“被选中”,但无法进入边 hover / 双击增点链路 + +## 回归根因 +### 根因 1:辅助边被改成完全透明,导致命中/hover 链路失效 +- 在 `getBuffPath()` 中,`pathBuff.strokeColor` 被改成了完全透明的颜色 +- 这会导致 `pathBuff` 在某些情况下不再稳定触发 hover / hit 行为 +- 后果: + - `hoveredBufferPathInfo` 无法建立 + - 边高亮不出现 + - 双击新增点依赖的 `pathBuff` 命中链路失效 + +### 根因 2:选中态恢复时,没有稳定选中“真正可编辑的 path” +- `polygon` / `brush` / `point` 可能存在 `CompoundPath + child Path` 的结构 +- 之前恢复选中态时,更多是“对象被选中”,但没有保证“某个可编辑 child path 被明确选中并生成辅助边/辅助点” +- 后果: + - 外部选中/恢复选中后,缺少 `pathBuff` + - 即使对象看起来是选中状态,也无法 hover 边、双击增点 + +## 修复方案 +### 已做 +- [x] 在 `components/label/usePaperStore.ts` 中新增 `ensureEditablePaperPathSelected()` + - 统一从同一对象 id 下挑出一个真实可编辑的 `paper.Path` + - 排除 `isHollowPolygon` + - 保证只有该可编辑 path 维持 `selected = true` + +- [x] 修复 `applyPaperSelectedPath()` + - 对 `polygon` / `brush` / `point` 不再只依赖当前 item 是否刚好是 `paper.Path` + - 改为统一拿到可编辑 path 后恢复: + - `handleBufferPaths` + - `handleCircles` + - `handleMask` + - `handleText` + +- [x] 修复 `components/label/utils/objectVisibility.ts` 中的 `applySelectedItemVisual()` + - 保证右侧面板/隐藏恢复/重绘恢复时,同样能恢复到真实可编辑 path 的辅助编辑态 + +- [x] 修复 `getBuffPath()` 的命中可用性 + - 不再使用 alpha 为 `0` 的完全透明描边 + - 改为极低透明度(`alpha = 0.001`)以保留 hover/hit 能力,同时视觉上保持不可见 + +- [x] 完成静态校验 + - 执行 `pnpm exec tsc --noEmit` + - 当前补丁未引入新的 TypeScript 类型错误 + +## 待验证 +- [ ] 直接点击已存在 `polygon` 对象后,悬浮边是否出现高亮 +- [ ] 直接点击已存在 `polygon` 对象后,双击边是否成功新增点 +- [ ] 直接点击已存在 `brush` 对象后,悬浮边是否出现高亮 +- [ ] 直接点击已存在 `brush` 对象后,双击边是否成功新增点 +- [ ] 直接点击已存在 `point` 对象后,悬浮边是否出现高亮 +- [ ] 直接点击已存在 `point` 对象后,双击边是否成功新增点 +- [ ] 从右侧对象列表切换选中后,上述三类对象是否仍可编辑 +- [ ] 多区域/CompoundPath 对象时,是否总能命中当前可编辑子路径 + +## 涉及文件 +- `components/label/usePaperStore.ts` +- `components/label/utils/objectVisibility.ts` +- `5168f53_polygon_brush_point_edit_fix.md` diff --git a/components/label/usePaperStore.ts b/components/label/usePaperStore.ts index caff1dd..3192f3d 100644 --- a/components/label/usePaperStore.ts +++ b/components/label/usePaperStore.ts @@ -899,6 +899,28 @@ const isPaperObjectType = (type: unknown) => typeof type === "string" && PAPER_OBJECT_TYPES.includes(type as (typeof PAPER_OBJECT_TYPES)[number]) +export const ensureEditablePaperPathSelected = (objectId: number) => { + const objectPaths = usePaperStore + .getState() + .getItemsById(objectId) + .filter( + (item): item is paper.Path => + item instanceof paper.Path && isPaperObjectType(item.data?.type) + ) + + if (!objectPaths.length) return null + + const editablePaths = objectPaths.filter((item) => !item.data?.isHollowPolygon) + const activePath = + editablePaths.find((item) => item.data?.selected) || editablePaths[0] || null + + objectPaths.forEach((item) => { + item.data.selected = item === activePath + }) + + return activePath +} + const isPaperContextHitType = (type: unknown) => typeof type === "string" && PAPER_CONTEXT_HIT_TYPES.includes( @@ -1001,6 +1023,13 @@ const resetPaperShapeFillColor = (item: paper.Item) => { } const applyPaperSelectedPath = (item: paper.Path | paper.CompoundPath) => { + const objectId = Number(item.data?.id) + const editablePath = Number.isFinite(objectId) + ? ensureEditablePaperPathSelected(objectId) + : item instanceof paper.Path && !item.data?.isHollowPolygon + ? item + : null + if (item.data.type === "rectangle" && item instanceof paper.Path) { item.data.selected = true usePaperSupportStore.getState().handleBufferPaths(item) @@ -1008,19 +1037,25 @@ const applyPaperSelectedPath = (item: paper.Path | paper.CompoundPath) => { item.fillColor = item.data.fillColor || item.fillColor || null } else if (item.data.type === "brush") { item.data.selected = true - if (item instanceof paper.Path) { - usePaperSupportStore.getState().handleMask(item) - usePaperSupportStore.getState().handleBufferPaths(item) - usePaperSupportStore.getState().handleCircles(item) + if (editablePath) { + usePaperSupportStore.getState().handleMask(editablePath) + usePaperSupportStore.getState().handleBufferPaths(editablePath) + usePaperSupportStore.getState().handleCircles(editablePath) } - } else if (item.data.type === "point" && item instanceof paper.Path) { + } else if (item.data.type === "point") { item.data.selected = true - usePaperSupportStore.getState().handleMask(item) - usePaperSupportStore.getState().handleText(item) - usePaperSupportStore.getState().handleBufferPaths(item) - usePaperSupportStore.getState().handleCircles(item) + if (editablePath) { + usePaperSupportStore.getState().handleMask(editablePath) + usePaperSupportStore.getState().handleText(editablePath) + usePaperSupportStore.getState().handleBufferPaths(editablePath) + usePaperSupportStore.getState().handleCircles(editablePath) + } } else if (item.data.type === "polygon") { item.data.selected = true + if (editablePath) { + usePaperSupportStore.getState().handleBufferPaths(editablePath) + usePaperSupportStore.getState().handleCircles(editablePath) + } if (!(item instanceof paper.Path) || !item.data?.isHollowPolygon) { item.fillColor = item.data.fillColor || item.fillColor || null } @@ -6653,15 +6688,17 @@ export const usePaperStore = create((set) => ({ item: paper.Segment, index: number, id: number, - _color: any + color: any ) => { if (!item.next) return null + const hitStrokeColor = clonePaperColor(color) || new paper.Color("#fff") + hitStrokeColor.alpha = 0.001 let bufferPath: paper.Path = new paper.Path( Object.assign( { parent: usePaperStore.getState().group!, segments: [item.point, item.next], - strokeColor: new paper.Color(0, 0, 0, 0), // 保留 hit 区域,但本体不可见 + strokeColor: hitStrokeColor, // 透明度不能为 0,否则部分命中/hover 事件会失效 strokeScaling: false, strokeWidth: 6, // 适度放宽 hover 区域,但避免干扰对象外默认点击 strokeCap: "round", diff --git a/components/label/utils/objectVisibility.ts b/components/label/utils/objectVisibility.ts index 21b526b..9b43cf1 100644 --- a/components/label/utils/objectVisibility.ts +++ b/components/label/utils/objectVisibility.ts @@ -1,7 +1,7 @@ import paper from "paper" import { useLabelStore, useObjectStore } from "../store" import { useOtherToolsStore } from "../useOtherToolsStore" -import { usePaperStore } from "../usePaperStore" +import { ensureEditablePaperPathSelected, usePaperStore } from "../usePaperStore" import { usePaperSupportStore } from "../usePaperSupportStore" import { useTopToolsStore } from "../useTopToolsStore" @@ -93,6 +93,13 @@ const applySelectedItemVisual = (item: paper.Item) => { if (!["rectangle", "polygon", "brush", "point"].includes(itemType || "")) return + const objectId = Number(item.data?.id) + const editablePath = Number.isFinite(objectId) + ? ensureEditablePaperPathSelected(objectId) + : item instanceof paper.Path && !item.data?.isHollowPolygon + ? item + : null + if (itemType === "rectangle" && item instanceof paper.Path) { item.data.selected = true usePaperSupportStore.getState().handleBufferPaths(item) @@ -100,19 +107,25 @@ const applySelectedItemVisual = (item: paper.Item) => { item.fillColor = item.data.fillColor || item.fillColor || null } else if (itemType === "brush") { item.data.selected = true - if (item instanceof paper.Path) { - usePaperSupportStore.getState().handleMask(item) - usePaperSupportStore.getState().handleBufferPaths(item) - usePaperSupportStore.getState().handleCircles(item) + if (editablePath) { + usePaperSupportStore.getState().handleMask(editablePath) + usePaperSupportStore.getState().handleBufferPaths(editablePath) + usePaperSupportStore.getState().handleCircles(editablePath) } - } else if (itemType === "point" && item instanceof paper.Path) { + } else if (itemType === "point") { item.data.selected = true - usePaperSupportStore.getState().handleMask(item) - usePaperSupportStore.getState().handleText(item) - usePaperSupportStore.getState().handleBufferPaths(item) - usePaperSupportStore.getState().handleCircles(item) + if (editablePath) { + usePaperSupportStore.getState().handleMask(editablePath) + usePaperSupportStore.getState().handleText(editablePath) + usePaperSupportStore.getState().handleBufferPaths(editablePath) + usePaperSupportStore.getState().handleCircles(editablePath) + } } else if (itemType === "polygon") { item.data.selected = true + if (editablePath) { + usePaperSupportStore.getState().handleBufferPaths(editablePath) + usePaperSupportStore.getState().handleCircles(editablePath) + } if (!(item instanceof paper.Path) || !item.data?.isHollowPolygon) { item.fillColor = item.data.fillColor || item.fillColor || null }