@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
@@ -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
 
@@ -70,7 +73,26 @@ 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
  /**
@@ -78,36 +100,127 @@ export class Extend extends Tool {
78
100
  */
79
101
  action() {
80
102
  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
- }
103
+ if (typeof this.selectedItem.fromPolylinePoints !== 'function') {
104
+ DesignCore.Core.notify(`${this.type} - ${this.selectedItem.type} ${Strings.Message.NOEXTEND}`);
105
+ this.selectedItem = null;
106
+ return;
97
107
  }
98
108
 
109
+ const intersectPoints = this.#collectIntersectPoints(this.selectedItem);
110
+
99
111
  if (intersectPoints.length) {
100
- const stateChanges = this.selectedItem.extend(intersectPoints);
112
+ const stateChanges = this.#extendEntity(this.selectedItem, intersectPoints);
101
113
  if (stateChanges?.length) {
102
114
  DesignCore.Scene.commit(stateChanges);
115
+ } else {
116
+ DesignCore.Core.notify(`${this.type} - ${this.selectedItem.type} ${Strings.Message.NOEXTEND}`);
103
117
  }
104
118
  } else {
105
119
  DesignCore.Core.notify(`${this.type} - ${this.selectedItem.type} ${Strings.Message.NOEXTEND}`);
106
120
  }
107
121
  }
108
122
 
109
- // reset selected item
110
123
  this.selectedItem = null;
111
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, BasePolyline, 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;
189
+ }
190
+ }
191
+
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 })];
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
8
  import { BasePolyline } from '../entities/basePolyline.js';
8
- import { AddState, RemoveState, UpdateState } from '../lib/stateManager.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
 
