@design-core/core 0.0.7 → 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.
Files changed (117) hide show
  1. package/.github/workflows/DesignCoreQA.yml +3 -0
  2. package/core/designCore.js +1 -1
  3. package/core/dimensions/alignedDimension.js +7 -7
  4. package/core/dimensions/angularDimension.js +32 -32
  5. package/core/dimensions/{baseCircularDimension.js → circularDimensionBase.js} +19 -19
  6. package/core/dimensions/diametricDimension.js +10 -10
  7. package/core/dimensions/dimension.js +42 -42
  8. package/core/dimensions/{baseDimension.js → dimensionBase.js} +17 -17
  9. package/core/dimensions/{baseLinearDimension.js → linearDimensionBase.js} +9 -9
  10. package/core/dimensions/radialDimension.js +8 -8
  11. package/core/dimensions/rotatedDimension.js +8 -8
  12. package/core/entities/arc.js +22 -22
  13. package/core/entities/arctext.js +13 -13
  14. package/core/entities/circle.js +7 -7
  15. package/core/entities/hatch.js +210 -40
  16. package/core/entities/insert.js +11 -6
  17. package/core/entities/point.js +19 -19
  18. package/core/entities/polyline.js +3 -3
  19. package/core/entities/{basePolyline.js → polylineBase.js} +18 -18
  20. package/core/entities/text.js +4 -4
  21. package/core/lib/boundingBox.js +11 -11
  22. package/core/lib/canvas.js +13 -6
  23. package/core/lib/clipboard.js +2 -2
  24. package/core/lib/dxf/dxf.js +11 -11
  25. package/core/lib/dxf/dxfIterator.js +2 -2
  26. package/core/lib/entityManager.js +19 -19
  27. package/core/lib/inputManager.js +1 -1
  28. package/core/lib/matrix.js +1 -1
  29. package/core/lib/patterns.js +1 -1
  30. package/core/lib/polylineUtils.js +9 -9
  31. package/core/lib/renderers/cairoRenderer.js +5 -5
  32. package/core/lib/renderers/canvasRenderer.js +4 -4
  33. package/core/lib/renderers/pdfRenderer.js +7 -7
  34. package/core/lib/renderers/rendererBase.js +1 -1
  35. package/core/lib/scene.js +1 -1
  36. package/core/lib/selectionManager.js +18 -18
  37. package/core/lib/utils.js +5 -5
  38. package/core/properties/entityProperties.js +2 -1
  39. package/core/properties/flags.js +12 -0
  40. package/core/properties/property.js +8 -3
  41. package/core/properties/propertyManager.js +52 -37
  42. package/core/tables/block.js +50 -46
  43. package/core/tables/dimStyle.js +5 -5
  44. package/core/tables/ltypeManager.js +1 -1
  45. package/core/tables/vport.js +2 -2
  46. package/core/tools/chamfer.js +6 -6
  47. package/core/tools/chamferFilletBase.js +4 -4
  48. package/core/tools/copy.js +8 -8
  49. package/core/tools/copybase.js +3 -3
  50. package/core/tools/copyclip.js +3 -3
  51. package/core/tools/cornerEntity.js +2 -2
  52. package/core/tools/cutclip.js +3 -3
  53. package/core/tools/explode.js +6 -6
  54. package/core/tools/extend.js +13 -13
  55. package/core/tools/fillet.js +6 -6
  56. package/core/tools/matchProp.js +1 -1
  57. package/core/tools/mirror.js +11 -11
  58. package/core/tools/move.js +5 -5
  59. package/core/tools/offset.js +29 -29
  60. package/core/tools/purge.js +10 -10
  61. package/core/tools/rotate.js +16 -16
  62. package/core/tools/scale.js +6 -6
  63. package/core/tools/trim.js +15 -15
  64. package/cspell.json +323 -0
  65. package/package.json +1 -1
  66. package/test/dimensions/alignedDimension.test.js +3 -3
  67. package/test/dimensions/angularDimension.test.js +7 -7
  68. package/test/dimensions/diametricDimension.test.js +9 -9
  69. package/test/dimensions/dimension.test.js +12 -12
  70. package/test/dimensions/{baseDimension.test.js → dimensionBase.test.js} +12 -12
  71. package/test/dimensions/{baseLinearDimension.test.js → linearDimensionBase.test.js} +7 -7
  72. package/test/dimensions/radialDimension.test.js +9 -9
  73. package/test/dimensions/rotatedDimension.test.js +4 -4
  74. package/test/entities/arc.test.js +10 -10
  75. package/test/entities/arctext.test.js +6 -6
  76. package/test/entities/circle.test.js +18 -18
  77. package/test/entities/hatch.test.js +362 -39
  78. package/test/entities/insert.test.js +9 -9
  79. package/test/entities/point.test.js +3 -3
  80. package/test/entities/{basePolyline.test.js → polylineBase.test.js} +131 -62
  81. package/test/entities/text.test.js +19 -19
  82. package/test/integration/exportIntegration.test.js +3 -3
  83. package/test/integration/userInputIntegration.test.js +5 -5
  84. package/test/lib/boundingBox.test.js +3 -3
  85. package/test/lib/canvas.test.js +9 -9
  86. package/test/lib/clipboard.test.js +73 -0
  87. package/test/lib/inputManager.test.js +4 -4
  88. package/test/lib/scene.test.js +9 -9
  89. package/test/lib/selectionManager.test.js +3 -3
  90. package/test/lib/snapping.test.js +1 -1
  91. package/test/properties/flags.test.js +24 -0
  92. package/test/properties/propertyManager.test.js +93 -22
  93. package/test/renderers/pdfRenderer.test.js +2 -2
  94. package/test/tables/block.test.js +63 -14
  95. package/test/tables/layerManager.test.js +2 -2
  96. package/test/test-helpers/hatchGridRenderer.js +3 -3
  97. package/test/test-helpers/test-helpers.js +6 -6
  98. package/test/tools/chamfer.test.js +60 -60
  99. package/test/tools/chamferFilletBase.test.js +36 -36
  100. package/test/tools/copy.test.js +6 -6
  101. package/test/tools/copybase.test.js +2 -2
  102. package/test/tools/copyclip.test.js +2 -2
  103. package/test/tools/cornerEntity.test.js +8 -8
  104. package/test/tools/cutclip.test.js +2 -2
  105. package/test/tools/erase.test.js +6 -6
  106. package/test/tools/explode.test.js +2 -2
  107. package/test/tools/extend.test.js +84 -84
  108. package/test/tools/fillet.test.js +62 -62
  109. package/test/tools/matchProp.test.js +6 -6
  110. package/test/tools/mirror.test.js +26 -26
  111. package/test/tools/move.test.js +7 -7
  112. package/test/tools/offset.test.js +65 -65
  113. package/test/tools/purge.test.js +4 -4
  114. package/test/tools/rotate.test.js +10 -10
  115. package/test/tools/scale.test.js +28 -28
  116. package/test/tools/trim.test.js +109 -109
  117. package/test/tools/zoom.test.js +15 -17
