@design-core/core 0.0.5 → 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 +33 -284
  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 +36 -13
  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 +129 -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 +296 -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
@@ -122,7 +122,7 @@ export class Scale extends Tool {
122
122
  if (this.points.length >= 1 && this.referencePoint === null) {
123
123
  // Draw a line from base point to mouse
124
124
  const points = [this.points[0], mousePoint];
125
- DesignCore.Scene.tempEntities.create('Line', { points: points });
125
+ DesignCore.Scene.previewEntities.create('Line', { points: points });
126
126
 
127
127
  const base = this.points[0];
128
128
  const distance = base.distance(mousePoint);
@@ -3,6 +3,10 @@ import { Strings } from '../lib/strings.js';
3
3
  import { Tool } from './tool.js';
4
4
  import { Input, PromptOptions } from '../lib/inputManager.js';
5
5
  import { Logging } from '../lib/logging.js';
6
+ import { AddState, RemoveState } from '../lib/stateManager.js';
7
+ import { Utils } from '../lib/utils.js';
8
+ import { Colour } from '../lib/colour.js';
9
+ import { PolylineUtils } from '../lib/polylineUtils.js';
6
10
 
7
11
  import { DesignCore } from '../designCore.js';
8
12
 
@@ -38,10 +42,10 @@ export class Trim extends Tool {
38
42
  */
39
43
  async execute() {
40
44
  try {
41
- const op = new PromptOptions(Strings.Input.BOUNDARY, [Input.Type.SELECTIONSET]);
45
+ const boundaryOp = new PromptOptions(Strings.Input.BOUNDARY, [Input.Type.SELECTIONSET]);
42
46
 
43
47
  if (!DesignCore.Scene.selectionManager.selectionSet.selectionSet.length) {
44
- const boundary = await DesignCore.Scene.inputManager.requestInput(op);
48
+ const boundary = await DesignCore.Scene.inputManager.requestInput(boundaryOp);
45
49
  if (boundary === undefined) return;
46
50
  }
47
51
 
@@ -51,9 +55,9 @@ export class Trim extends Tool {
51
55
  this.selectedBoundaryItems.push(boundaryItem);
52
56
  }
53
57
 
54
- const op2 = new PromptOptions(Strings.Input.SELECT, [Input.Type.SINGLESELECTION]);
58
+ const selectOp = new PromptOptions(Strings.Input.SELECT, [Input.Type.SINGLESELECTION]);
55
59
  while (true) {
56
- const selection = await DesignCore.Scene.inputManager.requestInput(op2);
60
+ const selection = await DesignCore.Scene.inputManager.requestInput(selectOp);
57
61
  if (selection === undefined) break;
58
62
  this.selectedItem = DesignCore.Scene.entities.get(selection.selectedItemIndex);
59
63
  DesignCore.Scene.selectionManager.removeLastSelection();
@@ -70,7 +74,29 @@ export class Trim extends Tool {
70
74
  * Preview the command during execution
71
75
  */
72
76
  preview() {
73
- // No Preview
77
+ if (!this.selectedBoundaryItems.length) return;
78
+
79
+ const index = DesignCore.Scene.selectionManager.findClosestItem(DesignCore.Mouse.pointOnScene());
80
+ if (index === undefined) return;
81
+
82
+ const entity = DesignCore.Scene.entities.get(index);
83
+ if (typeof entity.toPolylinePoints !== 'function' || typeof entity.fromPolylinePoints !== 'function') return;
84
+ const intersectPoints = this.#collectIntersectPoints(entity);
85
+ if (!intersectPoints.length) return;
86
+
87
+ const stateChanges = this.#trimEntity(entity, intersectPoints);
88
+ if (!stateChanges?.length) return;
89
+
90
+ // Draw the full entity in a dulled colour, then draw survivors on top.
91
+ const dulledEntity = Utils.cloneObject(entity);
92
+ dulledEntity.setProperty('colour', Colour.blend(entity.getDrawColour(), DesignCore.Settings.canvasbackgroundcolour, DesignCore.Settings.previewBlendFactor));
93
+ DesignCore.Scene.previewEntities.add(dulledEntity);
94
+
95
+ for (const change of stateChanges) {
96
+ if (change instanceof AddState) {
97
+ DesignCore.Scene.previewEntities.add(change.entity);
98
+ }
99
+ }
74
100
  }
75
101
 
76
102
  /**
@@ -78,26 +104,16 @@ export class Trim extends Tool {
78
104
  */
79
105
  action() {
80
106
  if (this.selectedItem && this.selectedBoundaryItems.length) {
81
- const intersectPoints = [];
82
-
83
- for (const boundaryItem of this.selectedBoundaryItems) {
84
- if (boundaryItem !== this.selectedItem) {
85
- try {
86
- const intersect = Intersection.intersectPolylinePolyline(boundaryItem.toPolylinePoints(), this.selectedItem.toPolylinePoints());
87
- if (intersect.points.length) {
88
- for (let point = 0; point < intersect.points.length; point++) {
89
- intersectPoints.push(intersect.points[point]);
90
- }
91
- }
92
- } catch {
93
- Logging.instance.warn(`${this.constructor.name}: Error intersecting between ${boundaryItem.type} and ${this.selectedItem.type}`);
94
- continue;
95
- }
96
- }
107
+ if (typeof this.selectedItem.fromPolylinePoints !== 'function') {
108
+ DesignCore.Core.notify(`${this.type} - ${this.selectedItem.type} ${Strings.Message.NOTRIM}`);
109
+ this.selectedItem = null;
110
+ return;
97
111
  }
98
112
 
113
+ const intersectPoints = this.#collectIntersectPoints(this.selectedItem);
114
+
99
115
  if (intersectPoints.length) {
100
- const stateChanges = this.selectedItem.trim(intersectPoints);
116
+ const stateChanges = this.#trimEntity(this.selectedItem, intersectPoints);
101
117
  if (stateChanges?.length) {
102
118
  DesignCore.Scene.commit(stateChanges);
103
119
  }
@@ -106,8 +122,234 @@ export class Trim extends Tool {
106
122
  }
107
123
  }
108
124
 
109
- // reset selected item
110
125
  this.selectedItem = null;
111
126
  }
127
+
128
+ /**
129
+ * Collect all intersection points between the boundary items and the given entity.
130
+ * Unsupported entity combinations are silently skipped.
131
+ * @param {Object} entity
132
+ * @return {Array}
133
+ */
134
+ #collectIntersectPoints(entity) {
135
+ const intersectPoints = [];
136
+ for (const boundaryItem of this.selectedBoundaryItems) {
137
+ if (boundaryItem === entity) continue;
138
+ try {
139
+ const intersect = Intersection.intersectPolylinePolyline(boundaryItem.toPolylinePoints(), entity.toPolylinePoints());
140
+ intersectPoints.push(...intersect.points);
141
+ } catch {
142
+ // skip unsupported entity combinations
143
+ }
144
+ }
145
+ return intersectPoints;
146
+ }
147
+
148
+ /**
149
+ * Compute state changes to trim entity at the pair of intersection points
150
+ * surrounding the mouse position. Operates entirely in polyline-point space
151
+ * via entity.toPolylinePoints() / entity.fromPolylinePoints(), so it works
152
+ * for any entity that implements that protocol (Line, Arc, Circle,
153
+ * BasePolyline, and future entities such as Spline or Ellipse).
154
+ * @param {Object} entity
155
+ * @param {Array} intersectPoints
156
+ * @return {Array}
157
+ */
158
+ #trimEntity(entity, intersectPoints) {
159
+ const polyPoints = entity.toPolylinePoints();
160
+ const mousePosition = DesignCore.Mouse.pointOnScene();
161
+ const isClosed = polyPoints[0].isSame(polyPoints.at(-1));
162
+
163
+ // Drop any intersection point that coincides with an existing vertex
164
+ const filtered = intersectPoints.filter((p) => !polyPoints.some((v) => v.isSame(p)));
165
+ if (!filtered.length) return [];
166
+
167
+ // Locate each intersection point within the polyline segments
168
+ const locatedIntersections = [];
169
+ for (const point of filtered) {
170
+ for (let i = 1; i < polyPoints.length; i++) {
171
+ if (PolylineUtils.isPointOnSegment(point, polyPoints[i - 1], polyPoints[i])) {
172
+ locatedIntersections.push({
173
+ segmentIndex: i,
174
+ point,
175
+ positionAlongSegment: PolylineUtils.positionOnSegment(polyPoints, point, i),
176
+ });
177
+ break;
178
+ }
179
+ }
180
+ }
181
+
182
+ if (!locatedIntersections.length) return [];
183
+
184
+ locatedIntersections.sort((a, b) => {
185
+ if (a.segmentIndex !== b.segmentIndex) return a.segmentIndex - b.segmentIndex;
186
+ return a.positionAlongSegment - b.positionAlongSegment;
187
+ });
188
+
189
+ // Find which segment the mouse is closest to
190
+ let mouseSegmentIndex = 0;
191
+ let mouseClosest = null;
192
+ let minDistance = Infinity;
193
+ for (let i = 1; i < polyPoints.length; i++) {
194
+ const closest = PolylineUtils.closestPointOnSegment(polyPoints, mousePosition, i);
195
+ if (closest) {
196
+ const dist = mousePosition.distance(closest);
197
+ if (dist < minDistance) {
198
+ minDistance = dist;
199
+ mouseSegmentIndex = i;
200
+ mouseClosest = closest;
201
+ }
202
+ }
203
+ }
204
+
205
+ const mousePos = mouseClosest ?
206
+ PolylineUtils.positionOnSegment(polyPoints, mouseClosest, mouseSegmentIndex) :
207
+ 0;
208
+
209
+ // Find the nearest intersections before and after the mouse position
210
+ let trimBefore = null;
211
+ let trimAfter = null;
212
+ for (const loc of locatedIntersections) {
213
+ if (loc.segmentIndex < mouseSegmentIndex) {
214
+ trimBefore = loc;
215
+ } else if (loc.segmentIndex === mouseSegmentIndex) {
216
+ if (loc.positionAlongSegment <= mousePos) {
217
+ trimBefore = loc;
218
+ } else if (!trimAfter) {
219
+ trimAfter = loc;
220
+ }
221
+ } else if (!trimAfter) {
222
+ trimAfter = loc;
223
+ }
224
+ }
225
+
226
+ const stateChanges = [];
227
+
228
+ if (isClosed) {
229
+ // Wrap cyclically: if trimBefore is null use the last intersection,
230
+ // if trimAfter is null use the first.
231
+ if (!trimBefore) trimBefore = locatedIntersections.at(-1);
232
+ if (!trimAfter) trimAfter = locatedIntersections[0];
233
+
234
+ // A single intersection point cannot divide a closed shape.
235
+ if (trimBefore === trimAfter) return [];
236
+
237
+ // Build the single kept open portion from trimAfter to trimBefore,
238
+ // going away from the mouse (i.e. not through the mouse segment).
239
+ let outputPoints;
240
+ if (trimAfter.segmentIndex < trimBefore.segmentIndex ||
241
+ (trimAfter.segmentIndex === trimBefore.segmentIndex &&
242
+ trimAfter.positionAlongSegment < trimBefore.positionAlongSegment)) {
243
+ // Forward walk: trimAfter → trimBefore without crossing the seam
244
+ outputPoints = this.#buildPortion(polyPoints, trimAfter, trimBefore);
245
+ } else {
246
+ // Wrapping walk: trimAfter → end → seam → start → trimBefore
247
+ outputPoints = this.#buildWrappingPortion(polyPoints, trimAfter, trimBefore);
248
+ }
249
+
250
+ if (outputPoints && outputPoints.length >= 2) {
251
+ const output = Utils.cloneObject(entity).fromPolylinePoints(outputPoints);
252
+ output.flags?.removeValue(1);
253
+ stateChanges.push(new AddState(output));
254
+ stateChanges.push(new RemoveState(entity));
255
+ }
256
+ return stateChanges;
257
+ }
258
+
259
+ // Open entity: build portionOne (start → trimBefore) and portionTwo (trimAfter → end)
260
+ const startLoc = { segmentIndex: 1, point: polyPoints[0] };
261
+ const endLoc = { segmentIndex: polyPoints.length - 1, point: polyPoints.at(-1) };
262
+ const portionOnePoints = trimBefore ? this.#buildPortion(polyPoints, startLoc, trimBefore) : null;
263
+ const portionTwoPoints = trimAfter ? this.#buildPortion(polyPoints, trimAfter, endLoc) : null;
264
+
265
+ if (portionOnePoints) {
266
+ const output = Utils.cloneObject(entity).fromPolylinePoints(portionOnePoints);
267
+ output.flags?.removeValue(1);
268
+ stateChanges.push(new AddState(output));
269
+ }
270
+ if (portionTwoPoints) {
271
+ const output = Utils.cloneObject(entity).fromPolylinePoints(portionTwoPoints);
272
+ output.flags?.removeValue(1);
273
+ stateChanges.push(new AddState(output));
274
+ }
275
+
276
+ if (trimBefore || trimAfter) {
277
+ stateChanges.push(new RemoveState(entity));
278
+ }
279
+
280
+ return stateChanges;
281
+ }
282
+
283
+ /**
284
+ * Build a sub-polyline from fromLoc to toLoc going forward (no seam crossing).
285
+ * Both locations must appear in forward order along the polyline.
286
+ * @param {Array} polyPoints
287
+ * @param {Object} fromLoc - { segmentIndex, point, positionAlongSegment }
288
+ * @param {Object} toLoc - { segmentIndex, point, positionAlongSegment }
289
+ * @return {Array|null}
290
+ */
291
+ #buildPortion(polyPoints, fromLoc, toLoc) {
292
+ const fromSegStart = polyPoints[fromLoc.segmentIndex - 1];
293
+ const fromSegEnd = polyPoints[fromLoc.segmentIndex];
294
+
295
+ // Same-segment case: compute the sub-arc bulge directly from the arc geometry
296
+ if (fromLoc.segmentIndex === toLoc.segmentIndex) {
297
+ const startPoint = fromLoc.point.clone();
298
+ if (fromSegStart.bulge !== 0 && fromSegStart.bulge !== undefined) {
299
+ const center = fromSegStart.bulgeCentrePoint(fromSegEnd);
300
+ const direction = fromSegStart.bulge > 0 ? 1 : -1;
301
+ const startAngle = center.angle(fromLoc.point);
302
+ const endAngle = center.angle(toLoc.point);
303
+ const includedAngle = ((endAngle - startAngle) * direction + 4 * Math.PI) % (2 * Math.PI);
304
+ startPoint.bulge = Math.tan(includedAngle / 4) * direction;
305
+ }
306
+ return [startPoint, toLoc.point.clone()];
307
+ }
308
+
309
+ const points = [];
310
+
311
+ // Start point: fromLoc.point with partial bulge covering the rest of its segment
312
+ const startPoint = fromLoc.point.clone();
313
+ if (fromSegStart.bulge !== 0 && fromSegStart.bulge !== undefined) {
314
+ startPoint.bulge = fromSegStart.partialBulge(fromSegEnd, fromLoc.point, true);
315
+ }
316
+ points.push(startPoint);
317
+
318
+ // Intermediate vertices between the two segments
319
+ for (let i = fromLoc.segmentIndex; i <= toLoc.segmentIndex - 2; i++) {
320
+ points.push(polyPoints[i].clone());
321
+ }
322
+
323
+ // Start of toLoc's segment with partial bulge trimmed to toLoc.point
324
+ const toSegStart = polyPoints[toLoc.segmentIndex - 1];
325
+ const toSegEnd = polyPoints[toLoc.segmentIndex];
326
+ const lastSegStart = toSegStart.clone();
327
+ if (toSegStart.bulge !== 0 && toSegStart.bulge !== undefined) {
328
+ lastSegStart.bulge = toSegStart.partialBulge(toSegEnd, toLoc.point);
329
+ }
330
+ if (!points.at(-1).isSame(lastSegStart)) points.push(lastSegStart);
331
+
332
+ const endPoint = toLoc.point.clone();
333
+ if (!points.at(-1).isSame(endPoint)) points.push(endPoint);
334
+
335
+ return points.length >= 2 ? points : null;
336
+ }
337
+
338
+ /**
339
+ * Build a sub-polyline from fromLoc to toLoc crossing the seam (wrapping).
340
+ * Used for closed entities when the kept portion wraps through the closure point.
341
+ * Implemented as two forward #buildPortion calls joined at the seam.
342
+ * @param {Array} polyPoints
343
+ * @param {Object} fromLoc - { segmentIndex, point, positionAlongSegment }
344
+ * @param {Object} toLoc - { segmentIndex, point, positionAlongSegment }
345
+ * @return {Array|null}
346
+ */
347
+ #buildWrappingPortion(polyPoints, fromLoc, toLoc) {
348
+ const firstHalf = this.#buildPortion(polyPoints, fromLoc, { segmentIndex: polyPoints.length - 1, point: polyPoints.at(-1) });
349
+ const secondHalf = this.#buildPortion(polyPoints, { segmentIndex: 1, point: polyPoints[0] }, toLoc);
350
+ if (!firstHalf || !secondHalf) return null;
351
+ firstHalf.pop(); // discard closure point before joining
352
+ return firstHalf.concat(secondHalf);
353
+ }
112
354
  }
113
355
 
@@ -99,7 +99,7 @@ export class Zoom extends Tool {
99
99
  new Point(p1.x, p1.y),
100
100
  ];
101
101
 
102
- DesignCore.Scene.tempEntities.create('Polyline', { points: points });
102
+ DesignCore.Scene.previewEntities.create('Polyline', { points: points });
103
103
  }
104
104
  }
105
105
 
package/jest.config.js ADDED
@@ -0,0 +1,6 @@
1
+ export default {
2
+ coveragePathIgnorePatterns: [
3
+ '/node_modules/',
4
+ '/test/',
5
+ ],
6
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@design-core/core",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
@@ -9,7 +9,7 @@
9
9
  "eslint": "^8.26.0",
10
10
  "eslint-config-google": "^0.14.0",
11
11
  "eslint-config-standard": "^17.0.0",
12
- "eslint-plugin-import": "^2.26.0",
12
+ "eslint-plugin-import": "^2.32.0",
13
13
  "eslint-plugin-n": "^15.3.0",
14
14
  "eslint-plugin-promise": "^6.1.1",
15
15
  "jest": "^29.7.0"
@@ -17,7 +17,9 @@
17
17
  "description": "2D CAD Library",
18
18
  "exports": {
19
19
  ".": "./core/core/core.js",
20
- "./designCore.js": "./core/designCore.js"
20
+ "./designCore.js": "./core/designCore.js",
21
+ "./property.js": "./core/properties/property.js",
22
+ "./canvasRenderer.js": "./core/lib/renderers/canvasRenderer.js"
21
23
  },
22
24
  "directories": {
23
25
  "test": "test"
@@ -34,7 +34,7 @@ test.each(scenarios)('Dimension.execute handles $desc', async (scenario) => {
34
34
  // check if the text value matches the expected dimension value
35
35
  for (const entity of dimensionBlockEntities) {
36
36
  if (entity.type === 'Text') {
37
- expect(Number(entity.string)).toBeCloseTo(dimensionValue);
37
+ expect(Number(entity.getProperty('string'))).toBeCloseTo(dimensionValue);
38
38
  }
39
39
  }
40
40
  }, { selectedItems });
@@ -104,23 +104,29 @@ describe('AlignedDimension.getPointsFromSelection', () => {
104
104
  });
105
105
 
106
106
  test('AlignedDimension.preview runs without error and calls createTempItem', () => {
107
- const origCreateTempItem = DesignCore.Scene.tempEntities.create;
107
+ const origAddAux = DesignCore.Scene.auxiliaryEntities.add;
108
+ const origCreateTempItem = DesignCore.Scene.previewEntities.create;
108
109
  const origPointOnScene = DesignCore.Mouse.pointOnScene;
109
110
  // Manual mock for createTempItem
110
111
  const createTempItemCalls = [];
111
- DesignCore.Scene.tempEntities.create = function(type, obj) {
112
+ DesignCore.Scene.previewEntities.create = function(type, obj) {
112
113
  createTempItemCalls.push([type, obj]);
113
114
  };
115
+ // Manual mock for auxiliaryEntities.add
116
+ const addAuxCalls = [];
117
+ DesignCore.Scene.auxiliaryEntities.add = function(item) {
118
+ addAuxCalls.push(item);
119
+ };
114
120
  // Manual mock for pointOnScene
115
121
  DesignCore.Mouse.pointOnScene = function() {
116
122
  return new Point(5, 5);
117
123
  };
118
124
 
119
- // Test with 1 point (should call createTempItem with 'Line')
125
+ // Test with 1 point (should add a RubberBand to auxiliaryEntities)
120
126
  const dim1 = new AlignedDimension();
121
127
  dim1.points = [new Point(0, 0)];
122
128
  expect(() => dim1.preview()).not.toThrow();
123
- expect(createTempItemCalls.some((call) => call[0] === 'Line')).toBe(true);
129
+ expect(addAuxCalls.length).toBeGreaterThanOrEqual(1);
124
130
 
125
131
  // Test with >1 point (should call createTempItem with this.type)
126
132
  const dim2 = new AlignedDimension();
@@ -129,7 +135,8 @@ test('AlignedDimension.preview runs without error and calls createTempItem', ()
129
135
  expect(createTempItemCalls.some((call) => call[0] === dim2.type)).toBe(true);
130
136
 
131
137
  // Restore
132
- DesignCore.Scene.tempEntities.create = origCreateTempItem;
138
+ DesignCore.Scene.auxiliaryEntities.add = origAddAux;
139
+ DesignCore.Scene.previewEntities.create = origCreateTempItem;
133
140
  DesignCore.Mouse.pointOnScene = origPointOnScene;
134
141
  });
135
142
 
@@ -204,7 +211,7 @@ AcDbAlignedDimension
204
211
  expect(file.contents).toEqual(dxfString);
205
212
 
206
213
  // create new entity from entity data to ensure all props are loaded
207
- const newDimension = new AlignedDimension(dimension);
214
+ const newDimension = new AlignedDimension({ handle: dimension.getProperty('handle'), points: dimension.points, ...dimension });
208
215
  file = new File();
209
216
  newDimension.dxf(file);
210
217
  expect(file.contents).toEqual(dxfString);
@@ -69,9 +69,9 @@ test.each(scenarios)('Dimension.execute handles $desc', async (scenario) => {
69
69
  for (const entity of dimensionBlockEntities) {
70
70
  if (entity.type === 'Text') {
71
71
  // remove all non-numeric characters except . and -
72
- const numbersOnly = entity.string.replace(/[^0-9.-]+/g, '');
72
+ const numbersOnly = entity.getProperty('string').replace(/[^0-9.-]+/g, '');
73
73
  expect(Number(numbersOnly)).toBeCloseTo(dimensionValue);
74
- expect(entity.string).toContain('°');
74
+ expect(entity.getProperty('string')).toContain('°');
75
75
  }
76
76
  }
77
77
  }, { selectedItems });
@@ -100,11 +100,11 @@ test('getPointsFromSelection returns correct sequenced points', () => {
100
100
  });
101
101
 
102
102
  test('AngularDimension.preview runs without error and calls createTempItem', () => {
103
- const origCreateTempItem = DesignCore.Scene.tempEntities.create;
103
+ const origCreateTempItem = DesignCore.Scene.previewEntities.create;
104
104
  const origPointOnScene = DesignCore.Mouse.pointOnScene;
105
105
  // Manual mock for createTempItem
106
106
  const createTempItemCalls = [];
107
- DesignCore.Scene.tempEntities.create = function(type, obj) {
107
+ DesignCore.Scene.previewEntities.create = function(type, obj) {
108
108
  createTempItemCalls.push([type, obj]);
109
109
  };
110
110
  // Manual mock for pointOnScene
@@ -138,7 +138,7 @@ test('AngularDimension.preview runs without error and calls createTempItem', ()
138
138
  expect(createTempItemCalls.some((call) => call[0] === dim2.type)).toBe(true);
139
139
 
140
140
  // Restore
141
- DesignCore.Scene.tempEntities.create = origCreateTempItem;
141
+ DesignCore.Scene.previewEntities.create = origCreateTempItem;
142
142
  DesignCore.Mouse.pointOnScene = origPointOnScene;
143
143
  });
144
144
 
@@ -234,7 +234,7 @@ AcDb2LineAngularDimension
234
234
  expect(file.contents).toEqual(dxfString);
235
235
 
236
236
  // create new entity from entity data to ensure all props are loaded
237
- const newDimension = new AngularDimension(dimension);
237
+ const newDimension = new AngularDimension({ handle: dimension.getProperty('handle'), points: dimension.points, ...dimension });
238
238
  file = new File();
239
239
  newDimension.dxf(file);
240
240
  expect(file.contents).toEqual(dxfString);
@@ -2,6 +2,8 @@ import { BaseDimension } from '../../core/dimensions/baseDimension.js';
2
2
  import { Point } from '../../core/entities/point.js';
3
3
  import { Core } from '../../core/core/core.js';
4
4
  import { Text } from '../../core/entities/text.js';
5
+ import { Line } from '../../core/entities/line.js';
6
+ import { jest } from '@jest/globals';
5
7
  // import { DimType } from '../../core/properties/dimType.js';
6
8
 
7
9
  // initialise core
@@ -16,11 +18,11 @@ describe('BaseDimension', () => {
16
18
  });
17
19
 
18
20
  test('constructor sets default properties', () => {
19
- expect(baseDim.textOverride).toBe('');
20
- expect(baseDim.blockName).toBe('');
21
- expect(baseDim.dimensionStyle).toBe('STANDARD');
22
- expect(baseDim.leaderLength).toBe(0);
23
- expect(baseDim.linearDimAngle).toBe(0);
21
+ expect(baseDim.getProperty('textOverride')).toBe('');
22
+ expect(baseDim.getProperty('blockName')).toBe('');
23
+ expect(baseDim.getProperty('dimensionStyle')).toBe('STANDARD');
24
+ expect(baseDim.getProperty('leaderLength')).toBe(0);
25
+ expect(baseDim.getProperty('linearDimAngle')).toBe(0);
24
26
  });
25
27
 
26
28
  test('getDimensionValue formats values by dimension type', () => {
@@ -166,17 +168,17 @@ describe('BaseDimension', () => {
166
168
  });
167
169
 
168
170
  test('getDimensionValue uses textOverride', () => {
169
- baseDim.textOverride = 'foo <> bar';
171
+ baseDim.setProperty('textOverride', 'foo <> bar');
170
172
  expect(baseDim.getDimensionValue(10)).toBe('foo 10.0000 bar');
171
173
  });
172
174
 
173
175
  test('getDimensionText returns a Text object with correct values', () => {
174
176
  const text = baseDim.getDimensionText(10, new Point(1, 2), Math.PI / 4);
175
177
  expect(text).toBeInstanceOf(Text);
176
- expect(text.height).toBe(0.18);
178
+ expect(text.getProperty('height')).toBe(0.18);
177
179
  expect(text.points[0].x).toBe(1);
178
180
  expect(text.points[0].y).toBe(2);
179
- expect(typeof text.string).toBe('string');
181
+ expect(typeof text.getProperty('string')).toBe('string');
180
182
  });
181
183
 
182
184
  test('getTextDirection flips angle in lower half', () => {
@@ -236,4 +238,63 @@ describe('BaseDimension', () => {
236
238
  baseDim.block = { touched: () => false };
237
239
  expect(baseDim.touched({})).toBe(false);
238
240
  });
241
+
242
+ describe('draw()', () => {
243
+ /**
244
+ * Return a BaseDimension with a stubbed buildDimension() that produces one Line.
245
+ * @return {BaseDimension}
246
+ */
247
+ function makeDim() {
248
+ const dim = new BaseDimension({});
249
+ dim.points = [new Point(0, 0), new Point(10, 0)];
250
+ dim.buildDimension = jest.fn(() => {
251
+ return [new Line({ points: [new Point(0, 0), new Point(10, 0)] })];
252
+ });
253
+ return dim;
254
+ }
255
+
256
+ test('draw() returns the block items array', () => {
257
+ const dim = makeDim();
258
+ const result = dim.draw({});
259
+ expect(result).toBe(dim.block.items);
260
+ });
261
+
262
+ test('draw() calls refresh() (via buildDimension) when block is empty', () => {
263
+ const dim = makeDim();
264
+ expect(dim.block.items).toHaveLength(0);
265
+ dim.draw({});
266
+ expect(dim.buildDimension).toHaveBeenCalledTimes(1);
267
+ expect(dim.block.items.length).toBeGreaterThan(0);
268
+ });
269
+
270
+ test('draw() does not call refresh() when block already has items', () => {
271
+ const dim = makeDim();
272
+ // pre-populate the block
273
+ dim.block.addItem(new Line({ points: [new Point(0, 0), new Point(1, 1)] }));
274
+ dim.draw({});
275
+ expect(dim.buildDimension).not.toHaveBeenCalled();
276
+ });
277
+
278
+ test('draw() returns populated items after initial refresh', () => {
279
+ const dim = makeDim();
280
+ const items = dim.draw({});
281
+ expect(items).toBeInstanceOf(Array);
282
+ expect(items.length).toBeGreaterThan(0);
283
+ });
284
+
285
+ test('draw() returns same array reference on repeated calls', () => {
286
+ const dim = makeDim();
287
+ const first = dim.draw({});
288
+ const second = dim.draw({});
289
+ expect(first).toBe(second);
290
+ });
291
+
292
+ test('draw() block items have BYBLOCK colour after refresh', () => {
293
+ const dim = makeDim();
294
+ dim.draw({});
295
+ for (const item of dim.block.items) {
296
+ expect(item.entityColour.aci).toBe(0); // BYBLOCK = ACI 0
297
+ }
298
+ });
299
+ });
239
300
  });
@@ -0,0 +1,62 @@
1
+ import { AlignedDimension } from '../../core/dimensions/alignedDimension.js';
2
+ import { BaseLinearDimension } from '../../core/dimensions/baseLinearDimension.js';
3
+ import { Point } from '../../core/entities/point.js';
4
+ import { DesignCore } from '../../core/designCore.js';
5
+ import { Core } from '../../core/core/core.js';
6
+
7
+ // initialise core
8
+ new Core();
9
+
10
+ // ─── constructor ─────────────────────────────────────────────────────────────
11
+
12
+ test('BaseLinearDimension can be instantiated', () => {
13
+ const dim = new BaseLinearDimension();
14
+ expect(dim).toBeInstanceOf(BaseLinearDimension);
15
+ expect(dim.points).toBeDefined();
16
+ expect(Array.isArray(dim.points)).toBe(true);
17
+ });
18
+
19
+ // ─── preview ─────────────────────────────────────────────────────────────────
20
+
21
+ test('BaseLinearDimension.preview - 0 points does nothing', () => {
22
+ const dim = new AlignedDimension();
23
+ DesignCore.Scene.auxiliaryEntities.clear();
24
+ DesignCore.Scene.previewEntities.clear();
25
+
26
+ expect(() => dim.preview()).not.toThrow();
27
+ expect(DesignCore.Scene.auxiliaryEntities.count()).toBe(0);
28
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
29
+ });
30
+
31
+ test('BaseLinearDimension.preview - 1 point adds RubberBand to auxiliaryEntities', () => {
32
+ const dim = new AlignedDimension();
33
+ dim.points = [new Point(0, 0)];
34
+
35
+ const origAdd = DesignCore.Scene.auxiliaryEntities.add;
36
+ const added = [];
37
+ DesignCore.Scene.auxiliaryEntities.add = (item) => added.push(item);
38
+ DesignCore.Scene.previewEntities.clear();
39
+
40
+ expect(() => dim.preview()).not.toThrow();
41
+ expect(added.length).toBeGreaterThanOrEqual(1);
42
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
43
+
44
+ DesignCore.Scene.auxiliaryEntities.add = origAdd;
45
+ });
46
+
47
+ test('BaseLinearDimension.preview - >1 points creates temp entity', () => {
48
+ const dim = new AlignedDimension();
49
+ dim.points = [new Point(0, 0), new Point(10, 0)];
50
+
51
+ const origCreate = DesignCore.Scene.previewEntities.create;
52
+ const created = [];
53
+ DesignCore.Scene.previewEntities.create = (type, data) => created.push({ type, data });
54
+ DesignCore.Scene.auxiliaryEntities.clear();
55
+
56
+ expect(() => dim.preview()).not.toThrow();
57
+ expect(created.length).toBeGreaterThanOrEqual(1);
58
+ expect(created[0].type).toBe(dim.type);
59
+ expect(DesignCore.Scene.auxiliaryEntities.count()).toBe(0);
60
+
61
+ DesignCore.Scene.previewEntities.create = origCreate;
62
+ });