@design-core/core 0.0.1 → 0.0.3

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 (254) hide show
  1. package/.eslintrc.json +14 -7
  2. package/.github/workflows/DesignCoreQA.yml +3 -1
  3. package/README.md +11 -4
  4. package/TODO.md +19 -0
  5. package/core/core/core.js +49 -42
  6. package/core/designCore.js +61 -2
  7. package/core/dimensions/alignedDimension.js +142 -0
  8. package/core/dimensions/angularDimension.js +550 -0
  9. package/core/dimensions/baseCircularDimension.js +244 -0
  10. package/core/dimensions/baseDimension.js +558 -0
  11. package/core/dimensions/baseLinearDimension.js +371 -0
  12. package/core/dimensions/diametricDimension.js +82 -0
  13. package/core/dimensions/dimension.js +298 -0
  14. package/core/dimensions/linearDimension.js +29 -0
  15. package/core/dimensions/radialDimension.js +78 -0
  16. package/core/dimensions/rotatedDimension.js +167 -0
  17. package/core/entities/arc.js +314 -60
  18. package/core/entities/arctext.js +642 -0
  19. package/core/entities/basePolyline.js +553 -94
  20. package/core/entities/circle.js +181 -54
  21. package/core/entities/entity.js +154 -74
  22. package/core/entities/hatch.js +807 -0
  23. package/core/entities/insert.js +200 -39
  24. package/core/entities/line.js +196 -134
  25. package/core/entities/lwpolyline.js +21 -44
  26. package/core/entities/point.js +194 -29
  27. package/core/entities/polyline.js +54 -7
  28. package/core/entities/rectangle.js +43 -13
  29. package/core/entities/solid.js +43 -6
  30. package/core/entities/text.js +324 -181
  31. package/core/lib/boundingBox.js +73 -13
  32. package/core/lib/canvas.js +256 -65
  33. package/core/lib/clipboard.js +198 -0
  34. package/core/lib/colour.js +144 -0
  35. package/core/lib/colours.js +367 -301
  36. package/core/lib/commandLine.js +16 -28
  37. package/core/lib/commandManager.js +115 -58
  38. package/core/lib/constants.js +23 -0
  39. package/core/lib/dxf/dxf.js +249 -34
  40. package/core/lib/dxf/dxfFile.js +23 -38
  41. package/core/lib/dxf/dxfIterator.js +17 -5
  42. package/core/lib/dxf/dxfRead.js +23 -7
  43. package/core/lib/dxf/dxfWrite.js +39 -129
  44. package/core/lib/dxf/handleManager.js +97 -0
  45. package/core/lib/dxf/sections/blocks.js +16 -2
  46. package/core/lib/dxf/sections/entities.js +17 -3
  47. package/core/lib/dxf/sections/header.js +10 -1
  48. package/core/lib/dxf/sections/objects.js +36 -0
  49. package/core/lib/dxf/sections/section.js +43 -5
  50. package/core/lib/dxf/sections/tables.js +17 -3
  51. package/core/lib/entityManager.js +178 -0
  52. package/core/lib/fileio.js +14 -4
  53. package/core/lib/headers.js +229 -0
  54. package/core/lib/input.js +46 -0
  55. package/core/lib/inputManager.js +199 -194
  56. package/core/lib/intersect.js +341 -468
  57. package/core/lib/logging.js +25 -1
  58. package/core/lib/matrix.js +6 -8
  59. package/core/lib/mouse.js +24 -79
  60. package/core/lib/patterns.js +690 -0
  61. package/core/lib/promptOptions.js +133 -0
  62. package/core/lib/scene.js +71 -174
  63. package/core/lib/selectionManager.js +74 -48
  64. package/core/lib/selectionWindow.js +24 -10
  65. package/core/lib/settings.js +18 -11
  66. package/core/lib/snapping.js +129 -21
  67. package/core/lib/stateManager.js +288 -0
  68. package/core/lib/strings.js +57 -5
  69. package/core/lib/utils.js +95 -11
  70. package/core/objects/dictionary.js +43 -0
  71. package/core/objects/dictionaryManager.js +80 -0
  72. package/core/properties/dimType.js +117 -0
  73. package/core/properties/flags.js +66 -0
  74. package/core/properties/property.js +26 -0
  75. package/core/properties/propertyManager.js +65 -19
  76. package/core/tables/appId.js +31 -0
  77. package/core/tables/appIdManager.js +44 -0
  78. package/core/tables/block.js +323 -0
  79. package/core/tables/blockManager.js +48 -0
  80. package/core/tables/blockRecord.js +29 -0
  81. package/core/tables/blockRecordManager.js +37 -0
  82. package/core/tables/dimStyle.js +627 -0
  83. package/core/tables/dimStyleManager.js +187 -0
  84. package/core/tables/endblk.js +28 -0
  85. package/core/tables/layer.js +219 -0
  86. package/core/tables/layerManager.js +47 -0
  87. package/core/tables/ltype.js +85 -0
  88. package/core/tables/ltypeManager.js +65 -0
  89. package/core/tables/style.js +145 -0
  90. package/core/tables/styleManager.js +55 -0
  91. package/core/tables/styleManagerBase.js +110 -0
  92. package/core/tables/tableManagerBase.js +354 -0
  93. package/core/tables/ucs.js +28 -0
  94. package/core/tables/ucsManager.js +44 -0
  95. package/core/tables/view.js +28 -0
  96. package/core/tables/viewManager.js +44 -0
  97. package/core/tables/vport.js +70 -0
  98. package/core/tables/vportManager.js +65 -0
  99. package/core/tools/chamfer.js +343 -0
  100. package/core/tools/chamferFilletBase.js +162 -0
  101. package/core/tools/copy.js +68 -40
  102. package/core/tools/copybase.js +65 -0
  103. package/core/tools/copyclip.js +59 -0
  104. package/core/tools/cornerEntity.js +138 -0
  105. package/core/tools/cutclip.js +72 -0
  106. package/core/tools/distance.js +37 -9
  107. package/core/tools/erase.js +43 -13
  108. package/core/tools/explode.js +117 -0
  109. package/core/tools/extend.js +71 -32
  110. package/core/tools/fillet.js +336 -0
  111. package/core/tools/identify.js +33 -6
  112. package/core/tools/matchProp.js +106 -0
  113. package/core/tools/mirror.js +141 -0
  114. package/core/tools/move.js +65 -32
  115. package/core/tools/offset.js +321 -0
  116. package/core/tools/pan.js +54 -0
  117. package/core/tools/pasteclip.js +93 -0
  118. package/core/tools/purge.js +90 -0
  119. package/core/tools/rotate.js +105 -70
  120. package/core/tools/scale.js +166 -0
  121. package/core/tools/tool.js +14 -4
  122. package/core/tools/trim.js +71 -32
  123. package/core/tools/zoom.js +152 -0
  124. package/package.json +3 -305
  125. package/test/dimensions/alignedDimension.test.js +211 -0
  126. package/test/dimensions/angularDimension.test.js +243 -0
  127. package/test/dimensions/baseDimension.test.js +239 -0
  128. package/test/dimensions/diametricDimension.test.js +203 -0
  129. package/test/dimensions/dimension.test.js +102 -0
  130. package/test/dimensions/radialDimension.test.js +203 -0
  131. package/test/dimensions/rotatedDimension.test.js +224 -0
  132. package/test/entities/arc.test.js +463 -41
  133. package/test/entities/arctext.test.js +329 -0
  134. package/test/entities/basePolyline.test.js +459 -10
  135. package/test/entities/circle.test.js +312 -7
  136. package/test/entities/entity.test.js +28 -9
  137. package/test/entities/hatch.test.js +606 -0
  138. package/test/entities/insert.test.js +90 -0
  139. package/test/entities/line.test.js +182 -6
  140. package/test/entities/point.test.js +375 -37
  141. package/test/entities/solid.test.js +156 -0
  142. package/test/entities/text.test.js +533 -7
  143. package/test/integration/exportIntegration.reference.dxf +1706 -0
  144. package/test/integration/exportIntegration.test.js +186 -0
  145. package/test/integration/importExportIntegration.test.js +126 -0
  146. package/test/integration/intersectionIntegration.test.js +880 -0
  147. package/test/integration/userInputIntegration.reference.dxf +1410 -0
  148. package/test/integration/userInputIntegration.test.js +269 -0
  149. package/test/lib/boundingBox.test.js +26 -2
  150. package/test/lib/canvas.test.js +613 -5
  151. package/test/lib/clipboard.test.js +70 -0
  152. package/test/lib/colour.test.js +114 -0
  153. package/test/lib/colours.test.js +63 -18
  154. package/test/lib/commandLine.test.js +159 -22
  155. package/test/lib/commandManager.test.js +4 -4
  156. package/test/lib/constants.test.js +9 -0
  157. package/test/lib/dxf/dxfFile.test.js +6 -2
  158. package/test/lib/dxf/dxfIterator.test.js +1 -1
  159. package/test/lib/dxf/dxfWrite.test.js +173 -0
  160. package/test/lib/dxf/handleManager.test.js +137 -0
  161. package/test/lib/dxf/sections/blocks.test.js +5 -5
  162. package/test/lib/dxf/sections/entities.test.js +4 -4
  163. package/test/lib/dxf/sections/header.test.js +18 -4
  164. package/test/lib/dxf/sections/objects.test.js +132 -0
  165. package/test/lib/dxf/sections/section.test.js +29 -29
  166. package/test/lib/dxf/sections/tables.test.js +3 -3
  167. package/test/lib/entityManager.test.js +163 -0
  168. package/test/lib/headers.test.js +233 -0
  169. package/test/lib/inputManager.test.js +1144 -5
  170. package/test/lib/intersect.test.js +454 -331
  171. package/test/lib/logging.test.js +1 -1
  172. package/test/lib/matrix.test.js +1 -1
  173. package/test/lib/mouse.test.js +327 -4
  174. package/test/lib/patterns.test.js +105 -0
  175. package/test/lib/promptOptions.test.js +179 -0
  176. package/test/lib/scene.test.js +16 -52
  177. package/test/lib/selectionManager.test.js +17 -5
  178. package/test/lib/settings.test.js +111 -5
  179. package/test/lib/snapping.test.js +142 -4
  180. package/test/lib/stateManager.test.js +155 -0
  181. package/test/lib/utils.test.js +81 -1
  182. package/test/objects/dictionary.test.js +81 -0
  183. package/test/objects/dictionaryManager.test.js +34 -0
  184. package/test/properties/dimType.test.js +62 -0
  185. package/test/properties/flags.test.js +164 -0
  186. package/test/properties/property.test.js +13 -0
  187. package/test/properties/propertyManager.test.js +10 -11
  188. package/test/tables/appId.test.js +46 -0
  189. package/test/tables/appIdManager.test.js +72 -0
  190. package/test/tables/block.test.js +157 -0
  191. package/test/tables/blockManager.test.js +52 -0
  192. package/test/tables/blockRecord.test.js +39 -0
  193. package/test/tables/blockRecordManager.test.js +60 -0
  194. package/test/tables/dimstyle.test.js +72 -0
  195. package/test/tables/endblk.test.js +30 -0
  196. package/test/tables/layer.test.js +95 -0
  197. package/test/tables/layerManager.test.js +172 -0
  198. package/test/{styles → tables}/ltype.test.js +7 -7
  199. package/test/{styles → tables}/ltypeManager.test.js +2 -2
  200. package/test/tables/style.test.js +44 -0
  201. package/test/tables/stylemanagerbase.test.js +220 -0
  202. package/test/tables/tableManagerBase.test.js +194 -0
  203. package/test/tables/ucs.test.js +37 -0
  204. package/test/tables/ucsManager.test.js +48 -0
  205. package/test/tables/view.test.js +37 -0
  206. package/test/tables/viewManager.test.js +48 -0
  207. package/test/tables/vport.test.js +117 -0
  208. package/test/tables/vportManager.test.js +54 -0
  209. package/test/test-helpers/test-helpers.js +66 -0
  210. package/test/tools/chamfer.test.js +970 -0
  211. package/test/tools/chamferFilletBase.test.js +446 -0
  212. package/test/tools/copy.test.js +22 -14
  213. package/test/tools/copybase.test.js +93 -0
  214. package/test/tools/copyclip.test.js +92 -0
  215. package/test/tools/cornerEntity.test.js +201 -0
  216. package/test/tools/cutclip.test.js +94 -0
  217. package/test/tools/distance.test.js +140 -18
  218. package/test/tools/erase.test.js +12 -12
  219. package/test/tools/explode.test.js +45 -0
  220. package/test/tools/extend.test.js +326 -44
  221. package/test/tools/fillet.test.js +831 -0
  222. package/test/tools/identify.test.js +115 -16
  223. package/test/tools/matchProp.test.js +84 -0
  224. package/test/tools/mirror.test.js +475 -0
  225. package/test/tools/move.test.js +75 -19
  226. package/test/tools/offset.test.js +748 -0
  227. package/test/tools/pan.test.js +72 -0
  228. package/test/tools/pasteclip.test.js +100 -0
  229. package/test/tools/purge.test.js +89 -0
  230. package/test/tools/rotate.test.js +89 -23
  231. package/test/tools/scale.test.js +529 -0
  232. package/test/tools/trim.test.js +419 -84
  233. package/test/tools/zoom.test.js +504 -0
  234. package/core/entities/alignedDimension.js +0 -279
  235. package/core/entities/angularDimension.js +0 -194
  236. package/core/entities/baseDimension.js +0 -227
  237. package/core/entities/block.js +0 -247
  238. package/core/entities/diametricDimension.js +0 -133
  239. package/core/entities/dimension.js +0 -202
  240. package/core/entities/radialDimension.js +0 -126
  241. package/core/styles/dimStyle.js +0 -719
  242. package/core/styles/dimStyleManager.js +0 -47
  243. package/core/styles/layer.js +0 -133
  244. package/core/styles/layerManager.js +0 -40
  245. package/core/styles/ltype.js +0 -86
  246. package/core/styles/ltypeManager.js +0 -50
  247. package/core/styles/style.js +0 -158
  248. package/core/styles/styleManager.js +0 -43
  249. package/core/styles/styleManagerBase.js +0 -299
  250. package/test/entities/block.test.js +0 -22
  251. package/test/styles/dimstyle.test.js +0 -19
  252. package/test/styles/layerManager.test.js +0 -150
  253. package/test/styles/style.test.js +0 -33
  254. package/test/styles/stylemanagerbase.test.js +0 -219
