@zzalai/leafer-point-annotation 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,9 @@
1
1
  # 点标注与笔刷涂抹工具 - 功能架构文档
2
2
 
3
+ > **版本**: v1.1.x | **用途**: 新开 AI 会话时快速理解项目结构与代码组织
4
+
5
+ ---
6
+
3
7
  ## 1. 架构概述
4
8
 
5
9
  ### 1.1 架构设计原则
@@ -9,436 +13,423 @@
9
13
  | **单一职责** | 每个组件/模块只负责一个功能 |
10
14
  | **分层架构** | 清晰的层次划分:UI层、业务层、数据层 |
11
15
  | **可扩展性** | 预留扩展接口,支持后续功能迭代 |
12
- | **响应式设计** | 使用 Vue3 Composition API |
13
- | **性能优先** | 优化 Canvas 渲染和事件处理 |
16
+ | **响应式设计** | Vue3 Composition API + `<script setup>` |
17
+ | **性能优先** | Canvas 原生绘制 + LeaferJS 渲染管线,避免频繁 DOM 操作 |
18
+ | **类型安全** | 全量 TypeScript,核心接口(PointStyle、BrushStyle、OptionsSource)严格定义 |
14
19
 
15
20
  ### 1.2 整体架构图
16
21
 
17
22
  ```
18
23
  ┌─────────────────────────────────────────────────────────────┐
19
- │ UI 层
20
- │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
21
- │ │ BrushSize │ │BrushStyle │ │ PointAnnotation │ │
22
- │ │ Slider.vue │ │ Panel.vue │ │ (主画布组件) │ │
23
- │ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘
24
- │ │ │ │ │
25
- └─────────┼─────────────────┼────────────────────┼──────────┘
26
- │ │
27
- ▼ ▼
24
+ │ UI 层 (Vue 组件)
25
+ │ ┌──────────────┐ ┌────────────────┐ ┌─────────────────┐
26
+ │ │ BrushSize │ │ BrushStyle │ │ PointAnnotation │ │
27
+ │ │ Slider.vue │ │ Panel.vue │ │ (主画布组件) │ │
28
+ │ └──────┬───────┘ └───────┬────────┘ └────────┬────────┘
29
+ │ │ │ │ │
30
+ └─────────┼──────────────────┼────────────────────┼──────────┘
31
+ │ │
32
+ ▼ ▼
28
33
  ┌─────────────────────────────────────────────────────────────┐
29
- 业务逻辑层
30
- ┌──────────────────────────────────────────────────────┐
31
- │ │ PointAnnotation.vue (主组件) │ │
32
- ┌──────────────┐ ┌──────────────┐ ┌───────────┐
33
- │ │ │ 工具切换逻辑 │ │ 事件处理逻辑 │ │ 命令管理 │ │ │
34
- │ │ └──────────────┘ └──────────────┘ └───────────┘ │ │
35
- │ └──────────────────────────────────────────────────────┘ │
36
- └───────────────────────────┬────────────────────────────────┘
34
+ 业务逻辑层 (PointAnnotation.vue)
35
+ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌─────────┐
36
+ │ │ 工具切换 │ 事件处理 │ │ 命令管理 │ │ 数据管理│
37
+ └────────────┘ └────────────┘ └────────────┘ └─────────┘
38
+ └───────────────────────────┬─────────────────────────────────┘
37
39
 
38
- ┌─────────────────┼─────────────────┐
39
- ▼ ▼
40
- ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
41
- │ 元素封装层 │ │ 工具类层 │ │ 类型定义层
42
- │ PointAnnotation │ │CanvasBrush.ts │ │ types/index
43
- │ Element.ts │ │PointCommands.ts │ │ .ts
44
- │ │ │BrushCommands.ts │ │
45
- │ │ │COCOExporter.ts │ │
46
- │ │ │YOLOExporter.ts │ │
47
- └─────────────────┘ └─────────────────┘ └─────────────────┘
40
+ ┌─────────────────┼──────────────────┐
41
+ ▼ ▼
42
+ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐
43
+ │ 元素封装层 │ │ 工具类层 │ │ 类型定义层
44
+ │ PointAnnotation │ │ CanvasBrush.ts │ │ types/index.ts
45
+ │ Element.ts │ │ BrushCommands.ts │ │
46
+ │ │ │ PointCommands.ts │ │
47
+ │ │ │ BrushStroke.ts │ │
48
+ │ │ │ COCOExporter.ts │ │
49
+ │ │ YOLOExporter.ts │ │ │
50
+ └─────────────────┘ └─────────────────┘ └──────────────────┘
48
51
  ```
49
52
 
50
53
  ---
51
54
 
52
- ## 2. 模块划分
55
+ ## 2. 模块划分(按文件)
53
56
 
54
- ### 2.1 UI 层
57
+ ### 2.1 根目录入口
55
58
 
56
- | 模块 | 文件路径 | 职责 |
57
- |------|----------|------|
58
- | BrushSizeSlider | src/components/BrushSizeSlider.vue | 笔刷大小调节浮动组件 |
59
- | BrushStylePanel | src/components/BrushStylePanel.vue | 笔刷样式配置面板(颜色、透明度、大小等) |
60
- | Canvas | 集成在 PointAnnotation.vue | LeaferJS 画布渲染、事件监听 |
61
- | App.vue (测试入口) | src/App.vue | 提供完整的测试界面和示例 |
59
+ | 文件 | 职责 |
60
+ |------|------|
61
+ | `src/index.ts` | 对外导出 PointAnnotation 组件(named + default export) |
62
+ | `src/main.ts` | dev 模式下的入口,挂载 App.vue `#app` |
63
+ | `src/App.vue` | **开发调试演示页**,集成了所有功能的测试按钮(点标注、笔刷、图层、导出、Mask、enableBrush 等) |
62
64
 
