ofs-dxf-ts 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/API.md +411 -0
  2. package/CHANGELOG.md +26 -0
  3. package/LICENSE +21 -0
  4. package/README.md +119 -0
  5. package/dist/core/dxf-builder.d.ts +348 -0
  6. package/dist/core/dxf-builder.js +549 -0
  7. package/dist/core/dxf-document.d.ts +220 -0
  8. package/dist/core/dxf-document.js +529 -0
  9. package/dist/core/dxf-entities.d.ts +327 -0
  10. package/dist/core/dxf-entities.js +22 -0
  11. package/dist/core/dxf-serializer.d.ts +48 -0
  12. package/dist/core/dxf-serializer.js +595 -0
  13. package/dist/core/hatch-patterns.d.ts +15 -0
  14. package/dist/core/hatch-patterns.js +214 -0
  15. package/dist/core/modelspace.d.ts +106 -0
  16. package/dist/core/modelspace.js +351 -0
  17. package/dist/geometry/math.utils.d.ts +8 -0
  18. package/dist/geometry/math.utils.js +43 -0
  19. package/dist/geometry/offset.utils.d.ts +32 -0
  20. package/dist/geometry/offset.utils.js +87 -0
  21. package/dist/geometry/polyline.utils.d.ts +10 -0
  22. package/dist/geometry/polyline.utils.js +63 -0
  23. package/dist/geometry/text.utils.d.ts +15 -0
  24. package/dist/geometry/text.utils.js +28 -0
  25. package/dist/index.d.ts +23 -0
  26. package/dist/index.js +29 -0
  27. package/dist/parser/auto-cad-color-index.d.ts +8 -0
  28. package/dist/parser/auto-cad-color-index.js +265 -0
  29. package/dist/parser/dim-style-codes.d.ts +5 -0
  30. package/dist/parser/dim-style-codes.js +32 -0
  31. package/dist/parser/dxf-parser.d.ts +16 -0
  32. package/dist/parser/dxf-parser.js +824 -0
  33. package/dist/parser/entities/3dface.d.ts +5 -0
  34. package/dist/parser/entities/3dface.js +76 -0
  35. package/dist/parser/entities/arc.d.ts +5 -0
  36. package/dist/parser/entities/arc.js +34 -0
  37. package/dist/parser/entities/attdef.d.ts +5 -0
  38. package/dist/parser/entities/attdef.js +86 -0
  39. package/dist/parser/entities/attribute.d.ts +5 -0
  40. package/dist/parser/entities/attribute.js +106 -0
  41. package/dist/parser/entities/circle.d.ts +5 -0
  42. package/dist/parser/entities/circle.js +40 -0
  43. package/dist/parser/entities/dimension.d.ts +5 -0
  44. package/dist/parser/entities/dimension.js +65 -0
  45. package/dist/parser/entities/ellipse.d.ts +5 -0
  46. package/dist/parser/entities/ellipse.js +42 -0
  47. package/dist/parser/entities/hatch.d.ts +5 -0
  48. package/dist/parser/entities/hatch.js +321 -0
  49. package/dist/parser/entities/insert.d.ts +5 -0
  50. package/dist/parser/entities/insert.js +52 -0
  51. package/dist/parser/entities/line.d.ts +5 -0
  52. package/dist/parser/entities/line.js +30 -0
  53. package/dist/parser/entities/lwpolyline.d.ts +5 -0
  54. package/dist/parser/entities/lwpolyline.js +101 -0
  55. package/dist/parser/entities/mtext.d.ts +5 -0
  56. package/dist/parser/entities/mtext.js +54 -0
  57. package/dist/parser/entities/point.d.ts +5 -0
  58. package/dist/parser/entities/point.js +30 -0
  59. package/dist/parser/entities/polyline.d.ts +5 -0
  60. package/dist/parser/entities/polyline.js +85 -0
  61. package/dist/parser/entities/solid.d.ts +5 -0
  62. package/dist/parser/entities/solid.js +35 -0
  63. package/dist/parser/entities/spline.d.ts +5 -0
  64. package/dist/parser/entities/spline.js +69 -0
  65. package/dist/parser/entities/text.d.ts +5 -0
  66. package/dist/parser/entities/text.js +47 -0
  67. package/dist/parser/entities/vertex.d.ts +5 -0
  68. package/dist/parser/entities/vertex.js +59 -0
  69. package/dist/parser/extended-data-parser.d.ts +18 -0
  70. package/dist/parser/extended-data-parser.js +100 -0
  71. package/dist/parser/index.d.ts +10 -0
  72. package/dist/parser/index.js +9 -0
  73. package/dist/parser/log.d.ts +14 -0
  74. package/dist/parser/log.js +39 -0
  75. package/dist/parser/parse-helpers.d.ts +23 -0
  76. package/dist/parser/parse-helpers.js +128 -0
  77. package/dist/parser/scanner.d.ts +29 -0
  78. package/dist/parser/scanner.js +143 -0
  79. package/dist/parser/types.d.ts +78 -0
  80. package/dist/parser/types.js +5 -0
  81. package/dist/symbols/slope-symbol.d.ts +21 -0
  82. package/dist/symbols/slope-symbol.js +81 -0
  83. package/dist/symbols/structures.d.ts +9 -0
  84. package/dist/symbols/structures.js +120 -0
  85. package/dist/template/blank-document.d.ts +13 -0
  86. package/dist/template/blank-document.js +13 -0
  87. package/dist/template/block-registry.d.ts +16 -0
  88. package/dist/template/block-registry.js +135 -0
  89. package/dist/template/index.d.ts +17 -0
  90. package/dist/template/index.js +17 -0
  91. package/dist/template/template-injector.d.ts +45 -0
  92. package/dist/template/template-injector.js +487 -0
  93. package/dist/template/template-reader.d.ts +28 -0
  94. package/dist/template/template-reader.js +80 -0
  95. package/dist/types/index.d.ts +24 -0
  96. package/dist/types/index.js +8 -0
  97. package/package.json +59 -0
