@operato/image-marker 8.0.0-alpha.10 → 8.0.0-alpha.18

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/CHANGELOG.md CHANGED
@@ -3,6 +3,24 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [8.0.0-alpha.18](https://github.com/hatiolab/operato/compare/v8.0.0-alpha.17...v8.0.0-alpha.18) (2024-10-04)
7
+
8
+
9
+ ### :bug: Bug Fix
10
+
11
+ * ox-image-marker ([ae56c0a](https://github.com/hatiolab/operato/commit/ae56c0a28af20d029e907181af0babe63526cae5))
12
+
13
+
14
+
15
+ ## [8.0.0-alpha.17](https://github.com/hatiolab/operato/compare/v8.0.0-alpha.16...v8.0.0-alpha.17) (2024-10-04)
16
+
17
+
18
+ ### :bug: Bug Fix
19
+
20
+ * ox-image-marker ([7393507](https://github.com/hatiolab/operato/commit/73935070ba969772cdf6e8c1eff90b0379f535b1))
21
+
22
+
23
+
6
24
  ## [8.0.0-alpha.10](https://github.com/hatiolab/operato/compare/v8.0.0-alpha.9...v8.0.0-alpha.10) (2024-09-18)
7
25
 
8
26
 
@@ -12,6 +12,10 @@ export declare class OxImageMarker extends BaseImageMarker {
12
12
  private startY;
13
13
  private offsetX;
14
14
  private offsetY;
15
+ connectedCallback(): void;
16
+ disconnectedCallback(): void;
17
+ private handleKeyDown;
18
+ private deleteSelectedShape;
15
19
  render(): import("lit-html").TemplateResult<1>;
16
20
  protected onSvgMouseDown(event: MouseEvent): void;
17
21
  private startShapeDragging;
@@ -30,4 +34,7 @@ export declare class OxImageMarker extends BaseImageMarker {
30
34
  private onHandleMouseMove;
31
35
  private onHandleMouseUp;
32
36
  private onTextDoubleClick;
37
+ private showTextInput;
38
+ private adjustInputWidth;
39
+ private measureTextWidth;
33
40
  }
@@ -15,53 +15,119 @@ let OxImageMarker = class OxImageMarker extends BaseImageMarker {
15
15
  this.startY = 0;
16
16
  this.offsetX = 0;
17
17
  this.offsetY = 0;
18
- // 마우스 이동 이벤트 핸들러 (도형 그리기 중)
18
+ this.handleKeyDown = (event) => {
19
+ if ((event.key === 'Delete' || event.key === 'Backspace') && this.selectedShape) {
20
+ this.deleteSelectedShape();
21
+ }
22
+ };
19
23
  this.onMouseMove = (event) => {
20
- if (!this.drawing || !this.selectedShape)
24
+ if (!this.drawing)
21
25
  return;
22
26
  const { x, y } = this.getSvgPoint(event);
23
- switch (this.selectedShape.type) {
24
- case 'text':
25
- break;
26
- case 'rectangle':
27
- this.selectedShape.width = x - this.startX;
28
- this.selectedShape.height = y - this.startY;
29
- break;
30
- case 'circle':
31
- const dx = x - this.selectedShape.cx;
32
- const dy = y - this.selectedShape.cy;
33
- this.selectedShape.r = Math.sqrt(dx * dx + dy * dy);
34
- break;
35
- case 'triangle':
36
- const x2 = x;
37
- const y2 = y;
38
- const x3 = this.startX - (x - this.startX);
39
- const y3 = y;
40
- this.selectedShape.points = `${this.startX},${this.startY} ${x2},${y2} ${x3},${y3}`;
41
- break;
42
- case 'line':
43
- case 'arrow':
44
- this.selectedShape.x2 = x;
45
- this.selectedShape.y2 = y;
46
- break;
27
+ const minDistance = 5;
28
+ const distance = Math.sqrt(Math.pow(x - this.startX, 2) + Math.pow(y - this.startY, 2));
29
+ if (!this.selectedShape && distance >= minDistance) {
30
+ let newShape;
31
+ switch (this.currentShapeType) {
32
+ case 'rectangle':
33
+ newShape = {
34
+ id: Date.now().toString(),
35
+ type: 'rectangle',
36
+ x: this.startX,
37
+ y: this.startY,
38
+ width: 0,
39
+ height: 0
40
+ };
41
+ break;
42
+ case 'circle':
43
+ newShape = {
44
+ id: Date.now().toString(),
45
+ type: 'circle',
46
+ cx: this.startX,
47
+ cy: this.startY,
48
+ r: 0
49
+ };
50
+ break;
51
+ case 'triangle':
52
+ newShape = {
53
+ id: Date.now().toString(),
54
+ type: 'triangle',
55
+ x1: this.startX,
56
+ y1: this.startY,
57
+ width: 0,
58
+ height: 0,
59
+ points: `${this.startX},${this.startY} ${this.startX},${this.startY} ${this.startX},${this.startY}`
60
+ };
61
+ break;
62
+ case 'line':
63
+ newShape = {
64
+ id: Date.now().toString(),
65
+ type: 'line',
66
+ x1: this.startX,
67
+ y1: this.startY,
68
+ width: 0,
69
+ height: 0
70
+ };
71
+ break;
72
+ case 'arrow':
73
+ newShape = {
74
+ id: Date.now().toString(),
75
+ type: 'arrow',
76
+ x1: this.startX,
77
+ y1: this.startY,
78
+ width: 0,
79
+ height: 0
80
+ };
81
+ break;
82
+ default:
83
+ return;
84
+ }
85
+ this.selectedShape = newShape;
86
+ this.shapes = [...this.shapes, newShape];
87
+ }
88
+ if (this.selectedShape) {
89
+ switch (this.selectedShape.type) {
90
+ case 'rectangle':
91
+ this.selectedShape.x = Math.min(x, this.startX);
92
+ this.selectedShape.y = Math.min(y, this.startY);
93
+ this.selectedShape.width = Math.abs(x - this.startX);
94
+ this.selectedShape.height = Math.abs(y - this.startY);
95
+ break;
96
+ case 'circle':
97
+ const dx = x - this.selectedShape.cx;
98
+ const dy = y - this.selectedShape.cy;
99
+ this.selectedShape.r = Math.sqrt(dx * dx + dy * dy);
100
+ break;
101
+ case 'triangle':
102
+ const x2 = x;
103
+ const y2 = y;
104
+ const x3 = this.startX - (x - this.startX);
105
+ const y3 = y;
106
+ this.selectedShape.points = `${this.startX},${this.startY} ${x2},${y2} ${x3},${y3}`;
107
+ break;
108
+ case 'line':
109
+ case 'arrow':
110
+ this.selectedShape.x2 = x;
111
+ this.selectedShape.y2 = y;
112
+ break;
113
+ }
114
+ this.requestUpdate();
47
115
  }
48
- this.requestUpdate();
49
116
  };
50
- // 마우스 업 이벤트 핸들러 (도형 그리기 종료)
51
117
  this.onMouseUp = () => {
52
118
  this.drawing = false;
53
119
  const svg = this.svg;
54
120
  svg.removeEventListener('mousemove', this.onMouseMove);
55
121
  svg.removeEventListener('mouseup', this.onMouseUp);
56
- this.selectedShape.selected = true;
122
+ if (this.selectedShape) {
123
+ this.selectedShape.selected = true;
124
+ }
57
125
  this.requestUpdate();
58
126
  };
59
127
  this.onShapeMouseMove = (event) => {
60
- if (!this.selectedShape) {
128
+ if (!this.selectedShape)
61
129
  return;
62
- }
63
130
  const { x, y } = this.getSvgPoint(event);
64
- // 마우스 이동에 따라 도형의 위치 업데이트
65
131
  switch (this.selectedShape.type) {
66
132
  case 'text':
67
133
  case 'rectangle':
@@ -91,7 +157,7 @@ let OxImageMarker = class OxImageMarker extends BaseImageMarker {
91
157
  this.selectedShape.y2 = this.selectedShape.y2 + deltaY;
92
158
  break;
93
159
  }
94
- this.requestUpdate(); // 화면 업데이트
160
+ this.requestUpdate();
95
161
  };
96
162
  this.onShapeMouseUp = () => {
97
163
  const svg = this.svg;
@@ -179,6 +245,23 @@ let OxImageMarker = class OxImageMarker extends BaseImageMarker {
179
245
  }
180
246
  `
181
247
  ]; }
248
+ connectedCallback() {
249
+ super.connectedCallback();
250
+ // 키다운 이벤트 리스너 추가
251
+ window.addEventListener('keydown', this.handleKeyDown);
252
+ }
253
+ disconnectedCallback() {
254
+ super.disconnectedCallback();
255
+ // 키다운 이벤트 리스너 제거
256
+ window.removeEventListener('keydown', this.handleKeyDown);
257
+ }
258
+ deleteSelectedShape() {
259
+ if (this.selectedShape) {
260
+ this.shapes = this.shapes.filter(s => s.id !== this.selectedShape.id);
261
+ this.selectedShape = null;
262
+ this.requestUpdate();
263
+ }
264
+ }
182
265
  render() {
183
266
  return html `
184
267
  <div class="container">
@@ -200,12 +283,7 @@ let OxImageMarker = class OxImageMarker extends BaseImageMarker {
200
283
  const { mode, shapeType } = e.detail;
201
284
  this.setMode(mode, shapeType);
202
285
  }}
203
- @clicked-delete=${() => {
204
- if (this.selectedShape) {
205
- this.shapes = this.shapes.filter(s => s.id !== this.selectedShape.id);
206
- this.requestUpdate();
207
- }
208
- }}
286
+ @clicked-delete=${() => this.deleteSelectedShape()}
209
287
  @clicked-save=${() => {
210
288
  this.dispatchEvent(new CustomEvent('shapes-changed', {
211
289
  detail: this.shapes,
@@ -249,82 +327,27 @@ let OxImageMarker = class OxImageMarker extends BaseImageMarker {
249
327
  }
250
328
  }
251
329
  startDrawing(event) {
252
- this.drawing = true;
253
330
  const { x, y } = this.getSvgPoint(event);
254
- this.startX = x;
255
- this.startY = y;
256
- let newShape;
257
- switch (this.currentShapeType) {
258
- case 'text':
259
- newShape = {
260
- id: Date.now().toString(),
261
- type: 'text',
262
- x: this.startX,
263
- y: this.startY,
264
- text: 'ABC'
265
- };
266
- break;
267
- case 'rectangle':
268
- newShape = {
269
- id: Date.now().toString(),
270
- type: 'rectangle',
271
- x: this.startX,
272
- y: this.startY,
273
- width: 0,
274
- height: 0
275
- };
276
- break;
277
- case 'circle':
278
- newShape = {
279
- id: Date.now().toString(),
280
- type: 'circle',
281
- cx: this.startX,
282
- cy: this.startY,
283
- r: 0
284
- };
285
- break;
286
- case 'triangle':
287
- newShape = {
288
- id: Date.now().toString(),
289
- type: 'triangle',
290
- x1: this.startX,
291
- y1: this.startY,
292
- width: 0,
293
- height: 0,
294
- points: `${this.startX},${this.startY} ${this.startX},${this.startY} ${this.startX},${this.startY}`
295
- };
296
- break;
297
- case 'line':
298
- newShape = {
299
- id: Date.now().toString(),
300
- type: 'line',
301
- x1: this.startX,
302
- y1: this.startY,
303
- width: 0,
304
- height: 0
305
- };
306
- break;
307
- case 'arrow':
308
- newShape = {
309
- id: Date.now().toString(),
310
- type: 'arrow',
311
- x1: this.startX,
312
- y1: this.startY,
313
- width: 0,
314
- height: 0
315
- };
316
- break;
317
- default:
318
- return;
331
+ if (this.currentShapeType === 'text') {
332
+ const textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
333
+ textElement.setAttribute('x', x.toString());
334
+ textElement.setAttribute('y', y.toString());
335
+ textElement.textContent = 'ABC';
336
+ this.svg.appendChild(textElement);
337
+ requestAnimationFrame(() => {
338
+ this.showTextInput(textElement, null, { x, y });
339
+ });
340
+ }
341
+ else {
342
+ this.startX = x;
343
+ this.startY = y;
344
+ this.drawing = true;
319
345
  }
320
- this.selectedShape = newShape;
321
- this.shapes = [...this.shapes, newShape];
322
346
  }
323
347
  setMode(mode, shapeType) {
324
348
  this.currentMode = mode;
325
349
  this.currentShapeType = shapeType;
326
350
  }
327
- // SVG 좌표계로 마우스 좌표 변환
328
351
  getSvgPoint(event) {
329
352
  const svg = this.svg;
330
353
  const point = svg.createSVGPoint();
@@ -333,23 +356,21 @@ let OxImageMarker = class OxImageMarker extends BaseImageMarker {
333
356
  const ctm = svg.getScreenCTM().inverse();
334
357
  return point.matrixTransform(ctm);
335
358
  }
336
- // 도형 렌더링 함수
337
359
  renderShape(shape) {
338
360
  const commonAttrs = {
339
361
  stroke: shape.selected ? 'blue' : 'red',
340
- 'stroke-width': shape.selected ? 2 : 1,
362
+ 'stroke-width': 1,
341
363
  fill: 'transparent',
342
364
  cursor: 'pointer'
343
365
  };
344
366
  switch (shape.type) {
345
367
  case 'text':
346
368
  return svg `
347
-
348
369
  <text
349
370
  x="${shape.x}"
350
371
  y="${shape.y}"
351
372
  fill="${commonAttrs.stroke}"
352
- style="cursor: ${commonAttrs.cursor}; font-size: 16px; text-decoration: ${shape.selected ? 'underline' : 'none'};"
373
+ style="cursor: ${commonAttrs.cursor}; font-size: 16px;"
353
374
  @mousedown="${(e) => this.onShapeMouseDown(e, shape)}"
354
375
  @dblclick="${(e) => this.onTextDoubleClick(e, shape)}"
355
376
  >${shape.text}</text>
@@ -440,7 +461,6 @@ let OxImageMarker = class OxImageMarker extends BaseImageMarker {
440
461
  selected: s.id === shape.id
441
462
  }));
442
463
  this.selectedShape = this.shapes.find(s => s.id == shape.id);
443
- // 마우스 클릭 좌표와 도형의 현재 좌표 차이 계산
444
464
  const { x, y } = this.getSvgPoint(event);
445
465
  switch (shape.type) {
446
466
  case 'text':
@@ -461,15 +481,13 @@ let OxImageMarker = class OxImageMarker extends BaseImageMarker {
461
481
  this.offsetY = y - shape.y1;
462
482
  break;
463
483
  }
464
- // 드래그 이벤트 리스너 추가
465
484
  const svg = this.svg;
466
485
  svg.addEventListener('mousemove', this.onShapeMouseMove);
467
486
  svg.addEventListener('mouseup', this.onShapeMouseUp);
468
487
  }
469
488
  getScale() {
470
- if (!this.svg) {
489
+ if (!this.svg)
471
490
  return 1;
472
- }
473
491
  const [vbWidth, vbHeight] = this.viewBox.split(' ').map(Number);
474
492
  const scaleX = vbWidth / this.svg.clientWidth;
475
493
  const scaleY = vbHeight / this.svg.clientHeight;
@@ -539,33 +557,109 @@ let OxImageMarker = class OxImageMarker extends BaseImageMarker {
539
557
  }
540
558
  onTextDoubleClick(event, shape) {
541
559
  event.stopPropagation();
542
- var input = document.createElement('input');
560
+ const textElement = event.target;
561
+ this.showTextInput(textElement, shape);
562
+ }
563
+ showTextInput(textElement, shape = null, options) {
564
+ const input = document.createElement('input');
543
565
  input.style.position = 'absolute';
544
- input.style.left = `${event.clientX}px`;
545
- input.style.top = `${event.clientY}px`;
546
- input.value = shape.text || '';
566
+ const ctm = textElement.getScreenCTM() || { a: 1, e: 0, d: 1, f: 0 };
567
+ const bbox = textElement.getBBox();
568
+ const screenX = bbox.x * ctm.a + ctm.e;
569
+ const screenY = bbox.y * ctm.d + ctm.f;
570
+ input.style.left = `${screenX}px`;
571
+ input.style.top = `${screenY}px`;
572
+ const computedStyle = window.getComputedStyle(textElement);
573
+ const fontSize = parseFloat(computedStyle.fontSize) * ctm.a;
574
+ const fontFamily = computedStyle.fontFamily;
575
+ const fontWeight = computedStyle.fontWeight;
576
+ const textColor = computedStyle.fill || computedStyle.color;
577
+ const lineHeight = parseFloat(computedStyle.lineHeight) * ctm.d;
578
+ input.style.fontSize = `${fontSize}px`;
579
+ input.style.fontFamily = fontFamily;
580
+ input.style.fontWeight = fontWeight;
581
+ input.style.color = textColor;
582
+ input.style.lineHeight = `${lineHeight}px`;
583
+ input.style.padding = '0';
584
+ input.style.margin = '0';
585
+ input.style.border = 'none';
586
+ input.style.outline = 'none';
587
+ input.style.background = 'transparent';
588
+ if (shape) {
589
+ input.value = shape.text || '';
590
+ }
591
+ textElement.style.visibility = 'hidden';
547
592
  document.body.appendChild(input);
548
- input.focus();
593
+ this.adjustInputWidth(input);
594
+ requestAnimationFrame(() => input.focus());
595
+ input.addEventListener('input', () => {
596
+ this.adjustInputWidth(input);
597
+ });
549
598
  input.addEventListener('keydown', e => {
550
599
  if (e.key === 'Enter') {
551
- shape.text = input.value;
600
+ const textValue = input.value.trim();
601
+ if (textValue) {
602
+ if (shape) {
603
+ shape.text = textValue;
604
+ }
605
+ else {
606
+ const newShape = {
607
+ id: Date.now().toString(),
608
+ type: 'text',
609
+ x: options.x,
610
+ y: options.y,
611
+ text: textValue
612
+ };
613
+ this.shapes = [...this.shapes, newShape];
614
+ }
615
+ }
616
+ else if (shape) {
617
+ this.shapes = this.shapes.filter(s => s.id !== shape.id);
618
+ }
552
619
  document.body.removeChild(input);
553
- input = null;
620
+ if (!shape) {
621
+ this.svg.removeChild(textElement);
622
+ }
623
+ else {
624
+ textElement.style.visibility = 'visible';
625
+ }
554
626
  this.requestUpdate();
555
627
  }
556
628
  else if (e.key === 'Escape') {
557
629
  document.body.removeChild(input);
558
- input = null;
559
- this.requestUpdate();
630
+ if (!shape) {
631
+ this.svg.removeChild(textElement);
632
+ }
633
+ else {
634
+ textElement.style.visibility = 'visible';
635
+ }
560
636
  }
561
637
  });
562
638
  input.addEventListener('blur', () => {
563
639
  requestAnimationFrame(() => {
564
- input && document.body.removeChild(input);
565
- input = null;
640
+ if (!shape) {
641
+ this.svg.removeChild(textElement);
642
+ }
643
+ else {
644
+ textElement.style.visibility = 'visible';
645
+ }
646
+ if (input.parentElement) {
647
+ document.body.removeChild(input);
648
+ }
566
649
  });
567
650
  });
568
651
  }
652
+ adjustInputWidth(input) {
653
+ const minWidth = 50;
654
+ const textWidth = this.measureTextWidth(input.value || input.placeholder, input);
655
+ input.style.width = `${Math.max(textWidth + 10, minWidth)}px`;
656
+ }
657
+ measureTextWidth(text, input) {
658
+ const canvas = document.createElement('canvas');
659
+ const context = canvas.getContext('2d');
660
+ context.font = window.getComputedStyle(input).font;
661
+ return context.measureText(text).width;
662
+ }
569
663
  };
570
664
  __decorate([
571
665
  state()