@design-core/core 0.0.6 → 0.0.7

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 (145) hide show
  1. package/.eslintrc.json +4 -0
  2. package/TODO.md +49 -6
  3. package/core/core/core.js +0 -3
  4. package/core/dimensions/alignedDimension.js +5 -4
  5. package/core/dimensions/angularDimension.js +6 -5
  6. package/core/dimensions/baseCircularDimension.js +3 -2
  7. package/core/dimensions/baseDimension.js +59 -93
  8. package/core/dimensions/baseLinearDimension.js +6 -5
  9. package/core/dimensions/diametricDimension.js +4 -3
  10. package/core/dimensions/dimension.js +21 -14
  11. package/core/dimensions/radialDimension.js +4 -3
  12. package/core/dimensions/rotatedDimension.js +12 -11
  13. package/core/entities/arc.js +112 -145
  14. package/core/entities/arctext.js +212 -174
  15. package/core/entities/basePolyline.js +30 -282
  16. package/core/entities/circle.js +77 -99
  17. package/core/entities/entity.js +114 -93
  18. package/core/entities/hatch.js +322 -249
  19. package/core/entities/insert.js +66 -81
  20. package/core/entities/line.js +21 -142
  21. package/core/entities/lwpolyline.js +16 -11
  22. package/core/entities/polyline.js +4 -4
  23. package/core/entities/rectangle.js +3 -1
  24. package/core/entities/solid.js +6 -15
  25. package/core/entities/text.js +188 -199
  26. package/core/lib/auxiliary/rubberBand.js +32 -0
  27. package/core/lib/auxiliary/selectionWindow.js +63 -0
  28. package/core/lib/auxiliary/snapPoint.js +113 -0
  29. package/core/lib/auxiliary/trackingLine.js +76 -0
  30. package/core/lib/canvas.js +228 -166
  31. package/core/lib/clipboard.js +7 -1
  32. package/core/lib/colour.js +16 -0
  33. package/core/lib/commandLine.js +1 -6
  34. package/core/lib/commandManager.js +18 -8
  35. package/core/lib/constants.js +28 -0
  36. package/core/lib/dxf/dxf.js +4 -3
  37. package/core/lib/entityManager.js +18 -11
  38. package/core/lib/inputManager.js +35 -11
  39. package/core/lib/intersect.js +25 -0
  40. package/core/lib/patterns.js +8 -1
  41. package/core/lib/plotOptions.js +67 -0
  42. package/core/lib/polylineUtils.js +75 -0
  43. package/core/lib/renderers/cairoRenderer.js +371 -0
  44. package/core/lib/renderers/canvasRenderer.js +315 -0
  45. package/core/lib/renderers/pdfRenderer.js +366 -0
  46. package/core/lib/renderers/rendererBase.js +284 -0
  47. package/core/lib/renderers/svgRenderer.js +343 -0
  48. package/core/lib/scene.js +27 -5
  49. package/core/lib/selectionManager.js +2 -2
  50. package/core/lib/settings.js +4 -2
  51. package/core/lib/snapping.js +4 -191
  52. package/core/lib/stateManager.js +30 -1
  53. package/core/lib/utils.js +21 -2
  54. package/core/properties/entityProperties.js +104 -0
  55. package/core/properties/property.js +106 -2
  56. package/core/properties/propertyManager.js +29 -13
  57. package/core/tables/block.js +4 -37
  58. package/core/tables/endblk.js +1 -1
  59. package/core/tables/layer.js +8 -0
  60. package/core/tables/styleManagerBase.js +3 -2
  61. package/core/tables/tableManagerBase.js +8 -3
  62. package/core/tools/chamfer.js +111 -137
  63. package/core/tools/chamferFilletBase.js +135 -34
  64. package/core/tools/copy.js +3 -3
  65. package/core/tools/explode.js +2 -2
  66. package/core/tools/extend.js +132 -19
  67. package/core/tools/fillet.js +86 -183
  68. package/core/tools/matchProp.js +2 -11
  69. package/core/tools/mirror.js +7 -6
  70. package/core/tools/move.js +3 -3
  71. package/core/tools/offset.js +3 -5
  72. package/core/tools/pasteclip.js +1 -1
  73. package/core/tools/rotate.js +6 -3
  74. package/core/tools/scale.js +1 -1
  75. package/core/tools/trim.js +265 -23
  76. package/core/tools/zoom.js +1 -1
  77. package/jest.config.js +6 -0
  78. package/package.json +5 -3
  79. package/test/dimensions/alignedDimension.test.js +14 -7
  80. package/test/dimensions/angularDimension.test.js +6 -6
  81. package/test/dimensions/baseDimension.test.js +69 -8
  82. package/test/dimensions/baseLinearDimension.test.js +62 -0
  83. package/test/dimensions/diametricDimension.test.js +6 -6
  84. package/test/dimensions/radialDimension.test.js +6 -6
  85. package/test/dimensions/rotatedDimension.test.js +14 -7
  86. package/test/entities/arc.test.js +80 -83
  87. package/test/entities/arctext.test.js +180 -53
  88. package/test/entities/basePolyline.test.js +68 -2
  89. package/test/entities/circle.test.js +57 -85
  90. package/test/entities/entity.test.js +42 -8
  91. package/test/entities/hatch.test.js +543 -70
  92. package/test/entities/insert.test.js +96 -1
  93. package/test/entities/line.test.js +15 -41
  94. package/test/entities/lwpolyline.test.js +104 -0
  95. package/test/entities/polyline.test.js +11 -0
  96. package/test/entities/rectangle.test.js +11 -0
  97. package/test/entities/solid.test.js +1 -1
  98. package/test/entities/text.test.js +294 -69
  99. package/test/integration/exportIntegration.reference.dxf +9 -9
  100. package/test/integration/exportIntegration.test.js +2 -2
  101. package/test/integration/intersectionIntegration.test.js +4 -5
  102. package/test/integration/pdfRendererIntegration.reference.pdf +17709 -0
  103. package/test/integration/pdfRendererIntegration.test.js +61 -0
  104. package/test/integration/svgRendererIntegration.reference.svg +3343 -0
  105. package/test/integration/svgRendererIntegration.test.js +61 -0
  106. package/test/integration/userInputIntegration.reference.dxf +1165 -105
  107. package/test/integration/userInputIntegration.test.js +239 -132
  108. package/test/lib/canvas.test.js +742 -123
  109. package/test/lib/constants.test.js +27 -0
  110. package/test/lib/entityManager.test.js +4 -4
  111. package/test/lib/inputManager.test.js +80 -19
  112. package/test/lib/intersect.test.js +83 -0
  113. package/test/lib/plotOptions.test.js +179 -0
  114. package/test/lib/rubberBand.test.js +84 -0
  115. package/test/lib/settings.test.js +5 -6
  116. package/test/lib/snapping.test.js +71 -129
  117. package/test/lib/stateManager.test.js +7 -7
  118. package/test/lib/utils.test.js +36 -2
  119. package/test/properties/entityProperties.test.js +132 -0
  120. package/test/properties/property.test.js +53 -0
  121. package/test/properties/propertyManager.test.js +3 -3
  122. package/test/renderers/canvasRenderer.test.js +317 -0
  123. package/test/renderers/pdfRenderer.test.js +271 -0
  124. package/test/renderers/rendererBase.test.js +213 -0
  125. package/test/renderers/svgRenderer.test.js +326 -0
  126. package/test/tables/block.test.js +31 -1
  127. package/test/tables/endblk.test.js +2 -2
  128. package/test/tables/layer.test.js +30 -0
  129. package/test/test-helpers/hatchGridRenderer.js +180 -0
  130. package/test/test-helpers/test-helpers.js +152 -0
  131. package/test/tools/chamfer.test.js +227 -5
  132. package/test/tools/chamferFilletBase.test.js +98 -9
  133. package/test/tools/copy.test.js +2 -2
  134. package/test/tools/explode.test.js +3 -3
  135. package/test/tools/extend.test.js +331 -0
  136. package/test/tools/fillet.test.js +181 -0
  137. package/test/tools/matchProp.test.js +5 -5
  138. package/test/tools/mirror.test.js +13 -13
  139. package/test/tools/offset.test.js +170 -3
  140. package/test/tools/pasteclip.test.js +5 -5
  141. package/test/tools/rotate.test.js +32 -2
  142. package/test/tools/scale.test.js +5 -5
  143. package/test/tools/trim.test.js +526 -0
  144. package/test/tools/zoom.test.js +7 -7
  145. package/core/lib/selectionWindow.js +0 -83
