@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
@@ -16,28 +16,28 @@ const arcInputScenarios = [
16
16
  {
17
17
  desc: 'two points and an angle',
18
18
  input: [new SingleSelection(0, new Point(5, 0)), 2.5, 'Test Arc'],
19
- selectedItems: [new Arc({ points: [new Point(), new Point(100, 0), new Point(-100, 0)] })],
19
+ selectedEntities: [new Arc({ points: [new Point(), new Point(100, 0), new Point(-100, 0)] })],
20
20
  expectedTextHeight: 2.5,
21
21
  expectedText: 'Test Arc',
22
22
  },
23
23
  ];
24
24
 
25
25
  test.each(arcInputScenarios)('ArcText.execute handles $desc', async (scenario) => {
26
- const { input, selectedItems, expectedTextHeight, expectedText } = scenario;
26
+ const { input, selectedEntities, expectedTextHeight, expectedText } = scenario;
27
27
 
28
28
  await withMockInput(DesignCore.Scene, input, async () => {
29
29
  const arcText = new ArcAlignedText();
30
30
  await arcText.execute();
31
31
 
32
- expect(arcText.styleName).toBe('STANDARD');
33
- expect(arcText.height).toBe(expectedTextHeight);
34
- expect(arcText.string).toBe(expectedText);
32
+ expect(arcText.getProperty('styleName')).toBe('STANDARD');
33
+ expect(arcText.getProperty('height')).toBe(expectedTextHeight);
34
+ expect(arcText.getProperty('string')).toBe(expectedText);
35
35
 
36
36
  // arc props
37
- expect(arcText.radius).toBe(100);
37
+ expect(arcText.getProperty('radius')).toBe(100);
38
38
  expect(arcText.startAngle()).toBe(0);
39
39
  expect(arcText.endAngle()).toBeCloseTo(3.14159);
40
- }, { selectedItems });
40
+ }, { selectedEntities });
41
41
  });
42
42
 
