@operato/image-marker 8.0.0-alpha.10

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.
Files changed (68) hide show
  1. package/.editorconfig +29 -0
  2. package/.storybook/main.js +3 -0
  3. package/.storybook/preview.js +52 -0
  4. package/.storybook/server.mjs +8 -0
  5. package/CHANGELOG.md +11 -0
  6. package/README.md +0 -0
  7. package/assets/images/no-image.png +0 -0
  8. package/demo/index.html +22 -0
  9. package/dist/src/base-image-marker.d.ts +24 -0
  10. package/dist/src/base-image-marker.js +137 -0
  11. package/dist/src/base-image-marker.js.map +1 -0
  12. package/dist/src/index.d.ts +0 -0
  13. package/dist/src/index.js +2 -0
  14. package/dist/src/index.js.map +1 -0
  15. package/dist/src/ox-image-marker-mode.d.ts +13 -0
  16. package/dist/src/ox-image-marker-mode.js +140 -0
  17. package/dist/src/ox-image-marker-mode.js.map +1 -0
  18. package/dist/src/ox-image-marker-view.d.ts +4 -0
  19. package/dist/src/ox-image-marker-view.js +26 -0
  20. package/dist/src/ox-image-marker-view.js.map +1 -0
  21. package/dist/src/ox-image-marker.d.ts +33 -0
  22. package/dist/src/ox-image-marker.js +583 -0
  23. package/dist/src/ox-image-marker.js.map +1 -0
  24. package/dist/src/types.d.ts +20 -0
  25. package/dist/src/types.js +2 -0
  26. package/dist/src/types.js.map +1 -0
  27. package/dist/stories/ox-image-marker-mode.stories.d.ts +16 -0
  28. package/dist/stories/ox-image-marker-mode.stories.js +31 -0
  29. package/dist/stories/ox-image-marker-mode.stories.js.map +1 -0
  30. package/dist/stories/ox-image-marker-view.stories.d.ts +41 -0
  31. package/dist/stories/ox-image-marker-view.stories.js +103 -0
  32. package/dist/stories/ox-image-marker-view.stories.js.map +1 -0
  33. package/dist/stories/ox-image-marker.stories.d.ts +33 -0
  34. package/dist/stories/ox-image-marker.stories.js +86 -0
  35. package/dist/stories/ox-image-marker.stories.js.map +1 -0
  36. package/dist/tsconfig.tsbuildinfo +1 -0
  37. package/package.json +97 -0
  38. package/src/base-image-marker.ts +144 -0
  39. package/src/index.ts +0 -0
  40. package/src/ox-image-marker-mode.ts +145 -0
  41. package/src/ox-image-marker-view.ts +23 -0
  42. package/src/ox-image-marker.ts +626 -0
  43. package/src/types.ts +20 -0
  44. package/stories/ox-image-marker-mode.stories.ts +41 -0
  45. package/stories/ox-image-marker-view.stories.ts +134 -0
  46. package/stories/ox-image-marker.stories.ts +102 -0
  47. package/themes/app-theme.css +138 -0
  48. package/themes/calendar-theme.css +59 -0
  49. package/themes/dark.css +51 -0
  50. package/themes/form-theme.css +70 -0
  51. package/themes/grist-theme.css +175 -0
  52. package/themes/layout-theme.css +94 -0
  53. package/themes/light.css +51 -0
  54. package/themes/material-theme.css +23 -0
  55. package/themes/md-typescale-styles.css +100 -0
  56. package/themes/oops-theme.css +22 -0
  57. package/themes/report-theme.css +47 -0
  58. package/themes/spacing.css +23 -0
  59. package/themes/state-color.css +6 -0
  60. package/themes/tooltip-theme.css +11 -0
  61. package/translations/en.json +1 -0
  62. package/translations/ja.json +1 -0
  63. package/translations/ko.json +1 -0
  64. package/translations/ms.json +1 -0
  65. package/translations/zh.json +1 -0
  66. package/tsconfig.json +25 -0
  67. package/web-dev-server.config.mjs +27 -0
  68. package/web-test-runner.config.mjs +41 -0
