fix(label): selected path add point

This commit is contained in:
2026-04-18 21:13:17 +08:00
parent a3748a8539
commit b050af0267
3 changed files with 162 additions and 21 deletions

View File

@@ -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`

View File

@@ -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<PaperState>((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",

View File

@@ -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
}