63
- ### 2.2 业务逻辑层
65
+ ### 2.2 UI 层 - 组件
64
66
 
65
- | 模块 | 文件路径 | 职责 |
66
- |------|----------|------|
67
- | 工具切换 | PointAnnotation.vue | 管理 currentTool 状态、Editor 动态配置 |
68
- | 事件处理 | PointAnnotation.vue | 处理鼠标/键盘事件、触发相应操作 |
69
- | 命令管理 | PointAnnotation.vue | 集成 CommandManager、管理撤销/重做 |
70
- | 数据管理 | PointAnnotation.vue | 管理点标注和笔刷数据的增删改查 |
71
- | 导出导入 | PointAnnotation.vue | 处理数据导出和导入逻辑 |
72
- | 标签编辑 | PointAnnotation.vue | 管理标签编辑状态,根据工具控制可编辑性 |
67
+ | 组件 | 文件 | 职责 |
68
+ |------|------|------|
69
+ | PointAnnotation | `src/components/PointAnnotation.vue` | **核心主组件**,近 1900 行,整合所有标注与笔刷能力 |
70
+ | BrushSizeSlider | `src/components/BrushSizeSlider.vue` | 笔刷大小浮动滑块(由 BrushStylePanel 调用) |
71
+ | BrushStylePanel | `src/components/BrushStylePanel.vue` | 笔刷样式配置面板(颜色、透明度、大小 slider、连续性) |
73
72
 
74
- ### 2.3 元素封装层
73
+ ### 2.3 元素封装层 - LeaferJS 自定义元素
75
74
 
76
- | 模块 | 文件路径 | 职责 |
77
- |------|----------|------|
78
- | PointAnnotationElement | src/elements/PointAnnotationElement.ts | 封装点标注元素(Group + Ellipse + Text),支持 hover/selected 状态 |
75
+ | 文件 | 职责 |
76
+ |------|------|
77
+ | `src/elements/PointAnnotationElement.ts` | 自定义点标注元素(继承 `Group`,含 `Ellipse` + `Text`),内置 hover/press/selected 状态切换、`sequenceNumber` 自动重排、`updateLabel()` 方法 |
79
78
 
80
- ### 2.4 工具类层
79
+ ### 2.4 工具类层 - utils
81
80
 
82
- | 模块 | 文件路径 | 职责 |
83
- |------|----------|------|
84
- | CanvasBrush | src/utils/CanvasBrush.ts | 使用 LeaferJS Canvas 实现笔刷绘制,支持 draw/erase/clear/hasContent |
85
- | PointCommands | src/utils/PointCommands.ts | 点标注的 Add/Remove 命令实现 |
86
- | BrushCommands | src/utils/BrushCommands.ts | 笔刷的快照命令实现(BrushSnapshotCommand) |
87
- | COCOExporter | src/utils/COCOExporter.ts | COCO 格式数据导出 |
88
- | YOLOExporter | src/utils/YOLOExporter.ts | YOLO 格式数据导出 |
81
+ | 文件 | 职责 |
82
+ |------|------|
83
+ | `src/utils/CanvasBrush.ts` | 笔刷底层:每个图层一个实例,含 canvas/ctx、stroke/clear/fillPolygon/getImageData/hasContent/getGroup |
84
+ | `src/utils/BrushCommands.ts` | 笔刷相关命令(撤销/重做用):`BrushSnapshotCommand` 基于 ImageData 快照 |
85
+ | `src/utils/PointCommands.ts` | 点标注相关命令:`AddPointCommand`、`RemovePointCommand` |
86
+ | `src/utils/BrushStroke.ts` | 笔刷笔画数据结构与辅助函数(已由 CanvasBrush 接管核心绘制,主要供数据导出用) |
87
+ | `src/utils/COCOExporter.ts` | 导出 COCO JSON 格式(点标注为 keypoints) |
88
+ | `src/utils/YOLOExporter.ts` | 导出 YOLO 标注格式 |
89
89
 
90
90
  ### 2.5 类型定义层
91
91
 
92
- | 模块 | 文件路径 | 职责 |
93
- |------|----------|------|
94
- | Types | src/types/index.ts | 定义所有 TypeScript 类型接口(PointAnnotationBrushStyleExportOptions 等) |
92
+ | 文件 | 职责 |
93
+ |------|------|
94
+ | `src/types/index.ts` | **所有对外接口集中地**:`PointAnnotation`、`PointStyle`、`BrushLayerConfig`、`BrushStyle`、`BrushStrokeData`、`ToolType`、`ExportFormat`、`ExportOptions`、`Statistics`、`ExportData`,以及 `DEFAULT_POINT_STYLE` / `DEFAULT_BRUSH_STYLE` |
95
95
 
96
96
  ---
97
97
 
98
- ## 3. 核心组件设计
99
-
100
- ### 3.1 PointAnnotation.vue(主组件)
101
-
102
- **核心职责**:
103
- - 初始化 LeaferJS 应用和图片加载
104
- - 管理工具状态和切换逻辑
105
- - 处理用户交互事件
106
- - 管理撤销/重做队列
107
- - 提供对外 API
108
- - 处理数据导出/导入
98
+ ## 3. PointAnnotation 主组件内部结构(重点)
109
99
 