@@ -0,0 +1,595 @@
1
+ /**
2
+ * DXF Format Serializer.
3
+ * Converts internal entity representations and table records to DXF tag text.
4
+ * Section assembly (tables, blocks, objects, handle seeding) lives in
5
+ * template/template-injector, which serves both template and blank documents.
6
+ */
7
+ import { HATCH_PATTERNS } from './hatch-patterns.js';
8
+ let handleCounter = 1000;
9
+ export function nextHandle() {
10
+ return (handleCounter++).toString(16).toUpperCase();
11
+ }
12
+ export function resetHandleCounter(start = 1000) {
13
+ handleCounter = start;
14
+ }
15
+ /** The handle `nextHandle()` will return next — the value to store in $HANDSEED. */
16
+ export function peekHandle() {
17
+ return handleCounter.toString(16).toUpperCase();
18
+ }
19
+ /** Format a computed number for DXF: no exponent notation, no negative zero. */
20
+ export function fmt(n) {
21
+ if (!Number.isFinite(n) || Math.abs(n) < 1e-12)
22
+ return '0';
23
+ const s = String(n);
24
+ return s.includes('e') ? n.toFixed(12).replace(/\.?0+$/, '') : s;
25
+ }
26
+ // ─── Entity Serializer ───
27
+ function serializeEntity(entity, ctx = {}) {
28
+ switch (entity.type) {
29
+ case 'LINE': return serializeLine(entity, ctx);
30
+ case 'LWPOLYLINE': return serializeLwPolyline(entity, ctx);
31
+ case 'POLYLINE': return serializePolyline(entity, ctx);
32
+ case 'CIRCLE': return serializeCircle(entity, ctx);
33
+ case 'ARC': return serializeArc(entity, ctx);
34
+ case 'ELLIPSE': return serializeEllipse(entity, ctx);
35
+ case 'TEXT': return serializeText(entity, ctx);
36
+ case 'MTEXT': return serializeMText(entity, ctx);
37
+ case 'INSERT': return serializeInsert(entity, ctx);
38
+ case 'POINT': return serializePoint(entity, ctx);
39
+ case 'SOLID': return serializeSolid(entity, ctx);
40
+ case 'TRACE': return serializeTrace(entity, ctx);
41
+ case 'SPLINE': return serializeSpline(entity, ctx);
42
+ case 'HATCH': return serializeHatch(entity, ctx);
43
+ case 'DIMENSION': return serializeDimension(entity, ctx);
44
+ case 'LEADER': return serializeLeader(entity, ctx);
45
+ case 'VIEWPORT': return serializeViewport(entity, ctx);
46
+ case 'IMAGE': return serializeImage(entity, ctx);
47
+ case 'WIPEOUT': return serializeWipeout(entity, ctx);
48
+ case 'REGION': return serializeRegion(entity, ctx);
49
+ case 'MESH': return serializeMesh(entity, ctx);
50
+ case 'MLEADER': return serializeMLeader(entity, ctx);
51
+ default: return '';
52
+ }
53
+ }
54
+ function entityCommon(entity, ctx, handle = entity.handle ?? nextHandle()) {
55
+ const lines = ['5', handle];
56
+ if (ctx.owner)
57
+ lines.push('330', ctx.owner);
58
+ lines.push('100', 'AcDbEntity');
59
+ if (ctx.paperSpace)
60
+ lines.push('67', '1');
61
+ lines.push('8', entity.layer);
62
+ if (entity.linetype && entity.linetype.toUpperCase() !== 'BYLAYER') {
63
+ lines.push('6', entity.linetype);
64
+ }
65
+ if (entity.color !== undefined && entity.color !== 256) {
66
+ lines.push('62', String(entity.color));
67
+ }
68
+ if (entity.lineweight !== undefined) {
69
+ lines.push('370', String(entity.lineweight));
70
+ }
71
+ return lines;
72
+ }
73
+ /** Single-line DXF values cannot contain line breaks. */
74
+ function oneLine(text) {
75
+ return text.replace(/\r?\n/g, ' ');
76
+ }
77
+ function serializeLine(e, ctx) {
78
+ const lines = ['0', 'LINE', ...entityCommon(e, ctx)];
79
+ lines.push('100', 'AcDbLine');
80
+ lines.push('10', String(e.start[0]));
81
+ lines.push('20', String(e.start[1]));
82
+ lines.push('30', '0');
83
+ lines.push('11', String(e.end[0]));
84
+ lines.push('21', String(e.end[1]));
85
+ lines.push('31', '0');
86
+ return lines.join('\n');
87
+ }
88
+ function serializeLwPolyline(e, ctx) {
89
+ const lines = ['0', 'LWPOLYLINE', ...entityCommon(e, ctx)];
90
+ lines.push('100', 'AcDbPolyline');
91
+ lines.push('90', String(e.points.length));
92
+ lines.push('70', e.closed ? '1' : '0');
93
+ for (const pt of e.points) {
94
+ lines.push('10', String(pt[0]));
95
+ lines.push('20', String(pt[1]));
96
+ }
97
+ return lines.join('\n');
98
+ }
99
+ function serializeCircle(e, ctx) {
100
+ const lines = ['0', 'CIRCLE', ...entityCommon(e, ctx)];
101
+ lines.push('100', 'AcDbCircle');
102
+ lines.push('10', String(e.center[0]));
103
+ lines.push('20', String(e.center[1]));
104
+ lines.push('30', '0');
105
+ lines.push('40', String(e.radius));
106
+ return lines.join('\n');
107
+ }
108
+ function serializeArc(e, ctx) {
109
+ const lines = ['0', 'ARC', ...entityCommon(e, ctx)];
110
+ lines.push('100', 'AcDbCircle');
111
+ lines.push('10', String(e.center[0]));
112
+ lines.push('20', String(e.center[1]));
113
+ lines.push('30', '0');
114
+ lines.push('40', String(e.radius));
115
+ lines.push('100', 'AcDbArc');
116
+ lines.push('50', String(e.startAngle));
117
+ lines.push('51', String(e.endAngle));
118
+ return lines.join('\n');
119
+ }
120
+ function serializeText(e, ctx) {
121
+ const lines = ['0', 'TEXT', ...entityCommon(e, ctx)];
122
+ lines.push('100', 'AcDbText');
123
+ lines.push('10', String(e.position[0]));
124
+ lines.push('20', String(e.position[1]));
125
+ lines.push('30', '0');
126
+ lines.push('40', String(e.height));
127
+ lines.push('1', oneLine(e.text));
128
+ if (e.rotation)
129
+ lines.push('50', String(e.rotation));
130
+ if (e.style && e.style !== 'Standard')
131
+ lines.push('7', e.style);
132
+ const aligned = e.halign !== 0 || e.valign !== 0;
133
+ if (aligned) {
134
+ const ap = e.alignmentPoint ?? e.position;
135
+ lines.push('72', String(e.halign));
136
+ lines.push('11', String(ap[0]), '21', String(ap[1]), '31', '0');
137
+ }
138
+ lines.push('100', 'AcDbText');
139
+ if (aligned)
140
+ lines.push('73', String(e.valign));
141
+ return lines.join('\n');
142
+ }
143
+ function serializeMText(e, ctx) {
144
+ const lines = ['0', 'MTEXT', ...entityCommon(e, ctx)];
145
+ lines.push('100', 'AcDbMText');
146
+ lines.push('10', String(e.position[0]));
147
+ lines.push('20', String(e.position[1]));
148
+ lines.push('30', '0');
149
+ lines.push('40', String(e.height));
150
+ lines.push('41', String(e.width));
151
+ lines.push('71', String(e.attachmentPoint));
152
+ // MTEXT line breaks are "\P"; content longer than 250 chars goes in code-3 chunks.
153
+ const content = e.text.replace(/\r?\n/g, '\\P');
154
+ const chunks = content.match(/[\s\S]{1,250}/g) ?? [''];
155
+ for (let i = 0; i < chunks.length - 1; i++)
156
+ lines.push('3', chunks[i]);
157
+ lines.push('1', chunks[chunks.length - 1]);
158
+ if (e.style && e.style !== 'Standard')
159
+ lines.push('7', e.style);
160
+ if (e.rotation)
161
+ lines.push('50', String(e.rotation));
162
+ return lines.join('\n');
163
+ }
164
+ function serializeInsert(e, ctx) {
165
+ const lines = ['0', 'INSERT', ...entityCommon(e, ctx)];
166
+ lines.push('100', 'AcDbBlockReference');
167
+ lines.push('2', e.blockName);
168
+ lines.push('10', String(e.position[0]));
169
+ lines.push('20', String(e.position[1]));
170
+ lines.push('30', '0');
171
+ if (e.xScale !== 1)
172
+ lines.push('41', String(e.xScale));
173
+ if (e.yScale !== 1)
174
+ lines.push('42', String(e.yScale));
175
+ if (e.zScale !== 1)
176
+ lines.push('43', String(e.zScale));
177
+ if (e.rotation)
178
+ lines.push('50', String(e.rotation));
179
+ return lines.join('\n');
180
+ }
181
+ function serializePoint(e, ctx) {
182
+ const lines = ['0', 'POINT', ...entityCommon(e, ctx)];
183
+ lines.push('100', 'AcDbPoint');
184
+ lines.push('10', String(e.position[0]));
185
+ lines.push('20', String(e.position[1]));
186
+ lines.push('30', '0');
187
+ return lines.join('\n');
188
+ }
189
+ function serializePolyline(e, ctx) {
190
+ const handle = nextHandle();
191
+ const lines = ['0', 'POLYLINE', ...entityCommon(e, ctx, handle)];
192
+ lines.push('100', 'AcDb3dPolyline');
193
+ lines.push('66', '1');
194
+ lines.push('10', '0', '20', '0', '30', '0');
195
+ lines.push('70', String(8 | (e.closed ? 1 : 0))); // 8 = 3D polyline
196
+ const parts = [lines.join('\n')];
197
+ const sub = (type) => {
198
+ const l = ['0', type, '5', nextHandle(), '330', handle, '100', 'AcDbEntity'];
199
+ if (ctx.paperSpace)
200
+ l.push('67', '1');
201
+ l.push('8', e.layer);
202
+ return l;
203
+ };
204
+ for (const pt of e.points) {
205
+ const vl = sub('VERTEX');
206
+ vl.push('100', 'AcDbVertex', '100', 'AcDb3dPolylineVertex');
207
+ vl.push('10', String(pt[0]), '20', String(pt[1]), '30', String(pt[2] || 0), '70', '32');
208
+ parts.push(vl.join('\n'));
209
+ }
210
+ parts.push(sub('SEQEND').join('\n'));
211
+ return parts.join('\n');
212
+ }
213
+ function serializeEllipse(e, ctx) {
214
+ const lines = ['0', 'ELLIPSE', ...entityCommon(e, ctx)];
215
+ lines.push('100', 'AcDbEllipse');
216
+ lines.push('10', String(e.center[0]), '20', String(e.center[1]), '30', '0');
217
+ lines.push('11', String(e.majorAxis[0]), '21', String(e.majorAxis[1]), '31', '0');
218
+ lines.push('40', String(e.ratioMinorToMajor));
219
+ lines.push('41', String(e.startParam));
220
+ lines.push('42', String(e.endParam));
221
+ return lines.join('\n');
222
+ }
223
+ function serializeSolid(e, ctx) {
224
+ const lines = ['0', 'SOLID', ...entityCommon(e, ctx)];
225
+ lines.push('100', 'AcDbTrace');
226
+ lines.push('10', String(e.points[0][0]), '20', String(e.points[0][1]), '30', '0');
227
+ lines.push('11', String(e.points[1][0]), '21', String(e.points[1][1]), '31', '0');
228
+ lines.push('12', String(e.points[2][0]), '22', String(e.points[2][1]), '32', '0');
229
+ lines.push('13', String(e.points[3][0]), '23', String(e.points[3][1]), '33', '0');
230
+ return lines.join('\n');
231
+ }
232
+ function serializeTrace(e, ctx) {
233
+ const lines = ['0', 'TRACE', ...entityCommon(e, ctx)];
234
+ lines.push('100', 'AcDbTrace');
235
+ lines.push('10', String(e.points[0][0]), '20', String(e.points[0][1]), '30', '0');
236
+ lines.push('11', String(e.points[1][0]), '21', String(e.points[1][1]), '31', '0');
237
+ lines.push('12', String(e.points[2][0]), '22', String(e.points[2][1]), '32', '0');
238
+ lines.push('13', String(e.points[3][0]), '23', String(e.points[3][1]), '33', '0');
239
+ return lines.join('\n');
240
+ }
241
+ function serializeSpline(e, ctx) {
242
+ const lines = ['0', 'SPLINE', ...entityCommon(e, ctx)];
243
+ lines.push('100', 'AcDbSpline');
244
+ lines.push('70', e.closed ? '11' : '8');
245
+ lines.push('71', String(e.degree));
246
+ lines.push('72', String(e.knots.length));
247
+ lines.push('73', String(e.controlPoints.length));
248
+ lines.push('74', String(e.fitPoints.length));
249
+ for (const k of e.knots)
250
+ lines.push('40', String(k));
251
+ if (e.weights) {
252
+ for (const w of e.weights)
253
+ lines.push('41', String(w));
254
+ }
255
+ for (const cp of e.controlPoints)
256
+ lines.push('10', String(cp[0]), '20', String(cp[1]), '30', '0');
257
+ for (const fp of e.fitPoints)
258
+ lines.push('11', String(fp[0]), '21', String(fp[1]), '31', '0');
259
+ return lines.join('\n');
260
+ }
261
+ // ─── HATCH ───
262
+ function rotateScale(p, scale, angleDeg) {
263
+ const a = angleDeg * Math.PI / 180;
264
+ const x = p[0] * scale, y = p[1] * scale;
265
+ return [x * Math.cos(a) - y * Math.sin(a), x * Math.sin(a) + y * Math.cos(a)];
266
+ }
267
+ /** Resolve a hatch's pattern lines (custom or built-in), scaled and rotated for output. */
268
+ export function hatchPatternLines(e) {
269
+ const base = e.patternLines ?? HATCH_PATTERNS[e.patternName.toUpperCase()];
270
+ if (!base) {
271
+ throw new Error(`Unknown hatch pattern "${e.patternName}". Use one of ${Object.keys(HATCH_PATTERNS).join(', ')}, ` +
272
+ `or pass patternLines.`);
273
+ }
274
+ return base.map(([angle, basePoint, offset, dashes]) => [
275
+ angle + e.angle,
276
+ rotateScale(basePoint, e.scale, e.angle),
277
+ rotateScale(offset, e.scale, e.angle),
278
+ dashes.map(d => d * e.scale),
279
+ ]);
280
+ }
281
+ function serializeHatch(e, ctx) {
282
+ const lines = ['0', 'HATCH', ...entityCommon(e, ctx)];
283
+ lines.push('100', 'AcDbHatch');
284
+ lines.push('10', '0', '20', '0', '30', '0');
285
+ lines.push('210', '0', '220', '0', '230', '1');
286
+ lines.push('2', e.solid ? 'SOLID' : e.patternName.toUpperCase());
287
+ lines.push('70', e.solid ? '1' : '0');
288
+ lines.push('71', '0'); // not associative
289
+ lines.push('91', String(e.boundaries.length));
290
+ e.boundaries.forEach((b, i) => {
291
+ lines.push('92', i === 0 ? '3' : '2'); // 2 = polyline path, 1 = external
292
+ lines.push('72', '0'); // no bulges
293
+ lines.push('73', '1'); // closed
294
+ lines.push('93', String(b.points.length));
295
+ for (const pt of b.points)
296
+ lines.push('10', String(pt[0]), '20', String(pt[1]));
297
+ lines.push('97', '0'); // no source boundary objects
298
+ });
299
+ lines.push('75', '0'); // odd parity (holes work)
300
+ lines.push('76', e.patternLines ? '2' : '1'); // 1 = predefined, 2 = custom
301
+ if (!e.solid) {
302
+ const patternLines = hatchPatternLines(e);
303
+ lines.push('52', fmt(e.angle));
304
+ lines.push('41', fmt(e.scale));
305
+ lines.push('77', '0');
306
+ lines.push('78', String(patternLines.length));
307
+ for (const [angle, base, offset, dashes] of patternLines) {
308
+ lines.push('53', fmt(angle));
309
+ lines.push('43', fmt(base[0]), '44', fmt(base[1]));
310
+ lines.push('45', fmt(offset[0]), '46', fmt(offset[1]));
311
+ lines.push('79', String(dashes.length));
312
+ for (const d of dashes)
313
+ lines.push('49', fmt(d));
314
+ }
315
+ }
316
+ lines.push('98', '0');
317
+ return lines.join('\n');
318
+ }
319
+ function serializeDimension(e, ctx) {
320
+ const lines = ['0', 'DIMENSION', ...entityCommon(e, ctx)];
321
+ lines.push('100', 'AcDbDimension');
322
+ lines.push('2', '*D0'); // block name auto-generated
323
+ lines.push('10', String(e.definitionPoint[0]), '20', String(e.definitionPoint[1]), '30', '0');
324
+ lines.push('11', String(e.textMidpoint[0]), '21', String(e.textMidpoint[1]), '31', '0');
325
+ lines.push('70', String(e.dimensionType));
326
+ if (e.text)
327
+ lines.push('1', e.text);
328
+ if (e.style)
329
+ lines.push('3', e.style);
330
+ if (e.dimensionType <= 1) {
331
+ lines.push('100', 'AcDbAlignedDimension');
332
+ if (e.point1)
333
+ lines.push('13', String(e.point1[0]), '23', String(e.point1[1]), '33', '0');
334
+ if (e.point2)
335
+ lines.push('14', String(e.point2[0]), '24', String(e.point2[1]), '34', '0');
336
+ if (e.rotation)
337
+ lines.push('50', String(e.rotation));
338
+ if (e.dimensionType === 0)
339
+ lines.push('100', 'AcDbRotatedDimension');
340
+ }
341
+ return lines.join('\n');
342
+ }
343
+ function serializeLeader(e, ctx) {
344
+ const lines = ['0', 'LEADER', ...entityCommon(e, ctx)];
345
+ lines.push('100', 'AcDbLeader');
346
+ if (e.style)
347
+ lines.push('3', e.style);
348
+ lines.push('71', e.hasArrowhead ? '1' : '0');
349
+ lines.push('72', '0'); // leader path type (0=straight)
350
+ lines.push('73', String(e.annotationType));
351
+ lines.push('76', String(e.vertices.length));
352
+ for (const v of e.vertices)
353
+ lines.push('10', String(v[0]), '20', String(v[1]), '30', '0');
354
+ return lines.join('\n');
355
+ }
356
+ // ─── VIEWPORT ───
357
+ function serializeViewport(e, ctx) {
358
+ const lines = ['0', 'VIEWPORT', ...entityCommon(e, ctx)];
359
+ lines.push('100', 'AcDbViewport');
360
+ lines.push('10', fmt(e.center[0]), '20', fmt(e.center[1]), '30', '0');
361
+ lines.push('40', fmt(e.width));
362
+ lines.push('41', fmt(e.height));
363
+ lines.push('68', String(e.status));
364
+ lines.push('69', String(e.id ?? e.status));
365
+ lines.push('12', fmt(e.viewCenter[0]), '22', fmt(e.viewCenter[1]));
366
+ lines.push('13', '0', '23', '0'); // snap base
367
+ lines.push('14', '10', '24', '10'); // snap spacing
368
+ lines.push('15', '10', '25', '10'); // grid spacing
369
+ lines.push('16', '0', '26', '0', '36', '1'); // view direction
370
+ lines.push('17', '0', '27', '0', '37', '0'); // view target
371
+ lines.push('42', '50'); // lens length
372
+ lines.push('43', '0', '44', '0'); // front/back clip
373
+ lines.push('45', fmt(e.viewHeight));
374
+ lines.push('50', '0', '51', '0'); // snap angle, twist
375
+ lines.push('72', '1000'); // circle zoom
376
+ for (const name of e.frozenLayers ?? []) {
377
+ const h = ctx.layerHandles?.get(name.toUpperCase());
378
+ if (h)
379
+ lines.push('331', h);
380
+ }
381
+ // 32768 = always set by AutoCAD; 16384 = display locked
382
+ lines.push('90', String(32768 | (e.locked ? 16384 : 0)));
383
+ lines.push('1', '');
384
+ lines.push('281', '0'); // render mode
385
+ lines.push('71', '1'); // UCS per viewport
386
+ lines.push('74', '0'); // UCS icon
387
+ lines.push('110', '0', '120', '0', '130', '0');
388
+ lines.push('111', '1', '121', '0', '131', '0');
389
+ lines.push('112', '0', '122', '1', '132', '0');
390
+ lines.push('79', '0');
391
+ lines.push('146', '0');
392
+ lines.push('170', '0'); // shade plot mode
393
+ lines.push('61', '5'); // grid frequency
394
+ lines.push('292', '1', '282', '1'); // default lighting
395
+ lines.push('141', '0', '142', '0'); // brightness, contrast
396
+ lines.push('63', '250', '421', '3355443'); // ambient light color
397
+ return lines.join('\n');
398
+ }
399
+ // ─── IMAGE ───
400
+ /** Convert a WCS point to IMAGE clip-boundary pixel coordinates (origin top-left). */
401
+ function wcsToImagePixel(e, p) {
402
+ const [ux, uy] = e.uVector, [vx, vy] = e.vVector;
403
+ // origin of pixel (0, height) grid, see DXF IMAGE clip boundary convention
404
+ const ox = e.insertionPoint[0] + ux * 0.5 - vx * 0.5;
405
+ const oy = e.insertionPoint[1] + uy * 0.5 - vy * 0.5;
406
+ const dx = p[0] - ox, dy = p[1] - oy;
407
+ const det = ux * vy - uy * vx;
408
+ const px = (dx * vy - dy * vx) / det;
409
+ const qy = (ux * dy - uy * dx) / det;
410
+ return [px, e.imageSize[1] - qy];
411
+ }
412
+ function serializeImage(e, ctx) {
413
+ const refs = ctx.images?.get(e);
414
+ if (!refs) {
415
+ throw new Error('IMAGE entities must be serialized through DxfDocument (image definition missing).');
416
+ }
417
+ const clip = e.clipBoundary && e.clipBoundary.length >= 3 ? [...e.clipBoundary] : undefined;
418
+ if (clip) {
419
+ const [f, l] = [clip[0], clip[clip.length - 1]];
420
+ if (f[0] !== l[0] || f[1] !== l[1])
421
+ clip.push(f); // AutoCAD requires a closed clip polygon
422
+ }
423
+ const lines = ['0', 'IMAGE', ...entityCommon(e, ctx, refs.handle)];
424
+ lines.push('100', 'AcDbRasterImage');
425
+ lines.push('90', '0');
426
+ lines.push('10', fmt(e.insertionPoint[0]), '20', fmt(e.insertionPoint[1]), '30', '0');
427
+ lines.push('11', fmt(e.uVector[0]), '21', fmt(e.uVector[1]), '31', '0');
428
+ lines.push('12', fmt(e.vVector[0]), '22', fmt(e.vVector[1]), '32', '0');
429
+ lines.push('13', String(e.imageSize[0]), '23', String(e.imageSize[1]));
430
+ lines.push('340', refs.defHandle);
431
+ lines.push('70', String(1 | 2 | (clip ? 4 : 0))); // show, show unaligned, use clip
432
+ lines.push('280', clip ? '1' : '0');
433
+ lines.push('281', String(e.brightness ?? 50));
434
+ lines.push('282', String(e.contrast ?? 50));
435
+ lines.push('283', String(e.fade ?? 0));
436
+ lines.push('360', refs.reactorHandle);
437
+ if (clip) {
438
+ lines.push('71', '2', '91', String(clip.length));
439
+ for (const p of clip) {
440
+ const [px, py] = wcsToImagePixel(e, p);
441
+ lines.push('14', fmt(px), '24', fmt(py));
442
+ }
443
+ }
444
+ else {
445
+ lines.push('71', '1', '91', '2');
446
+ lines.push('14', '-0.5', '24', '-0.5');
447
+ lines.push('14', fmt(e.imageSize[0] - 0.5), '24', fmt(e.imageSize[1] - 0.5));
448
+ }
449
+ lines.push('290', '0');
450
+ return lines.join('\n');
451
+ }
452
+ function serializeWipeout(e, ctx) {
453
+ const lines = ['0', 'WIPEOUT', ...entityCommon(e, ctx)];
454
+ lines.push('100', 'AcDbWipeout');
455
+ lines.push('90', '0');
456
+ lines.push('10', String(e.insertionPoint[0]), '20', String(e.insertionPoint[1]), '30', '0');
457
+ lines.push('11', String(e.uVector[0]), '21', String(e.uVector[1]), '31', '0');
458
+ lines.push('12', String(e.vVector[0]), '22', String(e.vVector[1]), '32', '0');
459
+ lines.push('13', String(e.imageSize[0]), '23', String(e.imageSize[1]));
460
+ lines.push('70', '7');
461
+ lines.push('280', '1'); // clipping on
462
+ lines.push('71', '2'); // clip boundary type (2=polygon)
463
+ lines.push('91', String(e.clipBoundary.length));
464
+ for (const pt of e.clipBoundary)
465
+ lines.push('14', String(pt[0]), '24', String(pt[1]));
466
+ return lines.join('\n');
467
+ }
468
+ function serializeRegion(e, ctx) {
469
+ const lines = ['0', 'REGION', ...entityCommon(e, ctx)];
470
+ lines.push('100', 'AcDbModelerGeometry');
471
+ lines.push('70', '1');
472
+ for (const line of e.acisData)
473
+ lines.push('1', line);
474
+ return lines.join('\n');
475
+ }
476
+ function serializeMesh(e, ctx) {
477
+ const lines = ['0', 'MESH', ...entityCommon(e, ctx)];
478
+ lines.push('100', 'AcDbSubDMesh');
479
+ lines.push('71', '2'); // version
480
+ lines.push('72', String(e.subdivisionLevel));
481
+ lines.push('93', String(e.vertices.length));
482
+ for (const v of e.vertices)
483
+ lines.push('10', String(v[0]), '20', String(v[1]), '30', String(v[2]));
484
+ const faceData = [];
485
+ for (const f of e.faces) {
486
+ faceData.push(f.length, ...f);
487
+ }
488
+ lines.push('94', String(faceData.length));
489
+ for (const d of faceData)
490
+ lines.push('90', String(d));
491
+ lines.push('95', '0'); // edge count
492
+ lines.push('90', '0'); // edge crease count
493
+ return lines.join('\n');
494
+ }
495
+ function serializeMLeader(e, ctx) {
496
+ const lines = ['0', 'MULTILEADER', ...entityCommon(e, ctx)];
497
+ lines.push('100', 'AcDbMLeader');
498
+ lines.push('270', '2'); // version
499
+ lines.push('300', 'CONTEXT_DATA{');
500
+ lines.push('40', String(e.textHeight)); // text height
501
+ lines.push('10', String(e.textPosition[0]), '20', String(e.textPosition[1]), '30', '0');
502
+ lines.push('41', String(e.textWidth));
503
+ lines.push('304', e.text);
504
+ lines.push('301', '}');
505
+ // Leader line data
506
+ lines.push('302', 'LEADER{');
507
+ lines.push('290', '1'); // has set last leader line point
508
+ lines.push('291', '1'); // has set dogleg vector
509
+ lines.push('10', String(e.vertices[0]?.[0] || 0), '20', String(e.vertices[0]?.[1] || 0), '30', '0');
510
+ for (const v of e.vertices) {
511
+ lines.push('10', String(v[0]), '20', String(v[1]), '30', '0');
512
+ }
513
+ lines.push('303', '}');
514
+ // Style
515
+ lines.push('170', String(e.leaderType)); // leader type
516
+ lines.push('171', String(e.contentType)); // content type
517
+ lines.push('41', String(e.doglegLength)); // dogleg length
518
+ lines.push('40', String(e.arrowSize)); // arrow size
519
+ return lines.join('\n');
520
+ }
521
+ // ─── Table Records ───
522
+ function recordHead(type, handle, owner, subclass) {
523
+ return ['0', type, '5', handle, '330', owner, '100', 'AcDbSymbolTableRecord', '100', subclass];
524
+ }
525
+ /** LAYER table record. `plotStyle` / `material` are the document's default object handles. */
526
+ export function layerRecord(l, handle, owner, refs = {}) {
527
+ const color = Math.abs(l.color || 7);
528
+ const lines = recordHead('LAYER', handle, owner, 'AcDbLayerTableRecord');
529
+ lines.push('2', l.name);
530
+ lines.push('70', String((l.frozen ? 1 : 0) | (l.locked ? 4 : 0)));
531
+ lines.push('62', String(l.off ? -color : color));
532
+ lines.push('6', l.linetype || 'Continuous');
533
+ lines.push('370', '-3');
534
+ if (refs.plotStyle)
535
+ lines.push('390', refs.plotStyle);
536
+ if (refs.material)
537
+ lines.push('347', refs.material);
538
+ return lines.join('\n');
539
+ }
540
+ export function linetypeRecord(lt, handle, owner) {
541
+ const lines = recordHead('LTYPE', handle, owner, 'AcDbLinetypeTableRecord');
542
+ lines.push('2', lt.name);
543
+ lines.push('70', '0');
544
+ lines.push('3', lt.description);
545
+ lines.push('72', '65');
546
+ lines.push('73', String(lt.pattern.length));
547
+ lines.push('40', fmt(lt.pattern.reduce((sum, v) => sum + Math.abs(v), 0)));
548
+ for (const p of lt.pattern)
549
+ lines.push('49', fmt(p), '74', '0');
550
+ return lines.join('\n');
551
+ }
552
+ export function textStyleRecord(s, handle, owner) {
553
+ const lines = recordHead('STYLE', handle, owner, 'AcDbTextStyleTableRecord');
554
+ lines.push('2', s.name);
555
+ lines.push('70', '0');
556
+ lines.push('40', String(s.height || 0));
557
+ lines.push('41', '1');
558
+ lines.push('50', '0');
559
+ lines.push('71', '0');
560
+ lines.push('42', '2.5');
561
+ lines.push('3', s.font);
562
+ lines.push('4', '');
563
+ return lines.join('\n');
564
+ }
565
+ export function blockRecord(name, handle, owner, layoutHandle = '0') {
566
+ const lines = recordHead('BLOCK_RECORD', handle, owner, 'AcDbBlockTableRecord');
567
+ lines.push('2', name);
568
+ lines.push('340', layoutHandle);
569
+ lines.push('70', '0'); // insert units
570
+ lines.push('280', '1'); // explodable
571
+ lines.push('281', '0'); // scale uniformly: no
572
+ return lines.join('\n');
573
+ }
574
+ /**
575
+ * BLOCK … ENDBLK definition. `blockRecordHandle` owns the block and its content.
576
+ * Paper space layout blocks pass `paperSpace` so their content is flagged 67=1.
577
+ */
578
+ export function blockDefinition(name, blockRecordHandle, basePoint, entities, ctx = {}) {
579
+ const paper = ctx.paperSpace ? ['67', '1'] : [];
580
+ const parts = [];
581
+ parts.push([
582
+ '0', 'BLOCK', '5', nextHandle(), '330', blockRecordHandle, '100', 'AcDbEntity', ...paper,
583
+ '8', '0', '100', 'AcDbBlockBegin', '2', name, '70', '0',
584
+ '10', String(basePoint[0]), '20', String(basePoint[1]), '30', '0', '3', name, '1', '',
585
+ ].join('\n'));
586
+ const entCtx = { ...ctx, owner: blockRecordHandle };
587
+ for (const e of entities)
588
+ parts.push(serializeEntity(e, entCtx));
589
+ parts.push([
590
+ '0', 'ENDBLK', '5', nextHandle(), '330', blockRecordHandle, '100', 'AcDbEntity', ...paper,
591
+ '8', '0', '100', 'AcDbBlockEnd',
592
+ ].join('\n'));
593
+ return parts.join('\n');
594
+ }
595
+ export { serializeEntity };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Built-in hatch pattern definitions (ISO, millimetre based — as in acadiso.pat).
3
+ *
4
+ * Each line is `[angle, [baseX, baseY], [offsetX, offsetY], dashes]` exactly as
5
+ * stored in a DXF HATCH (group codes 53 / 43,44 / 45,46 / 49): the offset vector
6
+ * is already in world orientation. Scale/rotation are applied at serialization.
7
+ *
8
+ * A curated subset of ezdxf's ISO pattern table (MIT). Pass your own
9
+ * definition via `addHatch(..., { patternLines })` for anything not listed.
10
+ *
11
+ * GENERATED by tools/gen_hatch_patterns.py — do not edit by hand.
12
+ */
13
+ /** One hatch pattern line: angle (deg), base point, offset vector, dash lengths. */
14
+ export type HatchPatternLine = [number, [number, number], [number, number], number[]];
15
+ export declare const HATCH_PATTERNS: Record<string, HatchPatternLine[]>;