@design-core/core 0.0.6 → 50.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 (173) hide show
  1. package/.eslintrc.json +4 -0
  2. package/.github/workflows/DesignCoreQA.yml +3 -0
  3. package/TODO.md +49 -6
  4. package/core/core/core.js +0 -3
  5. package/core/designCore.js +1 -1
  6. package/core/dimensions/alignedDimension.js +12 -11
  7. package/core/dimensions/angularDimension.js +38 -37
  8. package/core/dimensions/{baseCircularDimension.js → circularDimensionBase.js} +22 -21
  9. package/core/dimensions/diametricDimension.js +14 -13
  10. package/core/dimensions/dimension.js +62 -55
  11. package/core/dimensions/{baseDimension.js → dimensionBase.js} +73 -107
  12. package/core/dimensions/{baseLinearDimension.js → linearDimensionBase.js} +15 -14
  13. package/core/dimensions/radialDimension.js +12 -11
  14. package/core/dimensions/rotatedDimension.js +20 -19
  15. package/core/entities/arc.js +123 -156
  16. package/core/entities/arctext.js +222 -184
  17. package/core/entities/circle.js +77 -99
  18. package/core/entities/entity.js +114 -93
  19. package/core/entities/hatch.js +346 -273
  20. package/core/entities/insert.js +68 -83
  21. package/core/entities/line.js +21 -142
  22. package/core/entities/lwpolyline.js +16 -11
  23. package/core/entities/point.js +19 -19
  24. package/core/entities/polyline.js +7 -7
  25. package/core/entities/{basePolyline.js → polylineBase.js} +47 -299
  26. package/core/entities/rectangle.js +3 -1
  27. package/core/entities/solid.js +6 -15
  28. package/core/entities/text.js +190 -201
  29. package/core/lib/auxiliary/rubberBand.js +32 -0
  30. package/core/lib/auxiliary/selectionWindow.js +63 -0
  31. package/core/lib/auxiliary/snapPoint.js +113 -0
  32. package/core/lib/auxiliary/trackingLine.js +76 -0
  33. package/core/lib/boundingBox.js +11 -11
  34. package/core/lib/canvas.js +232 -168
  35. package/core/lib/clipboard.js +9 -3
  36. package/core/lib/colour.js +16 -0
  37. package/core/lib/commandLine.js +1 -6
  38. package/core/lib/commandManager.js +18 -8
  39. package/core/lib/constants.js +28 -0
  40. package/core/lib/dxf/dxf.js +15 -14
  41. package/core/lib/dxf/dxfIterator.js +2 -2
  42. package/core/lib/entityManager.js +33 -26
  43. package/core/lib/inputManager.js +36 -12
  44. package/core/lib/intersect.js +25 -0
  45. package/core/lib/matrix.js +1 -1
  46. package/core/lib/patterns.js +9 -2
  47. package/core/lib/plotOptions.js +67 -0
  48. package/core/lib/polylineUtils.js +75 -0
  49. package/core/lib/renderers/cairoRenderer.js +371 -0
  50. package/core/lib/renderers/canvasRenderer.js +315 -0
  51. package/core/lib/renderers/pdfRenderer.js +366 -0
  52. package/core/lib/renderers/rendererBase.js +284 -0
  53. package/core/lib/renderers/svgRenderer.js +343 -0
  54. package/core/lib/scene.js +28 -6
  55. package/core/lib/selectionManager.js +20 -20
  56. package/core/lib/settings.js +4 -2
  57. package/core/lib/snapping.js +4 -191
  58. package/core/lib/stateManager.js +30 -1
  59. package/core/lib/utils.js +26 -7
  60. package/core/properties/entityProperties.js +104 -0
  61. package/core/properties/property.js +106 -2
  62. package/core/properties/propertyManager.js +60 -44
  63. package/core/tables/block.js +50 -81
  64. package/core/tables/dimStyle.js +5 -5
  65. package/core/tables/endblk.js +1 -1
  66. package/core/tables/layer.js +8 -0
  67. package/core/tables/ltypeManager.js +1 -1
  68. package/core/tables/styleManagerBase.js +3 -2
  69. package/core/tables/tableManagerBase.js +8 -3
  70. package/core/tables/vport.js +2 -2
  71. package/core/tools/chamfer.js +117 -143
  72. package/core/tools/chamferFilletBase.js +138 -37
  73. package/core/tools/copy.js +11 -11
  74. package/core/tools/copybase.js +3 -3
  75. package/core/tools/copyclip.js +3 -3
  76. package/core/tools/cornerEntity.js +2 -2
  77. package/core/tools/cutclip.js +3 -3
  78. package/core/tools/explode.js +7 -7
  79. package/core/tools/extend.js +138 -25
  80. package/core/tools/fillet.js +92 -189
  81. package/core/tools/matchProp.js +3 -12
  82. package/core/tools/mirror.js +15 -14
  83. package/core/tools/move.js +8 -8
  84. package/core/tools/offset.js +31 -33
  85. package/core/tools/pasteclip.js +1 -1
  86. package/core/tools/purge.js +10 -10
  87. package/core/tools/rotate.js +19 -16
  88. package/core/tools/scale.js +7 -7
  89. package/core/tools/trim.js +271 -29
  90. package/core/tools/zoom.js +1 -1
  91. package/cspell.json +321 -0
  92. package/jest.config.js +6 -0
  93. package/package.json +5 -3
  94. package/test/dimensions/alignedDimension.test.js +17 -10
  95. package/test/dimensions/angularDimension.test.js +13 -13
  96. package/test/dimensions/diametricDimension.test.js +15 -15
  97. package/test/dimensions/dimension.test.js +12 -12
  98. package/test/dimensions/{baseDimension.test.js → dimensionBase.test.js} +73 -12
  99. package/test/dimensions/linearDimensionBase.test.js +62 -0
  100. package/test/dimensions/radialDimension.test.js +15 -15
  101. package/test/dimensions/rotatedDimension.test.js +18 -11
  102. package/test/entities/arc.test.js +83 -86
  103. package/test/entities/arctext.test.js +184 -57
  104. package/test/entities/circle.test.js +68 -96
  105. package/test/entities/entity.test.js +42 -8
  106. package/test/entities/hatch.test.js +569 -96
  107. package/test/entities/insert.test.js +99 -4
  108. package/test/entities/line.test.js +15 -41
  109. package/test/entities/lwpolyline.test.js +104 -0
  110. package/test/entities/point.test.js +3 -3
  111. package/test/entities/polyline.test.js +11 -0
  112. package/test/entities/{basePolyline.test.js → polylineBase.test.js} +192 -57
  113. package/test/entities/rectangle.test.js +11 -0
  114. package/test/entities/solid.test.js +1 -1
  115. package/test/entities/text.test.js +302 -77
  116. package/test/integration/exportIntegration.reference.dxf +9 -9
  117. package/test/integration/exportIntegration.test.js +5 -5
  118. package/test/integration/intersectionIntegration.test.js +4 -5
  119. package/test/integration/pdfRendererIntegration.reference.pdf +17709 -0
  120. package/test/integration/pdfRendererIntegration.test.js +61 -0
  121. package/test/integration/svgRendererIntegration.reference.svg +3343 -0
  122. package/test/integration/svgRendererIntegration.test.js +61 -0
  123. package/test/integration/userInputIntegration.reference.dxf +1165 -105
  124. package/test/integration/userInputIntegration.test.js +239 -132
  125. package/test/lib/boundingBox.test.js +3 -3
  126. package/test/lib/canvas.test.js +744 -125
  127. package/test/lib/constants.test.js +27 -0
  128. package/test/lib/entityManager.test.js +4 -4
  129. package/test/lib/inputManager.test.js +81 -20
  130. package/test/lib/intersect.test.js +83 -0
  131. package/test/lib/plotOptions.test.js +179 -0
  132. package/test/lib/rubberBand.test.js +84 -0
  133. package/test/lib/scene.test.js +9 -9
  134. package/test/lib/selectionManager.test.js +3 -3
  135. package/test/lib/settings.test.js +5 -6
  136. package/test/lib/snapping.test.js +72 -130
  137. package/test/lib/stateManager.test.js +7 -7
  138. package/test/lib/utils.test.js +36 -2
  139. package/test/properties/entityProperties.test.js +132 -0
  140. package/test/properties/property.test.js +53 -0
  141. package/test/properties/propertyManager.test.js +25 -25
  142. package/test/renderers/canvasRenderer.test.js +317 -0
  143. package/test/renderers/pdfRenderer.test.js +271 -0
  144. package/test/renderers/rendererBase.test.js +213 -0
  145. package/test/renderers/svgRenderer.test.js +326 -0
  146. package/test/tables/block.test.js +92 -13
  147. package/test/tables/endblk.test.js +2 -2
  148. package/test/tables/layer.test.js +30 -0
  149. package/test/tables/layerManager.test.js +2 -2
  150. package/test/test-helpers/hatchGridRenderer.js +180 -0
  151. package/test/test-helpers/test-helpers.js +158 -6
  152. package/test/tools/chamfer.test.js +273 -51
  153. package/test/tools/chamferFilletBase.test.js +125 -36
  154. package/test/tools/copy.test.js +8 -8
  155. package/test/tools/copybase.test.js +2 -2
  156. package/test/tools/copyclip.test.js +2 -2
  157. package/test/tools/cornerEntity.test.js +8 -8
  158. package/test/tools/cutclip.test.js +2 -2
  159. package/test/tools/erase.test.js +6 -6
  160. package/test/tools/explode.test.js +5 -5
  161. package/test/tools/extend.test.js +373 -42
  162. package/test/tools/fillet.test.js +230 -49
  163. package/test/tools/matchProp.test.js +11 -11
  164. package/test/tools/mirror.test.js +39 -39
  165. package/test/tools/move.test.js +7 -7
  166. package/test/tools/offset.test.js +222 -55
  167. package/test/tools/pasteclip.test.js +5 -5
  168. package/test/tools/purge.test.js +4 -4
  169. package/test/tools/rotate.test.js +39 -9
  170. package/test/tools/scale.test.js +33 -33
  171. package/test/tools/trim.test.js +579 -53
  172. package/test/tools/zoom.test.js +12 -12
  173. package/core/lib/selectionWindow.js +0 -83
