@node-projects/web-component-designer 0.1.347 → 0.1.350

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 (61) hide show
  1. package/dist/elements/helper/CssCombiner.d.ts +9 -0
  2. package/dist/elements/helper/CssCombiner.js +147 -7
  3. package/dist/elements/helper/CssCombiner.js.map +1 -1
  4. package/dist/elements/helper/Helper.js +3 -1
  5. package/dist/elements/helper/Helper.js.map +1 -1
  6. package/dist/elements/helper/LayoutHelper.js +44 -4
  7. package/dist/elements/helper/LayoutHelper.js.map +1 -1
  8. package/dist/elements/helper/PopupHelper.d.ts +1 -0
  9. package/dist/elements/helper/PopupHelper.js +51 -0
  10. package/dist/elements/helper/PopupHelper.js.map +1 -0
  11. package/dist/elements/helper/SelectionHelper.d.ts +2 -2
  12. package/dist/elements/helper/SelectionHelper.js +22 -9
  13. package/dist/elements/helper/SelectionHelper.js.map +1 -1
  14. package/dist/elements/helper/contextMenu/ContextMenu.js +11 -1
  15. package/dist/elements/helper/contextMenu/ContextMenu.js.map +1 -1
  16. package/dist/elements/helper/contextMenu/IContextMenuItem.d.ts +2 -1
  17. package/dist/elements/helper/contextMenu/IContextMenuItem.js.map +1 -1
  18. package/dist/elements/services/DefaultServiceBootstrap.js +4 -3
  19. package/dist/elements/services/DefaultServiceBootstrap.js.map +1 -1
  20. package/dist/elements/services/copyPasteService/CopyPasteAsJsonService.d.ts +3 -0
  21. package/dist/elements/services/copyPasteService/CopyPasteAsJsonService.js +41 -27
  22. package/dist/elements/services/copyPasteService/CopyPasteAsJsonService.js.map +1 -1
  23. package/dist/elements/services/copyPasteService/CopyPasteService.js +1 -1
  24. package/dist/elements/services/copyPasteService/CopyPasteService.js.map +1 -1
  25. package/dist/elements/services/copyPasteService/PasteFormatSnapshot.d.ts +18 -0
  26. package/dist/elements/services/copyPasteService/PasteFormatSnapshot.js +87 -0
  27. package/dist/elements/services/copyPasteService/PasteFormatSnapshot.js.map +1 -0
  28. package/dist/elements/services/htmlWriterService/FormatingHtmlWriterService.d.ts +4 -2
  29. package/dist/elements/services/htmlWriterService/FormatingHtmlWriterService.js +29 -9
  30. package/dist/elements/services/htmlWriterService/FormatingHtmlWriterService.js.map +1 -1
  31. package/dist/elements/services/htmlWriterService/HtmlWriterService.d.ts +6 -2
  32. package/dist/elements/services/htmlWriterService/HtmlWriterService.js +48 -22
  33. package/dist/elements/services/htmlWriterService/HtmlWriterService.js.map +1 -1
  34. package/dist/elements/services/propertiesService/IEditorTypesService.d.ts +1 -1
  35. package/dist/elements/widgets/designerView/DomConverter.d.ts +1 -1
  36. package/dist/elements/widgets/designerView/designerCanvas.js +2 -2
  37. package/dist/elements/widgets/designerView/designerCanvas.js.map +1 -1
  38. package/dist/elements/widgets/designerView/extensions/EditText/EditTextExtension.d.ts +30 -2
  39. package/dist/elements/widgets/designerView/extensions/EditText/EditTextExtension.js +390 -9
  40. package/dist/elements/widgets/designerView/extensions/EditText/EditTextExtension.js.map +1 -1
  41. package/dist/elements/widgets/designerView/extensions/contextMenu/PasteFormatContextMenu.d.ts +11 -0
  42. package/dist/elements/widgets/designerView/extensions/contextMenu/PasteFormatContextMenu.js +62 -0
  43. package/dist/elements/widgets/designerView/extensions/contextMenu/PasteFormatContextMenu.js.map +1 -0
  44. package/dist/elements/widgets/designerView/extensions/svg/UnifiedGeometryExtension.d.ts +20 -0
  45. package/dist/elements/widgets/designerView/extensions/svg/UnifiedGeometryExtension.js +412 -19
  46. package/dist/elements/widgets/designerView/extensions/svg/UnifiedGeometryExtension.js.map +1 -1
  47. package/dist/elements/widgets/designerView/extensions/svg/UnifiedGeometryExtensionProvider.d.ts +1 -0
  48. package/dist/elements/widgets/designerView/extensions/svg/UnifiedGeometryExtensionProvider.js +16 -0
  49. package/dist/elements/widgets/designerView/extensions/svg/UnifiedGeometryExtensionProvider.js.map +1 -1
  50. package/dist/elements/widgets/designerView/tools/DrawPathTool.d.ts +8 -5
  51. package/dist/elements/widgets/designerView/tools/DrawPathTool.js +50 -36
  52. package/dist/elements/widgets/designerView/tools/DrawPathTool.js.map +1 -1
  53. package/dist/elements/widgets/designerView/tools/ITool.d.ts +1 -1
  54. package/dist/index-min.js +13 -6
  55. package/dist/index-min.js.map +4 -4
  56. package/dist/index.d.ts +17 -2
  57. package/dist/index.js +17 -2
  58. package/dist/index.js.map +1 -1
  59. package/package.json +1 -1
  60. package/tests/CssCombiner.test.ts +340 -0
  61. package/tests/PasteFormatSnapshot.test.ts +92 -0