110
- **状态管理**:
111
- ```typescript
112
- // 工具状态
113
- const currentTool = ref<ToolType>('select');
100
+ > 位置:`src/components/PointAnnotation.vue` (~1900 行,script setup + 模板 + style 三段式)
114
101
 
115
- // 数据状态
116
- const pointAnnotations = ref<PointAnnotation[]>([]);
117
- const pointCounter = ref(1);
102
+ ### 3.1 Props 与 Events
118
103
 
119
- // 画布状态
120
- const loadStatus = ref('idle');
121
- const imageWidth = ref<number | null>(null);
122
- const imageHeight = ref<number | null>(null);
104
+ ```ts
105
+ // Props(defineProps 写法)
106
+ {
107
+ imageSource?: { url: string, id?: string } // 图片来源;不传则显示上传区域(支持点击+拖拽)
108
+ options?: OptionsSource // 所有可配置项(见下方)
109
+ currentLayer?: string // 受控模式:父组件驱动当前图层
110
+ }
123
111
 
124
- // 本地图片上传状态
125
- const hasLocalImage = ref(false);
126
- const localImageUrl = ref('');
127
- const isDragOver = ref(false);
128
-
129
- // UI 状态
130
- const showBrushPanel = ref(false);
131
- const brushButtonRect = ref({ x: 0, y: 0, width: 0, height: 0 });
132
- const showTools = computed(() => loadStatus.value === 'success');
133
-
134
- // 笔刷样式
135
- const localBrushStyle = ref<BrushStyle>({ ...DEFAULT_BRUSH_STYLE });
112
+ // Events(defineEmits)
113
+ pointChange | loadStart | loadSuccess | loadError
114
+ undoStateChange | redoStateChange
115
+ update:currentLayer | layerChange
136
116
  ```
137
117
 
138
- **事件处理**:
139
- | 事件 | 处理方法 | 触发操作 |
140
- |------|----------|----------|
141
- | pointerdown | handlePointerDown | 创建点标注/开始笔刷绘制 |
142
- | pointermove | handlePointerMove | 笔刷绘制/鼠标追踪 |
143
- | pointerup | handlePointerUp | 完成笔刷绘制 |
144
- | keydown | handleKeyDown | 热键处理(tinykeys) |
145
- | dragover | handleDragOver | 拖拽文件时高亮边框 |
146
- | dragleave | handleDragLeave | 拖拽离开时取消高亮 |
147
- | drop | handleDrop | 拖拽文件时处理上传 |
148
-
149
- **对外 API**:
150
- ```typescript
151
- defineExpose({
152
- getPointAnnotations,
153
- getImageInfo,
154
- exportCanvasJSON,
155
- exportMaskImage,
156
- exportCOCO,
157
- exportYOLO,
158
- importCanvasJSON,
159
- loadImage,
160
- clearBrush,
161
- zoomIn,
162
- zoomOut,
163
- resetZoom,
164
- undo,
165
- redo,
166
- getCurrentTool,
167
- setTool,
168
- createPointAnnotation,
169
- removePointAnnotation,
170
- });
118
+ ### 3.2 OptionsSource - 完整配置项
119
+
120
+ ```ts
121
+ interface OptionsSource {
122
+ // 点标注样式
123
+ pointStyle?: Partial<PointStyle>
124
+
125
+ // 笔刷样式
126
+ brushStyle?: Partial<BrushStyle>
127
+
128
+ // 多图层笔刷(不传则单图层)
129
+ brushLayers?: BrushLayerConfig[]
130
+ maxBrushLayers?: number
131
+
132
+ // 限制
133
+ maxPoints?: number
134
+ maxUndoSteps?: number // 默认 100(硬编码在 init 中)
135
+
136
+ // Mask 导出默认
137
+ maskExportFormat?: 'png' | 'jpeg' | 'jpg'
138
+ maskExportForeground?: 'black' | 'white'
139
+
140
+ // UI 开关
141
+ showToolbar?: boolean // 默认 true
142
+ showZoomController?: boolean // 默认 true
143
+ canvasBackground?: string // 画布背景色(默认 #f6f6f6)
144
+ zoomMin?: number // 最小缩放比例
145
+ zoomMax?: number // 最大缩放比例
146
+
147
+ // 功能开关(核心新增)
148
+ enableBrush?: boolean // 默认 true;设为 false 时:
149
+ // - 笔刷/橡皮擦按钮与配置面板隐藏
150
+ // - brushTool/eraserTool 方法不生效
151
+ // - 快捷键 b/e 不生效
152
+ // - initBrushLayers 跳过 canvas 创建
153
+ // - mask 导出相关方法返回 null/{}
154
+ // - 删除确认文案不包含"笔刷"
155
+ }
171
156
  ```
172
157
 
173
- ### 3.2 PointAnnotationElement(点标注元素)
158
+ ### 3.3 LeaferJS 画布结构
174
159
 
175
- **结构设计**:
176
160
  ```
177
- Group (容器) - id: 点数据的 id, _element_tag: 'point-annotation'
178
- ├── Ellipse (圆点) - 负责视觉样式、hover/selected 效果
179
- └── Text (标签) - 负责显示标签文本、支持编辑,带 boxStyle 背景
161
+ App (leafer-ui)
162
+ └── contentLayer (Group, name: "contentLayer")
163
+ ├── imageBox (Image) // 背景图(由 loadImage 加载)
164
+ ├── pointLayer (Group) // 所有 PointAnnotationElement 挂在此处
165
+ └── [canvas-brush-group-*] // 每个笔刷图层一个 group(含 html canvas)
180
166
  ```
181
167
 
182
- **核心方法**:
183
- | 方法 | 功能 |
168
+ ### 3.4 关键 computed 状态(文档必知)
169
+
170
+ | 变量 | 含义 |
184
171
  |------|------|
