@design-core/core 0.0.6 → 50.1.0

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 (173) hide show
  1. package/.eslintrc.json +4 -0
  2. package/.github/workflows/DesignCoreQA.yml +3 -0
  3. package/TODO.md +49 -6
  4. package/core/core/core.js +0 -3
  5. package/core/designCore.js +1 -1
  6. package/core/dimensions/alignedDimension.js +12 -11
  7. package/core/dimensions/angularDimension.js +38 -37
  8. package/core/dimensions/{baseCircularDimension.js → circularDimensionBase.js} +22 -21
  9. package/core/dimensions/diametricDimension.js +14 -13
  10. package/core/dimensions/dimension.js +62 -55
  11. package/core/dimensions/{baseDimension.js → dimensionBase.js} +73 -107
  12. package/core/dimensions/{baseLinearDimension.js → linearDimensionBase.js} +15 -14
  13. package/core/dimensions/radialDimension.js +12 -11
  14. package/core/dimensions/rotatedDimension.js +20 -19
  15. package/core/entities/arc.js +123 -156
  16. package/core/entities/arctext.js +222 -184
  17. package/core/entities/circle.js +77 -99
  18. package/core/entities/entity.js +114 -93
  19. package/core/entities/hatch.js +346 -273
  20. package/core/entities/insert.js +68 -83
  21. package/core/entities/line.js +21 -142
  22. package/core/entities/lwpolyline.js +16 -11
  23. package/core/entities/point.js +19 -19
  24. package/core/entities/polyline.js +7 -7
  25. package/core/entities/{basePolyline.js → polylineBase.js} +47 -299
  26. package/core/entities/rectangle.js +3 -1
  27. package/core/entities/solid.js +6 -15
  28. package/core/entities/text.js +190 -201
  29. package/core/lib/auxiliary/rubberBand.js +32 -0
  30. package/core/lib/auxiliary/selectionWindow.js +63 -0
  31. package/core/lib/auxiliary/snapPoint.js +113 -0
  32. package/core/lib/auxiliary/trackingLine.js +76 -0
  33. package/core/lib/boundingBox.js +11 -11
  34. package/core/lib/canvas.js +232 -168
  35. package/core/lib/clipboard.js +9 -3
  36. package/core/lib/colour.js +16 -0
  37. package/core/lib/commandLine.js +1 -6
  38. package/core/lib/commandManager.js +18 -8
  39. package/core/lib/constants.js +28 -0
  40. package/core/lib/dxf/dxf.js +15 -14
  41. package/core/lib/dxf/dxfIterator.js +2 -2
  42. package/core/lib/entityManager.js +33 -26
  43. package/core/lib/inputManager.js +36 -12
  44. package/core/lib/intersect.js +25 -0
  45. package/core/lib/matrix.js +1 -1
  46. package/core/lib/patterns.js +9 -2
  47. package/core/lib/plotOptions.js +67 -0
  48. package/core/lib/polylineUtils.js +75 -0
  49. package/core/lib/renderers/cairoRenderer.js +371 -0
  50. package/core/lib/renderers/canvasRenderer.js +315 -0
  51. package/core/lib/renderers/pdfRenderer.js +366 -0
  52. package/core/lib/renderers/rendererBase.js +284 -0
  53. package/core/lib/renderers/svgRenderer.js +343 -0
  54. package/core/lib/scene.js +28 -6
  55. package/core/lib/selectionManager.js +20 -20
  56. package/core/lib/settings.js +4 -2
  57. package/core/lib/snapping.js +4 -191
  58. package/core/lib/stateManager.js +30 -1
  59. package/core/lib/utils.js +26 -7
  60. package/core/properties/entityProperties.js +104 -0
  61. package/core/properties/property.js +106 -2
  62. package/core/properties/propertyManager.js +60 -44
  63. package/core/tables/block.js +50 -81
  64. package/core/tables/dimStyle.js +5 -5
  65. package/core/tables/endblk.js +1 -1
  66. package/core/tables/layer.js +8 -0
  67. package/core/tables/ltypeManager.js +1 -1
  68. package/core/tables/styleManagerBase.js +3 -2
  69. package/core/tables/tableManagerBase.js +8 -3
  70. package/core/tables/vport.js +2 -2
  71. package/core/tools/chamfer.js +117 -143
  72. package/core/tools/chamferFilletBase.js +138 -37
  73. package/core/tools/copy.js +11 -11
  74. package/core/tools/copybase.js +3 -3
  75. package/core/tools/copyclip.js +3 -3
  76. package/core/tools/cornerEntity.js +2 -2
  77. package/core/tools/cutclip.js +3 -3
  78. package/core/tools/explode.js +7 -7
  79. package/core/tools/extend.js +138 -25
  80. package/core/tools/fillet.js +92 -189
  81. package/core/tools/matchProp.js +3 -12
  82. package/core/tools/mirror.js +15 -14
  83. package/core/tools/move.js +8 -8
  84. package/core/tools/offset.js +31 -33
  85. package/core/tools/pasteclip.js +1 -1
  86. package/core/tools/purge.js +10 -10
  87. package/core/tools/rotate.js +19 -16
  88. package/core/tools/scale.js +7 -7
  89. package/core/tools/trim.js +271 -29
  90. package/core/tools/zoom.js +1 -1
  91. package/cspell.json +321 -0
  92. package/jest.config.js +6 -0
  93. package/package.json +5 -3
  94. package/test/dimensions/alignedDimension.test.js +17 -10
  95. package/test/dimensions/angularDimension.test.js +13 -13
  96. package/test/dimensions/diametricDimension.test.js +15 -15
  97. package/test/dimensions/dimension.test.js +12 -12
  98. package/test/dimensions/{baseDimension.test.js → dimensionBase.test.js} +73 -12
  99. package/test/dimensions/linearDimensionBase.test.js +62 -0
  100. package/test/dimensions/radialDimension.test.js +15 -15
  101. package/test/dimensions/rotatedDimension.test.js +18 -11
  102. package/test/entities/arc.test.js +83 -86
  103. package/test/entities/arctext.test.js +184 -57
  104. package/test/entities/circle.test.js +68 -96
  105. package/test/entities/entity.test.js +42 -8
  106. package/test/entities/hatch.test.js +569 -96
  107. package/test/entities/insert.test.js +99 -4
  108. package/test/entities/line.test.js +15 -41
  109. package/test/entities/lwpolyline.test.js +104 -0
  110. package/test/entities/point.test.js +3 -3
  111. package/test/entities/polyline.test.js +11 -0
  112. package/test/entities/{basePolyline.test.js → polylineBase.test.js} +192 -57
  113. package/test/entities/rectangle.test.js +11 -0
  114. package/test/entities/solid.test.js +1 -1
  115. package/test/entities/text.test.js +302 -77
  116. package/test/integration/exportIntegration.reference.dxf +9 -9
  117. package/test/integration/exportIntegration.test.js +5 -5
  118. package/test/integration/intersectionIntegration.test.js +4 -5
  119. package/test/integration/pdfRendererIntegration.reference.pdf +17709 -0
  120. package/test/integration/pdfRendererIntegration.test.js +61 -0
  121. package/test/integration/svgRendererIntegration.reference.svg +3343 -0
  122. package/test/integration/svgRendererIntegration.test.js +61 -0
  123. package/test/integration/userInputIntegration.reference.dxf +1165 -105
  124. package/test/integration/userInputIntegration.test.js +239 -132
  125. package/test/lib/boundingBox.test.js +3 -3
  126. package/test/lib/canvas.test.js +744 -125
  127. package/test/lib/constants.test.js +27 -0
  128. package/test/lib/entityManager.test.js +4 -4
  129. package/test/lib/inputManager.test.js +81 -20
  130. package/test/lib/intersect.test.js +83 -0
  131. package/test/lib/plotOptions.test.js +179 -0
  132. package/test/lib/rubberBand.test.js +84 -0
  133. package/test/lib/scene.test.js +9 -9
  134. package/test/lib/selectionManager.test.js +3 -3
  135. package/test/lib/settings.test.js +5 -6
  136. package/test/lib/snapping.test.js +72 -130
  137. package/test/lib/stateManager.test.js +7 -7
  138. package/test/lib/utils.test.js +36 -2
  139. package/test/properties/entityProperties.test.js +132 -0
  140. package/test/properties/property.test.js +53 -0
  141. package/test/properties/propertyManager.test.js +25 -25
  142. package/test/renderers/canvasRenderer.test.js +317 -0
  143. package/test/renderers/pdfRenderer.test.js +271 -0
  144. package/test/renderers/rendererBase.test.js +213 -0
  145. package/test/renderers/svgRenderer.test.js +326 -0
  146. package/test/tables/block.test.js +92 -13
  147. package/test/tables/endblk.test.js +2 -2
  148. package/test/tables/layer.test.js +30 -0
  149. package/test/tables/layerManager.test.js +2 -2
  150. package/test/test-helpers/hatchGridRenderer.js +180 -0
  151. package/test/test-helpers/test-helpers.js +158 -6
  152. package/test/tools/chamfer.test.js +273 -51
  153. package/test/tools/chamferFilletBase.test.js +125 -36
  154. package/test/tools/copy.test.js +8 -8
  155. package/test/tools/copybase.test.js +2 -2
  156. package/test/tools/copyclip.test.js +2 -2
  157. package/test/tools/cornerEntity.test.js +8 -8
  158. package/test/tools/cutclip.test.js +2 -2
  159. package/test/tools/erase.test.js +6 -6
  160. package/test/tools/explode.test.js +5 -5
  161. package/test/tools/extend.test.js +373 -42
  162. package/test/tools/fillet.test.js +230 -49
  163. package/test/tools/matchProp.test.js +11 -11
  164. package/test/tools/mirror.test.js +39 -39
  165. package/test/tools/move.test.js +7 -7
  166. package/test/tools/offset.test.js +222 -55
  167. package/test/tools/pasteclip.test.js +5 -5
  168. package/test/tools/purge.test.js +4 -4
  169. package/test/tools/rotate.test.js +39 -9
  170. package/test/tools/scale.test.js +33 -33
  171. package/test/tools/trim.test.js +579 -53
  172. package/test/tools/zoom.test.js +12 -12
  173. package/core/lib/selectionWindow.js +0 -83
