@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.
- package/.github/workflows/publish.yml +40 -0
- package/README.md +14 -1
- package/core/lib/canvas.js +4 -1
- package/core/lib/commandManager.js +5 -0
- package/core/lib/inputManager.js +1 -0
- package/core/lib/mouse.js +10 -0
- package/core/lib/plotOptions.js +17 -0
- package/core/lib/strings.js +2 -0
- package/core/tools/windowPick.js +75 -0
- package/cspell.json +2 -0
- package/package.json +7 -2
- package/test/integration/plotPageSizeIntegration.test.js +113 -0
- package/test/lib/plotOptions.test.js +58 -1
- package/test/tools/windowPick.test.js +129 -0
|
@@ -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
|
|
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
|
+
```
|
package/core/lib/canvas.js
CHANGED
|
@@ -305,7 +305,9 @@ export class Canvas {
|
|
|
305
305
|
*/
|
|
306
306
|
exportTo(renderer, plotOptions) {
|
|
307
307
|
let area;
|
|
308
|
-
if (plotOptions.plotArea === PlotOptions.Area.
|
|
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,
|
package/core/lib/inputManager.js
CHANGED
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}
|
package/core/lib/plotOptions.js
CHANGED
|
@@ -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');
|
package/core/lib/strings.js
CHANGED
|
@@ -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.
|
|
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', '
|
|
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
|
+
});
|