43
43
  test('ArcAlignedCharacter', () => {
@@ -52,7 +52,7 @@ test('ArcAlignedCharacter', () => {
52
52
  expect(ac.position.y).toBe(point.y);
53
53
  expect(ac.angle).toBeCloseTo(angle);
54
54
  expect(ac.baseline.x).toBe(10.5);
55
- expect(ac.baseline.y).toBe(19.7);
55
+ expect(ac.baseline.y).toBe(19.5);
56
56
  expect(ac.boundingBox).toBeInstanceOf(BoundingBox);
57
57
  });
58
58
 
@@ -105,7 +105,7 @@ test('ArcText getArcAlignedCharacters', () => {
105
105
  // offsetFromLeft 0 - mm
106
106
  // textReversed 0 = forward, 1 = reversed
107
107
  // textOrientation 1 = outward, 2 = inward
108
- // textAlignment 1 = fit to arc, 2 = left align, 3 = right align, 4 = center
108
+ // textAlignment 1 = fit to arc, 2 = left align, 3 = right align, 4 = centre
109
109
  // ArcSide convex = 1, concave = 2
110
110
  let chars = [];
111
111
 
@@ -115,14 +115,14 @@ test('ArcText getArcAlignedCharacters', () => {
115
115
  expect(chars.length).toBe(4);
116
116
  // First character
117
117
  expect(chars[0].character).toBe('T');
118
- expect(chars[0].position.x).toBeCloseTo(-2.39222);
119
- expect(chars[0].position.y).toBeCloseTo(101.22173);
120
- expect(chars[0].angle).toBeCloseTo(0.02362);
118
+ expect(chars[0].position.x).toBeCloseTo(-2.05483);
119
+ expect(chars[0].position.y).toBeCloseTo(101.22915);
120
+ expect(chars[0].angle).toBeCloseTo(0.02030);
121
121
  // Fourth character
122
122
  expect(chars.at(-1).character).toBe('t');
123
- expect(chars.at(-1).position.x).toBeCloseTo(2.39222);
124
- expect(chars.at(-1).position.y).toBeCloseTo(101.22173);
125
- expect(chars.at(-1).angle).toBeCloseTo(-0.02362);
123
+ expect(chars.at(-1).position.x).toBeCloseTo(2.05483);
124
+ expect(chars.at(-1).position.y).toBeCloseTo(101.22915);
125
+ expect(chars.at(-1).angle).toBeCloseTo(-0.02030);
126
126
 
127
127
 
128
128
  // //////// Text Alignment Tests //////////
@@ -133,29 +133,29 @@ test('ArcText getArcAlignedCharacters', () => {
133
133
  expect(chars.length).toBe(4);
134
134
  // First character - Reversed!
135
135
  expect(chars[0].character).toBe('t');
136
- expect(chars[0].position.x).toBeCloseTo(101.24722);
137
- expect(chars[0].position.y).toBeCloseTo(0.74997);
138
- expect(chars[0].angle).toBeCloseTo(-1.56338);
136
+ expect(chars[0].position.x).toBeCloseTo(101.23852);
137
+ expect(chars[0].position.y).toBeCloseTo(1.52491);
138
+ expect(chars[0].angle).toBeCloseTo(-1.55573);
139
139
  // Last character - Reversed!
140
140
  expect(chars.at(-1).character).toBe('T');
141
- expect(chars.at(-1).position.x).toBeCloseTo(101.09875);
142
- expect(chars.at(-1).position.y).toBeCloseTo(5.53213);
143
- expect(chars.at(-1).angle).toBeCloseTo(-1.51613);
141
+ expect(chars.at(-1).position.x).toBeCloseTo(101.09324);
142
+ expect(chars.at(-1).position.y).toBeCloseTo(5.63200);
143
+ expect(chars.at(-1).angle).toBeCloseTo(-1.51514);
144
144
 
145
- // Create ArcAlignedText - left aligned - textAlignment = 3
145
+ // Create ArcAlignedText - left aligned - textAlignment = 2
146
146
  const arcTextAlignLeft = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100, textAlignment: 2 });
147
147
  chars = arcTextAlignLeft.getArcAlignedCharacters();
148
148
  expect(chars.length).toBe(4);
149
149
  // First character
150
150
  expect(chars[0].character).toBe('T');
151
- expect(chars[0].position.x).toBeCloseTo(-101.247222);
152
- expect(chars[0].position.y).toBeCloseTo(0.749979);
153
- expect(chars[0].angle).toBeCloseTo(1.56338);
151
+ expect(chars[0].position.x).toBeCloseTo(-101.24664);
152
+ expect(chars[0].position.y).toBeCloseTo(0.82499);
153
+ expect(chars[0].angle).toBeCloseTo(1.56265);
154
154
  // Last character
155
155
  expect(chars.at(-1).character).toBe('t');
156
- expect(chars.at(-1).position.x).toBeCloseTo(-101.09875);
157
- expect(chars.at(-1).position.y).toBeCloseTo(5.53213);
158
- expect(chars.at(-1).angle).toBeCloseTo(1.51613);
156
+ expect(chars.at(-1).position.x).toBeCloseTo(-101.12976);
157
+ expect(chars.at(-1).position.y).toBeCloseTo(4.93298);
158
+ expect(chars.at(-1).angle).toBeCloseTo(1.52206);
159
159
 
160
160
  // Create ArcAlignedText - fit to arc - textAlignment = 1
161
161
  const arcTextFitToArc = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100, textAlignment: 1 });
@@ -163,14 +163,14 @@ test('ArcText getArcAlignedCharacters', () => {
163
163
  expect(chars.length).toBe(4);
164
164
  // First character
165
165
  expect(chars[0].character).toBe('T');
166
- expect(chars[0].position.x).toBeCloseTo(-101.247222);
167
- expect(chars[0].position.y).toBeCloseTo(0.74997);
168
- expect(chars[0].angle).toBeCloseTo(1.56338);
166
+ expect(chars[0].position.x).toBeCloseTo(-101.24664);
167
+ expect(chars[0].position.y).toBeCloseTo(0.82499);
168
+ expect(chars[0].angle).toBeCloseTo(1.56265);
169
169
  // Last character
170
170
  expect(chars.at(-1).character).toBe('t');
171
- expect(chars.at(-1).position.x).toBeCloseTo(101.24722);
172
- expect(chars.at(-1).position.y).toBeCloseTo(0.74997);
173
- expect(chars.at(-1).angle).toBeCloseTo(-1.56338);
171
+ expect(chars.at(-1).position.x).toBeCloseTo(101.23852);
172
+ expect(chars.at(-1).position.y).toBeCloseTo(1.52491);
173
+ expect(chars.at(-1).angle).toBeCloseTo(-1.55573);
174
174
 
175
175
  // //////// Arc Side Tests //////////
176
176
 
@@ -180,14 +180,14 @@ test('ArcText getArcAlignedCharacters', () => {
180
180
  expect(chars.length).toBe(4);
181
181
  // First character
182
182
  expect(chars[0].character).toBe('T');
183
- expect(chars[0].position.x).toBeCloseTo(-2.39222);
184
- expect(chars[0].position.y).toBeCloseTo(101.22173);
185
- expect(chars[0].angle).toBeCloseTo(0.02362);
183
+ expect(chars[0].position.x).toBeCloseTo(-2.05483);
184
+ expect(chars[0].position.y).toBeCloseTo(101.22915);
185
+ expect(chars[0].angle).toBeCloseTo(0.02030);
186
186
  // Fourth character
187
187
  expect(chars.at(-1).character).toBe('t');
188
- expect(chars.at(-1).position.x).toBeCloseTo(2.39222);
189
- expect(chars.at(-1).position.y).toBeCloseTo(101.22173);
190
- expect(chars.at(-1).angle).toBeCloseTo(-0.02362);
188
+ expect(chars.at(-1).position.x).toBeCloseTo(2.05483);
189
+ expect(chars.at(-1).position.y).toBeCloseTo(101.22915);
190
+ expect(chars.at(-1).angle).toBeCloseTo(-0.02030);
191
191
 
192
192
  // Create ArcAlignedText with string 'Test' and radius 100
193
193
  const arcTextConcave = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100, arcSide: 2 });
@@ -195,14 +195,14 @@ test('ArcText getArcAlignedCharacters', () => {
195
195
  expect(chars.length).toBe(4);
196
196
  // First character
197
197
  expect(chars[0].character).toBe('T');
198
- expect(chars[0].position.x).toBeCloseTo(-2.39222);
199
- expect(chars[0].position.y).toBeCloseTo(98.72102);
200
- expect(chars[0].angle).toBeCloseTo(0.02422);
198
+ expect(chars[0].position.x).toBeCloseTo(-2.05482);
199
+ expect(chars[0].position.y).toBeCloseTo(98.72862);
200
+ expect(chars[0].angle).toBeCloseTo(0.02081);
201
201
  // Fourth character
202
202
  expect(chars.at(-1).character).toBe('t');
203
- expect(chars.at(-1).position.x).toBeCloseTo(2.39222);
204
- expect(chars.at(-1).position.y).toBeCloseTo(98.72102);
205
- expect(chars.at(-1).angle).toBeCloseTo(-0.02362);
203
+ expect(chars.at(-1).position.x).toBeCloseTo(2.05482);
204
+ expect(chars.at(-1).position.y).toBeCloseTo(98.72862);
205
+ expect(chars.at(-1).angle).toBeCloseTo(-0.02081);
206
206
  });
207
207
 
208
208
  test('ArcText.dxf', () => {
@@ -290,7 +290,21 @@ STANDARD
290
290
  expect(file.contents).toEqual(dxfString);
291
291
 
292
292
  // create new entity from entity data to ensure all props are loaded
293
- const newArcAlignedText = new ArcAlignedText(arctext);
293
+ const newArcAlignedText = new ArcAlignedText({
294
+ handle: arctext.getProperty('handle'),
295
+ points: arctext.points,
296
+ string: arctext.getProperty('string'),
297
+ styleName: arctext.getProperty('styleName'),
298
+ radius: arctext.getProperty('radius'),
299
+ height: arctext.getProperty('height'),
300
+ characterSpacing: arctext.getProperty('characterSpacing'),
301
+ offsetFromArc: arctext.getProperty('offsetFromArc'),
302
+ offsetFromRight: arctext.getProperty('offsetFromRight'),
303
+ offsetFromLeft: arctext.getProperty('offsetFromLeft'),
304
+ textOrientation: arctext.getProperty('textOrientation'),
305
+ textAlignment: arctext.getProperty('textAlignment'),
306
+ arcSide: arctext.getProperty('arcSide'),
307
+ });
294
308
  file = new File();
295
309
  newArcAlignedText.dxf(file);
296
310
  expect(file.contents).toEqual(dxfString);
@@ -301,32 +315,32 @@ test('Test ArcText.snaps', () => {
301
315
  const point = new Point(100, 100);
302
316
  const snaps = arcText.snaps(point, 1);
303
317
  const nodeSnaps = snaps.filter((s) => s.type === 'node');
304
- expect(nodeSnaps[0].snapPoint.x).toBeCloseTo(-2.39222);
305
- expect(nodeSnaps[0].snapPoint.y).toBeCloseTo(101.22173);
318
+ expect(nodeSnaps[0].snapPoint.x).toBeCloseTo(-2.05483);
319
+ expect(nodeSnaps[0].snapPoint.y).toBeCloseTo(101.22915);
306
320
  });
307
321
 
308
322
  test('Test ArcText.closestPoint', () => {
309
323
  const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100 });
310
324
  const point = new Point(100, 100);
311
325
  const closest = arcText.closestPoint(point);
312
- expect(closest[0].x).toBeCloseTo(2.39222);
313
- expect(closest[0].y).toBeCloseTo(101.22173);
326
+ expect(closest[0].x).toBeCloseTo(2.05483);
327
+ expect(closest[0].y).toBeCloseTo(101.22915);
314
328
  });
315
329
 
316
330
  test('Test ArcText.boundingBox', () => {
317
331
  const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100 });
318
- expect(arcText.boundingBox().xMin).toBeCloseTo(-3.14222);
319
- expect(arcText.boundingBox().xMax).toBeCloseTo(3.14222);
320
- expect(arcText.boundingBox().yMin).toBeCloseTo(99.97173);
321
- expect(arcText.boundingBox().yMax).toBeCloseTo(102.49685);
332
+ expect(arcText.boundingBox().xMin).toBeCloseTo(-2.81733);
333
+ expect(arcText.boundingBox().xMax).toBeCloseTo(2.46733);
334
+ expect(arcText.boundingBox().yMin).toBeCloseTo(99.97915);
335
+ expect(arcText.boundingBox().yMax).toBeCloseTo(102.49878);
322
336
  });
323
337
 
324
338
  test('Test ArcText.toPolylinePoints - returns correct points array', () => {
325
339
  const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100 });
326
340
  const result = arcText.toPolylinePoints();
327
341
  expect(result.length).toBe(4);
328
- expect(result[0].x).toBeCloseTo(-2.39222);
329
- expect(result[0].y).toBeCloseTo(101.22173);
342
+ expect(result[0].x).toBeCloseTo(-2.05483);
343
+ expect(result[0].y).toBeCloseTo(101.22915);
330
344
  });
331
345
 
332
346
  test('ArcText.snaps returns a centre snap at points[0]', () => {
@@ -343,3 +357,116 @@ test('ArcText.snaps returns node snaps only for non-space characters', () => {
343
357
  const nodeSnaps = arcText.snaps(new Point(0, 0), 100).filter((s) => s.type === 'node');
344
358
  expect(nodeSnaps.length).toBe(2);
345
359
  });
360
+
361
+ test('ArcAlignedCharacter respects explicit height and width', () => {
362
+ const ac = new ArcAlignedCharacter('A', new Point(0, 0), 0, 5, 3);
363
+ expect(ac.height).toBe(5);
364
+ expect(ac.width).toBe(3);
365
+ });
366
+
367
+ test('ArcText getArcAlignedCharacters empty string returns empty array', () => {
368
+ const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: '', radius: 100 });
369
+ expect(arcText.getArcAlignedCharacters()).toHaveLength(0);
370
+ });
371
+
372
+ test('ArcText boundingBox empty string returns BoundingBox instance', () => {
373
+ const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: '', radius: 100 });
374
+ expect(arcText.boundingBox()).toBeInstanceOf(BoundingBox);
375
+ });
376
+
377
+ test('ArcText toPolylinePoints empty string returns empty array', () => {
378
+ const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: '', radius: 100 });
379
+ expect(arcText.toPolylinePoints()).toHaveLength(0);
380
+ });
381
+
382
+ test('ArcText snaps with empty string returns only centre snap', () => {
383
+ const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: '', radius: 100 });
384
+ const snaps = arcText.snaps(new Point(0, 0), 1);
385
+ expect(snaps).toHaveLength(1);
386
+ expect(snaps[0].type).toBe('centre');
387
+ });
388
+
389
+ test('ArcText closestPoint returns distance 0.1 when within height * 0.35', () => {
390
+ // Single char 'T' centre-aligned sits at (0, 101.25); height = 2.5, threshold = 0.875
391
+ const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: 'T', radius: 100 });
392
+ const chars = arcText.getArcAlignedCharacters();
393
+ const charPos = chars[0].position;
394
+ // 0.5 units away is within the 0.875 threshold
395
+ const closePoint = new Point(charPos.x + 0.5, charPos.y);
396
+ const [, dist] = arcText.closestPoint(closePoint);
397
+ expect(dist).toBe(0.1);
398
+ });
399
+
400
+ test('ArcText getArcAlignedCharacters fit-to-arc single character places at midpoint', () => {
401
+ const t = new ArcAlignedText({ points: [new Point(0, 0)], radius: 10, string: 'X',
402
+ startAngle: 0, endAngle: 180, textAlignment: 1 });
403
+ const chars = t.getArcAlignedCharacters();
404
+ expect(chars).toHaveLength(1);
405
+ // midpoint of 0°–180° arc at radius 10 is 90° → point near (0, 10)
406
+ expect(chars[0].position.x).toBeCloseTo(0, 0);
407
+ expect(chars[0].position.y).toBeGreaterThan(0);
408
+ });
409
+
410
+ test('ArcText getArcAlignedCharacters single character is placed at arc midpoint', () => {
411
+ // Single char with centre alignment: charOffsetAngles = [0], stringStartPoint = arcMidPoint
412
+ // arcMidAngle(0, PI) = PI/2 → project(PI/2, 101.25) = (0, 101.25), angle = PI/2 - PI/2 = 0
413
+ const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: 'X', radius: 100 });
414
+ const chars = arcText.getArcAlignedCharacters();
415
+ expect(chars).toHaveLength(1);
416
+ expect(chars[0].character).toBe('X');
417
+ expect(chars[0].position.x).toBeCloseTo(0, 4);
418
+ expect(chars[0].position.y).toBeCloseTo(101.25, 4);
419
+ expect(chars[0].angle).toBeCloseTo(0, 4);
420
+ });
421
+
422
+ test('ArcText getArcAlignedCharacters textOrientation inward reverses character order', () => {
423
+ // textOrientation 2 reverses the string before placing characters
424
+ const arcText = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100, textOrientation: 2 });
425
+ const chars = arcText.getArcAlignedCharacters();
426
+ expect(chars).toHaveLength(4);
427
+ expect(chars[0].character).toBe('t'); // reversed
428
+ expect(chars.at(-1).character).toBe('T'); // reversed
429
+ });
430
+
431
+ test('ArcText getArcAlignedCharacters offsetFromLeft shifts character positions', () => {
432
+ // offsetFromLeft moves the string start away from the arc end for left-aligned text
433
+ const base = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100, textAlignment: 2, offsetFromLeft: 0 });
434
+ const offset = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100, textAlignment: 2, offsetFromLeft: 10 });
435
+ const baseChars = base.getArcAlignedCharacters();
436
+ const offsetChars = offset.getArcAlignedCharacters();
437
+ expect(offsetChars[0].position.x).not.toBeCloseTo(baseChars[0].position.x, 1);
438
+ });
439
+
440
+ test('ArcText getArcAlignedCharacters offsetFromRight shifts character positions', () => {
441
+ // offsetFromRight moves the string start away from the arc start for right-aligned text
442
+ const base = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100, textAlignment: 3, offsetFromRight: 0 });
443
+ const offset = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Test', radius: 100, textAlignment: 3, offsetFromRight: 10 });
444
+ const baseChars = base.getArcAlignedCharacters();
445
+ const offsetChars = offset.getArcAlignedCharacters();
446
+ expect(offsetChars[0].position.x).not.toBeCloseTo(baseChars[0].position.x, 1);
447
+ });
448
+
449
+ test('ArcAlignedText.toCharacters returns one entry per character with scene coords', () => {
450
+ const arc = new ArcAlignedText({ points: [new Point(0, 0)], string: 'AB', radius: 100 });
451
+ const chars = arc.toCharacters();
452
+ expect(chars).toHaveLength(2);
453
+ expect(chars[0].char).toBe('A');
454
+ expect(chars[1].char).toBe('B');
455
+ expect(typeof chars[0].x).toBe('number');
456
+ expect(typeof chars[0].y).toBe('number');
457
+ expect(typeof chars[0].rotation).toBe('number');
458
+ });
459
+
460
+ test('ArcAlignedText.draw calls renderer.drawText with all characters', () => {
461
+ const arc = new ArcAlignedText({ points: [new Point(0, 0)], string: 'Hi', radius: 50 });
462
+ let calledChars;
463
+ const mockRenderer = {
464
+ drawText(characters) {
465
+ calledChars = characters;
466
+ },
467
+ };
468
+ arc.draw(mockRenderer);
469
+ expect(calledChars).toHaveLength(2);
470
+ expect(calledChars[0].char).toBe('H');
471
+ expect(calledChars[1].char).toBe('i');
472
+ });
@@ -96,7 +96,7 @@ ByLayer
96
96
  30
