@design-core/core 50.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/entities/hatch.js +182 -12
- package/core/entities/insert.js +6 -1
- package/core/lib/canvas.js +11 -3
- 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/properties/entityProperties.js +2 -1
- package/core/properties/flags.js +12 -0
- package/core/properties/property.js +7 -2
- package/core/properties/propertyManager.js +15 -0
- package/core/tables/block.js +2 -0
- package/core/tools/windowPick.js +75 -0
- package/cspell.json +4 -0
- package/package.json +7 -2
- package/test/entities/hatch.test.js +323 -0
- package/test/integration/plotPageSizeIntegration.test.js +113 -0
- package/test/lib/clipboard.test.js +73 -0
- package/test/lib/plotOptions.test.js +58 -1
- package/test/properties/flags.test.js +24 -0
- package/test/properties/propertyManager.test.js +71 -0
- package/test/tools/windowPick.test.js +129 -0
- package/test/tools/zoom.test.js +10 -12
|
@@ -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/entities/hatch.js
CHANGED
|
@@ -19,6 +19,7 @@ import { Line } from './line.js';
|
|
|
19
19
|
import { Arc } from './arc.js';
|
|
20
20
|
import { Polyline } from './polyline.js';
|
|
21
21
|
import { Property } from '../properties/property.js';
|
|
22
|
+
import { PatternLine } from '../lib/patterns.js';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Hatch Entity Class
|
|
@@ -41,6 +42,14 @@ export class Hatch extends Entity {
|
|
|
41
42
|
enumerable: false,
|
|
42
43
|
});
|
|
43
44
|
|
|
45
|
+
// resolved PatternLine array — populated at construction from inline DXF
|
|
46
|
+
// data (type 0/2) or from the Patterns library (type 1)
|
|
47
|
+
Object.defineProperty(this, 'patternLines', {
|
|
48
|
+
value: [],
|
|
49
|
+
writable: true,
|
|
50
|
+
enumerable: false,
|
|
51
|
+
});
|
|
52
|
+
|
|
44
53
|
// Boundary shapes stored in EntityProperties so all entity data lives in one place.
|
|
45
54
|
this.properties.add(Property.Names.CHILDENTITIES, {
|
|
46
55
|
type: Property.Type.ENTITIES,
|
|
@@ -53,14 +62,24 @@ export class Hatch extends Entity {
|
|
|
53
62
|
|
|
54
63
|
const rawPatternName = String(Property.loadValue([data?.[2], data?.patternName], 'ANSI31')).toUpperCase();
|
|
55
64
|
// DXF Groupcode 70 - Solid Fill Flag (1 = solid, 0 = pattern): if set, override pattern name
|
|
56
|
-
|
|
65
|
+
// DXF Groupcode 76 - Hatch pattern type: 0 = User-defined ('U'); 1 = Predefined; 2 = Custom
|
|
66
|
+
const patternType = data?.[76] ?? 1;
|
|
67
|
+
// User-defined hatches (76=0) are normalised to the sentinel name 'U'
|
|
68
|
+
// so the pattern type is implied by the name rather than stored separately.
|
|
69
|
+
const resolvedPatternName = data?.[70] === 1 ? 'SOLID' : patternType === 0 ? 'U' : rawPatternName;
|
|
70
|
+
if (patternType === 1 && resolvedPatternName !== 'SOLID' && !Patterns.patternExists(resolvedPatternName)) {
|
|
71
|
+
Logging.instance.warn(`Hatch: pattern '${resolvedPatternName}' not found`);
|
|
72
|
+
}
|
|
57
73
|
|
|
58
74
|
// DXF Groupcode 2 - Hatch pattern name
|
|
59
75
|
this.properties.add(Property.Names.PATTERNNAME, {
|
|
60
76
|
type: Property.Type.LIST,
|
|
61
77
|
value: resolvedPatternName,
|
|
62
78
|
dxfCode: 2,
|
|
63
|
-
options: () =>
|
|
79
|
+
options: () => [
|
|
80
|
+
{ display: 'User Defined', value: 'U' },
|
|
81
|
+
...Object.keys(Patterns.hatch_patterns).map((name) => ({ display: name, value: name })),
|
|
82
|
+
],
|
|
64
83
|
});
|
|
65
84
|
// DXF Groupcode 41 - Hatch pattern scale
|
|
66
85
|
this.properties.add(Property.Names.SCALE, {
|
|
@@ -74,6 +93,28 @@ export class Hatch extends Entity {
|
|
|
74
93
|
value: Property.loadValue([data?.[52], data?.angle], 0),
|
|
75
94
|
dxfCode: 52,
|
|
76
95
|
});
|
|
96
|
+
// DXF Groupcode 47 - Line spacing (user-defined hatches only)
|
|
97
|
+
this.properties.add(Property.Names.HATCHSPACING, {
|
|
98
|
+
type: Property.Type.NUMBER,
|
|
99
|
+
value: Property.loadValue([data?.[47]], 1),
|
|
100
|
+
dxfCode: 47,
|
|
101
|
+
readOnly: (entity) => entity.getProperty(Property.Names.PATTERNNAME) !== 'U',
|
|
102
|
+
});
|
|
103
|
+
// DXF Groupcode 77 - Double flag (user-defined hatches only): 0 = not double; 1 = double
|
|
104
|
+
this.properties.add(Property.Names.HATCHDOUBLE, {
|
|
105
|
+
type: Property.Type.BOOLEAN,
|
|
106
|
+
value: (data?.[77] ?? 0) === 1,
|
|
107
|
+
dxfCode: 77,
|
|
108
|
+
readOnly: (entity) => entity.getProperty(Property.Names.PATTERNNAME) !== 'U',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Populate patternLines: inline data for user-defined (76=0) and custom
|
|
112
|
+
// (76=2) patterns; Patterns library for predefined (76=1, default).
|
|
113
|
+
if ((patternType === 0 || patternType === 2) && data) {
|
|
114
|
+
this.patternLines = this.#buildInlinePatternLines(data);
|
|
115
|
+
} else if (resolvedPatternName !== 'SOLID' && Patterns.patternExists(resolvedPatternName)) {
|
|
116
|
+
this.patternLines = this._copyLibraryPattern(resolvedPatternName);
|
|
117
|
+
}
|
|
77
118
|
|
|
78
119
|
// add a single point to this.points if no other points exist
|
|
79
120
|
if (!this.points.length) {
|
|
@@ -103,6 +144,116 @@ export class Hatch extends Entity {
|
|
|
103
144
|
}
|
|
104
145
|
}
|
|
105
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Return unfrozen copies of library PatternLine objects for a named pattern.
|
|
149
|
+
* Patterns.getPattern() returns frozen objects (shared cache); cloneObject()
|
|
150
|
+
* uses Reflect.ownKeys and would fail writing into frozen instances.
|
|
151
|
+
* @param {string} name - upper-case pattern name
|
|
152
|
+
* @return {Array<PatternLine>} array of mutable PatternLine copies
|
|
153
|
+
*/
|
|
154
|
+
_copyLibraryPattern(name) {
|
|
155
|
+
return Patterns.getPattern(name).map((pl) => {
|
|
156
|
+
const copy = new PatternLine();
|
|
157
|
+
copy.angle = pl.angle;
|
|
158
|
+
copy.xOrigin = pl.xOrigin;
|
|
159
|
+
copy.yOrigin = pl.yOrigin;
|
|
160
|
+
copy.xDelta = pl.xDelta;
|
|
161
|
+
copy.yDelta = pl.yDelta;
|
|
162
|
+
copy.dashes = [...pl.dashes];
|
|
163
|
+
return copy;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Synthesise PatternLine objects for a user-defined hatch from line spacing.
|
|
169
|
+
* Used when the pattern name is 'U' and the pattern has no explicit family data.
|
|
170
|
+
* @param {number} spacing - perpendicular distance between parallel lines
|
|
171
|
+
* @param {boolean} isDouble - when true, add a second family at 90° to the first
|
|
172
|
+
* @return {Array<PatternLine>}
|
|
173
|
+
*/
|
|
174
|
+
_buildSpacingPatternLines(spacing, isDouble) {
|
|
175
|
+
const pl = new PatternLine();
|
|
176
|
+
pl.angle = 0;
|
|
177
|
+
pl.xOrigin = 0;
|
|
178
|
+
pl.yOrigin = 0;
|
|
179
|
+
pl.xDelta = 0;
|
|
180
|
+
pl.yDelta = spacing;
|
|
181
|
+
pl.dashes = [];
|
|
182
|
+
const result = [pl];
|
|
183
|
+
if (isDouble) {
|
|
184
|
+
const pl2 = new PatternLine();
|
|
185
|
+
pl2.angle = 90;
|
|
186
|
+
pl2.xOrigin = 0;
|
|
187
|
+
pl2.yOrigin = 0;
|
|
188
|
+
pl2.xDelta = 0;
|
|
189
|
+
pl2.yDelta = spacing;
|
|
190
|
+
pl2.dashes = [];
|
|
191
|
+
result.push(pl2);
|
|
192
|
+
}
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Build PatternLine objects from inline DXF pattern data (group code 76 = 0 or 2).
|
|
198
|
+
* DXF stores deltas pre-multiplied by scale; we divide by scale to recover the
|
|
199
|
+
* normalised (scale=1) values that buildPatternCache expects.
|
|
200
|
+
* @param {Object} data - raw DXF group-code data for the HATCH entity
|
|
201
|
+
* @return {Array<PatternLine>} array of PatternLine objects
|
|
202
|
+
*/
|
|
203
|
+
#buildInlinePatternLines(data) {
|
|
204
|
+
const scale = data[41] ?? 1;
|
|
205
|
+
// DXF group code 53 per-family angles are written by AutoCAD as absolute
|
|
206
|
+
// angles that already include the entity rotation (group code 52). Subtract
|
|
207
|
+
// the entity angle here so that buildPatternCache's addition of ANGLE
|
|
208
|
+
// restores the correct absolute rotation without doubling it.
|
|
209
|
+
const entityAngle = data[52] ?? 0;
|
|
210
|
+
const angles = [].concat(data[53] ?? []);
|
|
211
|
+
const dxDeltas = [].concat(data[45] ?? []);
|
|
212
|
+
const dyDeltas = [].concat(data[46] ?? []);
|
|
213
|
+
const dashCounts = [].concat(data[79] ?? []);
|
|
214
|
+
const allDashes = [].concat(data[49] ?? []);
|
|
215
|
+
|
|
216
|
+
// Pure user-defined hatch (76=0): no inline family data — synthesise
|
|
217
|
+
// from group code 47 (line spacing) and 77 (double flag).
|
|
218
|
+
if (!angles.length) {
|
|
219
|
+
return this._buildSpacingPatternLines((data[47] ?? 1) / scale, data[77] === 1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const patternLines = [];
|
|
223
|
+
let dashIdx = 0;
|
|
224
|
+
|
|
225
|
+
angles.forEach((angle, i) => {
|
|
226
|
+
const pl = new PatternLine();
|
|
227
|
+
pl.angle = angle - entityAngle;
|
|
228
|
+
pl.xOrigin = 0;
|
|
229
|
+
pl.yOrigin = 0;
|
|
230
|
+
// DXF deltas are already in drawing units, pre-multiplied by scale; divide out.
|
|
231
|
+
pl.xDelta = (dxDeltas[i] ?? 0) / scale;
|
|
232
|
+
pl.yDelta = (dyDeltas[i] ?? 0) / scale;
|
|
233
|
+
|
|
234
|
+
const count = dashCounts[i] ?? 0;
|
|
235
|
+
pl.dashes = allDashes.slice(dashIdx, dashIdx + count).map((d) => d / scale);
|
|
236
|
+
dashIdx += count;
|
|
237
|
+
|
|
238
|
+
patternLines.push(pl);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// DXF group code 77: double flag — add a perpendicular family
|
|
242
|
+
if (data[77] === 1 && patternLines.length > 0) {
|
|
243
|
+
const first = patternLines[0];
|
|
244
|
+
const pl = new PatternLine();
|
|
245
|
+
pl.angle = first.angle + 90;
|
|
246
|
+
pl.xOrigin = 0;
|
|
247
|
+
pl.yOrigin = 0;
|
|
248
|
+
pl.xDelta = first.xDelta;
|
|
249
|
+
pl.yDelta = first.yDelta;
|
|
250
|
+
pl.dashes = [...first.dashes];
|
|
251
|
+
patternLines.push(pl);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return patternLines;
|
|
255
|
+
}
|
|
256
|
+
|
|
106
257
|
/**
|
|
107
258
|
* Build and cache world-space line segments for the current pattern.
|
|
108
259
|
* For pattern hatches, segments are pre-clipped against the boundary using
|
|
@@ -114,7 +265,7 @@ export class Hatch extends Entity {
|
|
|
114
265
|
return;
|
|
115
266
|
}
|
|
116
267
|
|
|
117
|
-
if (
|
|
268
|
+
if (!this.patternLines.length) {
|
|
118
269
|
this.cachedPattern = [];
|
|
119
270
|
return;
|
|
120
271
|
}
|
|
@@ -147,7 +298,7 @@ export class Hatch extends Entity {
|
|
|
147
298
|
const s = this.getProperty(Property.Names.SCALE);
|
|
148
299
|
const lines = [];
|
|
149
300
|
|
|
150
|
-
const pattern =
|
|
301
|
+
const pattern = this.patternLines;
|
|
151
302
|
pattern.forEach((patternLine) => {
|
|
152
303
|
let dashLength = bbXLength / 2;
|
|
153
304
|
if (patternLine.dashes.length) {
|
|
@@ -613,7 +764,7 @@ export class Hatch extends Entity {
|
|
|
613
764
|
// Build cache if stale (cachedPattern === null means never built or invalidated)
|
|
614
765
|
if (this.cachedPattern === null) this.buildPatternCache();
|
|
615
766
|
|
|
616
|
-
if (this.getProperty(Property.Names.PATTERNNAME) === 'SOLID') {
|
|
767
|
+
if (this.cachedPattern.length === 0 && this.getProperty(Property.Names.PATTERNNAME) === 'SOLID') {
|
|
617
768
|
// Design renders solid hatches with a direct fill() call rather than the
|
|
618
769
|
// dense cross-hatch line pattern used by commercial CAD applications
|
|
619
770
|
// (Commercial CAD's SOLID pattern uses two line families at 0.0001-unit spacing).
|
|
@@ -694,17 +845,20 @@ export class Hatch extends Entity {
|
|
|
694
845
|
file.writeGroupCode('97', '0'); // Number of source boundary objects
|
|
695
846
|
}
|
|
696
847
|
file.writeGroupCode('75', '1'); // Hatch style: 0 = Hatch “odd parity” area (Normal style) 1 = Hatch outermost area only (Outer style) 2 = Hatch through entire area (Ignore style)
|
|
697
|
-
|
|
848
|
+
// Hatch pattern type: 0 = User-defined; 1 = Predefined; 2 = Custom
|
|
849
|
+
const patternName = this.getProperty(Property.Names.PATTERNNAME);
|
|
850
|
+
const isUserDefined = patternName === 'U';
|
|
851
|
+
const isLibraryPattern = !isUserDefined && patternName !== 'SOLID' && Patterns.patternExists(patternName);
|
|
852
|
+
file.writeGroupCode('76', isUserDefined ? '0' : isLibraryPattern ? '1' : '2');
|
|
698
853
|
|
|
699
|
-
if (
|
|
854
|
+
if (patternName !== 'SOLID') {
|
|
700
855
|
file.writeGroupCode('52', this.getProperty(Property.Names.ANGLE)); // Hatch Pattern angle
|
|
701
856
|
file.writeGroupCode('41', this.getProperty(Property.Names.SCALE)); // Hatch Pattern scale
|
|
702
|
-
file.writeGroupCode('77', '0'); // Hatch pattern double flag(pattern fill only): 0 = not double; 1 = double
|
|
703
|
-
file.writeGroupCode('78',
|
|
857
|
+
file.writeGroupCode('77', this.getProperty(Property.Names.HATCHDOUBLE) ? '1' : '0'); // Hatch pattern double flag(pattern fill only): 0 = not double; 1 = double
|
|
858
|
+
file.writeGroupCode('78', this.patternLines.length); // Number of pattern definition lines
|
|
704
859
|
|
|
705
860
|
// Pattern data
|
|
706
|
-
|
|
707
|
-
pattern.forEach((patternLine) => {
|
|
861
|
+
this.patternLines.forEach((patternLine) => {
|
|
708
862
|
file.writeGroupCode('53', patternLine.angle + this.getProperty(Property.Names.ANGLE)); // Pattern line angle
|
|
709
863
|
file.writeGroupCode('43', patternLine.xOrigin); // Pattern line base X
|
|
710
864
|
file.writeGroupCode('44', patternLine.yOrigin); // Pattern line base y
|
|
@@ -719,7 +873,7 @@ export class Hatch extends Entity {
|
|
|
719
873
|
});
|
|
720
874
|
}
|
|
721
875
|
|
|
722
|
-
file.writeGroupCode('47', '0.5'); // pixel size
|
|
876
|
+
file.writeGroupCode('47', isUserDefined ? this.getProperty(Property.Names.HATCHSPACING) : '0.5'); // line spacing (user-defined) / pixel size
|
|
723
877
|
file.writeGroupCode('98', '1'); // Number of seed points
|
|
724
878
|
// seed points
|
|
725
879
|
file.writeGroupCode('10', '1');
|
|
@@ -842,6 +996,22 @@ export class Hatch extends Entity {
|
|
|
842
996
|
const upper = String(value).toUpperCase();
|
|
843
997
|
if (this.getProperty(Property.Names.PATTERNNAME) === upper) return;
|
|
844
998
|
super.setProperty(property, upper);
|
|
999
|
+
if (upper === 'U') {
|
|
1000
|
+
this.patternLines = this._buildSpacingPatternLines(
|
|
1001
|
+
this.getProperty(Property.Names.HATCHSPACING),
|
|
1002
|
+
this.getProperty(Property.Names.HATCHDOUBLE),
|
|
1003
|
+
);
|
|
1004
|
+
} else {
|
|
1005
|
+
this.patternLines = (upper !== 'SOLID' && Patterns.patternExists(upper)) ? this._copyLibraryPattern(upper) : [];
|
|
1006
|
+
}
|
|
1007
|
+
} else if (property === Property.Names.HATCHSPACING || property === Property.Names.HATCHDOUBLE) {
|
|
1008
|
+
super.setProperty(property, value);
|
|
1009
|
+
if (this.getProperty(Property.Names.PATTERNNAME) === 'U') {
|
|
1010
|
+
this.patternLines = this._buildSpacingPatternLines(
|
|
1011
|
+
this.getProperty(Property.Names.HATCHSPACING),
|
|
1012
|
+
this.getProperty(Property.Names.HATCHDOUBLE),
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
845
1015
|
} else if (property === Property.Names.POINTS) {
|
|
846
1016
|
// Special handling for hatch points to move child entities
|
|
847
1017
|
// Consider the changes from the hatch points to be an offset and rotation
|
package/core/entities/insert.js
CHANGED
|
@@ -51,7 +51,11 @@ export class Insert extends Entity {
|
|
|
51
51
|
// DXF Groupcode 2 - Block name
|
|
52
52
|
if (data.blockName !== undefined) {
|
|
53
53
|
const block = DesignCore.Scene.blockManager.getItemByName(data.blockName);
|
|
54
|
-
|
|
54
|
+
if (block) {
|
|
55
|
+
this.block = block;
|
|
56
|
+
} else {
|
|
57
|
+
Logging.instance.warn(`Insert: block '${data.blockName}' not found`);
|
|
58
|
+
}
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
// Named scalar rotation (internal API); DXF code 50 is handled by fromDxf
|
|
@@ -247,6 +251,7 @@ export class Insert extends Entity {
|
|
|
247
251
|
*/
|
|
248
252
|
boundingBox() {
|
|
249
253
|
const blockBB = this.block.boundingBox();
|
|
254
|
+
if (!blockBB) return new BoundingBox();
|
|
250
255
|
const topLeft = blockBB.pt1.add(this.points[0]);
|
|
251
256
|
const bottomRight = blockBB.pt2.add(this.points[0]);
|
|
252
257
|
return new BoundingBox(topLeft, bottomRight);
|
package/core/lib/canvas.js
CHANGED
|
@@ -222,16 +222,21 @@ export class Canvas {
|
|
|
222
222
|
const width = xMax - xMin;
|
|
223
223
|
const height = yMax - yMin;
|
|
224
224
|
|
|
225
|
-
if (width === 0
|
|
225
|
+
if (width === 0 && height === 0) {
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
const selectionCenter = new Point(xMin + width / 2, yMin + height / 2);
|
|
230
230
|
const screenCenter = new Point(this.width / 2, this.height / 2);
|
|
231
231
|
const translateDelta = DesignCore.Mouse.transformToScene(screenCenter).subtract(selectionCenter);
|
|
232
|
-
|
|
232
|
+
// fall back to the non-zero axis when the window is a single line
|
|
233
|
+
const targetScale = width === 0 ? this.height / height :
|
|
234
|
+
height === 0 ? this.width / width :
|
|
235
|
+
Math.min((this.width / width), (this.height / height));
|
|
233
236
|
const scaleDelta = targetScale / this.getScale() * 0.9;
|
|
234
237
|
|
|
238
|
+
if (!isFinite(scaleDelta) || scaleDelta <= 0) return;
|
|
239
|
+
|
|
235
240
|
this.matrix.scale(scaleDelta, scaleDelta);
|
|
236
241
|
this.matrix.translate((selectionCenter.x / scaleDelta) - selectionCenter.x, (selectionCenter.y / scaleDelta) - selectionCenter.y);
|
|
237
242
|
this.matrix.translate(translateDelta.x / scaleDelta, translateDelta.y / scaleDelta);
|
|
@@ -300,7 +305,9 @@ export class Canvas {
|
|
|
300
305
|
*/
|
|
301
306
|
exportTo(renderer, plotOptions) {
|
|
302
307
|
let area;
|
|
303
|
-
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) {
|
|
304
311
|
const viewport = this.getSceneOffset();
|
|
305
312
|
area = new BoundingBox(new Point(viewport.xmin, viewport.ymin), new Point(viewport.xmax, viewport.ymax));
|
|
306
313
|
} else {
|
|
@@ -313,6 +320,7 @@ export class Canvas {
|
|
|
313
320
|
pageWidth: plotOptions.pageWidth,
|
|
314
321
|
pageHeight: plotOptions.pageHeight,
|
|
315
322
|
plotScale: plotOptions.plotScale,
|
|
323
|
+
margin: plotOptions.margin,
|
|
316
324
|
});
|
|
317
325
|
if (!matrix) return false;
|
|
318
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',
|
|
@@ -58,7 +58,8 @@ export class EntityProperties {
|
|
|
58
58
|
set(name, value, entity) {
|
|
59
59
|
const prop = this._store[name];
|
|
60
60
|
if (!prop) return;
|
|
61
|
-
|
|
61
|
+
const isReadOnly = typeof prop.readOnly === 'function' ? prop.readOnly(entity) : prop.readOnly;
|
|
62
|
+
if (isReadOnly) return;
|
|
62
63
|
if (prop.set) {
|
|
63
64
|
prop.set(entity, value);
|
|
64
65
|
} else {
|
package/core/properties/flags.js
CHANGED
|
@@ -28,6 +28,18 @@ export class Flags {
|
|
|
28
28
|
return this.flags;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Serialise as a plain number so JSON.stringify produces e.g. `"flags":1`
|
|
33
|
+
* instead of `"flags":{"flags":1}`. Without this, the round-trip through
|
|
34
|
+
* clipboard stringify/parse loses the flag value (e.g. the closed flag on a
|
|
35
|
+
* polyline), because the reconstructed plain object is not a Flags instance
|
|
36
|
+
* and Property.loadValue cannot extract the numeric value from it.
|
|
37
|
+
* @return {number}
|
|
38
|
+
*/
|
|
39
|
+
toJSON() {
|
|
40
|
+
return this.flags;
|
|
41
|
+
}
|
|
42
|
+
|
|
31
43
|
/**
|
|
32
44
|
* Get the backwards value
|
|
33
45
|
* @param {number} flagValue
|
|
@@ -29,9 +29,12 @@ export class Property {
|
|
|
29
29
|
return this._value;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/** @param {any} val - new value; ignored when readOnly
|
|
32
|
+
/** @param {any} val - new value; ignored when readOnly is true.
|
|
33
|
+
* Callable readOnly is evaluated by the caller (entityProperties.set) with
|
|
34
|
+
* the entity in scope; treat it as writable here to avoid stale closure issues. */
|
|
33
35
|
set value(val) {
|
|
34
|
-
|
|
36
|
+
const isReadOnly = typeof this.readOnly === 'function' ? false : this.readOnly;
|
|
37
|
+
if (!isReadOnly) {
|
|
35
38
|
this._value = val;
|
|
36
39
|
}
|
|
37
40
|
}
|
|
@@ -83,6 +86,8 @@ export class Property {
|
|
|
83
86
|
// ── Hatch ─────────────────────────────────────────────────────────────────
|
|
84
87
|
CHILDENTITIES: 'childEntities',
|
|
85
88
|
PATTERNNAME: 'patternName',
|
|
89
|
+
HATCHSPACING: 'hatchSpacing',
|
|
90
|
+
HATCHDOUBLE: 'hatchDouble',
|
|
86
91
|
// ── Text ──────────────────────────────────────────────────────────────────
|
|
87
92
|
BACKWARDS: 'backwards',
|
|
88
93
|
HORIZONTALALIGNMENT: 'horizontalAlignment',
|
|
@@ -151,6 +151,21 @@ export class PropertyManager {
|
|
|
151
151
|
return DesignCore.Scene.entities.get(index).properties.definition(property);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Get the representative entity for a named property from the selection set.
|
|
156
|
+
* Used alongside getEntityPropertyDefinition when readOnly callbacks need an entity.
|
|
157
|
+
* @param {string} entityType
|
|
158
|
+
* @param {string} property
|
|
159
|
+
* @return {Object|undefined}
|
|
160
|
+
*/
|
|
161
|
+
getEntityForProperty(entityType, property) {
|
|
162
|
+
const index = DesignCore.Scene.selectionManager.selectionSet.selectionSet.find((i) => {
|
|
163
|
+
const entity = DesignCore.Scene.entities.get(i);
|
|
164
|
+
return (entityType === 'All' || entity.type === entityType) && entity.properties?.has(property);
|
|
165
|
+
});
|
|
166
|
+
return index !== undefined ? DesignCore.Scene.entities.get(index) : undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
154
169
|
/**
|
|
155
170
|
* Get a list the property values
|
|
156
171
|
* @param {string} entityType
|
package/core/tables/block.js
CHANGED
|
@@ -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
|
+
}
|