@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
@@ -0,0 +1,807 @@
1
+ import { Entity } from './entity.js';
2
+ import { DXFFile } from '../lib/dxf/dxfFile.js';
3
+ import { BoundingBox } from '../lib/boundingBox.js';
4
+
5
+ import { Strings } from '../lib/strings.js';
6
+ import { Input, PromptOptions } from '../lib/inputManager.js';
7
+ import { Logging } from '../lib/logging.js';
8
+
9
+ import { Point } from './point.js';
10
+ import { Flags } from '../properties/flags.js';
11
+
12
+ import { Utils } from '../lib/utils.js';
13
+ import { Patterns } from '../lib/patterns.js';
14
+ import { Intersection } from '../lib/intersect.js';
15
+
16
+ import { DesignCore } from '../designCore.js';
17
+
18
+ import { Line } from './line.js';
19
+ import { Arc } from './arc.js';
20
+ import { Polyline } from './polyline.js';
21
+ import { Property } from '../properties/property.js';
22
+
23
+ /**
24
+ * Hatch Entity Class
25
+ * @extends Entity
26
+ */
27
+ export class Hatch extends Entity {
28
+ /**
29
+ * Create a Hatch Entity
30
+ * @param {Array} data
31
+ */
32
+ constructor(data) {
33
+ super(data);
34
+
35
+ // store the boundary shapes
36
+ Object.defineProperty(this, 'childEntities', {
37
+ value: [],
38
+ writable: true,
39
+ });
40
+
41
+ Object.defineProperty(this, 'pattern', {
42
+ value: 'ANSI31',
43
+ writable: true,
44
+ });
45
+
46
+ Object.defineProperty(this, 'solid', {
47
+ value: false,
48
+ writable: true,
49
+ });
50
+
51
+ Object.defineProperty(this, 'patternName', {
52
+ // value: 'ANSI31',
53
+ // writable: true,
54
+ enumerable: true,
55
+ get: this.getPatternName,
56
+ set: this.setPatternName,
57
+ });
58
+
59
+ Object.defineProperty(this, 'angle', {
60
+ value: 0,
61
+ writable: true,
62
+ enumerable: true,
63
+ });
64
+
65
+ Object.defineProperty(this, 'scale', {
66
+ value: 1,
67
+ writable: true,
68
+ enumerable: true,
69
+ });
70
+
71
+ // hide inherited properties
72
+ // needs to be enumerable=false to not appear in the object props
73
+ Object.defineProperty(this, 'lineType', {
74
+ enumerable: false,
75
+ });
76
+
77
+ Object.defineProperty(this, 'lineWidth', {
78
+ enumerable: false,
79
+ });
80
+
81
+ // add a single point to this.points if no other points exist
82
+ if (!this.points.length) {
83
+ this.points.push(new Point());
84
+ }
85
+
86
+ // DXF Groupcode 70 - Solid Fill Flag (1 = solid, 0 = pattern)
87
+ this.solid = Boolean(Property.loadValue([data?.solid, data?.[70]], 0));
88
+ // DXF Groupcode 2 - Hatch pattern name
89
+ this.patternName = Property.loadValue([data?.patternName, data?.[2]], 'ANSI31');
90
+ // DXF Groupcode 41 - Hatch pattern scale
91
+ this.scale = Property.loadValue([data?.scale, data?.[41]], 1);
92
+ // DXF Groupcode 52 - Hatch pattern angle
93
+ this.angle = Property.loadValue([data?.angle, data?.[52]], 0);
94
+
95
+ if (data) {
96
+ if (data.hasOwnProperty('childEntities')) {
97
+ if (Array.isArray(data.childEntities)) {
98
+ this.childEntities = data.childEntities;
99
+ }
100
+ } else {
101
+ const shapes = this.processBoundaryData(data);
102
+ if (shapes.length) {
103
+ this.childEntities = shapes;
104
+ }
105
+ }
106
+ }
107
+
108
+ // ensure first point is 0,0 and last point is 1,1
109
+ if (this.points.at(0).x !== 0 || this.points.at(0).y !== 0) {
110
+ this.points[0] = new Point(0, 0);
111
+ }
112
+
113
+ if (this.points.length < 2 || (this.points.at(-1).x !== 1 || this.points.at(-1).y !== 1)) {
114
+ this.points.push(new Point(1, 1));
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Get the hatch pattern name
120
+ * @return {string}
121
+ */
122
+ getPatternName() {
123
+ return this.pattern;
124
+ }
125
+
126
+ /**
127
+ * Set the hatch pattern name
128
+ * @param {string} name
129
+ */
130
+ setPatternName(name) {
131
+ this.pattern = name.toUpperCase();
132
+ this.solid = this.pattern === 'SOLID';
133
+ }
134
+
135
+ /**
136
+ * Process the dxf data, creating hatch boundaries
137
+ * @param {Array} data
138
+ * @return {Array} - Array of boundary items
139
+ */
140
+ processBoundaryData(data) {
141
+ if (!data.hasOwnProperty('points')) {
142
+ return [];
143
+ }
144
+
145
+ const childEntities = [];
146
+
147
+ // copy this.points and remove first point
148
+ // First and last points define the hatch, not the hatch boundary
149
+ // Some CAD systems don't include the last point so leave it in place
150
+ const points = data.points.slice(1);
151
+
152
+ // Process the boundary paths into objects
153
+ if (data.hasOwnProperty('91')) {
154
+ // DXF Groupcode 91 - Number of boundary paths (loops)
155
+ const boundaryPathCount = data[91];
156
+ // 92 - Boundary path type flag (bit coded): 0 = Default; 1 = External; 2 = Polyline 4 = Derived; 8 = Textbox; 16 = Outermost
157
+ const boundaryPathType = this.getDataValue(data, 92);
158
+
159
+ for (let i = 0; i < boundaryPathCount; i++) {
160
+ // check if boundary path is polyline
161
+ const boundaryPathTypeFlag = new Flags();
162
+ boundaryPathTypeFlag.setFlagValue(boundaryPathType);
163
+ const isPolyline = boundaryPathTypeFlag.hasFlag(2);
164
+
165
+ if (data.hasOwnProperty('93')) {
166
+ // DXF Groupcode 93 - Number of edges in this boundary path (only if boundary is not a polyline)
167
+ // const edgeCountData = data[93];
168
+ const edgeCount = this.getDataValue(data, 93);
169
+
170
+ const shape = new Polyline();
171
+
172
+ for (let edgeNum = 0; edgeNum < edgeCount; edgeNum++) {
173
+ if (data.hasOwnProperty('72')) {
174
+ // DXF Groupcode 72 - Edge type (only if boundary is not a polyline):
175
+ // 1 = Line; 2 = Circular arc; 3 = Elliptic arc; 4 = Spline
176
+ // const edgeTypeData = data[72];
177
+ const edgeType = this.getDataValue(data, 72);
178
+
179
+ if (isPolyline) {
180
+ // Polyline
181
+ // 72 - Has Bulge Flag; 73 - Is Closed Flag; 93 - Number of vertices; 10 - X; 20 - Y; 42 - Bulge
182
+ const polyPoint = points.shift();
183
+ if (polyPoint.sequence !== undefined && polyPoint.sequence !== 10) {
184
+ const msg = 'Invalid point sequence';
185
+ const err = (`${this.type} - ${msg}`);
186
+ throw Error(err);
187
+ }
188
+
189
+ shape.points.push(polyPoint);
190
+ } else if (edgeType === 1) {
191
+ // Line
192
+ // 10 - X; 20 - Y; 11 - X; 21 - Y
193
+ const shapePoints = [];
194
+ const startPoint = points.shift();
195
+ if (startPoint.sequence !== undefined && startPoint.sequence !== 10) {
196
+ const msg = 'Invalid point sequence';
197
+ const err = (`${this.type} - ${msg}`);
198
+ throw Error(err);
199
+ }
200
+
201
+ const endPoint = points.shift();
202
+ if (endPoint.sequence !== undefined && endPoint.sequence !== 11) {
203
+ const msg = 'Invalid point sequence';
204
+ const err = (`${this.type} - ${msg}`);
205
+ throw Error(err);
206
+ }
207
+
208
+ shapePoints.push(startPoint);
209
+ shapePoints.push(endPoint);
210
+ const line = new Line({ points: shapePoints });
211
+ shape.points.push(...line.toPolylinePoints());
212
+ } else if (edgeType === 2) {
213
+ // ARC
214
+ // 10 - X; 20 - Y; 40 - Radius; 50 - Start Angle; 51 - End Angle; 73 - Is Counter Clockwise
215
+ const shapeData = { points: [] };
216
+
217
+ const centerPoint = points.shift();
218
+ if (centerPoint.sequence !== undefined && centerPoint.sequence !== 10) {
219
+ const msg = 'Invalid point sequence';
220
+ const err = (`${this.type} - ${msg}`);
221
+ throw Error(err);
222
+ }
223
+ shapeData.points.push(centerPoint);
224
+ shapeData[40] = this.getDataValue(data, 40);
225
+ shapeData.startAngle = this.getDataValue(data, 50);
226
+ shapeData.endAngle = this.getDataValue(data, 51);
227
+ // arc direction: - ccw > 0, cw <= 0 default 1
228
+ shapeData.direction = this.getDataValue(data, 73);
229
+
230
+ if (shapeData.direction === 0) {
231
+ const circle = 360;
232
+ shapeData.startAngle = circle - shapeData.startAngle;
233
+ shapeData.endAngle = circle - shapeData.endAngle;
234
+ }
235
+
236
+ const arc = new Arc(shapeData);
237
+ shape.points.push(...arc.toPolylinePoints());
238
+ } else if (edge.edgeType === 3) {
239
+ // Ellipse
240
+ // 10 - X; 20 - Y; 11 - X; 21 - Y; 40 - Length of minor axis (percentage of major axis length);
241
+ // 50 - Start angle; 51 - End angle; 73 - Is counterclockwise flag
242
+ const msg = 'Ellipse not supported';
243
+ const err = (`${this.type} - ${msg}`);
244
+ throw Error(err);
245
+ } else if (edgeType === 4) {
246
+ // Spline
247
+ // 94 - Degree; 73 - Rational; 74 - Periodic; 95 - Number of knots; 96 - Number of control points; 40 - Knot values (multiple entries)
248
+ // 10 - X of Control Point; 20 - Y of Control Point; 42 - Weights (optional, default = 1); 97 - Number of fit data
249
+ // 11 - X of Fit datum; 21 - Y of Fit datum; 12 - X of Start Tangent; 22 - Y of Start Tangent
250
+ // 13 - X of End tangent; 23 - Y of End tangent
251
+ const msg = 'Spline not supported';
252
+ const err = (`${this.type} - ${msg}`);
253
+ throw Error(err);
254
+ }
255
+ }
256
+ }
257
+
258
+ childEntities.push(shape);
259
+ }
260
+ }
261
+ }
262
+
263
+ return childEntities;
264
+ }
265
+
266
+
267
+ /**
268
+ * Get value from incoming data
269
+ * handle arrays and single values
270
+ * @param {any} data
271
+ * @param {number} dxfCode
272
+ * @return {any}
273
+ */
274
+ getDataValue(data, dxfCode) {
275
+ let value;
276
+ if (Array.isArray(data[dxfCode])) {
277
+ value = data[dxfCode].shift();
278
+ } else {
279
+ value = data[dxfCode];
280
+ }
281
+ return value;
282
+ }
283
+
284
+ /**
285
+ * Register the command
286
+ * @return {Object}
287
+ * command = name of the command
288
+ * shortcut = shortcut for the command
289
+ * type = type to group command in toolbars (omitted if not shown)
290
+ */
291
+ static register() {
292
+ const command = { command: 'Hatch', shortcut: 'H' };
293
+ return command;
294
+ }
295
+
296
+ /**
297
+ * Execute method
298
+ * executes the workflow, requesting input required to create an entity
299
+ */
300
+ async execute() {
301
+ try {
302
+ const op = new PromptOptions(Strings.Input.SELECTIONSET, [Input.Type.SELECTIONSET]);
303
+ let validBoundary = false;
304
+
305
+ while (!validBoundary) {
306
+ const result = await DesignCore.Scene.inputManager.requestInput(op);
307
+ if (result === undefined) return;
308
+
309
+ const selectedItems = DesignCore.Scene.selectionManager.selectedItems.slice(0);
310
+ const boundary = this.processSelection(selectedItems);
311
+
312
+ if (boundary.length) {
313
+ this.childEntities = boundary;
314
+ validBoundary = true;
315
+ } else {
316
+ // reset selection
317
+ DesignCore.Scene.selectionManager.reset();
318
+ const msg = `${this.type} - ${Strings.Error.SELECTION}`;
319
+ DesignCore.Core.notify(msg);
320
+ }
321
+ }
322
+
323
+ DesignCore.Scene.inputManager.executeCommand(this);
324
+ } catch (err) {
325
+ Logging.instance.error(`${this.type} - ${err}`);
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Preview the entity during creation
331
+ */
332
+ preview() {
333
+ const selectedItems = DesignCore.Scene.selectionManager.selectedItems.slice(0);
334
+ const shapes = this.processSelection(selectedItems);
335
+ if (shapes.length) {
336
+ DesignCore.Scene.tempEntities.create(this.type, { points: this.points, childEntities: shapes });
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Convert the selection to boundary items
342
+ * @param {Array} selectedItems
343
+ * @return {Array} - Array of boundary items
344
+ */
345
+ processSelection(selectedItems) {
346
+ const selectedchildEntities = [];
347
+
348
+ let iterationPoints = [];
349
+
350
+ // track the last index count to make sure the loop is closed
351
+ let lastIndexCount = selectedItems.length + 1;
352
+
353
+ while (selectedItems.length) {
354
+ if (selectedItems.length === lastIndexCount) {
355
+ return [];
356
+ }
357
+
358
+ lastIndexCount = selectedItems.length;
359
+
360
+ for (let i = 0; i < selectedItems.length; i++) {
361
+ // no points collected - get the first index from selected items
362
+
363
+ const currentItem = selectedItems[i];
364
+ // if the item can't be converted to polyline points, remove from selected items and go again
365
+ if (typeof currentItem.toPolylinePoints === 'undefined' || currentItem.type === 'Text' || currentItem.type === 'ArcAlignedText') {
366
+ selectedItems.splice(i, 1);
367
+ break;
368
+ }
369
+
370
+ let currentPoints = currentItem.toPolylinePoints();
371
+
372
+ // check if the start or end point of the item are connected to the end of the iteration points
373
+ if (!iterationPoints.length || currentPoints.at(0).isSame(iterationPoints.at(-1)) || currentPoints.at(-1).isSame(iterationPoints.at(-1))) {
374
+ // check if the item is reversed
375
+ if (iterationPoints.length && currentPoints.at(-1).isSame(iterationPoints.at(-1))) {
376
+ currentPoints = currentPoints.reverse();
377
+
378
+ // Reverse the bulge for arcs
379
+ const startBulge = currentPoints.at(0).bulge;
380
+ const endBulge = currentPoints.at(-1).bulge;
381
+
382
+ currentPoints.at(0).bulge = endBulge * -1;
383
+ currentPoints.at(-1).bulge = startBulge * -1;
384
+ }
385
+
386
+ // Merge the shared connection point rather than pushing a duplicate.
387
+ // Arc.toPolylinePoints() stores the outgoing arc bulge on its start
388
+ // point; the previous entity left that slot with bulge=0 (destination
389
+ // only). Transfer the bulge and skip the duplicate so that:
390
+ // - arc geometry is preserved exactly
391
+ // - floating-point trig differences (e.g. Math.cos(3π/2) ≈ -1.8e-16
392
+ // instead of 0) cannot produce a spurious near-zero segment
393
+ if (iterationPoints.length && currentPoints[0].isSame(iterationPoints.at(-1))) {
394
+ if (currentPoints[0].bulge !== 0) {
395
+ iterationPoints.at(-1).bulge = currentPoints[0].bulge;
396
+ }
397
+ iterationPoints.push(...currentPoints.slice(1));
398
+ } else {
399
+ iterationPoints.push(...currentPoints);
400
+ }
401
+
402
+ // remove the index from selected items
403
+ selectedItems = selectedItems.filter((index) => index !== selectedItems[i]);
404
+
405
+ if (iterationPoints.at(0).isSame(iterationPoints.at(-1))) {
406
+ const shape = new Polyline();
407
+ // Drop the redundant closure point — the first point already records
408
+ // the same position; keeping it would create a zero-length segment
409
+ shape.points.push(...iterationPoints.slice(0, -1));
410
+ selectedchildEntities.push(shape);
411
+ iterationPoints = [];
412
+ break;
413
+ }
414
+ }
415
+ }
416
+ }
417
+
418
+ return selectedchildEntities;
419
+ }
420
+
421
+ /**
422
+ * Draw the entity
423
+ * @param {Object} ctx - context
424
+ * @param {number} scale
425
+ */
426
+ draw(ctx, scale) {
427
+ // ensure the scale is valid
428
+ if (this.scale < 0.01) {
429
+ this.scale = 1;
430
+ }
431
+
432
+ if (!this.childEntities.length) return;
433
+
434
+ ctx.save();
435
+
436
+ // Start a new composite path for all boundary loops
437
+ try {
438
+ ctx.beginPath();
439
+ } catch { // Cairo
440
+ ctx.newPath();
441
+ }
442
+
443
+ // Trace all boundary paths as sub-paths in a single composite path
444
+ // Using even-odd fill rule, islands are automatically handled:
445
+ // areas enclosed by an odd number of boundaries are filled,
446
+ // areas enclosed by an even number are left empty
447
+ for (let i = 0; i < this.childEntities.length; i++) {
448
+ const shape = this.childEntities[i];
449
+ if (!shape.points.length) continue;
450
+
451
+ // Start a new sub-path for this boundary
452
+ ctx.moveTo(shape.points[0].x, shape.points[0].y);
453
+ // Trace the boundary path (without stroking)
454
+ shape.draw(ctx, scale, false);
455
+ // Close the sub-path
456
+ ctx.closePath();
457
+ }
458
+
459
+ // Clip using even-odd rule
460
+ try { // Cairo - clip() takes no arguments, uses the fill rule set above
461
+ ctx.setFillRule(1); // Cairo.FillRule.EVEN_ODD
462
+ ctx.clip();
463
+ } catch { // HTML Canvas
464
+ ctx.clip('evenodd');
465
+ }
466
+
467
+ // Start a new path for the pattern/fill area
468
+ try {
469
+ ctx.beginPath();
470
+ } catch { // Cairo
471
+ ctx.newPath();
472
+ }
473
+
474
+ // Create an oversized rectangle covering all boundaries
475
+ const bb = this.boundingBox();
476
+ try {
477
+ ctx.rect(bb.xMin - bb.xLength, bb.yMin - bb.yLength, bb.xLength * 3, bb.yLength * 3);
478
+ } catch { // Cairo
479
+ ctx.rectangle(bb.xMin - bb.xLength, bb.yMin - bb.yLength, bb.xLength * 3, bb.yLength * 3);
480
+ }
481
+
482
+ // Draw the pattern/fill once over the composite clipped region
483
+ this.createPattern(ctx, scale, bb);
484
+
485
+ ctx.restore();
486
+ }
487
+
488
+ /**
489
+ * Draw the hatch pattern to the context
490
+ * @param {Object} ctx
491
+ * @param {number} scale
492
+ * @param {BoundingBox} boundingbox - bounding box of the hatch boundaries, used to size the pattern
493
+ */
494
+ createPattern(ctx, scale, boundingbox) {
495
+ if (!Patterns.patternExists(this.patternName) || this.solid) {
496
+ ctx.fill();
497
+ return;
498
+ }
499
+
500
+ const boundingBox = boundingbox;
501
+ const centerPoint = boundingBox.centerPoint;
502
+ const bbXLength = boundingBox.xLength;
503
+ const bbYLength = boundingBox.yLength;
504
+
505
+ const pattern = Patterns.getPattern(this.patternName);
506
+ pattern.forEach((patternLine) => {
507
+ // Each pattern line is considered to be the first member of a line family,
508
+ // Patterns are created by applying the delta offsets in both directions to generate an infinite family of parallel lines.
509
+ // originX and originY values are the offsets of the line from the origin and are applied without rotation
510
+ // The delta-x value indicates the displacement between members of the family in the direction of the line. It is used only for dashed lines.
511
+ // The delta-y value indicates the spacing between members of the family; that is, it is measured perpendicular to the lines.
512
+ // A line is considered to be of infinite length.
513
+ // A dash pattern is superimposed on the line.
514
+
515
+ // define dashlength - i.e. the length of each dash summed
516
+ // where there is no dash defined use half the boundingbox xlength
517
+ let dashLength = bbXLength / 2;
518
+ if (patternLine.dashes.length) {
519
+ dashLength = patternLine.getDashLength();
520
+ }
521
+
522
+ // copy dashes to avoid mutating the pattern line
523
+ let dashes = [...patternLine.dashes];
524
+ let dashOffset = 0;
525
+
526
+ if (dashes[0] < 0) {
527
+ // if the first dash is negative move it to the end
528
+ const firstIndex = dashes.shift();
529
+ dashOffset = Math.abs(firstIndex);
530
+ dashes.push(firstIndex);
531
+ }
532
+
533
+ dashes = dashes.map((x) => Math.abs(x) + 0.00001);
534
+
535
+ try {
536
+ ctx.setLineDash(dashes, 0);
537
+ ctx.lineWidth = (1 / scale / this.scale);
538
+ ctx.beginPath();
539
+ } catch { // Cairo
540
+ ctx.setDash(dashes, 0);
541
+ ctx.setLineWidth(1 / scale / this.scale);
542
+ ctx.newPath();
543
+ }
544
+
545
+ const rotation = Utils.degrees2radians(patternLine.angle + this.angle);
546
+
547
+ // Project the bounding box onto the pattern's rotated local axes
548
+ // to compute tight line counts instead of using the worst-case max dimension.
549
+ const halfW = bbXLength / (2 * this.scale);
550
+ const halfH = bbYLength / (2 * this.scale);
551
+ const sinR = Math.abs(Math.sin(rotation));
552
+ const cosR = Math.abs(Math.cos(rotation));
553
+
554
+ // Half-extent along the line direction (X in local frame)
555
+ const halfX = halfW * cosR + halfH * sinR;
556
+ // Half-extent perpendicular to lines (Y in local frame)
557
+ const halfY = halfW * sinR + halfH * cosR;
558
+
559
+ const xIncrement = Math.ceil(halfX / dashLength);
560
+ const yIncrement = Math.ceil(halfY / Math.abs(patternLine.yDelta));
561
+
562
+ // Set transform once for all lines in this pattern line family
563
+ ctx.save();
564
+ // translate to the center of the shape
565
+ // apply the origin offsets without rotation
566
+ ctx.translate(centerPoint.x + patternLine.xOrigin * this.scale, centerPoint.y + patternLine.yOrigin * this.scale);
567
+ // scale the context
568
+ ctx.scale(this.scale, this.scale);
569
+ // rotate the context
570
+ ctx.rotate(rotation);
571
+
572
+ for (let i = -yIncrement; i < yIncrement; i++) {
573
+ // apply the deltaX offset for odd iterations
574
+ // define offsets for the current iteration
575
+ const xOffset = patternLine.xDelta * Math.abs(i) + dashLength * xIncrement;
576
+ const yOffset = patternLine.yDelta * i;
577
+
578
+ ctx.moveTo(-xOffset + dashOffset, yOffset);
579
+ ctx.lineTo(xOffset, yOffset);
580
+ }
581
+
582
+ ctx.stroke();
583
+ ctx.restore();
584
+ });
585
+ }
586
+
587
+ /**
588
+ * Write the entity to file in the dxf format
589
+ * @param {DXFFile} file
590
+ */
591
+ dxf(file) {
592
+ // skip if no boundary shapes
593
+ if (!this.childEntities.length) {
594
+ return;
595
+ }
596
+
597
+ file.writeGroupCode('0', 'HATCH');
598
+ file.writeGroupCode('5', this.handle, DXFFile.Version.R2000); // Handle
599
+ file.writeGroupCode('100', 'AcDbEntity', DXFFile.Version.R2000);
600
+ file.writeGroupCode('8', this.layer);
601
+ file.writeGroupCode('100', 'AcDbHatch', DXFFile.Version.R2000);
602
+ file.writeGroupCode('10', '0.0');
603
+ file.writeGroupCode('20', '0.0');
604
+ file.writeGroupCode('30', '0.0');
605
+
606
+ file.writeGroupCode('210', '0.0'); // Extrusion Direction X
607
+ file.writeGroupCode('220', '0.0'); // Extrusion Direction Y
608
+ file.writeGroupCode('230', '1.0'); // Extrusion Direction Z
609
+
610
+ file.writeGroupCode('2', this.patternName); // Hatch pattern name
611
+ file.writeGroupCode('70', this.solid ? 1 : 0); // Solid Fill Flag (1 = solid, 0 = pattern)
612
+ file.writeGroupCode('71', '0'); // Associativity flag (associative = 1; non-associative = 0); for MPolygon, solid-fill flag (has solidfill = 1; lacks solid fill = 0)
613
+
614
+
615
+ file.writeGroupCode('91', this.childEntities.length); // Number of boundary path loops
616
+
617
+ for (let i = 0; i < this.childEntities.length; i++) {
618
+ const shape = this.childEntities[i];
619
+ file.writeGroupCode('92', '7'); // Boundary path type flag (bit coded): 0 = Default; 1 = External; 2 = Polyline 4 = Derived; 8 = Textbox; 16 = Outermost
620
+ file.writeGroupCode('72', '1'); // Edge type (only if boundary is not a polyline): 1 = Line; 2 = Circular arc; 3 = Elliptic arc; 4 = Spline
621
+ file.writeGroupCode('73', '1'); // For MPolygon, boundary annotation flag (boundary is an annotated boundary = 1; boundary is not an annotated boundary = 0)
622
+ file.writeGroupCode('93', shape.points.length); // Number of edges in this boundary path / number of points in polyline
623
+
624
+
625
+ for (let j = 0; j < shape.points.length; j++) {
626
+ file.writeGroupCode('10', shape.points[j].x); // X
627
+ file.writeGroupCode('20', shape.points[j].y); // Y
628
+ file.writeGroupCode('42', shape.points[j].bulge);
629
+ }
630
+
631
+ file.writeGroupCode('97', '0'); // Number of source boundary objects
632
+ }
633
+ file.writeGroupCode('75', '1'); // Hatch style: 0 = Hatch “odd parity” area (Normal style) 1 = Hatch outermost area only (Outer style) 2 = Hatch through entire area (Ignore style)
634
+ file.writeGroupCode('76', '1'); // Hatch pattern type: 0 = User-defined; 1 = Predefined; 2 = Custom
635
+
636
+ if (!this.solid) {
637
+ file.writeGroupCode('52', this.angle); // Hatch Pattern angle
638
+ file.writeGroupCode('41', this.scale); // Hatch Pattern scale
639
+ file.writeGroupCode('77', '0'); // Hatch pattern double flag(pattern fill only): 0 = not double; 1 = double
640
+ file.writeGroupCode('78', Patterns.getPatternLineCount(this.patternName)); // Number of pattern definition lines
641
+
642
+ // Pattern data
643
+ const pattern = Patterns.getPattern(this.patternName);
644
+ pattern.forEach((patternLine) => {
645
+ file.writeGroupCode('53', patternLine.angle + this.angle); // Pattern line angle
646
+ file.writeGroupCode('43', patternLine.xOrigin); // Pattern line base X
647
+ file.writeGroupCode('44', patternLine.yOrigin); // Pattern line base y
648
+ const deltaPoint = new Point(patternLine.xDelta * this.scale, patternLine.yDelta * this.scale);
649
+ const rotatedDelta = deltaPoint.rotate(new Point(), Utils.degrees2radians(patternLine.angle + this.angle));
650
+ file.writeGroupCode('45', rotatedDelta.x); // Pattern line offset x
651
+ file.writeGroupCode('46', rotatedDelta.y); // Pattern line offset y
652
+ file.writeGroupCode('79', patternLine.dashes.length); // Number of dash length items
653
+ patternLine.dashes.forEach((dash) => {
654
+ file.writeGroupCode('49', dash * this.scale); // Dash length
655
+ });
656
+ });
657
+ }
658
+
659
+ file.writeGroupCode('47', '0.5'); // pixel size
660
+ file.writeGroupCode('98', '1'); // Number of seed points
661
+ // seed points
662
+ file.writeGroupCode('10', '1');
663
+ file.writeGroupCode('20', '1');
664
+ }
665
+
666
+ /**
667
+ * Get snap points
668
+ * @param {Point} mousePoint
669
+ * @param {number} delta
670
+ * @return {Array} - array of snap points
671
+ */
672
+ snaps(mousePoint, delta) {
673
+ const snaps = [];
674
+ return snaps;
675
+ }
676
+
677
+ /**
678
+ * Get closest point on entity
679
+ * @param {Point} P
680
+ * @return {Array} - [Point, distance]
681
+ */
682
+ closestPoint(P) {
683
+ if (this.isInside(P)) {
684
+ return [P, 0];
685
+ }
686
+ return [P, Infinity];
687
+ }
688
+
689
+ /**
690
+ * Determine if point is inside the hatch
691
+ * @param {Point} P
692
+ * @return {boolean} - true if inside
693
+ */
694
+ isInside(P) {
695
+ // P = P.subtract(this.points[0]);
696
+ for (let i = 0; i < this.childEntities.length; i++) {
697
+ const shape = this.childEntities[i];
698
+
699
+ if (shape.boundingBox().isInside(P)) {
700
+ const polyline = [...shape.points];
701
+
702
+ // check the polyline is closed
703
+ if (!shape.points.at(0).isSame(shape.points.at(-1))) {
704
+ polyline.push(shape.points.at(0));
705
+ }
706
+
707
+ // create a line from P, twice the length of the bounding box
708
+ const line = [P, new Point(P.x + shape.boundingBox().xLength, P.y)];
709
+
710
+ const intersect = Intersection.intersectPolylinePolyline(polyline, line);
711
+ const intersects = intersect.points.length;
712
+ // P is inside shape if there is a odd number of intersects
713
+ if (Math.abs(intersects % 2) == 1) {
714
+ return true;
715
+ }
716
+ }
717
+ }
718
+
719
+ return false;
720
+ }
721
+
722
+ /**
723
+ * Return boundingbox for entity
724
+ * @return {BoundingBox}
725
+ */
726
+ boundingBox() {
727
+ if (this.childEntities.length === 0) {
728
+ return new BoundingBox();
729
+ }
730
+
731
+ let xmin = Infinity;
732
+ let xmax = -Infinity;
733
+ let ymin = Infinity;
734
+ let ymax = -Infinity;
735
+
736
+ for (let i = 0; i < this.childEntities.length; i++) {
737
+ const shape = this.childEntities[i];
738
+
739
+ const boundingBox = shape.boundingBox();
740
+
741
+ xmin = Math.min(xmin, boundingBox.xMin);
742
+ xmax = Math.max(xmax, boundingBox.xMax);
743
+ ymin = Math.min(ymin, boundingBox.yMin);
744
+ ymax = Math.max(ymax, boundingBox.yMax);
745
+ }
746
+
747
+ const topLeft = new Point(xmin, ymax);
748
+ const bottomRight = new Point(xmax, ymin);
749
+
750
+ return new BoundingBox(topLeft, bottomRight);
751
+ }
752
+
753
+ /**
754
+ * Determine if the entity is touch the selection window
755
+ * @param {Object} selection - {min: Point, max: Point}
756
+ * @return {boolean} true if touched
757
+ */
758
+ touched(selection) {
759
+ for (let i = 0; i < this.childEntities.length; i++) {
760
+ if (this.childEntities[i].touched(selection)) {
761
+ return true;
762
+ }
763
+ }
764
+
765
+ return false;
766
+ }
767
+
768
+ /**
769
+ * Set a property if it exists
770
+ * @param {string} property
771
+ * @param {any} value
772
+ */
773
+ setProperty(property, value) {
774
+ if (this.hasOwnProperty(property)) {
775
+ if (property === 'points') {
776
+ // Special handling for hatch points to move child entities
777
+ // Consider the changes from the hatch points to be an offset and rotation
778
+ const ang = value[0].angle(value.at(-1));
779
+ const theta = ang - this.points[0].angle(this.points.at(-1));
780
+
781
+ if (theta !== 0) {
782
+ // set rotation center
783
+ const center = this.points[0];
784
+ // apply rotation to child entities points
785
+ this.childEntities.forEach((child) => {
786
+ const rotatedPoints = child.points.map((p) => new Point(p.x, p.y, p.bulge, p.sequence).rotate(center, theta));
787
+ child.setProperty('points', rotatedPoints);
788
+ });
789
+
790
+ this.angle+= Utils.radians2degrees(theta);
791
+ }
792
+
793
+ const delta = value[0].subtract(this.points[0]);
794
+ if (delta.x !== 0 || delta.y !== 0) {
795
+ // apply translation to child entities points
796
+ this.childEntities.forEach((child) => {
797
+ const offsetPoints = child.points.map((p) => new Point(p.x, p.y, p.bulge, p.sequence).add(delta));
798
+ child.setProperty('points', offsetPoints);
799
+ });
800
+ }
801
+ }
802
+
803
+ // other properties as normal
804
+ this[property] = value;
805
+ }
806
+ }
807
+ }