@@ -11,6 +11,10 @@ export class Constants {
11
11
  static Tolerance = {
12
12
  /** Near-zero threshold for floating-point geometric comparisons (1e-10). */
13
13
  EPSILON: 1e-10,
14
+ /** Small relative offset for geometric offsets (1e-9).
15
+ * Suitable where a nudge must be negligible compared to the scale of
16
+ * the geometry but larger than floating-point noise. */
17
+ DELTA: 1e-9,
14
18
  };
15
19
 
16
20
  /**
@@ -20,4 +24,28 @@ export class Constants {
20
24
  /** Number of decimal places used by Utils.round(). */
21
25
  DECIMALPLACES: 5,
22
26
  };
27
+
28
+ /**
29
+ * Standard paper sizes in PDF points (portrait orientation).
30
+ * 1 PDF point = 1/72 inch = 25.4/72 mm.
31
+ * Swap width and height for landscape.
32
+ *
33
+ * ISO sizes are defined by ISO 216: A4 = 210×297mm, A3 = 297×420mm, etc.
34
+ * ANSI sizes follow ANSI/ASME Y14.1.
35
+ */
36
+ static PageSizes = {
37
+ // ISO A-series (portrait)
38
+ 'A4': { width: 595, height: 842 },
39
+ 'A3': { width: 842, height: 1191 },
40
+ 'A2': { width: 1191, height: 1684 },
41
+ 'A1': { width: 1684, height: 2384 },
42
+ 'A0': { width: 2384, height: 3370 },
43
+ // ANSI sizes (portrait)
44
+ 'Letter': { width: 612, height: 792 },
45
+ 'Legal': { width: 612, height: 1008 },
46
+ 'Tabloid': { width: 792, height: 1224 },
47
+ 'ANSI C': { width: 1224, height: 1584 },
48
+ 'ANSI D': { width: 1584, height: 2448 },
49
+ 'ANSI E': { width: 2448, height: 3168 },
50
+ };
23
51
  }