@@ -1,8 +1,9 @@
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
 
5
- import { BasePolyline } from '../../core/entities/basePolyline.js';
6
+ import { PolylineBase } from '../../core/entities/polylineBase.js';
6
7
  import { Circle } from '../../core/entities/circle.js';
7
8
  import { Line } from '../../core/entities/line.js';
8
9
  import { Polyline } from '../../core/entities/polyline.js';
@@ -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
 
@@ -17,7 +20,7 @@ import { Core } from '../../core/core/core.js';
17
20
  new Core();
18
21
 
19
22
  const points = [new Point(100, 100, 1), new Point(200, 100, 1)];
20
- const boundaryShape = new BasePolyline({ points: points });
23
+ const boundaryShape = new PolylineBase({ points: points });
21
24
 
22
25
  const hatch = new Hatch({ handle: '1' });
23
26
  hatch.setProperty('childEntities', [boundaryShape]);
@@ -56,10 +59,10 @@ test.each(hatchInputScenarios)('Hatch.execute handles $desc', async (scenario) =
56
59
 
57
60
  await withMockInput(DesignCore.Scene, [true], async () => {
58
61
  // Manual mock for selected items
59
- DesignCore.Scene.selectionManager.selectedItems = [];
62
+ DesignCore.Scene.selectionManager.selectedEntities = [];
60
63
 
61
64
  for (let i = 0; i < boundaryItems.length; i++) {
62
- DesignCore.Scene.selectionManager.selectedItems.push(boundaryItems[i]);
65
+ DesignCore.Scene.selectionManager.selectedEntities.push(boundaryItems[i]);
63
66
  }
64
67
 
65
68
  const hatch = new Hatch({ patternName: pattern, scale: scale, angle: angle });
@@ -448,60 +451,60 @@ test('Test Hatch.processBoundaryData', () => {
448
451
 
449
452
  test('Test Hatch.processSelection', () => {
450
453
  // create selected items defining a square
451
- let selectedItems = [];
452
- selectedItems.push(new Line({ points: [new Point(100, 100), new Point(200, 100)] }));
453
- selectedItems.push(new Line({ points: [new Point(200, 100), new Point(200, 200)] }));
454
- selectedItems.push(new Line({ points: [new Point(200, 200), new Point(100, 200)] }));
455
- selectedItems.push(new Line({ points: [new Point(100, 200), new Point(100, 100)] }));
454
+ let selectedEntities = [];
455
+ selectedEntities.push(new Line({ points: [new Point(100, 100), new Point(200, 100)] }));
456
+ selectedEntities.push(new Line({ points: [new Point(200, 100), new Point(200, 200)] }));
457
+ selectedEntities.push(new Line({ points: [new Point(200, 200), new Point(100, 200)] }));
458
+ selectedEntities.push(new Line({ points: [new Point(100, 200), new Point(100, 100)] }));
456
459
 
457
460
  let hatch = new Hatch();
458
- let boundaryData = hatch.processSelection(selectedItems);
461
+ let boundaryData = hatch.processSelection(selectedEntities);
459
462
  expect(boundaryData.length).toEqual(1);
460
463
  expect(boundaryData[0].points.length).toEqual(4);
461
464
 
462
465
 
463
466
  // create selected items defining a circle
464
- selectedItems = [];
465
- selectedItems.push(new Circle({ points: [new Point(100, 100), new Point(200, 100)] }));
467
+ selectedEntities = [];
468
+ selectedEntities.push(new Circle({ points: [new Point(100, 100), new Point(200, 100)] }));
466
469
 
467
470
  hatch = new Hatch();
468
- boundaryData = hatch.processSelection(selectedItems);
471
+ boundaryData = hatch.processSelection(selectedEntities);
469
472
  expect(boundaryData.length).toEqual(1);
470
473
  expect(boundaryData[0].points.length).toEqual(2);
471
474
 
472
475
  // create selected items defining a square (polyline)
473
- selectedItems = [];
474
- selectedItems.push(new Polyline({
476
+ selectedEntities = [];
477
+ selectedEntities.push(new Polyline({
475
478
  points: [new Point(100, 100), new Point(200, 100), new Point(200, 200),
476
479
  new Point(100, 200), new Point(100, 100)],
477
480
  }));
478
481
 
479
482
  hatch = new Hatch();
480
- boundaryData = hatch.processSelection(selectedItems);
483
+ boundaryData = hatch.processSelection(selectedEntities);
481
484
  expect(boundaryData.length).toEqual(1);
482
485
  expect(boundaryData[0].points.length).toEqual(4);
483
486
 
484
487
  // create selected items defining a pill shape
485
- selectedItems = [];
486
- selectedItems.push(new Line({ points: [new Point(100, 100), new Point(200, 100)] }));
487
- selectedItems.push(new Arc({ points: [new Point(200, 150), new Point(200, 100), new Point(200, 200)] }));
488
- selectedItems.push(new Line({ points: [new Point(200, 200), new Point(100, 200)] }));
489
- selectedItems.push(new Arc({ points: [new Point(100, 150), new Point(100, 200), new Point(100, 100)] }));
488
+ selectedEntities = [];
489
+ selectedEntities.push(new Line({ points: [new Point(100, 100), new Point(200, 100)] }));
490
+ selectedEntities.push(new Arc({ points: [new Point(200, 150), new Point(200, 100), new Point(200, 200)] }));
491
+ selectedEntities.push(new Line({ points: [new Point(200, 200), new Point(100, 200)] }));
492
+ selectedEntities.push(new Arc({ points: [new Point(100, 150), new Point(100, 200), new Point(100, 100)] }));
490
493
 
491
494
  hatch = new Hatch();
492
- boundaryData = hatch.processSelection(selectedItems);
495
+ boundaryData = hatch.processSelection(selectedEntities);
493
496
  expect(boundaryData.length).toEqual(1);
494
497
  // 2 lines + 2 arcs: each shared connection point is merged (not duplicated),
495
498
  // so the result is 4 unique vertices, not 5
496
499
  expect(boundaryData[0].points.length).toEqual(4);
497
500
 
498
501
  // Test a selection with invalid items
499
- selectedItems = [];
500
- selectedItems.push(new Text({ points: [new Point(100, 100), new Point(200, 100)] }));
501
- selectedItems.push(new Circle({ points: [new Point(100, 100), new Point(200, 100)] }));
502
+ selectedEntities = [];
503
+ selectedEntities.push(new Text({ points: [new Point(100, 100), new Point(200, 100)] }));
504
+ selectedEntities.push(new Circle({ points: [new Point(100, 100), new Point(200, 100)] }));
502
505
 
503
506
  hatch = new Hatch();
504
- boundaryData = hatch.processSelection(selectedItems);
507
+ boundaryData = hatch.processSelection(selectedEntities);
505
508
  expect(boundaryData.length).toEqual(1);
506
509
  expect(boundaryData[0].points.length).toEqual(2);
507
510
  });
@@ -539,7 +542,7 @@ function makeMockCtx() {
539
542
 
540
543
  test('Test Hatch.draw solid fill renders filled boundary', () => {
541
544
  const solidHatch = new Hatch({ patternName: 'SOLID' });
542
- solidHatch.setProperty('childEntities', [new BasePolyline({ points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] })]);
545
+ solidHatch.setProperty('childEntities', [new PolylineBase({ points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] })]);
543
546
 
544
547
  const ctx = makeMockCtx();
545
548
  solidHatch.draw(ctx);
@@ -549,7 +552,7 @@ test('Test Hatch.draw solid fill renders filled boundary', () => {
549
552
 
550
553
  test('Test Hatch.draw unknown pattern does not stroke pattern lines', () => {
551
554
  const unknownHatch = new Hatch({ patternName: 'ANSI31' });
552
- unknownHatch.setProperty('childEntities', [new BasePolyline({ points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] })]);
555
+ unknownHatch.setProperty('childEntities', [new PolylineBase({ points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] })]);
553
556
  // Override pattern to a nonexistent name
554
557
  unknownHatch.setProperty('patternName', 'NOTAPATTERN');
555
558
 
@@ -567,7 +570,7 @@ test('Test Hatch.draw tight bound line count - square boundary', () => {
567
570
  // yIncrement = ceil(70.71 / 3.175) = 23 → loop i=-23..22 = 46 raw lines
568
571
  // i=-23 (|ly|=73.025 > halfY=70.71) misses the square → 45 clipped segments
569
572
  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)] })]);
573
+ squareHatch.setProperty('childEntities', [new PolylineBase({ points: [new Point(0, 0), new Point(100, 0), new Point(100, 100), new Point(0, 100)] })]);
571
574
  const ctx = makeMockCtx();
572
575
  squareHatch.draw(ctx);
573
576
  expect(ctx.getMoveCalls()).toBe(45);
@@ -580,7 +583,7 @@ test('Test Hatch.draw tight bound line count - flat boundary', () => {
580
583
  // yIncrement = ceil(38.89 / 3.175) = 13 → loop i=-13..12 = 26 raw lines
581
584
  // i=-13 (|ly|=41.275 > halfY=38.89) misses the flat box → 25 clipped segments
582
585
  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)] })]);
586
+ flatHatch.setProperty('childEntities', [new PolylineBase({ points: [new Point(0, 0), new Point(100, 0), new Point(100, 10), new Point(0, 10)] })]);
584
587
  const ctx = makeMockCtx();
585
588
  flatHatch.draw(ctx);
586
589
  expect(ctx.getMoveCalls()).toBe(25);
@@ -589,10 +592,10 @@ test('Test Hatch.draw tight bound line count - flat boundary', () => {
589
592
  test('Test Hatch.draw tight bound produces fewer lines for flat boundary', () => {
590
593
  // A flat boundary (100x10) should produce fewer lines than a square (100x100).
591
594
  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)] })]);
595
+ squareHatch.setProperty('childEntities', [new PolylineBase({ points: [new Point(0, 0), new Point(100, 0), new Point(100, 100), new Point(0, 100)] })]);
593
596
 
594
597
  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
+ flatHatch.setProperty('childEntities', [new PolylineBase({ points: [new Point(0, 0), new Point(100, 0), new Point(100, 10), new Point(0, 10)] })]);
596
599
 
597
600
  const squareCtx = makeMockCtx();
598
601
  squareHatch.draw(squareCtx);
@@ -614,14 +617,14 @@ test('Hatch.buildPatternCache no childEntities sets cachedPattern to empty array
614
617
 
615
618
  test('Hatch.buildPatternCache solid fill sets cachedPattern to empty array', () => {
616
619
  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)] })]);
