@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.
@@ -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 = data.label;
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
- hoverStyle: {
35
- fill: this.style.hoverCircleFill ? this.style.hoverCircleFill : this.style.circleFill
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: data.label,
84
+ text: this._defaultLabel,
44
85
  fontSize: this.style.labelFontSize,
45
86
  fill: this.style.labelTextColor,
46
87
  padding: this.style.labelPadding,
47
- editable: true,
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
- // this.hitChildren = false;
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
  }
@@ -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: string;
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: 12, // 8x8 的圆点
98
- circleFill: '#ff4d4f',
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: '#1890ff',
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: 12,
125
+ labelFontSize: 16,
109
126
  labelPadding: [2,4],
110
127
  fixedSizeOnZoom: false, // 默认关闭固定大小功能
111
128
  fixedSizeScale: 1, // 默认缩放系数为1
@@ -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
  }