@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
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Shared hatch pattern grid renderer.
3
+ *
4
+ * Draws a full hatch-pattern reference sheet onto any RendererBase-compatible
5
+ * renderer: a 10-column grid of all 82 hatch patterns (alternating square and
6
+ * circle boundaries), pattern name labels, a 5 mm border rectangle, and a
7
+ * title. Used by both the PDF and SVG integration tests so the visual output
8
+ * stays identical across renderer implementations.
9
+ *
10
+ * Usage:
11
+ * const renderer = new SomeRenderer(PAGE_W, PAGE_H);
12
+ * buildHatchGrid(renderer);
13
+ * const output = renderer.getOutput();
14
+ */
15
+
16
+ import { Core } from '../../core/core/core.js';
17
+ import { Hatch } from '../../core/entities/hatch.js';
18
+ import { BasePolyline } from '../../core/entities/basePolyline.js';
19
+ import { Point } from '../../core/entities/point.js';
20
+ import { Text } from '../../core/entities/text.js';
21
+
22
+ // ─── Page & grid constants ────────────────────────────────────────────────────
23
+
24
+ export const PAGE_W = 1190; // landscape A3
25
+ export const PAGE_H = 842;
26
+ export const MARGIN = 5 * 72 / 25.4; // 5 mm in points ≈ 14.17
27
+
28
+ export const COLS = 10;
29
+ export const CELL_W = 115;
30
+ export const CELL_H = 85;
31
+ export const SHAPE_R = 32; // half-side of square / radius of circle (64 pt total)
32
+ export const LABEL_FONT = 6;
33
+ export const TITLE_FONT = 14;
34
+
35
+ // ─── All hatch patterns ───────────────────────────────────────────────────────
36
+
37
+ export const PATTERNS = [
38
+ 'SOLID',
39
+ 'ANGLE', 'ANSI31', 'ANSI32', 'ANSI33', 'ANSI34', 'ANSI35', 'ANSI36', 'ANSI37', 'ANSI38',
40
+ 'AR-B816', 'AR-B816C', 'AR-B88', 'AR-BRELM', 'AR-BRSTD', 'AR-CONC',
41
+ 'AR-HBONE', 'AR-PARQ1', 'AR-RROOF', 'AR-RSHKE', 'AR-SAND',
42
+ 'BOX', 'BRASS', 'BRICK', 'BRSTONE', 'CLAY', 'CORK', 'CROSS',
43
+ 'DASH', 'DOLMIT', 'DOTS', 'EARTH', 'ESCHER', 'FLEX',
44
+ 'GOST_GLASS', 'GOST_WOOD', 'GOST_GROUND', 'GRASS', 'GRATE', 'GRAVEL',
45
+ 'HEX', 'HONEY', 'HOUND', 'INSUL',
46
+ 'ACAD_ISO02W100', 'ACAD_ISO03W100', 'ACAD_ISO04W100', 'ACAD_ISO05W100',
47
+ 'ACAD_ISO06W100', 'ACAD_ISO07W100', 'ACAD_ISO08W100', 'ACAD_ISO09W100',
48
+ 'ACAD_ISO10W100', 'ACAD_ISO11W100', 'ACAD_ISO12W100', 'ACAD_ISO13W100',
49
+ 'ACAD_ISO14W100', 'ACAD_ISO15W100',
50
+ 'JIS_LC_20', 'JIS_LC_20A', 'JIS_LC_8', 'JIS_LC_8A',
51
+ 'JIS_RC_10', 'JIS_RC_15', 'JIS_RC_18', 'JIS_RC_30', 'JIS_STN_1E', 'JIS_WOOD',
52
+ 'LINE', 'MUDST', 'NET', 'NET3', 'PLAST', 'PLASTI',
53
+ 'SACNCR', 'SQUARE', 'STARS', 'STEEL', 'SWAMP', 'TRANS', 'TRIANG', 'ZIGZAG',
54
+ ];
55
+
56
+ // ─── Private helpers ──────────────────────────────────────────────────────────
57
+
58
+ /**
59
+ * Return n evenly-spaced {x, y} points around a circle.
60
+ * Used to draw a visible circle outline via straight-line segments
61
+ * (renderers that lack native arc support use chord approximation).
62
+ *
63
+ * @param {number} cx
64
+ * @param {number} cy
65
+ * @param {number} r
66
+ * @param {number} [n=32]
67
+ * @return {{x: number, y: number}[]}
68
+ */
69
+ function circleOutlinePoints(cx, cy, r, n = 32) {
70
+ const pts = [];
71
+ for (let k = 0; k < n; k++) {
72
+ const a = (k / n) * 2 * Math.PI;
73
+ pts.push({ x: cx + r * Math.cos(a), y: cy + r * Math.sin(a) });
74
+ }
75
+ return pts;
76
+ }
77
+
78
+ // ─── Public API ───────────────────────────────────────────────────────────────
79
+
80
+ /**
81
+ * Draw the hatch pattern reference sheet onto `renderer`.
82
+ *
83
+ * Sets up an identity transform then draws:
84
+ * - A 5 mm border rectangle
85
+ * - A "Hatch Pattern Reference" title
86
+ * - A 10-column grid of all 82 hatch patterns, alternating square and circle
87
+ * boundaries, with pattern name labels beneath each shape
88
+ *
89
+ * `new Core()` is called internally because Hatch.draw() requires
90
+ * DesignCore.Scene.selectionManager to be present.
91
+ *
92
+ * @param {object} renderer
93
+ */
94
+ export function buildHatchGrid(renderer) {
95
+ // Required so that Hatch.draw() can access DesignCore.Scene.selectionManager
96
+ new Core();
97
+
98
+ // Identity transform — draw directly in page coordinates (Y-up, 1 unit = 1 pt)
99
+ renderer.setTransform({ a: 1, d: 1, e: 0, f: 0 });
100
+
101
+ // Border rectangle inset 5 mm from each edge
102
+ renderer.setColour({ r: 0, g: 0, b: 0 });
103
+ renderer.setLineWidth(1);
104
+ renderer.setDash([], 0);
105
+ renderer.drawShape([
106
+ { x: MARGIN, y: MARGIN },
107
+ { x: PAGE_W - MARGIN, y: MARGIN },
108
+ { x: PAGE_W - MARGIN, y: PAGE_H - MARGIN },
109
+ { x: MARGIN, y: PAGE_H - MARGIN },
110
+ ], { closed: true, stroke: true });
111
+
112
+ // Title
113
+ const title = 'Hatch Pattern Reference';
114
+ const titleX = PAGE_W / 2 - Text.getApproximateWidth(title, TITLE_FONT) / 2;
115
+ const titleY = PAGE_H - MARGIN - TITLE_FONT - 2;
116
+ renderer.setColour({ r: 0, g: 0, b: 0 });
117
+ renderer.setLineWidth(0.5);
118
+ renderer.drawText([{ char: title, x: titleX, y: titleY, rotation: 0 }], 'Helvetica', TITLE_FONT);
119
+
120
+ // Grid — centred horizontally, starting below the title
121
+ const xGrid = (PAGE_W - COLS * CELL_W) / 2;
122
+ const yGridTop = titleY - TITLE_FONT - 8;
123
+
124
+ PATTERNS.forEach((name, i) => {
125
+ const row = Math.floor(i / COLS);
126
+ const col = i % COLS;
127
+ const cellX = xGrid + col * CELL_W;
128
+ const cellYTop = yGridTop - row * CELL_H;
129
+
130
+ // Shape centre: 4 pt top padding + SHAPE_R inside the cell
131
+ const cx = cellX + CELL_W / 2;
132
+ const cy = cellYTop - 4 - SHAPE_R;
133
+
134
+ // Alternate square (even index) and circle (odd index) boundaries
135
+ let hatchBoundary;
136
+ let outlinePoints;
137
+
138
+ if (i % 2 === 0) {
139
+ hatchBoundary = new BasePolyline({ points: [
140
+ new Point(cx - SHAPE_R, cy - SHAPE_R),
141
+ new Point(cx + SHAPE_R, cy - SHAPE_R),
142
+ new Point(cx + SHAPE_R, cy + SHAPE_R),
143
+ new Point(cx - SHAPE_R, cy + SHAPE_R),
144
+ ] });
145
+ outlinePoints = hatchBoundary.toPolylinePoints();
146
+ } else {
147
+ // Two exact semicircle arcs (bulge = 1) for clipping; polygon for visible outline
148
+ hatchBoundary = new BasePolyline({ points: [
149
+ new Point(cx + SHAPE_R, cy, 1),
150
+ new Point(cx - SHAPE_R, cy, 1),
151
+ ] });
152
+ outlinePoints = circleOutlinePoints(cx, cy, SHAPE_R);
153
+ }
154
+
155
+ // AR-* patterns have large inter-line spacing; scale down so lines are visible
156
+ const scale = name.startsWith('AR-') ? 0.1 : 1;
157
+ const hatch = new Hatch({ patternName: name, scale });
158
+ hatch.setProperty('childEntities', [hatchBoundary]);
159
+ hatch.buildPatternCache();
160
+
161
+ // Draw hatch fill
162
+ renderer.setColour({ r: 0, g: 0, b: 0 });
163
+ renderer.setLineWidth(0.25);
164
+ renderer.setDash([], 0);
165
+ hatch.draw(renderer);
166
+
167
+ // Draw boundary outline on top
168
+ renderer.setColour({ r: 0, g: 0, b: 0 });
169
+ renderer.setLineWidth(0.5);
170
+ renderer.setDash([], 0);
171
+ renderer.drawShape(outlinePoints, { closed: true, stroke: true });
172
+
173
+ // Pattern name label, centred below the shape
174
+ const labelY = cellYTop - 4 - SHAPE_R * 2 - LABEL_FONT - 2;
175
+ const labelX = cellX + (CELL_W - Text.getApproximateWidth(name, LABEL_FONT)) / 2;
176
+ renderer.setColour({ r: 60, g: 60, b: 60 });
177
+ renderer.setLineWidth(0.25);
178
+ renderer.drawText([{ char: name, x: labelX, y: labelY, rotation: 0 }], 'Helvetica', LABEL_FONT);
179
+ });
180
+ }
@@ -40,6 +40,7 @@ export async function withMockInput(scene, inputs, testFn, options = {}) {
40
40
  return inputs[callCount++];
41
41
  }