@@ -30,6 +30,7 @@ export class DXF {
30
30
  this.reader.read(data);
31
31
  }
32
32
 
33
+
33
34
  /**
34
35
  * Write
35
36
  * @param {string} version
@@ -302,22 +303,22 @@ export class DXF {
302
303
  const command = child[0];
303
304
  // check if the child is a valid entity
304
305
  if (DesignCore.CommandManager.isCommand(command)) {
305
- // create an instance of the child entity
306
306
  const item = DesignCore.CommandManager.createNew(command, child);
307
307
 
308
308
  // Register the child entity handle
309
- if (item.handle) {
310
- DesignCore.HandleManager.checkHandle(item.handle);
309
+ const itemHandle = item?.getProperty?.('handle') ?? item?.handle;
310
+ if (itemHandle) {
311
+ DesignCore.HandleManager.checkHandle(itemHandle);
311
312
  }
312
313
 
313
- if (block.hasOwnProperty('items') === false) {
314
- block.items = [];
314
+ if (block.hasOwnProperty('entities') === false) {
315
+ block.entities = [];
315
316
  }
316
317
 
317
318
  // add the child to the block items
318
319
  // TODO: create a block instance and add the items.
319
320
  // check block is valid before adding to scene
320
- block.items.push(item);
321
+ block.entities.push(item);
321
322
  }
322
323
  });
323
324
  }
@@ -346,7 +347,7 @@ export class DXF {
346
347
  entity.points = this.parsePoints(entity.points);
347
348
  }
348
349
 
349
- this.addItem(entity);
350
+ this.addEntity(entity);
350
351
  });
351
352
 
352
353
  // ensure all layers, ltypes, styles, dimstyles exist
@@ -377,18 +378,18 @@ export class DXF {
377
378
  }
378
379
 
379
380
  /**
380
- * Add Item
381
- * @param {Object} item
381
+ * Add Entity
382
+ * @param {Object} entity
382
383
  */
