@zzalai/leafer-point-annotation 1.1.1 → 1.1.3
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.
- package/README.md +493 -250
- package/README_EN.md +498 -250
- package/docs/assets/index-BcqmlFff.js +1 -0
- package/docs/assets/{index-Dqqq7qvI.css → index-dq8tjOSG.css} +1 -1
- package/docs/index.html +2 -2
- package/package.json +2 -1
- package/project-docs/ARCHITECTURE.md +345 -354
- package/project-docs/REQUIREMENTS.md +232 -500
- package/skills/project-context.md +402 -0
- package/src/App.vue +502 -17
- package/src/components/PointAnnotation.vue +639 -219
- package/src/elements/PointAnnotationElement.ts +115 -17
- package/src/types/index.ts +22 -5
- package/src/utils/CanvasBrush.ts +20 -0
- package/docs/assets/index-CPn8AE3g.js +0 -1
|
@@ -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
|
-
| **响应式设计** |
|
|
13
|
-
| **性能优先** |
|
|
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
|
-
│ │
|
|
22
|
-
│ │ Slider.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
|
-
│ │
|
|
32
|
-
│
|
|
33
|
-
|
|
34
|
-
│ │ └──────────────┘ └──────────────┘ └───────────┘ │ │
|
|
35
|
-
│ └──────────────────────────────────────────────────────┘ │
|
|
36
|
-
└───────────────────────────┬────────────────────────────────┘
|
|
34
|
+
│ 业务逻辑层 (PointAnnotation.vue) │
|
|
35
|
+
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌─────────┐ │
|
|
36
|
+
│ │ 工具切换 │ │ 事件处理 │ │ 命令管理 │ │ 数据管理│ │
|
|
37
|
+
│ └────────────┘ └────────────┘ └────────────┘ └─────────┘ │
|
|
38
|
+
└───────────────────────────┬─────────────────────────────────┘
|
|
37
39
|
│
|
|
38
|
-
|
|
39
|
-
▼ ▼
|
|
40
|
-
┌─────────────────┐ ┌─────────────────┐
|
|
41
|
-
│ 元素封装层 │ │ 工具类层
|
|
42
|
-
│ PointAnnotation │ │CanvasBrush.ts │ │
|
|
43
|
-
│ Element.ts │ │
|
|
44
|
-
│ │ │
|
|
45
|
-
│ │ │
|
|
46
|
-
│ │ │
|
|
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
|
|
57
|
+
### 2.1 根目录入口
|
|
55
58
|
|
|
56
|
-
|
|
|
57
|
-
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
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
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
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
|
-
|
|
|
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
|
-
|
|
|
85
|
-
|
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
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
|
-
|
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
104
|
+
```ts
|
|
105
|
+
// Props(defineProps 写法)
|
|
106
|
+
{
|
|
107
|
+
imageSource?: { url: string, id?: string } // 图片来源;不传则显示上传区域(支持点击+拖拽)
|
|
108
|
+
options?: OptionsSource // 所有可配置项(见下方)
|
|
109
|
+
currentLayer?: string // 受控模式:父组件驱动当前图层
|
|
110
|
+
}
|
|
123
111
|
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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.
|
|
158
|
+
### 3.3 LeaferJS 画布结构
|
|
174
159
|
|
|
175
|
-
**结构设计**:
|
|
176
160
|
```
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
|
186
|
-
|
|
|
187
|
-
|
|
|
188
|
-
|
|
|
189
|
-
|
|
|
190
|
-
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
|
206
|
-
|
|
|
207
|
-
|
|
|
208
|
-
|
|
|
209
|
-
|
|
|
210
|
-
|
|
|
211
|
-
|
|
|
212
|
-
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
260
|
-
|
|
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
|
-
|
|
283
|
+
- 将所有点按 `sequenceNumber` 升序排序,取 pixel 坐标连成闭合多边形
|
|
284
|
+
- 调用 `activeCanvasBrush.fillPolygon(points, color)` 填充到当前激活图层
|
|
285
|
+
- 操作前后快照保存 → 支持撤销/重做
|
|
266
286
|
|
|
267
|
-
### 5
|
|
287
|
+
### 4.5 Mask 导出(含 Blob/File)
|
|
268
288
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
###
|
|
296
|
+
### 4.6 点标注 COCO / YOLO 导出
|
|
276
297
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
286
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
348
|
+
GitHub Pages 配置:分支 `main` / 目录 `/docs`。
|
|
396
349
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
379
|
+
---
|
|
409
380
|
|
|
410
|
-
|
|
411
|
-
|------|------|
|
|
412
|
-
| **Group 透明度控制** | 透明度设置在 Group 上,避免 Canvas 上多次叠加 |
|
|
413
|
-
| **批量更新** | 使用 `set()` 批量更新属性,减少重绘次数 |
|
|
414
|
-
| **图层分离** | 图片层、点标注层、笔刷层分离,独立控制 |
|
|
415
|
-
| **路径填充** | 笔刷使用多个圆填充,避免复杂路径计算 |
|
|
381
|
+
## 7. 常见开发问题速查
|
|
416
382
|
|
|
417
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
+
```
|