42
42
  },
43
+ actionCommand: () => {},
43
44
  executeCommand: () => {},
44
45
  reset: () => {},
45
46
  ...extraMethods,
@@ -64,3 +65,154 @@ export async function withMockInput(scene, inputs, testFn, options = {}) {
64
65
  }
65
66
  }
66
67
 
68
+ /**
69
+ * A minimal RendererBase-compatible mock for use in unit tests.
70
+ *
71
+ * Tracks colour, line width and dash state so tests can assert on them.
72
+ * Optionally accepts a spy context object; when provided, `save`, `restore`
73
+ * and `applyTransform` delegate to it so tests can verify save/restore
74
+ * balance and transform calls via the context's own spies.
75
+ *
76
+ * @example
77
+ * const renderer = new MockRenderer();
78
+ * entity.draw(renderer);
79
+ * expect(renderer.strokeStyle).toBe('rgb(255,0,0)');
80
+ *
81
+ * @example
82
+ * const ctx = createSpyContext();
83
+ * const renderer = new MockRenderer(ctx);
84
+ * canvas.paint(ctx, 800, 600);
85
+ * expect(ctx.saveDepth).toBe(0); // all saves balanced
86
+ */
87
+ export class MockRenderer {
88
+ /**
89
+ * @param {object|null} [ctx=null] - optional spy context to delegate save/restore to
90
+ */
91
+ constructor(ctx = null) {
92
+ this._ctx = ctx;
93
+ this.strokeStyle = '';
94
+ this.fillStyle = '';
95
+ this.lineWidth = 0;
96
+ this.dashPattern = null;
97
+ }
98
+
99
+ /** @param {{ r: number, g: number, b: number }} rgb */
100
+ setColour(rgb) {
101
+ this.strokeStyle = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
102
+ this.fillStyle = this.strokeStyle;
103
+ if (this._ctx) {
104
+ this._ctx.strokeStyle = this.strokeStyle;
105
+ this._ctx.fillStyle = this.fillStyle;
106
+ }
107
+ }
108
+
109
+ /** @param {number} w */
110
+ setLineWidth(w) {
111
+ this.lineWidth = w;
112
+ if (this._ctx) this._ctx.lineWidth = w;
113
+ }
114
+
115
+ /** @param {number[]} pattern */
116
+ setDash(pattern) {
117
+ this.dashPattern = pattern;
118
+ }
119
+
120
+ /** @param {object} matrix */
121
+ setTransform(matrix) {}
122
+
123
+ /** @param {Function} styleTransform */
124
+ setStyle(styleTransform) {
125
+ this._styleTransform = styleTransform;
126
+ }
127
+
128
+ /** @param {{ r: number, g: number, b: number }|null} colour */
129
+ setBackgroundColour(colour) {
130
+ this._backgroundColour = colour;
131
+ }
132
+
133
+ /** @return {{ r: number, g: number, b: number }|null} */
134
+ /**
135
+ * @return {object|null}
136
+ */
137
+ getBackgroundColour() {
138
+ return this._backgroundColour ?? null;
139
+ }
140
+
141
+ /** */
142
+ fillBackground() {}
143
+
144
+ /**
145
+ * @param {Array} points
146
+ * @param {object} [options]
147
+ */
148
+ drawShape(points, options) {}
149
+
150
+ /**
151
+ * @param {Array} characters
152
+ * @param {string} fontName
153
+ * @param {number} height
154
+ */
155
+ drawText(characters, fontName, height) {}
156
+
157
+ /** Save current drawing state. */
158
+ save() {
159
+ if (this._ctx) this._ctx.save();
160
+ }
161
+
162
+ /** Restore previously saved drawing state. */
163
+ restore() {
164
+ if (this._ctx) this._ctx.restore();
165
+ }
166
+
167
+ /**
168
+ * @param {object} [transform]
169
+ * @param {number} [transform.x=0]
170
+ * @param {number} [transform.y=0]
171
+ */
172
+ applyTransform({ x = 0, y = 0 } = {}) {
173
+ if (this._ctx) this._ctx.translate(x, y);
174
+ }
175
+
176
+ /**
177
+ * @param {boolean} [isHighlighted=false]
178
+ * @param {object|null} [colour=null]
179
+ * @param {number} [lineWidthDelta=0]
180
+ */
181
+ setHighlight(isHighlighted = false, colour = null, lineWidthDelta = 0) {
182
+ this.isHighlighted = isHighlighted;
183
+ this.highlightColour = colour;
184
+ this.highlightLineWidthDelta = lineWidthDelta;
185
+ }
186
+
187
+ /** @return {number} */
188
+ measureText() {
189
+ return 0;
190
+ }
191
+
192
+ /** @return {number} */
193
+ measureCharWidth() {
194
+ return 0;
195
+ }
196
+
197
+ /**
198
+ * @param {Array} [segments]
199
+ * @param {Array} [dashes]
200
+ */
201
+ drawSegments(segments, dashes) {}
202
+
203
+ /** @param {Array} [points] */
204
+ tracePath(points) {}
205
+
206
+ /** Start a new path. */
207
+ beginPath() {}
208
+
209
+ /** Stroke the current path. */
210
+ stroke() {}
211
+
212
+ /** Close the current path. */
213
+ closePath() {}
214
+
215
+ /** @param {object} [options] */
216
+ applyPath(options) {}
217
+ }
218
+
@@ -1,6 +1,7 @@
1
1
  import { Core } from '../../core/core/core.js';
