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.
- package/API.md +411 -0
- package/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/core/dxf-builder.d.ts +348 -0
- package/dist/core/dxf-builder.js +549 -0
- package/dist/core/dxf-document.d.ts +220 -0
- package/dist/core/dxf-document.js +529 -0
- package/dist/core/dxf-entities.d.ts +327 -0
- package/dist/core/dxf-entities.js +22 -0
- package/dist/core/dxf-serializer.d.ts +48 -0
- package/dist/core/dxf-serializer.js +595 -0
- package/dist/core/hatch-patterns.d.ts +15 -0
- package/dist/core/hatch-patterns.js +214 -0
- package/dist/core/modelspace.d.ts +106 -0
- package/dist/core/modelspace.js +351 -0
- package/dist/geometry/math.utils.d.ts +8 -0
- package/dist/geometry/math.utils.js +43 -0
- package/dist/geometry/offset.utils.d.ts +32 -0
- package/dist/geometry/offset.utils.js +87 -0
- package/dist/geometry/polyline.utils.d.ts +10 -0
- package/dist/geometry/polyline.utils.js +63 -0
- package/dist/geometry/text.utils.d.ts +15 -0
- package/dist/geometry/text.utils.js +28 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +29 -0
- package/dist/parser/auto-cad-color-index.d.ts +8 -0
- package/dist/parser/auto-cad-color-index.js +265 -0
- package/dist/parser/dim-style-codes.d.ts +5 -0
- package/dist/parser/dim-style-codes.js +32 -0
- package/dist/parser/dxf-parser.d.ts +16 -0
- package/dist/parser/dxf-parser.js +824 -0
- package/dist/parser/entities/3dface.d.ts +5 -0
- package/dist/parser/entities/3dface.js +76 -0
- package/dist/parser/entities/arc.d.ts +5 -0
- package/dist/parser/entities/arc.js +34 -0
- package/dist/parser/entities/attdef.d.ts +5 -0
- package/dist/parser/entities/attdef.js +86 -0
- package/dist/parser/entities/attribute.d.ts +5 -0
- package/dist/parser/entities/attribute.js +106 -0
- package/dist/parser/entities/circle.d.ts +5 -0
- package/dist/parser/entities/circle.js +40 -0
- package/dist/parser/entities/dimension.d.ts +5 -0
- package/dist/parser/entities/dimension.js +65 -0
- package/dist/parser/entities/ellipse.d.ts +5 -0
- package/dist/parser/entities/ellipse.js +42 -0
- package/dist/parser/entities/hatch.d.ts +5 -0
- package/dist/parser/entities/hatch.js +321 -0
- package/dist/parser/entities/insert.d.ts +5 -0
- package/dist/parser/entities/insert.js +52 -0
- package/dist/parser/entities/line.d.ts +5 -0
- package/dist/parser/entities/line.js +30 -0
- package/dist/parser/entities/lwpolyline.d.ts +5 -0
- package/dist/parser/entities/lwpolyline.js +101 -0
- package/dist/parser/entities/mtext.d.ts +5 -0
- package/dist/parser/entities/mtext.js +54 -0
- package/dist/parser/entities/point.d.ts +5 -0
- package/dist/parser/entities/point.js +30 -0
- package/dist/parser/entities/polyline.d.ts +5 -0
- package/dist/parser/entities/polyline.js +85 -0
- package/dist/parser/entities/solid.d.ts +5 -0
- package/dist/parser/entities/solid.js +35 -0
- package/dist/parser/entities/spline.d.ts +5 -0
- package/dist/parser/entities/spline.js +69 -0
- package/dist/parser/entities/text.d.ts +5 -0
- package/dist/parser/entities/text.js +47 -0
- package/dist/parser/entities/vertex.d.ts +5 -0
- package/dist/parser/entities/vertex.js +59 -0
- package/dist/parser/extended-data-parser.d.ts +18 -0
- package/dist/parser/extended-data-parser.js +100 -0
- package/dist/parser/index.d.ts +10 -0
- package/dist/parser/index.js +9 -0
- package/dist/parser/log.d.ts +14 -0
- package/dist/parser/log.js +39 -0
- package/dist/parser/parse-helpers.d.ts +23 -0
- package/dist/parser/parse-helpers.js +128 -0
- package/dist/parser/scanner.d.ts +29 -0
- package/dist/parser/scanner.js +143 -0
- package/dist/parser/types.d.ts +78 -0
- package/dist/parser/types.js +5 -0
- package/dist/symbols/slope-symbol.d.ts +21 -0
- package/dist/symbols/slope-symbol.js +81 -0
- package/dist/symbols/structures.d.ts +9 -0
- package/dist/symbols/structures.js +120 -0
- package/dist/template/blank-document.d.ts +13 -0
- package/dist/template/blank-document.js +13 -0
- package/dist/template/block-registry.d.ts +16 -0
- package/dist/template/block-registry.js +135 -0
- package/dist/template/index.d.ts +17 -0
- package/dist/template/index.js +17 -0
- package/dist/template/template-injector.d.ts +45 -0
- package/dist/template/template-injector.js +487 -0
- package/dist/template/template-reader.d.ts +28 -0
- package/dist/template/template-reader.js +80 -0
- package/dist/types/index.d.ts +24 -0
- package/dist/types/index.js +8 -0
- package/package.json +59 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template injector — emit a DXF by injecting new content into the verbatim
|
|
3
|
+
* sections of a base document: a user template, or the built-in blank R2018
|
|
4
|
+
* document for drawings created from scratch.
|
|
5
|
+
*
|
|
6
|
+
* Every section of the base is preserved (CLASSES, ACDSDATA, … included); we:
|
|
7
|
+
* - allocate new handles above $HANDSEED and write the new $HANDSEED,
|
|
8
|
+
* - set or replace HEADER variables,
|
|
9
|
+
* - add LTYPE / LAYER / STYLE / BLOCK_RECORD records (existing names are kept),
|
|
10
|
+
* - add block definitions and paper space layout blocks to BLOCKS,
|
|
11
|
+
* - append model space and active-layout entities to ENTITIES,
|
|
12
|
+
* - add LAYOUT / IMAGEDEF / reactor objects and their dictionary entries.
|
|
13
|
+
*
|
|
14
|
+
* Only the TABLES, OBJECTS and CLASSES sections are tag-scanned; BLOCKS and
|
|
15
|
+
* ENTITIES (usually the bulk of a template) are edited by string position.
|
|
16
|
+
*/
|
|
17
|
+
import { blockDefinition, blockRecord, fmt, layerRecord, linetypeRecord, nextHandle, peekHandle, resetHandleCounter, serializeEntity, textStyleRecord, } from '../core/dxf-serializer.js';
|
|
18
|
+
/** Split a section into records, each starting at a group code 0 tag. */
|
|
19
|
+
function scanRecords(section) {
|
|
20
|
+
const records = [];
|
|
21
|
+
let cur = null;
|
|
22
|
+
let pos = 0;
|
|
23
|
+
while (pos < section.length) {
|
|
24
|
+
const codeEnd = section.indexOf('\n', pos);
|
|
25
|
+
if (codeEnd === -1)
|
|
26
|
+
break;
|
|
27
|
+
let valueEnd = section.indexOf('\n', codeEnd + 1);
|
|
28
|
+
if (valueEnd === -1)
|
|
29
|
+
valueEnd = section.length;
|
|
30
|
+
const code = section.substring(pos, codeEnd);
|
|
31
|
+
const value = section.substring(codeEnd + 1, valueEnd);
|
|
32
|
+
if (code === '0') {
|
|
33
|
+
if (cur) {
|
|
34
|
+
cur.end = pos;
|
|
35
|
+
records.push(cur);
|
|
36
|
+
}
|
|
37
|
+
cur = { type: value, handle: '', tags: [], start: pos, end: section.length };
|
|
38
|
+
}
|
|
39
|
+
else if (cur) {
|
|
40
|
+
if (!cur.handle && (code === '5' || code === '105'))
|
|
41
|
+
cur.handle = value;
|
|
42
|
+
cur.tags.push([code, value]);
|
|
43
|
+
}
|
|
44
|
+
pos = valueEnd + 1;
|
|
45
|
+
}
|
|
46
|
+
if (cur)
|
|
47
|
+
records.push(cur);
|
|
48
|
+
return records;
|
|
49
|
+
}
|
|
50
|
+
function tag(r, code) {
|
|
51
|
+
return r.tags.find(t => t[0] === code)?.[1];
|
|
52
|
+
}
|
|
53
|
+
function dictEntries(r) {
|
|
54
|
+
const out = new Map();
|
|
55
|
+
for (let i = 0; i < r.tags.length - 1; i++) {
|
|
56
|
+
const next = r.tags[i + 1][0];
|
|
57
|
+
if (r.tags[i][0] === '3' && (next === '350' || next === '360'))
|
|
58
|
+
out.set(r.tags[i][1], r.tags[i + 1][1]);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
function scanTables(tables) {
|
|
63
|
+
const out = new Map();
|
|
64
|
+
let cur = null;
|
|
65
|
+
for (const r of scanRecords(tables)) {
|
|
66
|
+
if (r.type === 'TABLE') {
|
|
67
|
+
cur = { handle: r.handle, endtab: -1, names: new Map() };
|
|
68
|
+
out.set((tag(r, '2') ?? '').toUpperCase(), cur);
|
|
69
|
+
}
|
|
70
|
+
else if (r.type === 'ENDTAB') {
|
|
71
|
+
if (cur)
|
|
72
|
+
cur.endtab = r.start;
|
|
73
|
+
cur = null;
|
|
74
|
+
}
|
|
75
|
+
else if (cur) {
|
|
76
|
+
const name = tag(r, '2');
|
|
77
|
+
if (name !== undefined)
|
|
78
|
+
cur.names.set(name.toUpperCase(), { name, handle: r.handle });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
function scanLayouts(objects) {
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const r of objects) {
|
|
86
|
+
if (r.type !== 'LAYOUT')
|
|
87
|
+
continue;
|
|
88
|
+
const i = r.tags.findIndex(t => t[0] === '100' && t[1] === 'AcDbLayout');
|
|
89
|
+
if (i === -1)
|
|
90
|
+
continue;
|
|
91
|
+
const rest = r.tags.slice(i + 1);
|
|
92
|
+
out.push({
|
|
93
|
+
name: rest.find(t => t[0] === '1')?.[1] ?? '',
|
|
94
|
+
handle: r.handle,
|
|
95
|
+
blockRecord: rest.find(t => t[0] === '330')?.[1] ?? '',
|
|
96
|
+
tabOrder: Number(rest.find(t => t[0] === '71')?.[1] ?? 0),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
/** Names of the layouts (tabs) defined in a base document, including "Model". */
|
|
102
|
+
export function listLayoutNames(rawSections) {
|
|
103
|
+
const objects = rawSections.get('OBJECTS');
|
|
104
|
+
return objects ? scanLayouts(scanRecords(objects)).map(l => l.name) : [];
|
|
105
|
+
}
|
|
106
|
+
function maxHandle(raw) {
|
|
107
|
+
let max = 0;
|
|
108
|
+
for (const section of raw.values()) {
|
|
109
|
+
const lines = section.split('\n');
|
|
110
|
+
for (let i = 0; i + 1 < lines.length; i += 2) {
|
|
111
|
+
if (lines[i] === '5' || lines[i] === '105')
|
|
112
|
+
max = Math.max(max, parseInt(lines[i + 1], 16) || 0);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return max;
|
|
116
|
+
}
|
|
117
|
+
// ─── Section editing ───
|
|
118
|
+
/** Batched string insertions, applied in one pass. Inserted text must end with "\n". */
|
|
119
|
+
class Edits {
|
|
120
|
+
items = [];
|
|
121
|
+
insert(at, text) {
|
|
122
|
+
this.items.push({ at, text, seq: this.items.length });
|
|
123
|
+
}
|
|
124
|
+
apply(s) {
|
|
125
|
+
if (!this.items.length)
|
|
126
|
+
return s;
|
|
127
|
+
const sorted = [...this.items].sort((a, b) => a.at - b.at || a.seq - b.seq);
|
|
128
|
+
const out = [];
|
|
129
|
+
let pos = 0;
|
|
130
|
+
for (const it of sorted) {
|
|
131
|
+
out.push(s.slice(pos, it.at), it.text);
|
|
132
|
+
pos = it.at;
|
|
133
|
+
}
|
|
134
|
+
out.push(s.slice(pos));
|
|
135
|
+
return out.join('');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function insertBeforeEndsec(section, text) {
|
|
139
|
+
if (!text)
|
|
140
|
+
return section;
|
|
141
|
+
const i = section.lastIndexOf('\n0\nENDSEC');
|
|
142
|
+
return i === -1 ? section : section.slice(0, i + 1) + text + '\n' + section.slice(i + 1);
|
|
143
|
+
}
|
|
144
|
+
function emptySection(name) {
|
|
145
|
+
return `0\nSECTION\n2\n${name}\n0\nENDSEC`;
|
|
146
|
+
}
|
|
147
|
+
/** Offset of the ENDBLK record closing block `name`, or -1. */
|
|
148
|
+
function findBlockEnd(blocks, name) {
|
|
149
|
+
const begin = blocks.indexOf(`\nAcDbBlockBegin\n2\n${name}\n`);
|
|
150
|
+
if (begin === -1)
|
|
151
|
+
return -1;
|
|
152
|
+
const end = blocks.indexOf('\n0\nENDBLK\n', begin);
|
|
153
|
+
return end === -1 ? -1 : end + 1;
|
|
154
|
+
}
|
|
155
|
+
function readHeaderVar(header, name) {
|
|
156
|
+
const lines = header.split('\n');
|
|
157
|
+
for (let i = 4; i + 3 < lines.length; i += 2) {
|
|
158
|
+
if (lines[i] === '9' && lines[i + 1] === name)
|
|
159
|
+
return lines[i + 3];
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
/** Set (replace or append) a HEADER variable, pair-aware. */
|
|
164
|
+
function setHeaderVar(header, name, values) {
|
|
165
|
+
const lines = header.split('\n');
|
|
166
|
+
const block = ['9', name, ...values.flatMap(v => [String(v.code), v.value])];
|
|
167
|
+
let start = -1;
|
|
168
|
+
// lines: 0 SECTION 2 HEADER (9 $NAME code value …)* 0 ENDSEC
|
|
169
|
+
for (let i = 4; i + 1 < lines.length; i += 2) {
|
|
170
|
+
if (lines[i] !== '9' && lines[i] !== '0')
|
|
171
|
+
continue;
|
|
172
|
+
if (start !== -1) {
|
|
173
|
+
lines.splice(start, i - start, ...block);
|
|
174
|
+
return lines.join('\n');
|
|
175
|
+
}
|
|
176
|
+
if (lines[i] === '9' && lines[i + 1] === name)
|
|
177
|
+
start = i;
|
|
178
|
+
}
|
|
179
|
+
lines.splice(lines.length - 2, 0, ...block);
|
|
180
|
+
return lines.join('\n');
|
|
181
|
+
}
|
|
182
|
+
// ─── Object writers ───
|
|
183
|
+
const CLASS_DEFS = {
|
|
184
|
+
IMAGE: ['AcDbRasterImage', 'ISM', 2175, 0, 1],
|
|
185
|
+
IMAGEDEF: ['AcDbRasterImageDef', 'ISM', 0, 0, 0],
|
|
186
|
+
IMAGEDEF_REACTOR: ['AcDbRasterImageDefReactor', 'ISM', 1, 0, 0],
|
|
187
|
+
RASTERVARIABLES: ['AcDbRasterVariables', 'ISM', 0, 0, 0],
|
|
188
|
+
};
|
|
189
|
+
function classRecord(name) {
|
|
190
|
+
const [cpp, app, flags, wasProxy, isEntity] = CLASS_DEFS[name];
|
|
191
|
+
return ['0', 'CLASS', '1', name, '2', cpp, '3', app, '90', String(flags), '91', '0',
|
|
192
|
+
'280', String(wasProxy), '281', String(isEntity)].join('\n');
|
|
193
|
+
}
|
|
194
|
+
function dictionaryObject(handle, owner, entries) {
|
|
195
|
+
return ['0', 'DICTIONARY', '5', handle, '102', '{ACAD_REACTORS', '330', owner, '102', '}',
|
|
196
|
+
'330', owner, '100', 'AcDbDictionary', '281', '1', ...entries].join('\n');
|
|
197
|
+
}
|
|
198
|
+
function imageDefObject(handle, owner, path, image, reactors) {
|
|
199
|
+
return ['0', 'IMAGEDEF', '5', handle,
|
|
200
|
+
'102', '{ACAD_REACTORS', '330', owner, ...reactors.flatMap(r => ['330', r]), '102', '}',
|
|
201
|
+
'330', owner, '100', 'AcDbRasterImageDef', '90', '0', '1', path,
|
|
202
|
+
'10', String(image.imageSize[0]), '20', String(image.imageSize[1]),
|
|
203
|
+
'11', fmt(Math.hypot(...image.uVector)), '21', fmt(Math.hypot(...image.vVector)),
|
|
204
|
+
'280', '1', '281', '0'].join('\n');
|
|
205
|
+
}
|
|
206
|
+
function imageDefReactorObject(handle, imageHandle) {
|
|
207
|
+
return ['0', 'IMAGEDEF_REACTOR', '5', handle, '330', imageHandle,
|
|
208
|
+
'100', 'AcDbRasterImageDefReactor', '90', '2', '330', imageHandle].join('\n');
|
|
209
|
+
}
|
|
210
|
+
/** $INSUNITS → RASTERVARIABLES units (0 none, 1 mm, 2 cm, 3 m, 4 km, 5 in, 6 ft, 7 yd, 8 mi). */
|
|
211
|
+
const RASTER_UNITS = { '4': 1, '5': 2, '6': 3, '7': 4, '1': 5, '2': 6, '10': 7, '3': 8 };
|
|
212
|
+
function rasterVariablesObject(handle, owner, insUnits) {
|
|
213
|
+
return ['0', 'RASTERVARIABLES', '5', handle, '102', '{ACAD_REACTORS', '330', owner, '102', '}',
|
|
214
|
+
'330', owner, '100', 'AcDbRasterVariables', '90', '0',
|
|
215
|
+
'70', '0', '71', '1', '72', String(RASTER_UNITS[insUnits ?? ''] ?? 0)].join('\n');
|
|
216
|
+
}
|
|
217
|
+
function layoutObject(def, handle, owner, blockRecordHandle, viewportHandle, tabOrder) {
|
|
218
|
+
const [ml, mb, mr, mt] = def.margins;
|
|
219
|
+
const w = def.paperWidth, h = def.paperHeight;
|
|
220
|
+
return ['0', 'LAYOUT', '5', handle, '102', '{ACAD_REACTORS', '330', owner, '102', '}', '330', owner,
|
|
221
|
+
'100', 'AcDbPlotSettings',
|
|
222
|
+
'1', '', '2', 'DWG To PDF.pc3', '4', `onfarm_(${w.toFixed(2)}_x_${h.toFixed(2)}_MM)`, '6', '',
|
|
223
|
+
'40', fmt(ml), '41', fmt(mb), '42', fmt(mr), '43', fmt(mt), '44', fmt(w), '45', fmt(h),
|
|
224
|
+
'46', '0', '47', '0', '48', '0', '49', '0', '140', '0', '141', '0', '142', '1', '143', '1',
|
|
225
|
+
'70', '672', '72', '1', '73', '0', '74', '5', '7', '', '75', '16', '147', '1',
|
|
226
|
+
'76', '0', '77', '2', '78', '300', '148', '0', '149', '0',
|
|
227
|
+
'100', 'AcDbLayout', '1', def.name, '70', '1', '71', String(tabOrder),
|
|
228
|
+
'10', fmt(-ml), '20', fmt(-mb), '11', fmt(w - ml), '21', fmt(h - mb),
|
|
229
|
+
'12', '0', '22', '0', '32', '0',
|
|
230
|
+
'14', '1.0E+20', '24', '1.0E+20', '34', '1.0E+20',
|
|
231
|
+
'15', '-1.0E+20', '25', '-1.0E+20', '35', '-1.0E+20',
|
|
232
|
+
'146', '0', '13', '0', '23', '0', '33', '0',
|
|
233
|
+
'16', '1', '26', '0', '36', '0', '17', '0', '27', '1', '37', '0',
|
|
234
|
+
'76', '0', '330', blockRecordHandle, '331', viewportHandle].join('\n');
|
|
235
|
+
}
|
|
236
|
+
/** The overall paper space viewport AutoCAD keeps as viewport #1 of a layout. */
|
|
237
|
+
function mainViewport(def, layer) {
|
|
238
|
+
const [ml, mb, mr, mt] = def.margins;
|
|
239
|
+
const center = [(def.paperWidth - ml - mr) / 2, (def.paperHeight - mb - mt) / 2];
|
|
240
|
+
return {
|
|
241
|
+
type: 'VIEWPORT', layer, handle: nextHandle(), center,
|
|
242
|
+
width: def.paperWidth * 1.1, height: def.paperHeight * 1.1,
|
|
243
|
+
viewCenter: center, viewHeight: def.paperHeight * 1.1, status: 1, id: 1,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function viewportFromDef(v, id, layer) {
|
|
247
|
+
const viewHeight = v.viewHeight ?? (v.scale !== undefined ? v.height * v.scale * (v.unitFactor ?? 0.001) : undefined);
|
|
248
|
+
if (viewHeight === undefined)
|
|
249
|
+
throw new Error('A layout viewport needs `viewHeight` or `scale`.');
|
|
250
|
+
return {
|
|
251
|
+
type: 'VIEWPORT', layer, center: v.center, width: v.width, height: v.height,
|
|
252
|
+
viewCenter: v.viewCenter, viewHeight, status: id, id,
|
|
253
|
+
frozenLayers: v.frozenLayers, locked: v.locked,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
// ─── Main entry ───
|
|
257
|
+
export function serializeDocument(doc) {
|
|
258
|
+
const raw = doc.rawSections;
|
|
259
|
+
let header = raw.get('HEADER') ?? emptySection('HEADER');
|
|
260
|
+
const seed = parseInt(readHeaderVar(header, '$HANDSEED') ?? '', 16);
|
|
261
|
+
resetHandleCounter(Number.isFinite(seed) && seed > 0 ? seed : maxHandle(raw) + 1);
|
|
262
|
+
const tablesRaw = raw.get('TABLES') ?? '';
|
|
263
|
+
const objectsRaw = raw.get('OBJECTS') ?? '';
|
|
264
|
+
const blocksRaw = raw.get('BLOCKS') ?? emptySection('BLOCKS');
|
|
265
|
+
const tableEdits = new Edits();
|
|
266
|
+
const objectEdits = new Edits();
|
|
267
|
+
const blockEdits = new Edits();
|
|
268
|
+
const newObjects = [];
|
|
269
|
+
const newBlocks = [];
|
|
270
|
+
let classes = raw.get('CLASSES');
|
|
271
|
+
const tables = scanTables(tablesRaw);
|
|
272
|
+
const objects = scanRecords(objectsRaw);
|
|
273
|
+
const objectsByHandle = new Map(objects.filter(r => r.handle).map(r => [r.handle, r]));
|
|
274
|
+
const root = objects.find(r => r.type === 'DICTIONARY');
|
|
275
|
+
const rootEntries = root ? dictEntries(root) : new Map();
|
|
276
|
+
/** Object named `name` in the root dictionary, if present. */
|
|
277
|
+
const rootObject = (name) => {
|
|
278
|
+
const handle = rootEntries.get(name);
|
|
279
|
+
return handle ? objectsByHandle.get(handle) : undefined;
|
|
280
|
+
};
|
|
281
|
+
const dictEntry = (dictName, entry) => {
|
|
282
|
+
const d = rootObject(dictName);
|
|
283
|
+
return d ? dictEntries(d).get(entry) : undefined;
|
|
284
|
+
};
|
|
285
|
+
const blockRecords = tables.get('BLOCK_RECORD');
|
|
286
|
+
const requireStructure = (feature) => {
|
|
287
|
+
if (!blockRecords || !root) {
|
|
288
|
+
throw new Error(`${feature} need a DXF R2000+ base document (BLOCK_RECORD table and OBJECTS section).`);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
/** Add a table record unless the name exists; returns the record handle. */
|
|
292
|
+
const addRecord = (tableName, name, write) => {
|
|
293
|
+
const table = tables.get(tableName);
|
|
294
|
+
if (!table)
|
|
295
|
+
return undefined;
|
|
296
|
+
const existing = table.names.get(name.toUpperCase());
|
|
297
|
+
if (existing)
|
|
298
|
+
return existing.handle;
|
|
299
|
+
const handle = nextHandle();
|
|
300
|
+
table.names.set(name.toUpperCase(), { name, handle });
|
|
301
|
+
tableEdits.insert(table.endtab, write(handle, table.handle) + '\n');
|
|
302
|
+
return handle;
|
|
303
|
+
};
|
|
304
|
+
// ── Linetypes, layers, text styles ──
|
|
305
|
+
for (const lt of doc.linetypes ?? [])
|
|
306
|
+
addRecord('LTYPE', lt.name, (h, o) => linetypeRecord(lt, h, o));
|
|
307
|
+
const linetypes = tables.get('LTYPE');
|
|
308
|
+
const layerRefs = { plotStyle: dictEntry('ACAD_PLOTSTYLENAME', 'Normal'), material: dictEntry('ACAD_MATERIAL', 'Global') };
|
|
309
|
+
for (const l of doc.layers ?? []) {
|
|
310
|
+
const known = !linetypes || linetypes.names.has((l.linetype || '').toUpperCase());
|
|
311
|
+
const layer = known ? l : { ...l, linetype: 'Continuous' };
|
|
312
|
+
addRecord('LAYER', l.name, (h, o) => layerRecord(layer, h, o, layerRefs));
|
|
313
|
+
}
|
|
314
|
+
for (const s of doc.styles ?? [])
|
|
315
|
+
addRecord('STYLE', s.name, (h, o) => textStyleRecord(s, h, o));
|
|
316
|
+
const layerHandles = new Map([...(tables.get('LAYER')?.names ?? new Map())].map(([k, v]) => [k, v.handle]));
|
|
317
|
+
// ── Block definitions ──
|
|
318
|
+
const blocks = doc.blocks ?? [];
|
|
319
|
+
if (blocks.length)
|
|
320
|
+
requireStructure('Block definitions');
|
|
321
|
+
const blockHandles = blocks.map(b => {
|
|
322
|
+
if (blockRecords.names.has(b.name.toUpperCase())) {
|
|
323
|
+
throw new Error(`Block "${b.name}" already exists in the base document.`);
|
|
324
|
+
}
|
|
325
|
+
return addRecord('BLOCK_RECORD', b.name, (h, o) => blockRecord(b.name, h, o));
|
|
326
|
+
});
|
|
327
|
+
const layouts = new Map();
|
|
328
|
+
const existingLayouts = scanLayouts(objects);
|
|
329
|
+
for (const l of existingLayouts) {
|
|
330
|
+
const br = [...(blockRecords?.names.values() ?? [])].find(v => v.handle === l.blockRecord);
|
|
331
|
+
layouts.set(l.name.toUpperCase(), {
|
|
332
|
+
name: l.name, blockRecord: l.blockRecord, blockName: br?.name ?? '',
|
|
333
|
+
isNew: false, entities: [], nextViewportId: 50,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
const viewportLayer = layerHandles.has('DEFPOINTS') ? 'Defpoints' : '0';
|
|
337
|
+
let tabOrder = Math.max(0, ...existingLayouts.map(l => l.tabOrder));
|
|
338
|
+
for (const def of doc.layouts ?? []) {
|
|
339
|
+
requireStructure('Layouts');
|
|
340
|
+
const layoutDict = rootObject('ACAD_LAYOUT');
|
|
341
|
+
if (!layoutDict)
|
|
342
|
+
throw new Error('The base document has no ACAD_LAYOUT dictionary.');
|
|
343
|
+
if (layouts.has(def.name.toUpperCase()))
|
|
344
|
+
throw new Error(`Layout "${def.name}" already exists.`);
|
|
345
|
+
let n = 0;
|
|
346
|
+
while (blockRecords.names.has(`*PAPER_SPACE${n}`))
|
|
347
|
+
n++;
|
|
348
|
+
const blockName = `*Paper_Space${n}`;
|
|
349
|
+
const handle = nextHandle();
|
|
350
|
+
const br = addRecord('BLOCK_RECORD', blockName, (h, o) => blockRecord(blockName, h, o, handle));
|
|
351
|
+
const main = mainViewport(def, viewportLayer);
|
|
352
|
+
const viewports = def.viewports.map((v, i) => viewportFromDef(v, i + 2, viewportLayer));
|
|
353
|
+
layouts.set(def.name.toUpperCase(), {
|
|
354
|
+
name: def.name, blockRecord: br, blockName, isNew: true,
|
|
355
|
+
entities: [main, ...viewports], nextViewportId: viewports.length + 2,
|
|
356
|
+
});
|
|
357
|
+
objectEdits.insert(layoutDict.end, `3\n${def.name}\n350\n${handle}\n`);
|
|
358
|
+
newObjects.push(layoutObject(def, handle, layoutDict.handle, br, main.handle, ++tabOrder));
|
|
359
|
+
}
|
|
360
|
+
for (const [name, entities] of doc.paperspace ?? []) {
|
|
361
|
+
const layout = layouts.get(name.toUpperCase());
|
|
362
|
+
if (!layout)
|
|
363
|
+
throw new Error(`Unknown layout "${name}".`);
|
|
364
|
+
for (const e of entities) {
|
|
365
|
+
layout.entities.push(e.type === 'VIEWPORT' && e.id === undefined ? { ...e, id: layout.nextViewportId++ } : e);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// ── Images: IMAGEDEF per file, reactor per IMAGE ──
|
|
369
|
+
const images = new Map();
|
|
370
|
+
const imageDefs = new Map();
|
|
371
|
+
const entityLists = [doc.modelspace, ...blocks.map(b => b.entities), ...[...layouts.values()].map(l => l.entities)];
|
|
372
|
+
for (const list of entityLists) {
|
|
373
|
+
for (const e of list) {
|
|
374
|
+
if (e.type !== 'IMAGE' || images.has(e))
|
|
375
|
+
continue;
|
|
376
|
+
let def = imageDefs.get(e.imagePath);
|
|
377
|
+
if (!def) {
|
|
378
|
+
def = { handle: nextHandle(), image: e, reactors: [] };
|
|
379
|
+
imageDefs.set(e.imagePath, def);
|
|
380
|
+
}
|
|
381
|
+
const refs = { handle: nextHandle(), defHandle: def.handle, reactorHandle: nextHandle() };
|
|
382
|
+
def.reactors.push(refs.reactorHandle);
|
|
383
|
+
images.set(e, refs);
|
|
384
|
+
newObjects.push(imageDefReactorObject(refs.reactorHandle, refs.handle));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (imageDefs.size) {
|
|
388
|
+
requireStructure('Images');
|
|
389
|
+
const existingDict = rootObject('ACAD_IMAGE_DICT');
|
|
390
|
+
const dictHandle = existingDict?.handle ?? nextHandle();
|
|
391
|
+
const used = new Set([...(existingDict ? dictEntries(existingDict).keys() : [])].map(n => n.toUpperCase()));
|
|
392
|
+
const entries = [];
|
|
393
|
+
for (const [path, def] of imageDefs) {
|
|
394
|
+
const base = path.split(/[\\/]/).pop().replace(/\.[^.]*$/, '') || 'image';
|
|
395
|
+
let name = base;
|
|
396
|
+
for (let i = 1; used.has(name.toUpperCase()); i++)
|
|
397
|
+
name = `${base}_${i}`;
|
|
398
|
+
used.add(name.toUpperCase());
|
|
399
|
+
entries.push('3', name, '350', def.handle);
|
|
400
|
+
newObjects.push(imageDefObject(def.handle, dictHandle, path, def.image, def.reactors));
|
|
401
|
+
}
|
|
402
|
+
if (existingDict) {
|
|
403
|
+
objectEdits.insert(existingDict.end, entries.join('\n') + '\n');
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
newObjects.push(dictionaryObject(dictHandle, root.handle, entries));
|
|
407
|
+
objectEdits.insert(root.end, `3\nACAD_IMAGE_DICT\n350\n${dictHandle}\n`);
|
|
408
|
+
}
|
|
409
|
+
if (!rootEntries.has('ACAD_IMAGE_VARS')) {
|
|
410
|
+
const h = nextHandle();
|
|
411
|
+
newObjects.push(rasterVariablesObject(h, root.handle, readHeaderVar(header, '$INSUNITS')));
|
|
412
|
+
objectEdits.insert(root.end, `3\nACAD_IMAGE_VARS\n350\n${h}\n`);
|
|
413
|
+
}
|
|
414
|
+
const present = new Set(scanRecords(classes ?? '').filter(r => r.type === 'CLASS').map(r => tag(r, '1')));
|
|
415
|
+
const missing = Object.keys(CLASS_DEFS).filter(n => !present.has(n)).map(classRecord).join('\n');
|
|
416
|
+
if (missing)
|
|
417
|
+
classes = insertBeforeEndsec(classes ?? emptySection('CLASSES'), missing);
|
|
418
|
+
}
|
|
419
|
+
// ── Serialize content ──
|
|
420
|
+
const base = { images, layerHandles };
|
|
421
|
+
const modelOwner = blockRecords?.names.get('*MODEL_SPACE')?.handle;
|
|
422
|
+
const entityText = doc.modelspace.map(e => serializeEntity(e, { ...base, owner: modelOwner }));
|
|
423
|
+
blocks.forEach((b, i) => newBlocks.push(blockDefinition(b.name, blockHandles[i], b.basePoint, b.entities, base)));
|
|
424
|
+
for (const layout of layouts.values()) {
|
|
425
|
+
if (!layout.entities.length)
|
|
426
|
+
continue;
|
|
427
|
+
const blockUpper = layout.blockName.toUpperCase();
|
|
428
|
+
if (blockUpper === '*MODEL_SPACE') {
|
|
429
|
+
entityText.push(...layout.entities.map(e => serializeEntity(e, { ...base, owner: modelOwner })));
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const ctx = { ...base, owner: layout.blockRecord, paperSpace: true };
|
|
433
|
+
if (layout.isNew) {
|
|
434
|
+
newBlocks.push(blockDefinition(layout.blockName, layout.blockRecord, [0, 0], layout.entities, ctx));
|
|
435
|
+
}
|
|
436
|
+
else if (blockUpper === '*PAPER_SPACE') {
|
|
437
|
+
entityText.push(...layout.entities.map(e => serializeEntity(e, ctx)));
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
const at = findBlockEnd(blocksRaw, layout.blockName);
|
|
441
|
+
if (at === -1)
|
|
442
|
+
throw new Error(`Paper space block for layout "${layout.name}" not found.`);
|
|
443
|
+
blockEdits.insert(at, layout.entities.map(e => serializeEntity(e, ctx)).join('\n') + '\n');
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// ── Header (last: $HANDSEED must cover every handle allocated above) ──
|
|
447
|
+
for (const [name, values] of doc.headerVars ?? [])
|
|
448
|
+
header = setHeaderVar(header, name, values);
|
|
449
|
+
header = setHeaderVar(header, '$HANDSEED', [{ code: 5, value: peekHandle() }]);
|
|
450
|
+
// ── Assemble in base-document section order ──
|
|
451
|
+
if (newObjects.length && objectsRaw) {
|
|
452
|
+
objectEdits.insert(objectsRaw.lastIndexOf('\n0\nENDSEC') + 1, newObjects.join('\n') + '\n');
|
|
453
|
+
}
|
|
454
|
+
const order = [...raw.keys()];
|
|
455
|
+
const ensureSection = (name, after) => {
|
|
456
|
+
if (order.includes(name))
|
|
457
|
+
return;
|
|
458
|
+
const i = order.indexOf(after);
|
|
459
|
+
order.splice(i === -1 ? order.length : i + 1, 0, name);
|
|
460
|
+
};
|
|
461
|
+
if (!order.includes('HEADER'))
|
|
462
|
+
order.unshift('HEADER');
|
|
463
|
+
if (classes)
|
|
464
|
+
ensureSection('CLASSES', 'HEADER');
|
|
465
|
+
if (newBlocks.length)
|
|
466
|
+
ensureSection('BLOCKS', order.includes('TABLES') ? 'TABLES' : 'HEADER');
|
|
467
|
+
ensureSection('ENTITIES', order.includes('BLOCKS') ? 'BLOCKS' : 'HEADER');
|
|
468
|
+
const body = (name) => {
|
|
469
|
+
switch (name) {
|
|
470
|
+
case 'HEADER': return header;
|
|
471
|
+
case 'CLASSES': return classes;
|
|
472
|
+
case 'TABLES': return tableEdits.apply(tablesRaw);
|
|
473
|
+
case 'BLOCKS': return insertBeforeEndsec(blockEdits.apply(blocksRaw), newBlocks.join('\n'));
|
|
474
|
+
case 'ENTITIES': return insertBeforeEndsec(raw.get('ENTITIES') ?? emptySection('ENTITIES'), entityText.join('\n'));
|
|
475
|
+
case 'OBJECTS': return objectEdits.apply(objectsRaw);
|
|
476
|
+
default: return raw.get(name);
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
return order.map(body).join('\n') + '\n0\nEOF\n';
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Inject entities and layers into a template.
|
|
483
|
+
* @deprecated Use `serializeDocument`, which also handles blocks, images and layouts.
|
|
484
|
+
*/
|
|
485
|
+
export function serializeWithTemplate(rawSections, newEntities, newLayers, headerVars) {
|
|
486
|
+
return serializeDocument({ rawSections, modelspace: newEntities, layers: newLayers, headerVars });
|
|
487
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template reader — parse a DXF template string into structured data.
|
|
3
|
+
*
|
|
4
|
+
* Two parallel views of the template are produced:
|
|
5
|
+
* 1. `parsed` — the internal parser's structured object (for indexing).
|
|
6
|
+
* 2. `rawSections` — the verbatim text of each SECTION, so the template can be
|
|
7
|
+
* re-emitted byte-for-byte and new entities injected into it
|
|
8
|
+
* (see template-injector). This is what preserves the
|
|
9
|
+
* original AutoCAD/Civil3D layers, blocks and styles exactly.
|
|
10
|
+
*/
|
|
11
|
+
import { ParsedDxf } from '../parser/index.js';
|
|
12
|
+
import { LayerDef, TextStyleDef } from '../core/dxf-entities.js';
|
|
13
|
+
export interface TemplateData {
|
|
14
|
+
/** Structured parse result (may be partial if parsing warned). */
|
|
15
|
+
parsed: ParsedDxf | null;
|
|
16
|
+
/** Verbatim text of each SECTION, keyed by section name (HEADER, TABLES, …). */
|
|
17
|
+
rawSections: Map<string, string>;
|
|
18
|
+
/** Block names defined in the template. */
|
|
19
|
+
blockNames: Set<string>;
|
|
20
|
+
/** Layer names defined in the template. */
|
|
21
|
+
layerNames: Set<string>;
|
|
22
|
+
/** Indexed layer definitions. */
|
|
23
|
+
layers: Map<string, LayerDef>;
|
|
24
|
+
/** Indexed text style definitions. */
|
|
25
|
+
styles: Map<string, TextStyleDef>;
|
|
26
|
+
}
|
|
27
|
+
/** Parse a DXF template string. */
|
|
28
|
+
export declare function readTemplate(content: string): TemplateData;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template reader — parse a DXF template string into structured data.
|
|
3
|
+
*
|
|
4
|
+
* Two parallel views of the template are produced:
|
|
5
|
+
* 1. `parsed` — the internal parser's structured object (for indexing).
|
|
6
|
+
* 2. `rawSections` — the verbatim text of each SECTION, so the template can be
|
|
7
|
+
* re-emitted byte-for-byte and new entities injected into it
|
|
8
|
+
* (see template-injector). This is what preserves the
|
|
9
|
+
* original AutoCAD/Civil3D layers, blocks and styles exactly.
|
|
10
|
+
*/
|
|
11
|
+
import { DxfParser } from '../parser/index.js';
|
|
12
|
+
/** Parse a DXF template string. */
|
|
13
|
+
export function readTemplate(content) {
|
|
14
|
+
const data = {
|
|
15
|
+
parsed: null,
|
|
16
|
+
rawSections: new Map(),
|
|
17
|
+
blockNames: new Set(),
|
|
18
|
+
layerNames: new Set(),
|
|
19
|
+
layers: new Map(),
|
|
20
|
+
styles: new Map(),
|
|
21
|
+
};
|
|
22
|
+
// 1. Structured parse (best-effort).
|
|
23
|
+
const parser = new DxfParser();
|
|
24
|
+
try {
|
|
25
|
+
data.parsed = parser.parseSync(content);
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
console.warn('DXF parser warning: partial parse.', e.message);
|
|
29
|
+
}
|
|
30
|
+
// 2. Extract raw sections for pass-through serialization.
|
|
31
|
+
extractRawSections(content, data.rawSections);
|
|
32
|
+
// 3. Index blocks / layers / styles from the parsed template.
|
|
33
|
+
indexTemplate(data);
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
function extractRawSections(content, into) {
|
|
37
|
+
const normalized = content
|
|
38
|
+
.replace(/\r\n/g, '\n')
|
|
39
|
+
.replace(/\r/g, '\n')
|
|
40
|
+
.split('\n')
|
|
41
|
+
.map(line => line.trim())
|
|
42
|
+
.join('\n');
|
|
43
|
+
const sectionRegex = /0\nSECTION\n2\n(\w+)\n([\s\S]*?)0\nENDSEC/g;
|
|
44
|
+
let match;
|
|
45
|
+
while ((match = sectionRegex.exec(normalized)) !== null) {
|
|
46
|
+
into.set(match[1], match[0]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function indexTemplate(data) {
|
|
50
|
+
const parsed = data.parsed;
|
|
51
|
+
if (!parsed)
|
|
52
|
+
return;
|
|
53
|
+
if (parsed.blocks) {
|
|
54
|
+
for (const blockName in parsed.blocks) {
|
|
55
|
+
data.blockNames.add(blockName);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (parsed.tables?.layer?.layers) {
|
|
59
|
+
for (const layerName in parsed.tables.layer.layers) {
|
|
60
|
+
data.layerNames.add(layerName);
|
|
61
|
+
const ld = parsed.tables.layer.layers[layerName];
|
|
62
|
+
data.layers.set(layerName, {
|
|
63
|
+
name: layerName,
|
|
64
|
+
color: ld.color || 7,
|
|
65
|
+
linetype: ld.lineType || 'CONTINUOUS',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (parsed.tables?.style?.styles) {
|
|
70
|
+
// The parser keys STYLE records by name (not an array).
|
|
71
|
+
for (const style of Object.values(parsed.tables.style.styles)) {
|
|
72
|
+
if (style.styleName) {
|
|
73
|
+
data.styles.set(style.styleName, {
|
|
74
|
+
name: style.styleName,
|
|
75
|
+
font: style.font || style.styleName,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic, API-agnostic types for the DXF library.
|
|
3
|
+
*
|
|
4
|
+
* These describe pure geometry/styling primitives only. They deliberately
|
|
5
|
+
* carry no knowledge of any backend API or project-specific JSON shape —
|
|
6
|
+
* the consuming application maps its own data into these types.
|
|
7
|
+
*/
|
|
8
|
+
/** A 2D coordinate: [x, y]. */
|
|
9
|
+
export type Coordinate = [number, number];
|
|
10
|
+
/** A 3D coordinate: [x, y, z]. */
|
|
11
|
+
export type Coordinate3D = [number, number, number];
|
|
12
|
+
/**
|
|
13
|
+
* A styled polyline produced by symbol/geometry helpers.
|
|
14
|
+
* Generic building block — a list of points plus drawing style.
|
|
15
|
+
*/
|
|
16
|
+
export interface OnfarmLine {
|
|
17
|
+
coordinate: Coordinate3D[];
|
|
18
|
+
/** Linetype name, e.g. 'CONTINUOUS', 'DASHED'. */
|
|
19
|
+
type: string;
|
|
20
|
+
/** Line width. */
|
|
21
|
+
width: number;
|
|
22
|
+
/** ACI color index. */
|
|
23
|
+
color: number;
|
|
24
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic, API-agnostic types for the DXF library.
|
|
3
|
+
*
|
|
4
|
+
* These describe pure geometry/styling primitives only. They deliberately
|
|
5
|
+
* carry no knowledge of any backend API or project-specific JSON shape —
|
|
6
|
+
* the consuming application maps its own data into these types.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|