97
97
  0.0
98
98
  39
99
- 2
99
+ 1
100
100
  40
101
101
  100
102
102
  `;
@@ -105,7 +105,7 @@ ByLayer
105
105
 
106
106
 
107
107
  // create new entity from entity data to ensure all props are loaded
108
- const newCircle = new Circle(circle);
108
+ const newCircle = new Circle({ handle: circle.getProperty('handle'), points: circle.points });
109
109
  file = new File();
110
110
  newCircle.dxf(file);
111
111
  expect(file.contents).toEqual(dxfString);
@@ -127,90 +127,23 @@ test('Test Circle.toPolylinePoints', () => {
127
127
  expect(circlePoints[2].bulge).toBe(0);
128
128
  });
129
129
 
130
- test('Test Circle.trim returns empty array when provided with empty intersection list', () => {
131
- const circle = new Circle({ points: [new Point(0, 0), new Point(100, 0)] });
132
-
133
- expect(circle.trim([])).toEqual([]);
134
- expect(circle.trim()).toEqual([]);
135
- });
136
-
137
- test('Test Circle.trim does not modify circle', () => {
138
- const circle = new Circle({ points: [new Point(0, 0), new Point(10, 0)] });
139
-
140
- DesignCore.Mouse.pointOnScene = () => new Point(7.71, 7.71);
141
-
142
- // test a single intersection - not valid
143
- const intersections = [new Point(10, 0)];
144
- expect(circle.trim(intersections)).toEqual([]);
145
-
146
- // test two intersections - should return an arc
147
- intersections.push(new Point(-10, 0));
148
- let trimWithTwoPoints = circle.trim(intersections);
149
- let trimResult = trimWithTwoPoints[0].entity;
150
- expect(trimResult.type).toEqual('Arc');
151
- // Verify trimmed arc has a unique handle (not the circle's)
152
- expect(trimResult.handle).toBeUndefined();
153
- expect(trimResult.points[0].x).toEqual(0);
154
- expect(trimResult.points[0].y).toEqual(0);
155
-
156
- expect(trimResult.points[1].x).toEqual(10);
157
- expect(trimResult.points[1].y).toEqual(0);
158
-
159
- expect(trimResult.points[2].x).toEqual(-10);
160
- expect(trimResult.points[2].y).toEqual(0);
161
-
162
- expect(trimWithTwoPoints[1].entity).toEqual(circle);
163
-
164
- // test three intersections - should return an arc from first two points found
165
- intersections.push(new Point(0, 10));
166
- trimWithTwoPoints = circle.trim(intersections);
167
- trimResult = trimWithTwoPoints[0].entity;
168
- expect(trimResult.type).toEqual('Arc');
169
- expect(trimResult.points[0].x).toEqual(0);
170
- expect(trimResult.points[0].y).toEqual(0);
171
-
172
- expect(trimResult.points[1].x).toEqual(10);
173
- expect(trimResult.points[1].y).toEqual(0);
174
-
175
- expect(trimResult.points[2].x).toEqual(0);
176
- expect(trimResult.points[2].y).toEqual(10);
177
-
178
- expect(trimWithTwoPoints[1].entity).toEqual(circle);
179
-
180
- // test four intersections - should return an arc from first two points found
181
- intersections.push(new Point(0, -10));
182
- trimWithTwoPoints = circle.trim(intersections);
183
- trimResult = trimWithTwoPoints[0].entity;
184
- expect(trimResult.type).toEqual('Arc');
185
- expect(trimResult.points[0].x).toEqual(0);
186
- expect(trimResult.points[0].y).toEqual(0);
187
-
188
- expect(trimResult.points[1].x).toEqual(10);
189
- expect(trimResult.points[1].y).toEqual(0);
190
-
191
- expect(trimResult.points[2].x).toEqual(0);
192
- expect(trimResult.points[2].y).toEqual(10);
193
-
194
- expect(trimWithTwoPoints[1].entity).toEqual(circle);
195
- });
196
-
197
130
  test('Circle.execute creates circle from diameter input', async () => {
198
- const center = new Point(0, 0);
131
+ const centre = new Point(0, 0);
199
132
 
200
- await withMockInput(DesignCore.Scene, [center, 'Diameter', 20], async () => {
133
+ await withMockInput(DesignCore.Scene, [centre, 'Diameter', 20], async () => {
201
134
  const circle = new Circle({});
202
135
  await circle.execute();
203
136
 
204
- expect(circle.points[0]).toBe(center);
137
+ expect(circle.points[0]).toBe(centre);
205
138
  expect(circle.getRadius()).toBeCloseTo(10);
206
139
  });
207
140
  });
208
141
 
209
142
  test('Circle.execute re-prompts on zero or negative radius', async () => {
210
- const center = new Point(0, 0);
143
+ const centre = new Point(0, 0);
211
144
 
212
145
  // 0 and -5 are rejected; 10 is accepted
213
- await withMockInput(DesignCore.Scene, [center, 0, -5, 10], async () => {
146
+ await withMockInput(DesignCore.Scene, [centre, 0, -5, 10], async () => {
214
147
  const circle = new Circle({});
215
148
  await circle.execute();
216
149
 
@@ -219,10 +152,10 @@ test('Circle.execute re-prompts on zero or negative radius', async () => {
219
152
  });
220
153
 
221
154
  test('Circle.execute re-prompts on zero or negative diameter', async () => {
222
- const center = new Point(0, 0);
155
+ const centre = new Point(0, 0);
223
156
 
224
157
  // 'Diameter', then 0 and -20 rejected, then 20 accepted (radius = 10)
225
- await withMockInput(DesignCore.Scene, [center, 'Diameter', 0, -20, 20], async () => {
158
+ await withMockInput(DesignCore.Scene, [centre, 'Diameter', 0, -20, 20], async () => {
226
159
  const circle = new Circle({});
227
160
  await circle.execute();
228
161
 
@@ -234,16 +167,16 @@ test('Circle.register returns command object', () => {
234
167
  expect(Circle.register()).toEqual({ command: 'Circle', shortcut: 'C', type: 'Entity' });
235
168
  });
236
169
 
237
- test('Circle constructor loads radius from DXF group code 40', () => {
238
- const circle = new Circle({ points: [new Point(0, 0)], 40: 50 });
170
+ test('Circle constructor loads radius from DXF group code 40 via fromDxf', () => {
171
+ const circle = new Circle(Circle.fromDxf({ points: [new Point(0, 0)], 40: 50 }));
239
172
  expect(circle.getRadius()).toBeCloseTo(50);
240
173
  expect(circle.points[1].x).toBeCloseTo(50);
241
174
  expect(circle.points[1].y).toBeCloseTo(0);
242
175
  });
243
176
 
244
177
  test('Circle.execute creates circle from numeric radius input', async () => {
245
- const center = new Point(0, 0);
246
- await withMockInput(DesignCore.Scene, [center, 25], async () => {
178
+ const centre = new Point(0, 0);
179
+ await withMockInput(DesignCore.Scene, [centre, 25], async () => {
247
180
  const circle = new Circle({});
248
181
  await circle.execute();
249
182
  expect(circle.getRadius()).toBeCloseTo(25);
@@ -252,8 +185,8 @@ test('Circle.execute creates circle from numeric radius input', async () => {
252
185
 
253
186
  test('Circle.execute re-prompts on zero-distance point input', async () => {
254
187
  const notifySpy = jest.spyOn(DesignCore.Core, 'notify').mockImplementation(() => {});
255
- const center = new Point(0, 0);
256
- await withMockInput(DesignCore.Scene, [center, new Point(0, 0), new Point(10, 0)], async () => {
188
+ const centre = new Point(0, 0);
189
+ await withMockInput(DesignCore.Scene, [centre, new Point(0, 0), new Point(10, 0)], async () => {
257
190
  const circle = new Circle({});
258
191
  await circle.execute();
259
192
  expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.NONZERO));
@@ -263,10 +196,10 @@ test('Circle.execute re-prompts on zero-distance point input', async () => {
263
196
  });
264
197
 
265
198
  test('Circle.preview does not throw with 0 or 1 points', () => {
266
- const origCreate = DesignCore.Scene.tempEntities.create;
199
+ const origCreate = DesignCore.Scene.previewEntities.create;
267
200
  const origMouse = DesignCore.Mouse.pointOnScene;
268
201
  const calls = [];
269
- DesignCore.Scene.tempEntities.create = (type, obj) => calls.push([type, obj]);
202
+ DesignCore.Scene.previewEntities.create = (type, obj) => calls.push([type, obj]);
270
203
  DesignCore.Mouse.pointOnScene = () => new Point(5, 5);
271
204
 
272
205
  const circle0 = new Circle({});
@@ -278,16 +211,20 @@ test('Circle.preview does not throw with 0 or 1 points', () => {
278
211
  expect(() => circle1.preview()).not.toThrow();
279
212
  expect(calls.some(([type]) => type === 'Circle')).toBe(true);
280
213
 
281
- DesignCore.Scene.tempEntities.create = origCreate;
214
+ DesignCore.Scene.previewEntities.create = origCreate;
282
215
  DesignCore.Mouse.pointOnScene = origMouse;
283
216
  });
284
217
 
285
- test('Circle.draw does not throw', () => {
218
+ test('Circle.draw calls renderer.drawShape with toPolylinePoints', () => {
286
219
  const circle = new Circle({ points: [new Point(0, 0), new Point(10, 0)] });
287
- const ctx = { arc: jest.fn(), stroke: jest.fn() };
288
- expect(() => circle.draw(ctx, 1)).not.toThrow();
289
- expect(ctx.arc).toHaveBeenCalledTimes(1);
290
- expect(ctx.stroke).toHaveBeenCalledTimes(1);
220
+ let capturedPoints;
221
+ const mockRenderer = {
222
+ drawShape(points) {
223
+ capturedPoints = points;
224
+ },
225
+ };
226
+ circle.draw(mockRenderer);
227
+ expect(capturedPoints).toEqual(circle.toPolylinePoints());
291
228
  });
292
229
 
293
230
  test('Circle.snaps returns centre snap', () => {
@@ -326,21 +263,21 @@ test('Circle.snaps nearest snap does not fire when too far', () => {
326
263
  test('Circle.fromPolylinePoints round-trip preserves centre and radius', () => {
327
264
  const circle = new Circle({ points: [new Point(5, 10), new Point(5, 10).project(0, 8)] });
328
265
  const polyPts = circle.toPolylinePoints();
329
- const rebuilt = new Circle({});
330
- rebuilt.fromPolylinePoints(polyPts);
266
+ const rebuilt = new Circle({}).fromPolylinePoints(polyPts);
267
+ expect(rebuilt.type).toBe('Circle');
331
268
  expect(rebuilt.points[0].x).toBeCloseTo(5, 10);
332
269
  expect(rebuilt.points[0].y).toBeCloseTo(10, 10);
333
- expect(rebuilt.radius).toBeCloseTo(8, 10);
270
+ expect(rebuilt.getProperty('radius')).toBeCloseTo(8, 10);
334
271
  });
335
272
 
336
273
  test('Circle.fromPolylinePoints precision drift is negligible', () => {
337
274
  const circle = new Circle({ points: [new Point(123.456, -789.012), new Point(123.456, -789.012).project(1.234, 56.789)] });
338
275
  const polyPts = circle.toPolylinePoints();
339
- const rebuilt = new Circle({});
340
- rebuilt.fromPolylinePoints(polyPts);
276
+ const rebuilt = new Circle({}).fromPolylinePoints(polyPts);
277
+ expect(rebuilt.type).toBe('Circle');
341
278
  const drift = Math.hypot(rebuilt.points[0].x - circle.points[0].x, rebuilt.points[0].y - circle.points[0].y);
342
279
  expect(drift).toBeLessThan(1e-10);
343
- expect(Math.abs(rebuilt.radius - circle.radius)).toBeLessThan(1e-10);
280
+ expect(Math.abs(rebuilt.getProperty('radius') - circle.getProperty('radius'))).toBeLessThan(1e-10);
344
281
  });
345
282
 
346
283
  test('Circle.snaps returns two tangent snaps from outside', () => {
@@ -357,6 +294,41 @@ test('Circle.snaps returns two tangent snaps from outside', () => {
357
294
  DesignCore.Scene.inputManager.inputPoint = savedInputPoint;
358
295
  });
359
296
 
297
+ describe('Circle.fromDxf', () => {
298
+ test('projects edge point from DXF radius code', () => {
299
+ const centre = new Point(0, 0);
300
+ const result = Circle.fromDxf({ points: [centre], 40: 10 });
301
+ expect(result.points.length).toBe(2);
302
+ expect(result.points[0]).toBe(centre);
303
+ expect(result.points[1].x).toBeCloseTo(10);
304
+ expect(result.points[1].y).toBeCloseTo(0);
305
+ });
306
+
307
+ test('preserves all other data properties', () => {
308
+ const result = Circle.fromDxf({ points: [new Point(0, 0)], 40: 10, 8: 'MyLayer' });
309
+ expect(result[8]).toBe('MyLayer');
310
+ });
311
+
312
+ test('returns data unchanged when no centre point', () => {
313
+ const data = { 40: 10 };
314
+ expect(Circle.fromDxf(data)).toBe(data);
315
+ });
316
+
317
+ test('returns data unchanged when no radius code', () => {
318
+ const data = { points: [new Point(0, 0)] };
319
+ expect(Circle.fromDxf(data)).toBe(data);
320
+ });
321
+
322
+ test('circle constructed via fromDxf matches circle constructed directly', () => {
323
+ const centre = new Point(100, 100);
324
+ const fromDxfCircle = new Circle(Circle.fromDxf({ points: [centre], 40: 50 }));
325
+ const directCircle = new Circle({ points: [centre, new Point(150, 100)] });
326
+ expect(fromDxfCircle.getRadius()).toBeCloseTo(directCircle.getRadius());
327
+ expect(fromDxfCircle.points[0].x).toBeCloseTo(directCircle.points[0].x);
328
+ expect(fromDxfCircle.points[0].y).toBeCloseTo(directCircle.points[0].y);
329
+ });
330
+ });
331
+
360
332
  test('Circle.snaps returns no tangent when fromPoint is inside the circle', () => {
361
333
  const circle = new Circle({ points: [new Point(0, 0), new Point(10, 0)] });
362
334
  const savedInputPoint = DesignCore.Scene.inputManager.inputPoint;