@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,23 +1,55 @@
|
|
|
1
|
-
import { Group, Ellipse, Text } from 'leafer-ui';
|
|
1
|
+
import { Group, Ellipse, Text, PointerEvent } from 'leafer-ui';
|
|
2
|
+
// import { EllipseBox } from '@leafer-in/box'
|
|
2
3
|
import type { PointAnnotation, PointStyle } from '@/types';
|
|
3
4
|
import { DEFAULT_POINT_STYLE } from '@/types';
|
|
4
5
|
import "@leafer-in/state"
|
|
5
|
-
import "@leafer-in/text-editor"
|
|
6
|
+
import "@leafer-in/text-editor" // 引入以支持 label.text 的 property.change 事件(虽然当前 label 不可编辑,但保留以防未来启用)
|
|
6
7
|
|
|
7
8
|
export class PointAnnotationElement extends Group {
|
|
8
9
|
public circle: Ellipse;
|
|
10
|
+
public circleText: Text;
|
|
9
11
|
public label: Text;
|
|
10
12
|
public data: PointAnnotation;
|
|
11
13
|
private style: PointStyle;
|
|
12
|
-
public _element_tag: string
|
|
14
|
+
public _element_tag: string;
|
|
13
15
|
private _lastValidLabel: string;
|
|
16
|
+
private _isSelected: boolean;
|
|
17
|
+
private _defaultLabel: string;
|
|
18
|
+
private _isRenumbering: boolean; // 是否在 renumber 期间,用于防止 label.text 变更触发 handleLabelChange 把 data.label 固定
|
|
14
19
|
|
|
15
20
|
constructor(data: PointAnnotation, style?: Partial<PointStyle>) {
|
|
16
21
|
super();
|
|
17
|
-
|
|
22
|
+
|
|
23
|
+
this._defaultLabel = data.label ?? `#${data.sequenceNumber}`;
|
|
18
24
|
this.data = data;
|
|
19
25
|
this.style = { ...DEFAULT_POINT_STYLE, ...style };
|
|
20
|
-
this._lastValidLabel =
|
|
26
|
+
this._lastValidLabel = this._defaultLabel;
|
|
27
|
+
this._isSelected = false;
|
|
28
|
+
this._isRenumbering = false;
|
|
29
|
+
|
|
30
|
+
// 创建圆点文本
|
|
31
|
+
this.circleText = new Text({
|
|
32
|
+
x: - this.style.circleRadius / 2,
|
|
33
|
+
y: - this.style.circleRadius / 2,
|
|
34
|
+
text: data.sequenceNumber ?? data.order, // 优先显示当前位置序号,兼容旧数据回退到 order
|
|
35
|
+
width: this.style.circleRadius,
|
|
36
|
+
height: this.style.circleRadius,
|
|
37
|
+
lineHeight: this.style.circleRadius,
|
|
38
|
+
fontSize: this.style.circleTextFontSize,
|
|
39
|
+
fontFamily: this.style.circleTextFontFamily,
|
|
40
|
+
fill: this.style.circleTextFill,
|
|
41
|
+
editable: false,
|
|
42
|
+
editConfig: {
|
|
43
|
+
strokeWidth: 0, // 需要置0,否则有边难看
|
|
44
|
+
moveable: false,
|
|
45
|
+
resizeable: false
|
|
46
|
+
},
|
|
47
|
+
// around: 'center',
|
|
48
|
+
textAlign: 'center',
|
|
49
|
+
// boxStyle: {
|
|
50
|
+
// fill: this.style.labelBackgroundColor,
|
|
51
|
+
// }
|
|
52
|
+
});
|
|
21
53
|
|
|
22
54
|
// 创建圆点 - Ellipse 的 x, y 是圆心位置,around 设置为 center
|
|
23
55
|
this.circle = new Ellipse({
|
|
@@ -31,20 +63,29 @@ export class PointAnnotationElement extends Group {
|
|
|
31
63
|
draggable: false,
|
|
32
64
|
editable: false,
|
|
33
65
|
around: 'center',
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
66
|
+
// transition: {
|
|
67
|
+
// duration: 200,
|
|
68
|
+
// easing: 'bounce-out',
|
|
69
|
+
// },
|
|
70
|
+
// hoverStyle: {
|
|
71
|
+
// fill: this.style.hoverCircleFill ? this.style.hoverCircleFill : this.style.circleFill
|
|
72
|
+
// },
|
|
37
73
|
});
|
|
38
74
|
|
|
39
75
|
// 创建标签 - around 设置为 bottom-left,显示在圆点右上角
|
|
76
|
+
// 注意:当前 label 不可编辑,主要用于展示自定义名称或默认 "#序号"。
|
|
77
|
+
// editable=false + hitChildren=false 双重保证用户无法直接编辑 label。
|
|
78
|
+
// 如需启用编辑,需要同时:
|
|
79
|
+
// 1) 把 hitChildren 改为 true(让 Group 的子元素接收鼠标事件)
|
|
80
|
+
// 2) 把 label.editable 改为 true
|
|
40
81
|
this.label = new Text({
|
|
41
82
|
x: this.style.circleRadius / 2 - 2,
|
|
42
83
|
y: - this.style.circleRadius / 2 + 2,
|
|
43
|
-
text:
|
|
84
|
+
text: this._defaultLabel,
|
|
44
85
|
fontSize: this.style.labelFontSize,
|
|
45
86
|
fill: this.style.labelTextColor,
|
|
46
87
|
padding: this.style.labelPadding,
|
|
47
|
-
editable:
|
|
88
|
+
editable: false,
|
|
48
89
|
editConfig: {
|
|
49
90
|
strokeWidth: 0, // 需要置0,否则有边难看
|
|
50
91
|
moveable: false,
|
|
@@ -66,6 +107,7 @@ export class PointAnnotationElement extends Group {
|
|
|
66
107
|
|
|
67
108
|
// 添加到组
|
|
68
109
|
this.add(this.circle);
|
|
110
|
+
this.add(this.circleText);
|
|
69
111
|
this.add(this.label);
|
|
70
112
|
|
|
71
113
|
// 设置组属性 - 组的位置就是圆心位置
|
|
@@ -73,7 +115,11 @@ export class PointAnnotationElement extends Group {
|
|
|
73
115
|
this.y = data.pixel.y;
|
|
74
116
|
this.draggable = true;
|
|
75
117
|
this.editable = true;
|
|
76
|
-
//
|
|
118
|
+
// hitChildren=false:让 Group 作为整体接收事件,不穿透到子元素(circle、circleText、label)。
|
|
119
|
+
// 副作用:label 的 editable 设置无效(因为 label 收不到鼠标事件)。
|
|
120
|
+
// 这是一个**有意的设计决策**:当前不需要编辑 label,整体交互更简洁。
|
|
121
|
+
this.hitChildren = false;
|
|
122
|
+
// this.hitChildren = true; // 如果要启用 label 编辑,需要启用此行并注释上一行
|
|
77
123
|
this.editConfig = {
|
|
78
124
|
strokeWidth: 0, // 需要置0,否则有边难看
|
|
79
125
|
resizeable: false
|
|
@@ -85,11 +131,30 @@ export class PointAnnotationElement extends Group {
|
|
|
85
131
|
}
|
|
86
132
|
|
|
87
133
|
private bindEvents(): void {
|
|
134
|
+
this.on(PointerEvent.ENTER, () => {
|
|
135
|
+
this.circle.set({
|
|
136
|
+
fill: this.style.selectedCircleFill,
|
|
137
|
+
stroke: this.style.selectedCircleStroke,
|
|
138
|
+
})
|
|
139
|
+
// this.label.editable = true;
|
|
140
|
+
})
|
|
141
|
+
this.on(PointerEvent.LEAVE, () => {
|
|
142
|
+
if (this._isSelected) return
|
|
143
|
+
this.circle.set({
|
|
144
|
+
fill: this.style.circleFill,
|
|
145
|
+
stroke: this.style.circleStroke,
|
|
146
|
+
})
|
|
147
|
+
// this.label.editable = true;
|
|
148
|
+
})
|
|
88
149
|
// Hover 效果已通过 hoverStyle 配置,无需监听事件
|
|
89
150
|
// this.circle.on('pointerenter', () => this.updateHover(true));
|
|
90
151
|
// this.circle.on('pointerleave', () => this.updateHover(false));
|
|
91
152
|
|
|
92
153
|
// 标签编辑完成
|
|
154
|
+
// 说明:当前 label 是 editable=false,这个事件**不会被用户操作触发**。
|
|
155
|
+
// 保留监听是为了:
|
|
156
|
+
// 1) 外部通过 updateLabel() 编程式修改 label 时也能触发一致性检查
|
|
157
|
+
// 2) 未来启用 label 编辑时不需要改这里
|
|
93
158
|
// this.label.on('text:change', (e: any) => this.handleLabelChange(e.text));
|
|
94
159
|
this.label.on('property.change', (e) => {
|
|
95
160
|
// console.log(e);
|
|
@@ -106,12 +171,15 @@ export class PointAnnotationElement extends Group {
|
|
|
106
171
|
public handlePointAnnotationSelected(isSelected: boolean): void {
|
|
107
172
|
const targetStyle = isSelected ? this.style.selectedCircleFill : this.style.circleFill;
|
|
108
173
|
const targetStroke = isSelected ? this.style.selectedCircleStroke : this.style.circleStroke;
|
|
109
|
-
const targetScale = isSelected ? this.style.selectedCircleScale : 1;
|
|
174
|
+
// const targetScale = isSelected ? this.style.selectedCircleScale : 1;
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
this._isSelected = isSelected;
|
|
110
178
|
|
|
111
179
|
this.circle.set({
|
|
112
180
|
fill: targetStyle,
|
|
113
181
|
stroke: targetStroke,
|
|
114
|
-
scale: targetScale,
|
|
182
|
+
// scale: targetScale,
|
|
115
183
|
});
|
|
116
184
|
}
|
|
117
185
|
|
|
@@ -119,12 +187,20 @@ export class PointAnnotationElement extends Group {
|
|
|
119
187
|
// 去除首尾空格
|
|
120
188
|
const trimmedLabel = newLabel.trim();
|
|
121
189
|
|
|
122
|
-
//
|
|
190
|
+
// 空值或空白,恢复上一次的值(保证 label 永不为空)
|
|
123
191
|
if (!trimmedLabel) {
|
|
124
|
-
this.label.text = this._lastValidLabel;
|
|
192
|
+
// this.label.text = this._lastValidLabel;
|
|
193
|
+
this.label.text = this._defaultLabel;
|
|
125
194
|
return;
|
|
126
195
|
}
|
|
127
|
-
|
|
196
|
+
|
|
197
|
+
// renumber 期间:只更新 _lastValidLabel 和显示,不固定 data.label
|
|
198
|
+
// 防止 renumber 时 label.text 变化触发此函数,把 data.label 永久固定下来
|
|
199
|
+
if (this._isRenumbering) {
|
|
200
|
+
this._lastValidLabel = trimmedLabel;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
128
204
|
// 更新有效标签
|
|
129
205
|
this._lastValidLabel = trimmedLabel;
|
|
130
206
|
this.data.label = trimmedLabel;
|
|
@@ -146,10 +222,32 @@ export class PointAnnotationElement extends Group {
|
|
|
146
222
|
}
|
|
147
223
|
|
|
148
224
|
public getLabel(): string {
|
|
149
|
-
return this.data.label;
|
|
225
|
+
return this.data.label ?? this._defaultLabel;
|
|
150
226
|
}
|
|
151
227
|
|
|
152
228
|
public getLastValidLabel(): string {
|
|
153
229
|
return this._lastValidLabel;
|
|
154
230
|
}
|
|
231
|
+
|
|
232
|
+
// 更新圆内显示的序号(删除点后自动重排时调用)
|
|
233
|
+
// 逻辑:如果用户改过 label(this.data.label 有值),只更新 circleText;
|
|
234
|
+
// 如果 label 仍是默认值("#序号"),则同步更新 label,保持序号一致性。
|
|
235
|
+
public updateSequenceNumber(newNum: number): void {
|
|
236
|
+
this.data.sequenceNumber = newNum;
|
|
237
|
+
if (this.circleText) {
|
|
238
|
+
this.circleText.text = String(newNum);
|
|
239
|
+
}
|
|
240
|
+
// 用户没改过 label → label 跟随序号一起更新;
|
|
241
|
+
// 用户已改过 label → label 保持用户自定义,不更新。
|
|
242
|
+
// 使用 _isRenumbering 标记防止 label.text 变更触发 handleLabelChange 把 data.label 固定下来。
|
|
243
|
+
if (!this.data.label) {
|
|
244
|
+
this._isRenumbering = true;
|
|
245
|
+
try {
|
|
246
|
+
this._defaultLabel = `#${newNum}`;
|
|
247
|
+
this.label.text = this._defaultLabel;
|
|
248
|
+
} finally {
|
|
249
|
+
this._isRenumbering = false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
155
253
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// 点标注数据结构
|
|
2
2
|
export interface PointAnnotation {
|
|
3
3
|
id: string;
|
|
4
|
+
order: number; // 创建时的顺序(永久不变,用于 ID 生成和历史记录)
|
|
5
|
+
sequenceNumber: number; // 当前位置的显示序号(会随增删点自动重排)
|
|
4
6
|
pixel: {
|
|
5
7
|
x: number;
|
|
6
8
|
y: number;
|
|
@@ -9,7 +11,7 @@ export interface PointAnnotation {
|
|
|
9
11
|
x: number;
|
|
10
12
|
y: number;
|
|
11
13
|
};
|
|
12
|
-
label
|
|
14
|
+
label?: string;
|
|
13
15
|
createdAt: number;
|
|
14
16
|
updatedAt: number;
|
|
15
17
|
}
|
|
@@ -25,6 +27,9 @@ export interface PointStyle {
|
|
|
25
27
|
selectedCircleFill: string;
|
|
26
28
|
selectedCircleStroke: string;
|
|
27
29
|
selectedCircleScale: number;
|
|
30
|
+
circleTextFontSize: number;
|
|
31
|
+
circleTextFontFamily: string;
|
|
32
|
+
circleTextFill: string;
|
|
28
33
|
labelBackgroundColor: string;
|
|
29
34
|
labelTextColor: string;
|
|
30
35
|
labelFontSize: number;
|
|
@@ -33,6 +38,15 @@ export interface PointStyle {
|
|
|
33
38
|
fixedSizeScale?: number; // 固定大小的缩放系数(默认为1)
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
// 笔刷图层配置
|
|
42
|
+
export interface BrushLayerConfig {
|
|
43
|
+
label: string; // 显示名称(下拉选框显示)
|
|
44
|
+
value: string; // 图层唯一标识
|
|
45
|
+
color?: string; // 该图层默认颜色
|
|
46
|
+
opacity?: number; // 该图层默认透明度
|
|
47
|
+
size?: number; // 该图层默认笔刷大小
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
// 笔刷样式配置
|
|
37
51
|
export interface BrushStyle {
|
|
38
52
|
color: string;
|
|
@@ -94,18 +108,21 @@ export interface ExportData {
|
|
|
94
108
|
|
|
95
109
|
// 默认点标注样式
|
|
96
110
|
export const DEFAULT_POINT_STYLE: PointStyle = {
|
|
97
|
-
circleRadius:
|
|
98
|
-
circleFill: '#
|
|
111
|
+
circleRadius: 20, // 20x20 的圆点
|
|
112
|
+
circleFill: '#1890ff',
|
|
99
113
|
circleStroke: '#ffffff',
|
|
100
114
|
circleStrokeWidth: 2,
|
|
101
115
|
hoverCircleFill: '#ff7875',
|
|
102
116
|
hoverCircleStroke: '#ffffff',
|
|
103
|
-
selectedCircleFill: '#
|
|
117
|
+
selectedCircleFill: '#ff4d4f',
|
|
104
118
|
selectedCircleStroke: '#ffffff',
|
|
105
119
|
selectedCircleScale: 1.2,
|
|
120
|
+
circleTextFontSize: 12,
|
|
121
|
+
circleTextFontFamily: '"Open Sans", Tahoma, Consolas, monospace, sans-serif, Roboto',
|
|
122
|
+
circleTextFill: 'white',
|
|
106
123
|
labelBackgroundColor: '#ffffff',
|
|
107
124
|
labelTextColor: '#333333',
|
|
108
|
-
labelFontSize:
|
|
125
|
+
labelFontSize: 16,
|
|
109
126
|
labelPadding: [2,4],
|
|
110
127
|
fixedSizeOnZoom: false, // 默认关闭固定大小功能
|
|
111
128
|
fixedSizeScale: 1, // 默认缩放系数为1
|
package/src/utils/CanvasBrush.ts
CHANGED
|
@@ -176,4 +176,24 @@ export class CanvasBrush {
|
|
|
176
176
|
}
|
|
177
177
|
return false;
|
|
178
178
|
}
|
|
179
|
+
|
|
180
|
+
// 根据点集合绘制闭合多边形并填充
|
|
181
|
+
// 用于把标注点的轨迹一键生成笔刷区域
|
|
182
|
+
public fillPolygon(points: { x: number; y: number }[], color: string): void {
|
|
183
|
+
if (points.length < 3) return;
|
|
184
|
+
const { context } = this.canvas;
|
|
185
|
+
context.save();
|
|
186
|
+
context.fillStyle = color;
|
|
187
|
+
context.globalAlpha = 1;
|
|
188
|
+
context.beginPath();
|
|
189
|
+
context.moveTo(points[0].x, points[0].y);
|
|
190
|
+
for (let i = 1; i < points.length; i++) {
|
|
191
|
+
context.lineTo(points[i].x, points[i].y);
|
|
192
|
+
}
|
|
193
|
+
context.closePath();
|
|
194
|
+
context.fill();
|
|
195
|
+
context.restore();
|
|
196
|
+
this.canvas.paint();
|
|
197
|
+
this.group.set({ dirty: true });
|
|
198
|
+
}
|
|
179
199
|
}
|