@@ -10,10 +10,11 @@ import { Line } from './line.js';
10
10
  import { Arc } from './arc.js';
11
11
  import { Flags } from '../properties/flags.js';
12
12
  import { Property } from '../properties/property.js';
13
- import { AddState, RemoveState, UpdateState } from '../lib/stateManager.js';
13
+
14
14
 
15
15
  import { DesignCore } from '../designCore.js';
16
- import { SnapPoint } from '../lib/snapping.js';
16
+ import { SnapPoint } from '../lib/auxiliary/snapPoint.js';
17
+ import { RubberBand } from '../lib/auxiliary/rubberBand.js';
17
18
 
18
19
  /**
19
20
  * Base Polyline Entity Class
@@ -75,6 +76,14 @@ export class BasePolyline extends Entity {
75
76
 
76
77
  this.flags.setFlagValue(Property.loadValue([data?.flags, data?.[70]], 0));
77
78
 
79
+ // DXF Groupcode 70 bit 1 - closed flag exposed via EntityProperties
80
+ this.properties.add(Property.Names.CLOSED, {
81
+ type: Property.Type.BOOLEAN,
82
+ get: (entity) => entity.flags.hasFlag(1),
83
+ set: (entity, value) => value ? entity.flags.addValue(1) : entity.flags.removeValue(1),
84
+ dxfCode: 70,
85
+ });
86
+
78
87
  // If the first and last points are the same the polyline should be considered closed.
79
88
  // Require at least 4 points so that after removing the duplicate endpoint at least 3 remain.
80
89
  if (this.points.length >= 4 && this.points[0].isSame(this.points.at(-1))) {
@@ -161,54 +170,25 @@ export class BasePolyline extends Entity {
161
170
 
162
171
  if (this.points.length >= 1) {
163
172
  const points = [...this.points, mousePoint];
164
- DesignCore.Scene.tempEntities.create(this.type, { points: points });
165
- }
166
-
167
- if (this.inputMode === this.modes.ARC) {
168
- const arcpoints = Utils.cloneObject(this.points);
169
- arcpoints.at(-1).bulge = this.getBulgeFromSegment(mousePoint);
170
- const points = [...arcpoints, mousePoint];
171
- DesignCore.Scene.tempEntities.create(this.type, { points: points });
173
+ if (this.inputMode === this.modes.ARC) {
174
+ // Straight segment is not the final form — show as rubber-band indicator
175
+ DesignCore.Scene.auxiliaryEntities.add(new RubberBand([this.points.at(-1), mousePoint]));
176
+ // Arc segment is the final form — draw on the current layer
177
+ const arcpoints = Utils.cloneObject(this.points);
178
+ arcpoints.at(-1).bulge = this.getBulgeFromSegment(mousePoint);
179
+ DesignCore.Scene.previewEntities.create(this.type, { points: [...arcpoints, mousePoint] });
180
+ } else {
181
+ DesignCore.Scene.previewEntities.create(this.type, { points: points });
182
+ }
172
183
  }
173
184
  }
174
185
 
175
186
  /**
176
187
  * Draw the polyline
177
- * @param {Object} ctx
178
- * @param {number} scale
179
- * @param {boolean} stroke - don't stroke hatch boundary shapes
188
+ * @param {Object} renderer
180
189
  */