2
2
  import { Point } from '../../core/entities/point.js';
3
3
  import { Chamfer } from '../../core/tools/chamfer.js';
4
+ import { DesignCore } from '../../core/designCore.js';
4
5
  import { Strings } from '../../core/lib/strings.js';
5
6
  import { SingleSelection } from '../../core/lib/selectionManager.js';
6
7
  import { expect, jest } from '@jest/globals';
@@ -872,11 +873,20 @@ test('Chamfer.action dist>0 trimMode=true closed Lwpolyline closing-wrap (seg4+s
872
873
  const poly = core.scene.entities.get(0);
873
874
  // Shared vertex (0,0) at index 0 replaced by two chamfer points → total 5 points
874
875
  expect(poly.points.length).toBe(5);
875
- // Endpoints must NOT have been moved to the intersection (open-ends symptom)
876
- // The first two new points should be the chamfer points, not (0,0)/(0,0)
877
- const allAtOrigin = poly.points[0].x === 0 && poly.points[0].y === 0 &&
878
- poly.points[1].x === 0 && poly.points[1].y === 0;
879
- expect(allAtOrigin).toBe(false);
876
+ // Verify the actual chamfer point coordinates and traversal order.
877
+ // The closing segment (0,10)→(0,0) is chamfered at (0,2),
878
+ // seg 1 (0,0)→(10,0) is chamfered at (2,0).
879
+ // After splice: [(0,2), (2,0), (10,0), (10,10), (0,10)]
880
+ expect(poly.points[0].x).toBeCloseTo(0);
881
+ expect(poly.points[0].y).toBeCloseTo(2);
882
+ expect(poly.points[1].x).toBeCloseTo(2);
883
+ expect(poly.points[1].y).toBeCloseTo(0);
884
+ expect(poly.points[2].x).toBeCloseTo(10);
885
+ expect(poly.points[2].y).toBeCloseTo(0);
886
+ expect(poly.points[3].x).toBeCloseTo(10);
887
+ expect(poly.points[3].y).toBeCloseTo(10);
888
+ expect(poly.points[4].x).toBeCloseTo(0);
889
+ expect(poly.points[4].y).toBeCloseTo(10);
880
890
  });