185
- | constructor | 初始化元素、绑定事件、配置 hoverStyle |
186
- | handlePointAnnotationSelected | 更新选中状态样式(fill/stroke/scale) |
187
- | handleLabelChange | 处理标签编辑变更(非空校验) |
188
- | updatePosition | 更新位置坐标 |
189
- | updateLabel | 更新标签文本 |
190
- | getLabel / getLastValidLabel | 获取标签值 |
191
-
192
- ### 3.3 CanvasBrush(笔刷绘制)
193
-
194
- **核心职责**:
195
- - 使用 LeaferJS Canvas 实现笔刷绘制
196
- - 外层 Group 控制整体透明度(避免多次叠加)
197
- - 支持绘制和擦除模式
198
- - 连续性阈值处理(两个点之间距离过远时自动连线)
199
- - 图片数据导出/恢复
200
- - hasContent() 检测是否有内容
201
-
202
- **核心方法**:
203
- | 方法 | 功能 |
172
+ | `hasImage` | `loadStatus === 'success'`;控制工具栏/画布是否可见 |
173
+ | `showToolbar` | `hasImage && options?.showToolbar !== false` |
174
+ | `showZoomController` | `hasImage && options?.showZoomController !== false` |
175
+ | `effectiveEnableBrush` | `options?.enableBrush !== false` |
176
+ | `pointStyle` | `DEFAULT_POINT_STYLE + options.pointStyle` |
177
+ | `brushStyle` | `DEFAULT_BRUSH_STYLE + options.brushStyle` |
178
+ | `localBrushStyle` | 本地响应式拷贝(滑块改的值存在这) |
179
+ | `effectiveBrushLayers` | 实际使用的图层配置;无配置时返回 `[{label:'默认图层', value:'default'}]` |
180
+ | `effectiveCurrentLayer` | 当前激活图层 value |
181
+ | `activeCanvasBrush` | 当前图层的 `CanvasBrush` 实例(供笔刷绘制、fillPolygon、clear 用) |
182
+
183
+ ### 3.5 关键数据结构
184
+
185
+ | 变量 | 类型 | 含义 |
186
+ |------|------|------|
187
+ | `pointAnnotations` | `ref<PointAnnotation[]>` | 所有点标注数据(响应式) |
188
+ | `pointCounter` | `ref<number>` | 下一个 order 数字 |
189
+ | `canvasBrushesByLayer` | `Record<string, CanvasBrush>` | key = layer.value;value = CanvasBrush 实例 |
190
+ | `commandManager` | `CommandManager`(来自 @zzalai/leafer-undo-redo) | 撤销/redo 命令栈 |
191
+
192
+ ### 3.6 defineExpose - 父组件可调用 API(完整列表)
193
+
194
+ > **重要**: 如果想自定义工具栏(隐藏组件自带工具栏),调用这些方法即可。
195
+
196
+ | 方法 | 说明 |
204
197
  |------|------|
205
- | constructor | 初始化 Canvas 和外层 Group |
206
- | draw | 绘制笔刷(使用多个圆填充路径) |
207
- | erase | 擦除操作(destination-out) |
208
- | clear | 清除所有内容 |
209
- | getImageData | 导出为 PNG dataURL |
210
- | restoreImageData | dataURL 恢复画布 |
211
- | hasContent | 检测是否有非透明像素 |
212
- | setPointerEvents | 控制 Canvas 是否拦截事件 |
213
-
214
- ### 3.4 笔刷命令设计
215
-
216
- **BrushSnapshotCommand**:
217
- - 笔刷操作使用快照方式实现撤销/重做
218
- - 保存操作前后的完整图片状态
219
- - undo/redo 时恢复对应状态
220
-
221
- ### 3.5 导出导入实现
222
-
223
- **导出格式**:
224
- 1. **JSON Full**:完整数据(点标注 + 笔刷 mask)
225
- 2. **JSON Points**:仅点标注数据
226
- 3. **COCO**:COCO 数据集格式
227
- 4. **YOLO**:YOLO 数据集格式
228
- 5. **Mask**:二值图(PNG/JPEG 可选)
229
-
230
- **二值图特性**:
231
- - 前景色可配置(黑/白)
232
- - PNG 支持透明背景
233
- - JPG 自动处理背景色(前景黑则背景白,反之亦然)
234
- - 使用 getImageData() 扫描像素,重新绘制为纯二值图
198
+ | **点标注数据** | |
199
+ | `getPointAnnotations()` | 返回当前所有标注点 |
200
+ | `createPointAnnotation(x, y, label?)` | 程序化添加一个点 |
201
+ | `removePointAnnotation(id)` | 程序化删除一个点 |
202
+ | `updatePointAnnotationLabel(id, label)` | 修改某个点的 label 文案 |
203
+ | **图片 & 画布** | |
204
+ | `getImageInfo()` | 返回 `{ url, width, height }` |
205
+ | `loadImage(url)` | 动态加载图片 |
206
+ | **工具切换** | |
207
+ | `getCurrentTool()` | 返回 `'select'/'point'/'brush'/'eraser'` |
208
+ | `setTool(tool)` | 切到指定工具(受 enableBrush 限制) |
209
+ | `selectTool()` / `pointTool()` | |
210
+ | `brushTool(openPanel?)` / `eraserTool()` | |
211
+ | **删除 / 清空** | |
212
+ | `deleteSelected()` | 删除当前选中的标注点 |
213
+ | `clearAllAnnotationsAndBrush()` | 清空所有点 + 笔刷(带确认) |
214
+ | `clearBrush()` / `clearAllBrushLayers()` | 清空当前/所有图层的笔刷 |
215
+ | **笔刷图层** | |
216
+ | `getCurrentLayer()` / `setActiveLayer(value)` | |
217
+ | `getAllLayers()` | 返回 `BrushLayerConfig[]` |
218
+ | **笔刷样式** | |
219
+ | `getBrushStyle()` | 返回当前 localBrushStyle 的拷贝 |
220
+ | `updateBrushStyle(partial)` | 动态更新笔刷颜色/透明度/大小/连续性 |
221
+ | **点轨迹生成笔刷** | |
222
+ | `createBrushFromPoints()` | 按点标注顺序连成闭合多边形,fillPolygon 填充到当前笔刷层 |
223
+ | **缩放** | |
224
+ | `zoomIn()` / `zoomOut()` / `resetZoom()` | |
225
+ | **撤销 / 重做** | |
226
+ | `undo()` / `redo()` | |
227
+ | **导出** | |
228
+ | `exportCanvasJSON()` / `importCanvasJSON(data)` | 全量导入导出 |
229
+ | `exportMaskImage(format?, fg?)` | 当前图层 mask(Data URL) |
230
+ | `exportMaskImageByLayer(layerValue, format?, fg?)` | 指定图层 mask |
231
+ | `exportAllMaskImages(format?, fg?)` | 所有图层 mask(Record<layerValue, dataURL>) |
232
+ | `getMaskBlob(layerValue?, format?, fg?)` | 当前/指定图层 Blob(后端上传用) |
233
+ | `getMaskFile(layerValue?, filename?, format?, fg?)` | 当前/指定图层 File |
234
+ | `getAllMaskBlobs(format?, fg?)` | 所有图层 Blob 集合 |
235
+ | `exportCOCO()` / `exportYOLO()` | 导出数据集格式 |
236
+
237
+ ### 3.7 快捷键(tinykeys)
238
+
239
+ | 按键 | 功能 | 限制 |
240
+ |------|------|------|
241
+ | `v` | 选择工具 | 画布 focus 或 hover 才生效 |
242
+ | `p` | 点标注工具 | 同上 |
243
+ | `b` | 笔刷工具 | 需 `effectiveEnableBrush=true` |
244
+ | `e` | 橡皮擦工具 | 需 `effectiveEnableBrush=true` |
245
+ | `Ctrl+Z` | 撤销 | |
246
+ | `Ctrl+Y` | 重做 | |
247
+ | `Delete` | 删除选中 | |
248
+ | `Ctrl++` / `Ctrl+-` / `Ctrl+0` | 缩放 +/− / 重置 | |
249
+ | `Alt` | 显示快捷键提示浮层 | |
235
250
 