181
- draw(ctx, scale, stroke = true) {
182
- for (let i = 0; i < this.points.length; i++) {
183
- if (this.points[i].bulge === 0) {
184
- ctx.lineTo(this.points[i].x, this.points[i].y);
185
- } else {
186
- // define the next point or the first point for closed shapes
187
- const nextPoint = this.points[i + 1] || this.points[0];
188
- const centerPoint = this.points[i].bulgeCentrePoint(nextPoint);
189
- const radius = this.points[i].bulgeRadius(nextPoint);
190
-
191
- if (this.points[i].bulge > 0) {
192
- // TODO: make this work with canvas
193
- ctx.arc(centerPoint.x, centerPoint.y, radius, centerPoint.angle(this.points[i]), centerPoint.angle(nextPoint));
194
- } else {
195
- try { // HTML
196
- ctx.arc(centerPoint.x, centerPoint.y, radius, centerPoint.angle(this.points[i]), centerPoint.angle(nextPoint), true);
197
- } catch { // Cairo
198
- ctx.arcNegative(centerPoint.x, centerPoint.y, radius, centerPoint.angle(this.points[i]), centerPoint.angle(nextPoint));
199
- }
200
- }
201
- }
202
- }
203
-
204
- // handle a closed shape
205
- if (this.flags.hasFlag(1)) {
206
- ctx.lineTo(this.points[0].x, this.points[0].y);
207
- }
208
-
209
- if (stroke) {
210
- ctx.stroke();
211
- }
190
+ draw(renderer) {
191
+ renderer.drawShape(this.toPolylinePoints());
212
192
  }
213
193
 
214
194
  /**
@@ -217,12 +197,12 @@ export class BasePolyline extends Entity {
217
197
  */
218
198
  dxf(file) {
219
199
  file.writeGroupCode('0', 'LWPOLYLINE');
220
- file.writeGroupCode('5', this.handle, DXFFile.Version.R2000); // Handle
200
+ file.writeGroupCode('5', this.getProperty(Property.Names.HANDLE), DXFFile.Version.R2000); // Handle
221
201
  file.writeGroupCode('100', 'AcDbEntity', DXFFile.Version.R2000);
222
202
  file.writeGroupCode('100', 'AcDbPolyline', DXFFile.Version.R2000);
223
- file.writeGroupCode('8', this.layer); // LAYERNAME
224
- file.writeGroupCode('6', this.lineType);
225
- file.writeGroupCode('39', this.lineWidth);
203
+ file.writeGroupCode('8', this.getProperty(Property.Names.LAYER)); // LAYERNAME
204
+ file.writeGroupCode('6', this.getProperty(Property.Names.LINETYPE));
205
+ file.writeGroupCode('39', this.getProperty(Property.Names.LINEWIDTH));
226
206
  file.writeGroupCode('90', this.points.length);
227
207
  file.writeGroupCode('70', this.flags.getFlagValue());
228
208
  for (let i = 0; i < this.points.length; i++) {
@@ -253,6 +233,7 @@ export class BasePolyline extends Entity {
253
233
  /**
254
234
  * Set entity points from a polyline point representation
255
235
  * @param {Array} points
236
+ * @return {BasePolyline}
256
237
  */
257
238
  fromPolylinePoints(points) {
258
239
  const pts = points.map((p) => new Point(p.x, p.y, p.bulge));
@@ -260,6 +241,7 @@ export class BasePolyline extends Entity {
260
241
  pts.pop();
261
242
  }
262
243
  this.points = pts;
244
+ return this;
263
245
  }
264
246
 
265
247
  /**
@@ -492,240 +474,6 @@ export class BasePolyline extends Entity {
492
474
  return A.distance(point);
493
475
  }
494
476
 
495
- /**
496
- * Extend the entity
497
- * Only extends when the end segment closest to the mouse is a line segment (bulge === 0)
498
- * @param {Array} intersections - array of intersection points
499
- * @return {Array} - array of state changes
500
- */
501
- extend(intersections) {
502
- const stateChanges = [];
503
-
504
- if (!intersections?.length) {
505
- return stateChanges;
506
- }
507
-
508
- const mousePosition = DesignCore.Mouse.pointOnScene();
509
- const lastIndex = this.points.length - 1;
510
-
511
- // Determine which end segment is closer to the mouse
512
- const closestOnFirst = this.closestPointOnSegment(mousePosition, 1);
513
- const closestOnLast = this.closestPointOnSegment(mousePosition, lastIndex);
514
- const distToFirst = mousePosition.distance(closestOnFirst);
515
- const distToLast = mousePosition.distance(closestOnLast);
516
-
517
- // Identify the selected end: the endpoint index and the bulge of its segment
518
- let endPointIndex;
519
- let endSegmentBulge;
520
-
521
- if (distToFirst < distToLast) {
522
- endPointIndex = 0;
523
- endSegmentBulge = this.points[0].bulge;
524
- } else if (distToLast < distToFirst) {
525
- endPointIndex = lastIndex;
526
- endSegmentBulge = this.points[lastIndex - 1].bulge;
527
- } else {
528
- // Tie (includes 2-point polyline where both segments are the same)
529
- // Break tie by distance to the actual endpoints
530
- if (mousePosition.distance(this.points[0]) <= mousePosition.distance(this.points[lastIndex])) {
531
- endPointIndex = 0;
532
- endSegmentBulge = this.points[0].bulge;
533
- } else {
534
- endPointIndex = lastIndex;
535
- endSegmentBulge = this.points[lastIndex - 1].bulge;
536
- }
537
- }
538
-
539
- // Only allow extending line segments (not arcs)
540
- if (endSegmentBulge !== 0) {
541
- DesignCore.Core.notify(`${this.type} - ${Strings.Message.NOEXTEND}`);
542
- return stateChanges;
543
- }
544
-
545
- const endPoint = this.points[endPointIndex];
546
- const adjacentPoint = endPointIndex === 0 ? this.points[1] : this.points[lastIndex - 1];
547
-
548
- // Sort intersections by distance from the end point
549
- Utils.sortPointsByDistance(intersections, endPoint);
550
-
551
- // Find the closest intersection that lies beyond the endpoint
552
- const direction = endPoint.subtract(adjacentPoint);
553
- const newEndPoint = intersections.find((p) => {
554
- if (p.isSame(endPoint)) return false;
555
- if (p.subtract(endPoint).dot(direction) <= 0) return false;
556
- if (adjacentPoint.distance(p) <= adjacentPoint.distance(endPoint)) return false;
557
- return true;
558
- });
559
-
560
- if (!newEndPoint) {
561
- return stateChanges;
562
- }
563
-
564
- // Build the new points array
565
- const newPoints = this.points.map((p) => p.clone());
566
- newPoints[endPointIndex] = newEndPoint;
567
-
568
- if (newPoints[endPointIndex].isSame(this.points[endPointIndex])) {
569
- return stateChanges;
570
- }
571
-
572
- stateChanges.push(new UpdateState(this, { points: newPoints }));
573
- return stateChanges;
574
- }
575
-
576
- /**
577
- * Trim the entity
578
- * @param {Array} intersections - array of intersection points
579
- * @return {Array} - array of state changes
580
- */
581
- trim(intersections) {
582
- const stateChanges = [];
583
-
584
- if (!intersections?.length) {
585
- return stateChanges;
586
- }
587
-
588
- const mousePosition = DesignCore.Mouse.pointOnScene();
589
-
590
- // Find the segment index closest to the mouse
591
- let mouseSegmentIndex = 0;
592
- let minDistance = Infinity;
593
-
594
- for (let i = 1; i < this.points.length; i++) {
595
- const closestPoint = this.closestPointOnSegment(mousePosition, i);
596
- if (closestPoint) {
597
- const dist = mousePosition.distance(closestPoint);
598
- if (dist < minDistance) {
599
- minDistance = dist;
600
- mouseSegmentIndex = i;
601
- }
602
- }
603
- }
604
-
605
- // Filter out intersections that coincide with existing polyline vertices
606
- const filteredIntersections = intersections.filter((point) => !this.points.some((vertex) => vertex.isSame(point)));
607
-
608
- if (!filteredIntersections.length) {
609
- return stateChanges;
610
- }
611
-
612
- // Locate each intersection on its segment with an ordering value
613
- const locatedIntersections = [];
614
-
615
- for (const point of filteredIntersections) {
616
- for (let i = 1; i < this.points.length; i++) {
617
- if (this.isPointOnSegment(point, this.points[i - 1], this.points[i])) {
618
- locatedIntersections.push({
619
- segmentIndex: i,
620
- point: point,
621
- positionAlongSegment: this.positionOnSegment(point, i),
622
- });
623
- break;
624
- }
625
- }
626
- }
627
-
628
- if (!locatedIntersections.length) {
629
- return stateChanges;
630
- }
631
-
632
- // Sort by segment index, then by position along segment
633
- locatedIntersections.sort((a, b) => {
634
- if (a.segmentIndex !== b.segmentIndex) return a.segmentIndex - b.segmentIndex;
635
- return a.positionAlongSegment - b.positionAlongSegment;
636
- });
637
-
638
- // Precompute the mouse position along its segment
639
- const mouseClosest = this.closestPointOnSegment(mousePosition, mouseSegmentIndex);
640
- const mousePos = mouseClosest ? this.positionOnSegment(mouseClosest, mouseSegmentIndex) : 0;
641
-
642
- // Find the nearest intersections that bracket the mouse segment
643
- // - trimBefore: the last intersection at or before the mouse
644
- // - trimAfter: the first intersection at or after the mouse
645
- let trimBefore = null;
646
- let trimAfter = null;
647
-
648
- for (const loc of locatedIntersections) {
649
- if (loc.segmentIndex < mouseSegmentIndex) {
650
- trimBefore = loc;
651
- } else if (loc.segmentIndex === mouseSegmentIndex) {
652
- if (loc.positionAlongSegment <= mousePos) {
653
- trimBefore = loc;
654
- } else if (!trimAfter) {
655
- trimAfter = loc;
656
- }
657
- } else if (!trimAfter) {
658
- trimAfter = loc;
659
- }
660
- }
661
-
662
- // Build new polyline(s) from the remaining portions
663
- // Portion 1: start of polyline to trimBefore point
664
- if (trimBefore) {
665
- const points = [];
666
-
667
- for (let i = 0; i < trimBefore.segmentIndex - 1; i++) {
668
- points.push(this.points[i].clone());
669
- }
670
-
671
- // Add the segment start point (with adjusted bulge for arcs)
672
- const segStart = this.points[trimBefore.segmentIndex - 1];
673
- const segStartClone = segStart.clone();
674
- if (segStart.bulge !== 0) {
675
- segStartClone.bulge = segStart.partialBulge(this.points[trimBefore.segmentIndex], trimBefore.point);
676
- }
677
- points.push(segStartClone);
678
-
679
- // Add the trim point (skip if it coincides with the last added point)
680
- const trimPoint = trimBefore.point.clone();
681
- if (!points.at(-1).isSame(trimPoint)) {
682
- points.push(trimPoint);
683
- }
684
-
685
- if (points.length >= 2) {
686
- const polyline = Utils.cloneObject(this);
687
- polyline.points = points;
688
- polyline.flags.removeValue(1); // clear closed bit
689
- stateChanges.push(new AddState(polyline));
690
- }
691
- }
692
-
693
- // Portion 2: trimAfter point to end of polyline
694
- if (trimAfter) {
695
- const points = [];
696
- const trimPoint = trimAfter.point.clone();
697
-
698
- // If the segment is an arc, set remaining arc bulge
699
- const segStart = this.points[trimAfter.segmentIndex - 1];
700
- if (segStart.bulge !== 0) {
701
- trimPoint.bulge = segStart.partialBulge(this.points[trimAfter.segmentIndex], trimPoint, true);
702
- }
703
-
704
- points.push(trimPoint);
705
-
706
- // Add remaining points (skip if coincides with the trim point)
707
- for (let i = trimAfter.segmentIndex; i < this.points.length; i++) {
708
- const nextPoint = this.points[i].clone();
709
- if (!points.at(-1).isSame(nextPoint)) {
710
- points.push(nextPoint);
711
- }
712
- }
713
-
714
- if (points.length >= 2) {
715
- const polyline = Utils.cloneObject(this);
716
- polyline.points = points;
717
- polyline.flags.removeValue(1); // clear closed bit
718
- stateChanges.push(new AddState(polyline));
719
- }
720
- }
721
-
722
- if (trimBefore || trimAfter) {
723
- stateChanges.push(new RemoveState(this));
724
- }
725
-
726
- return stateChanges;
727
- }
728
-
729
477
  /**
730
478
  * Return boundingbox for entity
731
479
  * @return {BoundingBox}
@@ -5,12 +5,11 @@ import { Input, PromptOptions } from '../lib/inputManager.js';
5
5
  import { Logging } from '../lib/logging.js';
6
6
  import { DXFFile } from '../lib/dxf/dxfFile.js';
7
7
  import { BoundingBox } from '../lib/boundingBox.js';
8
- import { Property } from '../properties/property.js';
9
- import { Utils } from '../lib/utils.js';
10
-
11
8
  import { DesignCore } from '../designCore.js';
12
- import { AddState, RemoveState } from '../lib/stateManager.js';
13
- import { SnapPoint } from '../lib/snapping.js';
9
+
10
+ import { Property } from '../properties/property.js';
11
+ import { SnapPoint } from '../lib/auxiliary/snapPoint.js';
12
+ import { RubberBand } from '../lib/auxiliary/rubberBand.js';
14
13
 
15
14
  /**
16
15
  * Circle Entity Class
@@ -26,19 +25,20 @@ export class Circle extends Entity {
26
25
  constructor(data) {
27
26
  super(data);
28
27
 
29
- // add radius property with getter and setter
30
- // needs to be enumerable to appear in the object props
31
- Object.defineProperty(this, 'radius', {
32
- get: this.getRadius,
33
- set: this.setRadius,
34
- enumerable: true,
28
+ // radius: computed from points — stored in EntityProperties
29
+ this.properties.add(Property.Names.RADIUS, {
30
+ type: Property.Type.NUMBER,
31
+ get: (entity) => entity.points[1] ? entity.points[0].distance(entity.points[1]) : 0,
32
+ set: (entity, rad) => {
33
+ entity.points[1] = entity.points[0].project(0, rad);
34
+ },
35
+ dxfCode: 40,
35
36
  });
36
37
 
37
- if (data) {
38
- if (data.hasOwnProperty('radius') || data.hasOwnProperty('40')) {
39
- // DXF Groupcode 40 - Radius
40
- this.setRadius(Property.loadValue([data.radius, data[40]], 0));
41
- }
38
+ // Named scalar radius (internal API / named prop) — projects points[1] from center.
39
+ // Full-points data (post-execute or post-fromDxf) skips this.
40
+ if (data?.radius !== undefined && this.points.length < 2) {
41
+ this.setRadius(data.radius);
42
42
  }
43
43
  }
44
44
 
@@ -54,6 +54,21 @@ export class Circle extends Entity {
54
54
  return command;
55
55
  }
56
56
 
57
+ /**
58
+ * Normalise DXF group codes into the canonical points-based representation.
59
+ * Called by the DXF loader before construction.
60
+ * @param {Object} data
61
+ * @return {Object}
62
+ */
63
+ static fromDxf(data) {
64
+ const center = data.points?.[0];
65
+ if (!center || data[40] === undefined) return data;
66
+ return {
67
+ ...data,
68
+ points: [center, center.project(0, data[40])],
69
+ };
70
+ }
71
+
57
72
  /**
58
73
  * Execute method
59
74
  * executes the workflow, requesting input required to create an entity
@@ -113,8 +128,8 @@ export class Circle extends Entity {
113
128
  preview() {
114
129
  if (this.points.length >= 1) {
115
130
  const mousePoint = DesignCore.Mouse.pointOnScene();
116
- const points = [this.points.at(-1), mousePoint];
117
- DesignCore.Scene.tempEntities.create(this.type, { points: points });
131
+ DesignCore.Scene.auxiliaryEntities.add(new RubberBand([this.points.at(0), mousePoint]));
132
+ DesignCore.Scene.previewEntities.create(this.type, { points: [this.points.at(0), mousePoint] });
118
133
  }
119
134
  }
120
135
 
@@ -139,9 +154,12 @@ export class Circle extends Entity {
139
154
  * @param {Object} ctx - context
140
155
  * @param {number} scale
141
156
  */
142
- draw(ctx, scale) {
143
- ctx.arc(this.points[0].x, this.points[0].y, this.radius, 0, 6.283);
144
- ctx.stroke();
157
+ /**
158
+ * Draw the circle
159
+ * @param {Object} renderer
160
+ */
161
+ draw(renderer) {
162
+ renderer.drawShape(this.toPolylinePoints());
145
163
  }
146
164
 
147
165
  /**
@@ -150,17 +168,17 @@ export class Circle extends Entity {
150
168
  */
151
169
  dxf(file) {
152
170
  file.writeGroupCode('0', 'CIRCLE');
153
- file.writeGroupCode('5', this.handle, DXFFile.Version.R2000); // Handle
171
+ file.writeGroupCode('5', this.getProperty(Property.Names.HANDLE), DXFFile.Version.R2000); // Handle
154
172
  file.writeGroupCode('100', 'AcDbEntity', DXFFile.Version.R2000);
155
173
  file.writeGroupCode('100', 'AcDbCircle', DXFFile.Version.R2000);
156
- file.writeGroupCode('8', this.layer);
157
- file.writeGroupCode('6', this.lineType);
174
+ file.writeGroupCode('8', this.getProperty(Property.Names.LAYER));
175
+ file.writeGroupCode('6', this.getProperty(Property.Names.LINETYPE));
158
176
  this.writeDxfColour(file);
159
177
  file.writeGroupCode('10', this.points[0].x);
160
178
  file.writeGroupCode('20', this.points[0].y);
161
179
  file.writeGroupCode('30', '0.0');
162
- file.writeGroupCode('39', this.lineWidth);
163
- file.writeGroupCode('40', this.radius);
180
+ file.writeGroupCode('39', this.getProperty(Property.Names.LINEWIDTH));
181
+ file.writeGroupCode('40', this.getProperty(Property.Names.RADIUS));
164
182
  }
165
183
 
166
184
  /**
@@ -168,76 +186,34 @@ export class Circle extends Entity {
168
186
  * @return {Array}
169
187
  */
170
188
  toPolylinePoints() {
171
- const startPoint = this.points[0].project(0, this.radius);
189
+ const radius = this.getProperty(Property.Names.RADIUS);
190
+ const startPoint = this.points[0].project(0, radius);
172
191
  startPoint.bulge = 1;
173
- const endPoint = this.points[0].project(0, -this.radius);
192
+ const endPoint = this.points[0].project(0, -radius);
174
193
  endPoint.bulge = 1;
175
- const closurePoint = this.points[0].project(0, this.radius);
194
+ const closurePoint = this.points[0].project(0, radius);
176
195
  return [startPoint, endPoint, closurePoint];
177
196
  }
178
197
 
179
198
  /**
180
- * Set entity points from a polyline point representation
199
+ * Set entity points from a polyline point representation.
200
+ * If the points describe a full circle (start === end), mutates this Circle
201
+ * in place and returns it. Otherwise creates and returns an Arc, since a
202
+ * Circle cannot represent an open arc.
181
203
  * @param {Array} points
204
+ * @return {Circle|Arc}
182
205
  */
183
206
  fromPolylinePoints(points) {
184
- const center = points[0].bulgeCentrePoint(points[1]);
185
- const radius = center.distance(points[0]);
186
- this.points = [center, center.project(0, radius)];
187
- }
188
-
189
- /**
190
- * Trim the entity
191
- * @param {Array} intersections
192
- * @return {Array} - array of state changes
193
- */
194
- trim(intersections) {
195
- // array to hold state changes
196
- const stateChanges = [];
197
-
198
- if (intersections?.length === 0 || !intersections) {
199
- return stateChanges;
207
+ if (points.length >= 3 && points[0].isSame(points.at(-1))) {
208
+ const center = points[0].bulgeCentrePoint(points[1]);
209
+ const radius = center.distance(points[0]);
210
+ this.points = [center, center.project(0, radius)];
211
+ return this;
200
212
  }
201
-
202
- const direction = 1; // Circle is always CCW for trimming purposes
203
-
204
- // get the mouse position
205
- const mousePosition = DesignCore.Mouse.pointOnScene();
206
- // get the point on the arc closest to the mouse
207
- const pointOnCircle = mousePosition.closestPointOnArc(this.points[1], this.points[1], this.points[0]);
208
- // sort intersection points
209
- Utils.sortPointsOnArc(intersections, this.points[0], this.points[0], this.points[0], direction);
210
- // Repeat the first point to close the circle
211
- const testPoints = [...intersections, intersections.at(0)]; // Closing the circle
212
-
213
- // Test if mouse position is between two intersection points
214
- if (testPoints.length > 1) {
215
- for (let i = 0; i < testPoints.length - 1; i++) {
216
- const startPoint = testPoints[i];
217
- const endPoint = testPoints[i + 1];
218
-
219
- // check if the mouse is between startPoint and endPoint
220
- if (pointOnCircle.isOnArc(startPoint, endPoint, this.points[0], direction)) {
221
- // create a new arc entity
222
- const arc = DesignCore.CommandManager.createNew('Arc', this);
223
- arc.handle = undefined;
224
- // flip direction
225
- arc.direction = direction * -1;
226
- arc.points = [this.points[0], startPoint, endPoint];
227
- const addState = new AddState(arc);
228
- stateChanges.push(addState);
229
-
230
- // Remove the existing arc
231
- if (stateChanges.length > 0) {
232
- const removeState = new RemoveState(this);
233
- stateChanges.push(removeState);
234
- return stateChanges;
235
- }
236
- }
237
- }
238
- }
239
-
240
- return stateChanges;
213
+ const arc = DesignCore.CommandManager.createNew('Arc', this);
214
+ arc.setProperty('handle', undefined);
215
+ arc.fromPolylinePoints(points);
216
+ return arc;
241
217
  }
242
218
 
243
219
  /**
@@ -251,13 +227,14 @@ export class Circle extends Entity {
251
227
 
252
228
  const centre = this.points[0]; // circle centre point
253
229
 
230
+ const radius = this.getProperty(Property.Names.RADIUS);
254
231
  snaps.push(new SnapPoint(centre, SnapPoint.Type.CENTRE));
255
232
 
256
233
  snaps.push(
257
- new SnapPoint(new Point(centre.x + this.radius, centre.y), SnapPoint.Type.QUADRANT), // 0°
258
- new SnapPoint(new Point(centre.x, centre.y + this.radius), SnapPoint.Type.QUADRANT), // 90°
259
- new SnapPoint(new Point(centre.x - this.radius, centre.y), SnapPoint.Type.QUADRANT), // 180°
260
- new SnapPoint(new Point(centre.x, centre.y - this.radius), SnapPoint.Type.QUADRANT), // 270°
234
+ new SnapPoint(new Point(centre.x + radius, centre.y), SnapPoint.Type.QUADRANT), // 0°
235
+ new SnapPoint(new Point(centre.x, centre.y + radius), SnapPoint.Type.QUADRANT), // 90°
236
+ new SnapPoint(new Point(centre.x - radius, centre.y), SnapPoint.Type.QUADRANT), // 180°
237
+ new SnapPoint(new Point(centre.x, centre.y - radius), SnapPoint.Type.QUADRANT), // 270°
261
238
  );
262
239
 
263
240
  if (mousePoint) {
@@ -274,16 +251,16 @@ export class Circle extends Entity {
274
251
  const angleFromCentreToInput = centre.angle(fromPoint); // angle from centre toward fromPoint
275
252
 
276
253
  // Tangent: only possible when fromPoint is outside the circle radius
277
- if (distanceToCenter > this.radius) {
278
- const tangentHalfAngle = Math.acos(this.radius / distanceToCenter); // half-angle between the two tangent directions
254
+ if (distanceToCenter > radius) {
255
+ const tangentHalfAngle = Math.acos(radius / distanceToCenter); // half-angle between the two tangent directions
279
256
 
280
- snaps.push(new SnapPoint(centre.project(angleFromCentreToInput + tangentHalfAngle, this.radius), SnapPoint.Type.TANGENT));
281
- snaps.push(new SnapPoint(centre.project(angleFromCentreToInput - tangentHalfAngle, this.radius), SnapPoint.Type.TANGENT));
257
+ snaps.push(new SnapPoint(centre.project(angleFromCentreToInput + tangentHalfAngle, radius), SnapPoint.Type.TANGENT));
258
+ snaps.push(new SnapPoint(centre.project(angleFromCentreToInput - tangentHalfAngle, radius), SnapPoint.Type.TANGENT));
282
259
  }
283
260
 
284
261
  // Perpendicular: point on the circle that lies on the line from fromPoint through the centre
285
262
  if (distanceToCenter > 0) {
286
- snaps.push(new SnapPoint(centre.project(angleFromCentreToInput, this.radius), SnapPoint.Type.PERPENDICULAR));
263
+ snaps.push(new SnapPoint(centre.project(angleFromCentreToInput, radius), SnapPoint.Type.PERPENDICULAR));
287
264
  }
288
265
  }
289
266
 
@@ -308,10 +285,11 @@ export class Circle extends Entity {
308
285
  * @return {BoundingBox}
309
286
  */
310
287
  boundingBox() {
311
- const xmin = this.points[0].x - this.radius;
312
- const xmax = this.points[0].x + this.radius;
313
- const ymin = this.points[0].y - this.radius;
314
- const ymax = this.points[0].y + this.radius;
288
+ const radius = this.getProperty(Property.Names.RADIUS);
289
+ const xmin = this.points[0].x - radius;
290
+ const xmax = this.points[0].x + radius;
291
+ const ymin = this.points[0].y - radius;
292
+ const ymax = this.points[0].y + radius;
315
293
 
316
294
  const topLeft = new Point(xmin, ymax);
317
295
  const bottomRight = new Point(xmax, ymin);