@ifc-lite/drawing-2d 1.18.6 → 1.19.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/README.md +23 -0
- package/dist/dxf/aci-colors.d.ts +4 -0
- package/dist/dxf/aci-colors.d.ts.map +1 -0
- package/dist/dxf/aci-colors.js +74 -0
- package/dist/dxf/aci-colors.js.map +1 -0
- package/dist/dxf/convert.d.ts +32 -0
- package/dist/dxf/convert.d.ts.map +1 -0
- package/dist/dxf/convert.js +382 -0
- package/dist/dxf/convert.js.map +1 -0
- package/dist/dxf/geom.d.ts +70 -0
- package/dist/dxf/geom.d.ts.map +1 -0
- package/dist/dxf/geom.js +194 -0
- package/dist/dxf/geom.js.map +1 -0
- package/dist/dxf/index.d.ts +13 -0
- package/dist/dxf/index.d.ts.map +1 -0
- package/dist/dxf/index.js +44 -0
- package/dist/dxf/index.js.map +1 -0
- package/dist/dxf/parser.d.ts +9 -0
- package/dist/dxf/parser.d.ts.map +1 -0
- package/dist/dxf/parser.js +903 -0
- package/dist/dxf/parser.js.map +1 -0
- package/dist/dxf/types.d.ts +255 -0
- package/dist/dxf/types.d.ts.map +1 -0
- package/dist/dxf/types.js +10 -0
- package/dist/dxf/types.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/svg-exporter.d.ts +23 -0
- package/dist/svg-exporter.d.ts.map +1 -1
- package/dist/svg-exporter.js +80 -1
- package/dist/svg-exporter.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
/**
|
|
5
|
+
* ASCII DXF parser (issue #1782).
|
|
6
|
+
*
|
|
7
|
+
* DXF is a stream of (group code, value) line pairs organised into sections.
|
|
8
|
+
* This parser reads the sections that matter for a 2D reference underlay:
|
|
9
|
+
* HEADER ($INSUNITS), TABLES (LAYER records), BLOCKS, and ENTITIES. It is
|
|
10
|
+
* deliberately lenient: unknown group codes are ignored, unknown entity
|
|
11
|
+
* types are counted in `skipped`, and truncated sections parse as far as
|
|
12
|
+
* they go. Only structurally broken files (odd pairing, binary DXF) throw.
|
|
13
|
+
*/
|
|
14
|
+
import { sampleArc, sampleEllipse } from './geom.js';
|
|
15
|
+
const BINARY_DXF_SENTINEL = 'AutoCAD Binary DXF';
|
|
16
|
+
/** Cap on parsed entities (top level + per block) against hostile inputs. */
|
|
17
|
+
const MAX_ENTITIES = 500_000;
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
+
// GROUP-CODE READER
|
|
20
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
21
|
+
const GROUP_CODE_RE = /^-?\d+$/;
|
|
22
|
+
/** Split an ASCII DXF file into (code, value) pairs. */
|
|
23
|
+
export function readDxfPairs(text) {
|
|
24
|
+
if (text.startsWith(BINARY_DXF_SENTINEL)) {
|
|
25
|
+
throw new Error('Binary DXF files are not supported; re-save as ASCII DXF.');
|
|
26
|
+
}
|
|
27
|
+
const lines = text.split(/\r\n|\r|\n/);
|
|
28
|
+
// Trailing newlines produce empty final lines; tolerate them.
|
|
29
|
+
let end = lines.length;
|
|
30
|
+
while (end > 0 && lines[end - 1].trim() === '')
|
|
31
|
+
end--;
|
|
32
|
+
const pairs = [];
|
|
33
|
+
for (let i = 0; i < end; i += 2) {
|
|
34
|
+
const codeStr = lines[i].trim();
|
|
35
|
+
if (!GROUP_CODE_RE.test(codeStr)) {
|
|
36
|
+
throw new Error(`Malformed DXF: expected a group code at line ${i + 1}, got "${codeStr.slice(0, 32)}"`);
|
|
37
|
+
}
|
|
38
|
+
if (i + 1 >= end) {
|
|
39
|
+
throw new Error(`Malformed DXF: group code ${codeStr} at line ${i + 1} has no value line`);
|
|
40
|
+
}
|
|
41
|
+
pairs.push({ code: Number.parseInt(codeStr, 10), value: lines[i + 1] });
|
|
42
|
+
}
|
|
43
|
+
return pairs;
|
|
44
|
+
}
|
|
45
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
46
|
+
// TEXT DECODING
|
|
47
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
48
|
+
/** Decode DXF special sequences (%%d, %%p, %%c, \U+XXXX). */
|
|
49
|
+
export function decodeDxfText(raw) {
|
|
50
|
+
return raw
|
|
51
|
+
.replace(/%%[dD]/g, '°')
|
|
52
|
+
.replace(/%%[pP]/g, '±')
|
|
53
|
+
.replace(/%%[cC]/g, 'Ø')
|
|
54
|
+
.replace(/%%[uUoO]/g, '') // underline/overline toggles
|
|
55
|
+
.replace(/\\U\+([0-9A-Fa-f]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
56
|
+
}
|
|
57
|
+
/** Strip MTEXT inline formatting down to plain text. Best-effort. */
|
|
58
|
+
export function stripMtextFormatting(raw) {
|
|
59
|
+
let s = raw;
|
|
60
|
+
s = s.replace(/\\\\/g, '\u0001'); // placeholder protecting literal backslashes
|
|
61
|
+
s = s.replace(/\\P/g, '\n');
|
|
62
|
+
s = s.replace(/\\~/g, ' ');
|
|
63
|
+
s = s.replace(/\\S([^;]*)\^\s?([^;]*);/g, '$1/$2'); // stacked fractions
|
|
64
|
+
s = s.replace(/\\[ACFHQTWfp][^;]*;/g, ''); // parametrised format codes
|
|
65
|
+
s = s.replace(/\\[LlOoKkX]/g, ''); // toggle codes
|
|
66
|
+
s = s.replace(/[{}]/g, '');
|
|
67
|
+
s = s.replace(/\u0001/g, '\\');
|
|
68
|
+
return decodeDxfText(s);
|
|
69
|
+
}
|
|
70
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
71
|
+
// PARSER
|
|
72
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
73
|
+
function num(value) {
|
|
74
|
+
const n = Number.parseFloat(value);
|
|
75
|
+
return Number.isFinite(n) ? n : 0;
|
|
76
|
+
}
|
|
77
|
+
function int(value) {
|
|
78
|
+
const n = Number.parseInt(value.trim(), 10);
|
|
79
|
+
return Number.isFinite(n) ? n : 0;
|
|
80
|
+
}
|
|
81
|
+
/** Index of the next pair with code 0 at or after `start` (or `end`). */
|
|
82
|
+
function nextEntityStart(pairs, start, end) {
|
|
83
|
+
let i = start;
|
|
84
|
+
while (i < end && pairs[i].code !== 0)
|
|
85
|
+
i++;
|
|
86
|
+
return i;
|
|
87
|
+
}
|
|
88
|
+
function findEndSec(pairs, start) {
|
|
89
|
+
for (let i = start; i < pairs.length; i++) {
|
|
90
|
+
if (pairs[i].code === 0 && pairs[i].value.trim() === 'ENDSEC')
|
|
91
|
+
return i;
|
|
92
|
+
}
|
|
93
|
+
return pairs.length;
|
|
94
|
+
}
|
|
95
|
+
export function parseDxf(text) {
|
|
96
|
+
const pairs = readDxfPairs(text);
|
|
97
|
+
const doc = {
|
|
98
|
+
insunits: 0,
|
|
99
|
+
layers: new Map(),
|
|
100
|
+
blocks: new Map(),
|
|
101
|
+
entities: [],
|
|
102
|
+
skipped: {},
|
|
103
|
+
warnings: [],
|
|
104
|
+
};
|
|
105
|
+
let i = 0;
|
|
106
|
+
while (i < pairs.length) {
|
|
107
|
+
const p = pairs[i];
|
|
108
|
+
if (p.code === 0 && p.value.trim() === 'EOF')
|
|
109
|
+
break;
|
|
110
|
+
if (p.code === 0 && p.value.trim() === 'SECTION') {
|
|
111
|
+
const namePair = pairs[i + 1];
|
|
112
|
+
const name = namePair && namePair.code === 2 ? namePair.value.trim() : '';
|
|
113
|
+
const bodyStart = i + 2;
|
|
114
|
+
const end = findEndSec(pairs, bodyStart);
|
|
115
|
+
switch (name) {
|
|
116
|
+
case 'HEADER':
|
|
117
|
+
parseHeader(pairs, bodyStart, end, doc);
|
|
118
|
+
break;
|
|
119
|
+
case 'TABLES':
|
|
120
|
+
parseTables(pairs, bodyStart, end, doc);
|
|
121
|
+
break;
|
|
122
|
+
case 'BLOCKS':
|
|
123
|
+
parseBlocks(pairs, bodyStart, end, doc);
|
|
124
|
+
break;
|
|
125
|
+
case 'ENTITIES':
|
|
126
|
+
doc.entities = parseEntityList(pairs, bodyStart, end, doc);
|
|
127
|
+
break;
|
|
128
|
+
default:
|
|
129
|
+
break; // CLASSES, OBJECTS, THUMBNAILIMAGE — irrelevant here
|
|
130
|
+
}
|
|
131
|
+
i = end + 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
i++;
|
|
135
|
+
}
|
|
136
|
+
return doc;
|
|
137
|
+
}
|
|
138
|
+
function parseHeader(pairs, start, end, doc) {
|
|
139
|
+
for (let i = start; i < end; i++) {
|
|
140
|
+
if (pairs[i].code === 9 && pairs[i].value.trim() === '$INSUNITS') {
|
|
141
|
+
const next = pairs[i + 1];
|
|
142
|
+
if (next && next.code === 70)
|
|
143
|
+
doc.insunits = int(next.value);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function parseTables(pairs, start, end, doc) {
|
|
149
|
+
let i = start;
|
|
150
|
+
while (i < end) {
|
|
151
|
+
const p = pairs[i];
|
|
152
|
+
if (p.code === 0 && p.value.trim() === 'LAYER') {
|
|
153
|
+
const bodyEnd = nextEntityStart(pairs, i + 1, end);
|
|
154
|
+
const layer = { name: '', colorNumber: 7, visible: true };
|
|
155
|
+
for (let j = i + 1; j < bodyEnd; j++) {
|
|
156
|
+
const { code, value } = pairs[j];
|
|
157
|
+
switch (code) {
|
|
158
|
+
case 2:
|
|
159
|
+
layer.name = value.trim();
|
|
160
|
+
break;
|
|
161
|
+
case 62: {
|
|
162
|
+
const c = int(value);
|
|
163
|
+
if (c < 0)
|
|
164
|
+
layer.visible = false; // layer is off
|
|
165
|
+
layer.colorNumber = Math.abs(c) || 7;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case 70:
|
|
169
|
+
if ((int(value) & 1) !== 0)
|
|
170
|
+
layer.visible = false; // frozen
|
|
171
|
+
break;
|
|
172
|
+
case 6:
|
|
173
|
+
layer.linetype = value.trim();
|
|
174
|
+
break;
|
|
175
|
+
case 420:
|
|
176
|
+
layer.trueColor = int(value) & 0xffffff;
|
|
177
|
+
break;
|
|
178
|
+
case 370: {
|
|
179
|
+
const lw = int(value);
|
|
180
|
+
if (lw > 0)
|
|
181
|
+
layer.lineweightMm = lw / 100;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (layer.name)
|
|
187
|
+
doc.layers.set(layer.name, layer);
|
|
188
|
+
i = bodyEnd;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
i++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function parseBlocks(pairs, start, end, doc) {
|
|
195
|
+
let i = start;
|
|
196
|
+
while (i < end) {
|
|
197
|
+
const p = pairs[i];
|
|
198
|
+
if (p.code === 0 && p.value.trim() === 'BLOCK') {
|
|
199
|
+
const headerEnd = nextEntityStart(pairs, i + 1, end);
|
|
200
|
+
const block = { name: '', baseX: 0, baseY: 0, entities: [] };
|
|
201
|
+
for (let j = i + 1; j < headerEnd; j++) {
|
|
202
|
+
const { code, value } = pairs[j];
|
|
203
|
+
if (code === 2 && !block.name)
|
|
204
|
+
block.name = value.trim();
|
|
205
|
+
else if (code === 10)
|
|
206
|
+
block.baseX = num(value);
|
|
207
|
+
else if (code === 20)
|
|
208
|
+
block.baseY = num(value);
|
|
209
|
+
}
|
|
210
|
+
// Entities run until the matching ENDBLK.
|
|
211
|
+
let blockEnd = headerEnd;
|
|
212
|
+
while (blockEnd < end && !(pairs[blockEnd].code === 0 && pairs[blockEnd].value.trim() === 'ENDBLK')) {
|
|
213
|
+
blockEnd = nextEntityStart(pairs, blockEnd + 1, end);
|
|
214
|
+
}
|
|
215
|
+
block.entities = parseEntityList(pairs, headerEnd, blockEnd, doc);
|
|
216
|
+
if (block.name)
|
|
217
|
+
doc.blocks.set(block.name, block);
|
|
218
|
+
i = blockEnd + 1;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
i++;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/** Parse a run of entities between `start` and `end` (exclusive). */
|
|
225
|
+
function parseEntityList(pairs, start, end, doc) {
|
|
226
|
+
const entities = [];
|
|
227
|
+
let i = nextEntityStart(pairs, start, end);
|
|
228
|
+
while (i < end) {
|
|
229
|
+
const type = pairs[i].value.trim();
|
|
230
|
+
if (type === 'ENDSEC' || type === 'ENDBLK' || type === 'EOF')
|
|
231
|
+
break;
|
|
232
|
+
const bodyStart = i + 1;
|
|
233
|
+
const bodyEnd = nextEntityStart(pairs, bodyStart, end);
|
|
234
|
+
const body = pairs.slice(bodyStart, bodyEnd);
|
|
235
|
+
if (entities.length >= MAX_ENTITIES) {
|
|
236
|
+
doc.warnings.push(`Entity limit (${MAX_ENTITIES}) reached; remaining entities ignored.`);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
switch (type) {
|
|
240
|
+
case 'LINE':
|
|
241
|
+
entities.push(parseLine(body));
|
|
242
|
+
break;
|
|
243
|
+
case 'LWPOLYLINE':
|
|
244
|
+
entities.push(parseLwPolyline(body));
|
|
245
|
+
break;
|
|
246
|
+
case 'POLYLINE': {
|
|
247
|
+
const { entity, next } = parsePolyline(pairs, body, bodyEnd, end);
|
|
248
|
+
entities.push(entity);
|
|
249
|
+
i = next;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
case 'CIRCLE':
|
|
253
|
+
entities.push(parseCircle(body));
|
|
254
|
+
break;
|
|
255
|
+
case 'ARC':
|
|
256
|
+
entities.push(parseArc(body));
|
|
257
|
+
break;
|
|
258
|
+
case 'ELLIPSE':
|
|
259
|
+
entities.push(parseEllipse(body));
|
|
260
|
+
break;
|
|
261
|
+
case 'TEXT':
|
|
262
|
+
entities.push(parseText(body));
|
|
263
|
+
break;
|
|
264
|
+
case 'MTEXT':
|
|
265
|
+
entities.push(parseMtext(body));
|
|
266
|
+
break;
|
|
267
|
+
case 'SPLINE':
|
|
268
|
+
entities.push(parseSpline(body, doc));
|
|
269
|
+
break;
|
|
270
|
+
case 'SOLID':
|
|
271
|
+
case 'TRACE':
|
|
272
|
+
entities.push(parseSolid(body));
|
|
273
|
+
break;
|
|
274
|
+
case 'INSERT':
|
|
275
|
+
entities.push(parseInsert(body));
|
|
276
|
+
break;
|
|
277
|
+
case 'DIMENSION':
|
|
278
|
+
entities.push(parseDimension(body));
|
|
279
|
+
break;
|
|
280
|
+
case 'HATCH':
|
|
281
|
+
entities.push(parseHatch(body, doc));
|
|
282
|
+
break;
|
|
283
|
+
case 'VERTEX':
|
|
284
|
+
case 'SEQEND':
|
|
285
|
+
// Only meaningful inside a POLYLINE chain (handled there); stray
|
|
286
|
+
// occurrences are ignored.
|
|
287
|
+
break;
|
|
288
|
+
case 'ATTRIB':
|
|
289
|
+
case 'ATTDEF':
|
|
290
|
+
// Block attribute definitions/values; not part of the graphic underlay.
|
|
291
|
+
break;
|
|
292
|
+
default:
|
|
293
|
+
doc.skipped[type] = (doc.skipped[type] ?? 0) + 1;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
i = bodyEnd;
|
|
297
|
+
}
|
|
298
|
+
return entities;
|
|
299
|
+
}
|
|
300
|
+
function parseCommon(body) {
|
|
301
|
+
const common = {
|
|
302
|
+
layer: '0',
|
|
303
|
+
colorNumber: 256,
|
|
304
|
+
invisible: false,
|
|
305
|
+
extrusionZ: 1,
|
|
306
|
+
};
|
|
307
|
+
for (const { code, value } of body) {
|
|
308
|
+
switch (code) {
|
|
309
|
+
case 8:
|
|
310
|
+
common.layer = value.trim() || '0';
|
|
311
|
+
break;
|
|
312
|
+
case 62:
|
|
313
|
+
common.colorNumber = int(value);
|
|
314
|
+
break;
|
|
315
|
+
case 420:
|
|
316
|
+
common.trueColor = int(value) & 0xffffff;
|
|
317
|
+
break;
|
|
318
|
+
case 6:
|
|
319
|
+
common.linetype = value.trim();
|
|
320
|
+
break;
|
|
321
|
+
case 370: {
|
|
322
|
+
const lw = int(value); // 1/100 mm; negative values are BYLAYER/BYBLOCK/default
|
|
323
|
+
if (lw > 0)
|
|
324
|
+
common.lineweightMm = lw / 100;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
case 60:
|
|
328
|
+
common.invisible = int(value) === 1;
|
|
329
|
+
break;
|
|
330
|
+
case 230:
|
|
331
|
+
common.extrusionZ = num(value);
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return common;
|
|
336
|
+
}
|
|
337
|
+
function parseLine(body) {
|
|
338
|
+
const e = { ...parseCommon(body), kind: 'line', x1: 0, y1: 0, x2: 0, y2: 0 };
|
|
339
|
+
for (const { code, value } of body) {
|
|
340
|
+
if (code === 10)
|
|
341
|
+
e.x1 = num(value);
|
|
342
|
+
else if (code === 20)
|
|
343
|
+
e.y1 = num(value);
|
|
344
|
+
else if (code === 11)
|
|
345
|
+
e.x2 = num(value);
|
|
346
|
+
else if (code === 21)
|
|
347
|
+
e.y2 = num(value);
|
|
348
|
+
}
|
|
349
|
+
return e;
|
|
350
|
+
}
|
|
351
|
+
function parseLwPolyline(body) {
|
|
352
|
+
const e = { ...parseCommon(body), kind: 'polyline', vertices: [], closed: false };
|
|
353
|
+
let current = null;
|
|
354
|
+
for (const { code, value } of body) {
|
|
355
|
+
switch (code) {
|
|
356
|
+
case 10:
|
|
357
|
+
current = { x: num(value), y: 0, bulge: 0 };
|
|
358
|
+
e.vertices.push(current);
|
|
359
|
+
break;
|
|
360
|
+
case 20:
|
|
361
|
+
if (current)
|
|
362
|
+
current.y = num(value);
|
|
363
|
+
break;
|
|
364
|
+
case 42:
|
|
365
|
+
if (current)
|
|
366
|
+
current.bulge = num(value);
|
|
367
|
+
break;
|
|
368
|
+
case 70:
|
|
369
|
+
e.closed = (int(value) & 1) !== 0;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return e;
|
|
374
|
+
}
|
|
375
|
+
/** Classic POLYLINE: consume the following VERTEX chain up to SEQEND. */
|
|
376
|
+
function parsePolyline(pairs, headerBody, chainStart, end) {
|
|
377
|
+
const entity = {
|
|
378
|
+
...parseCommon(headerBody),
|
|
379
|
+
kind: 'polyline',
|
|
380
|
+
vertices: [],
|
|
381
|
+
closed: false,
|
|
382
|
+
};
|
|
383
|
+
for (const { code, value } of headerBody) {
|
|
384
|
+
if (code === 70)
|
|
385
|
+
entity.closed = (int(value) & 1) !== 0;
|
|
386
|
+
}
|
|
387
|
+
let i = chainStart;
|
|
388
|
+
while (i < end && pairs[i].code === 0) {
|
|
389
|
+
const type = pairs[i].value.trim();
|
|
390
|
+
const bodyEnd = nextEntityStart(pairs, i + 1, end);
|
|
391
|
+
if (type === 'VERTEX') {
|
|
392
|
+
const v = { x: 0, y: 0, bulge: 0 };
|
|
393
|
+
let flags = 0;
|
|
394
|
+
for (let j = i + 1; j < bodyEnd; j++) {
|
|
395
|
+
const { code, value } = pairs[j];
|
|
396
|
+
if (code === 10)
|
|
397
|
+
v.x = num(value);
|
|
398
|
+
else if (code === 20)
|
|
399
|
+
v.y = num(value);
|
|
400
|
+
else if (code === 42)
|
|
401
|
+
v.bulge = num(value);
|
|
402
|
+
else if (code === 70)
|
|
403
|
+
flags = int(value);
|
|
404
|
+
}
|
|
405
|
+
// Skip spline-frame control points (bit 4 set, bit 8 clear).
|
|
406
|
+
if ((flags & 16) === 0 || (flags & 8) !== 0)
|
|
407
|
+
entity.vertices.push(v);
|
|
408
|
+
i = bodyEnd;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (type === 'SEQEND') {
|
|
412
|
+
i = bodyEnd;
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
break; // unexpected entity: leave it for the main loop
|
|
416
|
+
}
|
|
417
|
+
return { entity, next: i };
|
|
418
|
+
}
|
|
419
|
+
function parseCircle(body) {
|
|
420
|
+
const e = { ...parseCommon(body), kind: 'circle', cx: 0, cy: 0, r: 0 };
|
|
421
|
+
for (const { code, value } of body) {
|
|
422
|
+
if (code === 10)
|
|
423
|
+
e.cx = num(value);
|
|
424
|
+
else if (code === 20)
|
|
425
|
+
e.cy = num(value);
|
|
426
|
+
else if (code === 40)
|
|
427
|
+
e.r = num(value);
|
|
428
|
+
}
|
|
429
|
+
return e;
|
|
430
|
+
}
|
|
431
|
+
function parseArc(body) {
|
|
432
|
+
const e = { ...parseCommon(body), kind: 'arc', cx: 0, cy: 0, r: 0, startDeg: 0, endDeg: 360 };
|
|
433
|
+
for (const { code, value } of body) {
|
|
434
|
+
if (code === 10)
|
|
435
|
+
e.cx = num(value);
|
|
436
|
+
else if (code === 20)
|
|
437
|
+
e.cy = num(value);
|
|
438
|
+
else if (code === 40)
|
|
439
|
+
e.r = num(value);
|
|
440
|
+
else if (code === 50)
|
|
441
|
+
e.startDeg = num(value);
|
|
442
|
+
else if (code === 51)
|
|
443
|
+
e.endDeg = num(value);
|
|
444
|
+
}
|
|
445
|
+
return e;
|
|
446
|
+
}
|
|
447
|
+
function parseEllipse(body) {
|
|
448
|
+
const e = {
|
|
449
|
+
...parseCommon(body),
|
|
450
|
+
kind: 'ellipse',
|
|
451
|
+
cx: 0,
|
|
452
|
+
cy: 0,
|
|
453
|
+
majorX: 1,
|
|
454
|
+
majorY: 0,
|
|
455
|
+
ratio: 1,
|
|
456
|
+
startParam: 0,
|
|
457
|
+
endParam: Math.PI * 2,
|
|
458
|
+
};
|
|
459
|
+
for (const { code, value } of body) {
|
|
460
|
+
if (code === 10)
|
|
461
|
+
e.cx = num(value);
|
|
462
|
+
else if (code === 20)
|
|
463
|
+
e.cy = num(value);
|
|
464
|
+
else if (code === 11)
|
|
465
|
+
e.majorX = num(value);
|
|
466
|
+
else if (code === 21)
|
|
467
|
+
e.majorY = num(value);
|
|
468
|
+
else if (code === 40)
|
|
469
|
+
e.ratio = num(value);
|
|
470
|
+
else if (code === 41)
|
|
471
|
+
e.startParam = num(value);
|
|
472
|
+
else if (code === 42)
|
|
473
|
+
e.endParam = num(value);
|
|
474
|
+
}
|
|
475
|
+
return e;
|
|
476
|
+
}
|
|
477
|
+
function parseText(body) {
|
|
478
|
+
const e = {
|
|
479
|
+
...parseCommon(body),
|
|
480
|
+
kind: 'text',
|
|
481
|
+
x: 0,
|
|
482
|
+
y: 0,
|
|
483
|
+
height: 1,
|
|
484
|
+
rotationDeg: 0,
|
|
485
|
+
text: '',
|
|
486
|
+
hAlign: 'left',
|
|
487
|
+
vAlign: 'baseline',
|
|
488
|
+
};
|
|
489
|
+
let alignX = null;
|
|
490
|
+
let alignY = null;
|
|
491
|
+
let hJust = 0;
|
|
492
|
+
let vJust = 0;
|
|
493
|
+
for (const { code, value } of body) {
|
|
494
|
+
switch (code) {
|
|
495
|
+
case 10:
|
|
496
|
+
e.x = num(value);
|
|
497
|
+
break;
|
|
498
|
+
case 20:
|
|
499
|
+
e.y = num(value);
|
|
500
|
+
break;
|
|
501
|
+
case 11:
|
|
502
|
+
alignX = num(value);
|
|
503
|
+
break;
|
|
504
|
+
case 21:
|
|
505
|
+
alignY = num(value);
|
|
506
|
+
break;
|
|
507
|
+
case 40:
|
|
508
|
+
e.height = num(value);
|
|
509
|
+
break;
|
|
510
|
+
case 50:
|
|
511
|
+
e.rotationDeg = num(value);
|
|
512
|
+
break;
|
|
513
|
+
case 72:
|
|
514
|
+
hJust = int(value);
|
|
515
|
+
break;
|
|
516
|
+
case 73:
|
|
517
|
+
vJust = int(value);
|
|
518
|
+
break;
|
|
519
|
+
case 1:
|
|
520
|
+
e.text = decodeDxfText(value);
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (hJust === 1 || hJust === 4)
|
|
525
|
+
e.hAlign = 'center';
|
|
526
|
+
else if (hJust === 2)
|
|
527
|
+
e.hAlign = 'right';
|
|
528
|
+
if (vJust === 1)
|
|
529
|
+
e.vAlign = 'bottom';
|
|
530
|
+
else if (vJust === 2)
|
|
531
|
+
e.vAlign = 'middle';
|
|
532
|
+
else if (vJust === 3)
|
|
533
|
+
e.vAlign = 'top';
|
|
534
|
+
// Non-default justification anchors at the second alignment point.
|
|
535
|
+
if ((hJust !== 0 || vJust !== 0) && alignX !== null && alignY !== null) {
|
|
536
|
+
e.x = alignX;
|
|
537
|
+
e.y = alignY;
|
|
538
|
+
}
|
|
539
|
+
return e;
|
|
540
|
+
}
|
|
541
|
+
function parseSpline(body, doc) {
|
|
542
|
+
const e = {
|
|
543
|
+
...parseCommon(body),
|
|
544
|
+
kind: 'spline',
|
|
545
|
+
degree: 3,
|
|
546
|
+
closed: false,
|
|
547
|
+
knots: [],
|
|
548
|
+
controlPoints: [],
|
|
549
|
+
fitPoints: [],
|
|
550
|
+
};
|
|
551
|
+
const weights = [];
|
|
552
|
+
let flags = 0;
|
|
553
|
+
let currentCtrl = null;
|
|
554
|
+
let currentFit = null;
|
|
555
|
+
for (const { code, value } of body) {
|
|
556
|
+
switch (code) {
|
|
557
|
+
case 70:
|
|
558
|
+
flags = int(value);
|
|
559
|
+
e.closed = (flags & 1) !== 0;
|
|
560
|
+
break;
|
|
561
|
+
case 71:
|
|
562
|
+
e.degree = Math.max(1, int(value));
|
|
563
|
+
break;
|
|
564
|
+
case 40:
|
|
565
|
+
e.knots.push(num(value));
|
|
566
|
+
break;
|
|
567
|
+
case 41:
|
|
568
|
+
weights.push(num(value));
|
|
569
|
+
break;
|
|
570
|
+
case 10:
|
|
571
|
+
currentCtrl = { x: num(value), y: 0 };
|
|
572
|
+
e.controlPoints.push(currentCtrl);
|
|
573
|
+
break;
|
|
574
|
+
case 20:
|
|
575
|
+
if (currentCtrl)
|
|
576
|
+
currentCtrl.y = num(value);
|
|
577
|
+
break;
|
|
578
|
+
case 11:
|
|
579
|
+
currentFit = { x: num(value), y: 0 };
|
|
580
|
+
e.fitPoints.push(currentFit);
|
|
581
|
+
break;
|
|
582
|
+
case 21:
|
|
583
|
+
if (currentFit)
|
|
584
|
+
currentFit.y = num(value);
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
// The tessellator evaluates a non-rational clamped B-spline. Rational
|
|
589
|
+
// splines with non-uniform weights and periodic splines deviate from
|
|
590
|
+
// that; surface a warning instead of failing (periodic knot vectors
|
|
591
|
+
// also fail the clamped-knot check and fall back to the control
|
|
592
|
+
// polygon). Fit points, when present, are exact either way.
|
|
593
|
+
if (e.fitPoints.length < 2) {
|
|
594
|
+
const nonUniform = weights.length > 0 && weights.some((w) => Math.abs(w - weights[0]) > 1e-9);
|
|
595
|
+
if (nonUniform) {
|
|
596
|
+
doc.warnings.push('Rational SPLINE with non-uniform weights approximated as non-rational.');
|
|
597
|
+
}
|
|
598
|
+
if ((flags & 2) !== 0) {
|
|
599
|
+
doc.warnings.push('Periodic SPLINE approximated by its control polygon.');
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return e;
|
|
603
|
+
}
|
|
604
|
+
function parseSolid(body) {
|
|
605
|
+
// DXF stores SOLID corners in Z-order: draw order is 1, 2, 4, 3.
|
|
606
|
+
const c = [
|
|
607
|
+
{ x: 0, y: 0 },
|
|
608
|
+
{ x: 0, y: 0 },
|
|
609
|
+
{ x: 0, y: 0 },
|
|
610
|
+
{ x: 0, y: 0 },
|
|
611
|
+
];
|
|
612
|
+
let has4 = false;
|
|
613
|
+
for (const { code, value } of body) {
|
|
614
|
+
switch (code) {
|
|
615
|
+
case 10:
|
|
616
|
+
c[0].x = num(value);
|
|
617
|
+
break;
|
|
618
|
+
case 20:
|
|
619
|
+
c[0].y = num(value);
|
|
620
|
+
break;
|
|
621
|
+
case 11:
|
|
622
|
+
c[1].x = num(value);
|
|
623
|
+
break;
|
|
624
|
+
case 21:
|
|
625
|
+
c[1].y = num(value);
|
|
626
|
+
break;
|
|
627
|
+
case 12:
|
|
628
|
+
c[3].x = num(value);
|
|
629
|
+
break;
|
|
630
|
+
case 22:
|
|
631
|
+
c[3].y = num(value);
|
|
632
|
+
break;
|
|
633
|
+
case 13:
|
|
634
|
+
c[2].x = num(value);
|
|
635
|
+
has4 = true;
|
|
636
|
+
break;
|
|
637
|
+
case 23:
|
|
638
|
+
c[2].y = num(value);
|
|
639
|
+
has4 = true;
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const corners = has4 && (c[2].x !== c[3].x || c[2].y !== c[3].y)
|
|
644
|
+
? [c[0], c[1], c[2], c[3]]
|
|
645
|
+
: [c[0], c[1], c[3]];
|
|
646
|
+
return { ...parseCommon(body), kind: 'solid', corners };
|
|
647
|
+
}
|
|
648
|
+
function parseMtext(body) {
|
|
649
|
+
const e = {
|
|
650
|
+
...parseCommon(body),
|
|
651
|
+
kind: 'text',
|
|
652
|
+
x: 0,
|
|
653
|
+
y: 0,
|
|
654
|
+
height: 1,
|
|
655
|
+
rotationDeg: 0,
|
|
656
|
+
text: '',
|
|
657
|
+
hAlign: 'left',
|
|
658
|
+
vAlign: 'top', // MTEXT default attachment is top-left
|
|
659
|
+
};
|
|
660
|
+
let chunks = '';
|
|
661
|
+
let dirX = null;
|
|
662
|
+
let dirY = null;
|
|
663
|
+
for (const { code, value } of body) {
|
|
664
|
+
switch (code) {
|
|
665
|
+
case 10:
|
|
666
|
+
e.x = num(value);
|
|
667
|
+
break;
|
|
668
|
+
case 20:
|
|
669
|
+
e.y = num(value);
|
|
670
|
+
break;
|
|
671
|
+
case 40:
|
|
672
|
+
e.height = num(value);
|
|
673
|
+
break;
|
|
674
|
+
case 50:
|
|
675
|
+
e.rotationDeg = num(value);
|
|
676
|
+
break;
|
|
677
|
+
case 11:
|
|
678
|
+
dirX = num(value);
|
|
679
|
+
break;
|
|
680
|
+
case 21:
|
|
681
|
+
dirY = num(value);
|
|
682
|
+
break;
|
|
683
|
+
case 71: {
|
|
684
|
+
const attach = int(value); // 1..9 grid: rows top/middle/bottom, columns left/center/right
|
|
685
|
+
const col = (attach - 1) % 3;
|
|
686
|
+
const row = Math.floor((attach - 1) / 3);
|
|
687
|
+
e.hAlign = col === 1 ? 'center' : col === 2 ? 'right' : 'left';
|
|
688
|
+
e.vAlign = row === 1 ? 'middle' : row === 2 ? 'bottom' : 'top';
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
case 3:
|
|
692
|
+
chunks += value;
|
|
693
|
+
break;
|
|
694
|
+
case 1:
|
|
695
|
+
chunks += value;
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (dirX !== null && dirY !== null && (dirX !== 0 || dirY !== 0)) {
|
|
700
|
+
e.rotationDeg = (Math.atan2(dirY, dirX) * 180) / Math.PI;
|
|
701
|
+
}
|
|
702
|
+
e.text = stripMtextFormatting(chunks);
|
|
703
|
+
return e;
|
|
704
|
+
}
|
|
705
|
+
function parseInsert(body) {
|
|
706
|
+
const e = {
|
|
707
|
+
...parseCommon(body),
|
|
708
|
+
kind: 'insert',
|
|
709
|
+
blockName: '',
|
|
710
|
+
x: 0,
|
|
711
|
+
y: 0,
|
|
712
|
+
scaleX: 1,
|
|
713
|
+
scaleY: 1,
|
|
714
|
+
rotationDeg: 0,
|
|
715
|
+
columnCount: 1,
|
|
716
|
+
rowCount: 1,
|
|
717
|
+
columnSpacing: 0,
|
|
718
|
+
rowSpacing: 0,
|
|
719
|
+
};
|
|
720
|
+
for (const { code, value } of body) {
|
|
721
|
+
switch (code) {
|
|
722
|
+
case 2:
|
|
723
|
+
e.blockName = value.trim();
|
|
724
|
+
break;
|
|
725
|
+
case 10:
|
|
726
|
+
e.x = num(value);
|
|
727
|
+
break;
|
|
728
|
+
case 20:
|
|
729
|
+
e.y = num(value);
|
|
730
|
+
break;
|
|
731
|
+
case 41:
|
|
732
|
+
e.scaleX = num(value) || 1;
|
|
733
|
+
break;
|
|
734
|
+
case 42:
|
|
735
|
+
e.scaleY = num(value) || 1;
|
|
736
|
+
break;
|
|
737
|
+
case 50:
|
|
738
|
+
e.rotationDeg = num(value);
|
|
739
|
+
break;
|
|
740
|
+
case 70:
|
|
741
|
+
e.columnCount = Math.max(1, int(value));
|
|
742
|
+
break;
|
|
743
|
+
case 71:
|
|
744
|
+
e.rowCount = Math.max(1, int(value));
|
|
745
|
+
break;
|
|
746
|
+
case 44:
|
|
747
|
+
e.columnSpacing = num(value);
|
|
748
|
+
break;
|
|
749
|
+
case 45:
|
|
750
|
+
e.rowSpacing = num(value);
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return e;
|
|
755
|
+
}
|
|
756
|
+
function parseDimension(body) {
|
|
757
|
+
const e = { ...parseCommon(body), kind: 'dimension', blockName: '' };
|
|
758
|
+
for (const { code, value } of body) {
|
|
759
|
+
if (code === 2)
|
|
760
|
+
e.blockName = value.trim();
|
|
761
|
+
}
|
|
762
|
+
return e;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* HATCH boundary parsing. The group codes inside a HATCH are positional:
|
|
766
|
+
* 91 = number of boundary paths, then per path 92 (type flags) followed by
|
|
767
|
+
* either a polyline vertex list (flag bit 1) or a typed edge list. Codes 72
|
|
768
|
+
* and 73 change meaning depending on position, so this parser walks the body
|
|
769
|
+
* sequentially with a cursor.
|
|
770
|
+
*/
|
|
771
|
+
function parseHatch(body, doc) {
|
|
772
|
+
const e = { ...parseCommon(body), kind: 'hatch', solid: false, paths: [] };
|
|
773
|
+
let j = 0;
|
|
774
|
+
const peek = () => body[j];
|
|
775
|
+
const take = () => body[j++];
|
|
776
|
+
const takeIf = (code) => {
|
|
777
|
+
const p = body[j];
|
|
778
|
+
if (p && p.code === code) {
|
|
779
|
+
j++;
|
|
780
|
+
return num(p.value);
|
|
781
|
+
}
|
|
782
|
+
return null;
|
|
783
|
+
};
|
|
784
|
+
// Scan up to the first boundary path for the solid flag.
|
|
785
|
+
while (j < body.length && body[j].code !== 92) {
|
|
786
|
+
const p = take();
|
|
787
|
+
if (p.code === 70)
|
|
788
|
+
e.solid = int(p.value) === 1;
|
|
789
|
+
}
|
|
790
|
+
while (j < body.length) {
|
|
791
|
+
const p = peek();
|
|
792
|
+
if (!p)
|
|
793
|
+
break;
|
|
794
|
+
if (p.code !== 92) {
|
|
795
|
+
j++;
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
const flags = int(take().value);
|
|
799
|
+
const isPolyline = (flags & 2) !== 0;
|
|
800
|
+
const path = { vertices: [] };
|
|
801
|
+
if (isPolyline) {
|
|
802
|
+
takeIf(72); // has-bulge flag
|
|
803
|
+
takeIf(73); // is-closed flag (paths are treated as closed regions anyway)
|
|
804
|
+
takeIf(93); // vertex count (we read by codes, not count)
|
|
805
|
+
while (j < body.length) {
|
|
806
|
+
const x = takeIf(10);
|
|
807
|
+
if (x === null)
|
|
808
|
+
break;
|
|
809
|
+
const y = takeIf(20) ?? 0;
|
|
810
|
+
const bulge = takeIf(42) ?? 0;
|
|
811
|
+
path.vertices.push({ x, y, bulge });
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
const edgeCount = takeIf(93) ?? 0;
|
|
816
|
+
for (let k = 0; k < edgeCount && j < body.length; k++) {
|
|
817
|
+
const edgeType = takeIf(72);
|
|
818
|
+
if (edgeType === null)
|
|
819
|
+
break;
|
|
820
|
+
if (edgeType === 1) {
|
|
821
|
+
const x1 = takeIf(10) ?? 0;
|
|
822
|
+
const y1 = takeIf(20) ?? 0;
|
|
823
|
+
const x2 = takeIf(11) ?? 0;
|
|
824
|
+
const y2 = takeIf(21) ?? 0;
|
|
825
|
+
if (path.vertices.length === 0)
|
|
826
|
+
path.vertices.push({ x: x1, y: y1, bulge: 0 });
|
|
827
|
+
path.vertices.push({ x: x2, y: y2, bulge: 0 });
|
|
828
|
+
}
|
|
829
|
+
else if (edgeType === 2) {
|
|
830
|
+
const cx = takeIf(10) ?? 0;
|
|
831
|
+
const cy = takeIf(20) ?? 0;
|
|
832
|
+
const r = takeIf(40) ?? 0;
|
|
833
|
+
let start = takeIf(50) ?? 0;
|
|
834
|
+
let end = takeIf(51) ?? 360;
|
|
835
|
+
const ccw = (takeIf(73) ?? 1) !== 0;
|
|
836
|
+
if (!ccw) {
|
|
837
|
+
// Clockwise edges store angles measured clockwise; negate and
|
|
838
|
+
// swap to recover the true geometry (ezdxf's convention).
|
|
839
|
+
const s = start;
|
|
840
|
+
start = -end;
|
|
841
|
+
end = -s;
|
|
842
|
+
}
|
|
843
|
+
const pts = sampleArc(cx, cy, r, start, end);
|
|
844
|
+
const startIdx = path.vertices.length > 0 ? 1 : 0;
|
|
845
|
+
for (let m = startIdx; m < pts.length; m++) {
|
|
846
|
+
path.vertices.push({ x: pts[m].x, y: pts[m].y, bulge: 0 });
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
else if (edgeType === 3) {
|
|
850
|
+
const cx = takeIf(10) ?? 0;
|
|
851
|
+
const cy = takeIf(20) ?? 0;
|
|
852
|
+
const mx = takeIf(11) ?? 1;
|
|
853
|
+
const my = takeIf(21) ?? 0;
|
|
854
|
+
const ratio = takeIf(40) ?? 1;
|
|
855
|
+
let startDeg = takeIf(50) ?? 0;
|
|
856
|
+
let endDeg = takeIf(51) ?? 360;
|
|
857
|
+
const ccw = (takeIf(73) ?? 1) !== 0;
|
|
858
|
+
if (!ccw) {
|
|
859
|
+
// Clockwise edges mirror the parameter sweep, same as arc edges.
|
|
860
|
+
const s = startDeg;
|
|
861
|
+
startDeg = -endDeg;
|
|
862
|
+
endDeg = -s;
|
|
863
|
+
}
|
|
864
|
+
const pts = sampleEllipse(cx, cy, mx, my, ratio, (startDeg * Math.PI) / 180, (endDeg * Math.PI) / 180);
|
|
865
|
+
const startIdx = path.vertices.length > 0 ? 1 : 0;
|
|
866
|
+
for (let m = startIdx; m < pts.length; m++) {
|
|
867
|
+
path.vertices.push({ x: pts[m].x, y: pts[m].y, bulge: 0 });
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
// Spline edge (4): approximate by its control/fit points.
|
|
872
|
+
doc.warnings.push('HATCH spline edge approximated by its control points.');
|
|
873
|
+
takeIf(94); // degree
|
|
874
|
+
takeIf(73); // rational
|
|
875
|
+
takeIf(74); // periodic
|
|
876
|
+
const knotCount = takeIf(95) ?? 0;
|
|
877
|
+
const ctrlCount = takeIf(96) ?? 0;
|
|
878
|
+
for (let m = 0; m < knotCount; m++)
|
|
879
|
+
takeIf(40);
|
|
880
|
+
for (let m = 0; m < ctrlCount; m++) {
|
|
881
|
+
const x = takeIf(10);
|
|
882
|
+
if (x === null)
|
|
883
|
+
break;
|
|
884
|
+
const y = takeIf(20) ?? 0;
|
|
885
|
+
takeIf(42); // weight
|
|
886
|
+
path.vertices.push({ x, y, bulge: 0 });
|
|
887
|
+
}
|
|
888
|
+
const fitCount = takeIf(97) ?? 0;
|
|
889
|
+
for (let m = 0; m < fitCount; m++) {
|
|
890
|
+
const x = takeIf(11);
|
|
891
|
+
if (x === null)
|
|
892
|
+
break;
|
|
893
|
+
takeIf(21);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (path.vertices.length >= 3)
|
|
899
|
+
e.paths.push(path);
|
|
900
|
+
}
|
|
901
|
+
return e;
|
|
902
|
+
}
|
|
903
|
+
//# sourceMappingURL=parser.js.map
|