383
- addItem(item) {
384
- if (item.hasOwnProperty('0') === false) {
384
+ addEntity(entity) {
385
+ if (entity.hasOwnProperty('0') === false) {
385
386
  return;
386
387
  }
387
388
 
388
- const command = item[0];
389
+ const command = entity[0];
389
390
  if (DesignCore.CommandManager.isCommand(command)) {
390
- const entity = DesignCore.CommandManager.createNew(command, item);
391
- DesignCore.Scene.entities.add(entity);
391
+ const newEntity = DesignCore.CommandManager.createNew(command, entity);
392
+ DesignCore.Scene.entities.add(newEntity);
392
393
  } else {
393
394
  Logging.instance.warn(`${Strings.Message.UNKNOWNCOMMAND} ${command}`);
394
395
  this.unsupportedElements = true;
@@ -23,11 +23,11 @@ Example DXF Entity Pairs:
23
23
  value: 100.0 - y position
24
24
  */
25
25
 
26
- /** DXF Interator Class */
26
+ /** DXF Iterator Class */
27
27
  export class DxfIterator {
28
28
  static _instance;
29
29
 
30
- /** Create DXFInterator */
30
+ /** Create DXFIterator */
31
31
  constructor() {
32
32
  this.currentIndex = 0;
33
33
  this.lines = [];
@@ -1,5 +1,6 @@
1
1
 
2
2
  import { DesignCore } from '../designCore.js';
3
+ import { Property } from '../properties/property.js';
3
4
 
4
5
  /**
5
6
  * Entity Manager Class
@@ -36,10 +37,13 @@ export class EntityManager {
36
37
  if (!entity) return;
37
38
 
38
39
  if (this.#trackHandles) {
39
- if (entity.handle === undefined) {
40
- entity.handle = DesignCore.HandleManager.next();
40
+ const existingHandle = entity.getProperty?.(Property.Names.HANDLE) ?? entity.handle;
41
+ if (existingHandle === undefined) {
42
+ if (entity.setProperty) {
43
+ entity.setProperty(Property.Names.HANDLE, DesignCore.HandleManager.next());
44
+ }
41
45
  } else {
42
- DesignCore.HandleManager.checkHandle(entity.handle);
46
+ DesignCore.HandleManager.checkHandle(existingHandle);
43
47
  }
44
48
  }
45
49
 
@@ -53,8 +57,9 @@ export class EntityManager {
53
57
  remove(index) {
54
58
  if (this.#trackHandles) {
55
59
  const entity = this.#entities[index];
56
- if (entity?.handle !== undefined) {
57
- DesignCore.HandleManager.releaseHandle(entity.handle);
60
+ const handle = entity?.getProperty?.(Property.Names.HANDLE) ?? entity?.handle;
61
+ if (handle !== undefined) {
62
+ DesignCore.HandleManager.releaseHandle(handle);
58
63
  }
59
64
  }
60
65
  this.#entities.splice(index, 1);
@@ -87,22 +92,26 @@ export class EntityManager {
87
92
  }
88
93
 
89
94
  /**
90
- * Find items in scene
95
+ * Find entities in scene
91
96
  * @param {string} type - entity type or "ANY"
92
97
  * @param {string} prop - object of entity parameters
93
98
  * @param {any} value - value of the property
94
- * @return {number} - index of items
99
+ * @return {number} - index of entities
95
100
  */
96
101
  find(type, prop, value) {
97
- const filteredItems = [];
98
-
99
- this.#entities.forEach((item, index) => {
100
- if ((type.toUpperCase() === 'ANY' || item.type.toUpperCase() === type.toUpperCase()) && item.hasOwnProperty(prop) && item[prop] === value) {
101
- filteredItems.push(index);
102
+ const filteredEntities = [];
103
+
104
+ this.#entities.forEach((entity, index) => {
105
+ if (type.toUpperCase() === 'ANY' || entity.type.toUpperCase() === type.toUpperCase()) {
106
+ const inStore = entity.properties?.has(prop);
107
+ const propValue = inStore ? entity.getProperty(prop) : (Object.prototype.hasOwnProperty.call(entity, prop) ? entity[prop] : undefined);
108
+ if (propValue !== undefined && propValue === value) {
109
+ filteredEntities.push(index);
110
+ }
102
111
  }
103
112
  });
104
113
 
105
- return filteredItems;
114
+ return filteredEntities;
106
115
  }
107
116
 
108
117
  /** Get index of entity
@@ -115,17 +124,17 @@ export class EntityManager {
115
124
 
116
125
 
117
126
  /**
118
- * Find closest item to point
127
+ * Find closest entity to point
119
128
  * @param {Point} point
120
- * @return {number} - return index of closest item or undefined
129
+ * @return {number} - return index of closest entity or undefined
121
130
  */
122
131
  findClosest(point) {
123
132
  let delta = 1.65 / DesignCore.Core.canvas.getScale(); // find a more suitable starting value
124
- let closestItemIndex;
133
+ let closestEntityIndex;
125
134
 
126
135
  for (let i = 0; i < this.#entities.length; i++) {
127
- // check the items layer is selectable - i.e. on, thawed, etc...
128
- const layer = DesignCore.LayerManager.getItemByName(this.#entities[i].layer);
136
+ // check the entities layer is selectable - i.e. on, thawed, etc...
137
+ const layer = DesignCore.LayerManager.getItemByName(this.#entities[i].getProperty(Property.Names.LAYER));
129
138
 
130
139
  if (!layer?.isSelectable) {
131
140
  continue;
@@ -135,11 +144,11 @@ export class EntityManager {
135
144
 
136
145
  if (distance < delta) {
137
146
  delta = distance;
138
- closestItemIndex = i;
147
+ closestEntityIndex = i;
139
148
  }
140
149
  }
141
150
 
142
- return closestItemIndex;
151
+ return closestEntityIndex;
143
152
  }
144
153
 
145
154
  /**
@@ -148,16 +157,14 @@ export class EntityManager {
148
157
  * @param {object} data
149
158
  */
150
159
  update(index, data) {
151
- const item = this.get(index);
160
+ const entity = this.get(index);
152
161
 
153
- if (!item) {
154
- throw Error('Item not found in scene');
162
+ if (!entity) {
163
+ throw Error('Entity not found in scene');
155
164
  }
156
165
 
157
166
  for (const property of Object.keys(data)) {
158
- if (item.hasOwnProperty(property)) {
159
- item.setProperty(property, data[property]);
160
- }
167
+ entity.setProperty(property, data[property]);
161
168
  }
162
169
  }
163
170
 
@@ -1,8 +1,8 @@
1
1
  import { Tool } from '../tools/tool.js';
2
2
  import { Snapping } from './snapping.js';
3
- import { Utils } from './utils.js';
4
3
 
5
4
  import { DesignCore } from '../designCore.js';
5
+ import { Property } from '../properties/property.js';
6
6
  import { Point } from '../entities/point.js';
7
7
  import { Input, MouseDown, MouseUp } from './input.js';
8
8
 
@@ -137,6 +137,14 @@ export class InputManager {
137
137
  const resolvedCommand = DesignCore.CommandManager.getCommand(command);
138
138
  DesignCore.CommandLine.addToCommandHistory(resolvedCommand);
139
139
  this.activeCommand = DesignCore.CommandManager.createNew(resolvedCommand);
140
+
141
+ // Clear selection for Entity commands
142
+ // Tools (Erase, Offset, etc.) consume preselection in execute().
143
+ if (!(this.activeCommand instanceof Tool)) {
144
+ DesignCore.Scene.selectionManager.reset();
145
+ DesignCore.Canvas.requestPaint();
146
+ }
147
+
140
148
  this.activeCommand.execute();
141
149
  }
142
150
 
@@ -217,11 +225,24 @@ export class InputManager {
217
225
  }
218
226
  }
219
227
 
228
+ /**
229
+ * Clears preview entities and re-runs the active command preview
230
+ */
231
+ refreshPreview() {
232
+ if (this.activeCommand === undefined) return;
233
+ DesignCore.Scene.previewEntities.clear();
234
+ DesignCore.Scene.auxiliaryEntities.clear();
235
+ DesignCore.Scene.hoverEntities.clear();
236
+ this.activeCommand.preview();
237
+ DesignCore.Canvas.requestPaint();
238
+ }
239
+
220
240
  /**
221
241
  * Handle mouse position changes
222
242
  */
223
243
  mouseMoved() {
224
- DesignCore.Scene.tempEntities.clear();
244
+ DesignCore.Scene.previewEntities.clear();
245
+ DesignCore.Scene.hoverEntities.clear();
225
246
  DesignCore.Scene.auxiliaryEntities.clear();
226
247
 
227
248
  if (DesignCore.Mouse.buttonOneDown) {
@@ -255,13 +276,16 @@ export class InputManager {
255
276
  }
256
277
  }
257
278
 
258
- const selecting = !snapped && this.highlightEntityUnderMouse();
259
-
260
- // preview active commands when items are not being selected
261
- if (this.activeCommand !== undefined && !selecting) {
279
+ // preview active commands
280
+ if (this.activeCommand !== undefined) {
262
281
  this.activeCommand.preview();
263
282
  }
264
283
 
284
+ // Only show hover glow when no command preview is occupying previewEntities
285
+ if (!snapped && DesignCore.Scene.previewEntities.count() === 0) {
286
+ this.highlightEntityUnderMouse();
287
+ }
288
+
265
289
  DesignCore.Canvas.requestPaint();
266
290
  }
267
291
 
@@ -282,7 +306,7 @@ export class InputManager {
282
306
 
283
307
  const index = DesignCore.Scene.selectionManager.findClosestItem(DesignCore.Mouse.pointOnScene());
284
308
  if (index !== undefined) {
285
- DesignCore.Scene.tempEntities.add(Utils.cloneObject(DesignCore.Scene.entities.get(index)));
309
+ DesignCore.Scene.hoverEntities.add(DesignCore.Scene.entities.get(index));
286
310
  return true;
287
311
  }
288
312
 
@@ -368,7 +392,7 @@ export class InputManager {
368
392
  }
369
393
 
370
394
  if (Input.getType(selection) === Input.Type.SINGLESELECTION) {
371
- DesignCore.Scene.selectionManager.addToSelectionSet(selection.selectedItemIndex);
395
+ DesignCore.Scene.selectionManager.addToSelectionSet(selection.selectedEntityIndex);
372
396
  }
373
397
  }
374
398
 
@@ -406,10 +430,10 @@ export class InputManager {
406
430
  this.activeCommand.action();
407
431
  } else {
408
432
  if (item !== undefined) {
409
- // set the items layer to the current layer
410
- item.layer = DesignCore.LayerManager.getCstyle();
411
- // return the item index
412
- return DesignCore.Scene.addItem(item.type, item, index);
433
+ // set the items layer to the current layer
434
+ item.setProperty(Property.Names.LAYER, DesignCore.LayerManager.getCstyle());
435
+ // commit the already-constructed entity directly, bypassing createNew
436
+ return DesignCore.Scene.commitEntity(item, index);
413
437
  }
414
438
  }
415
439
  }
@@ -448,5 +448,30 @@ export class Intersection {
448
448
 
449
449
  return result;
450
450
  };
451
+
452
+ /**
453
+ * Even-odd point-in-boundary test using a horizontal ray to the right.
454
+ * A tiny y-offset prevents the ray from passing exactly through the
455
+ * top or bottom of a curved boundary (e.g. a circle), which would produce a
456
+ * wrong crossing count. The offset scales with the y-coordinate of the test
457
+ * point so it stays negligible regardless of drawing scale or position.
458
+ * @param {number} px - x coordinate of the test point
459
+ * @param {number} py - y coordinate of the test point
460
+ * @param {Array} boundaries - array of closed point arrays (each a polyline loop)
461
+ * @param {number} boundaryExtentX - x coordinate of the ray endpoint, must be rightward of all geometry
462
+ * @return {boolean} true if the point is inside the boundaries under the even-odd rule
463
+ */
464
+ static isInsidePolyline(px, py, boundaries, boundaryExtentX) {
465
+ // Scale epsilon by the y-coordinate magnitude of the test point rather than
466
+ // boundaryExtentX: a y-offset should be negligible relative to the y-scale of
467
+ // the geometry, not the (unrelated) x-extent of the ray. The +1 handles py=0.
468
+ const rayEps = Constants.Tolerance.DELTA * (Math.abs(py) + 1);
469
+ const testLine = [new Point(px, py + rayEps), new Point(boundaryExtentX, py + rayEps)];
470
+ let count = 0;
471
+ for (const b of boundaries) {
472
+ count += Intersection.intersectPolylinePolyline(b, testLine).points.length;
473
+ }
474
+ return (count % 2) === 1;
475
+ }
451
476
  }
452
477
 
@@ -37,7 +37,7 @@ export class Matrix {
37
37
  };
38
38
 
39
39
  /**
40
- * Apply tranlation to matrix
40
+ * Apply translation to matrix
41
41
  * @param {number} x - translation in x
42
42
  * @param {number} y - translation in y
43
43
  */
@@ -116,8 +116,15 @@ export class Patterns {
116
116
 
117
117
  static hatch_patterns = {
118
118
  'SOLID': {
119
+ // Commercial CAD defines SOLID as two dense cross-hatch line families
120
+ // (45° and -45°) with a near-zero spacing of 0.0001 inches. Design does
121
+ // not use this pattern for rendering — solid hatches are drawn with a
122
+ // direct fill() call for quality and performance reasons. The definition
123
+ // is retained here so that patternExists('SOLID') returns true and the
124
+ // name is available for pattern selection and DXF round-trip validation.
119
125
  description: 'Solid Fill', pattern:
120
- `45, 0,0, 0,3.175`,
126
+ `45, 0,0, 0,0.00254
127
+ -45, 0,0, 0,0.00254`,
121
128
  },
122
129
 
123
130
  'ANGLE': {
@@ -455,7 +462,7 @@ export class Patterns {
455
462
  60, 0, 0, 4.7625, 2.749630645, -6.35, 3.175` },
456
463
 
457
464
  'HOUND': {
458
- description: 'Houndstooth check', pattern:
465
+ description: 'Hounds tooth check', pattern:
459
466
  `0, 0, 0, 6.35, 1.5875, 25.4, -12.7
460
467
  90, 0, 0, 6.35, 1.5875, 25.4, -12.7` },
461
468
 
@@ -0,0 +1,67 @@
1
+ import { RendererBase } from './renderers/rendererBase.js';
2
+
3
+ /**
4
+ * PlotOptions Class
5
+ * Encapsulates all parameters for a plot/export operation.
6
+ */
7
+ export class PlotOptions {
8
+ /** Valid plot area modes. */
9
+ static Area = {
10
+ EXTENTS: 'Extents',
11
+ DISPLAY: 'Display',
12
+ };
13
+
14
+ static #fileTypes = ['pdf', 'svg'];
15
+
16
+ /**
17
+ * @param {number} pageWidth - required, positive number (PDF points)
18
+ * @param {number} pageHeight - required, positive number (PDF points)
19
+ */
20
+ constructor(pageWidth, pageHeight) {
21
+ this.setOption('pageWidth', pageWidth);
22
+ this.setOption('pageHeight', pageHeight);
23
+ this.plotScale = null;
24
+ this.plotArea = PlotOptions.Area.EXTENTS;
25
+ this.style = RendererBase.Styles.NONE;
26
+ this.fileType = 'pdf';
27
+ }
28
+
29
+ /**
30
+ * Validate and set a single option.
31
+ * @param {string} key
32
+ * @param {*} value
33
+ */
34
+ setOption(key, value) {
35
+ switch (key) {
36
+ case 'pageWidth':
37
+ case 'pageHeight':
38
+ if (typeof value !== 'number' || value <= 0) {
39
+ throw new Error(`PlotOptions: ${key} must be a positive number`);
40
+ }
41
+ break;
42
+ case 'plotScale':
43
+ if (value !== null && (typeof value !== 'number' || value <= 0)) {
44
+ throw new Error('PlotOptions: plotScale must be a positive number or null');
45
+ }
46
+ break;
47
+ case 'plotArea':
48
+ if (!Object.values(PlotOptions.Area).includes(value)) {
49
+ throw new Error(`PlotOptions: plotArea must be one of: ${Object.values(PlotOptions.Area).join(', ')}`);
50
+ }
51
+ break;
52
+ case 'style':
53
+ if (typeof value !== 'function') {
54
+ throw new Error('PlotOptions: style must be a function');
55
+ }
56
+ break;
57
+ case 'fileType':
58
+ if (!PlotOptions.#fileTypes.includes(value)) {
59
+ throw new Error(`PlotOptions: fileType must be one of: ${PlotOptions.#fileTypes.join(', ')}`);
60
+ }
61
+ break;
62
+ default:
63
+ throw new Error(`PlotOptions: unknown option '${key}'`);
64
+ }
65
+ this[key] = value;
66
+ }
67
+ }
@@ -0,0 +1,75 @@
1
+ import { Utils } from './utils.js';
2
+
3
+ /**
4
+ * Shared polyline-segment geometry helpers used by the Trim and Extend tools.
5
+ * All methods accept an explicit `points` array rather than operating on
6
+ * `this.points`, so they are entity-independent and can be reused by any
7
+ * tool that works in polyline-point space.
8
+ */
9
+ export class PolylineUtils {
10
+ /**
11
+ * Test whether a point lies on the segment from A to B (line or arc).
12
+ * @param {Point} point
13
+ * @param {Point} A - segment start; carries bulge for arc segments
14
+ * @param {Point} B - segment end
15
+ * @return {boolean}
16
+ */
17
+ static isPointOnSegment(point, A, B) {
18
+ if (A.bulge !== 0 && A.bulge !== undefined) {
19
+ const centre = A.bulgeCentrePoint(B);
20
+ const radius = centre.distance(A);
21
+ // Point must lie on the circle before checking the angular range.
22
+ if (Utils.round(centre.distance(point)) !== Utils.round(radius)) return false;
23
+ const direction = A.bulge > 0 ? 1 : -1;
24
+ return point.isOnArc(A, B, centre, direction);
25
+ }
26
+ return point.isOnLine(A, B);
27
+ }
28
+
29
+ /**
30
+ * Get the closest point on a segment to a reference point.
31
+ * @param {Array} points - polyline points array
32
+ * @param {Point} point - reference point
33
+ * @param {number} segmentIndex - 1-based segment index
34
+ * @return {Point|null}
35
+ */
36
+ static closestPointOnSegment(points, point, segmentIndex) {
37
+ const A = points[segmentIndex - 1];
38
+ const B = points[segmentIndex % points.length];
39
+
40
+ if (A.bulge !== 0 && A.bulge !== undefined) {
41
+ const centre = A.bulgeCentrePoint(B);
42
+ const direction = A.bulge > 0 ? 1 : -1;
43
+ const closest = point.closestPointOnArc(A, B, centre, direction);
44
+ if (closest) return closest;
45
+ // Point not on arc — return the nearer endpoint
46
+ return point.distance(A) < point.distance(B) ? A : B;
47
+ }
48
+
49
+ return point.closestPointOnLine(A, B);
50
+ }
51
+
52
+ /**
53
+ * Get the normalised position of a point along a segment (used for ordering).
54
+ * For line segments returns chord distance from A; for arc segments returns
55
+ * the angular sweep from A, normalised to [0, 2π).
56
+ * @param {Array} points - polyline points array
57
+ * @param {Point} point - a point known to lie on the segment
58
+ * @param {number} segmentIndex - 1-based segment index
59
+ * @return {number}
60
+ */
61
+ static positionOnSegment(points, point, segmentIndex) {
62
+ const A = points[segmentIndex - 1];
63
+ const B = points[segmentIndex % points.length];
64
+
65
+ if (A.bulge !== 0 && A.bulge !== undefined) {
66
+ const centre = A.bulgeCentrePoint(B);
67
+ const direction = A.bulge > 0 ? 1 : -1;
68
+ const startAngle = centre.angle(A);
69
+ const pointAngle = centre.angle(point);
70
+ return ((pointAngle - startAngle) * direction + 4 * Math.PI) % (2 * Math.PI);
71
+ }
72
+
73
+ return A.distance(point);
74
+ }
75
+ }