@design-core/core 50.1.0 → 51.1.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/core/entities/hatch.js +182 -12
- package/core/entities/insert.js +6 -1
- package/core/lib/canvas.js +7 -2
- 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/cspell.json +2 -0
- package/package.json +1 -1
- package/test/entities/hatch.test.js +323 -0
- package/test/lib/clipboard.test.js +73 -0
- package/test/properties/flags.test.js +24 -0
- package/test/properties/propertyManager.test.js +71 -0
- package/test/tools/zoom.test.js +10 -12
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);
|
|
@@ -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
package/cspell.json
CHANGED
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Hatch } from '../../core/entities/hatch.js';
|
|
2
|
+
import { PatternLine } from '../../core/lib/patterns.js';
|
|
2
3
|
import { Point } from '../../core/entities/point.js';
|
|
3
4
|
import { DesignCore } from '../../core/designCore.js';
|
|
4
5
|
|
|
@@ -10,6 +11,8 @@ import { Arc } from '../../core/entities/arc.js';
|
|
|
10
11
|
import { Text } from '../../core/entities/text.js';
|
|
11
12
|
|
|
12
13
|
import { File, withMockInput } from '../test-helpers/test-helpers.js';
|
|
14
|
+
import { Logging } from '../../core/lib/logging.js';
|
|
15
|
+
import { jest } from '@jest/globals';
|
|
13
16
|
|
|
14
17
|
import { Core } from '../../core/core/core.js';
|
|
15
18
|
|
|
@@ -1066,6 +1069,40 @@ test('Hatch.draw HONEY dashed family sets lineDashOffset and strokes once per se
|
|
|
1066
1069
|
expect(ctx.getStrokeCalls()).toBe(expectedSegs);
|
|
1067
1070
|
});
|
|
1068
1071
|
|
|
1072
|
+
// ── unknown pattern ───────────────────────────────────────────────────────────
|
|
1073
|
+
|
|
1074
|
+
describe('unknown hatch pattern', () => {
|
|
1075
|
+
test('logs a warning when pattern name is not recognised', () => {
|
|
1076
|
+
const warnSpy = jest.spyOn(Logging.instance, 'warn').mockImplementation(() => {});
|
|
1077
|
+
try {
|
|
1078
|
+
new Hatch({ patternName: 'NOTAPATTERN' });
|
|
1079
|
+
expect(warnSpy).toHaveBeenCalledWith('Hatch: pattern \'NOTAPATTERN\' not found');
|
|
1080
|
+
} finally {
|
|
1081
|
+
warnSpy.mockRestore();
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
test('retains the original pattern name when pattern is not recognised', () => {
|
|
1086
|
+
const warnSpy = jest.spyOn(Logging.instance, 'warn').mockImplementation(() => {});
|
|
1087
|
+
try {
|
|
1088
|
+
const h = new Hatch({ patternName: 'NOTAPATTERN' });
|
|
1089
|
+
expect(h.getProperty('patternName')).toBe('NOTAPATTERN');
|
|
1090
|
+
} finally {
|
|
1091
|
+
warnSpy.mockRestore();
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
test('does not log a warning for a known pattern', () => {
|
|
1096
|
+
const warnSpy = jest.spyOn(Logging.instance, 'warn').mockImplementation(() => {});
|
|
1097
|
+
try {
|
|
1098
|
+
new Hatch({ patternName: 'ANSI31' });
|
|
1099
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
1100
|
+
} finally {
|
|
1101
|
+
warnSpy.mockRestore();
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1069
1106
|
// ── processSelection: uncloseable items ──────────────────────────────────────
|
|
1070
1107
|
|
|
1071
1108
|
test('Hatch.processSelection returns empty array when items cannot form a closed loop', () => {
|
|
@@ -1077,3 +1114,289 @@ test('Hatch.processSelection returns empty array when items cannot form a closed
|
|
|
1077
1114
|
const h = new Hatch();
|
|
1078
1115
|
expect(h.processSelection(disconnected)).toEqual([]);
|
|
1079
1116
|
});
|
|
1117
|
+
|
|
1118
|
+
// ── patternLines: library (76=1 default) ─────────────────────────────────────
|
|
1119
|
+
|
|
1120
|
+
test('Hatch.patternLines is non-enumerable', () => {
|
|
1121
|
+
const h = new Hatch({ patternName: 'ANSI31' });
|
|
1122
|
+
const descriptor = Object.getOwnPropertyDescriptor(h, 'patternLines');
|
|
1123
|
+
expect(descriptor.enumerable).toBe(false);
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
test('Hatch.patternLines populated from library for known predefined pattern', () => {
|
|
1127
|
+
const h = new Hatch({ patternName: 'ANSI31' });
|
|
1128
|
+
expect(h.patternLines.length).toBeGreaterThan(0);
|
|
1129
|
+
expect(h.patternLines[0]).toBeInstanceOf(PatternLine);
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
test('Hatch.patternLines is empty for unknown pattern', () => {
|
|
1133
|
+
const warnSpy = jest.spyOn(Logging.instance, 'warn').mockImplementation(() => {});
|
|
1134
|
+
try {
|
|
1135
|
+
const h = new Hatch({ patternName: 'NOTAPATTERN' });
|
|
1136
|
+
expect(h.patternLines).toEqual([]);
|
|
1137
|
+
} finally {
|
|
1138
|
+
warnSpy.mockRestore();
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
test('Hatch.patternLines is empty for SOLID pattern', () => {
|
|
1143
|
+
const h = new Hatch({ patternName: 'SOLID' });
|
|
1144
|
+
expect(h.patternLines).toEqual([]);
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
test('Hatch.patternLines rebuilt when patternName changes via setProperty', () => {
|
|
1148
|
+
const h = new Hatch({ patternName: 'ANSI31' });
|
|
1149
|
+
const originalLength = h.patternLines.length;
|
|
1150
|
+
expect(originalLength).toBeGreaterThan(0);
|
|
1151
|
+
|
|
1152
|
+
h.setProperty('patternName', 'HONEY');
|
|
1153
|
+
expect(h.patternLines.length).toBeGreaterThan(0);
|
|
1154
|
+
// HONEY has 3 families; ANSI31 has 1
|
|
1155
|
+
expect(h.patternLines.length).toBeGreaterThan(originalLength);
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
test('Hatch.patternLines becomes empty when patternName set to unknown', () => {
|
|
1159
|
+
const warnSpy = jest.spyOn(Logging.instance, 'warn').mockImplementation(() => {});
|
|
1160
|
+
try {
|
|
1161
|
+
const h = new Hatch({ patternName: 'ANSI31' });
|
|
1162
|
+
h.setProperty('patternName', 'NOTAPATTERN');
|
|
1163
|
+
expect(h.patternLines).toEqual([]);
|
|
1164
|
+
} finally {
|
|
1165
|
+
warnSpy.mockRestore();
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
// ── patternLines: user-defined inline (76=0) ─────────────────────────────────
|
|
1170
|
+
|
|
1171
|
+
test('Hatch with 76=0 uses inline pattern lines, not library', () => {
|
|
1172
|
+
const data = {
|
|
1173
|
+
'76': 0,
|
|
1174
|
+
'41': 1,
|
|
1175
|
+
'52': 0,
|
|
1176
|
+
'78': 1,
|
|
1177
|
+
'53': 0,
|
|
1178
|
+
'45': 0,
|
|
1179
|
+
'46': 10, // dy delta (10 inches)
|
|
1180
|
+
'79': 0,
|
|
1181
|
+
};
|
|
1182
|
+
const h = new Hatch(data);
|
|
1183
|
+
expect(h.patternLines.length).toBe(1);
|
|
1184
|
+
expect(h.patternLines[0]).toBeInstanceOf(PatternLine);
|
|
1185
|
+
expect(h.patternLines[0].angle).toBe(0);
|
|
1186
|
+
expect(h.patternLines[0].yDelta).toBeCloseTo(10, 5); // 10 (drawing units)
|
|
1187
|
+
expect(h.patternLines[0].dashes).toEqual([]);
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
test('Hatch with 76=0 correctly scales delta by dividing out the scale factor', () => {
|
|
1191
|
+
// scale=2: DXF stores delta pre-multiplied, so raw dy=20 → 20/2=10 (drawing units)
|
|
1192
|
+
const data = {
|
|
1193
|
+
'76': 0,
|
|
1194
|
+
'41': 2,
|
|
1195
|
+
'52': 0,
|
|
1196
|
+
'78': 1,
|
|
1197
|
+
'53': 0,
|
|
1198
|
+
'45': 0,
|
|
1199
|
+
'46': 20,
|
|
1200
|
+
'79': 0,
|
|
1201
|
+
};
|
|
1202
|
+
const h = new Hatch(data);
|
|
1203
|
+
expect(h.patternLines[0].yDelta).toBeCloseTo(10, 5);
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
test('Hatch with 76=0 and 77=1 (double flag) adds a perpendicular family', () => {
|
|
1207
|
+
const data = {
|
|
1208
|
+
'76': 0,
|
|
1209
|
+
'77': 1,
|
|
1210
|
+
'41': 1,
|
|
1211
|
+
'52': 0,
|
|
1212
|
+
'78': 1,
|
|
1213
|
+
'53': 0,
|
|
1214
|
+
'45': 0,
|
|
1215
|
+
'46': 10,
|
|
1216
|
+
'79': 0,
|
|
1217
|
+
};
|
|
1218
|
+
const h = new Hatch(data);
|
|
1219
|
+
expect(h.patternLines.length).toBe(2);
|
|
1220
|
+
expect(h.patternLines[1].angle).toBe(90);
|
|
1221
|
+
expect(h.patternLines[1].yDelta).toBeCloseTo(h.patternLines[0].yDelta, 5);
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
test('Hatch with 76=0 and dashes parses and converts to mm', () => {
|
|
1225
|
+
// [5, -5] → [5, -5] (drawing units)
|
|
1226
|
+
const data = {
|
|
1227
|
+
'76': 0,
|
|
1228
|
+
'41': 1,
|
|
1229
|
+
'52': 0,
|
|
1230
|
+
'78': 1,
|
|
1231
|
+
'53': 0,
|
|
1232
|
+
'45': 0,
|
|
1233
|
+
'46': 10,
|
|
1234
|
+
'79': 2,
|
|
1235
|
+
'49': [5, -5],
|
|
1236
|
+
};
|
|
1237
|
+
const h = new Hatch(data);
|
|
1238
|
+
expect(h.patternLines[0].dashes.length).toBe(2);
|
|
1239
|
+
expect(h.patternLines[0].dashes[0]).toBeCloseTo(5, 5);
|
|
1240
|
+
expect(h.patternLines[0].dashes[1]).toBeCloseTo(-5, 5);
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
test('Hatch with 76=0 and multiple line families parses each correctly', () => {
|
|
1244
|
+
const data = {
|
|
1245
|
+
'76': 0,
|
|
1246
|
+
'41': 1,
|
|
1247
|
+
'52': 0,
|
|
1248
|
+
'78': 2,
|
|
1249
|
+
'53': [0, 90],
|
|
1250
|
+
'45': [0, 0],
|
|
1251
|
+
'46': [10, 10],
|
|
1252
|
+
'79': [0, 0],
|
|
1253
|
+
};
|
|
1254
|
+
const h = new Hatch(data);
|
|
1255
|
+
expect(h.patternLines.length).toBe(2);
|
|
1256
|
+
expect(h.patternLines[0].angle).toBe(0);
|
|
1257
|
+
expect(h.patternLines[1].angle).toBe(90);
|
|
1258
|
+
expect(h.patternLines[0].yDelta).toBeCloseTo(10, 5);
|
|
1259
|
+
expect(h.patternLines[1].yDelta).toBeCloseTo(10, 5);
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
test('Hatch with 76=0 and no inline families synthesises from spacing (47)', () => {
|
|
1263
|
+
const data = { '76': 0, '41': 1, '52': 45, '47': 5 };
|
|
1264
|
+
const h = new Hatch(data);
|
|
1265
|
+
expect(h.patternLines.length).toBe(1);
|
|
1266
|
+
expect(h.patternLines[0].angle).toBe(0);
|
|
1267
|
+
expect(h.patternLines[0].yDelta).toBeCloseTo(5, 5);
|
|
1268
|
+
expect(h.patternLines[0].dashes).toEqual([]);
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
test('Hatch with 76=0 explicit families: data[53] absolute angle is stored relative to entity angle', () => {
|
|
1272
|
+
// AutoCAD writes data[53]=45 as the absolute line angle when entity angle (52) is 45.
|
|
1273
|
+
// Subtracting entityAngle makes it 0 so buildPatternCache's addition yields the correct 45°.
|
|
1274
|
+
const data = { '76': 0, '41': 1, '52': 45, '78': 1, '53': 45, '45': 0, '46': 10, '79': 0 };
|
|
1275
|
+
const h = new Hatch(data);
|
|
1276
|
+
expect(h.patternLines[0].angle).toBeCloseTo(0, 5); // 45 - 45 = 0 (relative)
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
test('Hatch with 76=0, no inline families, double flag (77=1) synthesises two families', () => {
|
|
1280
|
+
const data = { '76': 0, '77': 1, '41': 1, '52': 0, '47': 5 };
|
|
1281
|
+
const h = new Hatch(data);
|
|
1282
|
+
expect(h.patternLines.length).toBe(2);
|
|
1283
|
+
expect(h.patternLines[0].angle).toBe(0);
|
|
1284
|
+
expect(h.patternLines[1].angle).toBe(90);
|
|
1285
|
+
expect(h.patternLines[1].yDelta).toBeCloseTo(5, 5);
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
// ── patternLines: custom embedded (76=2) ─────────────────────────────────────
|
|
1289
|
+
|
|
1290
|
+
test('Hatch with 76=2 uses inline pattern lines', () => {
|
|
1291
|
+
const data = {
|
|
1292
|
+
'76': 2,
|
|
1293
|
+
'41': 1,
|
|
1294
|
+
'52': 0,
|
|
1295
|
+
'78': 1,
|
|
1296
|
+
'53': 45,
|
|
1297
|
+
'45': 0,
|
|
1298
|
+
'46': 10,
|
|
1299
|
+
'79': 0,
|
|
1300
|
+
};
|
|
1301
|
+
const h = new Hatch(data);
|
|
1302
|
+
expect(h.patternLines.length).toBe(1);
|
|
1303
|
+
expect(h.patternLines[0].angle).toBe(45);
|
|
1304
|
+
expect(h.patternLines[0].yDelta).toBeCloseTo(10, 5);
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
// ── dxf() output: 76 group code ──────────────────────────────────────────────
|
|
1308
|
+
|
|
1309
|
+
test('Hatch.dxf writes 76=1 for library patterns', () => {
|
|
1310
|
+
const h = new Hatch({ patternName: 'ANSI31', handle: '1' });
|
|
1311
|
+
h.setProperty('childEntities', makeSquare());
|
|
1312
|
+
const file = new File();
|
|
1313
|
+
h.dxf(file);
|
|
1314
|
+
const match = file.contents.match(/^76\n(\d+)/m);
|
|
1315
|
+
expect(match).not.toBeNull();
|
|
1316
|
+
expect(match[1]).toBe('1');
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
test('Hatch.dxf writes 76=0 for user-defined patterns', () => {
|
|
1320
|
+
const data = {
|
|
1321
|
+
'76': 0,
|
|
1322
|
+
'41': 1,
|
|
1323
|
+
'52': 0,
|
|
1324
|
+
'78': 1,
|
|
1325
|
+
'53': 0,
|
|
1326
|
+
'45': 0,
|
|
1327
|
+
'46': 10,
|
|
1328
|
+
'79': 0,
|
|
1329
|
+
'handle': '1',
|
|
1330
|
+
};
|
|
1331
|
+
const h = new Hatch(data);
|
|
1332
|
+
h.setProperty('childEntities', makeSquare());
|
|
1333
|
+
const file = new File();
|
|
1334
|
+
h.dxf(file);
|
|
1335
|
+
const match = file.contents.match(/^76\n(\d+)/m);
|
|
1336
|
+
expect(match).not.toBeNull();
|
|
1337
|
+
expect(match[1]).toBe('0');
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
test('Hatch.dxf 76=0 double-flag round-trip writes correct family count', () => {
|
|
1341
|
+
const data = {
|
|
1342
|
+
'76': 0,
|
|
1343
|
+
'77': 1,
|
|
1344
|
+
'41': 1,
|
|
1345
|
+
'52': 0,
|
|
1346
|
+
'78': 1,
|
|
1347
|
+
'53': 0,
|
|
1348
|
+
'45': 0,
|
|
1349
|
+
'46': 10,
|
|
1350
|
+
'79': 0,
|
|
1351
|
+
'handle': '1',
|
|
1352
|
+
};
|
|
1353
|
+
const h = new Hatch(data);
|
|
1354
|
+
h.setProperty('childEntities', makeSquare());
|
|
1355
|
+
const file = new File();
|
|
1356
|
+
h.dxf(file);
|
|
1357
|
+
const match = file.contents.match(/^78\n(\d+)/m);
|
|
1358
|
+
expect(match).not.toBeNull();
|
|
1359
|
+
expect(match[1]).toBe('2');
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
// ── buildPatternCache with inline patterns ────────────────────────────────────
|
|
1363
|
+
|
|
1364
|
+
test('Hatch with 76=0 inline pattern renders segments inside boundary', () => {
|
|
1365
|
+
const data = {
|
|
1366
|
+
'76': 0,
|
|
1367
|
+
'41': 1,
|
|
1368
|
+
'52': 0,
|
|
1369
|
+
'78': 1,
|
|
1370
|
+
'53': 0,
|
|
1371
|
+
'45': 0,
|
|
1372
|
+
'46': 1, // 1 (drawing unit) spacing
|
|
1373
|
+
'79': 0,
|
|
1374
|
+
};
|
|
1375
|
+
const h = new Hatch(data);
|
|
1376
|
+
h.setProperty('childEntities', makeSquare());
|
|
1377
|
+
h.buildPatternCache();
|
|
1378
|
+
|
|
1379
|
+
expect(h.cachedPattern.length).toBe(1);
|
|
1380
|
+
expect(h.cachedPattern[0].segments.length).toBeGreaterThan(0);
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
test('Hatch with 76=0 double flag renders two families of segments', () => {
|
|
1384
|
+
const data = {
|
|
1385
|
+
'76': 0,
|
|
1386
|
+
'77': 1,
|
|
1387
|
+
'41': 1,
|
|
1388
|
+
'52': 0,
|
|
1389
|
+
'78': 1,
|
|
1390
|
+
'53': 0,
|
|
1391
|
+
'45': 0,
|
|
1392
|
+
'46': 1,
|
|
1393
|
+
'79': 0,
|
|
1394
|
+
};
|
|
1395
|
+
const h = new Hatch(data);
|
|
1396
|
+
h.setProperty('childEntities', makeSquare());
|
|
1397
|
+
h.buildPatternCache();
|
|
1398
|
+
|
|
1399
|
+
expect(h.cachedPattern.length).toBe(2);
|
|
1400
|
+
expect(h.cachedPattern[0].segments.length).toBeGreaterThan(0);
|
|
1401
|
+
expect(h.cachedPattern[1].segments.length).toBeGreaterThan(0);
|
|
1402
|
+
});
|
|
@@ -4,6 +4,8 @@ import { Point } from '../../core/entities/point.js';
|
|
|
4
4
|
import { DesignCore } from '../../core/designCore.js';
|
|
5
5
|
import { Logging } from '../../core/lib/logging.js';
|
|
6
6
|
import { jest } from '@jest/globals';
|
|
7
|
+
import { Polyline } from '../../core/entities/polyline.js';
|
|
8
|
+
import { Text } from '../../core/entities/text.js';
|
|
7
9
|
|
|
8
10
|
// Initialize core (sets DesignCore.Core)
|
|
9
11
|
new Core();
|
|
@@ -67,4 +69,75 @@ describe('Clipboard', () => {
|
|
|
67
69
|
expect(fresh.Entities).toHaveLength(0);
|
|
68
70
|
errorSpy.mockRestore();
|
|
69
71
|
});
|
|
72
|
+
|
|
73
|
+
describe('stringify/parse flag survival', () => {
|
|
74
|
+
test('closed Polyline retains closed flag after stringify/parse roundtrip', () => {
|
|
75
|
+
// Create a closed polyline (flags bit 1 = closed)
|
|
76
|
+
const poly = new Polyline({
|
|
77
|
+
points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)],
|
|
78
|
+
flags: 1,
|
|
79
|
+
});
|
|
80
|
+
expect(poly.getProperty('closed')).toBe(true);
|
|
81
|
+
|
|
82
|
+
DesignCore.Clipboard.Entities = [poly];
|
|
83
|
+
const json = DesignCore.Clipboard.stringify();
|
|
84
|
+
|
|
85
|
+
const fresh = new Clipboard();
|
|
86
|
+
fresh.parse(json);
|
|
87
|
+
|
|
88
|
+
expect(fresh.Entities).toHaveLength(1);
|
|
89
|
+
expect(fresh.Entities[0].getProperty('closed')).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('closed Polyline retains correct point count after stringify/parse roundtrip', () => {
|
|
93
|
+
// A closed polyline stores 4 points (not 5 — the closing duplicate is removed)
|
|
94
|
+
const poly = new Polyline({
|
|
95
|
+
points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)],
|
|
96
|
+
flags: 1,
|
|
97
|
+
});
|
|
98
|
+
expect(poly.points).toHaveLength(4);
|
|
99
|
+
|
|
100
|
+
DesignCore.Clipboard.Entities = [poly];
|
|
101
|
+
const json = DesignCore.Clipboard.stringify();
|
|
102
|
+
|
|
103
|
+
const fresh = new Clipboard();
|
|
104
|
+
fresh.parse(json);
|
|
105
|
+
|
|
106
|
+
expect(fresh.Entities[0].points).toHaveLength(4);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('backwards (mirrored) Text retains backwards flag after stringify/parse roundtrip', () => {
|
|
110
|
+
// Text flags bit 2 = backwards
|
|
111
|
+
const text = new Text({
|
|
112
|
+
points: [new Point(0, 0), new Point(10, 0)],
|
|
113
|
+
textValue: 'hello',
|
|
114
|
+
flags: 2,
|
|
115
|
+
});
|
|
116
|
+
expect(text.getBackwards()).toBe(true);
|
|
117
|
+
|
|
118
|
+
DesignCore.Clipboard.Entities = [text];
|
|
119
|
+
const json = DesignCore.Clipboard.stringify();
|
|
120
|
+
|
|
121
|
+
const fresh = new Clipboard();
|
|
122
|
+
fresh.parse(json);
|
|
123
|
+
|
|
124
|
+
expect(fresh.Entities).toHaveLength(1);
|
|
125
|
+
expect(fresh.Entities[0].getBackwards()).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('non-flagged entities are unaffected', () => {
|
|
129
|
+
const poly = new Polyline({
|
|
130
|
+
points: [new Point(0, 0), new Point(10, 0), new Point(10, 10)],
|
|
131
|
+
});
|
|
132
|
+
expect(poly.getProperty('closed')).toBe(false);
|
|
133
|
+
|
|
134
|
+
DesignCore.Clipboard.Entities = [poly];
|
|
135
|
+
const json = DesignCore.Clipboard.stringify();
|
|
136
|
+
|
|
137
|
+
const fresh = new Clipboard();
|
|
138
|
+
fresh.parse(json);
|
|
139
|
+
|
|
140
|
+
expect(fresh.Entities[0].getProperty('closed')).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
70
143
|
});
|
|
@@ -162,3 +162,27 @@ test('Test flags.removeValue', () => {
|
|
|
162
162
|
// 2 shouldn't exist again
|
|
163
163
|
expect(flags.hasFlag(2)).toBe(false);
|
|
164
164
|
});
|
|
165
|
+
|
|
166
|
+
test('Test flags.toJSON returns the numeric flag value', () => {
|
|
167
|
+
const flags = new Flags();
|
|
168
|
+
expect(flags.toJSON()).toBe(0);
|
|
169
|
+
|
|
170
|
+
flags.setFlagValue(7);
|
|
171
|
+
expect(flags.toJSON()).toBe(7);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('Test flags.toJSON round-trips through JSON.stringify/parse', () => {
|
|
175
|
+
const flags = new Flags();
|
|
176
|
+
flags.setFlagValue(1);
|
|
177
|
+
|
|
178
|
+
// JSON.stringify should produce the plain number, not a nested object
|
|
179
|
+
const json = JSON.stringify({ flags });
|
|
180
|
+
expect(json).toBe('{"flags":1}');
|
|
181
|
+
|
|
182
|
+
// The parsed value is a plain number — Flags constructor accepts it via setFlagValue
|
|
183
|
+
const parsed = JSON.parse(json);
|
|
184
|
+
const restored = new Flags();
|
|
185
|
+
restored.setFlagValue(parsed.flags);
|
|
186
|
+
expect(restored.getFlagValue()).toBe(1);
|
|
187
|
+
expect(restored.hasFlag(1)).toBe(true);
|
|
188
|
+
});
|
|
@@ -113,3 +113,74 @@ test('Test propertyManager.getEntityPropertyValue', () => {
|
|
|
113
113
|
propertyValues = propertiesManager.getEntityPropertyValue('All', 'colour');
|
|
114
114
|
expect(propertyValues).toBe('Varies');
|
|
115
115
|
});
|
|
116
|
+
|
|
117
|
+
test('Test propertyManager.getEntityPropertyDefinition', () => {
|
|
118
|
+
// clear the selection set
|
|
119
|
+
DesignCore.Scene.reset();
|
|
120
|
+
|
|
121
|
+
// returns undefined when nothing is selected
|
|
122
|
+
let def = propertiesManager.getEntityPropertyDefinition('All', 'colour');
|
|
123
|
+
expect(def).toBeUndefined();
|
|
124
|
+
|
|
125
|
+
// select the line entity
|
|
126
|
+
DesignCore.Scene.selectionManager.selectionSet.selectionSet.push(0);
|
|
127
|
+
|
|
128
|
+
// returns a Property descriptor for a valid property
|
|
129
|
+
def = propertiesManager.getEntityPropertyDefinition('All', 'colour');
|
|
130
|
+
expect(def).toBeDefined();
|
|
131
|
+
expect(typeof def.type).toBe('string');
|
|
132
|
+
|
|
133
|
+
// returns undefined for a property the entity does not have
|
|
134
|
+
def = propertiesManager.getEntityPropertyDefinition('All', 'non-existent-prop');
|
|
135
|
+
expect(def).toBeUndefined();
|
|
136
|
+
|
|
137
|
+
// filters by entityType — Circle not selected, so returns undefined
|
|
138
|
+
def = propertiesManager.getEntityPropertyDefinition('Circle', 'colour');
|
|
139
|
+
expect(def).toBeUndefined();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('Test propertyManager.getEntityForProperty', () => {
|
|
143
|
+
// clear the selection set
|
|
144
|
+
DesignCore.Scene.reset();
|
|
145
|
+
|
|
146
|
+
// returns undefined when nothing is selected
|
|
147
|
+
let entity = propertiesManager.getEntityForProperty('All', 'colour');
|
|
148
|
+
expect(entity).toBeUndefined();
|
|
149
|
+
|
|
150
|
+
// select the line (index 0) and circle (index 1)
|
|
151
|
+
DesignCore.Scene.selectionManager.selectionSet.selectionSet.push(0);
|
|
152
|
+
DesignCore.Scene.selectionManager.selectionSet.selectionSet.push(1);
|
|
153
|
+
|
|
154
|
+
// returns an entity for 'All' and a known property
|
|
155
|
+
entity = propertiesManager.getEntityForProperty('All', 'colour');
|
|
156
|
+
expect(entity).toBeDefined();
|
|
157
|
+
expect(entity.type === 'Line' || entity.type === 'Circle').toBe(true);
|
|
158
|
+
|
|
159
|
+
// filters by entityType — only Circle entities
|
|
160
|
+
entity = propertiesManager.getEntityForProperty('Circle', 'colour');
|
|
161
|
+
expect(entity).toBeDefined();
|
|
162
|
+
expect(entity.type).toBe('Circle');
|
|
163
|
+
|
|
164
|
+
// returns undefined when the property does not exist on any selected entity
|
|
165
|
+
entity = propertiesManager.getEntityForProperty('All', 'non-existent-prop');
|
|
166
|
+
expect(entity).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('Test propertyManager.setPropertyCallbackFunction and selectionSetChanged', () => {
|
|
170
|
+
let callCount = 0;
|
|
171
|
+
const callback = () => {
|
|
172
|
+
callCount++;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// selectionSetChanged with no callback set should not throw
|
|
176
|
+
propertiesManager.selectionSetChanged();
|
|
177
|
+
|
|
178
|
+
propertiesManager.setPropertyCallbackFunction(callback);
|
|
179
|
+
|
|
180
|
+
// callback is invoked when selectionSetChanged is called
|
|
181
|
+
propertiesManager.selectionSetChanged();
|
|
182
|
+
expect(callCount).toBe(1);
|
|
183
|
+
|
|
184
|
+
propertiesManager.selectionSetChanged();
|
|
185
|
+
expect(callCount).toBe(2);
|
|
186
|
+
});
|
package/test/tools/zoom.test.js
CHANGED
|
@@ -181,40 +181,38 @@ test('Test Canvas.zoomToWindow - reversed points', () => {
|
|
|
181
181
|
expect(finalScale).not.toBe(initialScale);
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
test('Test Canvas.zoomToWindow - zero width (
|
|
184
|
+
test('Test Canvas.zoomToWindow - zero width (zooms using height)', () => {
|
|
185
185
|
core.canvas.width = 800;
|
|
186
186
|
core.canvas.height = 600;
|
|
187
187
|
core.canvas.matrix.scale(1, 1);
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// Points with same x coordinate (zero width)
|
|
189
|
+
// Points with same x coordinate (zero width, i.e. a vertical line)
|
|
192
190
|
const pt1 = new Point(50, 0);
|
|
193
191
|
const pt2 = new Point(50, 100);
|
|
194
192
|
|
|
195
193
|
core.canvas.zoomToWindow(pt1, pt2);
|
|
196
194
|
|
|
197
|
-
//
|
|
195
|
+
// Resulting scale depends only on the window size, not the prior scale,
|
|
196
|
+
// so assert the expected value rather than comparing against initialScale
|
|
198
197
|
const finalScale = core.canvas.getScale();
|
|
199
|
-
expect(finalScale).
|
|
198
|
+
expect(finalScale).toBeCloseTo((core.canvas.height / 100) * 0.9);
|
|
200
199
|
});
|
|
201
200
|
|
|
202
|
-
test('Test Canvas.zoomToWindow - zero height (
|
|
201
|
+
test('Test Canvas.zoomToWindow - zero height (zooms using width)', () => {
|
|
203
202
|
core.canvas.width = 800;
|
|
204
203
|
core.canvas.height = 600;
|
|
205
204
|
core.canvas.matrix.scale(1, 1);
|
|
206
205
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// Points with same y coordinate (zero height)
|
|
206
|
+
// Points with same y coordinate (zero height, i.e. a horizontal line)
|
|
210
207
|
const pt1 = new Point(0, 50);
|
|
211
208
|
const pt2 = new Point(100, 50);
|
|
212
209
|
|
|
213
210
|
core.canvas.zoomToWindow(pt1, pt2);
|
|
214
211
|
|
|
215
|
-
//
|
|
212
|
+
// Resulting scale depends only on the window size, not the prior scale,
|
|
213
|
+
// so assert the expected value rather than comparing against initialScale
|
|
216
214
|
const finalScale = core.canvas.getScale();
|
|
217
|
-
expect(finalScale).
|
|
215
|
+
expect(finalScale).toBeCloseTo((core.canvas.width / 100) * 0.9);
|
|
218
216
|
});
|
|
219
217
|
|
|
220
218
|
test('Test Canvas.zoomToWindow - same point (should not zoom)', () => {
|