@@ -1,4 +1,4 @@
1
- import { BasePolyline } from '../../core/entities/basePolyline.js';
1
+ import { PolylineBase } from '../../core/entities/polylineBase.js';
2
2
  import { Point } from '../../core/entities/point';
3
3
  import { Polyline } from '../../core/entities/polyline.js';
4
4
  import { Line } from '../../core/entities/line.js';
@@ -30,7 +30,7 @@ test.each(inputScenarios)('Polyline.execute handles $desc', async (scenario) =>
30
30
  const { inputs, expectedPoints } = scenario;
31
31
 
32
32
  await withMockInput(DesignCore.Scene, inputs, async () => {
33
- const polyline = new BasePolyline({});
33
+ const polyline = new PolylineBase({});
34
34
  await polyline.execute();
35
35
 
36
36
  expect(polyline.points.length).toBe(expectedPoints);
@@ -46,11 +46,23 @@ test.each(inputScenarios)('Polyline.execute handles $desc', async (scenario) =>
46
46
  }, { extraMethods: { actionCommand: () => {} } });
47
47
  });
48
48
 
49
- test('Test BasePolyline.getClosestSegment', () => {
49
+ test('Test PolylineBase.draw calls renderer.drawShape with toPolylinePoints', () => {
50
+ const polyline = new Polyline({ points: [new Point(0, 0), new Point(10, 0), new Point(10, 5)] });
51
+ let capturedPoints;
52
+ const mockRenderer = {
53
+ drawShape(points) {
54
+ capturedPoints = points;
55
+ },
56
+ };
57
+ polyline.draw(mockRenderer);
58
+ expect(capturedPoints).toEqual(polyline.toPolylinePoints());
59
+ });
60
+
61
+ test('Test PolylineBase.getClosestSegment', () => {
50
62
  // Polyline with a line and an arc segment
51
63
  const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10)];
52
64
  points[1].bulge = 1; // Arc from (10,0) to (10,10)
53
- const polyline = new BasePolyline({ points });
65
+ const polyline = new PolylineBase({ points });
54
66
 
55
67
  // Closest to the line segment
56
68
  const testPoint1 = new Point(5, 2);
@@ -65,11 +77,11 @@ test('Test BasePolyline.getClosestSegment', () => {
65
77
 
66
78
  // ─── closing segment (closed polylines) ──────────────────────────────────────
67
79
 
68
- test('BasePolyline.getClosestSegmentIndex returns closing-segment index for closed polyline', () => {
80
+ test('PolylineBase.getClosestSegmentIndex returns closing-segment index for closed polyline', () => {
69
81
  // Square: P0=(0,0) P1=(10,0) P2=(10,10) P3=(0,10), closed flag set.
70
82
  // Closing segment is P3→P0. A point near the left edge (0,5) is closest to that segment.
71
83
  const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
72
- const poly = new BasePolyline({ points });
84
+ const poly = new PolylineBase({ points });
73
85
  poly.flags.setFlagValue(1);
74
86
 
75
87
  const closestIdx = poly.getClosestSegmentIndex(new Point(0, 5));
@@ -77,9 +89,9 @@ test('BasePolyline.getClosestSegmentIndex returns closing-segment index for clos
77
89
  expect(closestIdx).toBe(4);
78
90
  });
79
91
 
80
- test('BasePolyline.getClosestSegment returns closing segment as a Line for closed polyline', () => {
92
+ test('PolylineBase.getClosestSegment returns closing segment as a Line for closed polyline', () => {
81
93
  const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
82
- const poly = new BasePolyline({ points });
94
+ const poly = new PolylineBase({ points });
83
95
  poly.flags.setFlagValue(1);
84
96
 
85
97
  const seg = poly.getClosestSegment(new Point(0, 5));
@@ -91,21 +103,21 @@ test('BasePolyline.getClosestSegment returns closing segment as a Line for close
91
103
  expect(seg.points[1].y).toBeCloseTo(0);
92
104
  });
93
105
 
94
- test('BasePolyline.areConsecutiveSegments returns false for non-adjacent segments on a closed polyline', () => {
106
+ test('PolylineBase.areConsecutiveSegments returns false for non-adjacent segments on a closed polyline', () => {
95
107
  // 4-point closed poly: segments 1 and 3 are NOT adjacent (bug: old code returned true).
96
108
  const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
97
- const poly = new BasePolyline({ points });
109
+ const poly = new PolylineBase({ points });
98
110
  poly.flags.setFlagValue(1);
99
111
 
100
112
  expect(poly.areConsecutiveSegments(1, 3)).toBe(false);
101
113
  });
102
114
 
103
- test('BasePolyline.areConsecutiveSegments returns true for closing segment and its neighbours', () => {
115
+ test('PolylineBase.areConsecutiveSegments returns true for closing segment and its neighbours', () => {
104
116
  // 4-point closed poly, N=4 (closing segment index).
105
117
  // Seg N-1=3 ends at P3, closing seg starts at P3 → consecutive (diff=1).
106
118
  // Closing seg ends at P0, seg 1 starts at P0 → consecutive (wrap case).
107
119
  const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
108
- const poly = new BasePolyline({ points });
120
+ const poly = new PolylineBase({ points });
109
121
  poly.flags.setFlagValue(1);
110
122
 
111
123
  expect(poly.areConsecutiveSegments(3, 4)).toBe(true); // diff = 1
@@ -113,11 +125,11 @@ test('BasePolyline.areConsecutiveSegments returns true for closing segment and i
113
125
  expect(poly.areConsecutiveSegments(1, 4)).toBe(true); // wrap (order reversed)
114
126
  });
115
127
 
116
- test('BasePolyline.closestPointOnSegment handles closing-segment index correctly', () => {
128
+ test('PolylineBase.closestPointOnSegment handles closing-segment index correctly', () => {
117
129
  // 4-point square, closing segment = P3=(0,10)→P0=(0,0). Index = 4.
118
130
  // Closest point on that segment to (0, 5) should be (0, 5).
119
131
  const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
120
- const poly = new BasePolyline({ points });
132
+ const poly = new PolylineBase({ points });
121
133
  poly.flags.setFlagValue(1);
122
134
 
123
135
  const closest = poly.closestPointOnSegment(new Point(0, 5), 4);
@@ -126,9 +138,9 @@ test('BasePolyline.closestPointOnSegment handles closing-segment index correctly
126
138
  });
127
139
 
128
140
 
129
- test('Test BasePolyline.closestPoint', () => {
141
+ test('Test PolylineBase.closestPoint', () => {
130
142
  const points = [new Point(100, 100), new Point(200, 100, -1), new Point(200, 50)];
131
- const polyline = new BasePolyline({ points: points });
143
+ const polyline = new PolylineBase({ points: points });
132
144
  // line segment
133
145
  const point1 = new Point(150, 85);
134
146
  const closest1 = polyline.closestPoint(point1);
@@ -144,14 +156,14 @@ test('Test BasePolyline.closestPoint', () => {
144
156
  expect(closest2[1]).toBeCloseTo(10.857);
145
157
  });
146
158
 
147
- test('Test BasePolyline.boundingBox', () => {
148
- let polyline = new BasePolyline({ points: [new Point(101, 102), new Point(201, 202)] });
159
+ test('Test PolylineBase.boundingBox', () => {
160
+ let polyline = new PolylineBase({ points: [new Point(101, 102), new Point(201, 202)] });
149
161
  expect(polyline.boundingBox().xMin).toBeCloseTo(101);
150
162
  expect(polyline.boundingBox().xMax).toBeCloseTo(201);
151
163
  expect(polyline.boundingBox().yMin).toBeCloseTo(102);
152
164
  expect(polyline.boundingBox().yMax).toBeCloseTo(202);
153
165
 
154
- polyline = new BasePolyline({ points: [new Point(101, 102), new Point(-201, 202)] });
166
+ polyline = new PolylineBase({ points: [new Point(101, 102), new Point(-201, 202)] });
155
167
  expect(polyline.boundingBox().xMin).toBeCloseTo(-201);
156
168
  expect(polyline.boundingBox().xMax).toBeCloseTo(101);
157
169
  expect(polyline.boundingBox().yMin).toBeCloseTo(102);
@@ -161,7 +173,7 @@ test('Test BasePolyline.boundingBox', () => {
161
173
  let points = [new Point(101, 102), new Point(200, 102), new Point(200, 0)];
162
174
  // set the bulge
163
175
  points[1].bulge = -1;
164
- polyline = new BasePolyline({ points: points });
176
+ polyline = new PolylineBase({ points: points });
165
177
  expect(polyline.boundingBox().xMin).toBeCloseTo(101);
166
178
  expect(polyline.boundingBox().xMax).toBeCloseTo(251);
167
179
  expect(polyline.boundingBox().yMin).toBeCloseTo(0);
@@ -170,19 +182,19 @@ test('Test BasePolyline.boundingBox', () => {
170
182
  points = [new Point(101, 102), new Point(200, 102), new Point(200, 0)];
171
183
  // set the bulge
172
184
  points[1].bulge = -1;
173
- polyline = new BasePolyline({ points: points });
185
+ polyline = new PolylineBase({ points: points });
174
186
  expect(polyline.boundingBox().xMin).toBeCloseTo(101);
175
187
  expect(polyline.boundingBox().xMax).toBeCloseTo(251);
176
188
  expect(polyline.boundingBox().yMin).toBeCloseTo(0);
177
189
  expect(polyline.boundingBox().yMax).toBeCloseTo(102);
178
190
  });
179
191
 
180
- test('Test BasePolyline.getBulgeFromSegment', () => {
192
+ test('Test PolylineBase.getBulgeFromSegment', () => {
181
193
  // start point: 100,0
182
- // center: 100,50
194
+ // centre: 100,50
183
195
  // radius: 50
184
196
  const points = [new Point(), new Point(100, 0)];
185
- const polyline = new BasePolyline({ points: points });
197
+ const polyline = new PolylineBase({ points: points });
186
198
 
187
199
  // zero delta angle:
188
200
  // bulge: 0
@@ -322,7 +334,7 @@ AcDbPolyline
322
334
  6
323
335
  ByLayer
324
336
  39
325
- 2
337
+ 1
326
338
  90
327
339
  3
328
340
  70
@@ -350,7 +362,7 @@ ByLayer
350
362
  expect(file.contents).toEqual(dxfString);
351
363
 
352
364
  // create new entity from entity data to ensure all props are loaded
353
- const newPolyline = new Polyline(polyline);
365
+ const newPolyline = new Polyline({ handle: polyline.getProperty('handle'), points: polyline.points });
354
366
  file = new File();
355
367
  newPolyline.dxf(file);
356
368
 
@@ -419,10 +431,10 @@ SEQEND
419
431
  expect(file.contents).toEqual(dxfString);
420
432
  });
421
433
 
422
- test('Test BasePolyline.toPolylinePoints', () => {
434
+ test('Test PolylineBase.toPolylinePoints', () => {
423
435
  const points = [new Point(100, 100), new Point(200, 100), new Point(200, 50)];
424
436
  points[1].bulge = -1;
425
- const polyline = new BasePolyline({ points: points });
437
+ const polyline = new PolylineBase({ points: points });
426
438
 
427
439
  const polylinePoints = polyline.toPolylinePoints();
428
440
  expect(polylinePoints[0].x).toBe(100);
@@ -438,10 +450,10 @@ test('Test BasePolyline.toPolylinePoints', () => {
438
450
  expect(polylinePoints[2].bulge).toBe(0);
439
451
  });
440
452
 
441
- test('Test BasePolyline.toPolylinePoints returns defensive copy for open polyline', () => {
453
+ test('Test PolylineBase.toPolylinePoints returns defensive copy for open polyline', () => {
442
454
  const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10)];
443
455
  points[1].bulge = -1;
444
- const polyline = new BasePolyline({ points: points });
456
+ const polyline = new PolylineBase({ points: points });
445
457
 
446
458
  const result = polyline.toPolylinePoints();
447
459
  // Mutate the returned array and one of the returned points
@@ -454,13 +466,13 @@ test('Test BasePolyline.toPolylinePoints returns defensive copy for open polylin
454
466
  expect(polyline.points[1].bulge).toBe(-1);
455
467
  });
456
468
 
457
- test('Test BasePolyline.toPolylinePoints closed polyline appends closure point', () => {
469
+ test('Test PolylineBase.toPolylinePoints closed polyline appends closure point', () => {
458
470
  // A triangle closed via the Close command (flag bit 1 set, no duplicate
459
471
  // point stored in this.points).
460
472
  const points = [new Point(0, 0), new Point(10, 0), new Point(5, 10)];
461
473
  const flags = new Flags();
462
474
  flags.addValue(1); // closed flag
463
- const polyline = new BasePolyline({ points: points, flags: flags });
475
+ const polyline = new PolylineBase({ points: points, flags: flags });
464
476
 
465
477
  const polylinePoints = polyline.toPolylinePoints();
466
478
  // Should have 4 points: the 3 vertices plus the closure point
@@ -494,7 +506,7 @@ test('Polyline.execute handles Close option', async () => {
494
506
  test('Polyline.execute supports Undo in line mode', async () => {
495
507
  const inputs = [new Point(0, 0), new Point(10, 0), new Point(20, 0), 'Undo', new Point(10, 10), 'Close'];
496
508
  await withMockInput(DesignCore.Scene, inputs, async () => {
497
- const polyline = new BasePolyline({});
509
+ const polyline = new PolylineBase({});
498
510
  await polyline.execute();
499
511
  expect(polyline.points.length).toBe(3);
500
512
  expect(polyline.points[0]).toEqual(inputs[0]);
@@ -507,7 +519,7 @@ test('Polyline.execute supports Undo in line mode', async () => {
507
519
  test('Polyline.execute supports Undo in arc mode', async () => {
508
520
  const inputs = [new Point(0, 0), new Point(10, 0), 'Arc', new Point(10, 10), new Point(20, 20), 'Undo', 'Line', new Point(10, 20), 'Close'];
509
521
  await withMockInput(DesignCore.Scene, inputs, async () => {
510
- const polyline = new BasePolyline({});
522
+ const polyline = new PolylineBase({});
511
523
  await polyline.execute();
512
524
  expect(polyline.points.length).toBe(4);
513
525
  expect(polyline.points[0]).toEqual(inputs[0]);
@@ -525,7 +537,7 @@ test('Polyline.execute supports Undo in arc mode', async () => {
525
537
  test('Polyline.execute supports Arc/Line mode switching', async () => {
526
538
  const inputs = [new Point(0, 0), new Point(10, 0), 'Arc', new Point(10, 10), 'Line', new Point(20, 10), 'Close'];
527
539
  await withMockInput(DesignCore.Scene, inputs, async () => {
528
- const polyline = new BasePolyline({});
540
+ const polyline = new PolylineBase({});
529
541
  await polyline.execute();
530
542
  expect(polyline.points.length).toBe(4);
531
543
  expect(polyline.points[0]).toEqual(inputs[0]);
@@ -541,29 +553,29 @@ test('Polyline.execute supports Arc/Line mode switching', async () => {
541
553
 
542
554
  // ─── Auto-close when first and last point are the same ───────────────────────
543
555
 
544
- test('BasePolyline constructor auto-closes when last point matches first', () => {
556
+ test('PolylineBase constructor auto-closes when last point matches first', () => {
545
557
  // Simulate a polyline loaded with a redundant closing point (e.g. from DXF data)
546
558
  const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 0)];
547
- const polyline = new BasePolyline({ points });
559
+ const polyline = new PolylineBase({ points });
548
560
 
549
561
  // Duplicate point should be removed and the closed flag set
550
562
  expect(polyline.points.length).toBe(3);
551
563
  expect(polyline.flags.hasFlag(1)).toBe(true);
552
564
  });
553
565
 
554
- test('BasePolyline constructor does not auto-close when fewer than 3 unique points remain after pop', () => {
566
+ test('PolylineBase constructor does not auto-close when fewer than 3 unique points remain after pop', () => {
555
567
  // [A, B, A] — only 2 unique points after removing the duplicate; must not auto-close
556
568
  const points = [new Point(0, 0), new Point(10, 0), new Point(0, 0)];
557
- const polyline = new BasePolyline({ points });
569
+ const polyline = new PolylineBase({ points });
558
570
 
559
571
  expect(polyline.points.length).toBe(3);
560
572
  expect(polyline.flags.hasFlag(1)).toBe(false);
561
573
  });
562
574
 
563
- test('BasePolyline constructor does not auto-close with only 2 identical points', () => {
575
+ test('PolylineBase constructor does not auto-close with only 2 identical points', () => {
564
576
  // Only 2 unique points — not enough to form a closed polygon
565
577
  const points = [new Point(0, 0), new Point(0, 0)];
566
- const polyline = new BasePolyline({ points });
578
+ const polyline = new PolylineBase({ points });
567
579
 
568
580
  expect(polyline.points.length).toBe(2);
569
581
  expect(polyline.flags.hasFlag(1)).toBe(false);
@@ -581,14 +593,14 @@ test('Polyline.execute auto-closes when start point is re-entered', async () =>
581
593
  // The constructor will have auto-closed the entity when pt4 == pt1
582
594
  if (entity.points.length === 4 && entity.points.at(-1).isSame(pt1)) {
583
595
  // simulate what the constructor does: a new entity created from this data would auto-close
584
- const rebuilt = new BasePolyline({ points: [...entity.points] });
596
+ const rebuilt = new PolylineBase({ points: [...entity.points] });
585
597
  if (rebuilt.flags.hasFlag(1)) autoClosedCount++;
586
598
  }
587
599
  return 0;
588
600
  };
589
601
 
590
602
  await withMockInput(DesignCore.Scene, [pt1, pt2, pt3, pt4], async () => {
591
- const polyline = new BasePolyline({});
603
+ const polyline = new PolylineBase({});
592
604
  await polyline.execute();
593
605
  // execute does not exit early — the duplicate point is on the instance
594
606
  expect(polyline.points.length).toBe(4);
@@ -597,10 +609,10 @@ test('Polyline.execute auto-closes when start point is re-entered', async () =>
597
609
  }, { extraMethods: { actionCommand: mockActionCommand } });
598
610
  });
599
611
 
600
- test('BasePolyline.fromPolylinePoints round-trip preserves open polyline', () => {
601
- const poly = new BasePolyline({ points: [new Point(0, 0, 0.5), new Point(10, 0), new Point(10, 10)] });
612
+ test('PolylineBase.fromPolylinePoints round-trip preserves open polyline', () => {
613
+ const poly = new PolylineBase({ points: [new Point(0, 0, 0.5), new Point(10, 0), new Point(10, 10)] });
602
614
  const polyPts = poly.toPolylinePoints();
603
- const rebuilt = new BasePolyline({});
615
+ const rebuilt = new PolylineBase({});
604
616
  rebuilt.fromPolylinePoints(polyPts);
605
617
  expect(rebuilt.points.length).toBe(3);
606
618
  expect(rebuilt.points[0].x).toBe(0);
@@ -610,16 +622,16 @@ test('BasePolyline.fromPolylinePoints round-trip preserves open polyline', () =>
610
622
  expect(rebuilt.points[2].y).toBe(10);
611
623
  });
612
624
 
613
- test('BasePolyline.fromPolylinePoints round-trip strips closure point for closed polyline', () => {
625
+ test('PolylineBase.fromPolylinePoints round-trip strips closure point for closed polyline', () => {
614
626
  const flags = new Flags();
615
627
  flags.addValue(1);
616
- const poly = new BasePolyline({ points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)], flags: flags });
628
+ const poly = new PolylineBase({ points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)], flags: flags });
617
629
  const polyPts = poly.toPolylinePoints();
618
630
  // toPolylinePoints appends a closure copy of the first point
619
631
  expect(polyPts.length).toBe(5);
620
632
  const flags2 = new Flags();
621
633
  flags2.addValue(1);
622
- const rebuilt = new BasePolyline({ flags: flags2 });
634
+ const rebuilt = new PolylineBase({ flags: flags2 });
623
635
  rebuilt.fromPolylinePoints(polyPts);
624
636
  expect(rebuilt.points.length).toBe(4);
625
637
  expect(rebuilt.points[0].x).toBe(0);
@@ -629,12 +641,12 @@ test('BasePolyline.fromPolylinePoints round-trip strips closure point for closed
629
641
 
630
642
  // ─── closestPoint: closed polyline closing segment ───────────────────────────
631
643
 
632
- test('BasePolyline.closestPoint finds closest point on closing segment of closed polyline', () => {
644
+ test('PolylineBase.closestPoint finds closest point on closing segment of closed polyline', () => {
633
645
  // Square: P0=(0,0), P1=(10,0), P2=(10,10), P3=(0,10), closed flag set.
634
646
  // Closing segment is P3=(0,10) → P0=(0,0).
635
647
  // A test point on the left edge (0, 5) must resolve to (0, 5) on the closing segment.
636
648
  const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
637
- const poly = new BasePolyline({ points });
649
+ const poly = new PolylineBase({ points });
638
650
  poly.flags.setFlagValue(1);
639
651
 
640
652
  const [closest, dist] = poly.closestPoint(new Point(0, 5));
@@ -643,11 +655,11 @@ test('BasePolyline.closestPoint finds closest point on closing segment of closed
643
655
  expect(dist).toBeCloseTo(0);
644
656
  });
645
657
 
646
- test('BasePolyline.closestPoint closing segment is preferred over other segments', () => {
658
+ test('PolylineBase.closestPoint closing segment is preferred over other segments', () => {
647
659
  // The test point (-1, 5) is clearly nearest the closing segment (left edge),
648
660
  // not the bottom, right, or top edges.
649
661
  const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
650
- const poly = new BasePolyline({ points });
662
+ const poly = new PolylineBase({ points });
651
663
  poly.flags.setFlagValue(1);
652
664
 
653
665
  const [closest, dist] = poly.closestPoint(new Point(-1, 5));
@@ -656,14 +668,14 @@ test('BasePolyline.closestPoint closing segment is preferred over other segments
656
668
  expect(dist).toBeCloseTo(1);
657
669
  });
658
670
 
659
- test('BasePolyline.closestPoint for open polyline does not include closing segment', () => {
671
+ test('PolylineBase.closestPoint for open polyline does not include closing segment', () => {
660
672
  // For point (-1, 1) on the open square:
661
673
  // – Nearest on seg P0→P1 (y=0): foot (-1,0) clips to (0,0), dist = √2 ≈ 1.41
662
674
  // – Nearest on seg P2→P3 (y=10): foot (-1,10) clips to (0,10), dist ≈ 9.06
663
675
  // – If the closing segment existed (x=0, y∈[0,10]): foot (0,1), dist = 1 ← closer!
664
676
  // An open polyline must NOT check the closing segment, so it returns (0,0).
665
677
  const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
666
- const poly = new BasePolyline({ points });
678
+ const poly = new PolylineBase({ points });
667
679
  // flag NOT set — open polyline
668
680
 
669
681
  const [closest] = poly.closestPoint(new Point(-1, 1));
@@ -671,11 +683,80 @@ test('BasePolyline.closestPoint for open polyline does not include closing segme
671
683
  expect(closest.y).toBeCloseTo(0);
672
684
  });
673
685
 
674
- test('BasePolyline.closestPoint is consistent with getClosestSegmentIndex for closed polyline', () => {
686
+ // ─── isPointOnSegment ────────────────────────────────────────────────────────
687
+
688
+ const lineSegmentCases = [
689
+ { desc: 'midpoint', point: new Point(5, 0), expected: true },
690
+ { desc: 'start endpoint', point: new Point(0, 0), expected: true },
691
+ { desc: 'end endpoint', point: new Point(10, 0), expected: true },
692
+ { desc: 'perpendicular off-line', point: new Point(5, 5), expected: false },
693
+ { desc: 'collinear beyond end', point: new Point(15, 0), expected: false },
694
+ ];
695
+
696
+ test.each(lineSegmentCases)('PolylineBase.isPointOnSegment line segment – $desc', ({ point, expected }) => {
697
+ const A = new Point(0, 0);
698
+ const B = new Point(10, 0);
699
+ const poly = new PolylineBase({ points: [A, B] });
700
+ expect(poly.isPointOnSegment(point, A, B)).toBe(expected);
701
+ });
702
+
703
+ // CCW 180° semicircle from (10,0) to (-10,0), bulge=1, centre=(0,0), radius=10.
704
+ // Arc spans angles [0°, 180°] — the upper semicircle.
705
+ const ccwArcCases = [
706
+ { desc: 'arc midpoint (0,10) at 90°', point: new Point(0, 10), expected: true },
707
+ { desc: 'start endpoint (10,0)', point: new Point(10, 0), expected: true },
708
+ { desc: 'end endpoint (-10,0)', point: new Point(-10, 0), expected: true },
709
+ { desc: 'lower semicircle (0,-10)', point: new Point(0, -10), expected: false },
710
+ ];
711
+
712
+ test.each(ccwArcCases)('PolylineBase.isPointOnSegment CCW arc (bulge=1, 180°) – $desc', ({ point, expected }) => {
713
+ const A = new Point(10, 0);
714
+ A.bulge = 1;
715
+ const B = new Point(-10, 0);
716
+ const poly = new PolylineBase({ points: [A, B] });
717
+ expect(poly.isPointOnSegment(point, A, B)).toBe(expected);
718
+ });
719
+
720
+ // CW 180° semicircle from (10,0) to (-10,0), bulge=-1, centre=(0,0), radius=10.
721
+ // Arc spans angles ≤0° or ≥180° — the lower semicircle.
722
+ const cwArcCases = [
723
+ { desc: 'arc midpoint (0,-10) at -90°', point: new Point(0, -10), expected: true },
724
+ { desc: 'start endpoint (10,0)', point: new Point(10, 0), expected: true },
725
+ { desc: 'end endpoint (-10,0)', point: new Point(-10, 0), expected: true },
726
+ { desc: 'upper semicircle (0,10)', point: new Point(0, 10), expected: false },
727
+ ];
728
+
729
+ test.each(cwArcCases)('PolylineBase.isPointOnSegment CW arc (bulge=-1, 180°) – $desc', ({ point, expected }) => {
730
+ const A = new Point(10, 0);
731
+ A.bulge = -1;
732
+ const B = new Point(-10, 0);
733
+ const poly = new PolylineBase({ points: [A, B] });
734
+ expect(poly.isPointOnSegment(point, A, B)).toBe(expected);
735
+ });
736
+
737
+ // CCW 180° arc from (0,10) to (0,-10), bulge=1, centre=(0,0), radius=10.
738
+ // startAngle=90° > endAngle=-90° so the check wraps: angles ≥90° OR ≤-90°.
739
+ // This covers the left semicircle (through (-10,0)).
740
+ const ccwArcWrapCases = [
741
+ { desc: 'left midpoint (-10,0) at 180°', point: new Point(-10, 0), expected: true },
742
+ { desc: 'start endpoint (0,10)', point: new Point(0, 10), expected: true },
743
+ { desc: 'end endpoint (0,-10)', point: new Point(0, -10), expected: true },
744
+ { desc: 'right side (10,0) at 0°', point: new Point(10, 0), expected: false },
745
+ ];
746
+
747
+ test.each(ccwArcWrapCases)('PolylineBase.isPointOnSegment CCW arc wrapping past ±180° (bulge=1, startAngle=90° > endAngle=-90°) – $desc', ({ point, expected }) => {
748
+ const A = new Point(0, 10);
749
+ A.bulge = 1;
750
+ const B = new Point(0, -10);
751
+ const poly = new PolylineBase({ points: [A, B] });
752
+ expect(poly.isPointOnSegment(point, A, B)).toBe(expected);
753
+ });
754
+
755
+ test('PolylineBase.closestPoint is consistent with getClosestSegmentIndex for closed polyline', () => {
675
756
  // For a closed square, both methods should agree that the closing segment
676
757
  // is nearest for a point on the left edge.
677
758
  const points = [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)];
678
- const poly = new BasePolyline({ points });
759
+ const poly = new PolylineBase({ points });
679
760
  poly.flags.setFlagValue(1);
680
761
 
681
762
  const testPoint = new Point(-2, 5);
@@ -687,3 +768,57 @@ test('BasePolyline.closestPoint is consistent with getClosestSegmentIndex for cl
687
768
  expect(closest.x).toBeCloseTo(0);
688
769
  expect(closest.y).toBeCloseTo(5);
689
770
  });
771
+
772
+ // ─── preview ─────────────────────────────────────────────────────────────────
773
+
774
+ test('PolylineBase.preview - 0 points does nothing', () => {
775
+ const poly = new PolylineBase({});
776
+ DesignCore.Scene.auxiliaryEntities.clear();
777
+ DesignCore.Scene.previewEntities.clear();
778
+
779
+ expect(() => poly.preview()).not.toThrow();
780
+ expect(DesignCore.Scene.auxiliaryEntities.count()).toBe(0);
781
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
782
+ });
783
+
784
+ test('PolylineBase.preview - LINE mode creates temp entity', () => {
785
+ const poly = new PolylineBase({});
786
+ poly.points.push(new Point(0, 0));
787
+
788
+ const origCreate = DesignCore.Scene.previewEntities.create;
789
+ const calls = [];
790
+ DesignCore.Scene.previewEntities.create = (type, data) => calls.push({ type, data });
791
+
792
+ DesignCore.Scene.auxiliaryEntities.clear();
793
+ poly.preview();
794
+
795
+ expect(calls.length).toBeGreaterThanOrEqual(1);
796
+ // LINE mode: no rubber-band auxiliary entity
797
+ expect(DesignCore.Scene.auxiliaryEntities.count()).toBe(0);
798
+
799
+ DesignCore.Scene.previewEntities.create = origCreate;
800
+ });
801
+
802
+ test('PolylineBase.preview - ARC mode adds rubber band and creates arc temp entity', () => {
803
+ const poly = new PolylineBase({});
804
+ poly.points.push(new Point(0, 0));
805
+ poly.points.push(new Point(10, 0));
806
+ poly.inputMode = poly.modes.ARC;
807
+
808
+ const origAdd = DesignCore.Scene.auxiliaryEntities.add;
809
+ const origCreate = DesignCore.Scene.previewEntities.create;
810
+ const auxAdded = [];
811
+ const tempCreated = [];
812
+ DesignCore.Scene.auxiliaryEntities.add = (item) => auxAdded.push(item);
813
+ DesignCore.Scene.previewEntities.create = (type, data) => tempCreated.push({ type, data });
814
+
815
+ poly.preview();
816
+
817
+ // ARC mode: rubber-band drawn in auxiliaryEntities
818
+ expect(auxAdded.length).toBeGreaterThanOrEqual(1);
819
+ // ARC mode: arc preview drawn in previewEntities
820
+ expect(tempCreated.length).toBeGreaterThanOrEqual(1);
821
+
822
+ DesignCore.Scene.auxiliaryEntities.add = origAdd;
823
+ DesignCore.Scene.previewEntities.create = origCreate;
824
+ });
@@ -0,0 +1,11 @@
1
+ import { Rectangle } from '../../core/entities/rectangle.js';
2
+ import { Core } from '../../core/core/core.js';
3
+
4
+ // initialise core
5
+ new Core();
6
+
7
+ // ─── register ────────────────────────────────────────────────────────────────
8
+
9
+ test('Rectangle.register returns correct command object', () => {
10
+ expect(Rectangle.register()).toEqual({ command: 'Rectangle', shortcut: 'REC', type: 'Entity' });
11
+ });
@@ -59,7 +59,7 @@ AcDbTrace
59
59
  expect(file.contents).toEqual(dxfString);
60
60
 
61
61
  // create new entity from entity data to ensure all props are loaded
62
- const newSolid = new Solid(solid);
62
+ const newSolid = new Solid({ handle: solid.getProperty('handle'), points: solid.points });
63
63
  file = new File();
64
64
  newSolid.dxf(file);
65
65
  expect(file.contents).toEqual(dxfString);