@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
@@ -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
- const resolvedPatternName = data?.[70] === 1 ? 'SOLID' : rawPatternName;
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: () => Object.keys(Patterns.hatch_patterns).map((name) => ({ display: name, value: name })),
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 (this.getProperty(Property.Names.PATTERNNAME) === 'SOLID' || !Patterns.patternExists(this.getProperty(Property.Names.PATTERNNAME))) {
268
+ if (!this.patternLines.length) {
118
269
  this.cachedPattern = [];
119
270
  return;
120
271
  }
@@ -141,13 +292,13 @@ export class Hatch extends Entity {
141
292
  }
142
293
  }
143
294
 
144
- const centerPoint = bb.centerPoint;
295
+ const centrePoint = bb.centrePoint;
145
296
  const bbXLength = bb.xLength;
146
297
  const bbYLength = bb.yLength;
147
298
  const s = this.getProperty(Property.Names.SCALE);
148
299
  const lines = [];
149
300
 
150
- const pattern = Patterns.getPattern(this.getProperty(Property.Names.PATTERNNAME));
301
+ const pattern = this.patternLines;
151
302
  pattern.forEach((patternLine) => {
152
303
  let dashLength = bbXLength / 2;
153
304
  if (patternLine.dashes.length) {
@@ -175,8 +326,8 @@ export class Hatch extends Entity {
175
326
 
176
327
  // World-space anchor for this family: bounding-box centre shifted by the
177
328
  // pattern line's origin offset (scaled to match the hatch scale).
178
- const cx = centerPoint.x + patternLine.xOrigin * s;
179
- const cy = centerPoint.y + patternLine.yOrigin * s;
329
+ const cx = centrePoint.x + patternLine.xOrigin * s;
330
+ const cy = centrePoint.y + patternLine.yOrigin * s;
180
331
 
181
332
  // Bounding-box half-extents in pattern-local (unscaled) space.
182
333
  const halfW = bbXLength / (2 * s);
@@ -390,13 +541,13 @@ export class Hatch extends Entity {
390
541
  // 10 - X; 20 - Y; 40 - Radius; 50 - Start Angle; 51 - End Angle; 73 - Is Counter Clockwise
391
542
  const shapeData = { points: [] };
392
543
 
393
- const centerPoint = points.shift();
394
- if (centerPoint.sequence !== undefined && centerPoint.sequence !== 10) {
544
+ const centrePoint = points.shift();
545
+ if (centrePoint.sequence !== undefined && centrePoint.sequence !== 10) {
395
546
  const msg = 'Invalid point sequence';
396
547
  const err = (`${this.type} - ${msg}`);
397
548
  throw Error(err);
398
549
  }
399
- shapeData.points.push(centerPoint);
550
+ shapeData.points.push(centrePoint);
400
551
  shapeData.radius = this.getDataValue(data, 40);
401
552
  shapeData.startAngle = this.getDataValue(data, 50);
402
553
  shapeData.endAngle = this.getDataValue(data, 51);
@@ -482,8 +633,8 @@ export class Hatch extends Entity {
482
633
  const result = await DesignCore.Scene.inputManager.requestInput(op);
483
634
  if (result === undefined) return;
484
635
 
485
- const selectedItems = DesignCore.Scene.selectionManager.selectedItems.slice(0);
486
- const boundary = this.processSelection(selectedItems);
636
+ const selectedEntities = DesignCore.Scene.selectionManager.selectedEntities.slice(0);
637
+ const boundary = this.processSelection(selectedEntities);
487
638
 
488
639
  if (boundary.length) {
489
640
  this.setProperty(Property.Names.CHILDENTITIES, boundary);
@@ -506,8 +657,8 @@ export class Hatch extends Entity {
506
657
  * Preview the entity during creation
507
658
  */
508
659
  preview() {
509
- const selectedItems = DesignCore.Scene.selectionManager.selectedItems.slice(0);
510
- const shapes = this.processSelection(selectedItems);
660
+ const selectedEntities = DesignCore.Scene.selectionManager.selectedEntities.slice(0);
661
+ const shapes = this.processSelection(selectedEntities);
511
662
  if (shapes.length) {
512
663
  DesignCore.Scene.previewEntities.create(this.type, { points: this.points, childEntities: shapes }); // childEntities is passed as plain data here, not via getProperty
513
664
  }
@@ -515,31 +666,31 @@ export class Hatch extends Entity {
515
666
 
516
667
  /**
517
668
  * Convert the selection to boundary items
518
- * @param {Array} selectedItems
669
+ * @param {Array} selectedEntities
519
670
  * @return {Array} - Array of boundary items
520
671
  */
521
- processSelection(selectedItems) {
672
+ processSelection(selectedEntities) {
522
673
  const selectedchildEntities = [];
523
674
 
524
675
  let iterationPoints = [];
525
676
 
526
677
  // track the last index count to make sure the loop is closed
527
- let lastIndexCount = selectedItems.length + 1;
678
+ let lastIndexCount = selectedEntities.length + 1;
528
679
 
529
- while (selectedItems.length) {
530
- if (selectedItems.length === lastIndexCount) {
680
+ while (selectedEntities.length) {
681
+ if (selectedEntities.length === lastIndexCount) {
531
682
  return [];
532
683
  }
533
684
 
534
- lastIndexCount = selectedItems.length;
685
+ lastIndexCount = selectedEntities.length;
535
686
 
536
- for (let i = 0; i < selectedItems.length; i++) {
537
- // no points collected - get the first index from selected items
687
+ for (let i = 0; i < selectedEntities.length; i++) {
688
+ // no points collected - get the first index from selected entities
538
689
 
539
- const currentItem = selectedItems[i];
540
- // if the item can't be converted to polyline points, remove from selected items and go again
690
+ const currentItem = selectedEntities[i];
691
+ // if the item can't be converted to polyline points, remove from selected entities and go again
541
692
  if (typeof currentItem.toPolylinePoints === 'undefined' || currentItem.type === 'Text' || currentItem.type === 'ArcAlignedText') {
542
- selectedItems.splice(i, 1);
693
+ selectedEntities.splice(i, 1);
543
694
  break;
544
695
  }
545
696
 
@@ -575,8 +726,8 @@ export class Hatch extends Entity {
575
726
  iterationPoints.push(...currentPoints);
576
727
  }
577
728
 
578
- // remove the index from selected items
579
- selectedItems = selectedItems.filter((index) => index !== selectedItems[i]);
729
+ // remove the index from selected entities
730
+ selectedEntities = selectedEntities.filter((index) => index !== selectedEntities[i]);
580
731
 
581
732
  if (iterationPoints.at(0).isSame(iterationPoints.at(-1))) {
582
733
  const shape = new Polyline();
@@ -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).
@@ -635,7 +786,7 @@ export class Hatch extends Entity {
635
786
  }
636
787
 
637
788
  // Draw the boundary outline when hovered or selected.
638
- const isSelected = DesignCore.Scene.selectionManager.selectedItems.includes(this);
789
+ const isSelected = DesignCore.Scene.selectionManager.selectedEntities.includes(this);
639
790
  const isHovered = DesignCore.Scene.hoverEntities.indexOf(this) !== -1;
640
791
  if (isSelected || isHovered) {
641
792
  renderer.setDash([], 0);
@@ -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
- file.writeGroupCode('76', '1'); // Hatch pattern type: 0 = User-defined; 1 = Predefined; 2 = Custom
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 (this.getProperty(Property.Names.PATTERNNAME) !== 'SOLID') {
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', Patterns.getPatternLineCount(this.getProperty(Property.Names.PATTERNNAME))); // Number of pattern definition lines
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
- const pattern = Patterns.getPattern(this.getProperty(Property.Names.PATTERNNAME));
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');
@@ -774,7 +928,7 @@ export class Hatch extends Entity {
774
928
  const intersect = Intersection.intersectPolylinePolyline(polyline, line);
775
929
  const intersects = intersect.points.length;
776
930
  // P is inside shape if there is a odd number of intersects
777
- if (Math.abs(intersects % 2) == 1) {
931
+ if (Math.abs(intersects % 2) === 1) {
778
932
  return true;
779
933
  }
780
934
  }
@@ -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
@@ -850,11 +1020,11 @@ export class Hatch extends Entity {
850
1020
  const theta = ang - this.points[0].angle(this.points.at(-1));
851
1021
 
852
1022
  if (theta !== 0) {
853
- // set rotation center
854
- const center = this.points[0];
1023
+ // set rotation centre
1024
+ const centre = this.points[0];
855
1025
  // apply rotation to child entities points
856
1026
  hatchChildren.forEach((child) => {
857
- const rotatedPoints = child.points.map((p) => new Point(p.x, p.y, p.bulge, p.sequence).rotate(center, theta));
1027
+ const rotatedPoints = child.points.map((p) => new Point(p.x, p.y, p.bulge, p.sequence).rotate(centre, theta));
858
1028
  child.setProperty(Property.Names.POINTS, rotatedPoints);
859
1029
  });
860
1030
 
@@ -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
- this.block = block;
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
@@ -153,13 +157,13 @@ export class Insert extends Entity {
153
157
  /**
154
158
  * Draw the entity
155
159
  * @param {Object} renderer
156
- * @return {Array} block items for the canvas to render recursively
160
+ * @return {Array} block entities for the canvas to render recursively
157
161
  */
158
162
  draw(renderer) {
159
163
  // blocks are associated with an insert point.
160
- // Apply the insert location and rotation so block items draw correctly.
164
+ // Apply the insert location and rotation so block entities draw correctly.
161
165
  renderer.applyTransform({ x: this.points[0].x, y: this.points[0].y, rotation: Utils.degrees2radians(this.getRotation()) });
162
- return this.block.items;
166
+ return this.block.entities;
163
167
  }
164
168
 
165
169
  /**
@@ -204,9 +208,9 @@ export class Insert extends Entity {
204
208
 
205
209
  for (let snap = 0; snap < blockSnaps.length; snap++) {
206
210
  const sp = blockSnaps[snap];
207
- // offset the item snap point by the block insert location
211
+ // offset the entity snap point by the block insert location
208
212
  const rotatedPoint = sp.snapPoint.add(this.points[0]);
209
- // rotate the snap point to match the item positions
213
+ // rotate the snap point to match the entity positions
210
214
  const adjustedPoint = rotatedPoint.rotate(this.points[0], Utils.degrees2radians(this.getRotation()));
211
215
  snaps.push(new SnapPoint(adjustedPoint, sp.type));
212
216
  }
@@ -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);
@@ -298,16 +298,16 @@ export class Point {
298
298
  * Find the closest point on a arc between start and end points
299
299
  * @param {Point} startPoint
300
300
  * @param {Point} endPoint
301
- * @param {Point} centerPoint
301
+ * @param {Point} centrePoint
302
302
  * @param {number} direction - CCW if > 0
303
303
  * @return {Point} the closest point on the arc or null
304
304
  */
305
- closestPointOnArc(startPoint, endPoint, centerPoint, direction = 0) {
306
- const length = this.distance(centerPoint);
307
- const radius = centerPoint.distance(startPoint);
305
+ closestPointOnArc(startPoint, endPoint, centrePoint, direction = 0) {
306
+ const length = this.distance(centrePoint);
307
+ const radius = centrePoint.distance(startPoint);
308
308
 
309
- const Cx = centerPoint.x + radius * (this.x - centerPoint.x) / length;
310
- const Cy = centerPoint.y + radius * (this.y - centerPoint.y) / length;
309
+ const Cx = centrePoint.x + radius * (this.x - centrePoint.x) / length;
310
+ const Cy = centrePoint.y + radius * (this.y - centrePoint.y) / length;
311
311
  const closest = new Point(Cx, Cy);
312
312
 
313
313
  // circle
@@ -315,7 +315,7 @@ export class Point {
315
315
  return closest;
316
316
  }
317
317
 
318
- if (closest.isOnArc(startPoint, endPoint, centerPoint, direction)) {
318
+ if (closest.isOnArc(startPoint, endPoint, centrePoint, direction)) {
319
319
  return closest;
320
320
  }
321
321
 
@@ -327,15 +327,15 @@ export class Point {
327
327
  * Determine if point is on arc segment
328
328
  * @param {Point} startPoint
329
329
  * @param {Point} endPoint
330
- * @param {Point} centerPoint
330
+ * @param {Point} centrePoint
331
331
  * @param {number} direction - CCW if > 0
332
332
  * @return {Point} true or false
333
333
  */
334
- isOnArc(startPoint, endPoint, centerPoint, direction = 0) {
334
+ isOnArc(startPoint, endPoint, centrePoint, direction = 0) {
335
335
  // direction: ccw arc > 0, clockwise arc <= 0
336
- const snapAngle = centerPoint.angle(this);
337
- const startAngle = centerPoint.angle(startPoint);
338
- const endAngle = centerPoint.angle(endPoint);
336
+ const snapAngle = centrePoint.angle(this);
337
+ const startAngle = centrePoint.angle(startPoint);
338
+ const endAngle = centrePoint.angle(endPoint);
339
339
 
340
340
  if (direction > 0) {
341
341
  // Counter Clockwise Arc
@@ -438,7 +438,7 @@ export class Point {
438
438
  }
439
439
 
440
440
  /**
441
- * Returns apothem; the distance from arc center to cord midpoint
441
+ * Returns apothem; the distance from arc centre to cord midpoint
442
442
  * @param {Point} nextPoint
443
443
  * @return {number} apothem
444
444
  */
@@ -465,12 +465,12 @@ export class Point {
465
465
 
466
466
  let a = this.apothem(nextPoint);
467
467
 
468
- // check if the center point is inverted. i.e. at 180 it goes inside the arc
468
+ // check if the centre point is inverted. i.e. at 180 it goes inside the arc
469
469
  if (Math.abs(this.bulgeAngle()) > Math.PI) {
470
470
  a = -a;
471
471
  }
472
472
 
473
- // get the direction from the midpoint to the center point
473
+ // get the direction from the midpoint to the centre point
474
474
  const direction = this.angle(nextPoint) + (Math.PI / 2) * Math.sign(this.bulge);
475
475
  // centre point is the apothem distance from the mid point in the given direction
476
476
  const centre = midp.project(direction, a);
@@ -486,12 +486,12 @@ export class Point {
486
486
  * @return {number} - the partial bulge value
487
487
  */
488
488
  partialBulge(nextPoint, splitPoint, afterSplit = false) {
489
- const center = this.bulgeCentrePoint(nextPoint);
489
+ const centre = this.bulgeCentrePoint(nextPoint);
490
490
  const direction = this.bulge > 0 ? 1 : -1;
491
491
 
492
- const startAngle = center.angle(this);
493
- const endAngle = center.angle(nextPoint);
494
- const splitAngle = center.angle(splitPoint);
492
+ const startAngle = centre.angle(this);
493
+ const endAngle = centre.angle(nextPoint);
494
+ const splitAngle = centre.angle(splitPoint);
495
495
 
496
496
  let includedAngle;
497
497
  if (afterSplit) {
@@ -1,12 +1,12 @@
1
1
  import { DXFFile } from '../lib/dxf/dxfFile.js';
2
- import { BasePolyline } from './basePolyline.js';
2
+ import { PolylineBase } from './polylineBase.js';
3
3
  import { Point } from './point.js';
4
4
 
5
5
  /**
6
6
  * Polyline Entity Class
7
- * @extends BasePolyline
7
+ * @extends PolylineBase
8
8
  */
9
- export class Polyline extends BasePolyline {
9
+ export class Polyline extends PolylineBase {
10
10
  static type = 'Polyline';
11
11
 
12
12
  /**
@@ -20,8 +20,8 @@ import { RubberBand } from '../lib/auxiliary/rubberBand.js';
20
20
  * Base Polyline Entity Class
21
21
  * @extends Entity
22
22
  */
23
- export class BasePolyline extends Entity {
24
- static type = 'BasePolyline';
23
+ export class PolylineBase extends Entity {
24
+ static type = 'PolylineBase';
25
25
 
26
26
  /**
27
27
  * Create a Base Polyline
@@ -233,7 +233,7 @@ export class BasePolyline extends Entity {
233
233
  /**
234
234
  * Set entity points from a polyline point representation
235
235
  * @param {Array} points
236
- * @return {BasePolyline}
236
+ * @return {PolylineBase}
237
237
  */
238
238
  fromPolylinePoints(points) {
239
239
  const pts = points.map((p) => new Point(p.x, p.y, p.bulge));
@@ -311,10 +311,10 @@ export class BasePolyline extends Entity {
311
311
  let closestPoint;
312
312
  let candidateSegment;
313
313
  if (A.bulge !== 0) {
314
- const center = A.bulgeCentrePoint(B);
314
+ const centre = A.bulgeCentrePoint(B);
315
315
  const direction = A.bulge > 0 ? 1 : -1;
316
- closestPoint = P.closestPointOnArc(A, B, center, direction);
317
- candidateSegment = new Arc({ points: [center, A, B], direction });
316
+ closestPoint = P.closestPointOnArc(A, B, centre, direction);
317
+ candidateSegment = new Arc({ points: [centre, A, B], direction });
318
318
  } else {
319
319
  closestPoint = P.closestPointOnLine(A, B);
320
320
  candidateSegment = new Line({ points: [A, B] });
@@ -420,9 +420,9 @@ export class BasePolyline extends Entity {
420
420
  */
421
421
  isPointOnSegment(point, A, B) {
422
422
  if (A.bulge !== 0) {
423
- const center = A.bulgeCentrePoint(B);
423
+ const centre = A.bulgeCentrePoint(B);
424
424
  const direction = A.bulge > 0 ? 1 : -1;
425
- return point.isOnArc(A, B, center, direction);
425
+ return point.isOnArc(A, B, centre, direction);
426
426
  }
427
427
  return point.isOnLine(A, B);
428
428
  }
@@ -438,9 +438,9 @@ export class BasePolyline extends Entity {
438
438
  const B = this.points[segmentIndex % this.points.length];
439
439
 
440
440
  if (A.bulge !== 0) {
441
- const center = A.bulgeCentrePoint(B);
441
+ const centre = A.bulgeCentrePoint(B);
442
442
  const direction = A.bulge > 0 ? 1 : -1;
443
- const closest = point.closestPointOnArc(A, B, center, direction);
443
+ const closest = point.closestPointOnArc(A, B, centre, direction);
444
444
 
445
445
  if (closest) {
446
446
  return closest;
@@ -464,10 +464,10 @@ export class BasePolyline extends Entity {
464
464
  const B = this.points[segmentIndex % this.points.length];
465
465
 
466
466
  if (A.bulge !== 0) {
467
- const center = A.bulgeCentrePoint(B);
467
+ const centre = A.bulgeCentrePoint(B);
468
468
  const direction = A.bulge > 0 ? 1 : -1;
469
- const startAngle = center.angle(A);
470
- const pointAngle = center.angle(point);
469
+ const startAngle = centre.angle(A);
470
+ const pointAngle = centre.angle(point);
471
471
  return ((pointAngle - startAngle) * direction + 4 * Math.PI) % (2 * Math.PI);
472
472
  }
473
473
 
@@ -490,8 +490,8 @@ export class BasePolyline extends Entity {
490
490
  let boundingBox = BoundingBox.lineBoundingBox(this.points[i], nextPoint);
491
491
 
492
492
  if (this.points[i].bulge !== 0) {
493
- const centerPoint = this.points[i].bulgeCentrePoint(nextPoint);
494
- boundingBox = BoundingBox.arcBoundingBox(centerPoint, this.points[i], nextPoint, this.points[i].bulge);
493
+ const centrePoint = this.points[i].bulgeCentrePoint(nextPoint);
494
+ boundingBox = BoundingBox.arcBoundingBox(centrePoint, this.points[i], nextPoint, this.points[i].bulge);
495
495
  }
496
496
 
497
497
  xmin = Math.min(xmin, boundingBox.xMin);
@@ -519,9 +519,9 @@ export class BasePolyline extends Entity {
519
519
  // get the angle at the end of the previous segment
520
520
  let lastSegAngle;
521
521
  if (lastSegIsArc) {
522
- const centerPoint = this.points.at(-2).bulgeCentrePoint(this.points.at(-1));
523
- // angle is perpendicular to the center-to-end point ray, in the direction of the bulge
524
- lastSegAngle = centerPoint.angle(this.points.at(-1)) + (Math.PI / 2) * Math.sign(lastSegBulge);
522
+ const centrePoint = this.points.at(-2).bulgeCentrePoint(this.points.at(-1));
523
+ // angle is perpendicular to the centre-to-end point ray, in the direction of the bulge
524
+ lastSegAngle = centrePoint.angle(this.points.at(-1)) + (Math.PI / 2) * Math.sign(lastSegBulge);
525
525
  } else {
526
526
  // line segment angle is the angle from point 0 to point 1
527
527
  lastSegAngle = this.points.at(-2).angle(this.points.at(-1));