236
251
  ---
237
252
 
238
- ## 4. 数据流转
253
+ ## 4. 核心功能实现要点
239
254
 
240
- ### 4.1 点标注数据流转
255
+ ### 4.1 点标注 (PointAnnotationElement)
241
256
 
242
- ```
243
- 用户点击 handlePointerDown createPointAnnotation
244
- PointAnnotationElement AddPointCommand CommandManager
245
- pointAnnotations (响应式数组) 导出数据
246
- ```
257
+ - **继承自 `Group`**,内部包含一个 `Ellipse`(圆点)和一个 `Text`(文字)
258
+ - **hover / press / selected 三态**:通过监听 LeaferJS 原生事件(`pointer.over`、`pointer.out`、`pointer.down`、`pointer.up`)控制填充色与缩放
259
+ - **sequenceNumber 自动重排**:删除点后调用 `renumberSequenceNumbers()`,保证显示序号 1,2,3... 连续;同时在 Text.text 变化时判断是否为"自动序号变值"以避免把重排当作用户手动修改保存
260
+ - **label 编辑**:`updateLabel(label)` 修改文字;label 文本不允许为空字符串
261
+ - **命令化**:点新增/删除都通过 `AddPointCommand`/`RemovePointCommand` 进撤销栈
247
262
 
248
- ### 4.2 笔刷数据流转
263
+ ### 4.2 多图层笔刷 (CanvasBrush)
249
264
 
250
- ```
251
- 用户绘制 handlePointerMoveCanvasBrush.draw
252
- (操作结束) BrushSnapshotCommand CommandManager →
253
- (内部保存 mask 图片)
254
- ```
265
+ - **每个图层一个 HTML canvas + CanvasBrush 实例**,挂在独立 Group 下(通过 Group.opacity 控制整体透明度,避免叠加问题)
266
+ - `brushLayers?: BrushLayerConfig[]` 不传时 单图层,value `"default"`
267
+ - `canvasBrushesByLayer[layerValue] = CanvasBrush` 便于快速定位当前图层画笔
268
+ - **绘制方式**:`stroke(x, y, color)` + `eraserStroke(x, y, size)` + `fillPolygon(points, color)`
269
+ - **快照式撤销**:`BrushSnapshotCommand` 在操作前后保存 `getImageData()`,redo/undo 时 `putImageData()`
255
270
 
256
- ### 4.3 导出/导入数据流转
271
+ ### 4.3 enableBrush 功能开关
257
272
 
