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,549 @@
1
+ /**
2
+ * DxfBuilder — Backward-compatible wrapper around DxfDocument.
3
+ *
4
+ * ✅ Works in both Node.js AND Browser/Angular environments.
5
+ *
6
+ * Usage (existing — unchanged):
7
+ * const builder = new DxfBuilder();
8
+ * builder.addPolyline(points, 'MyLayer', 3);
9
+ * builder.addText('Hello', 100, 200, 5, 0, 'TEXT', 7);
10
+ * const dxfString = builder.saveString();
11
+ *
12
+ * NEW: Template-based (Node.js):
13
+ * const builder = new DxfBuilder('./template/template2018.dxf');
14
+ *
15
+ * NEW: Template-based (Browser/Angular):
16
+ * const builder = DxfBuilder.fromString(templateDxfString);
17
+ *
18
+ * NEW: Convenience API:
19
+ * builder.addLayer('MyLayer', 3, 'DASHED');
20
+ * builder.addLine([0,0], [100,100], 'MyLayer', 1);
21
+ * builder.addArc(50, 50, 25, 0, 180, 'MyLayer', 4);
22
+ * builder.addHatch('SOLID', [[[0,0],[100,0],[100,100],[0,100]]], 'MyLayer', 3);
23
+ *
24
+ * NEW: Sheet composition:
25
+ * builder.defineBlock('BM', b => { b.addCircle(0, 0, 1.5, '0'); b.addLine([-2, 0], [2, 0], '0'); });
26
+ * builder.addBlock('BM', 519395.8, 1034018.6, 10, 0, 'SYMBOL');
27
+ * builder.addCallout([519300, 1034000], ['ก่อสร้างอาคารทางน้ำเข้า', 'ขนาด 2-1.75x1.75 ม.'], [519350, 1034150]);
28
+ * builder.addLayout('A1', 841, 594, [0, 0, 0, 0], [{ center: [420, 297], width: 800, height: 560,
29
+ * viewCenter: [519500, 1033500], scale: 12500 }]);
30
+ * builder.onLayout('A1', b => b.addText('แผนที่แสดงตำแหน่งหมุดหลักฐาน', 420, 20, 8));
31
+ */
32
+ import { DxfDocument } from './dxf-document.js';
33
+ import { Modelspace } from './modelspace.js';
34
+ import { TextEntityAlignment } from './dxf-entities.js';
35
+ import { BLOCK_REGISTRY } from '../template/block-registry.js';
36
+ import { rotatePoint } from '../geometry/math.utils.js';
37
+ import { estimateTextWidth } from '../geometry/text.utils.js';
38
+ export class DxfBuilder {
39
+ _document;
40
+ /** Current drawing target: model space, a layout, or a block being defined. */
41
+ _msp;
42
+ /**
43
+ * Create a new DxfBuilder.
44
+ * @param template Optional path to a DXF template file (Node.js only), or a loaded DxfDocument.
45
+ */
46
+ constructor(template) {
47
+ if (template instanceof DxfDocument) {
48
+ this._document = template;
49
+ }
50
+ else if (template) {
51
+ this._document = DxfDocument.readFile(template);
52
+ }
53
+ else {
54
+ this._document = DxfDocument.create();
55
+ }
56
+ this._msp = this._document.modelspace();
57
+ if (!template) {
58
+ this._initializeLayers();
59
+ }
60
+ }
61
+ /**
62
+ * Create a DxfBuilder from a DXF template string.
63
+ * ✅ Works in Browser/Angular.
64
+ */
65
+ static fromString(content) {
66
+ return new DxfBuilder(DxfDocument.fromString(content));
67
+ }
68
+ // ─── Property Access ───
69
+ /** Access the underlying DxfDocument. */
70
+ get doc() { return this._document; }
71
+ /** Access the current drawing target directly for ezdxf-style operations. */
72
+ get msp() { return this._msp; }
73
+ // ─── Layer Management ───
74
+ /**
75
+ * Add or update a layer.
76
+ * @param name Layer name
77
+ * @param color ACI color index (0-256)
78
+ * @param linetype Linetype name (e.g. 'CONTINUOUS', 'DASHED', 'CENTER')
79
+ */
80
+ addLayer(name, color = 7, linetype = 'CONTINUOUS') {
81
+ this._document.addLayer(name, color, linetype);
82
+ }
83
+ /**
84
+ * Add a linetype. Pattern values are in drawing units: positive = dash,
85
+ * negative = gap, 0 = dot. Existing names in the template are kept.
86
+ * @example builder.addLineType('BOUNDARY', '____ . ____', [20, -5, 0, -5]);
87
+ */
88
+ addLineType(name, description, pattern) {
89
+ this._document.addLineType(name, description, pattern);
90
+ }
91
+ /**
92
+ * Add a text style. Existing names in the template are kept.
93
+ * @example builder.addTextStyle('THAI', 'THSarabunNew.ttf');
94
+ */
95
+ addTextStyle(name, font, height) {
96
+ this._document.addTextStyle(name, font, height);
97
+ }
98
+ // ─── Geometry Entities ───
99
+ /**
100
+ * Add a single LINE entity (two-point segment).
101
+ */
102
+ addLine(start, end, layer = '0', color, linetype) {
103
+ this._ensureLayer(layer);
104
+ this._msp.add_line(start, end, { layer, color, linetype });
105
+ }
106
+ /**
107
+ * Add a LWPOLYLINE entity (multi-vertex polyline).
108
+ */
109
+ addPolyline(points, layer = '0', color, linetype, closed = false) {
110
+ this._ensureLayer(layer);
111
+ const pts = points.map(p => [p[0], p[1]]);
112
+ this._msp.add_lwpolyline(pts, { layer, color, linetype }, closed);
113
+ }
114
+ /**
115
+ * Add a CIRCLE entity.
116
+ */
117
+ addCircle(cx, cy, radius, layer = '0', color) {
118
+ this._ensureLayer(layer);
119
+ this._msp.add_circle([cx, cy], radius, { layer, color });
120
+ }
121
+ /**
122
+ * Add an ARC entity.
123
+ * @param cx Center X
124
+ * @param cy Center Y
125
+ * @param radius Radius
126
+ * @param startAngle Start angle in degrees
127
+ * @param endAngle End angle in degrees
128
+ */
129
+ addArc(cx, cy, radius, startAngle, endAngle, layer = '0', color) {
130
+ this._ensureLayer(layer);
131
+ this._msp.add_arc([cx, cy], radius, startAngle, endAngle, { layer, color });
132
+ }
133
+ /**
134
+ * Add an ELLIPSE entity.
135
+ * @param cx Center X
136
+ * @param cy Center Y
137
+ * @param majorAxisX Major axis endpoint X (relative to center)
138
+ * @param majorAxisY Major axis endpoint Y (relative to center)
139
+ * @param ratio Ratio of minor to major axis (0..1)
140
+ */
141
+ addEllipse(cx, cy, majorAxisX, majorAxisY, ratio, layer = '0', color) {
142
+ this._ensureLayer(layer);
143
+ this._msp.add_ellipse([cx, cy], [majorAxisX, majorAxisY], ratio, 0, Math.PI * 2, { layer, color });
144
+ }
145
+ /**
146
+ * Add a POINT entity.
147
+ */
148
+ addPoint(x, y, layer = '0', color) {
149
+ this._ensureLayer(layer);
150
+ this._msp.add_point([x, y], { layer, color });
151
+ }
152
+ // ─── Text Entities ───
153
+ /**
154
+ * Add a TEXT entity.
155
+ */
156
+ addText(text, x, y, height = 2, rotation = 0, layer = 'TEXT', color = 7) {
157
+ this._ensureLayer(layer);
158
+ this._msp.add_text(text, { height, rotation, layer, color }).set_placement([x, y]);
159
+ }
160
+ /**
161
+ * Add an MTEXT entity (multi-line text with word wrap).
162
+ */
163
+ addMText(text, x, y, width = 100, height = 2.5, layer = 'TEXT', color) {
164
+ this._ensureLayer(layer);
165
+ const e = this._msp.add_mtext(text, { height, layer, color, width });
166
+ e.position = [x, y];
167
+ }
168
+ // ─── Complex Entities ───
169
+ /**
170
+ * Add a HATCH entity (solid fill or pattern fill).
171
+ *
172
+ * @param patternName 'SOLID', a built-in pattern (see HATCH_PATTERNS, ISO/mm based),
173
+ * or any name together with `opts.patternLines`.
174
+ * @param boundaries Array of boundary polygons, each as an array of [x,y] points.
175
+ * Polygons inside another polygon become holes.
176
+ * @param layer Layer name
177
+ * @param color ACI color
178
+ * @param opts scale (pattern units × scale = drawing units), angle (degrees), patternLines
179
+ *
180
+ * @example
181
+ * // Solid fill
182
+ * builder.addHatch('SOLID', [[[0,0],[100,0],[100,100],[0,100]]], 'MyLayer', 3);
183
+ *
184
+ * // Pattern fill
185
+ * builder.addHatch('ANSI31', [[[0,0],[50,0],[50,50],[0,50]]], 'MyLayer', 7, { scale: 2, angle: 45 });
186
+ */
187
+ addHatch(patternName, boundaries, layer = '0', color, opts = {}) {
188
+ this._ensureLayer(layer);
189
+ const hatchBoundaries = boundaries.map(pts => ({
190
+ type: 'polyline',
191
+ points: pts,
192
+ closed: true,
193
+ }));
194
+ this._msp.add_hatch(patternName, hatchBoundaries, { layer, color }, {
195
+ solid: patternName.toUpperCase() === 'SOLID',
196
+ scale: opts.scale,
197
+ angle: opts.angle,
198
+ patternLines: opts.patternLines,
199
+ });
200
+ }
201
+ /**
202
+ * Add a raster IMAGE, e.g. a scanned topographic map as a background.
203
+ *
204
+ * DXF only references the image file: keep the file next to the .dxf and
205
+ * pass its file name, or pass an absolute path.
206
+ *
207
+ * @param imagePath file name/path as the CAD application should resolve it
208
+ * @param pixelSize image size in pixels [width, height] (DXF cannot read the file)
209
+ * @param insertion lower-left corner in drawing units
210
+ * @param worldSize image size in drawing units [width, height]
211
+ *
212
+ * @example
213
+ * // 1:50,000 sheet scanned at 7087×7087 px covering 15 km
214
+ * builder.addImage('topo_4927II.jpg', [7087, 7087], [513000, 1027000], [15000, 15000], 'RASTER');
215
+ */
216
+ addImage(imagePath, pixelSize, insertion, worldSize, layer = '0', opts = {}) {
217
+ this._ensureLayer(layer);
218
+ const perPixel = [worldSize[0] / pixelSize[0], worldSize[1] / pixelSize[1]];
219
+ this._msp.add_image(insertion, imagePath, pixelSize, perPixel, { layer }, opts);
220
+ }
221
+ /**
222
+ * Add a DIMENSION entity (linear dimension between two points).
223
+ */
224
+ addDimension(point1, point2, dimLinePosition, layer = 'DIM', color, text) {
225
+ this._ensureLayer(layer);
226
+ this._msp.add_dimension(point1, point2, dimLinePosition, { layer, color, text });
227
+ }
228
+ /**
229
+ * Add a LEADER entity (arrow with annotation line).
230
+ */
231
+ addLeader(vertices, layer = '0', color, hasArrowhead = true) {
232
+ this._ensureLayer(layer);
233
+ this._msp.add_leader(vertices, { layer, color, has_arrowhead: hasArrowhead });
234
+ }
235
+ /**
236
+ * Add a SPLINE entity (smooth curve through control points).
237
+ */
238
+ addSpline(controlPoints, degree = 3, layer = '0', color) {
239
+ this._ensureLayer(layer);
240
+ this._msp.add_spline(controlPoints, degree, { layer, color });
241
+ }
242
+ // ─── Structure / OnfarmLine ───
243
+ /** Add a line from an OnfarmLine structure object. */
244
+ addLineFromObject(onfarmLine, layer = 'structure') {
245
+ this._ensureLayer(layer);
246
+ const pts = onfarmLine.coordinate.map(p => [p[0], p[1]]);
247
+ this._msp.add_lwpolyline(pts, { layer, color: onfarmLine.color, linetype: onfarmLine.type });
248
+ }
249
+ // ─── Blocks ───
250
+ /**
251
+ * Define a reusable block (symbol). Draw it with the normal builder API in
252
+ * block-local coordinates around `basePoint`, then place it with addBlock().
253
+ *
254
+ * @example
255
+ * builder.defineBlock('SCHOOL', b => {
256
+ * b.addPolyline([[-2, -1.5], [2, -1.5], [2, 1.5], [-2, 1.5]], '0', 5, undefined, true);
257
+ * b.addHatch('SOLID', [[[-2, -1.5], [2, -1.5], [2, 1.5], [-2, 1.5]]], '0', 5);
258
+ * });
259
+ * builder.addBlock('SCHOOL', 518807, 1033917, 20, 0, 'POI');
260
+ */
261
+ defineBlock(name, draw, basePoint = [0, 0]) {
262
+ const collector = new Modelspace();
263
+ this._withTarget(collector, () => draw(this));
264
+ this._document.addBlockDefinition(name, collector.entities, basePoint);
265
+ }
266
+ /**
267
+ * Add a block reference (INSERT).
268
+ * Template or defined block → INSERT entity. Registry block → manual drawing. Neither → placeholder.
269
+ */
270
+ addBlock(name, x, y, scale, rotation, layer = '0', attributes = {}) {
271
+ this._ensureLayer(layer);
272
+ if (this._document.hasBlock(name)) {
273
+ this._msp.add_blockref(name, [x, y], { layer, xscale: scale, yscale: scale, zscale: scale, rotation });
274
+ }
275
+ else if (BLOCK_REGISTRY[name]) {
276
+ this._drawFromRegistry(name, x, y, scale, rotation, layer);
277
+ }
278
+ else {
279
+ this._msp.add_text(`[Block:${name}]`, { height: scale * 2, rotation, layer }).set_placement([x, y]);
280
+ }
281
+ let offset = 0;
282
+ for (const [key, value] of Object.entries(attributes)) {
283
+ this._msp.add_text(`${key}: ${value}`, { height: scale * 2, rotation, layer }).set_placement([x + 5, y + offset]);
284
+ offset += scale * 5;
285
+ }
286
+ }
287
+ // ─── View / Zoom Management ───
288
+ /**
289
+ * Auto-zoom to fit all entities with optional padding.
290
+ * Sets $EXTMIN, $EXTMAX, $VIEWCTR, $VIEWSIZE in HEADER.
291
+ * @example builder.zoomExtents(100); // 100 unit padding
292
+ */
293
+ zoomExtents(padding = 0) {
294
+ this._document.zoomExtents(padding);
295
+ }
296
+ /**
297
+ * Zoom to a specific window (rectangle).
298
+ * @example builder.zoomWindow(1000, 2000, 5000, 6000);
299
+ */
300
+ zoomWindow(minX, minY, maxX, maxY) {
301
+ this._document.zoomWindow(minX, minY, maxX, maxY);
302
+ }
303
+ /**
304
+ * Set the drawing extents manually.
305
+ */
306
+ setExtents(minX, minY, maxX, maxY) {
307
+ this._document.setExtents(minX, minY, maxX, maxY);
308
+ }
309
+ /**
310
+ * Set initial view center and zoom level.
311
+ */
312
+ setViewCenter(x, y, viewHeight) {
313
+ this._document.setViewCenter(x, y, viewHeight);
314
+ }
315
+ // ─── Layer Management (extended) ───
316
+ /** Freeze a layer (hidden, not plotted, not selectable). */
317
+ freezeLayer(name) { this._document.freezeLayer(name); }
318
+ /** Thaw a layer (undo freeze). */
319
+ thawLayer(name) { this._document.thawLayer(name); }
320
+ /** Lock a layer (visible but not editable). */
321
+ lockLayer(name) { this._document.lockLayer(name); }
322
+ /** Unlock a layer. */
323
+ unlockLayer(name) { this._document.unlockLayer(name); }
324
+ /** Turn a layer off (hidden but still selectable). */
325
+ setLayerOff(name) { this._document.setLayerOff(name); }
326
+ /** Turn a layer on. */
327
+ setLayerOn(name) { this._document.setLayerOn(name); }
328
+ /** Change layer color. */
329
+ setLayerColor(name, color) { this._document.setLayerColor(name, color); }
330
+ /** Change layer linetype. */
331
+ setLayerLinetype(name, linetype) { this._document.setLayerLinetype(name, linetype); }
332
+ // ─── Annotations ───
333
+ /**
334
+ * Add a callout: a leader from `anchor` to a boxed, multi-line label.
335
+ * The leader attaches to the box edge midpoint nearest the anchor.
336
+ *
337
+ * anchor ●──────────┤ ก่อสร้างอาคารทางน้ำเข้า │
338
+ * │ ขนาด 2-1.75x1.75 ม. │
339
+ *
340
+ * @param anchor the point being annotated
341
+ * @param text lines of text (array, or a string with "\n")
342
+ * @param boxAt top-left corner of the box
343
+ * @returns the box extent [minX, minY, maxX, maxY]
344
+ */
345
+ addCallout(anchor, text, boxAt, opts = {}) {
346
+ const layer = opts.layer ?? 'TEXT';
347
+ this._ensureLayer(layer);
348
+ const lines = Array.isArray(text) ? text : text.split('\n');
349
+ const h = opts.textHeight ?? 2.5;
350
+ const step = h * (opts.lineSpacing ?? 1.6);
351
+ const pad = opts.padding ?? h * 0.8;
352
+ const widest = Math.max(0, ...lines.map(t => estimateTextWidth(t, h, opts.widthFactor)));
353
+ const width = opts.boxWidth ?? widest + 2 * pad;
354
+ const height = 2 * pad + h * 1.3 + step * (lines.length - 1); // 0.3h leaves room for descenders
355
+ const [left, top] = boxAt;
356
+ const right = left + width, bottom = top - height;
357
+ this._msp.add_lwpolyline([[left, top], [right, top], [right, bottom], [left, bottom]], { layer, color: opts.boxColor ?? opts.color }, true);
358
+ lines.forEach((line, i) => {
359
+ if (!line)
360
+ return;
361
+ const y = top - pad - h - i * step;
362
+ const t = this._msp.add_text(line, { height: h, layer, color: opts.textColor ?? opts.color, style: opts.style });
363
+ if (opts.align === 'center')
364
+ t.set_placement([(left + right) / 2, y], TextEntityAlignment.CENTER);
365
+ else
366
+ t.set_placement([left + pad, y]);
367
+ });
368
+ const inside = anchor[0] >= left && anchor[0] <= right && anchor[1] >= bottom && anchor[1] <= top;
369
+ if (!inside) {
370
+ const midX = (left + right) / 2, midY = (top + bottom) / 2;
371
+ const dist = (p) => Math.hypot(p[0] - anchor[0], p[1] - anchor[1]);
372
+ const attach = [[left, midY], [right, midY], [midX, top], [midX, bottom]]
373
+ .reduce((best, p) => (dist(p) < dist(best) ? p : best));
374
+ const attr = { layer, color: opts.color };
375
+ this._msp.add_lwpolyline([anchor, attach], attr);
376
+ const size = opts.arrowSize ?? h * 0.8;
377
+ if (opts.arrow === 'dot') {
378
+ const ring = Array.from({ length: 16 }, (_, k) => {
379
+ const a = (k * Math.PI) / 8;
380
+ return [anchor[0] + (size / 2) * Math.cos(a), anchor[1] + (size / 2) * Math.sin(a)];
381
+ });
382
+ this._msp.add_hatch('SOLID', [{ type: 'polyline', points: ring, closed: true }], attr);
383
+ }
384
+ else if (opts.arrow === 'arrow') {
385
+ const len = dist(attach) || 1;
386
+ const dx = (attach[0] - anchor[0]) / len, dy = (attach[1] - anchor[1]) / len;
387
+ const bx = anchor[0] + dx * size, by = anchor[1] + dy * size, w = size / 3;
388
+ const p2 = [bx - dy * w, by + dx * w];
389
+ const p3 = [bx + dy * w, by - dx * w];
390
+ this._msp.add_solid([anchor, p2, p3, p3], attr);
391
+ }
392
+ }
393
+ return [left, bottom, right, top];
394
+ }
395
+ /**
396
+ * Add an annotation with a leader.
397
+ *
398
+ * @deprecated MULTILEADER entities written by this library were not
399
+ * AutoCAD-compatible. This now draws `addCallout(vertices[0], text, textPosition)`
400
+ * with an arrowhead; use addCallout directly.
401
+ */
402
+ addMLeader(vertices, text, textPosition, layer = '0', color, opts = {}) {
403
+ this.addCallout(vertices[0] ?? textPosition, text, textPosition, {
404
+ layer, color,
405
+ textHeight: opts.textHeight,
406
+ boxWidth: opts.textWidth,
407
+ arrow: 'arrow',
408
+ arrowSize: opts.arrowSize,
409
+ });
410
+ }
411
+ /**
412
+ * Add a TABLE (drawn as grid of LINE + TEXT entities).
413
+ * Cells may span columns/rows with `colSpan` / `rowSpan` (HTML table rules).
414
+ *
415
+ * @example
416
+ * builder.addTable([100, 500], [20, 40, 50, 50], 8, [
417
+ * [{ text: 'ลำดับ', rowSpan: 2 }, { text: 'หมุด', rowSpan: 2 }, { text: 'พิกัด', colSpan: 2 }],
418
+ * [{ text: 'North' }, { text: 'East' }],
419
+ * [{ text: '1' }, { text: 'BM.07' }, { text: '1,034,018.584' }, { text: '519,395.838' }],
420
+ * ], 'TABLE', 2.5);
421
+ */
422
+ addTable(position, columnWidths, rowHeight, rows, layer = '0', textHeight = 2.5, opts = {}) {
423
+ this._ensureLayer(layer);
424
+ this._msp.add_table({
425
+ position, columnWidths, rowHeight, rows, layer, textHeight,
426
+ rowHeights: opts.rowHeights,
427
+ headerColor: opts.headerColor,
428
+ gridColor: opts.gridColor,
429
+ textColor: opts.textColor,
430
+ });
431
+ }
432
+ // ─── Layout (Paper Space) ───
433
+ /**
434
+ * Add a Paper Space layout for plotting. Paper units are mm; the origin
435
+ * is the lower-left corner of the printable area (inside the margins).
436
+ * A viewport's model-space extent comes from `viewHeight`, or from `scale`
437
+ * (e.g. 12500 for 1:12,500 with the model in meters).
438
+ *
439
+ * @example
440
+ * builder.addLayout('A1', 841, 594, [0, 0, 0, 0], [{
441
+ * center: [420, 297], width: 800, height: 560,
442
+ * viewCenter: [519500, 1033500], scale: 12500,
443
+ * frozenLayers: ['GRID'],
444
+ * }]);
445
+ */
446
+ addLayout(name, paperWidth, paperHeight, margins = [10, 10, 10, 10], viewports = []) {
447
+ this._document.addLayout({
448
+ name, paperWidth, paperHeight, margins, viewports,
449
+ });
450
+ }
451
+ /**
452
+ * Draw into a paper space layout: every builder call made inside `draw`
453
+ * targets that layout. Works with layouts from the template (e.g.
454
+ * 'Layout1') and ones added with addLayout().
455
+ *
456
+ * @example
457
+ * builder.onLayout('A1', b => {
458
+ * b.addText('แผนที่แสดงตำแหน่งหมุดหลักฐาน', 300, 25, 8);
459
+ * b.addTable([560, 200], [15, 30, 45, 45], 7, rows, 'TABLE', 2.5);
460
+ * });
461
+ */
462
+ onLayout(name, draw) {
463
+ this._withTarget(this._document.paperspace(name), () => draw(this));
464
+ }
465
+ // ─── GeoReference ───
466
+ /**
467
+ * Set geographic coordinate reference for the drawing.
468
+ * Embeds CRS info into the DXF HEADER for GIS interoperability.
469
+ *
470
+ * @example
471
+ * // UTM Zone 47N (Thailand)
472
+ * builder.setGeoReference({
473
+ * crsId: 'EPSG:32647',
474
+ * designPoint: [0, 0],
475
+ * referencePoint: [600000, 1700000],
476
+ * northDirection: 0,
477
+ * scaleFactor: 1,
478
+ * coordinateType: 2,
479
+ * });
480
+ */
481
+ setGeoReference(geoRef) {
482
+ this._document.setGeoReference(geoRef);
483
+ }
484
+ // ─── Serialization ───
485
+ /** Serialize to DXF string. ✅ Works everywhere. */
486
+ saveString() { return this._document.toDxfString(); }
487
+ /** Save to file (Node.js only). */
488
+ saveas(filePath) { this._document.saveas(filePath); }
489
+ /** Download DXF in browser (Angular). */
490
+ downloadAs(filename) { this._document.downloadAs(filename); }
491
+ // ─── Private Methods ───
492
+ _withTarget(target, fn) {
493
+ const previous = this._msp;
494
+ this._msp = target;
495
+ try {
496
+ fn();
497
+ }
498
+ finally {
499
+ this._msp = previous;
500
+ }
501
+ }
502
+ _ensureLayer(layer) {
503
+ if (!this._document.hasLayer(layer)) {
504
+ this._document.addLayer(layer, 7, 'CONTINUOUS');
505
+ }
506
+ }
507
+ _initializeLayers() {
508
+ this._document.addLayer('CANAL', 8, 'CONTINUOUS');
509
+ this._document.addLayer('DRAINAGE_CANAL', 7, 'CONTINUOUS');
510
+ this._document.addLayer('DITCH', 6, 'CONTINUOUS');
511
+ this._document.addLayer('BOUNDARY_LINE', 1, 'CONTINUOUS');
512
+ this._document.addLayer('LandPlot', 252, 'CONTINUOUS');
513
+ this._document.addLayer('แปลงเดิม', 252, 'CONTINUOUS');
514
+ this._document.addLayer('GRID', 253, 'CONTINUOUS');
515
+ this._document.addLayer('CONTOUR', 251, 'CONTINUOUS');
516
+ this._document.addLayer('ถนน', 252, 'CONTINUOUS');
517
+ this._document.addLayer('อ่างเก็บน้ำ หนอง บึง', 4, 'CONTINUOUS');
518
+ this._document.addLayer('TEXT', 7, 'CONTINUOUS');
519
+ this._document.addLayer('เลขแปลง', 7, 'CONTINUOUS');
520
+ this._document.addLayer('structure', 4, 'CONTINUOUS');
521
+ this._document.addLayer('STR_CYAN', 4, 'CONTINUOUS');
522
+ this._document.addLayer('0', 7, 'CONTINUOUS');
523
+ }
524
+ _drawFromRegistry(name, x, y, scale, rotation, layer) {
525
+ const blockDef = BLOCK_REGISTRY[name];
526
+ if (!blockDef)
527
+ return;
528
+ for (const entity of blockDef.entities) {
529
+ const entLayer = entity.layer || layer;
530
+ this._ensureLayer(entLayer);
531
+ if (entity.type === 'LINE' && entity.points) {
532
+ const p1 = rotatePoint(entity.points[0][0] * scale, entity.points[0][1] * scale, rotation, x, y);
533
+ const p2 = rotatePoint(entity.points[1][0] * scale, entity.points[1][1] * scale, rotation, x, y);
534
+ this._msp.add_line([p1[0], p1[1]], [p2[0], p2[1]], { layer: entLayer, color: entity.color });
535
+ }
536
+ else if (entity.type === 'CIRCLE' && entity.center) {
537
+ const center = rotatePoint(entity.center[0] * scale, entity.center[1] * scale, rotation, x, y);
538
+ this._msp.add_circle([center[0], center[1]], (entity.radius || 1) * scale, { layer: entLayer, color: entity.color });
539
+ }
540
+ else if (entity.type === 'LWPOLYLINE' && entity.points) {
541
+ const rotatedPoints = entity.points.map(p => {
542
+ const rp = rotatePoint(p[0] * scale, p[1] * scale, rotation, x, y);
543
+ return [rp[0], rp[1]];
544
+ });
545
+ this._msp.add_lwpolyline(rotatedPoints, { layer: entLayer, color: entity.color });
546
+ }
547
+ }
548
+ }
549
+ }