@@ -1,15 +1,28 @@
1
- import {Strings} from '../lib/strings.js';
2
- import {Entity} from './entity.js';
3
- import {Input, PromptOptions} from '../lib/inputManager.js';
4
- import {Logging} from '../lib/logging.js';
5
- import {Utils} from '../lib/utils.js';
6
- import {DXFFile} from '../lib/dxf/dxfFile.js';
7
- import {BoundingBox} from '../lib/boundingBox.js';
8
- import {Point} from './point.js';
9
-
10
- import {DesignCore} from '../designCore.js';
11
-
1
+ import { Strings } from '../lib/strings.js';
2
+ import { Entity } from './entity.js';
3
+ import { Input, PromptOptions } from '../lib/inputManager.js';
4
+ import { Logging } from '../lib/logging.js';
5
+ import { Utils } from '../lib/utils.js';
6
+ import { DXFFile } from '../lib/dxf/dxfFile.js';
7
+ import { BoundingBox } from '../lib/boundingBox.js';
8
+ import { Point } from './point.js';
9
+ import { Line } from './line.js';
10
+ import { Arc } from './arc.js';
11
+ import { Flags } from '../properties/flags.js';
12
+ import { Property } from '../properties/property.js';
13
+ import { AddState, RemoveState, UpdateState } from '../lib/stateManager.js';
14
+
15
+ import { DesignCore } from '../designCore.js';
16
+
17
+ /**
18
+ * Base Polyline Entity Class
19
+ * @extends Entity
20
+ */
12
21
  export class BasePolyline extends Entity {
22
+ /**
23
+ * Create a Base Polyline
24
+ * @param {Array} data
25
+ */
13
26
  constructor(data) {
14
27
  super(data);
15
28
 
@@ -29,58 +42,71 @@ export class BasePolyline extends Entity {
29
42
  });
30
43
 
31
44
  Object.defineProperty(this, 'flags', {
32
- value: 0,
45
+ value: data?.flags instanceof Flags ? data.flags : new Flags(),
33
46
  writable: true,
34
47
  });
35
48
 
36
49
  if (data) {
37
- if (data[40]) {
50
+ if (data.hasOwnProperty('40')) {
38
51
  // DXF Groupcode 40 - Start Width
39
52
  }
40
53
 
41
- if (data[41]) {
54
+ if (data.hasOwnProperty('41')) {
42
55
  // DXF Groupcode 41 - End Width
43
56
  }
44
57
 
45
- if (data[43]) {
58
+ if (data.hasOwnProperty('43')) {
46
59
  // DXF Groupcode 43 - Constant Width
47
60
  }
61
+ }
48
62
 
49
- if (data.flags || data[70]) {
50
- // DXF Groupcode 70 - Polyline flag (bit-coded; default = 0):
51
- // 1 = This is a closed polyline (or a polygon mesh closed in the M direction)
52
- // 2 = Curve-fit vertices have been added
53
- // 4 = Spline-fit vertices have been added
54
- // 8 = This is a 3D polyline
55
- // 16 = This is a 3D polygon mesh
56
- // 32 = The polygon mesh is closed in the N direction
57
- // 64 = The polyline is a polyface mesh
58
- // 128 = The linetype pattern is generated continuously around the vertices of this polyline
59
-
60
- this.flags = data.flags || data[70];
61
- }
63
+ // DXF Groupcode 70 - Polyline flag (bit-coded; default = 0):
64
+ // 1 = This is a closed polyline (or a polygon mesh closed in the M direction)
65
+ // 2 = Curve-fit vertices have been added
66
+ // 4 = Spline-fit vertices have been added
67
+ // 8 = This is a 3D polyline
68
+ // 16 = This is a 3D polygon mesh
69
+ // 32 = The polygon mesh is closed in the N direction
70
+ // 64 = The polyline is a polyface mesh
71
+ // 128 = The linetype pattern is generated continuously around the vertices of this polyline
72
+
73
+ this.flags.setFlagValue(Property.loadValue([data?.flags, data?.[70]], 0));
74
+
75
+ // If the first and last points are the same the polyline should be considered closed.
76
+ // Require at least 4 points so that after removing the duplicate endpoint at least 3 remain.
77
+ if (this.points.length >= 4 && this.points[0].isSame(this.points.at(-1))) {
78
+ this.points.pop();
79
+ this.flags.addValue(1);
62
80
  }
63
81
  }
64
82
 
83
+ /**
84
+ * Execute method
85
+ * executes the workflow, requesting input required to create an entity
86
+ */
65
87
  async execute() {
66
88
  try {
67
89
  const op = new PromptOptions(Strings.Input.START, [Input.Type.POINT]);
68
90
  const pt1 = await DesignCore.Scene.inputManager.requestInput(op);
91
+ if (pt1 === undefined) return;
69
92
  this.points.push(pt1);
70
93
 
71
- let pt2;
72
- let op2;
73
94
  let index;
74
95
  while (true) {
75
- let options;
76
- let prompt = Strings.Input.NEXTPOINT;
96
+ const options = [];
77
97
  if (this.points.length >= 2) {
78
- options = this.inputMode === this.modes.LINE ? [this.modes.ARC] : [this.modes.LINE];
79
- prompt = `${Strings.Input.NEXTPOINT} or ${Strings.Input.OPTION}`;
98
+ options.push(this.inputMode === this.modes.LINE ? this.modes.ARC : this.modes.LINE);
99
+ }
100
+ if (this.points.length >= 3) {
101
+ options.push('Close');
102
+ }
103
+ if (this.points.length > 1) {
104
+ options.push('Undo');
80
105
  }
81
106
 
82
- op2 = new PromptOptions(prompt, [Input.Type.POINT], options);
83
- pt2 = await DesignCore.Scene.inputManager.requestInput(op2);
107
+ const op2 = new PromptOptions(Strings.Input.NEXTPOINT, [Input.Type.POINT, Input.Type.DYNAMIC], options);
108
+ const pt2 = await DesignCore.Scene.inputManager.requestInput(op2);
109
+ if (pt2 === undefined) break;
84
110
 
85
111
  if (Input.getType(pt2) === Input.Type.POINT) {
86
112
  if (this.inputMode === this.modes.ARC) {
@@ -92,10 +118,29 @@ export class BasePolyline extends Entity {
92
118
  index = DesignCore.Scene.inputManager.actionCommand(this, index);
93
119
  } else if (Input.getType(pt2) === Input.Type.STRING) {
94
120
  // options are converted to input in the prompt options class
95
- if (pt2 === this.modes.ARC) {
121
+ if (pt2 === 'Undo') {
122
+ this.points.pop();
123
+ // Always reset bulge to 0 after undo
124
+ if (this.points.length > 0) {
125
+ this.points.at(-1).bulge = 0;
126
+ }
127
+ // Keep the scene entity in sync with the updated points
128
+ if (index !== undefined) {
129
+ index = DesignCore.Scene.inputManager.actionCommand(this, index);
130
+ }
131
+ continue;
132
+ } else if (pt2 === 'Close') {
133
+ // Only set bulge if in arc mode and at least two points
134
+ if (this.inputMode === this.modes.ARC && this.points.length > 1) {
135
+ this.points.at(-1).bulge = this.getBulgeFromSegment(pt1);
136
+ }
137
+ // Set flags to indicate closed shape
138
+ this.flags.addValue(1);
139
+ DesignCore.Scene.inputManager.executeCommand(this, index);
140
+ return;
141
+ } else if (pt2 === this.modes.ARC) {
96
142
  this.inputMode = this.modes.ARC;
97
- }
98
- if (pt2 === this.modes.LINE) {
143
+ } else if (pt2 === this.modes.LINE) {
99
144
  this.inputMode = this.modes.LINE;
100
145
  }
101
146
  }
@@ -105,26 +150,33 @@ export class BasePolyline extends Entity {
105
150
  }
106
151
  }
107
152
 
153
+ /**
154
+ * Preview the entity during creation
155
+ */
108
156
  preview() {
109
157
  const mousePoint = DesignCore.Mouse.pointOnScene();
110
158
 
111
159
  if (this.points.length >= 1) {
112
160
  const points = [...this.points, mousePoint];
113
- DesignCore.Scene.createTempItem(this.type, {points: points});
161
+ DesignCore.Scene.tempEntities.create(this.type, { points: points });
114
162
  }
115
163
 
116
164
  if (this.inputMode === this.modes.ARC) {
117
- const arcpoints = Utils.cloneObject( this.points);
165
+ const arcpoints = Utils.cloneObject(this.points);
118
166
  arcpoints.at(-1).bulge = this.getBulgeFromSegment(mousePoint);
119
167
  const points = [...arcpoints, mousePoint];
120
- DesignCore.Scene.createTempItem(this.type, {points: points});
168
+ DesignCore.Scene.tempEntities.create(this.type, { points: points });
121
169
  }
122
170
  }
123
171
 
124
- draw(ctx, scale) {
125
- ctx.moveTo(this.points[0].x, this.points[0].y);
126
-
127
- for (let i = 1; i < this.points.length; i++) {
172
+ /**
173
+ * Draw the polyline
174
+ * @param {Object} ctx
175
+ * @param {number} scale
176
+ * @param {boolean} stroke - don't stroke hatch boundary shapes
177
+ */
178
+ draw(ctx, scale, stroke = true) {
179
+ for (let i = 0; i < this.points.length; i++) {
128
180
  if (this.points[i].bulge === 0) {
129
181
  ctx.lineTo(this.points[i].x, this.points[i].y);
130
182
  } else {
@@ -143,60 +195,76 @@ export class BasePolyline extends Entity {
143
195
  ctx.arcNegative(centerPoint.x, centerPoint.y, radius, centerPoint.angle(this.points[i]), centerPoint.angle(nextPoint));
144
196
  }
145
197
  }
146
-
147
- // debug centerpoint
148
- // ctx.arc(centerPoint.x, centerPoint.y, 3, 0, 2 * Math.PI);
149
198
  }
150
199
  }
151
200
 
152
201
  // handle a closed shape
153
- if (this.flags % 2 === 1) {
202
+ if (this.flags.hasFlag(1)) {
154
203
  ctx.lineTo(this.points[0].x, this.points[0].y);
155
204
  }
156
205
 
157
- ctx.stroke();
206
+ if (stroke) {
207
+ ctx.stroke();
208
+ }
158
209
  }
159
210
 
211
+ /**
212
+ * Write the entity to file in the dxf format
213
+ * @param {DXFFile} file
214
+ */
160
215
  dxf(file) {
161
- file.writeGroupCode('0', 'POLYLINE');
162
- file.writeGroupCode('5', file.nextHandle(), DXFFile.Version.R2000); // Handle
216
+ file.writeGroupCode('0', 'LWPOLYLINE');
217
+ file.writeGroupCode('5', this.handle, DXFFile.Version.R2000); // Handle
163
218
  file.writeGroupCode('100', 'AcDbEntity', DXFFile.Version.R2000);
164
- file.writeGroupCode('100', 'AcDb2dPolyline', DXFFile.Version.R2000);
219
+ file.writeGroupCode('100', 'AcDbPolyline', DXFFile.Version.R2000);
165
220
  file.writeGroupCode('8', this.layer); // LAYERNAME
166
- file.writeGroupCode('10', '0');
167
- file.writeGroupCode('20', '0');
168
- file.writeGroupCode('30', '0');
221
+ file.writeGroupCode('6', this.lineType);
169
222
  file.writeGroupCode('39', this.lineWidth);
170
- file.writeGroupCode('70', this.flags);
171
- file.writeGroupCode('66', '1'); // Vertices follow: required for R12, optional for R2000+
172
- this.vertices(file);
173
- file.writeGroupCode('0', 'SEQEND');
174
- file.writeGroupCode('5', file.nextHandle(), DXFFile.Version.R2000); // Handle
175
- file.writeGroupCode('100', 'AcDbEntity', DXFFile.Version.R2000);
176
- file.writeGroupCode('8', this.layer);
177
- }
178
-
179
- vertices(file) {
223
+ file.writeGroupCode('90', this.points.length);
224
+ file.writeGroupCode('70', this.flags.getFlagValue());
180
225
  for (let i = 0; i < this.points.length; i++) {
181
- file.writeGroupCode('0', 'VERTEX');
182
- file.writeGroupCode('5', file.nextHandle(), DXFFile.Version.R2000); // Handle
183
- file.writeGroupCode('100', 'AcDbEntity', DXFFile.Version.R2000);
184
- file.writeGroupCode('100', 'AcDbVertex', DXFFile.Version.R2000);
185
- file.writeGroupCode('100', 'AcDb2dVertex', DXFFile.Version.R2000);
186
- file.writeGroupCode('8', this.layer);
187
- file.writeGroupCode('10', this.points[i].x); // X
188
- file.writeGroupCode('20', this.points[i].y); // Y
189
- file.writeGroupCode('30', '0.0');
226
+ file.writeGroupCode('10', this.points[i].x);
227
+ file.writeGroupCode('20', this.points[i].y);
190
228
  file.writeGroupCode('42', this.points[i].bulge);
191
229
  }
192
230
  }
193
231
 
194
- intersectPoints() {
195
- return {
196
- points: this.points,
197
- };
232
+ /**
233
+ * Return a list of points representing a polyline version of this entity.
234
+ * For closed polylines (flag bit 1 set), appends a copy of the first point
235
+ * at the end so that callers (e.g. hatch boundary detection, intersection
236
+ * tests) can treat the sequence as a simple open chain where the first and
237
+ * last point are equal — the same convention used by Circle.toPolylinePoints().
238
+ * The closure point carries bulge = 0 because it is just the destination;
239
+ * the actual closing-segment bulge lives on the last stored point.
240
+ * @return {Array}
241
+ */
242
+ toPolylinePoints() {
243
+ const points = this.points.map((p) => new Point(p.x, p.y, p.bulge, p.sequence));
244
+ if (this.flags.hasFlag(1)) {
245
+ points.push(new Point(this.points[0].x, this.points[0].y));
246
+ }
247
+ return points;
198
248
  }
199
249
 
250
+ /**
251
+ * Set entity points from a polyline point representation
252
+ * @param {Array} points
253
+ */
254
+ fromPolylinePoints(points) {
255
+ const pts = points.map((p) => new Point(p.x, p.y, p.bulge));
256
+ if (this.flags.hasFlag(1) && pts.length >= 2 && pts[0].isSame(pts.at(-1))) {
257
+ pts.pop();
258
+ }
259
+ this.points = pts;
260
+ }
261
+
262
+ /**
263
+ * Get snap points
264
+ * @param {Point} mousePoint
265
+ * @param {number} delta
266
+ * @return {Array} - array of snap points
267
+ */
200
268
  snaps(mousePoint, delta) {
201
269
  const snaps = [];
202
270
 
@@ -209,16 +277,16 @@ export class BasePolyline extends Entity {
209
277
 
210
278
  if (DesignCore.Settings.midsnap) {
211
279
  for (let i = 1; i < this.points.length; i++) {
212
- if (this.points[i-1].bulge === 0) {
213
- snaps.push( this.points[i - 1].midPoint(this.points[i]));
280
+ if (this.points[i - 1].bulge === 0) {
281
+ snaps.push(this.points[i - 1].midPoint(this.points[i]));
214
282
  }
215
283
  }
216
284
  }
217
285
 
218
286
  if (DesignCore.Settings.centresnap) {
219
287
  for (let i = 1; i < this.points.length; i++) {
220
- if (this.points[i-1].bulge !== 0) {
221
- snaps.push( this.points[i - 1].bulgeCentrePoint(this.points[i]));
288
+ if (this.points[i - 1].bulge !== 0) {
289
+ snaps.push(this.points[i - 1].bulgeCentrePoint(this.points[i]));
222
290
  }
223
291
  }
224
292
  }
@@ -235,6 +303,89 @@ export class BasePolyline extends Entity {
235
303
  return snaps;
236
304
  }
237
305
 
306
+ /**
307
+ * Get the index and segment object for the segment closest to point P
308
+ * @param {Point} P
309
+ * @return {Object}
310
+ */
311
+ #getClosestSegmentData(P) {
312
+ let closestIndex = -1;
313
+ let closestSegment = null;
314
+ let minDistance = Infinity;
315
+
316
+ const segmentCount = this.flags.hasFlag(1) ? this.points.length : this.points.length - 1;
317
+ for (let i = 1; i <= segmentCount; i++) {
318
+ const A = this.points[i - 1];
319
+ const B = this.points[i % this.points.length];
320
+
321
+ let closestPoint;
322
+ let candidateSegment;
323
+ if (A.bulge !== 0) {
324
+ const center = A.bulgeCentrePoint(B);
325
+ const direction = A.bulge > 0 ? 1 : -1;
326
+ closestPoint = P.closestPointOnArc(A, B, center, direction);
327
+ candidateSegment = new Arc({ points: [center, A, B], direction });
328
+ } else {
329
+ closestPoint = P.closestPointOnLine(A, B);
330
+ candidateSegment = new Line({ points: [A, B] });
331
+ }
332
+
333
+ if (closestPoint) {
334
+ const dist = P.distance(closestPoint);
335
+ if (dist < minDistance) {
336
+ minDistance = dist;
337
+ closestIndex = i;
338
+ closestSegment = candidateSegment;
339
+ }
340
+ }
341
+ }
342
+
343
+ return { index: closestIndex, segment: closestSegment };
344
+ }
345
+
346
+ /**
347
+ * Get the 1-based index of the segment closest to point P
348
+ * @param {Point} P
349
+ * @return {number} - 1-based segment index, or -1 if no segments
350
+ */
351
+ getClosestSegmentIndex(P) {
352
+ return this.#getClosestSegmentData(P).index;
353
+ }
354
+
355
+ /**
356
+ * Get the segment closest to point P
357
+ * @param {Point} P
358
+ * @return {Line|Arc|null} - closest segment
359
+ */
360
+ getClosestSegment(P) {
361
+ return this.#getClosestSegmentData(P).segment;
362
+ }
363
+
364
+ /**
365
+ * Determine if two segments (by 1-based index) are consecutive in this polyline
366
+ * @param {number} segIndex1
367
+ * @param {number} segIndex2
368
+ * @return {boolean}
369
+ */
370
+ areConsecutiveSegments(segIndex1, segIndex2) {
371
+ const diff = Math.abs(segIndex1 - segIndex2);
372
+ if (diff === 1) return true;
373
+ // Closed polyline: closing segment (index = points.length) and segment 1 are consecutive
374
+ if (this.flags.hasFlag(1)) {
375
+ const closeIdx = this.points.length;
376
+ if ((segIndex1 === closeIdx && segIndex2 === 1) ||
377
+ (segIndex2 === closeIdx && segIndex1 === 1)) {
378
+ return true;
379
+ }
380
+ }
381
+ return false;
382
+ }
383
+
384
+ /**
385
+ * Get closest point on entity
386
+ * @param {Point} P
387
+ * @return {Array} - [Point, distance]
388
+ */
238
389
  closestPoint(P) {
239
390
  let distance = Infinity;
240
391
  let minPnt = P;
@@ -246,7 +397,11 @@ export class BasePolyline extends Entity {
246
397
  let pnt;
247
398
  if (A.bulge !== 0) {
248
399
  const C = A.bulgeCentrePoint(B);
249
- const direction = A.bulge;
400
+ // -ve bulge is clockwise
401
+ // +ve bulge is counter clockwise
402
+ // -ve arc is clockwise
403
+ // +ve arc is counter clockwise
404
+ const direction = A.bulge > 0 ? 1 : -1;
250
405
  pnt = P.closestPointOnArc(A, B, C, direction);
251
406
  } else {
252
407
  pnt = P.closestPointOnLine(A, B);
@@ -265,8 +420,312 @@ export class BasePolyline extends Entity {
265
420
  return [minPnt, distance];
266
421
  }
267
422
 
423
+ /**
424
+ * Check if a point falls on a specific segment of the polyline
425
+ * @param {Point} point - the point to test
426
+ * @param {Point} A - segment start point
427
+ * @param {Point} B - segment end point
428
+ * @return {boolean}
429
+ */
430
+ isPointOnSegment(point, A, B) {
431
+ if (A.bulge !== 0) {
432
+ const center = A.bulgeCentrePoint(B);
433
+ const direction = A.bulge > 0 ? 1 : -1;
434
+ return point.isOnArc(A, B, center, direction);
435
+ }
436
+ return point.isOnLine(A, B);
437
+ }
438
+
439
+ /**
440
+ * Get the closest point on a segment to a given point
441
+ * @param {Point} point - the reference point
442
+ * @param {number} segmentIndex - 1-based segment index
443
+ * @return {Point|null}
444
+ */
445
+ closestPointOnSegment(point, segmentIndex) {
446
+ const A = this.points[segmentIndex - 1];
447
+ const B = this.points[segmentIndex % this.points.length];
448
+
449
+ if (A.bulge !== 0) {
450
+ const center = A.bulgeCentrePoint(B);
451
+ const direction = A.bulge > 0 ? 1 : -1;
452
+ const closest = point.closestPointOnArc(A, B, center, direction);
453
+
454
+ if (closest) {
455
+ return closest;
456
+ }
457
+
458
+ // Point not on arc - return the nearer endpoint
459
+ return point.distance(A) < point.distance(B) ? A : B;
460
+ }
461
+
462
+ return point.closestPointOnLine(A, B);
463
+ }
464
+
465
+ /**
466
+ * Get the normalised position of a point along a segment for ordering
467
+ * @param {Point} point - a point on the segment
468
+ * @param {number} segmentIndex - 1-based segment index
469
+ * @return {number}
470
+ */
471
+ positionOnSegment(point, segmentIndex) {
472
+ const A = this.points[segmentIndex - 1];
473
+ const B = this.points[segmentIndex % this.points.length];
474
+
475
+ if (A.bulge !== 0) {
476
+ const center = A.bulgeCentrePoint(B);
477
+ const direction = A.bulge > 0 ? 1 : -1;
478
+ const startAngle = center.angle(A);
479
+ const pointAngle = center.angle(point);
480
+ return ((pointAngle - startAngle) * direction + 4 * Math.PI) % (2 * Math.PI);
481
+ }
482
+
483
+ return A.distance(point);
484
+ }
485
+
486
+ /**
487
+ * Extend the entity
488
+ * Only extends when the end segment closest to the mouse is a line segment (bulge === 0)
489
+ * @param {Array} intersections - array of intersection points
490
+ * @return {Array} - array of state changes
491
+ */
492
+ extend(intersections) {
493
+ const stateChanges = [];
494
+
495
+ if (!intersections?.length) {
496
+ return stateChanges;
497
+ }
498
+
499
+ const mousePosition = DesignCore.Mouse.pointOnScene();
500
+ const lastIndex = this.points.length - 1;
501
+
502
+ // Determine which end segment is closer to the mouse
503
+ const closestOnFirst = this.closestPointOnSegment(mousePosition, 1);
504
+ const closestOnLast = this.closestPointOnSegment(mousePosition, lastIndex);
505
+ const distToFirst = mousePosition.distance(closestOnFirst);
506
+ const distToLast = mousePosition.distance(closestOnLast);
507
+
508
+ // Identify the selected end: the endpoint index and the bulge of its segment
509
+ let endPointIndex;
510
+ let endSegmentBulge;
511
+
512
+ if (distToFirst < distToLast) {
513
+ endPointIndex = 0;
514
+ endSegmentBulge = this.points[0].bulge;
515
+ } else if (distToLast < distToFirst) {
516
+ endPointIndex = lastIndex;
517
+ endSegmentBulge = this.points[lastIndex - 1].bulge;
518
+ } else {
519
+ // Tie (includes 2-point polyline where both segments are the same)
520
+ // Break tie by distance to the actual endpoints
521
+ if (mousePosition.distance(this.points[0]) <= mousePosition.distance(this.points[lastIndex])) {
522
+ endPointIndex = 0;
523
+ endSegmentBulge = this.points[0].bulge;
524
+ } else {
525
+ endPointIndex = lastIndex;
526
+ endSegmentBulge = this.points[lastIndex - 1].bulge;
527
+ }
528
+ }
529
+
530
+ // Only allow extending line segments (not arcs)
531
+ if (endSegmentBulge !== 0) {
532
+ DesignCore.Core.notify(`${this.type} - ${Strings.Message.NOEXTEND}`);
533
+ return stateChanges;
534
+ }
535
+
536
+ const endPoint = this.points[endPointIndex];
537
+ const adjacentPoint = endPointIndex === 0 ? this.points[1] : this.points[lastIndex - 1];
538
+
539
+ // Sort intersections by distance from the end point
540
+ Utils.sortPointsByDistance(intersections, endPoint);
541
+
542
+ // Find the closest intersection that lies beyond the endpoint
543
+ const direction = endPoint.subtract(adjacentPoint);
544
+ const newEndPoint = intersections.find((p) => {
545
+ if (p.isSame(endPoint)) return false;
546
+ if (p.subtract(endPoint).dot(direction) <= 0) return false;
547
+ if (adjacentPoint.distance(p) <= adjacentPoint.distance(endPoint)) return false;
548
+ return true;
549
+ });
550
+
551
+ if (!newEndPoint) {
552
+ return stateChanges;
553
+ }
554
+
555
+ // Build the new points array
556
+ const newPoints = this.points.map((p) => p.clone());
557
+ newPoints[endPointIndex] = newEndPoint;
558
+
559
+ if (newPoints[endPointIndex].isSame(this.points[endPointIndex])) {
560
+ return stateChanges;
561
+ }
562
+
563
+ stateChanges.push(new UpdateState(this, { points: newPoints }));
564
+ return stateChanges;
565
+ }
566
+
567
+ /**
568
+ * Trim the entity
569
+ * @param {Array} intersections - array of intersection points
570
+ * @return {Array} - array of state changes
571
+ */
572
+ trim(intersections) {
573
+ const stateChanges = [];
574
+
575
+ if (!intersections?.length) {
576
+ return stateChanges;
577
+ }
578
+
579
+ const mousePosition = DesignCore.Mouse.pointOnScene();
580
+
581
+ // Find the segment index closest to the mouse
582
+ let mouseSegmentIndex = 0;
583
+ let minDistance = Infinity;
584
+
585
+ for (let i = 1; i < this.points.length; i++) {
586
+ const closestPoint = this.closestPointOnSegment(mousePosition, i);
587
+ if (closestPoint) {
588
+ const dist = mousePosition.distance(closestPoint);
589
+ if (dist < minDistance) {
590
+ minDistance = dist;
591
+ mouseSegmentIndex = i;
592
+ }
593
+ }
594
+ }
595
+
596
+ // Filter out intersections that coincide with existing polyline vertices
597
+ const filteredIntersections = intersections.filter((point) => !this.points.some((vertex) => vertex.isSame(point)));
598
+
599
+ if (!filteredIntersections.length) {
600
+ return stateChanges;
601
+ }
602
+
603
+ // Locate each intersection on its segment with an ordering value
604
+ const locatedIntersections = [];
605
+
606
+ for (const point of filteredIntersections) {
607
+ for (let i = 1; i < this.points.length; i++) {
608
+ if (this.isPointOnSegment(point, this.points[i - 1], this.points[i])) {
609
+ locatedIntersections.push({
610
+ segmentIndex: i,
611
+ point: point,
612
+ positionAlongSegment: this.positionOnSegment(point, i),
613
+ });
614
+ break;
615
+ }
616
+ }
617
+ }
618
+
619
+ if (!locatedIntersections.length) {
620
+ return stateChanges;
621
+ }
622
+
623
+ // Sort by segment index, then by position along segment
624
+ locatedIntersections.sort((a, b) => {
625
+ if (a.segmentIndex !== b.segmentIndex) return a.segmentIndex - b.segmentIndex;
626
+ return a.positionAlongSegment - b.positionAlongSegment;
627
+ });
628
+
629
+ // Precompute the mouse position along its segment
630
+ const mouseClosest = this.closestPointOnSegment(mousePosition, mouseSegmentIndex);
631
+ const mousePos = mouseClosest ? this.positionOnSegment(mouseClosest, mouseSegmentIndex) : 0;
632
+
633
+ // Find the nearest intersections that bracket the mouse segment
634
+ // - trimBefore: the last intersection at or before the mouse
635
+ // - trimAfter: the first intersection at or after the mouse
636
+ let trimBefore = null;
637
+ let trimAfter = null;
638
+
639
+ for (const loc of locatedIntersections) {
640
+ if (loc.segmentIndex < mouseSegmentIndex) {
641
+ trimBefore = loc;
642
+ } else if (loc.segmentIndex === mouseSegmentIndex) {
643
+ if (loc.positionAlongSegment <= mousePos) {
644
+ trimBefore = loc;
645
+ } else if (!trimAfter) {
646
+ trimAfter = loc;
647
+ }
648
+ } else if (!trimAfter) {
649
+ trimAfter = loc;
650
+ }
651
+ }
652
+
653
+ // Build new polyline(s) from the remaining portions
654
+ // Portion 1: start of polyline to trimBefore point
655
+ if (trimBefore) {
656
+ const points = [];
657
+
658
+ for (let i = 0; i < trimBefore.segmentIndex - 1; i++) {
659
+ points.push(this.points[i].clone());
660
+ }
661
+
662
+ // Add the segment start point (with adjusted bulge for arcs)
663
+ const segStart = this.points[trimBefore.segmentIndex - 1];
664
+ const segStartClone = segStart.clone();
665
+ if (segStart.bulge !== 0) {
666
+ segStartClone.bulge = segStart.partialBulge(this.points[trimBefore.segmentIndex], trimBefore.point);
667
+ }
668
+ points.push(segStartClone);
669
+
670
+ // Add the trim point (skip if it coincides with the last added point)
671
+ const trimPoint = trimBefore.point.clone();
672
+ if (!points.at(-1).isSame(trimPoint)) {
673
+ points.push(trimPoint);
674
+ }
675
+
676
+ if (points.length >= 2) {
677
+ const polyline = Utils.cloneObject(this);
678
+ polyline.points = points;
679
+ polyline.flags.removeValue(1); // clear closed bit
680
+ stateChanges.push(new AddState(polyline));
681
+ }
682
+ }
683
+
684
+ // Portion 2: trimAfter point to end of polyline
685
+ if (trimAfter) {
686
+ const points = [];
687
+ const trimPoint = trimAfter.point.clone();
688
+
689
+ // If the segment is an arc, set remaining arc bulge
690
+ const segStart = this.points[trimAfter.segmentIndex - 1];
691
+ if (segStart.bulge !== 0) {
692
+ trimPoint.bulge = segStart.partialBulge(this.points[trimAfter.segmentIndex], trimPoint, true);
693
+ }
694
+
695
+ points.push(trimPoint);
696
+
697
+ // Add remaining points (skip if coincides with the trim point)
698
+ for (let i = trimAfter.segmentIndex; i < this.points.length; i++) {
699
+ const nextPoint = this.points[i].clone();
700
+ if (!points.at(-1).isSame(nextPoint)) {
701
+ points.push(nextPoint);
702
+ }
703
+ }
704
+
705
+ if (points.length >= 2) {
706
+ const polyline = Utils.cloneObject(this);
707
+ polyline.points = points;
708
+ polyline.flags.removeValue(1); // clear closed bit
709
+ stateChanges.push(new AddState(polyline));
710
+ }
711
+ }
712
+
713
+ if (trimBefore || trimAfter) {
714
+ stateChanges.push(new RemoveState(this));
715
+ }
716
+
717
+ return stateChanges;
718
+ }
719
+
720
+ /**
721
+ * Return boundingbox for entity
722
+ * @return {BoundingBox}
723
+ */
268
724
  boundingBox() {
269
- let xmin; let xmax; let ymin; let ymax;
725
+ let xmin = Infinity;
726
+ let xmax = -Infinity;
727
+ let ymin = Infinity;
728
+ let ymax = -Infinity;
270
729
 
271
730
  for (let i = 0; i < this.points.length; i++) {
272
731
  const nextPoint = this.points[i + 1] || this.points[0];
@@ -278,10 +737,10 @@ export class BasePolyline extends Entity {
278
737
  boundingBox = BoundingBox.arcBoundingBox(centerPoint, this.points[i], nextPoint, this.points[i].bulge);
279
738
  }
280
739
 
281
- xmin = Math.min(xmin || Infinity, boundingBox.xMin);
282
- xmax = Math.max(xmax || -Infinity, boundingBox.xMax);
283
- ymin = Math.min(ymin || Infinity, boundingBox.yMin);
284
- ymax = Math.max(ymax || -Infinity, boundingBox.yMax);
740
+ xmin = Math.min(xmin, boundingBox.xMin);
741
+ xmax = Math.max(xmax, boundingBox.xMax);
742
+ ymin = Math.min(ymin, boundingBox.yMin);
743
+ ymax = Math.max(ymax, boundingBox.yMax);
285
744
  }
286
745
 
287
746
 
@@ -293,8 +752,8 @@ export class BasePolyline extends Entity {
293
752
 
294
753
  /**
295
754
  * Get the bulge value from the previous segment and the selected point
296
- * @param {point} point
297
- * @returns polyline bulge value
755
+ * @param {Point} point
756
+ * @return {number} polyline bulge value
298
757
  */
299
758
  getBulgeFromSegment(point) {
300
759
  const lastSegBulge = this.points.at(-2).bulge;
@@ -307,14 +766,14 @@ export class BasePolyline extends Entity {
307
766
  // angle is perpendicular to the center-to-end point ray, in the direction of the bulge
308
767
  lastSegAngle = centerPoint.angle(this.points.at(-1)) + (Math.PI / 2) * Math.sign(lastSegBulge);
309
768
  } else {
310
- // line segment angle is the angle from point 0 to to point 1
769
+ // line segment angle is the angle from point 0 to point 1
311
770
  lastSegAngle = this.points.at(-2).angle(this.points.at(-1));
312
771
  }
313
772
 
314
773
  const mouseAngle = this.points.at(-1).angle(point) % (2 * Math.PI);
315
774
  // get the angle delta between point and the previous segment
316
775
  // ensure that the angle is always less than 2 * Math.PI
317
- const angleDelta = ((mouseAngle - lastSegAngle) + 3 * Math.PI) % (2*Math.PI) - Math.PI;
776
+ const angleDelta = ((mouseAngle - lastSegAngle) + 3 * Math.PI) % (2 * Math.PI) - Math.PI;
318
777
  // angleDelta is 1/2 the included angle
319
778
  // bulge is tan of (included angle * 0.25)
320
779
  const bulge = Math.tan((angleDelta * 2) / 4);