258
- ```
259
- 导出:用户触发 exportData(format) Exporter 格式化 返回数据字符串/Blob
260
- 导入:用户触发 importCanvasJSON(json) → 解析数据 → 重建元素 → fitImageToCanvas
261
- ```
273
+ - 入口:`effectiveEnableBrush = computed(() => props.options?.enableBrush !== false)`
274
+ - **UI 层**:工具栏笔刷按钮 / eraser 按钮 / BrushStylePanel 全部加 `v-if`
275
+ - **方法层**:brushTool/eraserTool/updateBrushStyle/clearBrush/clearAllBrushLayers/createBrushFromPoints 开头加守卫
276
+ - **数据层**:`initBrushLayers` 跳过 canvas 创建;`setTool('brush'/'eraser')` 被拦截
277
+ - **导出层**:exportMaskImage / exportMaskImageByLayer / exportAllMaskImages / getMaskBlob / getMaskFile / getAllMaskBlobs 返回 null 或 {}
278
+ - **快捷键层**:`b`/`e` 按键 handler 开头加守卫
279
+ - **确认文案**:deleteSelected 的确认提示不出现"笔刷"字样
262
280
 
263
- ---
281
+ ### 4.4 点轨迹生成笔刷区域 (createBrushFromPoints)
264
282
 
265
- ## 5. 命令模式实现
283
+ - 将所有点按 `sequenceNumber` 升序排序,取 pixel 坐标连成闭合多边形
284
+ - 调用 `activeCanvasBrush.fillPolygon(points, color)` 填充到当前激活图层
285
+ - 操作前后快照保存 → 支持撤销/重做
266
286
 
267
- ### 5.1 命令类型
287
+ ### 4.5 Mask 导出(含 Blob/File)
268
288
 
269
- | 命令类 | 功能 | 撤销逻辑 |
270
- |--------|------|----------|
271
- | AddPointCommand | 添加点标注 | 移除点标注、从数组删除 |
272
- | RemovePointCommand | 删除点标注 | 重新添加点标注、插入原位置 |
273
- | BrushSnapshotCommand | 笔刷快照 | 恢复操作前的图片状态 |
289
+ - **原理**:遍历对应 canvas,将有内容像素 前景色(黑/白),无内容 透明或背景色
290
+ - **输出形式**:
291
+ - `data URL` (Base64 PNG/JPEG)
292
+ - `Blob`(后端上传 `multipart/form-data` 直接用)
293
+ - `File`(带 filename + mime,直接 formData.append)
294
+ - **单图层 / 多图层都支持**:`getAllMaskBlobs()` 返回 Record<layerValue, Blob>
274
295
 
275
- ### 5.2 命令管理器集成
296
+ ### 4.6 点标注 COCO / YOLO 导出
276
297
 
277
- ```typescript
278
- // 初始化命令管理器(历史限制 100)
279
- const commandManager = new CommandManager(100);
298
+ - COCO:把点标注写成 `annotations[].keypoints`(x,y,v 三值,v=2 表示可见)
299
+ - YOLO:生成 yolo 格式标签文件(归一化坐标)
300
+ - 详见 `src/utils/COCOExporter.ts`、`src/utils/YOLOExporter.ts`
280
301
 
281
- // 执行命令
282
- commandManager.executeCommand(new AddPointCommand(pointLayer, element, pointAnnotations.value, data));
302
+ ---
283
303
 
284
- // 撤销/重做
285
- commandManager.undo();
286
- commandManager.redo();
304
+ ## 5. 构建与发布
305
+
306
+ ### 5.1 package.json 关键脚本
307
+
308
+ ```json
309
+ {
310
+ "name": "@zzalai/leafer-point-annotation",
311
+ "version": "1.1.2",
312
+ "main": "./dist/leafer-point-annotation.umd.js",
313
+ "module": "./dist/leafer-point-annotation.es.js",
314
+ "types": "./dist/index.d.ts",
315
+ "scripts": {
316
+ "dev": "vite",
317
+ "build": "vite build", // 构建 dist(es/umd/css/dts)
318
+ "docs:build": "vite build --config vite.docs.config.ts", // 构建演示站点到 docs/
319
+ "build:all": "npm run build && npm run docs:build",
320
+ "preview": "vite preview",
321
+ "type-check": "tsc --noEmit"
322
+ }
323
+ }
287
324
  ```
288
325
 
289
- ---
326
+ ### 5.2 dist 目录结构
290
327
 
291
- ## 6. Editor 动态控制
292
-
293
- ### 6.1 控制策略
294
-
295
- | 工具 | Editor 状态 | 说明 |
296
- |------|-------------|------|
297
- | select | 启用 | 允许选择和拖拽 |
298
- | point | 启用 | 允许选择和拖拽点标注 |
299
- | brush | 禁用 | 避免干扰笔刷绘制 |
300
- | eraser | 禁用 | 避免干扰擦除操作 |
301
-
302
- ### 6.2 标签编辑控制
303
-
304
- | 工具 | 标签可编辑 | 说明 |
305
- |------|-----------|------|
306
- | select | true | 可以编辑标签 |
307
- | point | true | 可以编辑标签 |
308
- | brush | false | 禁用编辑,避免冲突 |
309
- | eraser | false | 禁用编辑,避免冲突 |
310
-
311
- ### 6.3 实现逻辑
312
-
313
- ```typescript
314
- const switchToSelect = () => {
315
- currentTool.value = 'select';
316
- showBrushPanel.value = false;
317
- if (!app) return;
318
- app.editor.config.moveable = false;
319
- app.editor.config.resizeable = false;
320
- app.editor.config.multipleSelect = true;
321
- canvasBrush?.setPointerEvents(false);
322
- updateLabelEditable(true);
323
- };
328
+ ```
329
+ dist/
330
+ ├── leafer-point-annotation.es.js (ESM)
331
+ ├── leafer-point-annotation.umd.js (UMD)
332
+ ├── leafer-point-annotation.css (样式,必须手动导入)
333
+ └── index.d.ts (TypeScript 类型)
324
334
  ```
325
335
 
326
- ---
336
+ > 使用者在项目中必须 `import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'`。
327
337
 