@@ -9,6 +9,7 @@ export class UnifiedGeometryExtension extends AbstractExtension {
9
9
  _reader;
10
10
  _dragState = null;
11
11
  _options;
12
+ _overlayStructureSignature = null;
12
13
  _eventCleanups = [];
13
14
  /** Map from 'anchor-{index}', 'cp1-{index}', 'cp2-{index}' to overlay circle */
14
15
  _overlayCircles = new Map();
@@ -20,18 +21,31 @@ export class UnifiedGeometryExtension extends AbstractExtension {
20
21
  this._options = options ?? {};
21
22
  }
22
23
  extend() {
23
- this._cleanupAll();
24
- if (!this._reader)
25
- return;
26
- this._geometry = this._reader.read(this.extendedItem.element);
27
- this._drawOverlays();
24
+ this.refresh();
28
25
  }
29
26
  refresh() {
30
- this._cleanupAll();
31
- if (!this._reader)
27
+ if (!this._reader) {
28
+ this._cleanupAll();
32
29
  return;
30
+ }
33
31
  this._geometry = this._reader.read(this.extendedItem.element);
34
- this._drawOverlays();
32
+ if (!this._geometry) {
33
+ this._cleanupAll();
34
+ return;
35
+ }
36
+ const structureSignature = this._getOverlayStructureSignature(this._geometry);
37
+ const shouldRebuild = this._overlayStructureSignature !== structureSignature || this._hasOverlayStructureMismatch(this._geometry);
38
+ if (shouldRebuild) {
39
+ this._cleanupAll();
40
+ this._overlayStructureSignature = structureSignature;
41
+ this._drawOverlays();
42
+ return;
43
+ }
44
+ if (!this._valuesHaveChanges(...this._getRefreshValues(this._geometry))) {
45
+ return;
46
+ }
47
+ this._updateOverlayAppearance();
48
+ this._updateOverlayPositions();
35
49
  }
36
50
  dispose() {
37
51
  this._cleanupAll();
@@ -46,20 +60,138 @@ export class UnifiedGeometryExtension extends AbstractExtension {
46
60
  catch { /* ignore */ }
47
61
  }
48
62
  this._eventCleanups = [];
63
+ this._overlayStructureSignature = null;
49
64
  this._overlayCircles.clear();
50
65
  this._overlayLines = [];
51
66
  this._removeAllOverlays();
52
67
  }
68
+ _getOverlayStructureSignature(geometry) {
69
+ return geometry.segments.map(segment => `${segment.type}:${segment.cp1 ? 1 : 0}:${segment.cp2 ? 1 : 0}`).join('|');
70
+ }
71
+ _hasOverlayStructureMismatch(geometry) {
72
+ let expectedLines = 0;
73
+ let expectedCircles = 0;
74
+ for (let i = 0; i < geometry.segments.length; i++) {
75
+ const segment = geometry.segments[i];
76
+ if (segment.type === SegmentType.Close) {
77
+ continue;
78
+ }
79
+ if (this._shouldRenderAnchorHandle(i, segment, geometry)) {
80
+ expectedCircles++;
81
+ }
82
+ if (segment.cp1) {
83
+ expectedCircles++;
84
+ expectedLines++;
85
+ if (segment.type === SegmentType.QuadraticBezier) {
86
+ expectedLines++;
87
+ }
88
+ }
89
+ if (segment.cp2) {
90
+ expectedCircles++;
91
+ expectedLines++;
92
+ }
93
+ }
94
+ return this._overlayCircles.size !== expectedCircles || this._overlayLines.length !== expectedLines;
95
+ }
96
+ _getRefreshValues(geometry) {
97
+ const transformedBoxQuad = this.extendedItem.element.getBoxQuads({ relativeTo: this.designerCanvas.canvas })[0];
98
+ const values = [
99
+ this.designerCanvas.zoomFactor,
100
+ this.designerCanvas.scaleFactor,
101
+ this._overlayStructureSignature ?? ''
102
+ ];
103
+ if (transformedBoxQuad) {
104
+ values.push(transformedBoxQuad.p1.x, transformedBoxQuad.p1.y, transformedBoxQuad.p2.x, transformedBoxQuad.p2.y, transformedBoxQuad.p3.x, transformedBoxQuad.p3.y, transformedBoxQuad.p4.x, transformedBoxQuad.p4.y);
105
+ }
106
+ for (const segment of geometry.segments) {
107
+ values.push(segment.type, segment.point.x, segment.point.y);
108
+ if (segment.cp1) {
109
+ values.push(segment.cp1.x, segment.cp1.y);
110
+ }
111
+ else {
112
+ values.push('cp1:none');
113
+ }
114
+ if (segment.cp2) {
115
+ values.push(segment.cp2.x, segment.cp2.y);
116
+ }
117
+ else {
118
+ values.push('cp2:none');
119
+ }
120
+ }
121
+ return values;
122
+ }
53
123
  /** Convert an SVG-element-local coordinate to overlay (canvas) coordinate
54
124
  * using the getBoxQuads polyfill convertPointFromNode */
55
125
  _toOverlay(p) {
56
- const tp = this.designerCanvas.canvas.convertPointFromNode({ x: p.x, y: p.y }, this.extendedItem.element);
126
+ const transform = this._getSvgOverlayTransform();
127
+ if (transform && transform.bbox.width !== 0 && transform.bbox.height !== 0) {
128
+ return this._mapSvgPointToOverlay(p, transform);
129
+ }
130
+ const fallbackLocalPoint = this._toElementReferenceBoxPoint(p);
131
+ const tp = this.designerCanvas.canvas.convertPointFromNode(fallbackLocalPoint, this.extendedItem.element);
57
132
  return { x: tp.x, y: tp.y };
58
133
  }
59
134
  /** Convert overlay (canvas) coordinate back to SVG-element-local coordinate */
60
135
  _fromOverlay(p) {
136
+ const transform = this._getSvgOverlayTransform();
137
+ if (transform && transform.bbox.width !== 0 && transform.bbox.height !== 0) {
138
+ return this._mapOverlayPointToSvg(p, transform);
139
+ }
61
140
  const tp = this.extendedItem.element.convertPointFromNode({ x: p.x, y: p.y }, this.designerCanvas.canvas);
62
- return { x: tp.x, y: tp.y };
141
+ return this._fromElementReferenceBoxPoint({ x: tp.x, y: tp.y });
142
+ }
143
+ _getSvgOverlayTransform() {
144
+ const element = this.extendedItem.element;
145
+ if (!(element instanceof SVGGraphicsElement) || element instanceof SVGSVGElement) {
146
+ return null;
147
+ }
148
+ const quad = element.getBoxQuads({ relativeTo: this.designerCanvas.canvas })[0];
149
+ if (!quad) {
150
+ return null;
151
+ }
152
+ return { bbox: element.getBBox(), quad };
153
+ }
154
+ _mapSvgPointToOverlay(p, transform) {
155
+ const { bbox, quad } = transform;
156
+ const u = (p.x - bbox.x) / bbox.width;
157
+ const v = (p.y - bbox.y) / bbox.height;
158
+ return {
159
+ x: quad.p1.x + (quad.p2.x - quad.p1.x) * u + (quad.p4.x - quad.p1.x) * v,
160
+ y: quad.p1.y + (quad.p2.y - quad.p1.y) * u + (quad.p4.y - quad.p1.y) * v,
161
+ };
162
+ }
163
+ _mapOverlayPointToSvg(p, transform) {
164
+ const { bbox, quad } = transform;
165
+ const basisX = { x: quad.p2.x - quad.p1.x, y: quad.p2.y - quad.p1.y };
166
+ const basisY = { x: quad.p4.x - quad.p1.x, y: quad.p4.y - quad.p1.y };
167
+ const dx = p.x - quad.p1.x;
168
+ const dy = p.y - quad.p1.y;
169
+ const determinant = basisX.x * basisY.y - basisX.y * basisY.x;
170
+ if (Math.abs(determinant) < 1e-10) {
171
+ return this._fromElementReferenceBoxPoint(this.extendedItem.element.convertPointFromNode({ x: p.x, y: p.y }, this.designerCanvas.canvas));
172
+ }
173
+ const u = (dx * basisY.y - dy * basisY.x) / determinant;
174
+ const v = (dy * basisX.x - dx * basisX.y) / determinant;
175
+ return {
176
+ x: bbox.x + u * bbox.width,
177
+ y: bbox.y + v * bbox.height,
178
+ };
179
+ }
180
+ _toElementReferenceBoxPoint(p) {
181
+ const bbox = this._getSvgGeometryBBoxOffset();
182
+ return { x: p.x - bbox.x, y: p.y - bbox.y };
183
+ }
184
+ _fromElementReferenceBoxPoint(p) {
185
+ const bbox = this._getSvgGeometryBBoxOffset();
186
+ return { x: p.x + bbox.x, y: p.y + bbox.y };
187
+ }
188
+ _getSvgGeometryBBoxOffset() {
189
+ const element = this.extendedItem.element;
190
+ if (!(element instanceof SVGGraphicsElement) || element instanceof SVGSVGElement) {
191
+ return { x: 0, y: 0 };
192
+ }
193
+ const bbox = element.getBBox();
194
+ return { x: bbox.x, y: bbox.y };
63
195
  }
64
196
  _drawOverlays() {
65
197
  if (!this._geometry)
@@ -77,7 +209,11 @@ export class UnifiedGeometryExtension extends AbstractExtension {
77
209
  if (prevAnchor) {
78
210
  const prevOverlay = this._toOverlay(prevAnchor);
79
211
  const line = this._drawLine(prevOverlay.x, prevOverlay.y, cp1Overlay.x, cp1Overlay.y, 'svg-control-line');
80
- line.style.strokeDasharray = (4 / this.designerCanvas.scaleFactor).toString();
212
+ this._overlayLines.push(line);
213
+ }
214
+ if (seg.type === SegmentType.QuadraticBezier) {
215
+ const anchorOverlay = this._toOverlay(seg.point);
216
+ const line = this._drawLine(anchorOverlay.x, anchorOverlay.y, cp1Overlay.x, cp1Overlay.y, 'svg-control-line');
81
217
  this._overlayLines.push(line);
82
218
  }
83
219
  this._drawControlCircle(cp1Overlay, i, 'cp1');
@@ -86,15 +222,31 @@ export class UnifiedGeometryExtension extends AbstractExtension {
86
222
  const cp2Overlay = this._toOverlay(seg.cp2);
87
223
  const anchorOverlay = this._toOverlay(seg.point);
88
224
  const line = this._drawLine(anchorOverlay.x, anchorOverlay.y, cp2Overlay.x, cp2Overlay.y, 'svg-control-line');
89
- line.style.strokeDasharray = (4 / this.designerCanvas.scaleFactor).toString();
90
225
  this._overlayLines.push(line);
91
226
  this._drawControlCircle(cp2Overlay, i, 'cp2');
92
227
  }
93
228
  // Draw the anchor point
94
- const overlayPt = this._toOverlay(seg.point);
95
- this._drawAnchorCircle(overlayPt, i, seg);
229
+ if (this._shouldRenderAnchorHandle(i, seg, this._geometry)) {
230
+ const overlayPt = this._toOverlay(seg.point);
231
+ this._drawAnchorCircle(overlayPt, i, seg);
232
+ }
96
233
  prevAnchor = seg.point;
97
234
  }
235
+ this._updateOverlayAppearance();
236
+ }
237
+ _updateOverlayAppearance() {
238
+ const anchorRadius = 5 / this.designerCanvas.scaleFactor;
239
+ const controlRadius = 4 / this.designerCanvas.scaleFactor;
240
+ const strokeWidth = (1 / this.designerCanvas.zoomFactor).toString();
241
+ const dashArray = (4 / this.designerCanvas.scaleFactor).toString();
242
+ for (const [key, circle] of this._overlayCircles) {
243
+ circle.setAttribute('r', (key.startsWith('anchor-') ? anchorRadius : controlRadius).toString());
244
+ circle.style.strokeWidth = strokeWidth;
245
+ }
246
+ for (const line of this._overlayLines) {
247
+ line.style.strokeDasharray = dashArray;
248
+ line.style.strokeWidth = strokeWidth;
249
+ }
98
250
  }
99
251
  _addTrackedListener(el, event, handler) {
100
252
  el.addEventListener(event, handler);
@@ -202,6 +354,15 @@ export class UnifiedGeometryExtension extends AbstractExtension {
202
354
  const newLocal = this._fromOverlay(newOverlay);
203
355
  const seg = this._geometry.segments[segIndex];
204
356
  if (handleType === 'anchor') {
357
+ if (this.extendedItem.element instanceof SVGRectElement && this._dragRectCorner(segIndex, newLocal)) {
358
+ return;
359
+ }
360
+ if (this.extendedItem.element instanceof SVGCircleElement && this._dragCircleCardinal(segIndex, newLocal)) {
361
+ return;
362
+ }
363
+ if (this.extendedItem.element instanceof SVGEllipseElement && this._dragEllipseCardinal(segIndex, newLocal)) {
364
+ return;
365
+ }
205
366
  seg.point.x = newLocal.x;
206
367
  seg.point.y = newLocal.y;
207
368
  }
@@ -214,6 +375,172 @@ export class UnifiedGeometryExtension extends AbstractExtension {
214
375
  seg.cp2.y = newLocal.y;
215
376
  }
216
377
  }
378
+ _dragRectCorner(segIndex, newPoint) {
379
+ if (!this._geometry || !this._dragState?.originalSegments)
380
+ return false;
381
+ if (this._geometry.segments.length < 4 || segIndex < 0 || segIndex > 3)
382
+ return false;
383
+ const originalSegments = this._dragState.originalSegments;
384
+ switch (segIndex) {
385
+ case 0:
386
+ this._setRectCornerPoints(newPoint, { x: originalSegments[2].point.x, y: newPoint.y }, { ...originalSegments[2].point }, { x: newPoint.x, y: originalSegments[2].point.y });
387
+ break;
388
+ case 1:
389
+ this._setRectCornerPoints({ x: originalSegments[3].point.x, y: newPoint.y }, newPoint, { x: newPoint.x, y: originalSegments[3].point.y }, { ...originalSegments[3].point });
390
+ break;
391
+ case 2:
392
+ this._setRectCornerPoints({ ...originalSegments[0].point }, { x: newPoint.x, y: originalSegments[0].point.y }, newPoint, { x: originalSegments[0].point.x, y: newPoint.y });
393
+ break;
394
+ case 3:
395
+ this._setRectCornerPoints({ x: newPoint.x, y: originalSegments[1].point.y }, { ...originalSegments[1].point }, { x: originalSegments[1].point.x, y: newPoint.y }, newPoint);
396
+ break;
397
+ default:
398
+ return false;
399
+ }
400
+ const closeSegment = this._geometry.segments[4];
401
+ if (closeSegment?.type === SegmentType.Close) {
402
+ closeSegment.point = { ...this._geometry.segments[0].point };
403
+ }
404
+ return true;
405
+ }
406
+ _setRectCornerPoints(topLeft, topRight, bottomRight, bottomLeft) {
407
+ this._geometry.segments[0].point = { ...topLeft };
408
+ this._geometry.segments[1].point = { ...topRight };
409
+ this._geometry.segments[2].point = { ...bottomRight };
410
+ this._geometry.segments[3].point = { ...bottomLeft };
411
+ }
412
+ _dragCircleCardinal(segIndex, newPoint) {
413
+ if (!this._geometry || !this._dragState?.originalSegments)
414
+ return false;
415
+ const cardinalIndex = this._normalizeCardinalIndex(segIndex);
416
+ if (cardinalIndex === null)
417
+ return false;
418
+ const original = this._dragState.originalSegments;
419
+ const centerX = (original[1].point.x + original[3].point.x) / 2;
420
+ const centerY = (original[0].point.y + original[2].point.y) / 2;
421
+ let cx = centerX;
422
+ let cy = centerY;
423
+ let radius = Math.abs(original[1].point.x - original[3].point.x) / 2;
424
+ switch (cardinalIndex) {
425
+ case 0: {
426
+ const oppositeY = original[2].point.y;
427
+ cy = (newPoint.y + oppositeY) / 2;
428
+ radius = Math.abs(oppositeY - newPoint.y) / 2;
429
+ break;
430
+ }
431
+ case 1: {
432
+ const oppositeX = original[3].point.x;
433
+ cx = (newPoint.x + oppositeX) / 2;
434
+ radius = Math.abs(newPoint.x - oppositeX) / 2;
435
+ break;
436
+ }
437
+ case 2: {
438
+ const oppositeY = original[0].point.y;
439
+ cy = (original[0].point.y + newPoint.y) / 2;
440
+ radius = Math.abs(newPoint.y - oppositeY) / 2;
441
+ break;
442
+ }
443
+ case 3: {
444
+ const oppositeX = original[1].point.x;
445
+ cx = (oppositeX + newPoint.x) / 2;
446
+ radius = Math.abs(oppositeX - newPoint.x) / 2;
447
+ break;
448
+ }
449
+ default:
450
+ return false;
451
+ }
452
+ this._setCardinalArcPoints(cx, cy, radius, radius);
453
+ return true;
454
+ }
455
+ _dragEllipseCardinal(segIndex, newPoint) {
456
+ if (!this._geometry || !this._dragState?.originalSegments)
457
+ return false;
458
+ const cardinalIndex = this._normalizeCardinalIndex(segIndex);
459
+ if (cardinalIndex === null)
460
+ return false;
461
+ const original = this._dragState.originalSegments;
462
+ let cx = (original[1].point.x + original[3].point.x) / 2;
463
+ let cy = (original[0].point.y + original[2].point.y) / 2;
464
+ let rx = Math.abs(original[1].point.x - original[3].point.x) / 2;
465
+ let ry = Math.abs(original[2].point.y - original[0].point.y) / 2;
466
+ switch (cardinalIndex) {
467
+ case 0: {
468
+ const oppositeY = original[2].point.y;
469
+ cy = (newPoint.y + oppositeY) / 2;
470
+ ry = Math.abs(oppositeY - newPoint.y) / 2;
471
+ break;
472
+ }
473
+ case 1: {
474
+ const oppositeX = original[3].point.x;
475
+ cx = (newPoint.x + oppositeX) / 2;
476
+ rx = Math.abs(newPoint.x - oppositeX) / 2;
477
+ break;
478
+ }
479
+ case 2: {
480
+ const oppositeY = original[0].point.y;
481
+ cy = (oppositeY + newPoint.y) / 2;
482
+ ry = Math.abs(newPoint.y - oppositeY) / 2;
483
+ break;
484
+ }
485
+ case 3: {
486
+ const oppositeX = original[1].point.x;
487
+ cx = (oppositeX + newPoint.x) / 2;
488
+ rx = Math.abs(oppositeX - newPoint.x) / 2;
489
+ break;
490
+ }
491
+ default:
492
+ return false;
493
+ }
494
+ this._setCardinalArcPoints(cx, cy, rx, ry);
495
+ return true;
496
+ }
497
+ _normalizeCardinalIndex(segIndex) {
498
+ if (segIndex >= 0 && segIndex <= 3) {
499
+ return segIndex;
500
+ }
501
+ if (segIndex === 4) {
502
+ return 0;
503
+ }
504
+ return null;
505
+ }
506
+ _setCardinalArcPoints(cx, cy, rx, ry) {
507
+ if (!this._geometry)
508
+ return;
509
+ this._geometry.segments[0].point = { x: cx, y: cy - ry };
510
+ this._geometry.segments[1].point = { x: cx + rx, y: cy };
511
+ this._geometry.segments[2].point = { x: cx, y: cy + ry };
512
+ this._geometry.segments[3].point = { x: cx - rx, y: cy };
513
+ if (this._geometry.segments[1].arc) {
514
+ this._geometry.segments[1].arc.rx = rx;
515
+ this._geometry.segments[1].arc.ry = ry;
516
+ }
517
+ if (this._geometry.segments[2].arc) {
518
+ this._geometry.segments[2].arc.rx = rx;
519
+ this._geometry.segments[2].arc.ry = ry;
520
+ }
521
+ if (this._geometry.segments[3].arc) {
522
+ this._geometry.segments[3].arc.rx = rx;
523
+ this._geometry.segments[3].arc.ry = ry;
524
+ }
525
+ if (this._geometry.segments[4]?.arc) {
526
+ this._geometry.segments[4].point = { x: cx, y: cy - ry };
527
+ this._geometry.segments[4].arc.rx = rx;
528
+ this._geometry.segments[4].arc.ry = ry;
529
+ }
530
+ if (this._geometry.segments[5]?.type === SegmentType.Close) {
531
+ this._geometry.segments[5].point = { x: cx, y: cy - ry };
532
+ }
533
+ }
534
+ _shouldRenderAnchorHandle(segIndex, seg, geometry) {
535
+ if (!(this.extendedItem.element instanceof SVGCircleElement) && !(this.extendedItem.element instanceof SVGEllipseElement)) {
536
+ return true;
537
+ }
538
+ if (segIndex !== 4) {
539
+ return true;
540
+ }
541
+ const firstSegment = geometry.segments[0];
542
+ return seg.point.x !== firstSegment?.point.x || seg.point.y !== firstSegment?.point.y;
543
+ }
217
544
  _dragWholeShape(dx, dy) {
218
545
  if (!this._dragState?.originalSegments || !this._geometry)
219
546
  return;
@@ -264,6 +591,14 @@ export class UnifiedGeometryExtension extends AbstractExtension {
264
591
  line.setAttribute('x2', cp1Overlay.x.toString());
265
592
  line.setAttribute('y2', cp1Overlay.y.toString());
266
593
  }
594
+ if (seg.type === SegmentType.QuadraticBezier && lineIdx < this._overlayLines.length) {
595
+ const anchorOverlay = this._toOverlay(seg.point);
596
+ const line = this._overlayLines[lineIdx++];
597
+ line.setAttribute('x1', anchorOverlay.x.toString());
598
+ line.setAttribute('y1', anchorOverlay.y.toString());
599
+ line.setAttribute('x2', cp1Overlay.x.toString());
600
+ line.setAttribute('y2', cp1Overlay.y.toString());
601
+ }
267
602
  const cp1Circle = this._overlayCircles.get(`cp1-${i}`);
268
603
  if (cp1Circle) {
269
604
  cp1Circle.setAttribute('cx', cp1Overlay.x.toString());
@@ -286,11 +621,13 @@ export class UnifiedGeometryExtension extends AbstractExtension {
286
621
  cp2Circle.setAttribute('cy', cp2Overlay.y.toString());
287
622
  }
288
623
  }
289
- const overlayPt = this._toOverlay(seg.point);
290
- const anchorCircle = this._overlayCircles.get(`anchor-${i}`);
291
- if (anchorCircle) {
292
- anchorCircle.setAttribute('cx', overlayPt.x.toString());
293
- anchorCircle.setAttribute('cy', overlayPt.y.toString());
624
+ if (this._shouldRenderAnchorHandle(i, seg, this._geometry)) {
625
+ const overlayPt = this._toOverlay(seg.point);
626
+ const anchorCircle = this._overlayCircles.get(`anchor-${i}`);
627
+ if (anchorCircle) {
628
+ anchorCircle.setAttribute('cx', overlayPt.x.toString());
629
+ anchorCircle.setAttribute('cy', overlayPt.y.toString());
630
+ }
294
631
  }
295
632
  prevAnchor = seg.point;
296
633
  }
@@ -423,6 +760,31 @@ export class UnifiedGeometryExtension extends AbstractExtension {
423
760
  }
424
761
  });
425
762
  }
