@design-core/core 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +4 -0
- package/TODO.md +49 -6
- package/core/core/core.js +0 -3
- package/core/dimensions/alignedDimension.js +5 -4
- package/core/dimensions/angularDimension.js +6 -5
- package/core/dimensions/baseCircularDimension.js +3 -2
- package/core/dimensions/baseDimension.js +59 -93
- package/core/dimensions/baseLinearDimension.js +6 -5
- package/core/dimensions/diametricDimension.js +4 -3
- package/core/dimensions/dimension.js +21 -14
- package/core/dimensions/radialDimension.js +4 -3
- package/core/dimensions/rotatedDimension.js +12 -11
- package/core/entities/arc.js +112 -145
- package/core/entities/arctext.js +212 -174
- package/core/entities/basePolyline.js +33 -284
- package/core/entities/circle.js +77 -99
- package/core/entities/entity.js +114 -93
- package/core/entities/hatch.js +322 -249
- package/core/entities/insert.js +66 -81
- package/core/entities/line.js +21 -142
- package/core/entities/lwpolyline.js +16 -11
- package/core/entities/polyline.js +4 -4
- package/core/entities/rectangle.js +3 -1
- package/core/entities/solid.js +6 -15
- package/core/entities/text.js +188 -199
- package/core/lib/auxiliary/rubberBand.js +32 -0
- package/core/lib/auxiliary/selectionWindow.js +63 -0
- package/core/lib/auxiliary/snapPoint.js +113 -0
- package/core/lib/auxiliary/trackingLine.js +76 -0
- package/core/lib/canvas.js +228 -166
- package/core/lib/clipboard.js +7 -1
- package/core/lib/colour.js +16 -0
- package/core/lib/commandLine.js +1 -6
- package/core/lib/commandManager.js +18 -8
- package/core/lib/constants.js +28 -0
- package/core/lib/dxf/dxf.js +4 -3
- package/core/lib/entityManager.js +18 -11
- package/core/lib/inputManager.js +36 -13
- package/core/lib/intersect.js +25 -0
- package/core/lib/patterns.js +8 -1
- package/core/lib/plotOptions.js +67 -0
- package/core/lib/polylineUtils.js +75 -0
- package/core/lib/renderers/cairoRenderer.js +371 -0
- package/core/lib/renderers/canvasRenderer.js +315 -0
- package/core/lib/renderers/pdfRenderer.js +366 -0
- package/core/lib/renderers/rendererBase.js +284 -0
- package/core/lib/renderers/svgRenderer.js +343 -0
- package/core/lib/scene.js +27 -5
- package/core/lib/selectionManager.js +2 -2
- package/core/lib/settings.js +4 -2
- package/core/lib/snapping.js +4 -191
- package/core/lib/stateManager.js +30 -1
- package/core/lib/utils.js +21 -2
- package/core/properties/entityProperties.js +104 -0
- package/core/properties/property.js +106 -2
- package/core/properties/propertyManager.js +29 -13
- package/core/tables/block.js +4 -37
- package/core/tables/endblk.js +1 -1
- package/core/tables/layer.js +8 -0
- package/core/tables/styleManagerBase.js +3 -2
- package/core/tables/tableManagerBase.js +8 -3
- package/core/tools/chamfer.js +111 -137
- package/core/tools/chamferFilletBase.js +135 -34
- package/core/tools/copy.js +3 -3
- package/core/tools/explode.js +2 -2
- package/core/tools/extend.js +132 -19
- package/core/tools/fillet.js +86 -183
- package/core/tools/matchProp.js +2 -11
- package/core/tools/mirror.js +7 -6
- package/core/tools/move.js +3 -3
- package/core/tools/offset.js +3 -5
- package/core/tools/pasteclip.js +1 -1
- package/core/tools/rotate.js +6 -3
- package/core/tools/scale.js +1 -1
- package/core/tools/trim.js +265 -23
- package/core/tools/zoom.js +1 -1
- package/jest.config.js +6 -0
- package/package.json +5 -3
- package/test/dimensions/alignedDimension.test.js +14 -7
- package/test/dimensions/angularDimension.test.js +6 -6
- package/test/dimensions/baseDimension.test.js +69 -8
- package/test/dimensions/baseLinearDimension.test.js +62 -0
- package/test/dimensions/diametricDimension.test.js +6 -6
- package/test/dimensions/radialDimension.test.js +6 -6
- package/test/dimensions/rotatedDimension.test.js +14 -7
- package/test/entities/arc.test.js +80 -83
- package/test/entities/arctext.test.js +180 -53
- package/test/entities/basePolyline.test.js +129 -2
- package/test/entities/circle.test.js +57 -85
- package/test/entities/entity.test.js +42 -8
- package/test/entities/hatch.test.js +543 -70
- package/test/entities/insert.test.js +96 -1
- package/test/entities/line.test.js +15 -41
- package/test/entities/lwpolyline.test.js +104 -0
- package/test/entities/polyline.test.js +11 -0
- package/test/entities/rectangle.test.js +11 -0
- package/test/entities/solid.test.js +1 -1
- package/test/entities/text.test.js +294 -69
- package/test/integration/exportIntegration.reference.dxf +9 -9
- package/test/integration/exportIntegration.test.js +2 -2
- package/test/integration/intersectionIntegration.test.js +4 -5
- package/test/integration/pdfRendererIntegration.reference.pdf +17709 -0
- package/test/integration/pdfRendererIntegration.test.js +61 -0
- package/test/integration/svgRendererIntegration.reference.svg +3343 -0
- package/test/integration/svgRendererIntegration.test.js +61 -0
- package/test/integration/userInputIntegration.reference.dxf +1165 -105
- package/test/integration/userInputIntegration.test.js +239 -132
- package/test/lib/canvas.test.js +742 -123
- package/test/lib/constants.test.js +27 -0
- package/test/lib/entityManager.test.js +4 -4
- package/test/lib/inputManager.test.js +80 -19
- package/test/lib/intersect.test.js +83 -0
- package/test/lib/plotOptions.test.js +179 -0
- package/test/lib/rubberBand.test.js +84 -0
- package/test/lib/settings.test.js +5 -6
- package/test/lib/snapping.test.js +71 -129
- package/test/lib/stateManager.test.js +7 -7
- package/test/lib/utils.test.js +36 -2
- package/test/properties/entityProperties.test.js +132 -0
- package/test/properties/property.test.js +53 -0
- package/test/properties/propertyManager.test.js +3 -3
- package/test/renderers/canvasRenderer.test.js +317 -0
- package/test/renderers/pdfRenderer.test.js +271 -0
- package/test/renderers/rendererBase.test.js +213 -0
- package/test/renderers/svgRenderer.test.js +326 -0
- package/test/tables/block.test.js +31 -1
- package/test/tables/endblk.test.js +2 -2
- package/test/tables/layer.test.js +30 -0
- package/test/test-helpers/hatchGridRenderer.js +180 -0
- package/test/test-helpers/test-helpers.js +152 -0
- package/test/tools/chamfer.test.js +227 -5
- package/test/tools/chamferFilletBase.test.js +98 -9
- package/test/tools/copy.test.js +2 -2
- package/test/tools/explode.test.js +3 -3
- package/test/tools/extend.test.js +331 -0
- package/test/tools/fillet.test.js +296 -0
- package/test/tools/matchProp.test.js +5 -5
- package/test/tools/mirror.test.js +13 -13
- package/test/tools/offset.test.js +170 -3
- package/test/tools/pasteclip.test.js +5 -5
- package/test/tools/rotate.test.js +32 -2
- package/test/tools/scale.test.js +5 -5
- package/test/tools/trim.test.js +526 -0
- package/test/tools/zoom.test.js +7 -7
- package/core/lib/selectionWindow.js +0 -83
|
@@ -3,7 +3,6 @@ import { Point } from '../../core/entities/point.js';
|
|
|
3
3
|
import { DesignCore } from '../../core/designCore.js';
|
|
4
4
|
|
|
5
5
|
import { BasePolyline } from '../../core/entities/basePolyline.js';
|
|
6
|
-
import { BoundingBox } from '../../core/lib/boundingBox.js';
|
|
7
6
|
import { Circle } from '../../core/entities/circle.js';
|
|
8
7
|
import { Line } from '../../core/entities/line.js';
|
|
9
8
|
import { Polyline } from '../../core/entities/polyline.js';
|
|
@@ -21,7 +20,7 @@ const points = [new Point(100, 100, 1), new Point(200, 100, 1)];
|
|
|
21
20
|
const boundaryShape = new BasePolyline({ points: points });
|
|
22
21
|
|
|
23
22
|
const hatch = new Hatch({ handle: '1' });
|
|
24
|
-
hatch.childEntities
|
|
23
|
+
hatch.setProperty('childEntities', [boundaryShape]);
|
|
25
24
|
|
|
26
25
|
const hatchInputScenarios = [
|
|
27
26
|
{
|
|
@@ -67,9 +66,9 @@ test.each(hatchInputScenarios)('Hatch.execute handles $desc', async (scenario) =
|
|
|
67
66
|
|
|
68
67
|
await hatch.execute();
|
|
69
68
|
|
|
70
|
-
expect(hatch.childEntities.length).toBe(1);
|
|
69
|
+
expect(hatch.getProperty('childEntities').length).toBe(1);
|
|
71
70
|
|
|
72
|
-
expect(hatch.childEntities[0].points.length).toBe(pointCount); // closed polyline adds extra point
|
|
71
|
+
expect(hatch.getProperty('childEntities')[0].points.length).toBe(pointCount); // closed polyline adds extra point
|
|
73
72
|
|
|
74
73
|
expect(hatch.points.length).toBe(2);
|
|
75
74
|
// hatch first point should be at 0,0
|
|
@@ -78,10 +77,9 @@ test.each(hatchInputScenarios)('Hatch.execute handles $desc', async (scenario) =
|
|
|
78
77
|
// hatch last point should be at 1,1
|
|
79
78
|
expect(hatch.points.at(-1).x).toBe(1);
|
|
80
79
|
expect(hatch.points.at(-1).y).toBe(1);
|
|
81
|
-
expect(hatch.
|
|
82
|
-
expect(hatch.scale).toBe(expectedScale);
|
|
83
|
-
expect(hatch.angle).toBe(expectedAngle);
|
|
84
|
-
expect(hatch.solid).toBe(expectedPattern === 'SOLID');
|
|
80
|
+
expect(hatch.getProperty('patternName')).toBe(expectedPattern);
|
|
81
|
+
expect(hatch.getProperty('scale')).toBe(expectedScale);
|
|
82
|
+
expect(hatch.getProperty('angle')).toBe(expectedAngle);
|
|
85
83
|
});
|
|
86
84
|
});
|
|
87
85
|
|
|
@@ -117,10 +115,9 @@ test('Test Hatch', () => {
|
|
|
117
115
|
|
|
118
116
|
const newHatch = new Hatch(data);
|
|
119
117
|
|
|
120
|
-
expect(newHatch.patternName).toBe('HONEY');
|
|
121
|
-
expect(newHatch.angle).toBe(45);
|
|
122
|
-
expect(newHatch.scale).toBe(2);
|
|
123
|
-
expect(newHatch.solid).toBe(false);
|
|
118
|
+
expect(newHatch.getProperty('patternName')).toBe('HONEY');
|
|
119
|
+
expect(newHatch.getProperty('angle')).toBe(45);
|
|
120
|
+
expect(newHatch.getProperty('scale')).toBe(2);
|
|
124
121
|
});
|
|
125
122
|
|
|
126
123
|
test('Test Hatch.getDataValue', () => {
|
|
@@ -134,19 +131,20 @@ test('Test Hatch.getDataValue', () => {
|
|
|
134
131
|
expect(hatch.getDataValue(data, 72)).toBe(0);
|
|
135
132
|
});
|
|
136
133
|
|
|
137
|
-
test('Test Hatch
|
|
138
|
-
expect(hatch.
|
|
139
|
-
|
|
134
|
+
test('Test Hatch patternName', () => {
|
|
135
|
+
expect(hatch.getProperty('patternName')).toBe('ANSI31');
|
|
136
|
+
|
|
137
|
+
hatch.setProperty('patternName', 'Solid');
|
|
138
|
+
expect(hatch.getProperty('patternName')).toBe('SOLID');
|
|
140
139
|
|
|
141
|
-
|
|
142
|
-
expect(hatch.
|
|
143
|
-
|
|
144
|
-
expect(hatch.pattern).toBe('SOLID');
|
|
145
|
-
expect(hatch.solid).toBe(true);
|
|
140
|
+
hatch.setProperty('patternName', 'Ansi31');
|
|
141
|
+
expect(hatch.getProperty('patternName')).toBe('ANSI31');
|
|
142
|
+
});
|
|
146
143
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
144
|
+
test('Test Hatch dxfCode 70 overrides patternName to SOLID', () => {
|
|
145
|
+
const data = { '2': 'ANSI31', '70': 1 };
|
|
146
|
+
const h = new Hatch(data);
|
|
147
|
+
expect(h.getProperty('patternName')).toBe('SOLID');
|
|
150
148
|
});
|
|
151
149
|
|
|
152
150
|
test('Test Hatch.closestPoint', () => {
|
|
@@ -298,8 +296,8 @@ ANSI31
|
|
|
298
296
|
|
|
299
297
|
// Export rotated and scaled
|
|
300
298
|
const rotatedScaleHatch = new Hatch({ handle: '1' });
|
|
301
|
-
rotatedScaleHatch.angle
|
|
302
|
-
rotatedScaleHatch.scale
|
|
299
|
+
rotatedScaleHatch.setProperty('angle', 45);
|
|
300
|
+
rotatedScaleHatch.setProperty('scale', 2);
|
|
303
301
|
|
|
304
302
|
file = new File();
|
|
305
303
|
rotatedScaleHatch.dxf(file);
|
|
@@ -312,7 +310,7 @@ ANSI31
|
|
|
312
310
|
const boundary = rotatedScaleHatch.processSelection([shape]);
|
|
313
311
|
expect(boundary[0]).toBeInstanceOf(Polyline);
|
|
314
312
|
|
|
315
|
-
rotatedScaleHatch.childEntities
|
|
313
|
+
rotatedScaleHatch.setProperty('childEntities', boundary);
|
|
316
314
|
rotatedScaleHatch.dxf(file);
|
|
317
315
|
// console.log(file.contents);
|
|
318
316
|
|
|
@@ -510,97 +508,572 @@ test('Test Hatch.processSelection', () => {
|
|
|
510
508
|
|
|
511
509
|
// Minimal mock canvas context that records moveTo calls and supports try/catch split
|
|
512
510
|
/**
|
|
513
|
-
* Create a minimal mock
|
|
514
|
-
* @return {Object} mock
|
|
511
|
+
* Create a minimal mock renderer
|
|
512
|
+
* @return {Object} mock renderer
|
|
515
513
|
*/
|
|
516
514
|
function makeMockCtx() {
|
|
517
|
-
let
|
|
515
|
+
let traceCount = 0;
|
|
518
516
|
let fillCalled = false;
|
|
519
517
|
return {
|
|
520
|
-
|
|
521
|
-
restore: () => {},
|
|
522
|
-
translate: () => {},
|
|
523
|
-
scale: () => {},
|
|
524
|
-
rotate: () => {},
|
|
518
|
+
setDash: () => {},
|
|
525
519
|
beginPath: () => {},
|
|
526
|
-
|
|
527
|
-
lineWidth: 0,
|
|
528
|
-
moveTo: () => {
|
|
529
|
-
moveCount++;
|
|
530
|
-
},
|
|
531
|
-
lineTo: () => {},
|
|
520
|
+
closePath: () => {},
|
|
532
521
|
stroke: () => {},
|
|
533
522
|
fill: () => {
|
|
534
523
|
fillCalled = true;
|
|
535
524
|
},
|
|
536
|
-
|
|
525
|
+
applyPath: (options) => {
|
|
526
|
+
if (options?.fill) fillCalled = true;
|
|
527
|
+
},
|
|
528
|
+
tracePath: () => {
|
|
529
|
+
traceCount++;
|
|
530
|
+
},
|
|
531
|
+
drawShape: () => {},
|
|
532
|
+
drawSegments: (segments) => {
|
|
533
|
+
traceCount += segments.length;
|
|
534
|
+
},
|
|
535
|
+
getMoveCalls: () => traceCount,
|
|
537
536
|
getFillCalled: () => fillCalled,
|
|
538
537
|
};
|
|
539
538
|
}
|
|
540
539
|
|
|
541
|
-
test('Test Hatch.
|
|
540
|
+
test('Test Hatch.draw solid fill renders filled boundary', () => {
|
|
542
541
|
const solidHatch = new Hatch({ patternName: 'SOLID' });
|
|
543
|
-
solidHatch.childEntities
|
|
542
|
+
solidHatch.setProperty('childEntities', [new BasePolyline({ points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] })]);
|
|
544
543
|
|
|
545
544
|
const ctx = makeMockCtx();
|
|
546
|
-
solidHatch.
|
|
545
|
+
solidHatch.draw(ctx);
|
|
547
546
|
|
|
548
547
|
expect(ctx.getFillCalled()).toBe(true);
|
|
549
|
-
expect(ctx.getMoveCalls()).toBe(0);
|
|
550
548
|
});
|
|
551
549
|
|
|
552
|
-
test('Test Hatch.
|
|
550
|
+
test('Test Hatch.draw unknown pattern does not stroke pattern lines', () => {
|
|
553
551
|
const unknownHatch = new Hatch({ patternName: 'ANSI31' });
|
|
554
|
-
unknownHatch.childEntities
|
|
555
|
-
// Override pattern to a nonexistent name
|
|
556
|
-
unknownHatch.
|
|
552
|
+
unknownHatch.setProperty('childEntities', [new BasePolyline({ points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] })]);
|
|
553
|
+
// Override pattern to a nonexistent name
|
|
554
|
+
unknownHatch.setProperty('patternName', 'NOTAPATTERN');
|
|
557
555
|
|
|
558
556
|
const ctx = makeMockCtx();
|
|
559
|
-
unknownHatch.
|
|
557
|
+
unknownHatch.draw(ctx);
|
|
560
558
|
|
|
561
|
-
|
|
562
|
-
expect(ctx.
|
|
559
|
+
// No pattern lines drawn
|
|
560
|
+
expect(ctx.getFillCalled()).toBe(false);
|
|
563
561
|
});
|
|
564
562
|
|
|
565
|
-
test('Test Hatch.
|
|
563
|
+
test('Test Hatch.draw tight bound line count - square boundary', () => {
|
|
566
564
|
// ANSI31: angle=45, yDelta=3.175, no dashes (dashLength = bbXLength/2)
|
|
567
565
|
// 100x100 square: halfW=halfH=50, sin45=cos45=√2/2
|
|
568
566
|
// halfY = 50*(√2/2) + 50*(√2/2) ≈ 70.71
|
|
569
|
-
// yIncrement = ceil(70.71 / 3.175) = 23 → loop i=-23..22 = 46 lines
|
|
570
|
-
|
|
567
|
+
// yIncrement = ceil(70.71 / 3.175) = 23 → loop i=-23..22 = 46 raw lines
|
|
568
|
+
// i=-23 (|ly|=73.025 > halfY=70.71) misses the square → 45 clipped segments
|
|
571
569
|
const squareHatch = new Hatch({ patternName: 'ANSI31' });
|
|
570
|
+
squareHatch.setProperty('childEntities', [new BasePolyline({ points: [new Point(0, 0), new Point(100, 0), new Point(100, 100), new Point(0, 100)] })]);
|
|
572
571
|
const ctx = makeMockCtx();
|
|
573
|
-
squareHatch.
|
|
574
|
-
expect(ctx.getMoveCalls()).toBe(
|
|
572
|
+
squareHatch.draw(ctx);
|
|
573
|
+
expect(ctx.getMoveCalls()).toBe(45);
|
|
575
574
|
});
|
|
576
575
|
|
|
577
|
-
test('Test Hatch.
|
|
576
|
+
test('Test Hatch.draw tight bound line count - flat boundary', () => {
|
|
578
577
|
// ANSI31: angle=45, yDelta=3.175, no dashes
|
|
579
578
|
// 100x10 flat box: halfW=50, halfH=5, sin45=cos45=√2/2
|
|
580
579
|
// halfY = (50+5)*(√2/2) ≈ 38.89
|
|
581
|
-
// yIncrement = ceil(38.89 / 3.175) = 13 → loop i=-13..12 = 26 lines
|
|
582
|
-
|
|
580
|
+
// yIncrement = ceil(38.89 / 3.175) = 13 → loop i=-13..12 = 26 raw lines
|
|
581
|
+
// i=-13 (|ly|=41.275 > halfY=38.89) misses the flat box → 25 clipped segments
|
|
583
582
|
const flatHatch = new Hatch({ patternName: 'ANSI31' });
|
|
583
|
+
flatHatch.setProperty('childEntities', [new BasePolyline({ points: [new Point(0, 0), new Point(100, 0), new Point(100, 10), new Point(0, 10)] })]);
|
|
584
584
|
const ctx = makeMockCtx();
|
|
585
|
-
flatHatch.
|
|
586
|
-
expect(ctx.getMoveCalls()).toBe(
|
|
585
|
+
flatHatch.draw(ctx);
|
|
586
|
+
expect(ctx.getMoveCalls()).toBe(25);
|
|
587
587
|
});
|
|
588
588
|
|
|
589
|
-
test('Test Hatch.
|
|
589
|
+
test('Test Hatch.draw tight bound produces fewer lines for flat boundary', () => {
|
|
590
590
|
// A flat boundary (100x10) should produce fewer lines than a square (100x100).
|
|
591
|
-
// This verifies that the tight projection avoids over-generating lines for
|
|
592
|
-
// non-square shapes, unlike the old Math.max(bbX, bbY) approach.
|
|
593
|
-
const squareBB = new BoundingBox(new Point(0, 100), new Point(100, 0));
|
|
594
|
-
const flatBB = new BoundingBox(new Point(0, 10), new Point(100, 0));
|
|
595
|
-
|
|
596
591
|
const squareHatch = new Hatch({ patternName: 'ANSI31' });
|
|
592
|
+
squareHatch.setProperty('childEntities', [new BasePolyline({ points: [new Point(0, 0), new Point(100, 0), new Point(100, 100), new Point(0, 100)] })]);
|
|
593
|
+
|
|
597
594
|
const flatHatch = new Hatch({ patternName: 'ANSI31' });
|
|
595
|
+
flatHatch.setProperty('childEntities', [new BasePolyline({ points: [new Point(0, 0), new Point(100, 0), new Point(100, 10), new Point(0, 10)] })]);
|
|
598
596
|
|
|
599
597
|
const squareCtx = makeMockCtx();
|
|
600
|
-
squareHatch.
|
|
598
|
+
squareHatch.draw(squareCtx);
|
|
601
599
|
|
|
602
600
|
const flatCtx = makeMockCtx();
|
|
603
|
-
flatHatch.
|
|
601
|
+
flatHatch.draw(flatCtx);
|
|
604
602
|
|
|
605
603
|
expect(flatCtx.getMoveCalls()).toBeLessThan(squareCtx.getMoveCalls());
|
|
606
604
|
});
|
|
605
|
+
|
|
606
|
+
// ── buildPatternCache — early return tests ───────────────────────────────────
|
|
607
|
+
|
|
608
|
+
test('Hatch.buildPatternCache no childEntities sets cachedPattern to empty array', () => {
|
|
609
|
+
const h = new Hatch({ patternName: 'HONEY' });
|
|
610
|
+
// childEntities is empty by default
|
|
611
|
+
h.buildPatternCache();
|
|
612
|
+
expect(h.cachedPattern).toEqual([]);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test('Hatch.buildPatternCache solid fill sets cachedPattern to empty array', () => {
|
|
616
|
+
const h = new Hatch({ patternName: 'SOLID' });
|
|
617
|
+
h.setProperty('childEntities', [new BasePolyline({ points: [new Point(0, 0), new Point(100, 0), new Point(100, 100), new Point(0, 100)] })]);
|
|
618
|
+
h.buildPatternCache();
|
|
619
|
+
expect(h.cachedPattern).toEqual([]);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test('Hatch.buildPatternCache unknown pattern sets cachedPattern to empty array', () => {
|
|
623
|
+
const h = new Hatch({ patternName: 'HONEY' });
|
|
624
|
+
h.setProperty('childEntities', [new BasePolyline({ points: [new Point(0, 0), new Point(100, 0), new Point(100, 100), new Point(0, 100)] })]);
|
|
625
|
+
h.setProperty('patternName', 'NOTAPATTERN');
|
|
626
|
+
h.buildPatternCache();
|
|
627
|
+
expect(h.cachedPattern).toEqual([]);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// ── buildPatternCache — pattern structure and scale tests ────────────────────
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Build a 100×100 square boundary
|
|
634
|
+
* @return {Array} array containing one BasePolyline boundary
|
|
635
|
+
*/
|
|
636
|
+
const makeSquare = () => [new BasePolyline({ points: [new Point(0, 0), new Point(100, 0), new Point(100, 100), new Point(0, 100)] })];
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Sum all segment counts across all families in a cached hatch
|
|
640
|
+
* @param {Hatch} h
|
|
641
|
+
* @return {number} total segment count
|
|
642
|
+
*/
|
|
643
|
+
const totalSegments = (h) => h.cachedPattern.reduce((s, f) => s + f.segments.length, 0);
|
|
644
|
+
|
|
645
|
+
test('Hatch.buildPatternCache HONEY has 3 families each with dashes and segments', () => {
|
|
646
|
+
const h = new Hatch({ patternName: 'HONEY', scale: 1 });
|
|
647
|
+
h.setProperty('childEntities', makeSquare());
|
|
648
|
+
h.buildPatternCache();
|
|
649
|
+
|
|
650
|
+
expect(h.cachedPattern.length).toBe(3);
|
|
651
|
+
for (const family of h.cachedPattern) {
|
|
652
|
+
expect(family.dashes.length).toBeGreaterThan(0);
|
|
653
|
+
expect(family.segments.length).toBeGreaterThan(0);
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test('Hatch.buildPatternCache HONEY scale 2 dashes are 2× scale 1 dashes', () => {
|
|
658
|
+
const h1 = new Hatch({ patternName: 'HONEY', scale: 1 });
|
|
659
|
+
h1.setProperty('childEntities', makeSquare());
|
|
660
|
+
h1.buildPatternCache();
|
|
661
|
+
|
|
662
|
+
const h2 = new Hatch({ patternName: 'HONEY', scale: 2 });
|
|
663
|
+
h2.setProperty('childEntities', makeSquare());
|
|
664
|
+
h2.buildPatternCache();
|
|
665
|
+
|
|
666
|
+
for (let i = 0; i < h1.cachedPattern.length; i++) {
|
|
667
|
+
const d1 = h1.cachedPattern[i].dashes;
|
|
668
|
+
const d2 = h2.cachedPattern[i].dashes;
|
|
669
|
+
expect(d2.length).toBe(d1.length);
|
|
670
|
+
for (let j = 0; j < d1.length; j++) {
|
|
671
|
+
expect(d2[j]).toBeCloseTo(d1[j] * 2, 5);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
test('Hatch.buildPatternCache HONEY scale 2 produces fewer segments than scale 1', () => {
|
|
677
|
+
const h1 = new Hatch({ patternName: 'HONEY', scale: 1 });
|
|
678
|
+
h1.setProperty('childEntities', makeSquare());
|
|
679
|
+
h1.buildPatternCache();
|
|
680
|
+
|
|
681
|
+
const h2 = new Hatch({ patternName: 'HONEY', scale: 2 });
|
|
682
|
+
h2.setProperty('childEntities', makeSquare());
|
|
683
|
+
h2.buildPatternCache();
|
|
684
|
+
|
|
685
|
+
expect(totalSegments(h2)).toBeLessThan(totalSegments(h1));
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test('Hatch.buildPatternCache ANSI31 scale 2 produces fewer segments than scale 1', () => {
|
|
689
|
+
const h1 = new Hatch({ patternName: 'ANSI31', scale: 1 });
|
|
690
|
+
h1.setProperty('childEntities', makeSquare());
|
|
691
|
+
h1.buildPatternCache();
|
|
692
|
+
|
|
693
|
+
const h2 = new Hatch({ patternName: 'ANSI31', scale: 2 });
|
|
694
|
+
h2.setProperty('childEntities', makeSquare());
|
|
695
|
+
h2.buildPatternCache();
|
|
696
|
+
|
|
697
|
+
expect(h2.cachedPattern[0].segments.length).toBeLessThan(h1.cachedPattern[0].segments.length);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
test('Hatch.buildPatternCache HONEY all dashPhase values are non-negative', () => {
|
|
701
|
+
const h = new Hatch({ patternName: 'HONEY', scale: 1 });
|
|
702
|
+
h.setProperty('childEntities', makeSquare());
|
|
703
|
+
h.buildPatternCache();
|
|
704
|
+
|
|
705
|
+
for (const family of h.cachedPattern) {
|
|
706
|
+
for (const seg of family.segments) {
|
|
707
|
+
expect(seg.dashPhase).toBeGreaterThanOrEqual(0);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
test('Hatch.buildPatternCache scale change invalidates and rebuilds cache with updated dashes', () => {
|
|
713
|
+
const h = new Hatch({ patternName: 'HONEY', scale: 1 });
|
|
714
|
+
h.setProperty('childEntities', makeSquare());
|
|
715
|
+
h.buildPatternCache();
|
|
716
|
+
|
|
717
|
+
const dashes1 = [...h.cachedPattern[0].dashes];
|
|
718
|
+
|
|
719
|
+
h.setProperty('scale', 2);
|
|
720
|
+
expect(h.cachedPattern).toBeNull();
|
|
721
|
+
|
|
722
|
+
h.buildPatternCache();
|
|
723
|
+
const dashes2 = [...h.cachedPattern[0].dashes];
|
|
724
|
+
|
|
725
|
+
for (let i = 0; i < dashes1.length; i++) {
|
|
726
|
+
expect(dashes2[i]).toBeCloseTo(dashes1[i] * 2, 5);
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// ── buildPatternCache — nested boundary tests ────────────────────────────────
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* A 20×20 inner square hole centred in the 100×100 outer square
|
|
734
|
+
* @return {BasePolyline} inner boundary polyline
|
|
735
|
+
*/
|
|
736
|
+
const makeInnerSquare = () => new BasePolyline({ points: [new Point(40, 40), new Point(60, 40), new Point(60, 60), new Point(40, 60)] });
|
|
737
|
+
|
|
738
|
+
test('Hatch.buildPatternCache ANSI31 nested boundary: no segment midpoint inside inner hole', () => {
|
|
739
|
+
// Even-odd rule: a ray from inside the hole crosses 2 boundaries → even → not hatched
|
|
740
|
+
const h = new Hatch({ patternName: 'ANSI31', scale: 1 });
|
|
741
|
+
h.setProperty('childEntities', [makeSquare()[0], makeInnerSquare()]);
|
|
742
|
+
h.buildPatternCache();
|
|
743
|
+
|
|
744
|
+
for (const family of h.cachedPattern) {
|
|
745
|
+
for (const seg of family.segments) {
|
|
746
|
+
const mx = (seg.x1 + seg.x2) / 2;
|
|
747
|
+
const my = (seg.y1 + seg.y2) / 2;
|
|
748
|
+
expect(mx > 40 && mx < 60 && my > 40 && my < 60).toBe(false);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
test('Hatch.buildPatternCache ANSI31 nested boundary produces more segments than outer-only', () => {
|
|
754
|
+
// Lines that cross the hole are split into two sub-segments, increasing total count
|
|
755
|
+
const outerOnly = new Hatch({ patternName: 'ANSI31', scale: 1 });
|
|
756
|
+
outerOnly.setProperty('childEntities', makeSquare());
|
|
757
|
+
outerOnly.buildPatternCache();
|
|
758
|
+
|
|
759
|
+
const withHole = new Hatch({ patternName: 'ANSI31', scale: 1 });
|
|
760
|
+
withHole.setProperty('childEntities', [makeSquare()[0], makeInnerSquare()]);
|
|
761
|
+
withHole.buildPatternCache();
|
|
762
|
+
|
|
763
|
+
expect(withHole.cachedPattern[0].segments.length).toBeGreaterThan(outerOnly.cachedPattern[0].segments.length);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
test('Hatch.buildPatternCache HONEY nested boundary: no segment midpoint inside inner hole', () => {
|
|
767
|
+
const h = new Hatch({ patternName: 'HONEY', scale: 1 });
|
|
768
|
+
h.setProperty('childEntities', [makeSquare()[0], makeInnerSquare()]);
|
|
769
|
+
h.buildPatternCache();
|
|
770
|
+
|
|
771
|
+
expect(h.cachedPattern.length).toBe(3);
|
|
772
|
+
for (const family of h.cachedPattern) {
|
|
773
|
+
for (const seg of family.segments) {
|
|
774
|
+
const mx = (seg.x1 + seg.x2) / 2;
|
|
775
|
+
const my = (seg.y1 + seg.y2) / 2;
|
|
776
|
+
expect(mx > 40 && mx < 60 && my > 40 && my < 60).toBe(false);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
test('Hatch.buildPatternCache HONEY nested boundary produces more segments than outer-only', () => {
|
|
782
|
+
const outerOnly = new Hatch({ patternName: 'HONEY', scale: 1 });
|
|
783
|
+
outerOnly.setProperty('childEntities', makeSquare());
|
|
784
|
+
outerOnly.buildPatternCache();
|
|
785
|
+
|
|
786
|
+
const withHole = new Hatch({ patternName: 'HONEY', scale: 1 });
|
|
787
|
+
withHole.setProperty('childEntities', [makeSquare()[0], makeInnerSquare()]);
|
|
788
|
+
withHole.buildPatternCache();
|
|
789
|
+
|
|
790
|
+
expect(totalSegments(withHole)).toBeGreaterThan(totalSegments(outerOnly));
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
// ── buildPatternCache — circle boundary tests ────────────────────────────────
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Build a circular boundary by converting a Circle entity to a polyline
|
|
797
|
+
* @param {number} cx - centre x
|
|
798
|
+
* @param {number} cy - centre y
|
|
799
|
+
* @param {number} r - radius
|
|
800
|
+
* @return {Polyline} boundary polyline
|
|
801
|
+
*/
|
|
802
|
+
const makeCircleBoundary = (cx, cy, r) => {
|
|
803
|
+
const h = new Hatch();
|
|
804
|
+
return h.processSelection([new Circle({ points: [new Point(cx, cy), new Point(cx + r, cy)] })])[0];
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
test('Hatch.buildPatternCache HONEY circle boundary has 3 families with dashes and segments', () => {
|
|
808
|
+
const h = new Hatch({ patternName: 'HONEY', scale: 1 });
|
|
809
|
+
h.setProperty('childEntities', [makeCircleBoundary(50, 50, 40)]);
|
|
810
|
+
h.buildPatternCache();
|
|
811
|
+
|
|
812
|
+
expect(h.cachedPattern.length).toBe(3);
|
|
813
|
+
for (const family of h.cachedPattern) {
|
|
814
|
+
expect(family.dashes.length).toBeGreaterThan(0);
|
|
815
|
+
expect(family.segments.length).toBeGreaterThan(0);
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
test('Hatch.buildPatternCache HONEY circle scale 2 dashes are 2× scale 1 dashes', () => {
|
|
820
|
+
const h1 = new Hatch({ patternName: 'HONEY', scale: 1 });
|
|
821
|
+
h1.setProperty('childEntities', [makeCircleBoundary(50, 50, 40)]);
|
|
822
|
+
h1.buildPatternCache();
|
|
823
|
+
|
|
824
|
+
const h2 = new Hatch({ patternName: 'HONEY', scale: 2 });
|
|
825
|
+
h2.setProperty('childEntities', [makeCircleBoundary(50, 50, 40)]);
|
|
826
|
+
h2.buildPatternCache();
|
|
827
|
+
|
|
828
|
+
for (let i = 0; i < h1.cachedPattern.length; i++) {
|
|
829
|
+
const d1 = h1.cachedPattern[i].dashes;
|
|
830
|
+
const d2 = h2.cachedPattern[i].dashes;
|
|
831
|
+
expect(d2.length).toBe(d1.length);
|
|
832
|
+
for (let j = 0; j < d1.length; j++) {
|
|
833
|
+
expect(d2[j]).toBeCloseTo(d1[j] * 2, 5);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
test('Hatch.buildPatternCache HONEY circle scale 2 produces fewer segments than scale 1', () => {
|
|
839
|
+
const h1 = new Hatch({ patternName: 'HONEY', scale: 1 });
|
|
840
|
+
h1.setProperty('childEntities', [makeCircleBoundary(50, 50, 40)]);
|
|
841
|
+
h1.buildPatternCache();
|
|
842
|
+
|
|
843
|
+
const h2 = new Hatch({ patternName: 'HONEY', scale: 2 });
|
|
844
|
+
h2.setProperty('childEntities', [makeCircleBoundary(50, 50, 40)]);
|
|
845
|
+
h2.buildPatternCache();
|
|
846
|
+
|
|
847
|
+
expect(totalSegments(h2)).toBeLessThan(totalSegments(h1));
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
test('Hatch.buildPatternCache HONEY circle all dashPhase values are non-negative', () => {
|
|
851
|
+
const h = new Hatch({ patternName: 'HONEY', scale: 1 });
|
|
852
|
+
h.setProperty('childEntities', [makeCircleBoundary(50, 50, 40)]);
|
|
853
|
+
h.buildPatternCache();
|
|
854
|
+
|
|
855
|
+
for (const family of h.cachedPattern) {
|
|
856
|
+
for (const seg of family.segments) {
|
|
857
|
+
expect(seg.dashPhase).toBeGreaterThanOrEqual(0);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
test('Hatch.buildPatternCache ANSI31 outer square + inner circle hole: no segment midpoint inside circle', () => {
|
|
863
|
+
const h = new Hatch({ patternName: 'ANSI31', scale: 1 });
|
|
864
|
+
h.setProperty('childEntities', [makeSquare()[0], makeCircleBoundary(50, 50, 15)]);
|
|
865
|
+
h.buildPatternCache();
|
|
866
|
+
|
|
867
|
+
for (const family of h.cachedPattern) {
|
|
868
|
+
for (const seg of family.segments) {
|
|
869
|
+
const mx = (seg.x1 + seg.x2) / 2;
|
|
870
|
+
const my = (seg.y1 + seg.y2) / 2;
|
|
871
|
+
expect((mx - 50) ** 2 + (my - 50) ** 2).toBeGreaterThanOrEqual(15 * 15);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
test('Hatch.buildPatternCache HONEY outer square + inner circle hole: no segment midpoint inside circle', () => {
|
|
877
|
+
const h = new Hatch({ patternName: 'HONEY', scale: 1 });
|
|
878
|
+
h.setProperty('childEntities', [makeSquare()[0], makeCircleBoundary(50, 50, 15)]);
|
|
879
|
+
h.buildPatternCache();
|
|
880
|
+
|
|
881
|
+
for (const family of h.cachedPattern) {
|
|
882
|
+
for (const seg of family.segments) {
|
|
883
|
+
const mx = (seg.x1 + seg.x2) / 2;
|
|
884
|
+
const my = (seg.y1 + seg.y2) / 2;
|
|
885
|
+
expect((mx - 50) ** 2 + (my - 50) ** 2).toBeGreaterThanOrEqual(15 * 15);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
test('Hatch.buildPatternCache ANSI31 outer circle + inner circle hole: no segment midpoint inside inner circle', () => {
|
|
891
|
+
const h = new Hatch({ patternName: 'ANSI31', scale: 1 });
|
|
892
|
+
h.setProperty('childEntities', [makeCircleBoundary(50, 50, 40), makeCircleBoundary(50, 50, 15)]);
|
|
893
|
+
h.buildPatternCache();
|
|
894
|
+
|
|
895
|
+
expect(h.cachedPattern[0].segments.length).toBeGreaterThan(0);
|
|
896
|
+
for (const family of h.cachedPattern) {
|
|
897
|
+
for (const seg of family.segments) {
|
|
898
|
+
const mx = (seg.x1 + seg.x2) / 2;
|
|
899
|
+
const my = (seg.y1 + seg.y2) / 2;
|
|
900
|
+
expect((mx - 50) ** 2 + (my - 50) ** 2).toBeGreaterThanOrEqual(15 * 15);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
test('Hatch.buildPatternCache HONEY outer circle + inner circle hole: no segment midpoint inside inner circle', () => {
|
|
906
|
+
const h = new Hatch({ patternName: 'HONEY', scale: 1 });
|
|
907
|
+
h.setProperty('childEntities', [makeCircleBoundary(50, 50, 40), makeCircleBoundary(50, 50, 15)]);
|
|
908
|
+
h.buildPatternCache();
|
|
909
|
+
|
|
910
|
+
expect(h.cachedPattern.length).toBe(3);
|
|
911
|
+
for (const family of h.cachedPattern) {
|
|
912
|
+
for (const seg of family.segments) {
|
|
913
|
+
const mx = (seg.x1 + seg.x2) / 2;
|
|
914
|
+
const my = (seg.y1 + seg.y2) / 2;
|
|
915
|
+
expect((mx - 50) ** 2 + (my - 50) ** 2).toBeGreaterThanOrEqual(15 * 15);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
// ── setProperty ──────────────────────────────────────────────────────────────
|
|
921
|
+
|
|
922
|
+
test('Hatch.setProperty unknown property does nothing', () => {
|
|
923
|
+
const h = new Hatch({ patternName: 'ANSI31', angle: 10 });
|
|
924
|
+
h.setProperty('nonExistentProperty', 99);
|
|
925
|
+
expect(h.hasOwnProperty('nonExistentProperty')).toBe(false);
|
|
926
|
+
expect(h.getProperty('angle')).toBe(10);
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
test('Hatch.setProperty known property updates value and invalidates cache', () => {
|
|
930
|
+
const h = new Hatch({ patternName: 'ANSI31', angle: 0 });
|
|
931
|
+
h.setProperty('childEntities', makeSquare());
|
|
932
|
+
h.buildPatternCache();
|
|
933
|
+
expect(h.cachedPattern).not.toBeNull();
|
|
934
|
+
|
|
935
|
+
h.setProperty('angle', 45);
|
|
936
|
+
|
|
937
|
+
expect(h.getProperty('angle')).toBe(45);
|
|
938
|
+
expect(h.cachedPattern).toBeNull();
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
test('Hatch.setProperty patternName updates pattern and solid flag via setter', () => {
|
|
942
|
+
const h = new Hatch({ patternName: 'ANSI31' });
|
|
943
|
+
expect(h.getProperty('patternName')).toBe('ANSI31');
|
|
944
|
+
|
|
945
|
+
h.setProperty('patternName', 'SOLID');
|
|
946
|
+
|
|
947
|
+
expect(h.getProperty('patternName')).toBe('SOLID');
|
|
948
|
+
expect(h.cachedPattern).toBeNull();
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
test('Hatch.setProperty points translation shifts child entity positions', () => {
|
|
952
|
+
const h = new Hatch({ patternName: 'ANSI31' });
|
|
953
|
+
// h.points = [Point(0,0), Point(1,1)] — 45° angle set by constructor
|
|
954
|
+
h.setProperty('childEntities', [new BasePolyline({ points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] })]);
|
|
955
|
+
|
|
956
|
+
// New points: same 45° angle (delta angle = 0 → no rotation), origin shifted by (5, 0)
|
|
957
|
+
h.setProperty('points', [new Point(5, 0), new Point(6, 1)]);
|
|
958
|
+
|
|
959
|
+
expect(h.getProperty('childEntities')[0].points[0].x).toBeCloseTo(5);
|
|
960
|
+
expect(h.getProperty('childEntities')[0].points[0].y).toBeCloseTo(0);
|
|
961
|
+
expect(h.getProperty('childEntities')[0].points[1].x).toBeCloseTo(15);
|
|
962
|
+
expect(h.getProperty('childEntities')[0].points[1].y).toBeCloseTo(0);
|
|
963
|
+
expect(h.cachedPattern).toBeNull();
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
test('Hatch.setProperty points rotation updates hatch angle', () => {
|
|
967
|
+
const h = new Hatch({ patternName: 'ANSI31', angle: 0 });
|
|
968
|
+
h.setProperty('childEntities', makeSquare());
|
|
969
|
+
// h.points = [Point(0,0), Point(1,1)] — 45° angle
|
|
970
|
+
// New points: [Point(0,0), Point(1,0)] — 0° angle → theta = -45°
|
|
971
|
+
h.setProperty('points', [new Point(0, 0), new Point(1, 0)]);
|
|
972
|
+
|
|
973
|
+
// hatch.angle should decrease by 45°
|
|
974
|
+
expect(h.getProperty('angle')).toBeCloseTo(-45, 1);
|
|
975
|
+
expect(h.cachedPattern).toBeNull();
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
// ── touched ──────────────────────────────────────────────────────────────────
|
|
979
|
+
|
|
980
|
+
test('Hatch.touched with no childEntities returns false', () => {
|
|
981
|
+
const h = new Hatch();
|
|
982
|
+
expect(h.touched({ min: new Point(-999, -999), max: new Point(999, 999) })).toBe(false);
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
test('Hatch.touched returns true when selection overlaps boundary', () => {
|
|
986
|
+
const h = new Hatch({ patternName: 'ANSI31' });
|
|
987
|
+
h.setProperty('childEntities', makeSquare()); // 0,0 → 100,100
|
|
988
|
+
expect(h.touched({ min: new Point(-10, -10), max: new Point(50, 50) })).toBe(true);
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
test('Hatch.touched returns false when selection does not overlap boundary', () => {
|
|
992
|
+
const h = new Hatch({ patternName: 'ANSI31' });
|
|
993
|
+
h.setProperty('childEntities', makeSquare()); // 0,0 → 100,100
|
|
994
|
+
expect(h.touched({ min: new Point(200, 200), max: new Point(300, 300) })).toBe(false);
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
// ── draw: scale guard and stroke/dash tracking ───────────────────────────────
|
|
998
|
+
|
|
999
|
+
test('Hatch.draw scale guard resets sub-zero scale to 1 and rebuilds cache', () => {
|
|
1000
|
+
const h = new Hatch({ patternName: 'ANSI31' });
|
|
1001
|
+
h.setProperty('childEntities', makeSquare());
|
|
1002
|
+
h.setProperty('scale', 0.001); // below guard threshold
|
|
1003
|
+
|
|
1004
|
+
const ctx = makeMockCtx();
|
|
1005
|
+
h.draw(ctx);
|
|
1006
|
+
|
|
1007
|
+
expect(h.getProperty('scale')).toBe(1);
|
|
1008
|
+
expect(h.cachedPattern).not.toBeNull();
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Create a mock renderer that tracks stroke call count and setDash offset arguments
|
|
1013
|
+
* @return {Object} tracking mock renderer
|
|
1014
|
+
*/
|
|
1015
|
+
function makeTrackingCtx() {
|
|
1016
|
+
let strokeCount = 0;
|
|
1017
|
+
const lineDashOffsets = [];
|
|
1018
|
+
return {
|
|
1019
|
+
setDash: () => {},
|
|
1020
|
+
beginPath: () => {},
|
|
1021
|
+
closePath: () => {},
|
|
1022
|
+
tracePath: () => {},
|
|
1023
|
+
stroke: () => {
|
|
1024
|
+
strokeCount++;
|
|
1025
|
+
},
|
|
1026
|
+
fill: () => {},
|
|
1027
|
+
drawShape: () => {},
|
|
1028
|
+
drawSegments: (segments, dashes) => {
|
|
1029
|
+
if (!dashes.length) {
|
|
1030
|
+
strokeCount++;
|
|
1031
|
+
} else {
|
|
1032
|
+
for (const seg of segments) {
|
|
1033
|
+
lineDashOffsets.push(seg.dashPhase ?? 0);
|
|
1034
|
+
strokeCount++;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
},
|
|
1038
|
+
getStrokeCalls: () => strokeCount,
|
|
1039
|
+
getLineDashOffsets: () => lineDashOffsets,
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
test('Hatch.draw ANSI31 solid-line family batches all segments into one stroke call', () => {
|
|
1044
|
+
const h = new Hatch({ patternName: 'ANSI31' });
|
|
1045
|
+
h.setProperty('childEntities', makeSquare());
|
|
1046
|
+
|
|
1047
|
+
const ctx = makeTrackingCtx();
|
|
1048
|
+
h.draw(ctx);
|
|
1049
|
+
|
|
1050
|
+
// ANSI31 has no dashes → solid-line branch → one beginPath + all tracePath + one stroke()
|
|
1051
|
+
expect(ctx.getStrokeCalls()).toBe(1);
|
|
1052
|
+
expect(ctx.getLineDashOffsets().length).toBe(0);
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
test('Hatch.draw HONEY dashed family sets lineDashOffset and strokes once per segment', () => {
|
|
1056
|
+
const h = new Hatch({ patternName: 'HONEY' });
|
|
1057
|
+
h.setProperty('childEntities', makeSquare());
|
|
1058
|
+
h.buildPatternCache();
|
|
1059
|
+
const expectedSegs = totalSegments(h);
|
|
1060
|
+
|
|
1061
|
+
const ctx = makeTrackingCtx();
|
|
1062
|
+
h.draw(ctx);
|
|
1063
|
+
|
|
1064
|
+
// Every dashed segment gets its own setDash offset and stroke call
|
|
1065
|
+
expect(ctx.getLineDashOffsets().length).toBe(expectedSegs);
|
|
1066
|
+
expect(ctx.getStrokeCalls()).toBe(expectedSegs);
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
// ── processSelection: uncloseable items ──────────────────────────────────────
|
|
1070
|
+
|
|
1071
|
+
test('Hatch.processSelection returns empty array when items cannot form a closed loop', () => {
|
|
1072
|
+
// Two parallel lines that share no endpoints — no closed boundary possible
|
|
1073
|
+
const disconnected = [
|
|
1074
|
+
new Line({ points: [new Point(0, 0), new Point(10, 0)] }),
|
|
1075
|
+
new Line({ points: [new Point(0, 5), new Point(10, 5)] }),
|
|
1076
|
+
];
|
|
1077
|
+
const h = new Hatch();
|
|
1078
|
+
expect(h.processSelection(disconnected)).toEqual([]);
|
|
1079
|
+
});
|