@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.
- package/.eslintrc.json +4 -0
- package/TODO.md +49 -6
- package/core/core/core.js +0 -3
- package/core/dimensions/alignedDimension.js +5 -4
- package/core/dimensions/angularDimension.js +6 -5
- package/core/dimensions/baseCircularDimension.js +3 -2
- package/core/dimensions/baseDimension.js +59 -93
- package/core/dimensions/baseLinearDimension.js +6 -5
- package/core/dimensions/diametricDimension.js +4 -3
- package/core/dimensions/dimension.js +21 -14
- package/core/dimensions/radialDimension.js +4 -3
- package/core/dimensions/rotatedDimension.js +12 -11
- package/core/entities/arc.js +112 -145
- package/core/entities/arctext.js +212 -174
- package/core/entities/basePolyline.js +33 -284
- package/core/entities/circle.js +77 -99
- package/core/entities/entity.js +114 -93
- package/core/entities/hatch.js +322 -249
- package/core/entities/insert.js +66 -81
- package/core/entities/line.js +21 -142
- package/core/entities/lwpolyline.js +16 -11
- package/core/entities/polyline.js +4 -4
- package/core/entities/rectangle.js +3 -1
- package/core/entities/solid.js +6 -15
- package/core/entities/text.js +188 -199
- package/core/lib/auxiliary/rubberBand.js +32 -0
- package/core/lib/auxiliary/selectionWindow.js +63 -0
- package/core/lib/auxiliary/snapPoint.js +113 -0
- package/core/lib/auxiliary/trackingLine.js +76 -0
- package/core/lib/canvas.js +228 -166
- package/core/lib/clipboard.js +7 -1
- package/core/lib/colour.js +16 -0
- package/core/lib/commandLine.js +1 -6
- package/core/lib/commandManager.js +18 -8
- package/core/lib/constants.js +28 -0
- package/core/lib/dxf/dxf.js +4 -3
- package/core/lib/entityManager.js +18 -11
- package/core/lib/inputManager.js +36 -13
- package/core/lib/intersect.js +25 -0
- package/core/lib/patterns.js +8 -1
- package/core/lib/plotOptions.js +67 -0
- package/core/lib/polylineUtils.js +75 -0
- package/core/lib/renderers/cairoRenderer.js +371 -0
- package/core/lib/renderers/canvasRenderer.js +315 -0
- package/core/lib/renderers/pdfRenderer.js +366 -0
- package/core/lib/renderers/rendererBase.js +284 -0
- package/core/lib/renderers/svgRenderer.js +343 -0
- package/core/lib/scene.js +27 -5
- package/core/lib/selectionManager.js +2 -2
- package/core/lib/settings.js +4 -2
- package/core/lib/snapping.js +4 -191
- package/core/lib/stateManager.js +30 -1
- package/core/lib/utils.js +21 -2
- package/core/properties/entityProperties.js +104 -0
- package/core/properties/property.js +106 -2
- package/core/properties/propertyManager.js +29 -13
- package/core/tables/block.js +4 -37
- package/core/tables/endblk.js +1 -1
- package/core/tables/layer.js +8 -0
- package/core/tables/styleManagerBase.js +3 -2
- package/core/tables/tableManagerBase.js +8 -3
- package/core/tools/chamfer.js +111 -137
- package/core/tools/chamferFilletBase.js +135 -34
- package/core/tools/copy.js +3 -3
- package/core/tools/explode.js +2 -2
- package/core/tools/extend.js +132 -19
- package/core/tools/fillet.js +86 -183
- package/core/tools/matchProp.js +2 -11
- package/core/tools/mirror.js +7 -6
- package/core/tools/move.js +3 -3
- package/core/tools/offset.js +3 -5
- package/core/tools/pasteclip.js +1 -1
- package/core/tools/rotate.js +6 -3
- package/core/tools/scale.js +1 -1
- package/core/tools/trim.js +265 -23
- package/core/tools/zoom.js +1 -1
- package/jest.config.js +6 -0
- package/package.json +5 -3
- package/test/dimensions/alignedDimension.test.js +14 -7
- package/test/dimensions/angularDimension.test.js +6 -6
- package/test/dimensions/baseDimension.test.js +69 -8
- package/test/dimensions/baseLinearDimension.test.js +62 -0
- package/test/dimensions/diametricDimension.test.js +6 -6
- package/test/dimensions/radialDimension.test.js +6 -6
- package/test/dimensions/rotatedDimension.test.js +14 -7
- package/test/entities/arc.test.js +80 -83
- package/test/entities/arctext.test.js +180 -53
- package/test/entities/basePolyline.test.js +129 -2
- package/test/entities/circle.test.js +57 -85
- package/test/entities/entity.test.js +42 -8
- package/test/entities/hatch.test.js +543 -70
- package/test/entities/insert.test.js +96 -1
- package/test/entities/line.test.js +15 -41
- package/test/entities/lwpolyline.test.js +104 -0
- package/test/entities/polyline.test.js +11 -0
- package/test/entities/rectangle.test.js +11 -0
- package/test/entities/solid.test.js +1 -1
- package/test/entities/text.test.js +294 -69
- package/test/integration/exportIntegration.reference.dxf +9 -9
- package/test/integration/exportIntegration.test.js +2 -2
- package/test/integration/intersectionIntegration.test.js +4 -5
- package/test/integration/pdfRendererIntegration.reference.pdf +17709 -0
- package/test/integration/pdfRendererIntegration.test.js +61 -0
- package/test/integration/svgRendererIntegration.reference.svg +3343 -0
- package/test/integration/svgRendererIntegration.test.js +61 -0
- package/test/integration/userInputIntegration.reference.dxf +1165 -105
- package/test/integration/userInputIntegration.test.js +239 -132
- package/test/lib/canvas.test.js +742 -123
- package/test/lib/constants.test.js +27 -0
- package/test/lib/entityManager.test.js +4 -4
- package/test/lib/inputManager.test.js +80 -19
- package/test/lib/intersect.test.js +83 -0
- package/test/lib/plotOptions.test.js +179 -0
- package/test/lib/rubberBand.test.js +84 -0
- package/test/lib/settings.test.js +5 -6
- package/test/lib/snapping.test.js +71 -129
- package/test/lib/stateManager.test.js +7 -7
- package/test/lib/utils.test.js +36 -2
- package/test/properties/entityProperties.test.js +132 -0
- package/test/properties/property.test.js +53 -0
- package/test/properties/propertyManager.test.js +3 -3
- package/test/renderers/canvasRenderer.test.js +317 -0
- package/test/renderers/pdfRenderer.test.js +271 -0
- package/test/renderers/rendererBase.test.js +213 -0
- package/test/renderers/svgRenderer.test.js +326 -0
- package/test/tables/block.test.js +31 -1
- package/test/tables/endblk.test.js +2 -2
- package/test/tables/layer.test.js +30 -0
- package/test/test-helpers/hatchGridRenderer.js +180 -0
- package/test/test-helpers/test-helpers.js +152 -0
- package/test/tools/chamfer.test.js +227 -5
- package/test/tools/chamferFilletBase.test.js +98 -9
- package/test/tools/copy.test.js +2 -2
- package/test/tools/explode.test.js +3 -3
- package/test/tools/extend.test.js +331 -0
- package/test/tools/fillet.test.js +296 -0
- package/test/tools/matchProp.test.js +5 -5
- package/test/tools/mirror.test.js +13 -13
- package/test/tools/offset.test.js +170 -3
- package/test/tools/pasteclip.test.js +5 -5
- package/test/tools/rotate.test.js +32 -2
- package/test/tools/scale.test.js +5 -5
- package/test/tools/trim.test.js +526 -0
- package/test/tools/zoom.test.js +7 -7
- 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
|
-
//
|
|
876
|
-
// The
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
expect(
|
|
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
|
+
});
|