328
- ## 7. 对外 API 设计
329
-
330
- ### 7.1 方法列表
331
-
332
- | 方法名 | 参数 | 返回值 | 功能描述 |
333
- |--------|------|--------|----------|
334
- | getPointAnnotations | 无 | PointAnnotation[] | 获取所有点标注数据 |
335
- | createPointAnnotation | (x, y) | string \| null | 创建标注点,返回 id |
336
- | removePointAnnotation | (id) | boolean | 删除指定 id 的点标注 |
337
- | clearBrush | 无 | void | 清除所有笔刷内容 |
338
- | exportCanvasJSON | 无 | string | 导出完整 JSON 数据 |
339
- | exportMaskImage | (format, fgColor) | string \| null | 导出二值图 dataURL |
340
- | exportCOCO | 无 | COCOExport | 导出 COCO 格式数据 |
341
- | exportYOLO | 无 | YOLOExport | 导出 YOLO 格式数据 |
342
- | importCanvasJSON | (json) | void | 导入 JSON 数据 |
343
- | loadImage | (url) | Promise | 加载图片 |
344
- | undo | 无 | void | 撤销操作 |
345
- | redo | 无 | void | 重做操作 |
346
- | setTool | (tool) | void | 设置当前工具 |
347
- | getCurrentTool | 无 | ToolType | 获取当前工具 |
348
- | zoomIn | 无 | void | 放大画布 |
349
- | zoomOut | 无 | void | 缩小画布 |
350
- | resetZoom | 无 | void | 重置缩放 |
351
-
352
- ### 7.2 Props 配置
353
-
354
- | 属性名 | 类型 | 默认值 | 说明 |
355
- |--------|------|--------|------|
356
- | imageSource | { id?: string, url: string } \| null | null | 图片源配置(可选,未提供时显示本地上传界面) |
357
- | pointStyle | Partial\<PointStyle\> | DEFAULT_POINT_STYLE | 点标注样式 |
358
- | brushStyle | Partial\<BrushStyle\> | DEFAULT_BRUSH_STYLE | 笔刷样式 |
359
- | options | { maskExportFormat, maskExportForeground } | - | 导出配置 |
360
-
361
- ### 7.3 图片切换逻辑
362
-
363
- **图片加载策略**:
364
- 1. 优先使用本地上传的图片(hasLocalImage 为 true)
365
- 2. 其次使用 props.imageSource.url
366
- 3. 无图片时显示上传界面
367
-
368
- **Props 监听**:
369
- ```typescript
370
- watch(
371
- () => props.imageSource?.url,
372
- (newUrl, oldUrl) => {
373
- if (newUrl) {
374
- // 有新图片 URL,加载新图片
375
- hasLocalImage.value = false;
376
- localImageUrl.value = '';
377
- loadImage(newUrl);
378
- } else if (oldUrl && !newUrl) {
379
- // 图片 URL 变空,清空画布回到上传状态
380
- hasLocalImage.value = false;
381
- localImageUrl.value = '';
382
- clearAllAnnotationsAndBrush();
383
- if (imageBox) {
384
- contentLayer.clear();
385
- imageBox.destroy();
386
- imageBox = null;
387
- }
388
- loadStatus.value = 'idle';
389
- }
390
- },
391
- { immediate: true }
392
- );
338
+ ### 5.3 docs 目录(GitHub Pages 演示站)
339
+
340
+ ```
341
+ docs/
342
+ ├── index.html
343
+ └── assets/
344
+ ├── index-*.js
345
+ └── index-*.css
393
346
  ```
394
347
 
395
- ### 7.4 Events
348
+ GitHub Pages 配置:分支 `main` / 目录 `/docs`。
396
349
 
397
- | 事件名 | 参数 | 说明 |
398
- |--------|------|------|
399
- | pointChange | PointAnnotation[] | 点标注数据变化 |
400
- | loadStart | - | 图片开始加载 |
401
- | loadSuccess | - | 图片加载成功 |
402
- | loadError | - | 图片加载失败 |
350
+ ### 5.4 发布流程
351
+
352
+ ```bash
353
+ pnpm install
354
+ pnpm run build:all # 同时生成 dist 和 docs
355
+ # 确认 dist .es.js / .umd.js / .css / index.d.ts
356
+ # 确认 docs 有最新的演示站点
357
+ npm publish # 发布到 npm(需先登录 npm)
358
+ ```
403
359
 
404
360
  ---
405
361
 
406
- ## 8. 性能优化策略
362
+ ## 6. 依赖清单(核心)
363
+
364
+ | 包 | 版本要求 | 用途 |
365
+ |----|----------|------|
366
+ | `vue` | ^3.3.0 (peerDependency) | 宿主框架 |
367
+ | `leafer-ui` | ^2.0.8 | Canvas 渲染引擎(App/Image/Group/Ellipse/Text) |
368
+ | `@leafer-in/editor` | ^2.0.8 | 编辑器(选择、框选、点状态管理) |
369
+ | `@leafer-in/viewport` | ^2.0.8 | 缩放/平移视口 |
370
+ | `@leafer-in/resize` | ^2.0.8 | 元素 resize 手柄 |
371
+ | `@leafer-in/state` | ^2.1.0 | hover/selected 状态 |
372
+ | `@leafer-in/text-editor` | ^2.1.0 | label 文本编辑 |
373
+ | `@leafer-in/view` | ^2.0.8 | view 盒子 |
374
+ | `@leafer-in/box` | ^2.1.6 | box 布局 |
375
+ | `@zzalai/leafer-undo-redo` | 1.0.3 | 撤销/重做 CommandManager |
376
+ | `tinykeys` | ^3.0.0 | 键盘热键 |
377
+ | `vue-pick-colors` | ^1.8.0 | 颜色选择器组件 |
407
378
 
408
- ### 8.1 Canvas 渲染优化
379
+ ---
409
380
 
410
- | 策略 | 说明 |
411
- |------|------|
412
- | **Group 透明度控制** | 透明度设置在 Group 上,避免 Canvas 上多次叠加 |
413
- | **批量更新** | 使用 `set()` 批量更新属性,减少重绘次数 |
414
- | **图层分离** | 图片层、点标注层、笔刷层分离,独立控制 |
415
- | **路径填充** | 笔刷使用多个圆填充,避免复杂路径计算 |
381
+ ## 7. 常见开发问题速查
416
382
 
417
- ### 8.2 事件处理优化
383
+ ### Q1. 本地图片上传后 props.imageSource.url 变更无效?
384
+ - 组件内部 `hasLocalImage`/`localImageUrl` 标记本地上传后优先本地;若 props.url 变更是在本地上传之后,会监听 `watch(props.imageSource?.url)` 并在检测到新 url 时清空本地标记重新加载。
418
385
 
419
- | 策略 | 说明 |
420
- |------|------|
421
- | **事件委托** | LeaferJS 自动处理事件冒泡 |
422
- | **条件监听** | 笔刷 Canvas 只在需要时拦截事件(pointerEvents) |
423
- | **updateLabelEditable** | 只在工具切换时更新标签可编辑性 |
386
+ ### Q2. 点标注删除后序号不连续?
387
+ - `removePointAnnotation` / `Delete` 操作后调用 `renumberSequenceNumbers()`,按数组顺序重写每个元素的 `sequenceNumber` 及显示文本。undo/redo 时 Text.text 变化通过「与当前 sequenceNumber 的比较」判定是否为自动重排,避免误存为用户自定义修改。
424
388
 
425
- ### 8.3 内存管理
389
+ ### Q3. 我只需要点标注功能,不需要笔刷,如何配置?
390
+ - `options={{ enableBrush: false }}`,所有笔刷 UI 与方法都会被屏蔽。
426
391
 
427
- | 策略 | 说明 |
428
- |------|------|
429
- | **及时清理** | 移除元素时调用 destroy() |
430
- | **历史限制** | 设置合理的撤销历史记录限制(默认 100) |
431
- | **Canvas 重用** | CanvasBrush 复用同一 Canvas 对象 |
392
+ ### Q4. 我要给后端上传 Mask,用 dataURL 还是 Blob?
393
+ - 后端推荐 Blob/File:`getMaskBlob()` 或 `getMaskFile()` 直接用于 `formData.append('file', blob)`。
432
394
 
433
- ### 8.4 导出优化
395
+ ### Q5. 父组件要自定义工具栏怎么搞?
396
+ - `options={{ showToolbar: false, showZoomController: false }}`,然后父组件通过 `ref` 调用 expose 中的任意方法(selectTool/pointTool/brushTool/deleteSelected/clearAll/export*/...)。
434
397
 
435
- | 策略 | 说明 |
436
- |------|------|
437
- | **异步处理** | 图片导出使用 async/await,避免阻塞 |
438
- | **DataURL 缓存** | 笔刷快照保存 dataURL,避免重复序列化 |
398
+ ### Q6. 多图层如何切换?
399
+ - 通过 `options.brushLayers: [{label, value, color?, opacity?, size?}]` 配置多个图层;父组件可通过 `v-model:currentLayer="'layerA'"` 或 `setActiveLayer('layerA')` 控制当前图层。
439
400
 
440
401
  ---
441
402
 
442
- **文档版本**:2.1
443
- **创建日期**:2026-04-28
444
- **最后更新**:2026-05-08
403
+ ## 8. 文件索引(快速定位)
404
+
405
+ ```
406
+ src/
407
+ ├── components/
408
+ │ ├── PointAnnotation.vue ← 核心(1900 行)
409
+ │ ├── BrushSizeSlider.vue ← 大小 slider
410
+ │ └── BrushStylePanel.vue ← 样式配置面板
411
+ ├── elements/
412
+ │ └── PointAnnotationElement.ts ← 点标注自定义元素
413
+ ├── utils/
414
+ │ ├── CanvasBrush.ts ← 笔刷底层(canvas/ctx、fillPolygon、快照)
415
+ │ ├── BrushCommands.ts ← BrushSnapshotCommand
416
+ │ ├── PointCommands.ts ← AddPointCommand/RemovePointCommand
417
+ │ ├── BrushStroke.ts ← 笔画数据结构
418
+ │ ├── COCOExporter.ts ← COCO 导出
419
+ │ └── YOLOExporter.ts ← YOLO 导出
420
+ ├── types/
421
+ │ └── index.ts ← 所有对外类型 + DEFAULT 常量
422
+ ├── App.vue ← 开发演示页面(含所有功能的测试按钮)
423
+ ├── index.ts ← 对外导出入口
424
+ └── main.ts ← dev 入口
425
+
426
+ project-docs/ ← 本文档所在目录(AI 会话上下文来源)
427
+ ├── ARCHITECTURE.md ← 本文(架构与模块)
428
+ ├── REQUIREMENTS.md ← 需求清单
429
+ ├── IMPLEMENTATION_PLAN.md ← 实现方案记录
430
+ ├── TODO.md ← 待办与已完成
431
+ └── leafer-development-guide/
432
+ ├── LEAFER_DEVELOPMENT_GUIDE.md
433
+ ├── LEAFER_UNDO_REDO_GUIDE.md
434
+ └── TINYKEYS_GUIDE.md
435
+ ```