620
+ h.setProperty('childEntities', [new PolylineBase({ points: [new Point(0, 0), new Point(100, 0), new Point(100, 100), new Point(0, 100)] })]);
618
621
  h.buildPatternCache();
619
622
  expect(h.cachedPattern).toEqual([]);
620
623
  });
621
624
 
622
625
  test('Hatch.buildPatternCache unknown pattern sets cachedPattern to empty array', () => {
623
626
  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)] })]);
627
+ h.setProperty('childEntities', [new PolylineBase({ points: [new Point(0, 0), new Point(100, 0), new Point(100, 100), new Point(0, 100)] })]);
625
628
  h.setProperty('patternName', 'NOTAPATTERN');
626
629
  h.buildPatternCache();
627
630
  expect(h.cachedPattern).toEqual([]);
@@ -631,9 +634,9 @@ test('Hatch.buildPatternCache unknown pattern sets cachedPattern to empty array'
631
634
 
632
635
  /**
633
636
  * Build a 100×100 square boundary
634
- * @return {Array} array containing one BasePolyline boundary
637
+ * @return {Array} array containing one PolylineBase boundary
635
638
  */
636
- const makeSquare = () => [new BasePolyline({ points: [new Point(0, 0), new Point(100, 0), new Point(100, 100), new Point(0, 100)] })];
639
+ const makeSquare = () => [new PolylineBase({ points: [new Point(0, 0), new Point(100, 0), new Point(100, 100), new Point(0, 100)] })];
637
640
 
638
641
  /**
639
642
  * Sum all segment counts across all families in a cached hatch
@@ -731,9 +734,9 @@ test('Hatch.buildPatternCache scale change invalidates and rebuilds cache with u
731
734
 
732
735
  /**
733
736
  * A 20×20 inner square hole centred in the 100×100 outer square
734
- * @return {BasePolyline} inner boundary polyline
737
+ * @return {PolylineBase} inner boundary polyline
735
738
  */
736
- const makeInnerSquare = () => new BasePolyline({ points: [new Point(40, 40), new Point(60, 40), new Point(60, 60), new Point(40, 60)] });
739
+ const makeInnerSquare = () => new PolylineBase({ points: [new Point(40, 40), new Point(60, 40), new Point(60, 60), new Point(40, 60)] });
737
740
 
738
741
  test('Hatch.buildPatternCache ANSI31 nested boundary: no segment midpoint inside inner hole', () => {
739
742
  // Even-odd rule: a ray from inside the hole crosses 2 boundaries → even → not hatched
@@ -951,7 +954,7 @@ test('Hatch.setProperty patternName updates pattern and solid flag via setter',
951
954
  test('Hatch.setProperty points translation shifts child entity positions', () => {
952
955
  const h = new Hatch({ patternName: 'ANSI31' });
953
956
  // 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)] })]);
957
+ h.setProperty('childEntities', [new PolylineBase({ points: [new Point(0, 0), new Point(10, 0), new Point(10, 10), new Point(0, 10)] })]);
955
958
 
956
959
  // New points: same 45° angle (delta angle = 0 → no rotation), origin shifted by (5, 0)
957
960
  h.setProperty('points', [new Point(5, 0), new Point(6, 1)]);
@@ -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
+ });
@@ -13,10 +13,10 @@ new Core();
13
13
 
14
14
  const insert = new Insert({ handle: '1', points: [new Point()] });
15
15
  const line = new Line({ points: [new Point(101, 102), new Point(201, 202)] });
16
- insert.block.addItem(line);
16
+ insert.block.addEntity(line);
17
17
 
18
18
  const rotatedInsert = new Insert({ points: [new Point()], rotation: 45 });
19
- rotatedInsert.block.addItem(line);
19
+ rotatedInsert.block.addEntity(line);
20
20
 
21
21
  test('Test Insert.snaps', () => {
22
22
  const point = new Point(100, 100);
@@ -51,7 +51,7 @@ test('Test Insert.boundingBox', () => {
51
51
  expect(insert.boundingBox().yMax).toBeCloseTo(202);
52
52
 
53
53
  const line2 = new Line({ points: [new Point(1001, 1002), new Point(2001, 2002)] });
54
- insert.block.addItem(line2);
54
+ insert.block.addEntity(line2);
55
55
 
56
56
  expect(insert.boundingBox().xMin).toBeCloseTo(101);
57
57
  expect(insert.boundingBox().xMax).toBeCloseTo(2001);
@@ -115,17 +115,17 @@ describe('Test Insert.draw', () => {
115
115
  test('draw() returns the block items array', () => {
116
116
  const ins = new Insert({ points: [new Point(0, 0)] });
117
117
  const line = new Line({ points: [new Point(0, 0), new Point(1, 1)] });
118
- ins.block.addItem(line);
118
+ ins.block.addEntity(line);
119
119
 
120
120
  const result = ins.draw(makeMockCtx());
121
- expect(result).toBe(ins.block.items);
121
+ expect(result).toBe(ins.block.entities);
122
122
  });
123
123
 
124
124
  test('draw() returns empty array when block has no items', () => {
125
125
  const ins = new Insert({ points: [new Point(0, 0)] });
126
126
  const result = ins.draw(makeMockCtx());
127
127
  expect(result).toEqual([]);
128
- expect(result).toBe(ins.block.items);
128
+ expect(result).toBe(ins.block.entities);
129
129
  });
130
130
 
131
131
  test('draw() calls renderer.applyTransform with the insert point coordinates', () => {
@@ -159,9 +159,9 @@ describe('Test Insert.draw', () => {
159
159
  test('draw() returns same items reference regardless of scale', () => {
160
160
  const ins = new Insert({ points: [new Point(0, 0)] });
161
161
  const line = new Line({ points: [new Point(0, 0), new Point(1, 1)] });
162
- ins.block.addItem(line);
163
- expect(ins.draw(makeMockCtx())).toBe(ins.block.items);
164
- expect(ins.draw(makeMockCtx())).toBe(ins.block.items);
162
+ ins.block.addEntity(line);
163
+ expect(ins.draw(makeMockCtx())).toBe(ins.block.entities);
164
+ expect(ins.draw(makeMockCtx())).toBe(ins.block.entities);
165
165
  });
166
166
  });
167
167
 
@@ -701,7 +701,7 @@ test('Test Point.bulgeAngle', () => {
701
701
 
702
702
  test('Test Point.bulgeRadius', () => {
703
703
  // start point: 100,0
704
- // center: 100,50
704
+ // centre: 100,50
705
705
  // radius: 50
706
706
  const point = new Point(100, 0);
707
707
 
@@ -771,7 +771,7 @@ test('Test Point.bulgeRadius', () => {
771
771
 
772
772
  test('Test Point.apothem', () => {
773
773
  // start point: 100,0
774
- // center: 100,50
774
+ // centre: 100,50
775
775
  // radius: 50
776
776
  const point = new Point(100, 0);
777
777
 
@@ -829,7 +829,7 @@ test('Test Point.apothem', () => {
829
829
 
830
830
  test('Test Point.bulgeCentrePoint', () => {
831
831
  // start point: 100,0
832
- // center: 100,50
832
+ // centre: 100,50
833
833
  // radius: 50
834
834
  const point = new Point(100, 0);
835
835