@design-core/core 51.1.0 → 51.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ # Publishes @design-core/core to npm when a semver tag (e.g. v51.1.0) is pushed.
2
+ # Requires an NPM_TOKEN repository secret (an "Automation" type npm access token).
3
+ name: Publish to npm
4
+
5
+ on:
6
+ push:
7
+ tags:
8
+ - 'v*.*.*'
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v3
16
+
17
+ - name: Use Node.js
18
+ uses: actions/setup-node@v3
19
+ with:
20
+ node-version: 18.x
21
+ registry-url: 'https://registry.npmjs.org'
22
+
23
+ - run: npm ci
24
+ - run: npm test -- --coverage
25
+ - run: npx eslint . --ext .js
26
+
27
+ # Fail fast if the tag and package.json version have drifted apart
28
+ - name: Verify tag matches package.json version
29
+ run: |
30
+ PKG_VERSION=$(node -p "require('./package.json').version")
31
+ TAG_VERSION=${GITHUB_REF_NAME#v}
32
+ if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
33
+ echo "Tag v$TAG_VERSION does not match package.json version $PKG_VERSION"
34
+ exit 1
35
+ fi
36
+
37
+ - name: Publish to npm
38
+ run: npm publish --access public
39
+ env:
40
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md CHANGED
@@ -14,10 +14,23 @@ Design-Core uses the industry standard Drawing Exchange Format (DXF) to read and
14
14
  * Design-Web: https://github.com/dubstar-04/Design-Web
15
15
  * HTML Canvas: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
16
16
  * Cairo: https://www.cairographics.org/
17
+ * npm package: https://www.npmjs.com/package/@design-core/core
17
18
 
18
19
 
19
20
  ## Publishing
20
21
 
21
- npm login
22
+ Releases are published to npm automatically by the [Publish to npm](.github/workflows/publish.yml) workflow whenever a `v*.*.*` tag is pushed:
23
+
24
+ ```bash
25
+ npm version patch # or minor / major, bumps package.json and creates a git tag
26
+ git push --follow-tags
27
+ ```
28
+
29
+ The workflow runs the test suite and linter, verifies the tag matches `package.json`'s version, then publishes.
22
30
 
31
+ To publish manually instead:
32
+
33
+ ```bash
34
+ npm login
23
35
  npm publish --access public
36
+ ```
@@ -305,7 +305,9 @@ export class Canvas {
305
305
  */
306
306
  exportTo(renderer, plotOptions) {
307
307
  let area;
308
- if (plotOptions.plotArea === PlotOptions.Area.DISPLAY) {
308
+ if (plotOptions.plotArea === PlotOptions.Area.WINDOW) {
309
+ area = new BoundingBox(plotOptions.windowArea.point1, plotOptions.windowArea.point2);
310
+ } else if (plotOptions.plotArea === PlotOptions.Area.DISPLAY) {
309
311
  const viewport = this.getSceneOffset();
310
312
  area = new BoundingBox(new Point(viewport.xmin, viewport.ymin), new Point(viewport.xmax, viewport.ymax));
311
313
  } else {
@@ -318,6 +320,7 @@ export class Canvas {
318
320
  pageWidth: plotOptions.pageWidth,
319
321
  pageHeight: plotOptions.pageHeight,
320
322
  plotScale: plotOptions.plotScale,
323
+ margin: plotOptions.margin,
321
324
  });
322
325
  if (!matrix) return false;
323
326
 
@@ -43,6 +43,9 @@ import { Purge } from '../tools/purge.js';
43
43
  import { Pan } from '../tools/pan.js';
44
44
  import { Zoom } from '../tools/zoom.js';
45
45
 
46
+ // Internal tools (not shown in any toolbar, invoked programmatically)
47
+ import { WindowPick } from '../tools/windowPick.js';
48
+
46
49
  // Clipboard tools
47
50
  import { Copybase } from '../tools/copybase.js';
48
51
  import { Copyclip } from '../tools/copyclip.js';
@@ -99,6 +102,8 @@ const classes = {
99
102
  Zoom,
100
103
  Pan,
101
104
 
105
+ WindowPick,
106
+
102
107
  Copybase,
103
108
  Copyclip,
104
109
  Cutclip,
@@ -39,6 +39,7 @@ export class InputManager {
39
39
  }
40
40
 
41
41
  this.promptOption = undefined;
42
+ DesignCore.Mouse.reset();
42
43
  DesignCore.Scene.reset();
43
44
  DesignCore.Canvas.setCursorForInputTypes([]);
44
45
  }
package/core/lib/mouse.js CHANGED
@@ -15,6 +15,16 @@ export class Mouse {
15
15
  this.lastButton = 0; // last button for double click check
16
16
  }
17
17
 
18
+ /**
19
+ * Clear button state - guards against a stuck button when a mouseUp
20
+ * never reaches the canvas (e.g. focus stolen by another window mid-click)
21
+ */
22
+ reset() {
23
+ this.buttonOneDown = false;
24
+ this.buttonTwoDown = false;
25
+ this.buttonThreeDown = false;
26
+ }
27
+
18
28
  /**
19
29
  * Returns the point on the design canvas
20
30
  * @return {Point}
@@ -9,6 +9,7 @@ export class PlotOptions {
9
9
  static Area = {
10
10
  EXTENTS: 'Extents',
11
11
  DISPLAY: 'Display',
12
+ WINDOW: 'Window',
12
13
  };
13
14
 
14
15
  static #fileTypes = ['pdf', 'svg'];
@@ -22,6 +23,8 @@ export class PlotOptions {
22
23
  this.setOption('pageHeight', pageHeight);
23
24
  this.plotScale = null;
24
25
  this.plotArea = PlotOptions.Area.EXTENTS;
26
+ this.windowArea = null;
27
+ this.margin = 40;
25
28
  this.style = RendererBase.Styles.NONE;
26
29
  this.fileType = 'pdf';
27
30
  }
@@ -49,6 +52,20 @@ export class PlotOptions {
49
52
  throw new Error(`PlotOptions: plotArea must be one of: ${Object.values(PlotOptions.Area).join(', ')}`);
50
53
  }
51
54
  break;
55
+ case 'windowArea':
56
+ // required when plotArea is WINDOW - two points defining the plot window, e.g. { point1, point2 }
57
+ if (value !== null && (!value || value.point1 === undefined || value.point2 === undefined)) {
58
+ throw new Error('PlotOptions: windowArea must be null or an object with point1 and point2');
59
+ }
60
+ break;
61
+ case 'margin':
62
+ if (!Number.isFinite(value) || value < 0) {
63
+ throw new Error('PlotOptions: margin must be a non-negative finite number');
64
+ }
65
+ if (this.pageWidth - 2 * value <= 0 || this.pageHeight - 2 * value <= 0) {
66
+ throw new Error('PlotOptions: margin leaves no usable page area');
67
+ }
68
+ break;
52
69
  case 'style':
53
70
  if (typeof value !== 'function') {
54
71
  throw new Error('PlotOptions: style must be a function');
@@ -110,6 +110,8 @@ export class Strings {
110
110
  DESTINATIONSET: 'Select destination object(s)',
111
111
  MIRRORFIRST: 'Specify first point of mirror line',
112
112
  MIRRORSECOND: 'Specify second point of mirror line',
113
+ FIRSTCORNER: 'Specify first corner',
114
+ SECONDCORNER: 'Specify second corner',
113
115
  ERASESOURCE: 'Erase source objects?',
114
116
  SCALEFACTOR: 'Specify scale factor',
115
117
  REFERENCELENGTH: 'Specify reference length',
@@ -0,0 +1,75 @@
1
+ import { Tool } from './tool.js';
2
+ import { Input, PromptOptions } from '../lib/inputManager.js';
3
+ import { Strings } from '../lib/strings.js';
4
+ import { Logging } from '../lib/logging.js';
5
+ import { Point } from '../entities/point.js';
6
+
7
+ import { DesignCore } from '../designCore.js';
8
+
9
+ /**
10
+ * WindowPick Command Class
11
+ * Internal tool used to interactively pick two corner points defining a rectangular window (e.g. for the plot window area).
12
+ * Not shown in any toolbar; invoked programmatically.
13
+ * @extends Tool
14
+ */
15
+ export class WindowPick extends Tool {
16
+ static type = 'WindowPick';
17
+
18
+ /** Create a WindowPick command */
19
+ constructor() {
20
+ super();
21
+ }
22
+
23
+ /**
24
+ * Register the command
25
+ * @return {Object}
26
+ * command = name of the command
27
+ */
28
+ static register() {
29
+ const command = { command: 'WindowPick' };
30
+ return command;
31
+ }
32
+
33
+ /**
34
+ * Execute method
35
+ * requests the two corner points defining the plot window
36
+ */
37
+ async execute() {
38
+ try {
39
+ const op1 = new PromptOptions(Strings.Input.FIRSTCORNER, [Input.Type.POINT]);
40
+ const pt1 = await DesignCore.Scene.inputManager.requestInput(op1);
41
+ if (pt1 === undefined) return;
42
+ this.points.push(pt1);
43
+
44
+ const op2 = new PromptOptions(Strings.Input.SECONDCORNER, [Input.Type.POINT]);
45
+ const pt2 = await DesignCore.Scene.inputManager.requestInput(op2);
46
+ if (pt2 === undefined) return;
47
+ this.points.push(pt2);
48
+
49
+ DesignCore.Scene.inputManager.executeCommand();
50
+ } catch (err) {
51
+ Logging.instance.error(`${this.type} - ${err}`);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Preview the command during execution
57
+ */
58
+ preview() {
59
+ if (this.points.length === 1) {
60
+ const mousePoint = DesignCore.Mouse.pointOnScene();
61
+ const p1 = this.points[0];
62
+ const p2 = mousePoint;
63
+
64
+ const points = [
65
+ new Point(p1.x, p1.y),
66
+ new Point(p2.x, p1.y),
67
+ new Point(p2.x, p2.y),
68
+ new Point(p1.x, p2.y),
69
+ new Point(p1.x, p1.y),
70
+ ];
71
+
72
+ DesignCore.Scene.previewEntities.create('Polyline', { points: points });
73
+ }
74
+ }
75
+ }
package/cspell.json CHANGED
@@ -195,6 +195,7 @@
195
195
  "RRGGBB",
196
196
  "rubberbandcolour",
197
197
  "SCALEFACTOR",
198
+ "SECONDCORNER",
198
199
  "selectedchild",
199
200
  "SELECTIONSET",
200
201
  "SEQEND",
@@ -277,6 +278,7 @@
277
278
  "INVALIDFILE",
278
279
  "INVALIDGROUPCODE",
279
280
  "INVALIDINTERSECTTYPE",
281
+ "FIRSTCORNER",
280
282
  "INVALIDNUMBER",
281
283
  "INVALIDPOINT",
282
284
  "INVALIDTYPE",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@design-core/core",
3
- "version": "51.1.0",
3
+ "version": "51.2.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
@@ -19,7 +19,12 @@
19
19
  ".": "./core/core/core.js",
20
20
  "./designCore.js": "./core/designCore.js",
21
21
  "./property.js": "./core/properties/property.js",
22
- "./canvasRenderer.js": "./core/lib/renderers/canvasRenderer.js"
22
+ "./canvasRenderer.js": "./core/lib/renderers/canvasRenderer.js",
23
+ "./constants.js": "./core/lib/constants.js",
24
+ "./plotOptions.js": "./core/lib/plotOptions.js",
25
+ "./rendererBase.js": "./core/lib/renderers/rendererBase.js",
26
+ "./svgRenderer.js": "./core/lib/renderers/svgRenderer.js",
27
+ "./pdfRenderer.js": "./core/lib/renderers/pdfRenderer.js"
23
28
  },
24
29
  "directories": {
25
30
  "test": "test"
@@ -0,0 +1,113 @@
1
+ import { Core } from '../../core/core/core.js';
2
+ import { Point } from '../../core/entities/point.js';
3
+ import { Constants } from '../../core/lib/constants.js';
4
+ import { PlotOptions } from '../../core/lib/plotOptions.js';
5
+ import { BoundingBox } from '../../core/lib/boundingBox.js';
6
+ import { SvgRenderer } from '../../core/lib/renderers/svgRenderer.js';
7
+ import { jest } from '@jest/globals';
8
+
9
+ // smallest -> largest; each rectangle fully contains all previous ones when aligned at the origin
10
+ const PAGE_NAMES_ASC = ['A4', 'A3', 'A2', 'A1', 'A0'];
11
+
12
+ // Portrait uses the raw (width < height) page size; Landscape swaps the two axes,
13
+ // matching the orientation toggle in the GTK plot dialog (plotWindow.js)
14
+ const ORIENTATIONS = {
15
+ Portrait: (size) => ({ pageWidth: size.width, pageHeight: size.height }),
16
+ Landscape: (size) => ({ pageWidth: size.height, pageHeight: size.width }),
17
+ };
18
+
19
+ /**
20
+ * Build a closed 4-corner loop, mirroring Rectangle.rectPoints()
21
+ * @param {Point} pt1
22
+ * @param {Point} pt2
23
+ * @return {Array}
24
+ */
25
+ function rectPoints(pt1, pt2) {
26
+ return [
27
+ new Point(pt1.x, pt1.y),
28
+ new Point(pt2.x, pt1.y),
29
+ new Point(pt2.x, pt2.y),
30
+ new Point(pt1.x, pt2.y),
31
+ new Point(pt1.x, pt1.y),
32
+ ];
33
+ }
34
+
35
+ describe.each(Object.keys(ORIENTATIONS))('%s orientation', (orientation) => {
36
+ const toPage = ORIENTATIONS[orientation];
37
+
38
+ test.each(PAGE_NAMES_ASC)('Extents matches %s page size exactly', (name) => {
39
+ const { pageWidth, pageHeight } = toPage(Constants.PageSizes[name]);
40
+ const core = new Core();
41
+ core.activate();
42
+ core.scene.addEntity('Polyline', { points: rectPoints(new Point(0, 0), new Point(pageWidth, pageHeight)) });
43
+
44
+ const area = core.scene.boundingBox();
45
+ expect(area.xLength).toBeCloseTo(pageWidth, 5);
46
+ expect(area.yLength).toBeCloseTo(pageHeight, 5);
47
+
48
+ const matrix = core.canvas.buildExportMatrix({
49
+ area, pageWidth, pageHeight, plotScale: 1, margin: 0,
50
+ });
51
+ expect(matrix.a).toBe(1);
52
+ expect(matrix.e).toBeCloseTo(0, 5);
53
+ expect(matrix.f).toBeCloseTo(0, 5);
54
+ });
55
+
56
+ test('Extents grows to match each page size as rectangles are added smallest to largest', () => {
57
+ const core = new Core();
58
+ core.activate();
59
+
60
+ for (const name of PAGE_NAMES_ASC) {
61
+ const { pageWidth, pageHeight } = toPage(Constants.PageSizes[name]);
62
+ core.scene.addEntity('Polyline', { points: rectPoints(new Point(0, 0), new Point(pageWidth, pageHeight)) });
63
+
64
+ const area = core.scene.boundingBox();
65
+ expect(area.xLength).toBeCloseTo(pageWidth, 5);
66
+ expect(area.yLength).toBeCloseTo(pageHeight, 5);
67
+ }
68
+ });
69
+
70
+ test('Window area isolates each nested rectangle regardless of surrounding sizes', () => {
71
+ const core = new Core();
72
+ core.activate();
73
+
74
+ const rects = {};
75
+ for (const name of PAGE_NAMES_ASC) {
76
+ const { pageWidth, pageHeight } = toPage(Constants.PageSizes[name]);
77
+ rects[name] = { p1: new Point(0, 0), p2: new Point(pageWidth, pageHeight) };
78
+ core.scene.addEntity('Polyline', { points: rectPoints(rects[name].p1, rects[name].p2) });
79
+ }
80
+
81
+ for (const name of PAGE_NAMES_ASC) {
82
+ const { pageWidth, pageHeight } = toPage(Constants.PageSizes[name]);
83
+ const { p1, p2 } = rects[name];
84
+
85
+ const windowArea = new BoundingBox(p1, p2);
86
+ expect(windowArea.xLength).toBeCloseTo(pageWidth, 5);
87
+ expect(windowArea.yLength).toBeCloseTo(pageHeight, 5);
88
+
89
+ // full exportTo path: WINDOW plotArea must isolate only this rectangle's bounds,
90
+ // not the full-scene extents (which would be A0 for every iteration)
91
+ const plotOptions = new PlotOptions(pageWidth, pageHeight);
92
+ plotOptions.setOption('plotArea', PlotOptions.Area.WINDOW);
93
+ plotOptions.setOption('windowArea', { point1: p1, point2: p2 });
94
+ plotOptions.setOption('plotScale', 1);
95
+ plotOptions.setOption('margin', 0);
96
+
97
+ const renderer = new SvgRenderer(pageWidth, pageHeight);
98
+ const setTransformSpy = jest.spyOn(renderer, 'setTransform');
99
+ expect(core.canvas.exportTo(renderer, plotOptions)).toBe(true);
100
+
101
+ // with plotScale 1 and margin 0, the matrix must map exactly this rectangle's
102
+ // own bounds onto the page - a full-scene (A0) fallback would produce a very
103
+ // different translation since every rect shares the same (0,0) origin
104
+ const matrix = setTransformSpy.mock.calls[0][0];
105
+ expect(matrix.a).toBe(1);
106
+ expect(matrix.d).toBe(1);
107
+ expect(matrix.e).toBeCloseTo(0, 5);
108
+ expect(matrix.f).toBeCloseTo(0, 5);
109
+
110
+ setTransformSpy.mockRestore();
111
+ }
112
+ });
113
+ });
@@ -130,7 +130,7 @@ test('setOption accepts PlotOptions.Area.DISPLAY for plotArea', () => {
130
130
 
131
131
  test('setOption throws for an unknown plotArea value', () => {
132
132
  const po = new PlotOptions(595, 842);
133
- expect(() => po.setOption('plotArea', 'Window')).toThrow('plotArea');
133
+ expect(() => po.setOption('plotArea', 'Unknown')).toThrow('plotArea');
134
134
  });
135
135
 
136
136
  // ─── setOption — style ────────────────────────────────────────────────────────
@@ -152,6 +152,63 @@ test('setOption throws when style is not a function', () => {
152
152
  expect(() => po.setOption('style', 'monochrome')).toThrow('style');
153
153
  });
154
154
 
155
+ // ─── setOption — margin ───────────────────────────────────────────────────────
156
+
157
+ test('setOption accepts a valid margin', () => {
158
+ const po = new PlotOptions(595, 842);
159
+ po.setOption('margin', 40);
160
+ expect(po.margin).toBe(40);
161
+ });
162
+
163
+ test('setOption accepts zero margin', () => {
164
+ const po = new PlotOptions(595, 842);
165
+ po.setOption('margin', 0);
166
+ expect(po.margin).toBe(0);
167
+ });
168
+
169
+ test('setOption throws for negative margin', () => {
170
+ const po = new PlotOptions(595, 842);
171
+ expect(() => po.setOption('margin', -1)).toThrow('margin');
172
+ });
173
+
174
+ test('setOption throws for non-numeric margin', () => {
175
+ const po = new PlotOptions(595, 842);
176
+ expect(() => po.setOption('margin', '40')).toThrow('margin');
177
+ });
178
+
179
+ test('setOption throws for NaN margin', () => {
180
+ const po = new PlotOptions(595, 842);
181
+ expect(() => po.setOption('margin', NaN)).toThrow('margin');
182
+ });
183
+
184
+ test('setOption throws for Infinity margin', () => {
185
+ const po = new PlotOptions(595, 842);
186
+ expect(() => po.setOption('margin', Infinity)).toThrow('margin');
187
+ });
188
+
189
+ test('setOption throws when margin leaves zero usable width', () => {
190
+ const po = new PlotOptions(595, 842);
191
+ // 297.5 * 2 === pageWidth -> usable width would be exactly 0
192
+ expect(() => po.setOption('margin', 297.5)).toThrow('usable page area');
193
+ });
194
+
195
+ test('setOption throws when margin leaves zero usable height', () => {
196
+ const po = new PlotOptions(842, 595);
197
+ expect(() => po.setOption('margin', 297.5)).toThrow('usable page area');
198
+ });
199
+
200
+ test('setOption throws when margin exceeds half the narrower page dimension only', () => {
201
+ const po = new PlotOptions(595, 842);
202
+ // exceeds half of pageWidth (297.5) but not half of pageHeight (421)
203
+ expect(() => po.setOption('margin', 300)).toThrow('usable page area');
204
+ });
205
+
206
+ test('setOption accepts a margin just under half the narrower page dimension', () => {
207
+ const po = new PlotOptions(595, 842);
208
+ po.setOption('margin', 297);
209
+ expect(po.margin).toBe(297);
210
+ });
211
+
155
212
  // ─── setOption — fileType ─────────────────────────────────────────────────────
156
213
 
157
214
  test('setOption accepts pdf for fileType', () => {
@@ -0,0 +1,129 @@
1
+ import { Core } from '../../core/core/core.js';
2
+ import { DesignCore } from '../../core/designCore.js';
3
+ import { Point } from '../../core/entities/point.js';
4
+ import { WindowPick } from '../../core/tools/windowPick.js';
5
+ import { expect, jest } from '@jest/globals';
6
+ import { withMockInput } from '../test-helpers/test-helpers.js';
7
+
8
+ const core = new Core();
9
+
10
+ // ─── register ────────────────────────────────────────────────────────────────
11
+
12
+ test('WindowPick.register returns correct command object', () => {
13
+ const reg = WindowPick.register();
14
+ expect(reg.command).toBe('WindowPick');
15
+ expect(reg.shortcut).toBeUndefined();
16
+ expect(reg.type).toBeUndefined();
17
+ });
18
+
19
+ // ─── execute ──────────────────────────────────────────────────────────────────
20
+
21
+ test('WindowPick.execute collects two points and calls executeCommand', async () => {
22
+ const executeCommandSpy = jest.fn();
23
+
24
+ await withMockInput(
25
+ core.scene,
26
+ [new Point(0, 0), new Point(10, 10)],
27
+ async () => {
28
+ const tool = new WindowPick();
29
+ await tool.execute();
30
+ },
31
+ { extraMethods: { executeCommand: executeCommandSpy } },
32
+ );
33
+
34
+ expect(executeCommandSpy).toHaveBeenCalled();
35
+ });
36
+
37
+ test('WindowPick.execute returns early when first point is undefined', async () => {
38
+ const executeCommandSpy = jest.fn();
39
+
40
+ await withMockInput(
41
+ core.scene,
42
+ [],
43
+ async () => {
44
+ const tool = new WindowPick();
45
+ await tool.execute();
46
+ },
47
+ { extraMethods: { executeCommand: executeCommandSpy } },
48
+ );
49
+
50
+ expect(executeCommandSpy).not.toHaveBeenCalled();
51
+ });
52
+
53
+ test('WindowPick.execute returns early when second point is undefined', async () => {
54
+ const executeCommandSpy = jest.fn();
55
+
56
+ await withMockInput(
57
+ core.scene,
58
+ [new Point(0, 0)],
59
+ async () => {
60
+ const tool = new WindowPick();
61
+ await tool.execute();
62
+ },
63
+ { extraMethods: { executeCommand: executeCommandSpy } },
64
+ );
65
+
66
+ expect(executeCommandSpy).not.toHaveBeenCalled();
67
+ });
68
+
69
+ test('WindowPick.execute stores the two collected points', async () => {
70
+ let capturedTool;
71
+ const executeCommandSpy = jest.fn();
72
+
73
+ await withMockInput(
74
+ core.scene,
75
+ [new Point(1, 2), new Point(30, 40)],
76
+ async () => {
77
+ capturedTool = new WindowPick();
78
+ await capturedTool.execute();
79
+ },
80
+ { extraMethods: { executeCommand: executeCommandSpy } },
81
+ );
82
+
83
+ expect(capturedTool.points).toHaveLength(2);
84
+ expect(capturedTool.points[0].x).toBe(1);
85
+ expect(capturedTool.points[0].y).toBe(2);
86
+ expect(capturedTool.points[1].x).toBe(30);
87
+ expect(capturedTool.points[1].y).toBe(40);
88
+ });
89
+
90
+ // ─── preview ──────────────────────────────────────────────────────────────────
91
+
92
+ test('WindowPick.preview with one point creates a rectangle polyline preview', () => {
93
+ const tool = new WindowPick();
94
+ tool.points.push(new Point(10, 10));
95
+
96
+ core.mouse.mouseMoved(50, 50);
97
+
98
+ const initialCount = DesignCore.Scene.previewEntities.count();
99
+
100
+ tool.preview();
101
+
102
+ expect(DesignCore.Scene.previewEntities.count()).toBe(initialCount + 1);
103
+
104
+ const tempItem = DesignCore.Scene.previewEntities.get(DesignCore.Scene.previewEntities.count() - 1);
105
+ expect(tempItem.type).toBe('Polyline');
106
+ expect(tempItem.points.length).toBe(4);
107
+ });
108
+
109
+ test('WindowPick.preview with no points does not create a preview', () => {
110
+ const tool = new WindowPick();
111
+
112
+ const initialCount = DesignCore.Scene.previewEntities.count();
113
+
114
+ tool.preview();
115
+
116
+ expect(DesignCore.Scene.previewEntities.count()).toBe(initialCount);
117
+ });
118
+
119
+ test('WindowPick.preview with two points does not create another preview', () => {
120
+ const tool = new WindowPick();
121
+ tool.points.push(new Point(0, 0));
122
+ tool.points.push(new Point(10, 10));
123
+
124
+ const initialCount = DesignCore.Scene.previewEntities.count();
125
+
126
+ tool.preview();
127
+
128
+ expect(DesignCore.Scene.previewEntities.count()).toBe(initialCount);
129
+ });