@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,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
9
  import { BasePolyline } from '../entities/basePolyline.js';
9
- import { AddState, RemoveState, UpdateState } from '../lib/stateManager.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
 
@@ -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 { Line } from '../entities/line.js';
4
5
  import { BasePolyline } from '../entities/basePolyline.js';
5
- import { RemoveState, UpdateState } from '../lib/stateManager.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() {
98
+ validateSelection() {
99
+ if (!this.firstPick.entity) return null;
100
+ if (!this.firstPick.resolveEndpoints()) return null;
101
+
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 BasePolyline)) 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;
99
146
  const firstIsPolyline = this.firstPick.entity instanceof BasePolyline;
100
147
  const secondIsPolyline = this.secondPick.entity instanceof BasePolyline;
101
148
 
102
- // Line + Line: trim both lines to the intersection point.
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,9 +78,8 @@ 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;
@@ -92,13 +92,13 @@ export class Explode extends Tool {
92
92
  const block = insert.block;
93
93
  const blockItems = block.items;
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
99
  const copyofitem = Utils.cloneObject(blockItem);
100
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);
101
+ copyofitem.points = points;
102
102
  const stateChange = new AddState(copyofitem);
103
103
  stateChanges.push(stateChange);
104
104
  });