@design-core/core 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/.eslintrc.json +4 -0
  2. package/TODO.md +49 -6
  3. package/core/core/core.js +0 -3
  4. package/core/dimensions/alignedDimension.js +5 -4
  5. package/core/dimensions/angularDimension.js +6 -5
  6. package/core/dimensions/baseCircularDimension.js +3 -2
  7. package/core/dimensions/baseDimension.js +59 -93
  8. package/core/dimensions/baseLinearDimension.js +6 -5
  9. package/core/dimensions/diametricDimension.js +4 -3
  10. package/core/dimensions/dimension.js +21 -14
  11. package/core/dimensions/radialDimension.js +4 -3
  12. package/core/dimensions/rotatedDimension.js +12 -11
  13. package/core/entities/arc.js +112 -145
  14. package/core/entities/arctext.js +212 -174
  15. package/core/entities/basePolyline.js +33 -284
  16. package/core/entities/circle.js +77 -99
  17. package/core/entities/entity.js +114 -93
  18. package/core/entities/hatch.js +322 -249
  19. package/core/entities/insert.js +66 -81
  20. package/core/entities/line.js +21 -142
  21. package/core/entities/lwpolyline.js +16 -11
  22. package/core/entities/polyline.js +4 -4
  23. package/core/entities/rectangle.js +3 -1
  24. package/core/entities/solid.js +6 -15
  25. package/core/entities/text.js +188 -199
  26. package/core/lib/auxiliary/rubberBand.js +32 -0
  27. package/core/lib/auxiliary/selectionWindow.js +63 -0
  28. package/core/lib/auxiliary/snapPoint.js +113 -0
  29. package/core/lib/auxiliary/trackingLine.js +76 -0
  30. package/core/lib/canvas.js +228 -166
  31. package/core/lib/clipboard.js +7 -1
  32. package/core/lib/colour.js +16 -0
  33. package/core/lib/commandLine.js +1 -6
  34. package/core/lib/commandManager.js +18 -8
  35. package/core/lib/constants.js +28 -0
  36. package/core/lib/dxf/dxf.js +4 -3
  37. package/core/lib/entityManager.js +18 -11
  38. package/core/lib/inputManager.js +36 -13
  39. package/core/lib/intersect.js +25 -0
  40. package/core/lib/patterns.js +8 -1
  41. package/core/lib/plotOptions.js +67 -0
  42. package/core/lib/polylineUtils.js +75 -0
  43. package/core/lib/renderers/cairoRenderer.js +371 -0
  44. package/core/lib/renderers/canvasRenderer.js +315 -0
  45. package/core/lib/renderers/pdfRenderer.js +366 -0
  46. package/core/lib/renderers/rendererBase.js +284 -0
  47. package/core/lib/renderers/svgRenderer.js +343 -0
  48. package/core/lib/scene.js +27 -5
  49. package/core/lib/selectionManager.js +2 -2
  50. package/core/lib/settings.js +4 -2
  51. package/core/lib/snapping.js +4 -191
  52. package/core/lib/stateManager.js +30 -1
  53. package/core/lib/utils.js +21 -2
  54. package/core/properties/entityProperties.js +104 -0
  55. package/core/properties/property.js +106 -2
  56. package/core/properties/propertyManager.js +29 -13
  57. package/core/tables/block.js +4 -37
  58. package/core/tables/endblk.js +1 -1
  59. package/core/tables/layer.js +8 -0
  60. package/core/tables/styleManagerBase.js +3 -2
  61. package/core/tables/tableManagerBase.js +8 -3
  62. package/core/tools/chamfer.js +111 -137
  63. package/core/tools/chamferFilletBase.js +135 -34
  64. package/core/tools/copy.js +3 -3
  65. package/core/tools/explode.js +2 -2
  66. package/core/tools/extend.js +132 -19
  67. package/core/tools/fillet.js +86 -183
  68. package/core/tools/matchProp.js +2 -11
  69. package/core/tools/mirror.js +7 -6
  70. package/core/tools/move.js +3 -3
  71. package/core/tools/offset.js +3 -5
  72. package/core/tools/pasteclip.js +1 -1
  73. package/core/tools/rotate.js +6 -3
  74. package/core/tools/scale.js +1 -1
  75. package/core/tools/trim.js +265 -23
  76. package/core/tools/zoom.js +1 -1
  77. package/jest.config.js +6 -0
  78. package/package.json +5 -3
  79. package/test/dimensions/alignedDimension.test.js +14 -7
  80. package/test/dimensions/angularDimension.test.js +6 -6
  81. package/test/dimensions/baseDimension.test.js +69 -8
  82. package/test/dimensions/baseLinearDimension.test.js +62 -0
  83. package/test/dimensions/diametricDimension.test.js +6 -6
  84. package/test/dimensions/radialDimension.test.js +6 -6
  85. package/test/dimensions/rotatedDimension.test.js +14 -7
  86. package/test/entities/arc.test.js +80 -83
  87. package/test/entities/arctext.test.js +180 -53
  88. package/test/entities/basePolyline.test.js +129 -2
  89. package/test/entities/circle.test.js +57 -85
  90. package/test/entities/entity.test.js +42 -8
  91. package/test/entities/hatch.test.js +543 -70
  92. package/test/entities/insert.test.js +96 -1
  93. package/test/entities/line.test.js +15 -41
  94. package/test/entities/lwpolyline.test.js +104 -0
  95. package/test/entities/polyline.test.js +11 -0
  96. package/test/entities/rectangle.test.js +11 -0
  97. package/test/entities/solid.test.js +1 -1
  98. package/test/entities/text.test.js +294 -69
  99. package/test/integration/exportIntegration.reference.dxf +9 -9
  100. package/test/integration/exportIntegration.test.js +2 -2
  101. package/test/integration/intersectionIntegration.test.js +4 -5
  102. package/test/integration/pdfRendererIntegration.reference.pdf +17709 -0
  103. package/test/integration/pdfRendererIntegration.test.js +61 -0
  104. package/test/integration/svgRendererIntegration.reference.svg +3343 -0
  105. package/test/integration/svgRendererIntegration.test.js +61 -0
  106. package/test/integration/userInputIntegration.reference.dxf +1165 -105
  107. package/test/integration/userInputIntegration.test.js +239 -132
  108. package/test/lib/canvas.test.js +742 -123
  109. package/test/lib/constants.test.js +27 -0
  110. package/test/lib/entityManager.test.js +4 -4
  111. package/test/lib/inputManager.test.js +80 -19
  112. package/test/lib/intersect.test.js +83 -0
  113. package/test/lib/plotOptions.test.js +179 -0
  114. package/test/lib/rubberBand.test.js +84 -0
  115. package/test/lib/settings.test.js +5 -6
  116. package/test/lib/snapping.test.js +71 -129
  117. package/test/lib/stateManager.test.js +7 -7
  118. package/test/lib/utils.test.js +36 -2
  119. package/test/properties/entityProperties.test.js +132 -0
  120. package/test/properties/property.test.js +53 -0
  121. package/test/properties/propertyManager.test.js +3 -3
  122. package/test/renderers/canvasRenderer.test.js +317 -0
  123. package/test/renderers/pdfRenderer.test.js +271 -0
  124. package/test/renderers/rendererBase.test.js +213 -0
  125. package/test/renderers/svgRenderer.test.js +326 -0
  126. package/test/tables/block.test.js +31 -1
  127. package/test/tables/endblk.test.js +2 -2
  128. package/test/tables/layer.test.js +30 -0
  129. package/test/test-helpers/hatchGridRenderer.js +180 -0
  130. package/test/test-helpers/test-helpers.js +152 -0
  131. package/test/tools/chamfer.test.js +227 -5
  132. package/test/tools/chamferFilletBase.test.js +98 -9
  133. package/test/tools/copy.test.js +2 -2
  134. package/test/tools/explode.test.js +3 -3
  135. package/test/tools/extend.test.js +331 -0
  136. package/test/tools/fillet.test.js +296 -0
  137. package/test/tools/matchProp.test.js +5 -5
  138. package/test/tools/mirror.test.js +13 -13
  139. package/test/tools/offset.test.js +170 -3
  140. package/test/tools/pasteclip.test.js +5 -5
  141. package/test/tools/rotate.test.js +32 -2
  142. package/test/tools/scale.test.js +5 -5
  143. package/test/tools/trim.test.js +526 -0
  144. package/test/tools/zoom.test.js +7 -7
  145. 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,12 +303,12 @@ 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
