@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
@@ -3,10 +3,12 @@ import { ChamferFilletBase } from './chamferFilletBase.js';
3
3
  import { Intersection } from '../lib/intersect.js';
4
4
  import { Input, PromptOptions } from '../lib/inputManager.js';
5
5
  import { Logging } from '../lib/logging.js';
6
+ import { Utils } from '../lib/utils.js';
6
7
  import { Point } from '../entities/point.js';
7
8
  import { Line } from '../entities/line.js';
8
- import { BasePolyline } from '../entities/basePolyline.js';
9
- import { AddState, RemoveState, UpdateState } from '../lib/stateManager.js';
9
+ import { PolylineBase } from '../entities/polylineBase.js';
10
+ import { AddState } from '../lib/stateManager.js';
11
+ import { Colour } from '../lib/colour.js';
10
12
 
11
13
  import { DesignCore } from '../designCore.js';
12
14
 
@@ -96,9 +98,9 @@ export class Chamfer extends ChamferFilletBase {
96
98
  }
97
99
 
98
100
  // input1 is a selection — validate and store first entity
99
- const firstEntity = DesignCore.Scene.entities.get(input1.selectedItemIndex);
101
+ const firstEntity = DesignCore.Scene.entities.get(input1.selectedEntityIndex);
100
102
  DesignCore.Scene.selectionManager.removeLastSelection();
