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,529 @@
1
+ /**
2
+ * DxfDocument — Core document class with template parsing.
3
+ *
4
+ * ✅ Works in both Node.js AND Browser/Angular environments.
5
+ *
6
+ * Every document is built on a base DXF: a user template, or the built-in
7
+ * blank DXF R2018 document when created from scratch. New layers, linetypes,
8
+ * text styles, blocks, images and layouts are injected into that base on
9
+ * output, so both modes produce the same, complete R2000+ structure.
10
+ *
11
+ * Usage (Node.js):
12
+ * const doc = DxfDocument.readFile('./template/template2018.dxf');
13
+ * doc.saveas('output.dxf');
14
+ *
15
+ * Usage (Browser/Angular):
16
+ * const doc = DxfDocument.fromString(templateDxfString);
17
+ * const dxfOutput = doc.toDxfString();
18
+ * // download via Blob, or send to server
19
+ *
20
+ * Usage (from scratch — both environments):
21
+ * const doc = DxfDocument.create();
22
+ * doc.addLayer('MyLayer', 7, 'CONTINUOUS');
23
+ * const msp = doc.modelspace();
24
+ * msp.add_circle([50, 50], 25);
25
+ * const output = doc.toDxfString();
26
+ */
27
+ import { Modelspace } from './modelspace.js';
28
+ import { readTemplate } from '../template/template-reader.js';
29
+ import { serializeDocument, listLayoutNames } from '../template/template-injector.js';
30
+ import { BLANK_DXF_R2018 } from '../template/blank-document.js';
31
+ let _fs = null;
32
+ try {
33
+ const proc = typeof process !== 'undefined' ? process : undefined;
34
+ if (proc?.versions?.node) {
35
+ _fs = proc.getBuiltinModule?.('fs') ?? null;
36
+ if (!_fs) {
37
+ const req = globalThis.require;
38
+ if (typeof req === 'function')
39
+ _fs = req('fs');
40
+ }
41
+ }
42
+ }
43
+ catch { /* browser environment — fs not available */ }
44
+ export class DxfDocument {
45
+ // Internal state
46
+ _layers = new Map();
47
+ _linetypes = new Map();
48
+ _styles = new Map();
49
+ _blocks = new Map();
50
+ _msp = new Modelspace();
51
+ _layouts = [];
52
+ /** Upper-cased layout name → entity collector for that layout. */
53
+ _paperspaces = new Map();
54
+ // Base document (template or blank)
55
+ _rawSections = new Map();
56
+ _templateBlockNames = new Set();
57
+ _templateLayerNames = new Set();
58
+ _templateStyleNames = new Set();
59
+ _templateLayoutNames = [];
60
+ // Parsed template reference (from dxf-parser)
61
+ _parsedTemplate = null;
62
+ constructor() { }
63
+ // ─── Static Constructors ───
64
+ /**
65
+ * Read a DXF file from disk (Node.js only).
66
+ * @throws Error if called in browser environment.
67
+ */
68
+ static readFile(filePath) {
69
+ if (!_fs) {
70
+ throw new Error('DxfDocument.readFile() requires Node.js. ' +
71
+ 'In browser/Angular, use DxfDocument.fromString(content) instead.');
72
+ }
73
+ const content = _fs.readFileSync(filePath, 'utf-8');
74
+ return DxfDocument.fromString(content);
75
+ }
76
+ /**
77
+ * Parse a DXF template from a string.
78
+ * ✅ Works in both Node.js and Browser/Angular.
79
+ */
80
+ static fromString(content) {
81
+ const doc = new DxfDocument();
82
+ doc._loadBase(content);
83
+ return doc;
84
+ }
85
+ /**
86
+ * Create a blank DXF document (DXF R2018, meters).
87
+ * ✅ Works in both Node.js and Browser/Angular.
88
+ */
89
+ static create() {
90
+ const doc = new DxfDocument();
91
+ doc._loadBase(BLANK_DXF_R2018);
92
+ doc._addDefaultLinetypes();
93
+ return doc;
94
+ }
95
+ // ─── Public API ───
96
+ /** Get the modelspace for adding entities. */
97
+ modelspace() {
98
+ return this._msp;
99
+ }
100
+ /** Add or update a layer. */
101
+ addLayer(name, color = 7, linetype = 'CONTINUOUS') {
102
+ this._layers.set(name, { name, color, linetype });
103
+ }
104
+ /** Check if a layer exists. */
105
+ hasLayer(name) {
106
+ return this._layers.has(name) || this._templateLayerNames.has(name);
107
+ }
108
+ /** Get layer definition. */
109
+ getLayer(name) {
110
+ return this._layers.get(name);
111
+ }
112
+ /** Access layers as a record (backward compat). */
113
+ get layers() {
114
+ const obj = {};
115
+ for (const [name, def] of this._layers) {
116
+ obj[name] = def;
117
+ }
118
+ for (const name of this._templateLayerNames) {
119
+ if (!obj[name]) {
120
+ obj[name] = { name, color: 7, linetype: 'CONTINUOUS' };
121
+ }
122
+ }
123
+ return obj;
124
+ }
125
+ /**
126
+ * Add a text style. For Thai text use a TrueType font file name,
127
+ * e.g. `addTextStyle('THAI', 'THSarabunNew.ttf')`.
128
+ * Styles that already exist in the base document are kept as they are.
129
+ */
130
+ addTextStyle(name, font, height) {
131
+ this._styles.set(name, { name, font, height });
132
+ }
133
+ /** ezdxf-compatible styles.new() */
134
+ get styles() {
135
+ const self = this;
136
+ return {
137
+ new: (name, dxfattribs) => {
138
+ self.addTextStyle(name, dxfattribs.font);
139
+ }
140
+ };
141
+ }
142
+ /**
143
+ * Add a linetype definition. Pattern values are in drawing units:
144
+ * positive = dash, negative = gap, 0 = dot.
145
+ * Linetypes that already exist in the base document are kept as they are.
146
+ */
147
+ addLineType(name, description, pattern) {
148
+ this._linetypes.set(name, { name, description, pattern });
149
+ }
150
+ // ─── GeoReference ───
151
+ _geoRef = null;
152
+ /**
153
+ * Set geographic coordinate reference for the drawing.
154
+ * This embeds coordinate system info into the DXF HEADER.
155
+ *
156
+ * @example
157
+ * // UTM Zone 47N (Thailand)
158
+ * doc.setGeoReference({
159
+ * crsId: 'EPSG:32647',
160
+ * designPoint: [0, 0],
161
+ * referencePoint: [600000, 1700000], // Easting, Northing
162
+ * northDirection: 0,
163
+ * scaleFactor: 1,
164
+ * coordinateType: 2, // projected
165
+ * });
166
+ */
167
+ setGeoReference(geoRef) {
168
+ this._geoRef = geoRef;
169
+ // Set header variables for coordinate system
170
+ this._headerVars.set('$INSUNITS', [{ code: 70, value: '6' }]); // Meters
171
+ // Store design/reference point mapping
172
+ this._headerVars.set('$UCSORG', [
173
+ { code: 10, value: String(geoRef.referencePoint[0]) },
174
+ { code: 20, value: String(geoRef.referencePoint[1]) },
175
+ { code: 30, value: '0' },
176
+ ]);
177
+ // Store north direction
178
+ this._headerVars.set('$ANGBASE', [{ code: 50, value: String(geoRef.northDirection) }]);
179
+ }
180
+ /** Get the geo reference definition (if set). */
181
+ get geoReference() { return this._geoRef; }
182
+ // ─── Layout (Paper Space) ───
183
+ /**
184
+ * Add a Paper Space layout for plotting.
185
+ * Paper space units are mm; the origin is the lower-left corner of the
186
+ * printable area (inside the margins), as in AutoCAD.
187
+ *
188
+ * @example
189
+ * doc.addLayout({
190
+ * name: 'A1',
191
+ * paperWidth: 841, // mm
192
+ * paperHeight: 594, // mm
193
+ * margins: [0, 0, 0, 0],
194
+ * viewports: [{
195
+ * center: [420, 297],
196
+ * width: 800, height: 560,
197
+ * viewCenter: [519500, 1033500],
198
+ * scale: 12500, // 1:12,500 — model in meters
199
+ * frozenLayers: ['GRID'],
200
+ * }]
201
+ * });
202
+ */
203
+ addLayout(layout) {
204
+ if (this.hasLayout(layout.name))
205
+ throw new Error(`Layout "${layout.name}" already exists.`);
206
+ this._layouts.push(layout);
207
+ }
208
+ /** Get all layout definitions added with addLayout(). */
209
+ get layouts() { return this._layouts; }
210
+ /** Names of all layouts: those of the base document (incl. "Model") and added ones. */
211
+ get layoutNames() {
212
+ return [...this._templateLayoutNames, ...this._layouts.map(l => l.name)];
213
+ }
214
+ /** Check if a layout exists (case-insensitive). */
215
+ hasLayout(name) {
216
+ return this.layoutNames.some(n => n.toUpperCase() === name.toUpperCase());
217
+ }
218
+ /**
219
+ * Entity collector for a layout: an existing layout of the base document
220
+ * (e.g. "Layout1") or one added with addLayout(). "Model" returns model space.
221
+ */
222
+ paperspace(name) {
223
+ if (name.toUpperCase() === 'MODEL')
224
+ return this._msp;
225
+ if (!this.hasLayout(name))
226
+ throw new Error(`Unknown layout "${name}".`);
227
+ const key = name.toUpperCase();
228
+ let entry = this._paperspaces.get(key);
229
+ if (!entry) {
230
+ entry = { name, space: new Modelspace() };
231
+ this._paperspaces.set(key, entry);
232
+ }
233
+ return entry.space;
234
+ }
235
+ // ─── Blocks ───
236
+ /**
237
+ * Define (or redefine) a block. Entity coordinates are block-local,
238
+ * relative to `basePoint`.
239
+ */
240
+ addBlockDefinition(name, entities, basePoint = [0, 0]) {
241
+ if (this._templateBlockNames.has(name)) {
242
+ throw new Error(`Block "${name}" already exists in the base document.`);
243
+ }
244
+ this._blocks.set(name, { name, basePoint, entities });
245
+ }
246
+ /** Check if a block exists (in template or user-defined). */
247
+ hasBlock(name) {
248
+ return this._blocks.has(name) || this._templateBlockNames.has(name);
249
+ }
250
+ /** Get all block names. */
251
+ getBlockNames() {
252
+ const names = new Set();
253
+ for (const name of this._blocks.keys())
254
+ names.add(name);
255
+ for (const name of this._templateBlockNames)
256
+ names.add(name);
257
+ return Array.from(names);
258
+ }
259
+ // ─── Layer Control (ezdxf-style) ───
260
+ /** Freeze a layer (entities on this layer are hidden and ignored). */
261
+ freezeLayer(name) {
262
+ const layer = this._layers.get(name);
263
+ if (layer)
264
+ layer.frozen = true;
265
+ }
266
+ /** Thaw a layer (undo freeze). */
267
+ thawLayer(name) {
268
+ const layer = this._layers.get(name);
269
+ if (layer)
270
+ layer.frozen = false;
271
+ }
272
+ /** Lock a layer (entities visible but not editable). */
273
+ lockLayer(name) {
274
+ const layer = this._layers.get(name);
275
+ if (layer)
276
+ layer.locked = true;
277
+ }
278
+ /** Unlock a layer. */
279
+ unlockLayer(name) {
280
+ const layer = this._layers.get(name);
281
+ if (layer)
282
+ layer.locked = false;
283
+ }
284
+ /** Turn a layer off (entities hidden but still selectable). */
285
+ setLayerOff(name) {
286
+ const layer = this._layers.get(name);
287
+ if (layer)
288
+ layer.off = true;
289
+ }
290
+ /** Turn a layer on. */
291
+ setLayerOn(name) {
292
+ const layer = this._layers.get(name);
293
+ if (layer)
294
+ layer.off = false;
295
+ }
296
+ /** Set layer color. */
297
+ setLayerColor(name, color) {
298
+ const layer = this._layers.get(name);
299
+ if (layer)
300
+ layer.color = color;
301
+ }
302
+ /** Set layer linetype. */
303
+ setLayerLinetype(name, linetype) {
304
+ const layer = this._layers.get(name);
305
+ if (layer)
306
+ layer.linetype = linetype;
307
+ }
308
+ // ─── HEADER Variables (View/Zoom/Extent) ───
309
+ _headerVars = new Map();
310
+ /**
311
+ * Set a HEADER variable (replaces the base document's value).
312
+ * @example doc.setHeaderVar('$INSUNITS', [{ code: 70, value: '6' }]); // Meters
313
+ */
314
+ setHeaderVar(name, values) {
315
+ this._headerVars.set(name, values);
316
+ }
317
+ /**
318
+ * Set the drawing extents (bounding box).
319
+ * This controls the initial zoom-to-extents view in CAD software.
320
+ * Equivalent to ezdxf: doc.header['$EXTMIN'] / doc.header['$EXTMAX']
321
+ */
322
+ setExtents(minX, minY, maxX, maxY) {
323
+ this._headerVars.set('$EXTMIN', [
324
+ { code: 10, value: String(minX) },
325
+ { code: 20, value: String(minY) },
326
+ { code: 30, value: '0' },
327
+ ]);
328
+ this._headerVars.set('$EXTMAX', [
329
+ { code: 10, value: String(maxX) },
330
+ { code: 20, value: String(maxY) },
331
+ { code: 30, value: '0' },
332
+ ]);
333
+ }
334
+ /**
335
+ * Auto-calculate extents from all entities in modelspace.
336
+ * Scans all entity coordinates and sets $EXTMIN/$EXTMAX accordingly.
337
+ */
338
+ zoomExtents(padding = 0) {
339
+ const bounds = this._calculateBounds();
340
+ if (bounds) {
341
+ this.setExtents(bounds.minX - padding, bounds.minY - padding, bounds.maxX + padding, bounds.maxY + padding);
342
+ this.setViewCenter((bounds.minX + bounds.maxX) / 2, (bounds.minY + bounds.maxY) / 2, Math.max(bounds.maxX - bounds.minX, bounds.maxY - bounds.minY) + padding * 2);
343
+ }
344
+ }
345
+ /**
346
+ * Set the view center and height (zoom level) for the initial view.
347
+ * Equivalent to ezdxf setting $VIEWCTR and $VIEWSIZE.
348
+ */
349
+ setViewCenter(x, y, viewHeight) {
350
+ this._headerVars.set('$VIEWCTR', [
351
+ { code: 10, value: String(x) },
352
+ { code: 20, value: String(y) },
353
+ ]);
354
+ if (viewHeight !== undefined) {
355
+ this._headerVars.set('$VIEWSIZE', [
356
+ { code: 40, value: String(viewHeight) },
357
+ ]);
358
+ }
359
+ }
360
+ /**
361
+ * Zoom to a specific window (rectangle).
362
+ */
363
+ zoomWindow(minX, minY, maxX, maxY) {
364
+ this.setExtents(minX, minY, maxX, maxY);
365
+ this.setViewCenter((minX + maxX) / 2, (minY + maxY) / 2, maxY - minY);
366
+ }
367
+ /** Access the HEADER vars map (used by serializer). */
368
+ get headerVars() {
369
+ return this._headerVars;
370
+ }
371
+ /**
372
+ * Calculate bounding box of all entities in modelspace.
373
+ */
374
+ _calculateBounds() {
375
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
376
+ let hasPoints = false;
377
+ const update = (x, y) => {
378
+ minX = Math.min(minX, x);
379
+ minY = Math.min(minY, y);
380
+ maxX = Math.max(maxX, x);
381
+ maxY = Math.max(maxY, y);
382
+ hasPoints = true;
383
+ };
384
+ for (const e of this._msp.entities) {
385
+ switch (e.type) {
386
+ case 'LINE':
387
+ update(e.start[0], e.start[1]);
388
+ update(e.end[0], e.end[1]);
389
+ break;
390
+ case 'LWPOLYLINE':
391
+ for (const p of e.points)
392
+ update(p[0], p[1]);
393
+ break;
394
+ case 'POLYLINE':
395
+ for (const p of e.points)
396
+ update(p[0], p[1]);
397
+ break;
398
+ case 'CIRCLE':
399
+ update(e.center[0] - e.radius, e.center[1] - e.radius);
400
+ update(e.center[0] + e.radius, e.center[1] + e.radius);
401
+ break;
402
+ case 'ARC':
403
+ update(e.center[0] - e.radius, e.center[1] - e.radius);
404
+ update(e.center[0] + e.radius, e.center[1] + e.radius);
405
+ break;
406
+ case 'TEXT':
407
+ update(e.position[0], e.position[1]);
408
+ break;
409
+ case 'MTEXT':
410
+ update(e.position[0], e.position[1]);
411
+ break;
412
+ case 'INSERT':
413
+ update(e.position[0], e.position[1]);
414
+ break;
415
+ case 'POINT':
416
+ update(e.position[0], e.position[1]);
417
+ break;
418
+ case 'ELLIPSE':
419
+ update(e.center[0], e.center[1]);
420
+ break;
421
+ case 'SPLINE':
422
+ for (const p of e.controlPoints)
423
+ update(p[0], p[1]);
424
+ break;
425
+ case 'LEADER':
426
+ for (const v of e.vertices)
427
+ update(v[0], v[1]);
428
+ break;
429
+ case 'HATCH':
430
+ for (const b of e.boundaries)
431
+ for (const p of b.points)
432
+ update(p[0], p[1]);
433
+ break;
434
+ case 'IMAGE': {
435
+ const [x, y] = e.insertionPoint;
436
+ const [w, h] = e.imageSize;
437
+ update(x, y);
438
+ update(x + e.uVector[0] * w + e.vVector[0] * h, y + e.uVector[1] * w + e.vVector[1] * h);
439
+ break;
440
+ }
441
+ case 'DIMENSION':
442
+ if (e.point1)
443
+ update(e.point1[0], e.point1[1]);
444
+ if (e.point2)
445
+ update(e.point2[0], e.point2[1]);
446
+ break;
447
+ }
448
+ }
449
+ return hasPoints ? { minX, minY, maxX, maxY } : null;
450
+ }
451
+ // ─── Backward Compatibility (dxf-writer API shim) ───
452
+ setActiveLayer(_name) { }
453
+ drawPolyline(points, closed = false) {
454
+ this._msp.add_lwpolyline(points, {}, closed);
455
+ }
456
+ drawCircle(cx, cy, radius) {
457
+ this._msp.add_circle([cx, cy], radius);
458
+ }
459
+ drawText(x, y, height, rotation, text) {
460
+ this._msp.add_text(text, { height, rotation }).set_placement([x, y]);
461
+ }
462
+ // ─── Serialization ───
463
+ /** Serialize to DXF string. ✅ Works everywhere. */
464
+ toDxfString() {
465
+ const paperspace = new Map();
466
+ for (const { name, space } of this._paperspaces.values())
467
+ paperspace.set(name, space.entities);
468
+ return serializeDocument({
469
+ rawSections: this._rawSections,
470
+ modelspace: this._msp.entities,
471
+ layers: Array.from(this._layers.values()).filter(l => !this._templateLayerNames.has(l.name)),
472
+ linetypes: Array.from(this._linetypes.values()),
473
+ styles: Array.from(this._styles.values()).filter(s => !this._templateStyleNames.has(s.name)),
474
+ blocks: Array.from(this._blocks.values()),
475
+ layouts: this._layouts,
476
+ paperspace,
477
+ headerVars: this._headerVars,
478
+ });
479
+ }
480
+ /**
481
+ * Save to file (Node.js only).
482
+ * @throws Error if called in browser environment.
483
+ */
484
+ saveas(filePath) {
485
+ if (!_fs) {
486
+ throw new Error('DxfDocument.saveas() requires Node.js. ' +
487
+ 'In browser/Angular, use doc.toDxfString() and create a Blob download instead.');
488
+ }
489
+ _fs.writeFileSync(filePath, this.toDxfString(), 'utf-8');
490
+ }
491
+ /**
492
+ * Download DXF file in browser.
493
+ * ✅ Works in Browser/Angular.
494
+ */
495
+ downloadAs(filename) {
496
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
497
+ throw new Error('DxfDocument.downloadAs() requires a browser environment.');
498
+ }
499
+ const blob = new Blob([this.toDxfString()], { type: 'application/dxf' });
500
+ const url = URL.createObjectURL(blob);
501
+ const a = document.createElement('a');
502
+ a.href = url;
503
+ a.download = filename;
504
+ a.click();
505
+ URL.revokeObjectURL(url);
506
+ }
507
+ // ─── Private Methods ───
508
+ _loadBase(content) {
509
+ const tpl = readTemplate(content);
510
+ this._parsedTemplate = tpl.parsed;
511
+ this._rawSections = tpl.rawSections;
512
+ this._templateBlockNames = tpl.blockNames;
513
+ this._templateLayerNames = tpl.layerNames;
514
+ this._templateStyleNames = new Set(tpl.styles.keys());
515
+ this._templateLayoutNames = listLayoutNames(tpl.rawSections);
516
+ for (const [name, def] of tpl.layers)
517
+ this._layers.set(name, def);
518
+ for (const [name, def] of tpl.styles)
519
+ this._styles.set(name, def);
520
+ }
521
+ _addDefaultLinetypes() {
522
+ this._linetypes.set('CONTINUOUS', { name: 'CONTINUOUS', description: 'Solid line', pattern: [] });
523
+ this._linetypes.set('DASHED', { name: 'DASHED', description: '_ _ _ _', pattern: [0.5, -0.5] });
524
+ this._linetypes.set('CENTER', { name: 'CENTER', description: '____ _ ____', pattern: [2, -1, 0.5, -1] });
525
+ this._linetypes.set('PHANTOM', { name: 'PHANTOM', description: '__ __ . __ __', pattern: [2.5, -0.5, 0.5, -0.5, 0.5, -0.5] });
526
+ this._linetypes.set('HIDDEN', { name: 'HIDDEN', description: '_ _ _ _', pattern: [0.25, -0.125] });
527
+ this._linetypes.set('DOT', { name: 'DOT', description: '. . . . .', pattern: [0, -0.25] });
528
+ }
529
+ }