763
+ if (seg.type !== SegmentType.Arc) {
764
+ items.push({
765
+ title: 'Convert to arc', action: () => {
766
+ this._convertToArc(segIndex, false);
767
+ this._applyGeometryToElement();
768
+ this._commitGeometryChange();
769
+ }
770
+ });
771
+ items.push({
772
+ title: 'Convert to inverted arc', action: () => {
773
+ this._convertToArc(segIndex, true);
774
+ this._applyGeometryToElement();
775
+ this._commitGeometryChange();
776
+ }
777
+ });
778
+ }
779
+ else {
780
+ items.push({
781
+ title: 'Invert arc', action: () => {
782
+ this._invertArc(segIndex);
783
+ this._applyGeometryToElement();
784
+ this._commitGeometryChange();
785
+ }
786
+ });
787
+ }
426
788
  }
427
789
  if (items.length > 0) {
428
790
  ContextMenu.show(items, event);
@@ -565,6 +927,37 @@ export class UnifiedGeometryExtension extends AbstractExtension {
565
927
  }
566
928
  delete seg.arc;
567
929
  }
930
+ _convertToArc(segIndex, inverted = false) {
931
+ if (!this._geometry)
932
+ return;
933
+ const seg = this._geometry.segments[segIndex];
934
+ const prevPt = this._findPreviousAnchor(segIndex);
935
+ if (!prevPt)
936
+ return;
937
+ const endPt = seg.point;
938
+ const dx = endPt.x - prevPt.x;
939
+ const dy = endPt.y - prevPt.y;
940
+ const dist = Math.sqrt(dx * dx + dy * dy);
941
+ const radius = Math.max(1, dist / 2);
942
+ seg.type = SegmentType.Arc;
943
+ seg.arc = {
944
+ rx: radius,
945
+ ry: radius,
946
+ rotation: 0,
947
+ largeArc: false,
948
+ sweep: !inverted,
949
+ };
950
+ delete seg.cp1;
951
+ delete seg.cp2;
952
+ }
953
+ _invertArc(segIndex) {
954
+ if (!this._geometry)
955
+ return;
956
+ const seg = this._geometry.segments[segIndex];
957
+ if (seg.type !== SegmentType.Arc || !seg.arc)
958
+ return;
959
+ seg.arc.sweep = !seg.arc.sweep;
960
+ }
568
961
  // -- Bezier evaluation --
569
962
  _evalCubicBezier(p0, p1, p2, p3, t) {
570
963
  const mt = 1 - t;