@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.
package/README_EN.md CHANGED
@@ -2,19 +2,52 @@
2
2
 
3
3
  English | [中文](README.md)
4
4
 
5
- A point annotation and brush painting tool based on Vue3 + LeaferJS, supporting COCO/YOLO/JSON export, designed for AI model training dataset annotation.
6
-
7
- ## Features
8
-
9
- - 📍 **Point Annotation** - Add editable points on images
10
- - 🖌️ **Brush Painting** - Freehand painting with eraser support
11
- - 🎨 **Custom Styles** - Configurable brush color, size, and opacity
12
- - 🔄 **Undo/Redo** - Complete history management
13
- - 📤 **Multi-format Export** - JSON/COCO/YOLO/Mask Image
14
- - 🔍 **Canvas Zoom** - Zoom, pan, reset support
15
- - ⌨️ **Hotkeys** - V/P/B/E/Ctrl+Z/Ctrl+Y and more
16
- - 📱 **Responsive Design** - Vue3 component architecture
17
- - 🖼️ **Local Upload** - Support local image upload and drag-and-drop
5
+ > A point annotation and brush painting tool based on **Vue 3 + LeaferJS**, supporting COCO/YOLO/JSON/Mask export, designed for AI model training dataset annotation.
6
+
7
+ - 📍 **Point Annotation** - Draggable, editable points with auto-renumbering and hover/selected states
8
+ - 🖌️ **Multi-layer Brush Painting** - Painting and erasing with adjustable color, opacity, size, and continuity
9
+ - 🔀 **Brush Polygon from Points** - One-click generation of brush polygon area from point annotation trajectory
10
+ - 🖼️ **Local Image Upload** - Click to select or drag-and-drop local images, or use remote image URLs
11
+ - 🎨 **Complete Style Customization** - Full point and brush style configuration
12
+ - ⬅️ **Undo / Redo** - Complete command-based history management
13
+ - 📤 **Multi-format Export** - JSON / COCO / YOLO / Mask (dataURL / Blob / File)
14
+ - 📱 **Custom Toolbar Support** - Hide built-in toolbar and build custom UI via ref API
15
+ - 🔒 **Brush Disabling** - Use `enableBrush: false` to disable all brush functionality for point-only annotation
16
+ - ⌨️ **Rich Keyboard Shortcuts** - `v`, `p`, `b`, `e`, `Ctrl+Z`, `Ctrl+Y`, `Delete`, and more
17
+
18
+ ---
19
+
20
+ ## Table of Contents
21
+
22
+ - [Installation](#installation)
23
+ - [Quick Start](#quick-start)
24
+ - [Props Configuration](#props-configuration)
25
+ - [imageSource](#imagesource)
26
+ - [options](#options)
27
+ - [currentLayer (v-model:currentLayer)](#currentlayer-v-modelcurrentlayer)
28
+ - [Events](#events)
29
+ - [Ref API (Parent Component Calls)](#ref-api-parent-component-calls)
30
+ - [Point Annotation](#point-annotation)
31
+ - [Image & Canvas](#image--canvas)
32
+ - [Tool Switching](#tool-switching)
33
+ - [Delete & Clear](#delete--clear)
34
+ - [Brush Layers](#brush-layers)
35
+ - [Brush Style](#brush-style)
36
+ - [Point Trajectory to Brush Area](#point-trajectory-to-brush-area)
37
+ - [Zoom](#zoom)
38
+ - [Undo / Redo](#undo--redo)
39
+ - [Import / Export](#import--export)
40
+ - [Usage Examples](#usage-examples)
41
+ - [Minimal Example](#minimal-example)
42
+ - [Full Customization (Hide Built-in Toolbar)](#full-customization-hide-built-in-toolbar)
43
+ - [Multi-layer Brush](#multi-layer-brush)
44
+ - [Backend Upload Mask (Blob/File)](#backend-upload-mask-blobfile)
45
+ - [Point-Only Annotation (Disable Brush)](#point-only-annotation-disable-brush)
46
+ - [Keyboard Shortcuts](#keyboard-shortcuts)
47
+ - [Development & Build](#development--build)
48
+ - [License](#license)
49
+
50
+ ---
18
51
 
19
52
  ## Installation
20
53
 
@@ -36,274 +69,543 @@ yarn add @zzalai/leafer-point-annotation
36
69
  pnpm add @zzalai/leafer-point-annotation
37
70
  ```
38
71
 
39
- ## Quick Start
72
+ > ⚠️ **Note**: `vue@^3.3.0` is a peer dependency (not automatically installed, must exist in the host project).
73
+
74
+ > ⚠️ **Important**: You must manually import the CSS:
75
+ > ```ts
76
+ > import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
77
+ > ```
78
+
79
+ ---
40
80
 
41
- ### Basic Usage
81
+ ## Quick Start
42
82
 
43
83
  ```vue
44
84
  <template>
45
- <div class="demo-container">
46
- <PointAnnotation
47
- ref="annotationRef"
48
- :imageSource="imageSource"
49
- :options="options"
50
- @pointChange="handlePointChange"
51
- @loadSuccess="handleLoadSuccess"
52
- />
53
- <div class="controls">
54
- <button @click="exportJSON">Export JSON</button>
55
- <button @click="exportMask">Export Mask</button>
56
- </div>
57
- </div>
85
+ <PointAnnotation
86
+ ref="annotationRef"
87
+ :image-source="imageSource"
88
+ :options="options"
89
+ @point-change="handlePointChange"
90
+ @load-success="handleLoadSuccess"
91
+ />
58
92
  </template>
59
93
 
60
94
  <script setup lang="ts">
61
95
  import { ref, computed } from 'vue'
62
96
  import { PointAnnotation } from '@zzalai/leafer-point-annotation'
63
97
 
98
+ // 👇 Must manually import styles
99
+ import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
100
+
64
101
  const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
102
+
103
+ // Method 1: Remote image
65
104
  const imageSource = computed(() => ({
66
- id: 'demo-image',
67
- url: 'https://example.com/image.jpg'
105
+ url: 'https://example.com/sample.jpg'
68
106
  }))
69
107
 
70
- const options = ref({
108
+ // Method 2: Don't pass imageSource - user can upload locally
109
+ // const imageSource = null
110
+
111
+ const options = {
112
+ enableBrush: true,
71
113
  pointStyle: {
72
114
  circleFill: '#ff4d4f',
73
- circleStroke: '#ffffff',
74
- labelBackgroundColor: '#ffffff'
115
+ circleStroke: '#ffffff'
75
116
  },
76
117
  brushStyle: {
77
- color: '#ff4d4f',
118
+ color: '#1890ff',
78
119
  opacity: 0.55,
79
120
  size: 100
80
- },
81
- maskExportFormat: 'png',
82
- maskExportForeground: 'black'
83
- })
121
+ }
122
+ }
84
123
 
85
- const handlePointChange = (points) => {
124
+ function handlePointChange(points: any[]) {
86
125
  console.log('Points changed:', points)
87
126
  }
88
127
 
89
- const handleLoadSuccess = () => {
90
- console.log('Image loaded successfully')
128
+ function handleLoadSuccess(info: any) {
129
+ console.log('Image loaded successfully:', info)
91
130
  }
131
+ </script>
132
+ ```
92
133
 
93
- const exportJSON = () => {
94
- const json = annotationRef.value?.exportCanvasJSON()
95
- if (json) {
96
- const blob = new Blob([json], { type: 'application/json' })
97
- const url = URL.createObjectURL(blob)
98
- const a = document.createElement('a')
99
- a.href = url
100
- a.download = 'annotation.json'
101
- a.click()
102
- }
103
- }
134
+ ---
104
135
 
105
- const exportMask = async () => {
106
- const mask = await annotationRef.value?.exportMaskImage('png', 'black')
107
- if (mask) {
108
- const a = document.createElement('a')
109
- a.href = mask
110
- a.download = 'mask.png'
111
- a.click()
112
- }
113
- }
114
- </script>
136
+ ## Props Configuration
115
137
 
116
- <style scoped>
117
- .demo-container {
118
- width: 100%;
119
- height: 600px;
120
- }
138
+ ### imageSource
121
139
 
122
- .controls {
123
- margin-top: 16px;
124
- display: flex;
125
- gap: 12px;
126
- }
127
- </style>
128
- ```
140
+ | Field | Type | Description |
141
+ |-------|------|-------------|
142
+ | `url` | `string` | Image URL (remote URL or dataURL) |
143
+ | `id` | `string` | Optional, business identifier |
144
+
145
+ > When `imageSource` is not provided, the component displays a large upload area supporting **click to select** and **drag-and-drop** file loading.
129
146
 
130
- ## API Documentation
147
+ ### options
131
148
 
132
- ### Props
149
+ Complete `OptionsSource` interface:
133
150
 
134
- | Property | Type | Default | Description |
135
- |----------|------|---------|-------------|
136
- | imageSource | `{ id?: string; url: string }` | `null` | Image source configuration (optional, shows upload UI when not provided) |
137
- | options | `Object` | `{}` | Configuration options |
151
+ ```ts
152
+ interface OptionsSource {
153
+ // ============ Feature Toggles ============
154
+ enableBrush?: boolean // Whether to enable brush (default: true);
155
+ // When false, brush buttons/panel are not rendered,
156
+ // and brushTool/eraserTool/mask export methods are disabled
138
157
 
139
- #### Options Configuration
158
+ // ============ UI Toggles ============
159
+ showToolbar?: boolean // Whether to show built-in toolbar (default: true)
160
+ showZoomController?: boolean // Whether to show built-in zoom controller (default: true)
161
+ canvasBackground?: string // Canvas background color (default: '#f6f6f6')
140
162
 
141
- ```typescript
142
- interface Options {
143
- pointStyle?: Partial<PointStyle>
144
- brushStyle?: Partial<BrushStyle>
145
- maskExportFormat?: 'png' | 'jpg' | 'jpeg'
146
- maskExportForeground?: 'black' | 'white'
147
- maxUndoSteps?: number
163
+ // ============ Zoom ============
164
+ zoomMin?: number // Minimum zoom ratio (default: 0.2)
165
+ zoomMax?: number // Maximum zoom ratio (default: 4)
166
+
167
+ // ============ Point Annotation ============
168
+ pointStyle?: Partial<PointStyle> // Point annotation style (overrides defaults)
169
+ maxPoints?: number // Maximum number of points (optional)
170
+
171
+ // ============ Brush ============
172
+ brushStyle?: Partial<BrushStyle> // Brush style (overrides defaults)
173
+ brushLayers?: BrushLayerConfig[] // Brush layer configuration (defaults to single layer if not provided)
174
+ maxBrushLayers?: number // Maximum number of layers (optional)
175
+
176
+ // ============ History ============
177
+ maxUndoSteps?: number // Maximum undo steps (default: 100)
178
+
179
+ // ============ Mask Export ============
180
+ maskExportFormat?: 'png' | 'jpeg' | 'jpg' // Default mask export format (default: png)
181
+ maskExportForeground?: 'black' | 'white' // Default mask foreground color (default: black)
148
182
  }
149
183
  ```
150
184
 
151
- #### PointStyle Configuration
185
+ #### PointStyle (Point Annotation Style)
152
186
 
153
- ```typescript
187
+ ```ts
154
188
  interface PointStyle {
155
189
  circleRadius: number
156
190
  circleFill: string
157
191
  circleStroke: string
158
192
  circleStrokeWidth: number
193
+
194
+ // hover
159
195
  hoverCircleFill: string
160
196
  hoverCircleStroke: string
197
+
198
+ // selected
161
199
  selectedCircleFill: string
162
200
  selectedCircleStroke: string
163
201
  selectedCircleScale: number
202
+
203
+ // text
204
+ circleTextFontSize: number
205
+ circleTextFontFamily: string
206
+ circleTextFill: string
207
+
208
+ // label
164
209
  labelBackgroundColor: string
165
210
  labelTextColor: string
166
211
  labelFontSize: number
167
- labelPadding: number[]
168
- fixedSizeOnZoom: boolean
169
- fixedSizeScale: number
212
+ labelPadding: number | number[]
213
+
214
+ // fixed size
215
+ fixedSizeOnZoom?: boolean // When enabled, points don't grow with canvas zoom
216
+ fixedSizeScale?: number // Fixed size scale factor
170
217
  }
171
218
  ```
172
219
 
173
- #### BrushStyle Configuration
220
+ Refer to [`src/types/index.ts`](src/types/index.ts) for default values.
221
+
222
+ #### BrushStyle (Brush Style)
174
223
 
175
- ```typescript
224
+ ```ts
176
225
  interface BrushStyle {
177
- color: string
178
- opacity: number
179
- size: number
180
- minSize: number
181
- maxSize: number
182
- continuity: number
226
+ color: string // Brush color (hex)
227
+ opacity: number // Opacity 0~1 (controlled via Group.opacity)
228
+ size: number // Brush size (pixels)
229
+ minSize: number // Slider minimum
230
+ maxSize: number // Slider maximum
231
+ continuity: number // Maximum distance threshold for continuous strokes
183
232
  }
184
233
  ```
185
234
 
186
- ### Events
187
-
188
- | Event Name | Parameters | Description |
189
- |------------|------------|-------------|
190
- | pointChange | `PointAnnotation[]` | Triggered when point annotations change |
191
- | loadStart | - | Triggered when image starts loading |
192
- | loadSuccess | - | Triggered when image loads successfully |
193
- | loadError | `error` | Triggered when image loading fails |
194
- | undoStateChange | - | Triggered when undo state changes |
195
- | redoStateChange | - | Triggered when redo state changes |
196
-
197
- ### Methods
198
-
199
- The component exposes the following methods:
200
-
201
- | Method Name | Parameters | Return Value | Description |
202
- |-------------|------------|--------------|-------------|
203
- | getPointAnnotations | - | `PointAnnotation[]` | Get all point annotation data |
204
- | getImageInfo | - | `Object` | Get image information |
205
- | exportCanvasJSON | - | `string` | Export complete JSON data |
206
- | exportMaskImage | `format?`, `fgColor?` | `Promise<string|null>` | Export mask image |
207
- | exportCOCO | - | `string` | Export COCO format JSON |
208
- | exportYOLO | - | `{ annotations: string; classNames: string }` | Export YOLO format |
209
- | importCanvasJSON | `jsonString`, `options?` | `Promise<boolean>` | Import JSON data |
210
- | loadImage | `url?` | `Promise<void>` | Load image |
211
- | clearBrush | - | `void` | Clear brush content |
212
- | zoomIn | - | `void` | Zoom in canvas |
213
- | zoomOut | - | `void` | Zoom out canvas |
214
- | resetZoom | - | `void` | Reset zoom |
215
- | undo | - | `void` | Undo operation |
216
- | redo | - | `void` | Redo operation |
217
- | getCurrentTool | - | `'select'\|'point'\|'brush'\|'eraser'` | Get current tool |
218
- | setTool | `tool` | `void` | Set current tool |
219
- | createPointAnnotation | `x`, `y` | `string\|null` | Create point annotation |
220
- | removePointAnnotation | `id` | `boolean` | Remove specific point annotation |
221
-
222
- ## Hotkeys
223
-
224
- | Hotkey | Function |
225
- |--------|----------|
226
- | V | Select tool |
227
- | P | Point annotation tool |
228
- | B | Brush tool |
229
- | E | Eraser tool |
230
- | Ctrl + Z | Undo |
231
- | Ctrl + Y | Redo |
232
- | Delete | Delete selected / Clear all |
233
- | Ctrl + + | Zoom in |
234
- | Ctrl + - | Zoom out |
235
- | Ctrl + 0 | Reset zoom |
236
- | Alt | Show/Hide hotkey hints |
237
-
238
- ## Export Formats
239
-
240
- ### JSON Full
241
-
242
- Includes complete annotation data and brush mask.
243
-
244
- ```json
245
- {
246
- "version": "1.0",
247
- "imageUrl": "https://example.com/image.jpg",
248
- "imageWidth": 1280,
249
- "imageHeight": 720,
250
- "pointAnnotations": [
251
- {
252
- "id": "point_xxx",
253
- "pixel": { "x": 100, "y": 200 },
254
- "normalized": { "x": 0.078, "y": 0.278 },
255
- "label": "#1",
256
- "createdAt": 1716960000000,
257
- "updatedAt": 1716960000000
258
- }
259
- ],
260
- "brushMask": "data:image/png;base64,...",
261
- "exportTime": 1716960000000
235
+ #### BrushLayerConfig (Multi-layer Configuration)
236
+
237
+ ```ts
238
+ interface BrushLayerConfig {
239
+ label: string // Layer display name
240
+ value: string // Layer unique identifier
241
+ color?: string // Default color for this layer
242
+ opacity?: number // Default opacity for this layer
243
+ size?: number // Default brush size for this layer
262
244
  }
263
245
  ```
264
246
 
265
- ### COCO
247
+ When `brushLayers` is not provided, defaults to a single layer `{label:'Default Layer', value:'default'}`.
248
+
249
+ ### currentLayer (v-model:currentLayer)
250
+
251
+ Controlled layer switching. For example:
252
+
253
+ ```vue
254
+ <PointAnnotation
255
+ :options="{ brushLayers: [
256
+ { label: 'Foreground', value: 'foreground' },
257
+ { label: 'Occlusion', value: 'occlusion' }
258
+ ]}"
259
+ v-model:current-layer="activeLayer"
260
+ />
261
+ ```
262
+
263
+ ---
264
+
265
+ ## Events
266
+
267
+ | Event | Parameters | Triggered When |
268
+ |-------|-----------|----------------|
269
+ | `point-change` | `(points: PointAnnotation[])` | Point added / deleted / modified / renumbered |
270
+ | `load-start` | - | Image starts loading |
271
+ | `load-success` | `{ url, width, height }` | Image loads successfully |
272
+ | `load-error` | `{ error }` | Image loading fails |
273
+ | `undo-state-change` | `{ canUndo }` | Undo stack state changes |
274
+ | `redo-state-change` | `{ canRedo }` | Redo stack state changes |
275
+ | `update:currentLayer` | `layerValue` | Current brush layer changes (use with v-model) |
276
+ | `layer-change` | `layerValue` | Same as update:currentLayer |
266
277
 
267
- For keypoint detection tasks.
278
+ ---
268
279
 
269
- ### YOLO
280
+ ## Ref API (Parent Component Calls)
270
281
 
271
- For YOLO series model training.
282
+ After accessing the component instance via `ref`, you can call the following methods.
272
283
 
273
- ### Mask Image
284
+ ```ts
285
+ const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
274
286
 
275
- PNG/JPG format binary image, foreground in black/white, background transparent/white.
287
+ // Example:
288
+ annotationRef.value?.pointTool() // Switch to point annotation tool
289
+ annotationRef.value?.createBrushFromPoints() // Generate brush area from point trajectory
290
+ annotationRef.value?.getMaskBlob() // Export current layer as Blob (for backend upload)
291
+ ```
276
292
 
277
- ## Project Documentation
293
+ ### Point Annotation
278
294
 
279
- - [Requirements](./project-docs/REQUIREMENTS.md) - Detailed functional requirements
280
- - [Architecture](./project-docs/ARCHITECTURE.md) - System architecture design
281
- - [Implementation Plan](./project-docs/IMPLEMENTATION_PLAN.md) - Development task planning
282
- - [Development Guide](./project-docs/leafer-development-guide/LEAFER_DEVELOPMENT_GUIDE.md) - LeaferJS development practical guide
295
+ | Method | Description |
296
+ |--------|-------------|
297
+ | `getPointAnnotations(): PointAnnotation[]` | Get all current points |
298
+ | `createPointAnnotation(x: number, y: number, label?: string): boolean` | Programmatically add a point |
299
+ | `removePointAnnotation(id: string): boolean` | Programmatically delete a point |
300
+ | `updatePointAnnotationLabel(id: string, label: string): boolean` | Modify a point's label text |
283
301
 
284
- ## Browser Support
302
+ ### Image & Canvas
285
303
 
286
- - Chrome 60+
287
- - Firefox 55+
288
- - Safari 12+
289
- - Edge 79+
304
+ | Method | Description |
305
+ |--------|-------------|
306
+ | `getImageInfo()` | `{ url, width, height }` |
307
+ | `loadImage(url: string)` | Dynamically load a new image |
290
308
 
291
- ## Dependencies
309
+ ### Tool Switching
292
310
 
293
- - Vue 3.3.0+
294
- - LeaferUI 2.0.8+
295
- - Tinykeys 3.0.0+
296
- - @zzalai/leafer-undo-redo 1.0.3+
311
+ | Method | Description |
312
+ |--------|-------------|
313
+ | `getCurrentTool(): 'select' \| 'point' \| 'brush' \| 'eraser'` | - |
314
+ | `setTool(tool)` | Switch to specified tool (brush/eraser blocked when `enableBrush=false`) |
315
+ | `selectTool()` | Select tool |
316
+ | `pointTool()` | Point annotation tool |
317
+ | `brushTool(openPanel?: boolean)` | Brush tool |
318
+ | `eraserTool()` | Eraser tool |
297
319
 
298
- ## License
320
+ ### Delete & Clear
321
+
322
+ | Method | Description |
323
+ |--------|-------------|
324
+ | `deleteSelected()` | Delete currently selected point (with confirm dialog) |
325
+ | `clearAllAnnotationsAndBrush()` | Clear all points + brush (with confirm dialog) |
326
+ | `clearBrush()` | Clear current layer's brush |
327
+ | `clearAllBrushLayers()` | Clear all layers' brush |
328
+
329
+ ### Brush Layers
330
+
331
+ | Method | Description |
332
+ |--------|-------------|
333
+ | `getCurrentLayer(): string` | Current active layer value |
334
+ | `setActiveLayer(value: string): boolean` | Switch to specified layer |
335
+ | `getAllLayers(): BrushLayerConfig[]` | All layer configurations |
336
+
337
+ ### Brush Style
338
+
339
+ | Method | Description |
340
+ |--------|-------------|
341
+ | `getBrushStyle(): BrushStyle` | Returns copy of current style |
342
+ | `updateBrushStyle(partial: Partial<BrushStyle>): void` | Dynamically update (e.g., color/opacity/size) |
343
+
344
+ ### Point Trajectory to Brush Area
345
+
346
+ | Method | Description |
347
+ |--------|-------------|
348
+ | `createBrushFromPoints(): boolean` | Connects pixel coordinates of points in `sequenceNumber` order to form closed polygon, fills with current brush style; no action when point count < 3 |
349
+
350
+ ### Zoom
351
+
352
+ | Method | Description |
353
+ |--------|-------------|
354
+ | `zoomIn()` | Zoom in |
355
+ | `zoomOut()` | Zoom out |
356
+ | `resetZoom()` | Reset to 100% |
357
+
358
+ ### Undo / Redo
359
+
360
+ | Method | Description |
361
+ |--------|-------------|
362
+ | `undo()` | Undo last action |
363
+ | `redo()` | Redo last action |
299
364
 
300
- MIT License
365
+ ### Import / Export
301
366
 
302
- ## Contributing
367
+ | Method | Description |
368
+ |--------|-------------|
369
+ | `exportCanvasJSON(): string` | Full export (points + brush snapshot + image info) |
370
+ | `importCanvasJSON(data: string \| object): boolean` | Restore canvas from exported JSON |
371
+ | `exportMaskImage(format?, fg?)`: `Promise<string \| null>` | Current layer mask (dataURL) |
372
+ | `exportMaskImageByLayer(layerValue, format?, fg?)`: `Promise<string \| null>` | Specified layer mask |
373
+ | `exportAllMaskImages(format?, fg?)`: `Promise<Record<string, string>>` | All layer masks |
374
+ | `getMaskBlob(layerValue?, format?, fg?)`: `Promise<Blob \| null>` | Current/specified layer Blob (for backend upload) |
375
+ | `getMaskFile(layerValue?, filename?, format?, fg?)`: `Promise<File \| null>` | Current/specified layer File |
376
+ | `getAllMaskBlobs(format?, fg?)`: `Promise<Record<string, Blob>>` | All layer Blob collection |
377
+ | `exportCOCO(): string` | Export COCO JSON (points = keypoints) |
378
+ | `exportYOLO(): string` | Export YOLO annotations |
303
379
 
304
- Issues and Pull Requests are welcome!
380
+ > Parameter notes: `format` = `'png' \| 'jpeg' \| 'jpg'`; `fg` = `'black' \| 'white'` (mask foreground color).
381
+ > Note: All Mask/Blob/File related methods require browser environment, and return null/{} when `enableBrush=false`.
382
+
383
+ ---
384
+
385
+ ## Usage Examples
386
+
387
+ ### Minimal Example
388
+
389
+ ```vue
390
+ <template>
391
+ <PointAnnotation
392
+ ref="annotationRef"
393
+ :image-source="{ url: 'https://example.com/image.jpg' }"
394
+ :options="{ enableBrush: false }"
395
+ />
396
+ </template>
397
+
398
+ <script setup lang="ts">
399
+ import { ref } from 'vue'
400
+ import { PointAnnotation } from '@zzalai/leafer-point-annotation'
401
+ import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
402
+
403
+ const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
404
+ </script>
405
+ ```
406
+
407
+ ### Full Customization (Hide Built-in Toolbar)
408
+
409
+ ```vue
410
+ <template>
411
+ <div class="my-toolbar">
412
+ <button @click="() => annotationRef.value?.pointTool()">Point</button>
413
+ <button @click="() => annotationRef.value?.brushTool()">Brush</button>
414
+ <button @click="() => annotationRef.value?.eraserTool()">Eraser</button>
415
+ <button @click="() => annotationRef.value?.deleteSelected()">Delete</button>
416
+ <button @click="() => annotationRef.value?.clearAllAnnotationsAndBrush()">Clear All</button>
417
+ <button @click="() => annotationRef.value?.undo()">Undo</button>
418
+ <button @click="() => annotationRef.value?.redo()">Redo</button>
419
+ <button @click="() => annotationRef.value?.createBrushFromPoints()">Points→Polygon</button>
420
+ <button @click="uploadMask">Upload Mask</button>
421
+ </div>
305
422
 
306
- ## Related Projects
423
+ <PointAnnotation
424
+ ref="annotationRef"
425
+ :image-source="{ url: 'https://example.com/image.jpg' }"
426
+ :options="{ showToolbar: false, showZoomController: false }"
427
+ @point-change="handlePoints"
428
+ />
429
+ </template>
430
+
431
+ <script setup lang="ts">
432
+ import { ref } from 'vue'
433
+ import { PointAnnotation } from '@zzalai/leafer-point-annotation'
434
+ import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
435
+
436
+ const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
437
+
438
+ function handlePoints(points: any[]) {
439
+ console.log('points:', points)
440
+ }
441
+
442
+ async function uploadMask() {
443
+ const blob = await annotationRef.value?.getMaskBlob()
444
+ if (!blob) return
445
+ const fd = new FormData()
446
+ fd.append('file', blob, 'mask.png')
447
+ await fetch('/api/upload', { method: 'POST', body: fd })
448
+ }
449
+ </script>
450
+ ```
451
+
452
+ ### Multi-layer Brush
453
+
454
+ ```vue
455
+ <template>
456
+ <PointAnnotation
457
+ ref="annotationRef"
458
+ :image-source="{ url: 'https://example.com/image.jpg' }"
459
+ :options="{
460
+ brushLayers: [
461
+ { label: 'Foreground', value: 'foreground', color: '#1890ff', opacity: 0.55 },
462
+ { label: 'Occlusion', value: 'occlusion', color: '#faad14', opacity: 0.55 },
463
+ { label: 'Background', value: 'background', color: '#52c41a', opacity: 0.55 }
464
+ ]
465
+ }"
466
+ v-model:current-layer="activeLayer"
467
+ />
468
+ </template>
469
+
470
+ <script setup lang="ts">
471
+ import { ref } from 'vue'
472
+ import { PointAnnotation } from '@zzalai/leafer-point-annotation'
473
+ import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
474
+
475
+ const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
476
+ const activeLayer = ref('foreground')
477
+ </script>
478
+ ```
479
+
480
+ ### Backend Upload Mask (Blob/File)
481
+
482
+ ```vue
483
+ <template>
484
+ <PointAnnotation ref="annotationRef" :image-source="{ url: '...' }" />
485
+ <button @click="uploadAllMasks">Upload All Layer Masks</button>
486
+ </template>
487
+
488
+ <script setup lang="ts">
489
+ import { ref } from 'vue'
490
+ import { PointAnnotation } from '@zzalai/leafer-point-annotation'
491
+ import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
492
+
493
+ const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
494
+
495
+ async function uploadAllMasks() {
496
+ const blobs = await annotationRef.value?.getAllMaskBlobs('png', 'black')
497
+ if (!blobs) return
498
+ for (const [layerValue, blob] of Object.entries(blobs)) {
499
+ const fd = new FormData()
500
+ fd.append('file', blob, `${layerValue}.png`)
501
+ await fetch('/api/mask-upload', { method: 'POST', body: fd })
502
+ }
503
+ }
504
+ </script>
505
+ ```
506
+
507
+ ### Point-Only Annotation (Disable Brush)
508
+
509
+ ```vue
510
+ <template>
511
+ <PointAnnotation
512
+ ref="annotationRef"
513
+ :image-source="{ url: '...' }"
514
+ :options="{ enableBrush: false }"
515
+ @point-change="handlePoints"
516
+ />
517
+ </template>
518
+
519
+ <script setup lang="ts">
520
+ import { ref } from 'vue'
521
+ import { PointAnnotation } from '@zzalai/leafer-point-annotation'
522
+ import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
523
+
524
+ const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
525
+ function handlePoints(points: any[]) {
526
+ console.log('Points:', points)
527
+ }
528
+ </script>
529
+ ```
530
+
531
+ ---
532
+
533
+ ## Keyboard Shortcuts
534
+
535
+ > Effective when: **Canvas has focus** or **mouse hovers over canvas**
536
+
537
+ | Key | Function | Restriction |
538
+ |-----|----------|-------------|
539
+ | `v` | Select tool | - |
540
+ | `p` | Point annotation tool | - |
541
+ | `b` | Brush tool | Requires `enableBrush=true` |
542
+ | `e` | Eraser tool | Requires `enableBrush=true` |
543
+ | `Ctrl + Z` | Undo | - |
544
+ | `Ctrl + Y` | Redo | - |
545
+ | `Delete` | Delete selected point | - |
546
+ | `Ctrl + +` | Zoom in | - |
547
+ | `Ctrl + -` | Zoom out | - |
548
+ | `Ctrl + 0` | Reset zoom | - |
549
+ | `Alt` | Show/Hide shortcut hint overlay | - |
550
+
551
+ ---
552
+
553
+ ## Development & Build
554
+
555
+ ```bash
556
+ # Install dependencies
557
+ pnpm install
558
+
559
+ # Local development (App.vue as demo entry)
560
+ pnpm dev
561
+
562
+ # Build library output (dist/)
563
+ pnpm build
564
+
565
+ # Build demo site (docs/)
566
+ pnpm docs:build
567
+
568
+ # Build library + demo site simultaneously
569
+ pnpm build:all
570
+
571
+ # Type check
572
+ pnpm tsc --noEmit
573
+ ```
574
+
575
+ ### Project Structure
576
+
577
+ ```
578
+ src/
579
+ ├── components/
580
+ │ ├── PointAnnotation.vue # Core main component (integrates all capabilities)
581
+ │ ├── BrushSizeSlider.vue # Brush size slider
582
+ │ └── BrushStylePanel.vue # Brush style configuration panel
583
+ ├── elements/
584
+ │ └── PointAnnotationElement.ts # Custom point element (Group + Ellipse + Text)
585
+ ├── utils/
586
+ │ ├── CanvasBrush.ts # Brush core (canvas + drawing snapshot)
587
+ │ ├── BrushCommands.ts # Brush undo commands
588
+ │ ├── PointCommands.ts # Point undo commands
589
+ │ ├── BrushStroke.ts # Brush stroke data
590
+ │ ├── COCOExporter.ts # COCO export
591
+ │ └── YOLOExporter.ts # YOLO export
592
+ ├── types/
593
+ │ └── index.ts # All public types and default values
594
+ ├── App.vue # Dev demo page
595
+ ├── index.ts # Public export
596
+ └── main.ts # Dev entry
597
+ ```
598
+
599
+ ### Release Process
600
+
601
+ 1. `pnpm install` → `pnpm build:all`
602
+ 2. Confirm `dist/` and `docs/` are up to date
603
+ 3. Update `version` in `package.json`
604
+ 4. `npm publish`
605
+ 5. `git push` to GitHub (triggers Pages redeployment)
606
+
607
+ ---
608
+
609
+ ## License
307
610
 
308
- - [@zzalai/leafer-multi-roi](https://github.com/otaku1951/leafer-multi-roi) - Multi-region ROI annotation tool
309
- - [@zzalai/leafer-undo-redo](https://github.com/otaku1951/leafer-undo-redo) - LeaferJS undo/redo plugin
611
+ MIT © zzalai