101
- if (!(firstEntity instanceof Line) && !(firstEntity instanceof BasePolyline)) {
103
+ if (!(firstEntity instanceof Line) && !(firstEntity instanceof PolylineBase)) {
102
104
  DesignCore.Core.notify(`${firstEntity.type} - ${Strings.Message.NOCHAMFER}`);
103
105
  continue;
104
106
  }
@@ -110,16 +112,16 @@ export class Chamfer extends ChamferFilletBase {
110
112
  const input2 = await DesignCore.Scene.inputManager.requestInput(op2);
111
113
  if (input2 === undefined) return;
112
114
 
113
- const candidate = DesignCore.Scene.entities.get(input2.selectedItemIndex);
115
+ const candidate = DesignCore.Scene.entities.get(input2.selectedEntityIndex);
114
116
  DesignCore.Scene.selectionManager.removeLastSelection();
115
- if (!(candidate instanceof Line) && !(candidate instanceof BasePolyline)) {
117
+ if (!(candidate instanceof Line) && !(candidate instanceof PolylineBase)) {
116
118
  DesignCore.Core.notify(`${this.type} - ${candidate.type} ${Strings.Message.NOCHAMFER}`);
117
119
  continue;
118
120
  }
119
121
  if (!this.secondPick.setPick(candidate, input2.selectedPoint, `${Strings.Strings.ARC} - ${Strings.Message.NOCHAMFER}`)) continue;
120
122
  // If both selections are the same polyline, segments must be consecutive
121
123
  // or they must be the open-end segments of an open polyline.
122
- if (candidate === firstEntity && firstEntity instanceof BasePolyline) {
124
+ if (candidate === firstEntity && firstEntity instanceof PolylineBase) {
123
125
  const isConsecutive = firstEntity.areConsecutiveSegments(this.firstPick.segmentIndex, this.secondPick.segmentIndex);
124
126
  const lastIdx = firstEntity.points.length - 1;
125
127
  const isOpenEnds = !firstEntity.flags.hasFlag(1) &&
@@ -146,7 +148,46 @@ export class Chamfer extends ChamferFilletBase {
146
148
  * Preview the command during execution
147
149
  */
148
150
  preview() {
149
- // No preview
151
+ const selection = this.validateSelection();
152
+ if (!selection) return;
153
+
154
+ const chamferMode = DesignCore.Scene.headers.chamferMode;
155
+ const trimMode = DesignCore.Scene.headers.trimMode;
156
+ const geo = this.#computeChamfer(this.firstPick, selection.tempSecond, selection.intersectionPoint);
157
+ if (!geo) return;
158
+ if (trimMode && !geo.chamferInBounds) return;
159
+
160
+ const isSharpCorner = !chamferMode &&
161
+ DesignCore.Scene.headers.chamferDistanceA === 0 &&
162
+ DesignCore.Scene.headers.chamferDistanceB === 0;
163
+
164
+ if (!trimMode) {
165
+ if (!isSharpCorner) DesignCore.Scene.previewEntities.add(geo.chamferLine);
166
+ return;
167
+ }
168
+
169
+ // trimMode: dull the original segments then show the shortened versions + chamfer line
170
+ const firstSeg = Utils.cloneObject(this.firstPick.activeSeg);
171
+ firstSeg.setProperty('colour', Colour.blend(this.firstPick.entity.getDrawColour(), DesignCore.Settings.canvasbackgroundcolour, DesignCore.Settings.previewBlendFactor));
172
+ DesignCore.Scene.previewEntities.add(firstSeg);
173
+
174
+ const secondSeg = Utils.cloneObject(selection.tempSecond.activeSeg);
175
+ secondSeg.setProperty('colour', Colour.blend(selection.tempSecond.entity.getDrawColour(), DesignCore.Settings.canvasbackgroundcolour, DesignCore.Settings.previewBlendFactor));
176
+ DesignCore.Scene.previewEntities.add(secondSeg);
177
+
178
+ const firstTrimLine = DesignCore.CommandManager.createNew('Line', {
179
+ points: [this.firstPick.lineKeptEnd(selection.intersectionPoint), geo.firstChamferPoint],
180
+ ...Utils.cloneProperties(this.firstPick.entity),
181
+ });
182
+ DesignCore.Scene.previewEntities.add(firstTrimLine);
183
+
184
+ const secondTrimLine = DesignCore.CommandManager.createNew('Line', {
185
+ points: [selection.tempSecond.lineKeptEnd(selection.intersectionPoint), geo.secondChamferPoint],
186
+ ...Utils.cloneProperties(selection.candidate),
187
+ });
188
+ DesignCore.Scene.previewEntities.add(secondTrimLine);
189
+
190
+ DesignCore.Scene.previewEntities.add(geo.chamferLine);
150
191
  }
151
192
 
152
193
  /**
@@ -156,174 +197,107 @@ export class Chamfer extends ChamferFilletBase {
156
197
  if (!this.firstPick.entity || !this.secondPick.entity) return;
157
198
  if (!this.resolveCornerGeometry(Strings.Message.NOCHAMFER)) return;
158
199
 
159
- const trimMode = DesignCore.Scene.headers.trimMode;
160
200
  const chamferMode = DesignCore.Scene.headers.chamferMode;
161
- const distA = DesignCore.Scene.headers.chamferDistanceA;
201
+ const trimMode = DesignCore.Scene.headers.trimMode;
162
202
 
163
- // distA = distB = 0: trim/extend both lines to the sharp intersection with no chamfer line
164
- if (!chamferMode && distA === 0 && DesignCore.Scene.headers.chamferDistanceB === 0) {
165
- if (trimMode) {
166
- const stateChanges = this.applySharpTrim();
167
- DesignCore.Scene.commit(stateChanges);
203
+ // Validate angle before computing — parallel-lines error would be misleading
204
+ if (chamferMode) {
205
+ const alpha = DesignCore.Scene.headers.chamferAngle * (Math.PI / 180);
206
+ if (alpha <= 0 || alpha >= Math.PI) {
207
+ DesignCore.Core.notify(`${this.type} - ${Strings.Error.ERROR}:${Strings.Error.INVALIDNUMBER}`);
208
+ return;
168
209
  }
210
+ }
211
+
212
+ const geo = this.#computeChamfer(this.firstPick, this.secondPick, this.intersectionPoint);
213
+ if (!geo) {
214
+ DesignCore.Core.notify(`${this.type} - ${Strings.Error.ERROR}:${Strings.Error.PARALLELLINES}`);
215
+ return;
216
+ }
217
+
218
+ // dist=0 in distance mode: sharp corner trim with no chamfer line (mirrors fillet radius=0)
219
+ const isSharpCorner = !chamferMode &&
220
+ DesignCore.Scene.headers.chamferDistanceA === 0 &&
221
+ DesignCore.Scene.headers.chamferDistanceB === 0;
222
+
223
+ if (trimMode && !geo.chamferInBounds) {
224
+ DesignCore.Core.notify(`${this.type} - ${Strings.Error.MAXVALUE}`);
225
+ return;
226
+ }
227
+
228
+ if (!trimMode) {
229
+ if (!isSharpCorner) DesignCore.Scene.commit([new AddState(geo.chamferLine)]);
169
230
  return;
170
231
  }
171
232
 
172
- // Compute the two chamfer endpoints on each line
173
- const firstUnit = this.firstPick.clickUnit(this.intersectionPoint);
174
- const secondUnit = this.secondPick.clickUnit(this.intersectionPoint);
233
+ const stateChanges = this.applyCornerTrim(
234
+ geo.firstChamferPoint, geo.secondChamferPoint,
235
+ isSharpCorner ? null : geo.chamferLine,
236
+ );
237
+ DesignCore.Scene.commit(stateChanges);
238
+ }
239
+
240
+ /**
241
+ * Compute the chamfer geometry for two picks meeting at intersectionPoint.
242
+ * Returns null if the lines are parallel/collinear or the chamfer direction is parallel to line2 (angle mode).
243
+ * @param {CornerEntity} firstPick
244
+ * @param {CornerEntity} secondPick
245
+ * @param {Point} intersectionPoint
246
+ * @return {{chamferLine, firstChamferPoint, secondChamferPoint, chamferInBounds}|null}
247
+ */
248
+ #computeChamfer(firstPick, secondPick, intersectionPoint) {
249
+ const chamferMode = DesignCore.Scene.headers.chamferMode;
250
+ const distA = DesignCore.Scene.headers.chamferDistanceA;
251
+ const firstUnit = firstPick.clickUnit(intersectionPoint);
252
+ const secondUnit = secondPick.clickUnit(intersectionPoint);
253
+
175
254
  let firstChamferPoint;
176
255
  let secondChamferPoint;
177
256
 
178
257
  if (!chamferMode) {
179
- // Distance method: measure distA along line1 and distB along line2 from intersection
258
+ // Distance method: distA = distB = 0 means sharp corner trim with no chamfer line
180
259
  const distB = DesignCore.Scene.headers.chamferDistanceB;
181
- firstChamferPoint = this.intersectionPoint.add(firstUnit.scale(distA));
182
- secondChamferPoint = this.intersectionPoint.add(secondUnit.scale(distB));
260
+ firstChamferPoint = intersectionPoint.add(firstUnit.scale(distA));
261
+ secondChamferPoint = intersectionPoint.add(secondUnit.scale(distB));
183
262
  } else {
184
- // Angle method: measure chamferLength along line1, then project using chamferAngle to find
185
- // where the chamfer line meets line2.
186
- // chamferAngle is stored in degrees (as entered by the user).
263
+ // Angle method
187
264
  const chamferLength = DesignCore.Scene.headers.chamferLength;
188
265
  const alpha = DesignCore.Scene.headers.chamferAngle * (Math.PI / 180);
266
+ if (alpha <= 0 || alpha >= Math.PI) return null;
189
267
 
190
- if (alpha <= 0 || alpha >= Math.PI) {
191
- DesignCore.Core.notify(`${this.type} - ${Strings.Error.ERROR}:${Strings.Error.INVALIDNUMBER}`);
192
- return;
193
- }
194
-
195
- firstChamferPoint = this.intersectionPoint.add(firstUnit.scale(chamferLength));
268
+ firstChamferPoint = intersectionPoint.add(firstUnit.scale(chamferLength));
196
269
 
197
- // Rotate firstUnit by ±(π - alpha) to get the chamfer direction from
198
- // firstChamferPoint. Choose the rotation that points toward line2 (positive
199
- // dot product with secondUnit).
200
270
  const rotAngle = Math.PI - alpha;
201
271
  const origin = new Point(0, 0);
202
272
  const candidate1 = firstUnit.rotate(origin, rotAngle);
203
273
  const chamferDir = candidate1.dot(secondUnit) >= 0 ? candidate1 : firstUnit.rotate(origin, -rotAngle);
204
274
 
205
- // Intersect chamfer ray from firstChamferPoint with the infinite line2
206
275
  const chamferResult = Intersection.intersectSegmentSegment(
207
276
  firstChamferPoint, firstChamferPoint.add(chamferDir),
208
- this.secondPick.lineStart, this.secondPick.lineEnd,
277
+ secondPick.lineStart, secondPick.lineEnd,
209
278
  true, true,
210
279
  );
280
+ if (chamferResult.status === Intersection.Status.PARALLEL ||
281
+ chamferResult.status === Intersection.Status.OVERLAPPING ||
282
+ chamferResult.status === Intersection.Status.COINCIDENT) return null;
211
283
 
212
- const chamferStatus = chamferResult.status;
213
- if (chamferStatus === Intersection.Status.PARALLEL ||
214
- chamferStatus === Intersection.Status.OVERLAPPING ||
215
- chamferStatus === Intersection.Status.COINCIDENT) {
216
- DesignCore.Core.notify(`${this.type} - ${Strings.Error.ERROR}:${Strings.Error.PARALLELLINES}`);
217
- return;
218
- }
219
-
220
- secondChamferPoint = chamferResult.points[0] || null;
221
- if (!secondChamferPoint) {
222
- // Chamfer direction is parallel to line2 — no intersection
223
- DesignCore.Core.notify(`${this.type} - ${Strings.Error.ERROR}:${Strings.Error.PARALLELLINES}`);
224
- return;
225
- }
284
+ secondChamferPoint = chamferResult.points[0];
285
+ if (!secondChamferPoint) return null;
226
286
  }
227
287
 
228
- // Verify both chamfer endpoints lie on the correct side of the intersection and
229
- // not beyond the kept endpoints. Uses the same dot-product approach as Fillet so
230
- // that segments are treated as infinite lines (extensions are allowed).
231
- if (trimMode) {
232
- const firstKeptDir = this.firstPick.lineKeptEnd(this.intersectionPoint).subtract(this.intersectionPoint);
233
- const secondKeptDir = this.secondPick.lineKeptEnd(this.intersectionPoint).subtract(this.intersectionPoint);
234
- const firstChamferDot = firstChamferPoint.subtract(this.intersectionPoint).dot(firstKeptDir);
235
- const secondChamferDot = secondChamferPoint.subtract(this.intersectionPoint).dot(secondKeptDir);
236
- if (firstChamferDot < 0 || firstChamferDot > firstKeptDir.dot(firstKeptDir) ||
237
- secondChamferDot < 0 || secondChamferDot > secondKeptDir.dot(secondKeptDir)) {
238
- DesignCore.Core.notify(`${this.type} - ${Strings.Error.MAXVALUE}`);
239
- return;
240
- }
241
- }
288
+ const firstKeptDir = firstPick.lineKeptEnd(intersectionPoint).subtract(intersectionPoint);
289
+ const secondKeptDir = secondPick.lineKeptEnd(intersectionPoint).subtract(intersectionPoint);
290
+ const firstDot = firstChamferPoint.subtract(intersectionPoint).dot(firstKeptDir);
291
+ const secondDot = secondChamferPoint.subtract(intersectionPoint).dot(secondKeptDir);
292
+ const chamferInBounds = firstDot >= 0 && firstDot <= firstKeptDir.dot(firstKeptDir) &&
293
+ secondDot >= 0 && secondDot <= secondKeptDir.dot(secondKeptDir);
242
294
 
243
295
  const chamferLine = DesignCore.CommandManager.createNew('Line', {
244
296
  points: [firstChamferPoint, secondChamferPoint],
297
+ ...Utils.cloneProperties(firstPick.entity),
245
298
  });
246
299
 
247
- const firstIsPolyline = this.firstPick.entity instanceof BasePolyline;
248
- const secondIsPolyline = this.secondPick.entity instanceof BasePolyline;
249
-
250
- // The standalone chamfer Line entity is added when no polyline trimming is involved.
251
- // For polyline trim cases the chamfer segment is encoded as a straight polyline segment,
252
- // except for the open-ends case where the chamfer line fills the gap between the two
253
- // open ends and is added separately there.
254
- const stateChanges = (!trimMode || (!firstIsPolyline && !secondIsPolyline)) ?
255
- [new AddState(chamferLine)] :
256
- [];
257
-
258
- if (trimMode) {
259
- if (!firstIsPolyline && !secondIsPolyline) {
260
- // Line + Line: trim both lines
261
- stateChanges.push(new UpdateState(this.firstPick.entity, { points: [this.firstPick.lineKeptEnd(this.intersectionPoint), firstChamferPoint] }));
262
- stateChanges.push(new UpdateState(this.secondPick.entity, { points: [this.secondPick.lineKeptEnd(this.intersectionPoint), secondChamferPoint] }));
263
- } else if (firstIsPolyline && secondIsPolyline && this.firstPick.entity === this.secondPick.entity) {
264
- const closeSegIdx = this.firstPick.entity.points.length;
265
- const lastIdx = closeSegIdx - 1;
266
- const segDiff = Math.abs(this.firstPick.segmentIndex - this.secondPick.segmentIndex);
267
- const isOpenEnds = !this.firstPick.entity.flags.hasFlag(1) && segDiff !== 1 && (
268
- (this.firstPick.segmentIndex === 1 && this.secondPick.segmentIndex === lastIdx) ||
269
- (this.firstPick.segmentIndex === lastIdx && this.secondPick.segmentIndex === 1)
270
- );
271
- const newPoints = this.firstPick.entity.points.map((p) => p.clone());
272
- if (isOpenEnds) {
273
- // Open ends: trim both endpoints to their chamfer points; chamfer line fills the gap.
274
- stateChanges.push(new AddState(chamferLine));
275
- if (this.firstPick.segmentIndex === 1) {
276
- newPoints[0] = firstChamferPoint.clone();
277
- newPoints[lastIdx] = secondChamferPoint.clone();
278
- } else {
279
- newPoints[0] = secondChamferPoint.clone();
280
- newPoints[lastIdx] = firstChamferPoint.clone();
281
- }
282
- } else {
283
- // Consecutive segments: replace the shared corner vertex with the two chamfer points
284
- // as a straight polyline segment — no separate chamfer Line entity needed.
285
- const seg1 = this.firstPick.segmentIndex;
286
- const seg2 = this.secondPick.segmentIndex;
287
- const isClosingWrap = (seg1 === closeSegIdx && seg2 === 1) || (seg2 === closeSegIdx && seg1 === 1);
288
- const isFirstLower = seg1 < seg2;
289
- const cornerIdx = isClosingWrap ? 0 : Math.min(seg1, seg2);
290
- const lowerChamfer = (isClosingWrap ? seg1 === closeSegIdx : !isFirstLower) ? secondChamferPoint : firstChamferPoint;
291
- const upperChamfer = (isClosingWrap ? seg1 === closeSegIdx : !isFirstLower) ? firstChamferPoint : secondChamferPoint;
292
- newPoints.splice(cornerIdx, 1, lowerChamfer.clone(), upperChamfer.clone());
293
- }
294
- stateChanges.push(new UpdateState(this.firstPick.entity, { points: newPoints }));
295
- } else {
296
- // Line + Polyline or Polyline + Line: consume the line into the polyline.
297
- const [poly, line] = firstIsPolyline ? [this.firstPick, this.secondPick] : [this.secondPick, this.firstPick];
298
- const polyChamferPoint = poly === this.firstPick ? firstChamferPoint : secondChamferPoint;
299
- const lineChamferPoint = line === this.firstPick ? firstChamferPoint : secondChamferPoint;
300
- const polySegIdx = poly.segmentIndex;
301
- const keepStart = poly.keepStart(this.intersectionPoint);
302
- let newPoints;
303
- if (keepStart) {
304
- // Keep start portion: points[0..polySegIdx-1], then chamfer, then line end
305
- newPoints = [
306
- ...poly.entity.points.slice(0, polySegIdx).map((p) => p.clone()),
307
- polyChamferPoint.clone(),
308
- lineChamferPoint.clone(),
309
- line.lineKeptEnd(this.intersectionPoint).clone(),
310
- ];
311
- } else {
312
- // Keep end portion: line end, then chamfer, then points[polySegIdx..end]
313
- newPoints = [
314
- line.lineKeptEnd(this.intersectionPoint).clone(),
315
- lineChamferPoint.clone(),
316
- polyChamferPoint.clone(),
317
- ...poly.entity.points.slice(polySegIdx).map((p) => p.clone()),
318
- ];
319
- }
320
-
321
- stateChanges.push(new RemoveState(line.entity));
322
- stateChanges.push(new UpdateState(poly.entity, { points: newPoints }));
323
- }
324
- }
325
-
326
- DesignCore.Scene.commit(stateChanges);
300
+ return { chamferLine, firstChamferPoint, secondChamferPoint, chamferInBounds };
327
301
  }
328
302
 
329
303
  /**
@@ -1,8 +1,9 @@
1
1
  import { Tool } from './tool.js';
2
2
  import { Strings } from '../lib/strings.js';
3
3
  import { Constants } from '../lib/constants.js';
4
- import { BasePolyline } from '../entities/basePolyline.js';
5
- import { RemoveState, UpdateState } from '../lib/stateManager.js';
4
+ import { Line } from '../entities/line.js';
5
+ import { PolylineBase } from '../entities/polylineBase.js';
6
+ import { AddState, RemoveState, UpdateState } from '../lib/stateManager.js';
6
7
  import { CornerEntity } from './cornerEntity.js';
7
8
  import { Intersection } from '../lib/intersect.js';
8
9
 
@@ -11,7 +12,7 @@ import { DesignCore } from '../designCore.js';
11
12
  /**
12
13
  * ChamferFilletBase Class
13
14
  * Base class for Chamfer and Fillet commands, providing shared entity selection
14
- * state, segment resolution, and sharp-trim logic.
15
+ * state, segment resolution, and shared corner-trim logic.
15
16
  * @extends Tool
16
17
  */
17
18
  export class ChamferFilletBase extends Tool {
@@ -90,74 +91,174 @@ export class ChamferFilletBase extends Tool {
90
91
  }
91
92
 
92
93
  /**
93
- * Trim both entities to the sharp intersection point with no arc or chamfer line.
94
- * Handles Line+Line, Polyline+Polyline (same entity), and Line+Polyline cases.
95
- * Reads geometry from instance fields set by resolveCornerGeometry().
96
- * @return {Array} - array of state changes to be committed by the caller
94
+ * Resolves the preview second-pick and virtual intersection for the mouse position.
95
+ * Returns { candidate, tempSecond, intersectionPoint } or null if no valid preview exists.
96
+ * @return {{candidate, tempSecond, intersectionPoint}|null}
97
97
  */
98
- applySharpTrim() {
99
- const firstIsPolyline = this.firstPick.entity instanceof BasePolyline;
100
- const secondIsPolyline = this.secondPick.entity instanceof BasePolyline;
98
+ validateSelection() {
99
+ if (!this.firstPick.entity) return null;
100
+ if (!this.firstPick.resolveEndpoints()) return null;
101
101
 
102
- // Line + Line: trim both lines to the intersection point.
102
+ const mousePoint = DesignCore.Mouse.pointOnScene();
103
+ const index = DesignCore.Scene.selectionManager.findClosestItem(mousePoint);
104
+ if (index === undefined) return null;
105
+
106
+ const candidate = DesignCore.Scene.entities.get(index);
107
+ if (!(candidate instanceof Line) && !(candidate instanceof PolylineBase)) return null;
108
+
109
+ const tempSecond = new CornerEntity();
110
+ if (!tempSecond.setPick(candidate, mousePoint, '')) return null;
111
+ if (!tempSecond.resolveEndpoints()) return null;
112
+
113
+ const result = Intersection.intersectSegmentSegment(
114
+ this.firstPick.lineStart, this.firstPick.lineEnd,
115
+ tempSecond.lineStart, tempSecond.lineEnd,
116
+ true, true,
117
+ );
118
+ if (result.status === Intersection.Status.PARALLEL ||
119
+ result.status === Intersection.Status.OVERLAPPING ||
120
+ result.status === Intersection.Status.COINCIDENT) return null;
121
+
122
+ const intersectionPoint = result.points[0];
123
+ if (!intersectionPoint) return null;
124
+
125
+ return { candidate, tempSecond, intersectionPoint };
126
+ }
127
+
128
+ /**
129
+ * Unified corner-trim method for sharp trims, chamfer, and fillet.
130
+ * Computes and applies state changes based on entity types.
131
+ *
132
+ * For sharp trims (newEntity === null), both corner points are the intersection
133
+ * point and a single point is spliced/assigned (not two copies).
134
+ *
135
+ * For fillet, firstCornerPoint carries the arc bulge from arc.toPolylinePoints().
136
+ * This method transfers the bulge to whichever point comes first in the polyline
137
+ * traversal order, ensuring correct arc encoding.
138
+ *
139
+ * @param {Point} firstCornerPoint - trim/chamfer/tangent point on the first entity
140
+ * @param {Point} secondCornerPoint - trim/chamfer/tangent point on the second entity
141
+ * @param {Entity|null} newEntity - entity to add (Arc for fillet, Line for chamfer, null for sharp trim)
142
+ * @return {Array} - array of state changes
143
+ */
144
+ applyCornerTrim(firstCornerPoint, secondCornerPoint, newEntity) {
145
+ const isSharpTrim = newEntity === null;
146
+ const firstIsPolyline = this.firstPick.entity instanceof PolylineBase;
147
+ const secondIsPolyline = this.secondPick.entity instanceof PolylineBase;
148
+
149
+ // Two standalone entities: update both, optionally add the new entity.
103
150
  if (!firstIsPolyline && !secondIsPolyline) {
104
- return [
105
- new UpdateState(this.firstPick.entity, { points: [this.firstPick.lineKeptEnd(this.intersectionPoint), this.intersectionPoint] }),
106
- new UpdateState(this.secondPick.entity, { points: [this.secondPick.lineKeptEnd(this.intersectionPoint), this.intersectionPoint] }),
107
- ];
151
+ const stateChanges = isSharpTrim ? [] : [new AddState(newEntity)];
152
+ // Corner points may carry a bulge value from arc.toPolylinePoints() — a Line
153
+ // endpoint must never have a non-zero bulge or it renders as an arc.
154
+ // toPolylinePoints() returns fresh Point objects so direct mutation is safe.
155
+ firstCornerPoint.bulge = 0;
156
+ secondCornerPoint.bulge = 0;
157
+ stateChanges.push(
158
+ new UpdateState(this.firstPick.entity, { points: [this.firstPick.lineKeptEnd(this.intersectionPoint), firstCornerPoint] }),
159
+ new UpdateState(this.secondPick.entity, { points: [this.secondPick.lineKeptEnd(this.intersectionPoint), secondCornerPoint] }),
160
+ );
161
+ return stateChanges;
108
162
  }
109
163
 
110
- // Polyline + Polyline (same entity): replace the shared corner vertex (or open endpoints)
111
- // with the intersection point.
164
+ // Same polyline: splice the corner points into the point array.
112
165
  if (firstIsPolyline && secondIsPolyline && this.firstPick.entity === this.secondPick.entity) {
113
- const lastIdx = this.firstPick.entity.points.length - 1;
166
+ const closeSegIdx = this.firstPick.entity.points.length;
167
+ const lastIdx = closeSegIdx - 1;
114
168
  const segDiff = Math.abs(this.firstPick.segmentIndex - this.secondPick.segmentIndex);
115
169
  const isOpenEnds = !this.firstPick.entity.flags.hasFlag(1) && segDiff !== 1 && (
116
170
  (this.firstPick.segmentIndex === 1 && this.secondPick.segmentIndex === lastIdx) ||
117
171
  (this.firstPick.segmentIndex === lastIdx && this.secondPick.segmentIndex === 1)
118
172
  );
119
-
120
173
  const newPoints = this.firstPick.entity.points.map((p) => p.clone());
174
+ const stateChanges = [];
121
175
 
122
176
  if (isOpenEnds) {
123
- newPoints[0] = this.intersectionPoint.clone();
124
- newPoints[lastIdx] = this.intersectionPoint.clone();
177
+ if (!isSharpTrim) stateChanges.push(new AddState(newEntity));
178
+ if (this.firstPick.segmentIndex === 1) {
179
+ newPoints[0] = firstCornerPoint.clone();
180
+ newPoints[lastIdx] = secondCornerPoint.clone();
181
+ } else {
182
+ newPoints[0] = secondCornerPoint.clone();
183
+ newPoints[lastIdx] = firstCornerPoint.clone();
184
+ }
125
185
  } else {
126
- const closeSegIdx = this.firstPick.entity.points.length;
127
186
  const seg1 = this.firstPick.segmentIndex;
128
187
  const seg2 = this.secondPick.segmentIndex;
129
188
  const isClosingWrap = (seg1 === closeSegIdx && seg2 === 1) || (seg2 === closeSegIdx && seg1 === 1);
189
+ const isFirstLower = seg1 < seg2;
130
190
  const cornerIdx = isClosingWrap ? 0 : Math.min(seg1, seg2);
131
- newPoints.splice(cornerIdx, 1, this.intersectionPoint.clone());
191
+
192
+ if (isSharpTrim) {
193
+ // Sharp trim: splice in a single copy of the intersection point.
194
+ newPoints.splice(cornerIdx, 1, firstCornerPoint.clone());
195
+ } else {
196
+ // Chamfer/fillet: splice in two points. Ensure the bulge (if any) is on
197
+ // the lower point (the one that comes first in polyline traversal).
198
+ const swapped = isClosingWrap ? seg1 !== closeSegIdx : !isFirstLower;
199
+ const lowerSrc = swapped ? secondCornerPoint : firstCornerPoint;
200
+ const upperSrc = swapped ? firstCornerPoint : secondCornerPoint;
201
+ const arcBulge = firstCornerPoint.bulge || 0;
202
+ const lowerClone = lowerSrc.clone();
203
+ const upperClone = upperSrc.clone();
204
+ lowerClone.bulge = swapped ? -arcBulge : arcBulge;
205
+ upperClone.bulge = 0;
206
+ newPoints.splice(cornerIdx, 1, lowerClone, upperClone);
207
+ }
132
208
  }
133
- return [new UpdateState(this.firstPick.entity, { points: newPoints })];
209
+
210
+ stateChanges.push(new UpdateState(this.firstPick.entity, { points: newPoints }));
211
+ return stateChanges;
134
212
  }
135
213
 
136
- // Line + Polyline (or Polyline + Line): consume the line into the polyline.
137
- const poly = firstIsPolyline ? this.firstPick : this.secondPick;
138
- const line = firstIsPolyline ? this.secondPick : this.firstPick;
214
+ // Standalone + Polyline: consume the standalone entity into the polyline.
215
+ const [poly, standalone] = firstIsPolyline ? [this.firstPick, this.secondPick] : [this.secondPick, this.firstPick];
216
+ const polyCornerPoint = poly === this.firstPick ? firstCornerPoint : secondCornerPoint;
217
+ const standaloneCornerPoint = poly === this.firstPick ? secondCornerPoint : firstCornerPoint;
218
+ const arcBulge = firstCornerPoint.bulge || 0;
139
219
  const polySegIdx = poly.segmentIndex;
140
220
  const keepStart = poly.keepStart(this.intersectionPoint);
141
221
  let newPoints;
142
- if (keepStart) {
143
- // Keep the polyline points up to (not including) the selected segment start,
144
- // trim to the intersection and append the kept end of the line.
222
+ if (isSharpTrim) {
223
+ // Sharp trim: single intersection point between poly and standalone kept ends.
224
+ if (keepStart) {
225
+ newPoints = [
226
+ ...poly.entity.points.slice(0, polySegIdx).map((p) => p.clone()),
227
+ polyCornerPoint.clone(),
228
+ standalone.lineKeptEnd(this.intersectionPoint).clone(),
229
+ ];
230
+ } else {
231
+ newPoints = [
232
+ standalone.lineKeptEnd(this.intersectionPoint).clone(),
233
+ polyCornerPoint.clone(),
234
+ ...poly.entity.points.slice(polySegIdx).map((p) => p.clone()),
235
+ ];
236
+ }
237
+ } else if (keepStart) {
238
+ // Keep start: [...polyBefore, polyCorner(with bulge), standaloneCorner, lineKeptEnd]
239
+ // The arc runs from polyCorner to standaloneCorner in polyline traversal order.
240
+ const polyClone = polyCornerPoint.clone();
241
+ polyClone.bulge = poly === this.firstPick ? arcBulge : -arcBulge;
145
242
  newPoints = [
146
243
  ...poly.entity.points.slice(0, polySegIdx).map((p) => p.clone()),
147
- this.intersectionPoint.clone(),
148
- line.lineKeptEnd(this.intersectionPoint).clone(),
244
+ polyClone,
245
+ standaloneCornerPoint.clone(),
246
+ standalone.lineKeptEnd(this.intersectionPoint).clone(),
149
247
  ];
150
248
  } else {
151
- // Keep the polyline points from the selected segment start onwards,
152
- // prepending the kept end of the line and the intersection.
249
+ // Keep end: [lineKeptEnd, standaloneCorner(with negated bulge), polyCorner, ...polyAfter]
250
+ // The arc runs from standaloneCorner to polyCorner in reversed traversal.
251
+ const standaloneClone = standaloneCornerPoint.clone();
252
+ standaloneClone.bulge = poly === this.firstPick ? -arcBulge : arcBulge;
153
253
  newPoints = [
154
- line.lineKeptEnd(this.intersectionPoint).clone(),
155
- this.intersectionPoint.clone(),
254
+ standalone.lineKeptEnd(this.intersectionPoint).clone(),
255
+ standaloneClone,
256
+ polyCornerPoint.clone(),
156
257
  ...poly.entity.points.slice(polySegIdx).map((p) => p.clone()),
157
258
  ];
158
259
  }
159
260
  return [
160
- new RemoveState(line.entity),
261
+ new RemoveState(standalone.entity),
161
262
  new UpdateState(poly.entity, { points: newPoints }),
162
263
  ];
163
264
  }
@@ -7,6 +7,7 @@ import { Point } from '../entities/point.js';
7
7
  import { AddState } 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
  * Copy Command Class
@@ -77,17 +78,16 @@ export class Copy extends Tool {
77
78
  if (this.points.length >= 1) {
78
79
  const mousePoint = DesignCore.Mouse.pointOnScene();
79
80
 
80
- // Draw a line
81
- const points = [this.points.at(-1), mousePoint];
82
- DesignCore.Scene.tempEntities.create('Line', { points: points });
81
+ // Draw a rubber-band line from base point to mouse
82
+ DesignCore.Scene.auxiliaryEntities.add(new RubberBand([this.points.at(-1), mousePoint]));
83
83
 
84
84
  const delta = mousePoint.subtract(this.lastMousePoint || this.points[0]);
85
85
  this.lastMousePoint = mousePoint;
86
86
 
87
- for (let i = 0; i < DesignCore.Scene.selectionManager.selectedItems.length; i++) {
88
- const item = DesignCore.Scene.selectionManager.selectedItems[i];
89
- const offsetPoints = item.points.map((p) => new Point(p.x, p.y, p.bulge, p.sequence).add(delta));
90
- item.setProperty('points', offsetPoints);
87
+ for (let i = 0; i < DesignCore.Scene.selectionManager.selectedEntities.length; i++) {
88
+ const entity = DesignCore.Scene.selectionManager.selectedEntities[i];
89
+ const offsetPoints = entity.points.map((p) => new Point(p.x, p.y, p.bulge, p.sequence).add(delta));
90
+ entity.setProperty('points', offsetPoints);
91
91
  }
92
92
  }
93
93
  }
@@ -103,10 +103,10 @@ export class Copy extends Tool {
103
103
  const stateChanges = [];
104
104
 
105
105
  for (let i = 0; i < DesignCore.Scene.selectionManager.selectionSet.selectionSet.length; i++) {
106
- const copyofitem = Utils.cloneObject(DesignCore.Scene.entities.get(DesignCore.Scene.selectionManager.selectionSet.selectionSet[i]));
107
- const offsetPoints = copyofitem.points.map((p) => new Point(p.x, p.y, p.bulge, p.sequence).add(delta));
108
- copyofitem.setProperty('points', offsetPoints);
109
- const stateChange = new AddState(copyofitem);
106
+ const copyOfEntity = Utils.cloneObject(DesignCore.Scene.entities.get(DesignCore.Scene.selectionManager.selectionSet.selectionSet[i]));
107
+ const offsetPoints = copyOfEntity.points.map((p) => new Point(p.x, p.y, p.bulge, p.sequence).add(delta));
108
+ copyOfEntity.setProperty('points', offsetPoints);
109
+ const stateChange = new AddState(copyOfEntity);
110
110
  stateChanges.push(stateChange);
111
111
  }
112
112
 
@@ -56,11 +56,11 @@ export class Copybase extends Tool {
56
56
  * Perform the command
57
57
  */
58
58
  action() {
59
- // generate clipboard data from selected items
60
- const selectedItems = DesignCore.Scene.selectionManager.selectedItems;
59
+ // generate clipboard data from selected entities
60
+ const selectedEntities = DesignCore.Scene.selectionManager.selectedEntities;
61
61
 
62
62
  // create clipboard data
63
- DesignCore.Clipboard.Entities = selectedItems;
63
+ DesignCore.Clipboard.Entities = selectedEntities;
64
64
  // set base point
65
65
  DesignCore.Clipboard.BasePoint = this.points[0];
66
66
  }
@@ -52,10 +52,10 @@ export class Copyclip extends Tool {
52
52
  * Perform the command
53
53
  */
54
54
  action() {
55
- // generate clipboard data from selected items
56
- const selectedItems = DesignCore.Scene.selectionManager.selectedItems;
55
+ // generate clipboard data from selected entities
56
+ const selectedEntities = DesignCore.Scene.selectionManager.selectedEntities;
57
57
 
58
58
  // create clipboard data
59
- DesignCore.Clipboard.Entities = selectedItems;
59
+ DesignCore.Clipboard.Entities = selectedEntities;
60
60
  }
61
61
  }