@@ -0,0 +1,626 @@
1
+ import './ox-image-marker-mode'
2
+
3
+ import { html, css, svg, nothing } from 'lit'
4
+ import { property, state, customElement, query } from 'lit/decorators.js'
5
+ import { ifDefined } from 'lit/directives/if-defined.js'
6
+ import { keyed } from 'lit/directives/keyed.js'
7
+
8
+ import { ImageMarkerMode, Shape, ShapeType } from './types'
9
+ import { BaseImageMarker } from './base-image-marker'
10
+
11
+ @customElement('ox-image-marker')
12
+ export class OxImageMarker extends BaseImageMarker {
13
+ static styles = [
14
+ ...BaseImageMarker.styles,
15
+ css`
16
+ :host {
17
+ position: relative;
18
+ display: flex;
19
+ width: 100%;
20
+ height: 100%;
21
+ }
22
+
23
+ ox-image-marker-mode {
24
+ position: absolute;
25
+ top: 0;
26
+ right: 0;
27
+ }
28
+
29
+ svg {
30
+ cursor: crosshair;
31
+ }
32
+
33
+ svg.dragging {
34
+ cursor: move;
35
+ }
36
+
37
+ .resize-handle {
38
+ cursor: nwse-resize;
39
+ }
40
+ `
41
+ ]
42
+
43
+ @state() private currentShapeType?: ShapeType
44
+ @state() private currentMode: ImageMarkerMode = 'select'
45
+ @state() private selectedShape: Shape | null = null
46
+
47
+ private drawing = false
48
+ private startX = 0
49
+ private startY = 0
50
+ private offsetX = 0
51
+ private offsetY = 0
52
+
53
+ render() {
54
+ return html`
55
+ <div class="container">
56
+ <svg
57
+ @mousedown=${this.onSvgMouseDown}
58
+ @wheel=${this.onWheel}
59
+ @dblclick=${this.onDoubleClick}
60
+ viewBox=${this.viewBox}
61
+ >
62
+ <image href=${this.imageUrl} @load=${this.onImageLoad}></image>
63
+ ${this.shapes.map(shape => keyed(shape.id, this.renderShape(shape)))}
64
+ </svg>
65
+ </div>
66
+
67
+ <ox-image-marker-mode
68
+ currentMode=${this.currentMode}
69
+ currentShapeType=${ifDefined(this.currentShapeType)}
70
+ @mode-changed=${(e: CustomEvent) => {
71
+ const { mode, shapeType } = e.detail
72
+ this.setMode(mode, shapeType)
73
+ }}
74
+ @clicked-delete=${() => {
75
+ if (this.selectedShape) {
76
+ this.shapes = this.shapes.filter(s => s.id !== this.selectedShape!.id)
77
+ this.requestUpdate()
78
+ }
79
+ }}
80
+ @clicked-save=${() => {
81
+ this.dispatchEvent(
82
+ new CustomEvent('shapes-changed', {
83
+ detail: this.shapes,
84
+ bubbles: true,
85
+ composed: true
86
+ })
87
+ )
88
+ }}
89
+ ></ox-image-marker-mode>
90
+ `
91
+ }
92
+
93
+ protected onSvgMouseDown(event: MouseEvent) {
94
+ const svg = this.svg
95
+
96
+ this.selectedShape = null
97
+ this.shapes = this.shapes.map(s => {
98
+ return { ...s, selected: false }
99
+ })
100
+
101
+ this.selectedShape = null
102
+ if (this.currentMode === 'move') {
103
+ this.isPanning = true
104
+ this.panStart = { x: event.clientX, y: event.clientY }
105
+ svg.classList.add('dragging')
106
+ svg.addEventListener('mousemove', this.onPanMouseMove)
107
+ svg.addEventListener('mouseup', this.onPanMouseUp)
108
+ } else if (this.currentMode === 'select') {
109
+ this.startShapeDragging(event)
110
+ svg.addEventListener('mousemove', this.onShapeMouseMove)
111
+ svg.addEventListener('mouseup', this.onShapeMouseUp)
112
+ } else if (this.currentMode === 'draw') {
113
+ this.startDrawing(event)
114
+ svg.addEventListener('mousemove', this.onMouseMove)
115
+ svg.addEventListener('mouseup', this.onMouseUp)
116
+ }
117
+ }
118
+
119
+ private startShapeDragging(event: MouseEvent) {
120
+ const { x, y } = this.getSvgPoint(event)
121
+ if (this.selectedShape) {
122
+ this.offsetX = x - (this.selectedShape.x || this.selectedShape.cx || 0)
123
+ this.offsetY = y - (this.selectedShape.y || this.selectedShape.cy || 0)
124
+ }
125
+ }
126
+
127
+ private startDrawing(event: MouseEvent) {
128
+ this.drawing = true
129
+ const { x, y } = this.getSvgPoint(event)
130
+ this.startX = x
131
+ this.startY = y
132
+
133
+ let newShape: Shape
134
+ switch (this.currentShapeType) {
135
+ case 'text':
136
+ newShape = {
137
+ id: Date.now().toString(),
138
+ type: 'text',
139
+ x: this.startX,
140
+ y: this.startY,
141
+ text: 'ABC'
142
+ }
143
+ break
144
+ case 'rectangle':
145
+ newShape = {
146
+ id: Date.now().toString(),
147
+ type: 'rectangle',
148
+ x: this.startX,
149
+ y: this.startY,
150
+ width: 0,
151
+ height: 0
152
+ }
153
+ break
154
+ case 'circle':
155
+ newShape = {
156
+ id: Date.now().toString(),
157
+ type: 'circle',
158
+ cx: this.startX,
159
+ cy: this.startY,
160
+ r: 0
161
+ }
162
+ break
163
+ case 'triangle':
164
+ newShape = {
165
+ id: Date.now().toString(),
166
+ type: 'triangle',
167
+ x1: this.startX,
168
+ y1: this.startY,
169
+ width: 0,
170
+ height: 0,
171
+ points: `${this.startX},${this.startY} ${this.startX},${this.startY} ${this.startX},${this.startY}`
172
+ }
173
+ break
174
+ case 'line':
175
+ newShape = {
176
+ id: Date.now().toString(),
177
+ type: 'line',
178
+ x1: this.startX,
179
+ y1: this.startY,
180
+ width: 0,
181
+ height: 0
182
+ }
183
+ break
184
+ case 'arrow':
185
+ newShape = {
186
+ id: Date.now().toString(),
187
+ type: 'arrow',
188
+ x1: this.startX,
189
+ y1: this.startY,
190
+ width: 0,
191
+ height: 0
192
+ }
193
+ break
194
+ default:
195
+ return
196
+ }
197
+
198
+ this.selectedShape = newShape
199
+ this.shapes = [...this.shapes, newShape]
200
+ }
201
+
202
+ private setMode(mode: ImageMarkerMode, shapeType: ShapeType) {
203
+ this.currentMode = mode
204
+ this.currentShapeType = shapeType
205
+ }
206
+
207
+ // 마우스 이동 이벤트 핸들러 (도형 그리기 중)
208
+ private onMouseMove = (event: MouseEvent) => {
209
+ if (!this.drawing || !this.selectedShape) return
210
+ const { x, y } = this.getSvgPoint(event)
211
+
212
+ switch (this.selectedShape.type) {
213
+ case 'text':
214
+ break
215
+ case 'rectangle':
216
+ this.selectedShape.width = x - this.startX
217
+ this.selectedShape.height = y - this.startY
218
+ break
219
+ case 'circle':
220
+ const dx = x - this.selectedShape.cx!
221
+ const dy = y - this.selectedShape.cy!
222
+ this.selectedShape.r = Math.sqrt(dx * dx + dy * dy)
223
+ break
224
+ case 'triangle':
225
+ const x2 = x
226
+ const y2 = y
227
+ const x3 = this.startX - (x - this.startX)
228
+ const y3 = y
229
+ this.selectedShape.points = `${this.startX},${this.startY} ${x2},${y2} ${x3},${y3}`
230
+ break
231
+ case 'line':
232
+ case 'arrow':
233
+ this.selectedShape.x2 = x
234
+ this.selectedShape.y2 = y
235
+ break
236
+ }
237
+
238
+ this.requestUpdate()
239
+ }
240
+
241
+ // 마우스 업 이벤트 핸들러 (도형 그리기 종료)
242
+ private onMouseUp = () => {
243
+ this.drawing = false
244
+ const svg = this.svg as SVGSVGElement
245
+ svg.removeEventListener('mousemove', this.onMouseMove)
246
+ svg.removeEventListener('mouseup', this.onMouseUp)
247
+
248
+ this.selectedShape!.selected = true
249
+ this.requestUpdate()
250
+ }
251
+
252
+ // SVG 좌표계로 마우스 좌표 변환
253
+ private getSvgPoint(event: MouseEvent) {
254
+ const svg = this.svg
255
+ const point = svg.createSVGPoint()
256
+ point.x = event.clientX
257
+ point.y = event.clientY
258
+ const ctm = svg.getScreenCTM()!.inverse()
259
+ return point.matrixTransform(ctm)
260
+ }
261
+
262
+ // 도형 렌더링 함수
263
+ protected renderShape(shape: Shape) {
264
+ const commonAttrs = {
265
+ stroke: shape.selected ? 'blue' : 'red',
266
+ 'stroke-width': shape.selected ? 2 : 1,
267
+ fill: 'transparent',
268
+ cursor: 'pointer'
269
+ }
270
+
271
+ switch (shape.type) {
272
+ case 'text':
273
+ return svg`
274
+
275
+ <text
276
+ x="${shape.x!}"
277
+ y="${shape.y!}"
278
+ fill="${commonAttrs.stroke}"
279
+ style="cursor: ${commonAttrs.cursor}; font-size: 16px; text-decoration: ${shape.selected ? 'underline' : 'none'};"
280
+ @mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
281
+ @dblclick="${(e: MouseEvent) => this.onTextDoubleClick(e, shape)}"
282
+ >${shape.text}</text>
283
+ `
284
+
285
+ case 'rectangle':
286
+ return svg`
287
+ <rect
288
+ x="${shape.x!}"
289
+ y="${shape.y!}"
290
+ width="${shape.width!}"
291
+ height="${shape.height!}"
292
+ stroke="${commonAttrs.stroke}"
293
+ stroke-width="${commonAttrs['stroke-width']}"
294
+ fill="${commonAttrs.fill}"
295
+ style="cursor: ${commonAttrs.cursor}"
296
+ @mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
297
+ ></rect>
298
+ ${shape.selected ? this.renderResizeHandles(shape) : ''}
299
+ `
300
+
301
+ case 'circle':
302
+ return svg`
303
+ <circle
304
+ cx="${shape.cx!}"
305
+ cy="${shape.cy!}"
306
+ r="${shape.r!}"
307
+ stroke="${commonAttrs.stroke}"
308
+ stroke-width="${commonAttrs['stroke-width']}"
309
+ fill="${commonAttrs.fill}"
310
+ style="cursor: ${commonAttrs.cursor}"
311
+ @mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
312
+ ></circle>
313
+ ${shape.selected ? this.renderResizeHandles(shape) : ''}
314
+ `
315
+
316
+ case 'triangle':
317
+ return svg`
318
+ <polygon
319
+ points="${shape.points!}"
320
+ stroke="${commonAttrs.stroke}"
321
+ stroke-width="${commonAttrs['stroke-width']}"
322
+ fill="${commonAttrs.fill}"
323
+ style="cursor: ${commonAttrs.cursor}"
324
+ @mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
325
+ ></polygon>
326
+ ${shape.selected ? this.renderResizeHandles(shape) : ''}
327
+ `
328
+
329
+ case 'line':
330
+ return svg`
331
+ <line
332
+ x1="${shape.x1!}"
333
+ y1="${shape.y1!}"
334
+ x2="${shape.x2 ?? shape.x1}"
335
+ y2="${shape.y2 ?? shape.y1}"
336
+ stroke="${commonAttrs.stroke}"
337
+ stroke-width="${commonAttrs['stroke-width']}"
338
+ style="cursor: ${commonAttrs.cursor}"
339
+ @mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
340
+ ></line>
341
+ ${shape.selected ? this.renderResizeHandles(shape) : ''}
342
+ `
343
+
344
+ case 'arrow':
345
+ return svg`
346
+ <defs>
347
+ <marker id="arrowhead-${shape.id}" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
348
+ <polygon points="0 0, 10 3.5, 0 7" fill="${commonAttrs.stroke}"></polygon>
349
+ </marker>
350
+ </defs>
351
+ <line
352
+ x1="${shape.x1!}"
353
+ y1="${shape.y1!}"
354
+ x2="${shape.x2 ?? shape.x1}"
355
+ y2="${shape.y2 ?? shape.y1}"
356
+ stroke="${commonAttrs.stroke}"
357
+ stroke-width="${commonAttrs['stroke-width']}"
358
+ marker-end="url(#arrowhead-${shape.id})"
359
+ style="cursor: ${commonAttrs.cursor}"
360
+ @mousedown="${(e: MouseEvent) => this.onShapeMouseDown(e, shape)}"
361
+ ></line>
362
+ ${shape.selected ? this.renderResizeHandles(shape) : ''}
363
+ `
364
+
365
+ default:
366
+ return nothing
367
+ }
368
+ }
369
+
370
+ private onShapeMouseDown(event: MouseEvent, shape: Shape) {
371
+ event.stopPropagation()
372
+
373
+ this.shapes = this.shapes.map(s => ({
374
+ ...s,
375
+ selected: s.id === shape.id
376
+ }))
377
+
378
+ this.selectedShape = this.shapes.find(s => s.id == shape.id)!
379
+
380
+ // 마우스 클릭 좌표와 도형의 현재 좌표 차이 계산
381
+ const { x, y } = this.getSvgPoint(event)
382
+
383
+ switch (shape.type) {
384
+ case 'text':
385
+ case 'rectangle':
386
+ case 'circle':
387
+ this.offsetX = x - (shape.x || shape.cx || 0)
388
+ this.offsetY = y - (shape.y || shape.cy || 0)
389
+ break
390
+
391
+ case 'triangle':
392
+ const points = this.selectedShape.points!.split(' ').map(p => p.split(',').map(Number))
393
+ const [px, py] = points[0] || [0, 0]
394
+
395
+ this.offsetX = x - px
396
+ this.offsetY = y - py
397
+
398
+ break
399
+
400
+ case 'line':
401
+ case 'arrow':
402
+ this.offsetX = x - shape.x1!
403
+ this.offsetY = y - shape.y1!
404
+ break
405
+ }
406
+
407
+ // 드래그 이벤트 리스너 추가
408
+ const svg = this.svg as SVGSVGElement
409
+ svg.addEventListener('mousemove', this.onShapeMouseMove)
410
+ svg.addEventListener('mouseup', this.onShapeMouseUp)
411
+ }
412
+
413
+ private onShapeMouseMove = (event: MouseEvent) => {
414
+ if (!this.selectedShape) {
415
+ return
416
+ }
417
+
418
+ const { x, y } = this.getSvgPoint(event)
419
+
420
+ // 마우스 이동에 따라 도형의 위치 업데이트
421
+ switch (this.selectedShape.type) {
422
+ case 'text':
423
+ case 'rectangle':
424
+ this.selectedShape.x = x - this.offsetX
425
+ this.selectedShape.y = y - this.offsetY
426
+ break
427
+ case 'circle':
428
+ this.selectedShape.cx = x - this.offsetX
429
+ this.selectedShape.cy = y - this.offsetY
430
+ break
431
+ case 'triangle':
432
+ const points = this.selectedShape.points!.split(' ').map(p => p.split(',').map(Number))
433
+ const [ox, oy] = points[0]
434
+
435
+ this.selectedShape.points = points
436
+ .map(([px, py]) => [px - ox + x - this.offsetX, py - oy + y - this.offsetY].join(','))
437
+ .join(' ')
438
+ break
439
+ case 'line':
440
+ case 'arrow':
441
+ const dxLine = x - this.offsetX
442
+ const dyLine = y - this.offsetY
443
+ const deltaX = dxLine - this.selectedShape.x1!
444
+ const deltaY = dyLine - this.selectedShape.y1!
445
+ this.selectedShape.x1 = dxLine
446
+ this.selectedShape.y1 = dyLine
447
+ this.selectedShape.x2 = this.selectedShape.x2! + deltaX
448
+ this.selectedShape.y2 = this.selectedShape.y2! + deltaY
449
+ break
450
+ }
451
+
452
+ this.requestUpdate() // 화면 업데이트
453
+ }
454
+
455
+ private onShapeMouseUp = () => {
456
+ const svg = this.svg as SVGSVGElement
457
+ svg.removeEventListener('mousemove', this.onShapeMouseMove)
458
+ svg.removeEventListener('mouseup', this.onShapeMouseUp)
459
+ }
460
+
461
+ private getScale(): number {
462
+ if (!this.svg) {
463
+ return 1
464
+ }
465
+
466
+ const [vbWidth, vbHeight] = this.viewBox.split(' ').map(Number)
467
+
468
+ const scaleX = vbWidth / this.svg.clientWidth
469
+ const scaleY = vbHeight / this.svg.clientHeight
470
+
471
+ return Math.max(scaleX, scaleY)
472
+ }
473
+
474
+ private renderResizeHandles(shape: Shape) {
475
+ let handles: Array<{ x: number; y: number; cursor: string }> = []
476
+
477
+ switch (shape.type) {
478
+ case 'text':
479
+ case 'rectangle':
480
+ handles = [
481
+ { x: shape.x!, y: shape.y!, cursor: 'nwse-resize' },
482
+ { x: shape.x! + shape.width!, y: shape.y!, cursor: 'nesw-resize' },
483
+ { x: shape.x!, y: shape.y! + shape.height!, cursor: 'nesw-resize' },
484
+ { x: shape.x! + shape.width!, y: shape.y! + shape.height!, cursor: 'nwse-resize' }
485
+ ]
486
+ break
487
+ case 'circle':
488
+ handles = [
489
+ { x: shape.cx! + shape.r!, y: shape.cy!, cursor: 'ew-resize' },
490
+ { x: shape.cx! - shape.r!, y: shape.cy!, cursor: 'ew-resize' },
491
+ { x: shape.cx!, y: shape.cy! + shape.r!, cursor: 'ns-resize' },
492
+ { x: shape.cx!, y: shape.cy! - shape.r!, cursor: 'ns-resize' }
493
+ ]
494
+ break
495
+ case 'triangle':
496
+ const points = shape.points!.split(' ').map(p => p.split(',').map(Number))
497
+ handles = points.map(([x, y]) => ({ x, y, cursor: 'move' }))
498
+ break
499
+ case 'line':
500
+ case 'arrow':
501
+ handles = [
502
+ { x: shape.x1!, y: shape.y1!, cursor: 'move' },
503
+ { x: shape.x2!, y: shape.y2!, cursor: 'move' }
504
+ ]
505
+ break
506
+ }
507
+
508
+ return svg`
509
+ ${handles.map(
510
+ handle => svg`
511
+ <g
512
+ class="resize-handle-group"
513
+ style="transform: translate(${handle.x}px, ${handle.y}px) scale(${1 / this.zoomLevel});"
514
+ >
515
+ <rect
516
+ x="-5"
517
+ y="-5"
518
+ width="10"
519
+ height="10"
520
+ fill="white"
521
+ stroke="black"
522
+ class="resize-handle"
523
+ style="cursor: ${handle.cursor};"
524
+ @mousedown="${(e: MouseEvent) => this.onHandleMouseDown(e, shape, handle)}"
525
+ ></rect>
526
+ </g>
527
+ `
528
+ )}
529
+ `
530
+ }
531
+
532
+ private onHandleMouseDown(event: MouseEvent, shape: Shape, handle: any) {
533
+ event.stopPropagation()
534
+ this.selectedShape = shape
535
+ this.offsetX = handle.x
536
+ this.offsetY = handle.y
537
+ const svg = this.svg as SVGSVGElement
538
+ svg.addEventListener('mousemove', this.onHandleMouseMove)
539
+ svg.addEventListener('mouseup', this.onHandleMouseUp)
540
+ }
541
+
542
+ private onHandleMouseMove = (event: MouseEvent) => {
543
+ if (!this.selectedShape) return
544
+ const { x, y } = this.getSvgPoint(event)
545
+
546
+ switch (this.selectedShape.type) {
547
+ case 'text':
548
+ case 'rectangle':
549
+ this.selectedShape.width = Math.abs(x - this.selectedShape.x!)
550
+ this.selectedShape.height = Math.abs(y - this.selectedShape.y!)
551
+ if (x < this.selectedShape.x!) this.selectedShape.x = x
552
+ if (y < this.selectedShape.y!) this.selectedShape.y = y
553
+ break
554
+ case 'circle':
555
+ const dx = x - this.selectedShape.cx!
556
+ const dy = y - this.selectedShape.cy!
557
+ this.selectedShape.r = Math.sqrt(dx * dx + dy * dy)
558
+ break
559
+ case 'triangle':
560
+ const points = this.selectedShape.points!.split(' ').map(p => p.split(',').map(Number))
561
+ const index = points.findIndex(([px, py]) => px === this.offsetX && py === this.offsetY)
562
+ if (index !== -1) {
563
+ points[index] = [x, y]
564
+ this.selectedShape.points = points.map(p => p.join(',')).join(' ')
565
+ this.offsetX = x
566
+ this.offsetY = y
567
+ }
568
+ break
569
+ case 'line':
570
+ case 'arrow':
571
+ if (this.offsetX === this.selectedShape.x1 && this.offsetY === this.selectedShape.y1) {
572
+ this.selectedShape.x1 = x
573
+ this.selectedShape.y1 = y
574
+ } else if (this.offsetX === this.selectedShape.x2 && this.offsetY === this.selectedShape.y2) {
575
+ this.selectedShape.x2 = x
576
+ this.selectedShape.y2 = y
577
+ }
578
+ this.offsetX = x
579
+ this.offsetY = y
580
+ break
581
+ }
582
+
583
+ this.requestUpdate()
584
+ }
585
+
586
+ private onHandleMouseUp = () => {
587
+ const svg = this.svg as SVGSVGElement
588
+ svg.removeEventListener('mousemove', this.onHandleMouseMove)
589
+ svg.removeEventListener('mouseup', this.onHandleMouseUp)
590
+ this.selectedShape = null
591
+ }
592
+
593
+ private onTextDoubleClick(event: MouseEvent, shape: Shape) {
594
+ event.stopPropagation()
595
+
596
+ var input: HTMLInputElement | null = document.createElement('input')
597
+ input.style.position = 'absolute'
598
+ input.style.left = `${event.clientX}px`
599
+ input.style.top = `${event.clientY}px`
600
+ input.value = shape.text || ''
601
+ document.body.appendChild(input)
602
+ input.focus()
603
+
604
+ input.addEventListener('keydown', e => {
605
+ if (e.key === 'Enter') {
606
+ shape.text = input!.value
607
+ document.body.removeChild(input!)
608
+ input = null
609
+
610
+ this.requestUpdate()
611
+ } else if (e.key === 'Escape') {
612
+ document.body.removeChild(input!)
613
+ input = null
614
+
615
+ this.requestUpdate()
616
+ }
617
+ })
618
+
619
+ input.addEventListener('blur', () => {
620
+ requestAnimationFrame(() => {
621
+ input && document.body.removeChild(input)
622
+ input = null
623
+ })
624
+ })
625
+ }
626
+ }
package/src/types.ts ADDED
@@ -0,0 +1,20 @@
1
+ export type ShapeType = 'text' | 'rectangle' | 'circle' | 'triangle' | 'line' | 'arrow'
2
+ export type ImageMarkerMode = 'move' | 'select' | 'draw'
3
+ export interface Shape {
4
+ id: string
5
+ type: ShapeType
6
+ x?: number
7
+ y?: number
8
+ width?: number
9
+ height?: number
10
+ cx?: number
11
+ cy?: number
12
+ r?: number
13
+ points?: string
14
+ x1?: number
15
+ y1?: number
16
+ x2?: number
17
+ y2?: number
18
+ selected?: boolean
19
+ text?: string
20
+ }
@@ -0,0 +1,41 @@
1
+ import '../src/ox-image-marker-mode.js'
2
+ import { html, TemplateResult } from 'lit'
3
+
4
+ export default {
5
+ title: 'ox-image-marker-mode',
6
+ component: 'ox-image-marker-mode',
7
+ argTypes: {}
8
+ }
9
+
10
+ interface Story<T> {
11
+ (args: T): TemplateResult
12
+ args?: Partial<T>
13
+ argTypes?: Record<string, unknown>
14
+ }
15
+
16
+ interface ArgTypes {}
17
+
18
+ const Template: Story<ArgTypes> = ({}: ArgTypes) => html`
19
+ <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet" />
20
+ <link href="/themes/light.css" rel="stylesheet" />
21
+ <link href="/themes/dark.css" rel="stylesheet" />
22
+ <link href="/themes/spacing.css" rel="stylesheet" />
23
+ <link href="/themes/md-typescale-styles.css" rel="stylesheet" />
24
+ <link
25
+ href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL@20..48,100..700,0..1"
26
+ rel="stylesheet"
27
+ />
28
+ <link
29
+ href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL@20..48,100..700,0..1"
30
+ rel="stylesheet"
31
+ />
32
+ <link
33
+ href="https://fonts.googleapis.com/css2?family=Material+Symbols+Sharp:opsz,wght,FILL@20..48,100..700,0..1"
34
+ rel="stylesheet"
35
+ />
36
+
37
+ <ox-image-marker-mode @mode-changed=${(e: CustomEvent) => console.log(e.detail)}></ox-image-marker-mode>
38
+ `
39
+
40
+ export const Default = Template.bind({})
41
+ Default.args = {}