314
  if (block.hasOwnProperty('items') === false) {
@@ -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);
@@ -97,8 +102,12 @@ export class EntityManager {
97
102
  const filteredItems = [];
98
103
 
99
104
  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);
105
+ if (type.toUpperCase() === 'ANY' || item.type.toUpperCase() === type.toUpperCase()) {
106
+ const inStore = item.properties?.has(prop);
107
+ const propValue = inStore ? item.getProperty(prop) : (Object.prototype.hasOwnProperty.call(item, prop) ? item[prop] : undefined);
108
+ if (propValue !== undefined && propValue === value) {
109
+ filteredItems.push(index);
110
+ }
102
111
  }
103
112
  });
104
113
 
@@ -125,7 +134,7 @@ export class EntityManager {
125
134
 
126
135
  for (let i = 0; i < this.#entities.length; i++) {
127
136
  // check the items layer is selectable - i.e. on, thawed, etc...
128
- const layer = DesignCore.LayerManager.getItemByName(this.#entities[i].layer);
137
+ const layer = DesignCore.LayerManager.getItemByName(this.#entities[i].getProperty(Property.Names.LAYER));
129
138
 
130
139
  if (!layer?.isSelectable) {
131
140
  continue;
@@ -155,9 +164,7 @@ export class EntityManager {
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
+ item.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
 
@@ -293,8 +317,7 @@ export class InputManager {
293
317
  * Handle single selection
294
318
  */
295
319
  singleSelect() {
296
- this.inputPoint = DesignCore.Mouse.pointOnScene();
297
- this.onLeftClick(this.inputPoint);
320
+ this.onLeftClick(DesignCore.Mouse.pointOnScene());
298
321
  }
299
322
 
300
323
  /**
@@ -407,10 +430,10 @@ export class InputManager {
407
430
  this.activeCommand.action();
408
431
  } else {
409
432
  if (item !== undefined) {
410
- // set the items layer to the current layer
411
- item.layer = DesignCore.LayerManager.getCstyle();
412
- // return the item index
413
- 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);
414
437
  }
415
438
  }
416
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
 
@@ -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': {
@@ -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 center = A.bulgeCentrePoint(B);
20
+ const radius = center.distance(A);
21
+ // Point must lie on the circle before checking the angular range.
22
+ if (Utils.round(center.distance(point)) !== Utils.round(radius)) return false;
23
+ const direction = A.bulge > 0 ? 1 : -1;
24
+ return point.isOnArc(A, B, center, 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 center = A.bulgeCentrePoint(B);
42
+ const direction = A.bulge > 0 ? 1 : -1;
43
+ const closest = point.closestPointOnArc(A, B, center, 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 center = A.bulgeCentrePoint(B);
67
+ const direction = A.bulge > 0 ? 1 : -1;
68
+ const startAngle = center.angle(A);
69
+ const pointAngle = center.angle(point);
70
+ return ((pointAngle - startAngle) * direction + 4 * Math.PI) % (2 * Math.PI);
71
+ }
72
+
73
+ return A.distance(point);
74
+ }
75
+ }