@@ -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
  /**
@@ -2,6 +2,7 @@ import { Tool } from './tool.js';
2
2
  import { Input, PromptOptions } from '../lib/inputManager.js';
3
3
  import { Strings } from '../lib/strings.js';
4
4
  import { Logging } from '../lib/logging.js';
5
+ import { Utils } from '../lib/utils.js';
5
6
  import { UpdateState } from '../lib/stateManager.js';
6
7
 
7
8
  import { DesignCore } from '../designCore.js';
@@ -21,8 +22,6 @@ export class MatchProp extends Tool {
21
22
  this.sourceIndex = null;
22
23
  // array of destination item indices
23
24
  this.destinationSetIndices = [];
24
- // properties to match
25
- this.properties = ['colour', 'layer', 'lineType', 'lineWidth', 'height', 'styleName', 'dimensionStyle'];
26
25
  }
27
26
 
28
27
  /**
@@ -76,21 +75,13 @@ export class MatchProp extends Tool {
76
75
 
77
76
  // get source item
78
77
  const sourceItem = DesignCore.Scene.entities.get(this.sourceIndex);
78
+ const propertySet = Utils.cloneProperties(sourceItem);
79
79
 
80
80
  const stateChanges = [];
81
81
 
82
82
  // loop through destination set
83
83
  for (let i = 0; i < this.destinationSetIndices.length; i++) {
84
84
  const targetItem = DesignCore.Scene.entities.get(this.destinationSetIndices[i]);
85
- const propertySet = {};
86
- // loop through properties and match
87
- for (let p = 0; p < this.properties.length; p++) {
88
- // check property exists on both items
89
- const prop = this.properties[p];
90
- if (sourceItem.hasOwnProperty(prop)) {
91
- propertySet[prop] = sourceItem[prop];
92
- }
93
- }
94
85
 
95
86
  // only add state change if there are properties to change
96
87
  if (Object.keys(propertySet).length > 0) {
@@ -7,6 +7,7 @@ import { Point } from '../entities/point.js';
7
7
  import { AddState, UpdateState } from '../lib/stateManager.js';
8
8
 
9
9
  import { DesignCore } from '../designCore.js';
10
+ import { RubberBand } from '../lib/auxiliary/rubberBand.js';
10
11
 
11
12
  /**
12
13
  * Mirror Command Class
@@ -74,8 +75,8 @@ export class Mirror extends Tool {
74
75
  const pt1 = this.points[0];
75
76
  const pt2 = this.points.length >= 2 ? this.points[1] : mousePoint;
76
77
 
77
- // Draw the mirror line
78
- DesignCore.Scene.tempEntities.create('Line', { points: [pt1, pt2] });
78
+ // Draw the mirror line as a rubber-band indicator
79
+ DesignCore.Scene.auxiliaryEntities.add(new RubberBand([pt1, pt2]));
79
80
 
80
81
  // Preview mirrored entities (skip if mirror line has zero length)
81
82
  if (!pt1.isSame(pt2)) {
@@ -84,8 +85,8 @@ export class Mirror extends Tool {
84
85
  const entityIndex = DesignCore.Scene.selectionManager.selectionSet.selectionSet[i];
85
86
  const originalItem = DesignCore.Scene.entities.get(entityIndex);
86
87
  item.setProperty('points', this.getMirroredPoints(originalItem.points, pt1, pt2));
87
- if (originalItem.direction !== undefined) {
88
- item.setProperty('direction', -originalItem.direction);
88
+ if (originalItem.properties?.has('direction')) {
89
+ item.setProperty('direction', -originalItem.getProperty('direction'));
89
90
  }
90
91
  }
91
92
  }
@@ -114,14 +115,14 @@ export class Mirror extends Tool {
114
115
  if (this.eraseSource) {
115
116
  // Replace the original with its mirrored version
116
117
  const updates = { points: mirroredPoints };
117
- if (item.direction !== undefined) updates.direction = -item.direction;
118
+ if (item.properties?.has('direction')) updates.direction = -item.getProperty('direction');
118
119
  const stateChange = new UpdateState(item, updates);
119
120
  stateChanges.push(stateChange);
120
121
  } else {
121
122
  // Keep the original and add a mirrored copy
122
123
  const copyOfItem = Utils.cloneObject(item);
123
124
  copyOfItem.setProperty('points', mirroredPoints);
124
- if (copyOfItem.direction !== undefined) copyOfItem.setProperty('direction', -copyOfItem.direction);
125
+ if (copyOfItem.properties?.has('direction')) copyOfItem.setProperty('direction', -copyOfItem.getProperty('direction'));
125
126
  const stateChange = new AddState(copyOfItem);
126
127
  stateChanges.push(stateChange);
127
128
  }
@@ -6,6 +6,7 @@ import { Point } from '../entities/point.js';
6
6
 
7
7
  import { DesignCore } from '../designCore.js';
8
8
  import { UpdateState } from '../lib/stateManager.js';
9
+ import { RubberBand } from '../lib/auxiliary/rubberBand.js';
9
10
 
10
11
  /**
11
12
  * Move Command Class
@@ -66,9 +67,8 @@ export class Move extends Tool {
66
67
  if (this.points.length >= 1) {
67
68
  const mousePoint = DesignCore.Mouse.pointOnScene();
68
69
 
69
- // Draw a line
70
- const points = [this.points.at(-1), mousePoint];
71
- DesignCore.Scene.tempEntities.create('Line', { points: points });
70
+ // Draw a rubber-band line from base point to mouse
71
+ DesignCore.Scene.auxiliaryEntities.add(new RubberBand([this.points.at(-1), mousePoint]));
72
72
 
73
73
  // Get the delta from the last mouse point
74
74
  const delta = mousePoint.subtract(this.lastMousePoint || this.points[0]);
@@ -107,9 +107,8 @@ export class Offset extends Tool {
107
107
  const offsetPoints = this.getOffsetPoints(this.selectedItem, mousePoint, offsetDistance);
108
108
 
109
109
  if (offsetPoints) {
110
- const previewEntity = Utils.cloneObject(this.selectedItem);
111
- previewEntity.fromPolylinePoints(offsetPoints);
112
- DesignCore.Scene.tempEntities.add(previewEntity);
110
+ const previewEntity = Utils.cloneObject(this.selectedItem).fromPolylinePoints(offsetPoints);
111
+ DesignCore.Scene.previewEntities.add(previewEntity);
113
112
  }
114
113
  }
115
114
  }
@@ -130,8 +129,7 @@ export class Offset extends Tool {
130
129
  return;
131
130
  }
132
131
 
133
- const offsetEntity = Utils.cloneObject(entity);
134
- offsetEntity.fromPolylinePoints(offsetPoints);
132
+ const offsetEntity = Utils.cloneObject(entity).fromPolylinePoints(offsetPoints);
135
133
 
136
134
  const stateChange = new AddState(offsetEntity);
137
135
  DesignCore.Scene.commit([stateChange]);
@@ -66,7 +66,7 @@ export class Pasteclip extends Tool {
66
66
  const tempEntity = Utils.cloneObject(entity);
67
67
  const offsetPoints = tempEntity.points.map((p) => new Point(p.x, p.y, p.bulge, p.sequence).add(delta));
68
68
  tempEntity.setProperty('points', offsetPoints);
69
- DesignCore.Scene.tempEntities.add(tempEntity);
69
+ DesignCore.Scene.previewEntities.add(tempEntity);
70
70
  }
71
71
  }
72
72
 
@@ -7,6 +7,7 @@ import { Point } from '../entities/point.js';
7
7
  import { UpdateState } from '../lib/stateManager.js';
8
8
 
9
9
  import { DesignCore } from '../designCore.js';
10
+ import { RubberBand } from '../lib/auxiliary/rubberBand.js';
10
11
 
11
12
  /**
12
13
  * Rotate Command Class
@@ -72,6 +73,9 @@ export class Rotate extends Tool {
72
73
  this.baseAngle = refBase.angle(refEnd);
73
74
  // set the base point to null
74
75
  this.basePoint = null;
76
+ // restore inputPoint to the rotation center so the tracking line
77
+ // uses the correct origin for the final angle input
78
+ DesignCore.Scene.inputManager.inputPoint = this.points[0];
75
79
  } else if (Input.getType(input2) === Input.Type.NUMBER) {
76
80
  const basePoint = this.points.at(0);
77
81
  const angle = Utils.degrees2radians(input2);
@@ -95,10 +99,9 @@ export class Rotate extends Tool {
95
99
  const mousePoint = DesignCore.Mouse.pointOnScene();
96
100
 
97
101
  if (this.points.length >= 1 && this.baseAngle !== null || this.basePoint !== null) {
98
- // Draw a line
102
+ // Draw a rubber-band line from center to mouse
99
103
  const center = this.basePoint === null ? this.points.at(0) : this.basePoint;
100
- const points = [center, mousePoint];
101
- DesignCore.Scene.tempEntities.create('Line', { points: points });
104
+ DesignCore.Scene.auxiliaryEntities.add(new RubberBand([center, mousePoint]));
102
105
  }
103
106
 
104
107
  if (this.points.length >= 1) {