@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,5 +1,5 @@
1
1
  import { Line } from '../entities/line.js';
2
- import { BasePolyline } from '../entities/basePolyline.js';
2
+ import { PolylineBase } from '../entities/polylineBase.js';
3
3
 
4
4
  import { DesignCore } from '../designCore.js';
5
5
 
@@ -61,7 +61,7 @@ export class CornerEntity {
61
61
  setPick(entity, clickPoint, arcSegmentErrorMsg) {
62
62
  this.entity = entity;
63
63
  this.clickPoint = clickPoint;
64
- if (!(this.entity instanceof BasePolyline)) {
64
+ if (!(this.entity instanceof PolylineBase)) {
65
65
  this.segment = null;
66
66
  this.segmentIndex = null;
67
67
  return true;
@@ -53,11 +53,11 @@ export class Cutclip extends Tool {
53
53
  * Perform the command
54
54
  */
55
55
  action() {
56
- // generate clipboard data from selected items
57
- const selectedItems = DesignCore.Scene.selectionManager.selectedItems;
56
+ // generate clipboard data from selected entities
57
+ const selectedEntities = DesignCore.Scene.selectionManager.selectedEntities;
58
58
 
59
59
  // create clipboard data
60
- DesignCore.Clipboard.Entities = selectedItems;
60
+ DesignCore.Clipboard.Entities = selectedEntities;
61
61
  // get a copy of the selection set
62
62
  const selections = DesignCore.Scene.selectionManager.selectionSet.selectionSet.slice();
63
63
  // create state changes for each of the selections
@@ -83,23 +83,23 @@ export class Explode extends Tool {
83
83
 
84
84
  // check the insert has a block and
85
85
  // check the block has items
86
- if (insert.block === undefined || insert.block.items.length == 0) {
86
+ if (insert.block === undefined || insert.block.entities.length === 0) {
87
87
  counter++;
88
88
  continue;
89
89
  }
90
90
 
91
91
  const insertPoint = insert.points[0];
92
92
  const block = insert.block;
93
- const blockItems = block.items;
93
+ const blockItems = block.entities;
94
94
 
95
- const rotation = Utils.degrees2radians(insert.rotation);
95
+ const rotation = Utils.degrees2radians(insert.getProperty('rotation'));
96
96
  const origin = new Point(0, 0);
97
97
 
98
98
  blockItems.forEach((blockItem) => {
99
- const copyofitem = Utils.cloneObject(blockItem);
100
- const points = copyofitem.points.map((p) => new Point(p.x, p.y, p.bulge, p.sequence).rotate(origin, rotation).add(insertPoint));
101
- copyofitem.setProperty('points', points);
102
- const stateChange = new AddState(copyofitem);
99
+ const copyOfItem = Utils.cloneObject(blockItem);
100
+ const points = copyOfItem.points.map((p) => new Point(p.x, p.y, p.bulge, p.sequence).rotate(origin, rotation).add(insertPoint));
101
+ copyOfItem.points = points;
102
+ const stateChange = new AddState(copyOfItem);
103
103
  stateChanges.push(stateChange);
104
104
  });
105
105
 
@@ -3,6 +3,9 @@ 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 { UpdateState } from '../lib/stateManager.js';
7
+ import { Utils } from '../lib/utils.js';
8
+ import { PolylineUtils } from '../lib/polylineUtils.js';
6
9
 
7
10
  import { DesignCore } from '../designCore.js';
8
11
 
@@ -16,7 +19,7 @@ export class Extend extends Tool {
16
19
  /** Create an Extend command */
17
20
  constructor() {
18
21
  super();
19
- this.selectedItem = null;
22
+ this.selectedEntity = null;
20
23
  this.selectedBoundaryItems = [];
21
24
  }
22
25
 
@@ -45,7 +48,7 @@ export class Extend extends Tool {
45
48
  if (boundary === undefined) return;
46
49
  }
47
50
 
48
- // add all selected items to boundary items
51
+ // add all selected entities to boundary items
49
52
  for (let i = 0; i < DesignCore.Scene.selectionManager.selectionSet.selectionSet.length; i++) {
50
53
  const boundaryItem = DesignCore.Scene.entities.get(DesignCore.Scene.selectionManager.selectionSet.selectionSet[i]);
51
54
  this.selectedBoundaryItems.push(boundaryItem);
@@ -55,7 +58,7 @@ export class Extend extends Tool {
55
58
  while (true) {
56
59
  const selection = await DesignCore.Scene.inputManager.requestInput(op2);
57
60
  if (selection === undefined) break;
58
- this.selectedItem = DesignCore.Scene.entities.get(selection.selectedItemIndex);
61
+ this.selectedEntity = DesignCore.Scene.entities.get(selection.selectedEntityIndex);
59
62
  DesignCore.Scene.selectionManager.removeLastSelection();
60
63
  DesignCore.Scene.inputManager.actionCommand();
61
64
  }
@@ -70,44 +73,154 @@ export class Extend extends Tool {
70
73
  * Preview the command during execution
71
74
  */
72
75
  preview() {
73
- // No Preview
76
+ if (!this.selectedBoundaryItems.length) return;
77
+
78
+ const index = DesignCore.Scene.selectionManager.findClosestItem(DesignCore.Mouse.pointOnScene());
79
+ if (index === undefined) return;
80
+
81
+ const entity = DesignCore.Scene.entities.get(index);
82
+ if (typeof entity.toPolylinePoints !== 'function' || typeof entity.fromPolylinePoints !== 'function') return;
83
+ const intersectPoints = this.#collectIntersectPoints(entity);
84
+ if (!intersectPoints.length) return;
85
+
86
+ const stateChanges = this.#extendEntity(entity, intersectPoints);
87
+ if (!stateChanges?.length) return;
88
+
89
+ for (const change of stateChanges) {
90
+ if (change instanceof UpdateState) {
91
+ const extended = Utils.cloneObject(entity);
92
+ extended.setProperty('points', change.properties.points);
93
+ DesignCore.Scene.previewEntities.add(extended);
94
+ }
95
+ }
74
96
  }
75
97
 
76
98
  /**
77
99
  * Perform the command
78
100
  */
79
101
  action() {
80
- 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(), true);
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
- }
102
+ if (this.selectedEntity && this.selectedBoundaryItems.length) {
103
+ if (typeof this.selectedEntity.fromPolylinePoints !== 'function') {
104
+ DesignCore.Core.notify(`${this.type} - ${this.selectedEntity.type} ${Strings.Message.NOEXTEND}`);
105
+ this.selectedEntity = null;
106
+ return;
97
107
  }
98
108
 
109
+ const intersectPoints = this.#collectIntersectPoints(this.selectedEntity);
110
+
99
111
  if (intersectPoints.length) {
100
- const stateChanges = this.selectedItem.extend(intersectPoints);
112
+ const stateChanges = this.#extendEntity(this.selectedEntity, intersectPoints);
101
113
  if (stateChanges?.length) {
102
114
  DesignCore.Scene.commit(stateChanges);
115
+ } else {
116
+ DesignCore.Core.notify(`${this.type} - ${this.selectedEntity.type} ${Strings.Message.NOEXTEND}`);
103
117
  }
104
118
  } else {
105
- DesignCore.Core.notify(`${this.type} - ${this.selectedItem.type} ${Strings.Message.NOEXTEND}`);
119
+ DesignCore.Core.notify(`${this.type} - ${this.selectedEntity.type} ${Strings.Message.NOEXTEND}`);
120
+ }
121
+ }
122
+
123
+ this.selectedEntity = null;
124
+ }
125
+
126
+ /**
127
+ * Collect all intersection points between the boundary items and the given entity.
128
+ * Unsupported entity combinations are silently skipped.
129
+ * @param {Object} entity
130
+ * @return {Array}
131
+ */
132
+ #collectIntersectPoints(entity) {
133
+ const intersectPoints = [];
134
+ for (const boundaryItem of this.selectedBoundaryItems) {
135
+ if (boundaryItem === entity) continue;
136
+ try {
137
+ const intersect = Intersection.intersectPolylinePolyline(boundaryItem.toPolylinePoints(), entity.toPolylinePoints(), true);
138
+ intersectPoints.push(...intersect.points);
139
+ } catch {
140
+ // skip unsupported entity combinations
141
+ }
142
+ }
143
+ return intersectPoints;
144
+ }
145
+
146
+ /**
147
+ * Compute a state change to extend entity to the nearest valid boundary
148
+ * intersection. Operates entirely in polyline-point space via
149
+ * entity.toPolylinePoints() / entity.fromPolylinePoints(), so it works for
150
+ * any entity that implements that protocol (Line, PolylineBase, and future
151
+ * entities such as Spline or Ellipse).
152
+ * Only straight end-segments (bulge === 0) can be extended.
153
+ * @param {Object} entity
154
+ * @param {Array} intersectPoints
155
+ * @return {Array}
156
+ */
157
+ #extendEntity(entity, intersectPoints) {
158
+ if (!intersectPoints?.length) return [];
159
+
160
+ const polyPoints = entity.toPolylinePoints();
161
+
162
+ // Closed entities have no open endpoints to extend
163
+ if (polyPoints[0].isSame(polyPoints.at(-1))) return [];
164
+
165
+ const mousePosition = DesignCore.Mouse.pointOnScene();
166
+ const lastIndex = polyPoints.length - 1;
167
+
168
+ // Determine which end segment is closest to the mouse
169
+ const closestOnFirst = PolylineUtils.closestPointOnSegment(polyPoints, mousePosition, 1);
170
+ const closestOnLast = PolylineUtils.closestPointOnSegment(polyPoints, mousePosition, lastIndex);
171
+ const distToFirst = mousePosition.distance(closestOnFirst);
172
+ const distToLast = mousePosition.distance(closestOnLast);
173
+
174
+ let endPointIndex;
175
+ let endSegmentBulge;
176
+ if (distToFirst < distToLast) {
177
+ endPointIndex = 0;
178
+ endSegmentBulge = polyPoints[0].bulge ?? 0;
179
+ } else if (distToLast < distToFirst) {
180
+ endPointIndex = lastIndex;
181
+ endSegmentBulge = polyPoints[lastIndex - 1].bulge ?? 0;
182
+ } else {
183
+ if (mousePosition.distance(polyPoints[0]) <= mousePosition.distance(polyPoints[lastIndex])) {
184
+ endPointIndex = 0;
185
+ endSegmentBulge = polyPoints[0].bulge ?? 0;
186
+ } else {
187
+ endPointIndex = lastIndex;
188
+ endSegmentBulge = polyPoints[lastIndex - 1].bulge ?? 0;
106
189
  }
107
190
  }
108
191
 
109
- // reset selected item
110
- this.selectedItem = null;
192
+ // Only straight end-segments can be extended
193
+ if (endSegmentBulge !== 0) return [];
194
+
195
+ const endPoint = polyPoints[endPointIndex];
196
+ const adjacentPoint = endPointIndex === 0 ? polyPoints[1] : polyPoints[lastIndex - 1];
197
+
198
+ Utils.sortPointsByDistance(intersectPoints, endPoint);
199
+
200
+ const direction = endPoint.subtract(adjacentPoint);
201
+ const newEndPoint = intersectPoints.find((p) => {
202
+ // Ignore the current end point itself
203
+ if (p.isSame(endPoint)) return false;
204
+ // Point must be ahead of the end point in the extension direction
205
+ if (p.subtract(endPoint).dot(direction) <= 0) return false;
206
+ // Point must be further from the adjacent point than the end point is (i.e. actually extends the segment)
207
+ if (adjacentPoint.distance(p) <= adjacentPoint.distance(endPoint)) return false;
208
+ // Point must lie on the extension line of the end segment; intersections from internal
209
+ // segments extended as infinite lines will have a non-zero cross product with direction
210
+ if (Utils.round(direction.cross(p.subtract(adjacentPoint))) !== 0) return false;
211
+ return true;
212
+ });
213
+
214
+ if (!newEndPoint) return [];
215
+
216
+ const newPolyPoints = polyPoints.map((p) => p.clone());
217
+ newPolyPoints[endPointIndex] = newEndPoint.clone();
218
+
219
+ if (newPolyPoints[endPointIndex].isSame(polyPoints[endPointIndex])) return [];
220
+
221
+ const clone = Utils.cloneObject(entity);
222
+ const output = clone.fromPolylinePoints(newPolyPoints);
223
+ return [new UpdateState(entity, { points: output.points })];
111
224
  }
112
225
  }
113
226
 
@@ -3,9 +3,11 @@ import { ChamferFilletBase } from './chamferFilletBase.js';
3
3
  import { Input, PromptOptions } from '../lib/inputManager.js';
4
4
  import { Logging } from '../lib/logging.js';
5
5
  import { Constants } from '../lib/constants.js';
6
+ import { Utils } from '../lib/utils.js';
6
7
  import { Line } from '../entities/line.js';
7
- import { BasePolyline } from '../entities/basePolyline.js';
8
- import { AddState, RemoveState, UpdateState } from '../lib/stateManager.js';
8
+ import { PolylineBase } from '../entities/polylineBase.js';
9
+ import { AddState } from '../lib/stateManager.js';
10
+ import { Colour } from '../lib/colour.js';
9
11
 
10
12
  import { DesignCore } from '../designCore.js';
11
13
 
@@ -63,9 +65,9 @@ export class Fillet extends ChamferFilletBase {
63
65
  }
64
66
 
65
67
  // input1 is a selection — validate and store first entity
66
- const firstEntity = DesignCore.Scene.entities.get(input1.selectedItemIndex);
68
+ const firstEntity = DesignCore.Scene.entities.get(input1.selectedEntityIndex);
67
69
  DesignCore.Scene.selectionManager.removeLastSelection();
68
- if (!(firstEntity instanceof Line) && !(firstEntity instanceof BasePolyline)) {
70
+ if (!(firstEntity instanceof Line) && !(firstEntity instanceof PolylineBase)) {
69
71
  DesignCore.Core.notify(`${this.type} - ${firstEntity.type} ${Strings.Message.NOFILLET}`);
70
72
  continue;
71
73
  }
@@ -77,16 +79,16 @@ export class Fillet extends ChamferFilletBase {
77
79
  const input2 = await DesignCore.Scene.inputManager.requestInput(op2);
78
80
  if (input2 === undefined) return;
79
81
 
80
- const candidate = DesignCore.Scene.entities.get(input2.selectedItemIndex);
82
+ const candidate = DesignCore.Scene.entities.get(input2.selectedEntityIndex);
81
83
  DesignCore.Scene.selectionManager.removeLastSelection();
82
- if (!(candidate instanceof Line) && !(candidate instanceof BasePolyline)) {
84
+ if (!(candidate instanceof Line) && !(candidate instanceof PolylineBase)) {
83
85
  DesignCore.Core.notify(`${this.type} - ${candidate.type} ${Strings.Message.NOFILLET}`);
84
86
  continue;
85
87
  }
86
88
  if (!this.secondPick.setPick(candidate, input2.selectedPoint, `${Strings.Strings.ARC} - ${Strings.Message.NOFILLET}`)) continue;
87
89
  // If both selections are the same polyline, segments must be consecutive
88
90
  // or they must be the open-end segments of an open polyline.
89
- if (candidate === firstEntity && firstEntity instanceof BasePolyline) {
91
+ if (candidate === firstEntity && firstEntity instanceof PolylineBase) {
90
92
  const isConsecutive = firstEntity.areConsecutiveSegments(this.firstPick.segmentIndex, this.secondPick.segmentIndex);
91
93
  const lastIdx = firstEntity.points.length - 1;
92
94
  const isOpenEnds = !firstEntity.flags.hasFlag(1) &&
@@ -113,7 +115,43 @@ export class Fillet extends ChamferFilletBase {
113
115
  * Preview the command during execution
114
116
  */
115
117
  preview() {
116
- // No preview
118
+ const selection = this.validateSelection();
119
+ if (!selection) return;
120
+
121
+ const filletRadius = DesignCore.Scene.headers.filletRadius;
122
+ const trimMode = DesignCore.Scene.headers.trimMode;
123
+
124
+ const geo = this.#computeFillet(this.firstPick, selection.tempSecond, selection.intersectionPoint, filletRadius);
125
+ if (!geo) return;
126
+ if (trimMode && !geo.tangentsInBounds) return;
127
+
128
+ if (!trimMode) {
129
+ if (geo.arc) DesignCore.Scene.previewEntities.add(geo.arc);
130
+ return;
131
+ }
132
+
133
+ // trimMode: dull the original segments then show the shortened versions + arc
134
+ const firstSeg = Utils.cloneObject(this.firstPick.activeSeg);
135
+ firstSeg.setProperty('colour', Colour.blend(this.firstPick.entity.getDrawColour(), DesignCore.Settings.canvasbackgroundcolour, DesignCore.Settings.previewBlendFactor));
136
+ DesignCore.Scene.previewEntities.add(firstSeg);
137
+
138
+ const secondSeg = Utils.cloneObject(selection.tempSecond.activeSeg);
139
+ secondSeg.setProperty('colour', Colour.blend(selection.tempSecond.entity.getDrawColour(), DesignCore.Settings.canvasbackgroundcolour, DesignCore.Settings.previewBlendFactor));
140
+ DesignCore.Scene.previewEntities.add(secondSeg);
141
+
142
+ const firstTrimLine = DesignCore.CommandManager.createNew('Line', {
143
+ points: [this.firstPick.lineKeptEnd(selection.intersectionPoint), geo.firstTangentPoint],
144
+ ...Utils.cloneProperties(this.firstPick.entity),
145
+ });
146
+ DesignCore.Scene.previewEntities.add(firstTrimLine);
147
+
148
+ const secondTrimLine = DesignCore.CommandManager.createNew('Line', {
149
+ points: [selection.tempSecond.lineKeptEnd(selection.intersectionPoint), geo.secondTangentPoint],
150
+ ...Utils.cloneProperties(selection.candidate),
151
+ });
152
+ DesignCore.Scene.previewEntities.add(secondTrimLine);
153
+
154
+ if (geo.arc) DesignCore.Scene.previewEntities.add(geo.arc);
117
155
  }
118
156
 
119
157
  /**
@@ -126,205 +164,70 @@ export class Fillet extends ChamferFilletBase {
126
164
  // radius = 0: trim/extend both lines to the sharp intersection with no arc
127
165
  const filletRadius = DesignCore.Scene.headers.filletRadius;
128
166
  const trimMode = DesignCore.Scene.headers.trimMode;
129
- if (filletRadius === 0) {
130
- if (trimMode) {
131
- const stateChanges = this.applySharpTrim();
132
- DesignCore.Scene.commit(stateChanges);
133
- }
134
- return;
135
- }
136
-
137
- // Angle between the two clicked-side direction vectors (i.e. the opening angle of the chosen corner)
138
- const firstUnit = this.firstPick.clickUnit(this.intersectionPoint);
139
- const secondUnit = this.secondPick.clickUnit(this.intersectionPoint);
140
- const cosAngle = Math.min(1, Math.max(-1, firstUnit.dot(secondUnit)));
141
- const cornerAngle = Math.acos(cosAngle);
142
-
143
- // Collinear or antiparallel lines cannot be filleted
144
- if (cornerAngle < Constants.Tolerance.EPSILON || cornerAngle > Math.PI - Constants.Tolerance.EPSILON) {
167
+ const geo = this.#computeFillet(this.firstPick, this.secondPick, this.intersectionPoint, filletRadius);
168
+ if (!geo) {
145
169
  DesignCore.Core.notify(`${this.type} - ${Strings.Error.ERROR}:${Strings.Error.PARALLELLINES}`);
146
170
  return;
147
171
  }
148
172
 
149
- // Bisector unit vector: points into the chosen corner toward the fillet centre
150
- const bisectorUnit = firstUnit.add(secondUnit).normalise();
151
-
152
- // Distance from the intersection point to the fillet centre along the bisector
153
- const intersectToCentreDistance = filletRadius / Math.sin(cornerAngle / 2);
154
-
155
- // Fillet arc centre point
156
- const arcCentre = this.intersectionPoint.add(bisectorUnit.scale(intersectToCentreDistance));
157
-
158
- // Tangent points where the fillet arc meets each line (foot of perpendicular from centre to line)
159
- const firstTangentPoint = arcCentre.perpendicular(this.firstPick.lineStart, this.firstPick.lineEnd);
160
- const secondTangentPoint = arcCentre.perpendicular(this.secondPick.lineStart, this.secondPick.lineEnd);
161
-
162
- // Verify the tangent points are reachable — same direction as the kept end from the
163
- // intersection and not farther than the kept end (which would flip the line direction).
164
- if (trimMode) {
165
- const firstKeptDir = this.firstPick.lineKeptEnd(this.intersectionPoint).subtract(this.intersectionPoint);
166
- const secondKeptDir = this.secondPick.lineKeptEnd(this.intersectionPoint).subtract(this.intersectionPoint);
167
- const firstTangentDot = firstTangentPoint.subtract(this.intersectionPoint).dot(firstKeptDir);
168
- const secondTangentDot = secondTangentPoint.subtract(this.intersectionPoint).dot(secondKeptDir);
169
- if (firstTangentDot < 0 || firstTangentDot > firstKeptDir.dot(firstKeptDir) ||
170
- secondTangentDot < 0 || secondTangentDot > secondKeptDir.dot(secondKeptDir)) {
171
- DesignCore.Core.notify(`${this.type} - ${Strings.Error.ERROR}:${Strings.Error.RADIUSTOOLARGE}`);
172
- return;
173
- }
173
+ if (trimMode && !geo.tangentsInBounds) {
174
+ DesignCore.Core.notify(`${this.type} - ${Strings.Error.ERROR}:${Strings.Error.RADIUSTOOLARGE}`);
175
+ return;
174
176
  }
175
177
 
176
- // Determine arc winding direction: if the cross product of the two centre-to-tangent
177
- // vectors is positive, the short arc from firstTangentPoint to secondTangentPoint
178
- // travels CCW (direction = 1), otherwise CW (direction = -1).
179
- const centreToFirst = firstTangentPoint.subtract(arcCentre);
180
- const centreToSecond = secondTangentPoint.subtract(arcCentre);
181
- const windingCross = centreToFirst.cross(centreToSecond);
182
- const arcDirection = windingCross > 0 ? 1 : -1;
183
-
184
- const arc = DesignCore.CommandManager.createNew('Arc', {
185
- points: [arcCentre, firstTangentPoint, secondTangentPoint],
186
- direction: arcDirection,
187
- });
188
-
189
- const firstIsPolyline = this.firstPick.entity instanceof BasePolyline;
190
- const secondIsPolyline = this.secondPick.entity instanceof BasePolyline;
191
-
192
178
  if (!trimMode) {
193
- DesignCore.Scene.commit([new AddState(arc)]);
179
+ if (geo.arc) DesignCore.Scene.commit([new AddState(geo.arc)]);
194
180
  return;
195
181
  }
196
182
 
197
- if (!firstIsPolyline && !secondIsPolyline) {
198
- const stateChanges = this.#trimLineAndLine(firstTangentPoint, secondTangentPoint, arc);
199
- DesignCore.Scene.commit(stateChanges);
200
- } else if (firstIsPolyline && secondIsPolyline && this.firstPick.entity === this.secondPick.entity) {
201
- const stateChanges = this.#trimPolyAndPoly(firstTangentPoint, secondTangentPoint, arcDirection, arcCentre, arc);
202
- DesignCore.Scene.commit(stateChanges);
203
- } else {
204
- const stateChanges = this.#trimLineAndPoly(firstTangentPoint, secondTangentPoint, arcDirection, arcCentre);
205
- DesignCore.Scene.commit(stateChanges);
206
- }
183
+ const [firstCornerPoint, secondCornerPoint] = geo.arc ?
184
+ geo.arc.toPolylinePoints() :
185
+ [this.intersectionPoint, this.intersectionPoint];
186
+ const stateChanges = this.applyCornerTrim(firstCornerPoint, secondCornerPoint, geo.arc ?? null);
187
+ DesignCore.Scene.commit(stateChanges);
207
188
  }
208
189
 
209
190
  /**
210
- * Apply trim and arc for a Line + Line fillet.
211
- * @param {Point} firstTangentPoint
212
- * @param {Point} secondTangentPoint
213
- * @param {Arc} arc
214
- * @return {Array}
191
+ * Compute the fillet geometry for two picks meeting at intersectionPoint.
192
+ * Returns null if the lines are collinear or antiparallel.
193
+ * When filletRadius is 0, returns arc: null with both tangent points at the intersection.
194
+ * @param {CornerEntity} firstPick
195
+ * @param {CornerEntity} secondPick
196
+ * @param {Point} intersectionPoint
197
+ * @param {number} filletRadius
198
+ * @return {{arc, firstTangentPoint, secondTangentPoint, tangentsInBounds}|null}
215
199
  */
216
- #trimLineAndLine(firstTangentPoint, secondTangentPoint, arc) {
217
- return [
218
- new AddState(arc),
219
- new UpdateState(this.firstPick.entity, { points: [this.firstPick.lineKeptEnd(this.intersectionPoint), firstTangentPoint] }),
220
- new UpdateState(this.secondPick.entity, { points: [this.secondPick.lineKeptEnd(this.intersectionPoint), secondTangentPoint] }),
221
- ];
222
- }
223
-
224
- /**
225
- * Apply trim and arc for a Polyline + Polyline (same entity) fillet.
226
- * Handles both the open-ends case and the consecutive-segments case.
227
- * @param {Point} firstTangentPoint
228
- * @param {Point} secondTangentPoint
229
- * @param {number} arcDirection
230
- * @param {Point} arcCentre
231
- * @param {Arc} arc
232
- * @return {Array}
233
- */
234
- #trimPolyAndPoly(firstTangentPoint, secondTangentPoint, arcDirection, arcCentre, arc) {
235
- const closeSegIdx = this.firstPick.entity.points.length;
236
- const lastIdx = closeSegIdx - 1;
237
- const segDiff = Math.abs(this.firstPick.segmentIndex - this.secondPick.segmentIndex);
238
- const isOpenEnds = !this.firstPick.entity.flags.hasFlag(1) && segDiff !== 1 && (
239
- (this.firstPick.segmentIndex === 1 && this.secondPick.segmentIndex === lastIdx) ||
240
- (this.firstPick.segmentIndex === lastIdx && this.secondPick.segmentIndex === 1)
241
- );
242
- const newPoints = this.firstPick.entity.points.map((p) => p.clone());
243
- const stateChanges = [];
244
-
245
- if (isOpenEnds) {
246
- stateChanges.push(new AddState(arc));
247
- if (this.firstPick.segmentIndex === 1) {
248
- newPoints[0] = firstTangentPoint.clone();
249
- newPoints[lastIdx] = secondTangentPoint.clone();
250
- } else {
251
- newPoints[0] = secondTangentPoint.clone();
252
- newPoints[lastIdx] = firstTangentPoint.clone();
253
- }
254
- } else {
255
- const seg1 = this.firstPick.segmentIndex;
256
- const seg2 = this.secondPick.segmentIndex;
257
- const isClosingWrap = (seg1 === closeSegIdx && seg2 === 1) || (seg2 === closeSegIdx && seg1 === 1);
258
- const isFirstLower = seg1 < seg2;
259
- const cornerIdx = isClosingWrap ? 0 : Math.min(seg1, seg2);
260
- const lowerTangent = (isClosingWrap ? seg1 !== closeSegIdx : !isFirstLower) ? secondTangentPoint : firstTangentPoint;
261
- const upperTangent = (isClosingWrap ? seg1 !== closeSegIdx : !isFirstLower) ? firstTangentPoint : secondTangentPoint;
262
- const polyArcDir = (isClosingWrap ? seg1 !== closeSegIdx : !isFirstLower) ? -arcDirection : arcDirection;
263
- const startAngle = arcCentre.angle(lowerTangent);
264
- const endAngle = arcCentre.angle(upperTangent);
265
- const includedAngle = ((endAngle - startAngle) * polyArcDir + 4 * Math.PI) % (2 * Math.PI);
266
- const bulge = polyArcDir * Math.tan(includedAngle / 4);
267
- const arcStartPoint = lowerTangent.clone();
268
- arcStartPoint.bulge = bulge;
269
- newPoints.splice(cornerIdx, 1, arcStartPoint, upperTangent.clone());
200
+ #computeFillet(firstPick, secondPick, intersectionPoint, filletRadius) {
201
+ if (filletRadius === 0) {
202
+ return { arc: null, firstTangentPoint: intersectionPoint, secondTangentPoint: intersectionPoint, tangentsInBounds: true };
270
203
  }
271
204
 
272
- stateChanges.push(new UpdateState(this.firstPick.entity, { points: newPoints }));
273
- return stateChanges;
274
- }
275
-
276
- /**
277
- * Apply trim and arc for a Line + Polyline (or Polyline + Line) fillet.
278
- * The line is consumed into the polyline with the arc encoded as a bulge.
279
- * @param {Point} firstTangentPoint
280
- * @param {Point} secondTangentPoint
281
- * @param {number} arcDirection
282
- * @param {Point} arcCentre
283
- * @return {Array}
284
- */
285
- #trimLineAndPoly(firstTangentPoint, secondTangentPoint, arcDirection, arcCentre) {
286
- const firstIsPolyline = this.firstPick.entity instanceof BasePolyline;
287
- const [poly, line] = firstIsPolyline ? [this.firstPick, this.secondPick] : [this.secondPick, this.firstPick];
288
- const lineTangentPoint = line === this.firstPick ? firstTangentPoint : secondTangentPoint;
289
- const polyTangentPoint = poly === this.firstPick ? firstTangentPoint : secondTangentPoint;
290
- const polyToLineDir = firstIsPolyline ? arcDirection : -arcDirection;
291
-
292
- const startAngle = arcCentre.angle(polyTangentPoint);
293
- const endAngle = arcCentre.angle(lineTangentPoint);
294
- const includedAngle = ((endAngle - startAngle) * polyToLineDir + 4 * Math.PI) % (2 * Math.PI);
295
- const bulge = polyToLineDir * Math.tan(includedAngle / 4);
296
- const arcStartPoint = polyTangentPoint.clone();
297
- arcStartPoint.bulge = bulge;
205
+ const firstUnit = firstPick.clickUnit(intersectionPoint);
206
+ const secondUnit = secondPick.clickUnit(intersectionPoint);
207
+ const cosAngle = Math.min(1, Math.max(-1, firstUnit.dot(secondUnit)));
208
+ const cornerAngle = Math.acos(cosAngle);
209
+ if (cornerAngle < Constants.Tolerance.EPSILON || cornerAngle > Math.PI - Constants.Tolerance.EPSILON) return null;
298
210
 
299
- const polySegIdx = poly.segmentIndex;
300
- const keepStart = poly.keepStart(this.intersectionPoint);
301
- let newPoints;
302
- if (keepStart) {
303
- // Keep the polyline points up to (not including) the selected segment start,
304
- // then the arc start (with bulge), the line tangent point, and the kept end of the line.
305
- newPoints = [
306
- ...poly.entity.points.slice(0, polySegIdx).map((p) => p.clone()),
307
- arcStartPoint,
308
- lineTangentPoint.clone(),
309
- line.lineKeptEnd(this.intersectionPoint).clone(),
310
- ];
311
- } else {
312
- // Keep the polyline points from the selected segment start onwards,
313
- // prepending the kept end of the line, the reversed arc start (with negated bulge), and the poly tangent point.
314
- const revArcStartPoint = lineTangentPoint.clone();
315
- revArcStartPoint.bulge = -bulge;
316
- newPoints = [
317
- line.lineKeptEnd(this.intersectionPoint).clone(),
318
- revArcStartPoint,
319
- polyTangentPoint.clone(),
320
- ...poly.entity.points.slice(polySegIdx).map((p) => p.clone()),
321
- ];
322
- }
211
+ const bisectorUnit = firstUnit.add(secondUnit).normalise();
212
+ const arcCentre = intersectionPoint.add(bisectorUnit.scale(filletRadius / Math.sin(cornerAngle / 2)));
213
+ const firstTangentPoint = arcCentre.perpendicular(firstPick.lineStart, firstPick.lineEnd);
214
+ const secondTangentPoint = arcCentre.perpendicular(secondPick.lineStart, secondPick.lineEnd);
215
+
216
+ const firstKeptDir = firstPick.lineKeptEnd(intersectionPoint).subtract(intersectionPoint);
217
+ const secondKeptDir = secondPick.lineKeptEnd(intersectionPoint).subtract(intersectionPoint);
218
+ const firstDot = firstTangentPoint.subtract(intersectionPoint).dot(firstKeptDir);
219
+ const secondDot = secondTangentPoint.subtract(intersectionPoint).dot(secondKeptDir);
220
+ const tangentsInBounds = firstDot >= 0 && firstDot <= firstKeptDir.dot(firstKeptDir) &&
221
+ secondDot >= 0 && secondDot <= secondKeptDir.dot(secondKeptDir);
222
+
223
+ const windingCross = firstTangentPoint.subtract(arcCentre).cross(secondTangentPoint.subtract(arcCentre));
224
+ const arc = DesignCore.CommandManager.createNew('Arc', {
225
+ points: [arcCentre, firstTangentPoint, secondTangentPoint],
226
+ direction: windingCross > 0 ? 1 : -1,
227
+ ...Utils.cloneProperties(firstPick.entity),
228
+ });
323
229
 
324
- return [
325
- new RemoveState(line.entity),
326
- new UpdateState(poly.entity, { points: newPoints }),
327
- ];
230
+ return { arc, firstTangentPoint, secondTangentPoint, tangentsInBounds };
328
231
  }
329
232
 
330
233
  /**