881
891
 
882
892
  test('Chamfer.action dist>0 trimMode=false closed Lwpolyline closing-wrap: standalone chamfer Line only', () => {
@@ -968,3 +978,215 @@ test('Chamfer.action dist>0 trimMode=false Line + Lwpolyline: standalone chamfer
968
978
  expect(core.scene.entities.get(0).points.length).toBe(2);
969
979
  expect(core.scene.entities.get(1).points.length).toBe(4);
970
980
  });
981
+
982
+ // ─── action: entity property inheritance ─────────────────────────────────────
983
+
984
+ test('Chamfer.action chamfer line inherits layer, lineWidth, lineType from first entity', () => {
985
+ // Perpendicular L-corner at origin; first entity on a non-default layer.
986
+ core.scene.clear();
987
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)], layer: 'walls', lineWidth: 5, lineType: 'DASHED' });
988
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
989
+
990
+ core.scene.headers.chamferDistanceA = 2;
991
+ core.scene.headers.chamferDistanceB = 2;
992
+ core.scene.headers.chamferMode = false;
993
+ core.scene.headers.trimMode = false;
994
+
995
+ const chamfer = new Chamfer();
996
+ chamfer.firstPick.entity = core.scene.entities.get(0);
997
+ chamfer.secondPick.entity = core.scene.entities.get(1);
998
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
999
+ chamfer.secondPick.clickPoint = new Point(0, 5);
1000
+ chamfer.action();
1001
+
1002
+ expect(core.scene.entities.count()).toBe(3);
1003
+ const chamferLine = core.scene.entities.get(2);
1004
+ expect(chamferLine.type).toBe('Line');
1005
+ expect(chamferLine.getProperty('layer')).toBe('walls');
1006
+ expect(chamferLine.getProperty('lineWidth')).toBe(5);
1007
+ expect(chamferLine.getProperty('lineType')).toBe('DASHED');
1008
+ });
1009
+
1010
+ test('Chamfer.preview does not throw', () => {
1011
+ const chamfer = new Chamfer();
1012
+ expect(() => chamfer.preview()).not.toThrow();
1013
+ });
1014
+
1015
+ test('Chamfer.preview returns early when findClosestItem returns undefined', () => {
1016
+ core.scene.clear();
1017
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
1018
+ DesignCore.Scene.previewEntities.clear();
1019
+
1020
+ const chamfer = new Chamfer();
1021
+ chamfer.firstPick.entity = core.scene.entities.get(0);
1022
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
1023
+
1024
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(undefined);
1025
+ DesignCore.Mouse.pointOnScene = () => new Point(0, 5);
1026
+ chamfer.preview();
1027
+ findSpy.mockRestore();
1028
+
1029
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
1030
+ });
1031
+
1032
+ test('Chamfer.preview adds chamfer line to previewEntities in no-trim mode', () => {
1033
+ core.scene.clear();
1034
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
1035
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
1036
+ DesignCore.Scene.previewEntities.clear();
1037
+
1038
+ core.scene.headers.chamferDistanceA = 2;
1039
+ core.scene.headers.chamferDistanceB = 2;
1040
+ core.scene.headers.chamferMode = false;
1041
+ core.scene.headers.trimMode = false;
1042
+
1043
+ const chamfer = new Chamfer();
1044
+ chamfer.firstPick.entity = core.scene.entities.get(0);
1045
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
1046
+
1047
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
1048
+ DesignCore.Mouse.pointOnScene = () => new Point(0, 5);
1049
+ chamfer.preview();
1050
+ findSpy.mockRestore();
1051
+
1052
+ expect(DesignCore.Scene.previewEntities.count()).toBeGreaterThanOrEqual(1);
1053
+ });
1054
+
1055
+ test('Chamfer.preview skips chamfer line in no-trim mode when distances are both 0 (sharp corner)', () => {
1056
+ core.scene.clear();
1057
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
1058
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
1059
+ DesignCore.Scene.previewEntities.clear();
1060
+
1061
+ core.scene.headers.chamferDistanceA = 0;
1062
+ core.scene.headers.chamferDistanceB = 0;
1063
+ core.scene.headers.chamferMode = false;
1064
+ core.scene.headers.trimMode = false;
1065
+
1066
+ const chamfer = new Chamfer();
1067
+ chamfer.firstPick.entity = core.scene.entities.get(0);
1068
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
1069
+
1070
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
1071
+ DesignCore.Mouse.pointOnScene = () => new Point(0, 5);
1072
+ chamfer.preview();
1073
+ findSpy.mockRestore();
1074
+
1075
+ expect(DesignCore.Scene.previewEntities.count()).toBe(0);
1076
+ });
1077
+
1078
+ test('Chamfer.preview adds 5 entities to previewEntities in trim mode', () => {
1079
+ core.scene.clear();
1080
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
1081
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
1082
+ DesignCore.Scene.previewEntities.clear();
1083
+
1084
+ core.scene.headers.chamferDistanceA = 2;
1085
+ core.scene.headers.chamferDistanceB = 2;
1086
+ core.scene.headers.chamferMode = false;
1087
+ core.scene.headers.trimMode = true;
1088
+
1089
+ const chamfer = new Chamfer();
1090
+ chamfer.firstPick.entity = core.scene.entities.get(0);
1091
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
1092
+
1093
+ const findSpy = jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(1);
1094
+ DesignCore.Mouse.pointOnScene = () => new Point(0, 5);
1095
+ chamfer.preview();
1096
+ findSpy.mockRestore();
1097
+
1098
+ // dulled firstSeg + dulled secondSeg + firstTrimLine + secondTrimLine + chamferLine = 5
1099
+ expect(DesignCore.Scene.previewEntities.count()).toBeGreaterThanOrEqual(5);
1100
+ });
1101
+
1102
+ test('Chamfer.execute returns early when second-entity input is undefined (inner loop)', async () => {
1103
+ core.scene.clear();
1104
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
1105
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
1106
+
1107
+ const executeCommandSpy = jest.fn();
1108
+ await withMockInput(
1109
+ core.scene,
1110
+ [new SingleSelection(0, new Point(-5, 0))],
1111
+ async () => {
1112
+ const chamfer = new Chamfer();
1113
+ await chamfer.execute();
1114
+ expect(executeCommandSpy).not.toHaveBeenCalled();
1115
+ },
1116
+ { extraMethods: { executeCommand: executeCommandSpy } },
1117
+ );
1118
+ });
1119
+
1120
+ test('Chamfer.execute notifies NONCONSECUTIVESEGMENTS for non-adjacent polyline segments', async () => {
1121
+ // 5-point polyline → 4 segments. Pick segment 1 (near -15,0) then segment 3 (near 5,0).
1122
+ // Neither consecutive nor open-ends → NONCONSECUTIVESEGMENTS, then undefined to exit.
1123
+ core.scene.clear();
1124
+ core.scene.addItem('Lwpolyline', { points: [new Point(-20, 0), new Point(-10, 0), new Point(0, 0), new Point(10, 0), new Point(20, 0)] });
1125
+
1126
+ const notifySpy = jest.spyOn(core, 'notify');
1127
+ await withMockInput(
1128
+ core.scene,
1129
+ [new SingleSelection(0, new Point(-15, 0)), new SingleSelection(0, new Point(5, 0)), undefined],
1130
+ async () => {
1131
+ const chamfer = new Chamfer();
1132
+ await chamfer.execute();
1133
+ },
1134
+ );
1135
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Message.NONCONSECUTIVESEGMENTS));
1136
+ notifySpy.mockRestore();
1137
+ });
1138
+
1139
+ test('Chamfer.execute Method→Distance sets chamferMode to false', async () => {
1140
+ core.scene.headers.chamferMode = true;
1141
+
1142
+ await withMockInput(
1143
+ core.scene,
1144
+ ['Method', 'Distance', undefined],
1145
+ async () => {
1146
+ const chamfer = new Chamfer();
1147
+ await chamfer.execute();
1148
+ },
1149
+ );
1150
+
1151
+ expect(core.scene.headers.chamferMode).toBe(false);
1152
+ });
1153
+
1154
+ test('Chamfer.execute Trim→Trim keeps trimMode=true', async () => {
1155
+ core.scene.headers.trimMode = true;
1156
+
1157
+ await withMockInput(
1158
+ core.scene,
1159
+ ['Trim', 'Trim', undefined],
1160
+ async () => {
1161
+ const chamfer = new Chamfer();
1162
+ await chamfer.execute();
1163
+ },
1164
+ );
1165
+
1166
+ expect(core.scene.headers.trimMode).toBe(true);
1167
+ });
1168
+
1169
+ test('Chamfer.action angle method angle=180 notifies INVALIDNUMBER', () => {
1170
+ // alpha = 180 * π/180 = π; checks alpha >= Math.PI → INVALIDNUMBER
1171
+ core.scene.clear();
1172
+ core.scene.addItem('Line', { points: [new Point(-10, 0), new Point(0, 0)] });
1173
+ core.scene.addItem('Line', { points: [new Point(0, 0), new Point(0, 10)] });
1174
+
1175
+ core.scene.headers.chamferLength = 2;
1176
+ core.scene.headers.chamferAngle = 180;
1177
+ core.scene.headers.chamferMode = true;
1178
+ core.scene.headers.trimMode = true;
1179
+
1180
+ const notifySpy = jest.spyOn(core, 'notify');
1181
+
1182
+ const chamfer = new Chamfer();
1183
+ chamfer.firstPick.entity = core.scene.entities.get(0);
1184
+ chamfer.secondPick.entity = core.scene.entities.get(1);
1185
+ chamfer.firstPick.clickPoint = new Point(-5, 0);
1186
+ chamfer.secondPick.clickPoint = new Point(0, 5);
1187
+ chamfer.action();
1188
+
1189
+ expect(notifySpy).toHaveBeenCalledWith(expect.stringContaining(Strings.Error.INVALIDNUMBER));
1190
+ expect(core.scene.entities.count()).toBe(2);
1191
+ notifySpy.mockRestore();
1192
+ });