tsdraft 0.0.0-stage → 0.2.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/CHANGELOG.md +50 -0
- package/LICENSE +21 -0
- package/README.md +150 -2
- package/dist/drawing.d.ts +116 -0
- package/dist/drawing.js +162 -0
- package/dist/dxf-r12.d.ts +10 -0
- package/dist/dxf-r12.js +113 -0
- package/dist/dxf-r2018.d.ts +10 -0
- package/dist/dxf-r2018.js +333 -0
- package/dist/dxf.d.ts +11 -0
- package/dist/dxf.js +18 -0
- package/dist/extents.d.ts +3 -0
- package/dist/extents.js +42 -0
- package/dist/format.d.ts +11 -0
- package/dist/format.js +24 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/lint.d.ts +30 -0
- package/dist/lint.js +109 -0
- package/dist/rules.d.ts +33 -0
- package/dist/rules.js +56 -0
- package/dist/svg.d.ts +9 -0
- package/dist/svg.js +70 -0
- package/package.json +61 -4
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { extents } from "./extents.js";
|
|
2
|
+
import { dxfText, fmt } from "./format.js";
|
|
3
|
+
/** `$INSUNITS` codes from the DXF Reference. */
|
|
4
|
+
const INSUNITS = { in: 1, ft: 2, mm: 4, cm: 5, m: 6 };
|
|
5
|
+
/**
|
|
6
|
+
* Writes an ASCII DXF for AutoCAD 2018 and later (AC1032), the newest DXF
|
|
7
|
+
* version Autodesk has published. AutoCAD 2027 still reads and writes AC1032.
|
|
8
|
+
*
|
|
9
|
+
* Unlike R12, this version has handles, owner links, an OBJECTS section and the
|
|
10
|
+
* `$INSUNITS` header, so the drawing's units reach CAD and xref and block
|
|
11
|
+
* insertion scale correctly. Text is UTF-8.
|
|
12
|
+
*/
|
|
13
|
+
export function toDXFR2018(drawing) {
|
|
14
|
+
let seed = 1;
|
|
15
|
+
const handle = () => (seed++).toString(16).toUpperCase();
|
|
16
|
+
const body = [];
|
|
17
|
+
const g = groupWriter(body);
|
|
18
|
+
const rootDictionary = handle();
|
|
19
|
+
const groupDictionary = handle();
|
|
20
|
+
const plotStyleDictionary = handle();
|
|
21
|
+
const normalPlotStyle = handle();
|
|
22
|
+
const modelSpace = handle();
|
|
23
|
+
const paperSpace = handle();
|
|
24
|
+
const [minX, minY, maxX, maxY] = extents(drawing.entities);
|
|
25
|
+
g(0, "SECTION");
|
|
26
|
+
g(2, "CLASSES");
|
|
27
|
+
g(0, "ENDSEC");
|
|
28
|
+
g(0, "SECTION");
|
|
29
|
+
g(2, "TABLES");
|
|
30
|
+
writeTable(g, handle, "VPORT", [
|
|
31
|
+
{
|
|
32
|
+
type: "VPORT",
|
|
33
|
+
subclass: "AcDbViewportTableRecord",
|
|
34
|
+
name: "*Active",
|
|
35
|
+
handle: handle(),
|
|
36
|
+
fields: (g) => writeActiveViewport(g, minX, minY, maxX, maxY),
|
|
37
|
+
},
|
|
38
|
+
]);
|
|
39
|
+
writeTable(g, handle, "LTYPE", [
|
|
40
|
+
["ByBlock", ""],
|
|
41
|
+
["ByLayer", ""],
|
|
42
|
+
["Continuous", "Solid line"],
|
|
43
|
+
].map(([name = "", description = ""]) => ({
|
|
44
|
+
type: "LTYPE",
|
|
45
|
+
subclass: "AcDbLinetypeTableRecord",
|
|
46
|
+
name,
|
|
47
|
+
handle: handle(),
|
|
48
|
+
fields: (g) => {
|
|
49
|
+
g(3, description);
|
|
50
|
+
g(72, 65);
|
|
51
|
+
g(73, 0);
|
|
52
|
+
g(40, 0);
|
|
53
|
+
},
|
|
54
|
+
})));
|
|
55
|
+
writeTable(g, handle, "LAYER", drawing.layers.map((layer) => ({
|
|
56
|
+
type: "LAYER",
|
|
57
|
+
subclass: "AcDbLayerTableRecord",
|
|
58
|
+
name: layer.name,
|
|
59
|
+
handle: handle(),
|
|
60
|
+
fields: (g) => {
|
|
61
|
+
g(62, layer.color);
|
|
62
|
+
g(6, "Continuous");
|
|
63
|
+
g(370, -3); // lineweight: default
|
|
64
|
+
g(390, normalPlotStyle); // AutoCAD rejects a layer without a plot style
|
|
65
|
+
},
|
|
66
|
+
})));
|
|
67
|
+
writeTable(g, handle, "STYLE", [
|
|
68
|
+
{
|
|
69
|
+
type: "STYLE",
|
|
70
|
+
subclass: "AcDbTextStyleTableRecord",
|
|
71
|
+
name: "Standard",
|
|
72
|
+
handle: handle(),
|
|
73
|
+
fields: (g) => {
|
|
74
|
+
g(40, 0); // fixed height: none
|
|
75
|
+
g(41, 1); // width factor
|
|
76
|
+
g(50, 0); // oblique angle
|
|
77
|
+
g(71, 0); // generation flags
|
|
78
|
+
g(42, 0.2); // last height used
|
|
79
|
+
g(3, "txt");
|
|
80
|
+
g(4, "");
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
writeTable(g, handle, "VIEW", []);
|
|
85
|
+
writeTable(g, handle, "UCS", []);
|
|
86
|
+
writeTable(g, handle, "APPID", [
|
|
87
|
+
{ type: "APPID", subclass: "AcDbRegAppTableRecord", name: "ACAD", handle: handle() },
|
|
88
|
+
]);
|
|
89
|
+
writeTable(g, handle, "DIMSTYLE", [
|
|
90
|
+
{ type: "DIMSTYLE", subclass: "AcDbDimStyleTableRecord", name: "Standard", handle: handle() },
|
|
91
|
+
]);
|
|
92
|
+
const blockRecordFields = (g) => {
|
|
93
|
+
g(280, 1); // explodable
|
|
94
|
+
g(281, 0); // scale uniformly: no
|
|
95
|
+
};
|
|
96
|
+
writeTable(g, handle, "BLOCK_RECORD", [
|
|
97
|
+
{
|
|
98
|
+
type: "BLOCK_RECORD",
|
|
99
|
+
subclass: "AcDbBlockTableRecord",
|
|
100
|
+
name: "*Model_Space",
|
|
101
|
+
handle: modelSpace,
|
|
102
|
+
fields: blockRecordFields,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
type: "BLOCK_RECORD",
|
|
106
|
+
subclass: "AcDbBlockTableRecord",
|
|
107
|
+
name: "*Paper_Space",
|
|
108
|
+
handle: paperSpace,
|
|
109
|
+
fields: blockRecordFields,
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
g(0, "ENDSEC");
|
|
113
|
+
g(0, "SECTION");
|
|
114
|
+
g(2, "BLOCKS");
|
|
115
|
+
writeBlock(g, handle, "*Model_Space", modelSpace, false);
|
|
116
|
+
writeBlock(g, handle, "*Paper_Space", paperSpace, true);
|
|
117
|
+
g(0, "ENDSEC");
|
|
118
|
+
g(0, "SECTION");
|
|
119
|
+
g(2, "ENTITIES");
|
|
120
|
+
for (const entity of drawing.entities)
|
|
121
|
+
writeEntity(g, entity, handle(), modelSpace);
|
|
122
|
+
g(0, "ENDSEC");
|
|
123
|
+
g(0, "SECTION");
|
|
124
|
+
g(2, "OBJECTS");
|
|
125
|
+
g(0, "DICTIONARY");
|
|
126
|
+
g(5, rootDictionary);
|
|
127
|
+
g(330, "0");
|
|
128
|
+
g(100, "AcDbDictionary");
|
|
129
|
+
g(281, 1); // duplicate records: keep existing
|
|
130
|
+
g(3, "ACAD_GROUP");
|
|
131
|
+
g(350, groupDictionary);
|
|
132
|
+
g(3, "ACAD_PLOTSTYLENAME");
|
|
133
|
+
g(350, plotStyleDictionary);
|
|
134
|
+
g(0, "DICTIONARY");
|
|
135
|
+
ownedBy(g, groupDictionary, rootDictionary);
|
|
136
|
+
g(100, "AcDbDictionary");
|
|
137
|
+
g(281, 1);
|
|
138
|
+
// Plot style names: a dictionary with a default, holding the "Normal" placeholder.
|
|
139
|
+
g(0, "ACDBDICTIONARYWDFLT");
|
|
140
|
+
ownedBy(g, plotStyleDictionary, rootDictionary);
|
|
141
|
+
g(100, "AcDbDictionary");
|
|
142
|
+
g(281, 1);
|
|
143
|
+
g(3, "Normal");
|
|
144
|
+
g(350, normalPlotStyle);
|
|
145
|
+
g(100, "AcDbDictionaryWithDefault");
|
|
146
|
+
g(340, normalPlotStyle);
|
|
147
|
+
g(0, "ACDBPLACEHOLDER");
|
|
148
|
+
ownedBy(g, normalPlotStyle, plotStyleDictionary);
|
|
149
|
+
g(0, "ENDSEC");
|
|
150
|
+
g(0, "EOF");
|
|
151
|
+
// The header goes first in the file but is written last, once every handle is assigned.
|
|
152
|
+
const header = [];
|
|
153
|
+
const h = groupWriter(header);
|
|
154
|
+
h(0, "SECTION");
|
|
155
|
+
h(2, "HEADER");
|
|
156
|
+
h(9, "$ACADVER");
|
|
157
|
+
h(1, "AC1032");
|
|
158
|
+
h(9, "$DWGCODEPAGE");
|
|
159
|
+
h(3, "ANSI_1252");
|
|
160
|
+
h(9, "$INSBASE");
|
|
161
|
+
point(h, 10, 0, 0);
|
|
162
|
+
h(9, "$EXTMIN");
|
|
163
|
+
point(h, 10, minX, minY);
|
|
164
|
+
h(9, "$EXTMAX");
|
|
165
|
+
point(h, 10, maxX, maxY);
|
|
166
|
+
h(9, "$INSUNITS");
|
|
167
|
+
h(70, INSUNITS[drawing.units]);
|
|
168
|
+
h(9, "$MEASUREMENT");
|
|
169
|
+
h(70, drawing.units === "in" || drawing.units === "ft" ? 0 : 1);
|
|
170
|
+
h(9, "$CLAYER");
|
|
171
|
+
h(8, "0");
|
|
172
|
+
h(9, "$HANDSEED");
|
|
173
|
+
h(5, handle());
|
|
174
|
+
h(0, "ENDSEC");
|
|
175
|
+
return [...header, ...body].join("\r\n") + "\r\n";
|
|
176
|
+
}
|
|
177
|
+
function groupWriter(out) {
|
|
178
|
+
return (code, value) => {
|
|
179
|
+
out.push(String(code), typeof value === "number" ? fmt(value) : value);
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/** Handle, reactor and owner fields for an object owned by a dictionary. */
|
|
183
|
+
function ownedBy(g, handle, owner) {
|
|
184
|
+
g(5, handle);
|
|
185
|
+
g(102, "{ACAD_REACTORS");
|
|
186
|
+
g(330, owner);
|
|
187
|
+
g(102, "}");
|
|
188
|
+
g(330, owner);
|
|
189
|
+
}
|
|
190
|
+
function point(g, code, x, y) {
|
|
191
|
+
g(code, x);
|
|
192
|
+
g(code + 10, y);
|
|
193
|
+
g(code + 20, 0);
|
|
194
|
+
}
|
|
195
|
+
function writeTable(g, handle, name, records) {
|
|
196
|
+
const table = handle();
|
|
197
|
+
g(0, "TABLE");
|
|
198
|
+
g(2, name);
|
|
199
|
+
g(5, table);
|
|
200
|
+
g(330, "0");
|
|
201
|
+
g(100, "AcDbSymbolTable");
|
|
202
|
+
g(70, records.length);
|
|
203
|
+
if (name === "DIMSTYLE") {
|
|
204
|
+
g(100, "AcDbDimStyleTable");
|
|
205
|
+
g(71, records.length);
|
|
206
|
+
for (const r of records)
|
|
207
|
+
g(340, r.handle);
|
|
208
|
+
}
|
|
209
|
+
for (const r of records) {
|
|
210
|
+
g(0, r.type);
|
|
211
|
+
// DIMSTYLE records use 105 for their handle, because 5 is DIMBLK's code there.
|
|
212
|
+
g(r.type === "DIMSTYLE" ? 105 : 5, r.handle);
|
|
213
|
+
g(330, table);
|
|
214
|
+
g(100, "AcDbSymbolTableRecord");
|
|
215
|
+
g(100, r.subclass);
|
|
216
|
+
g(2, r.name);
|
|
217
|
+
g(70, 0);
|
|
218
|
+
r.fields?.(g);
|
|
219
|
+
}
|
|
220
|
+
g(0, "ENDTAB");
|
|
221
|
+
}
|
|
222
|
+
/** The *Active viewport, zoomed to the drawing's extents so the file opens on the drawing. */
|
|
223
|
+
function writeActiveViewport(g, minX, minY, maxX, maxY) {
|
|
224
|
+
const aspect = 1.6;
|
|
225
|
+
const height = Math.max(maxY - minY, (maxX - minX) / aspect, 1) * 1.1;
|
|
226
|
+
g(10, 0); // lower-left corner of the viewport on screen
|
|
227
|
+
g(20, 0);
|
|
228
|
+
g(11, 1); // upper-right corner
|
|
229
|
+
g(21, 1);
|
|
230
|
+
g(12, (minX + maxX) / 2); // view center
|
|
231
|
+
g(22, (minY + maxY) / 2);
|
|
232
|
+
g(13, 0); // snap base
|
|
233
|
+
g(23, 0);
|
|
234
|
+
g(14, 0.5); // snap spacing
|
|
235
|
+
g(24, 0.5);
|
|
236
|
+
g(15, 0.5); // grid spacing
|
|
237
|
+
g(25, 0.5);
|
|
238
|
+
g(16, 0); // view direction
|
|
239
|
+
g(26, 0);
|
|
240
|
+
g(36, 1);
|
|
241
|
+
g(17, 0); // view target
|
|
242
|
+
g(27, 0);
|
|
243
|
+
g(37, 0);
|
|
244
|
+
g(40, height);
|
|
245
|
+
g(41, aspect);
|
|
246
|
+
g(42, 50); // lens length
|
|
247
|
+
g(43, 0); // front clip
|
|
248
|
+
g(44, 0); // back clip
|
|
249
|
+
g(50, 0); // snap rotation
|
|
250
|
+
g(51, 0); // view twist
|
|
251
|
+
g(71, 0); // view mode
|
|
252
|
+
g(72, 1000); // circle zoom percent
|
|
253
|
+
g(73, 1); // fast zoom
|
|
254
|
+
g(74, 3); // UCS icon
|
|
255
|
+
g(75, 0); // snap off
|
|
256
|
+
g(76, 0); // grid off
|
|
257
|
+
g(77, 0); // snap style
|
|
258
|
+
g(78, 0); // snap isopair
|
|
259
|
+
}
|
|
260
|
+
function writeBlock(g, handle, name, owner, paper) {
|
|
261
|
+
g(0, "BLOCK");
|
|
262
|
+
g(5, handle());
|
|
263
|
+
g(330, owner);
|
|
264
|
+
g(100, "AcDbEntity");
|
|
265
|
+
if (paper)
|
|
266
|
+
g(67, 1);
|
|
267
|
+
g(8, "0");
|
|
268
|
+
g(100, "AcDbBlockBegin");
|
|
269
|
+
g(2, name);
|
|
270
|
+
g(70, 0);
|
|
271
|
+
point(g, 10, 0, 0);
|
|
272
|
+
g(3, name);
|
|
273
|
+
g(1, "");
|
|
274
|
+
g(0, "ENDBLK");
|
|
275
|
+
g(5, handle());
|
|
276
|
+
g(330, owner);
|
|
277
|
+
g(100, "AcDbEntity");
|
|
278
|
+
if (paper)
|
|
279
|
+
g(67, 1);
|
|
280
|
+
g(8, "0");
|
|
281
|
+
g(100, "AcDbBlockEnd");
|
|
282
|
+
}
|
|
283
|
+
function writeEntity(g, e, handle, owner) {
|
|
284
|
+
const begin = (type, subclass) => {
|
|
285
|
+
g(0, type);
|
|
286
|
+
g(5, handle);
|
|
287
|
+
g(330, owner);
|
|
288
|
+
g(100, "AcDbEntity");
|
|
289
|
+
g(8, e.layer);
|
|
290
|
+
g(100, subclass);
|
|
291
|
+
};
|
|
292
|
+
switch (e.kind) {
|
|
293
|
+
case "line":
|
|
294
|
+
begin("LINE", "AcDbLine");
|
|
295
|
+
point(g, 10, ...e.from);
|
|
296
|
+
point(g, 11, ...e.to);
|
|
297
|
+
return;
|
|
298
|
+
case "polyline":
|
|
299
|
+
begin("LWPOLYLINE", "AcDbPolyline");
|
|
300
|
+
g(90, e.points.length);
|
|
301
|
+
g(70, e.closed ? 1 : 0);
|
|
302
|
+
g(43, 0); // constant width
|
|
303
|
+
for (const [x, y] of e.points) {
|
|
304
|
+
g(10, x);
|
|
305
|
+
g(20, y);
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
case "circle":
|
|
309
|
+
begin("CIRCLE", "AcDbCircle");
|
|
310
|
+
point(g, 10, ...e.center);
|
|
311
|
+
g(40, e.radius);
|
|
312
|
+
return;
|
|
313
|
+
case "arc":
|
|
314
|
+
begin("ARC", "AcDbCircle");
|
|
315
|
+
point(g, 10, ...e.center);
|
|
316
|
+
g(40, e.radius);
|
|
317
|
+
g(100, "AcDbArc");
|
|
318
|
+
g(50, e.startAngle);
|
|
319
|
+
g(51, e.endAngle);
|
|
320
|
+
return;
|
|
321
|
+
case "text":
|
|
322
|
+
begin("TEXT", "AcDbText");
|
|
323
|
+
point(g, 10, ...e.at);
|
|
324
|
+
g(40, e.height);
|
|
325
|
+
g(1, dxfText(e.value));
|
|
326
|
+
if (e.rotation !== 0)
|
|
327
|
+
g(50, e.rotation);
|
|
328
|
+
g(7, "Standard");
|
|
329
|
+
// TEXT repeats its subclass marker before the vertical alignment fields.
|
|
330
|
+
g(100, "AcDbText");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
}
|
package/dist/dxf.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Drawing, DxfVersion } from "./drawing.ts";
|
|
2
|
+
export interface DxfOptions {
|
|
3
|
+
/** Defaults to the version the drawing was created for, which is `"R12"` unless set. */
|
|
4
|
+
readonly version?: DxfVersion;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Writes the drawing as ASCII DXF. Throws a `DrawingError` listing every lint
|
|
8
|
+
* error when the drawing breaks a rule of the version being written, because
|
|
9
|
+
* AutoCAD would reject or garble the file.
|
|
10
|
+
*/
|
|
11
|
+
export declare function toDXF(drawing: Drawing, options?: DxfOptions): string;
|
package/dist/dxf.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { toDXFR12 } from "./dxf-r12.js";
|
|
2
|
+
import { toDXFR2018 } from "./dxf-r2018.js";
|
|
3
|
+
import { DrawingError, lint } from "./lint.js";
|
|
4
|
+
/**
|
|
5
|
+
* Writes the drawing as ASCII DXF. Throws a `DrawingError` listing every lint
|
|
6
|
+
* error when the drawing breaks a rule of the version being written, because
|
|
7
|
+
* AutoCAD would reject or garble the file.
|
|
8
|
+
*/
|
|
9
|
+
export function toDXF(drawing, options = {}) {
|
|
10
|
+
const version = options.version ?? drawing.dxf;
|
|
11
|
+
if (version !== "R12" && version !== "R2018") {
|
|
12
|
+
throw new Error(`Unsupported DXF version: ${JSON.stringify(version)}`);
|
|
13
|
+
}
|
|
14
|
+
const errors = lint(drawing, { version }).filter((d) => d.severity === "error");
|
|
15
|
+
if (errors.length > 0)
|
|
16
|
+
throw new DrawingError(errors);
|
|
17
|
+
return version === "R12" ? toDXFR12(drawing) : toDXFR2018(drawing);
|
|
18
|
+
}
|
package/dist/extents.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Approximate extents. Arcs use their full circle; text assumes 0.6 × height per character. */
|
|
2
|
+
export function extents(entities) {
|
|
3
|
+
let minX = Infinity;
|
|
4
|
+
let minY = Infinity;
|
|
5
|
+
let maxX = -Infinity;
|
|
6
|
+
let maxY = -Infinity;
|
|
7
|
+
const add = (x, y) => {
|
|
8
|
+
minX = Math.min(minX, x);
|
|
9
|
+
minY = Math.min(minY, y);
|
|
10
|
+
maxX = Math.max(maxX, x);
|
|
11
|
+
maxY = Math.max(maxY, y);
|
|
12
|
+
};
|
|
13
|
+
for (const e of entities) {
|
|
14
|
+
switch (e.kind) {
|
|
15
|
+
case "line":
|
|
16
|
+
add(...e.from);
|
|
17
|
+
add(...e.to);
|
|
18
|
+
break;
|
|
19
|
+
case "polyline":
|
|
20
|
+
for (const p of e.points)
|
|
21
|
+
add(...p);
|
|
22
|
+
break;
|
|
23
|
+
case "circle":
|
|
24
|
+
case "arc":
|
|
25
|
+
add(e.center[0] - e.radius, e.center[1] - e.radius);
|
|
26
|
+
add(e.center[0] + e.radius, e.center[1] + e.radius);
|
|
27
|
+
break;
|
|
28
|
+
case "text": {
|
|
29
|
+
const len = e.value.length * e.height * 0.6;
|
|
30
|
+
const r = rad(e.rotation);
|
|
31
|
+
add(...e.at);
|
|
32
|
+
add(e.at[0] + len * Math.cos(r), e.at[1] + len * Math.sin(r));
|
|
33
|
+
add(e.at[0] - e.height * Math.sin(r), e.at[1] + e.height * Math.cos(r));
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return minX === Infinity ? [0, 0, 1, 1] : [minX, minY, maxX, maxY];
|
|
39
|
+
}
|
|
40
|
+
function rad(degrees) {
|
|
41
|
+
return (degrees * Math.PI) / 180;
|
|
42
|
+
}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Formats a coordinate with at most 6 decimals, no float noise, and no "-0". */
|
|
2
|
+
export declare function fmt(value: number): string;
|
|
3
|
+
/**
|
|
4
|
+
* Escapes a string value for DXF. A caret starts a control-character escape in
|
|
5
|
+
* DXF, and AutoCAD discards the drawing when it meets a bare one, so a literal
|
|
6
|
+
* caret is written as a caret and a space.
|
|
7
|
+
*
|
|
8
|
+
* With `ascii`, characters outside ASCII become `\U+XXXX`, which is how DXF
|
|
9
|
+
* versions before 2007 carry them. Later versions are UTF-8 and need no escape.
|
|
10
|
+
*/
|
|
11
|
+
export declare function dxfText(value: string, ascii?: boolean): string;
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Formats a coordinate with at most 6 decimals, no float noise, and no "-0". */
|
|
2
|
+
export function fmt(value) {
|
|
3
|
+
const rounded = Number(value.toFixed(6));
|
|
4
|
+
return Object.is(rounded, -0) ? "0" : String(rounded);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Escapes a string value for DXF. A caret starts a control-character escape in
|
|
8
|
+
* DXF, and AutoCAD discards the drawing when it meets a bare one, so a literal
|
|
9
|
+
* caret is written as a caret and a space.
|
|
10
|
+
*
|
|
11
|
+
* With `ascii`, characters outside ASCII become `\U+XXXX`, which is how DXF
|
|
12
|
+
* versions before 2007 carry them. Later versions are UTF-8 and need no escape.
|
|
13
|
+
*/
|
|
14
|
+
export function dxfText(value, ascii = false) {
|
|
15
|
+
const escaped = value.replaceAll("^", "^ ");
|
|
16
|
+
if (!ascii)
|
|
17
|
+
return escaped;
|
|
18
|
+
let out = "";
|
|
19
|
+
for (let i = 0; i < escaped.length; i++) {
|
|
20
|
+
const code = escaped.charCodeAt(i);
|
|
21
|
+
out += code < 0x80 ? escaped[i] : `\\U+${code.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createDrawing, DrawingBuilder, sweep, type Arc, type Circle, type Drawing, type DrawingOptions, type DxfVersion, type Entity, type Layer, type LayerOptions, type Line, type Point, type Polyline, type Text, type TextOptions, type Units, } from "./drawing.ts";
|
|
2
|
+
export { toDXF, type DxfOptions } from "./dxf.ts";
|
|
3
|
+
export { DrawingError, lint, type Diagnostic, type LintOptions, type LintRule } from "./lint.ts";
|
|
4
|
+
export type { Aci, LayerColor, LayerName, Positive } from "./rules.ts";
|
|
5
|
+
export { toSVG, type SvgOptions } from "./svg.ts";
|
package/dist/index.js
ADDED
package/dist/lint.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Drawing, DxfVersion } from "./drawing.ts";
|
|
2
|
+
export type LintRule = "units" | "layer-name" | "duplicate-layer" | "layer-color" | "undefined-layer" | "geometry" | "text" | "zero-length";
|
|
3
|
+
export interface Diagnostic {
|
|
4
|
+
readonly rule: LintRule;
|
|
5
|
+
/** An error means AutoCAD rejects, repairs or garbles the output. `toDXF` refuses to write it. */
|
|
6
|
+
readonly severity: "error" | "warning";
|
|
7
|
+
readonly message: string;
|
|
8
|
+
/** The layer the problem is in, when it is a layer problem. */
|
|
9
|
+
readonly layer?: string;
|
|
10
|
+
/** Index into `drawing.entities`, when it is an entity problem. */
|
|
11
|
+
readonly entity?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface LintOptions {
|
|
14
|
+
/** The DXF version to check against. Defaults to the drawing's own. */
|
|
15
|
+
readonly version?: DxfVersion;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Checks a drawing against the rules AutoCAD enforces when it opens DXF.
|
|
19
|
+
*
|
|
20
|
+
* A drawing made with `createDrawing()` already passes the error rules for its
|
|
21
|
+
* own DXF version, because the builder throws at the offending call. `lint`
|
|
22
|
+
* matters when a `Drawing` was built by hand or by other code, when it is
|
|
23
|
+
* exported as a different version than it was built for, and for warnings.
|
|
24
|
+
*/
|
|
25
|
+
export declare function lint(drawing: Drawing, options?: LintOptions): Diagnostic[];
|
|
26
|
+
/** Thrown by `toDXF` when a drawing has lint errors for the version being written. */
|
|
27
|
+
export declare class DrawingError extends Error {
|
|
28
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
29
|
+
constructor(diagnostics: readonly Diagnostic[]);
|
|
30
|
+
}
|
package/dist/lint.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { layerColorProblem, layerNameProblem, textProblem } from "./rules.js";
|
|
2
|
+
/**
|
|
3
|
+
* Checks a drawing against the rules AutoCAD enforces when it opens DXF.
|
|
4
|
+
*
|
|
5
|
+
* A drawing made with `createDrawing()` already passes the error rules for its
|
|
6
|
+
* own DXF version, because the builder throws at the offending call. `lint`
|
|
7
|
+
* matters when a `Drawing` was built by hand or by other code, when it is
|
|
8
|
+
* exported as a different version than it was built for, and for warnings.
|
|
9
|
+
*/
|
|
10
|
+
export function lint(drawing, options = {}) {
|
|
11
|
+
const version = options.version ?? drawing.dxf;
|
|
12
|
+
const out = [];
|
|
13
|
+
if (!["in", "ft", "mm", "cm", "m"].includes(drawing.units)) {
|
|
14
|
+
out.push({
|
|
15
|
+
rule: "units",
|
|
16
|
+
severity: "error",
|
|
17
|
+
message: `Unknown units: ${JSON.stringify(drawing.units)}`,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
for (const layer of drawing.layers) {
|
|
22
|
+
const error = (rule, message) => out.push({ rule, severity: "error", message, layer: layer.name });
|
|
23
|
+
const nameProblem = layerNameProblem(layer.name, version);
|
|
24
|
+
if (nameProblem)
|
|
25
|
+
error("layer-name", `Layer ${JSON.stringify(layer.name)}: ${nameProblem}`);
|
|
26
|
+
const colorProblem = layerColorProblem(layer.color);
|
|
27
|
+
if (colorProblem)
|
|
28
|
+
error("layer-color", `Layer ${JSON.stringify(layer.name)}: ${colorProblem}`);
|
|
29
|
+
const key = layer.name.toLowerCase();
|
|
30
|
+
if (seen.has(key)) {
|
|
31
|
+
error("duplicate-layer", `Layer ${JSON.stringify(layer.name)} is defined twice. CAD ignores case in layer names`);
|
|
32
|
+
}
|
|
33
|
+
seen.add(key);
|
|
34
|
+
}
|
|
35
|
+
if (!seen.has("0")) {
|
|
36
|
+
out.push({ rule: "undefined-layer", severity: "error", message: 'Layer "0" is missing' });
|
|
37
|
+
}
|
|
38
|
+
drawing.entities.forEach((entity, index) => {
|
|
39
|
+
const add = (rule, severity, problem) => out.push({ rule, severity, message: `${entity.kind} #${index}: ${problem}`, entity: index });
|
|
40
|
+
if (!seen.has(entity.layer.toLowerCase())) {
|
|
41
|
+
add("undefined-layer", "error", `layer ${JSON.stringify(entity.layer)} is not defined`);
|
|
42
|
+
}
|
|
43
|
+
for (const problem of geometryProblems(entity))
|
|
44
|
+
add("geometry", "error", problem);
|
|
45
|
+
if (entity.kind === "text") {
|
|
46
|
+
const problem = textProblem(entity.value);
|
|
47
|
+
if (problem)
|
|
48
|
+
add("text", "error", problem);
|
|
49
|
+
}
|
|
50
|
+
if (entity.kind === "line" && same(entity.from, entity.to)) {
|
|
51
|
+
add("zero-length", "warning", "the line has no length");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/** Thrown by `toDXF` when a drawing has lint errors for the version being written. */
|
|
57
|
+
export class DrawingError extends Error {
|
|
58
|
+
diagnostics;
|
|
59
|
+
constructor(diagnostics) {
|
|
60
|
+
const shown = diagnostics.slice(0, 10).map((d) => `- ${d.message}`);
|
|
61
|
+
const more = diagnostics.length > 10 ? [`- and ${diagnostics.length - 10} more`] : [];
|
|
62
|
+
super(["The drawing cannot be written as DXF:", ...shown, ...more].join("\n"));
|
|
63
|
+
this.name = "DrawingError";
|
|
64
|
+
this.diagnostics = diagnostics;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function geometryProblems(e) {
|
|
68
|
+
const out = [];
|
|
69
|
+
const finite = (label, ...values) => {
|
|
70
|
+
if (!values.every(Number.isFinite))
|
|
71
|
+
out.push(`${label} must be finite numbers`);
|
|
72
|
+
};
|
|
73
|
+
const positive = (label, value) => {
|
|
74
|
+
if (!(Number.isFinite(value) && value > 0))
|
|
75
|
+
out.push(`${label} must be greater than 0`);
|
|
76
|
+
};
|
|
77
|
+
switch (e.kind) {
|
|
78
|
+
case "line":
|
|
79
|
+
finite("its points", ...e.from, ...e.to);
|
|
80
|
+
break;
|
|
81
|
+
case "polyline":
|
|
82
|
+
if (e.points.length < 2)
|
|
83
|
+
out.push("a polyline needs at least 2 points");
|
|
84
|
+
finite("its points", ...e.points.flat());
|
|
85
|
+
break;
|
|
86
|
+
case "circle":
|
|
87
|
+
finite("its center", ...e.center);
|
|
88
|
+
positive("its radius", e.radius);
|
|
89
|
+
break;
|
|
90
|
+
case "arc":
|
|
91
|
+
finite("its center", ...e.center);
|
|
92
|
+
positive("its radius", e.radius);
|
|
93
|
+
if (![e.startAngle, e.endAngle].every((a) => Number.isFinite(a) && a >= 0 && a < 360)) {
|
|
94
|
+
out.push("its angles must be from 0 up to 360");
|
|
95
|
+
}
|
|
96
|
+
else if (e.startAngle === e.endAngle) {
|
|
97
|
+
out.push("its start and end angles must differ");
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
case "text":
|
|
101
|
+
finite("its position and rotation", ...e.at, e.rotation);
|
|
102
|
+
positive("its height", e.height);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
function same(a, b) {
|
|
108
|
+
return a[0] === b[0] && a[1] === b[1];
|
|
109
|
+
}
|
package/dist/rules.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { DxfVersion } from "./drawing.ts";
|
|
2
|
+
declare const NAME_EMPTY = "Layer names cannot be empty";
|
|
3
|
+
declare const NAME_CHARS = "Layer names cannot contain < > / \\ \" : ; ? * | = ` ^ , or line breaks";
|
|
4
|
+
declare const NAME_EDGE = "Layer names cannot start or end with a space";
|
|
5
|
+
declare const NAME_R12_CHARS = "R12 layer names can only use letters, digits, $, - and _. Use DXF version \"R2018\" for other characters";
|
|
6
|
+
declare const NAME_R12_LENGTH = "R12 layer names cannot be longer than 31 characters. Use DXF version \"R2018\" for longer names";
|
|
7
|
+
declare const COLOR = "A layer color must be an AutoCAD Color Index integer from 1 to 255";
|
|
8
|
+
declare const POSITIVE = "This value must be greater than 0";
|
|
9
|
+
type ForbiddenChar = "<" | ">" | "/" | "\\" | '"' | ":" | ";" | "?" | "*" | "|" | "=" | "`" | "^" | "," | "\n" | "\r" | "\t";
|
|
10
|
+
type Chars<S extends string, Found = never> = S extends `${infer C}${infer Rest}` ? Chars<Rest, Found | C> : Found;
|
|
11
|
+
type R12Char = Chars<"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$_-">;
|
|
12
|
+
/** Walks the name one character at a time, counting, so it can check both the characters and the length. */
|
|
13
|
+
type R12Name<N extends string, Rest extends string = N, Count extends 0[] = []> = Count["length"] extends 32 ? typeof NAME_R12_LENGTH : Rest extends `${infer C}${infer Tail}` ? C extends R12Char ? R12Name<N, Tail, [...Count, 0]> : typeof NAME_R12_CHARS : N;
|
|
14
|
+
/**
|
|
15
|
+
* Resolves to `N` when it is a valid layer name for DXF version `V`, and to the
|
|
16
|
+
* error message otherwise, so the compiler reports the reason. Names that are
|
|
17
|
+
* not literals pass the compiler and are checked when the code runs.
|
|
18
|
+
*/
|
|
19
|
+
export type LayerName<N extends string, V extends DxfVersion> = string extends N ? N : N extends "" ? typeof NAME_EMPTY : N extends `${string}${ForbiddenChar}${string}` ? typeof NAME_CHARS : N extends ` ${string}` | `${string} ` ? typeof NAME_EDGE : V extends "R12" ? R12Name<N> : N;
|
|
20
|
+
type Enumerate<N extends number, A extends number[] = []> = A["length"] extends N ? A[number] : Enumerate<N, [...A, A["length"]]>;
|
|
21
|
+
/** AutoCAD Color Index: an integer from 1 to 255. */
|
|
22
|
+
export type Aci = Exclude<Enumerate<256>, 0>;
|
|
23
|
+
/** Resolves to `C` when it is a valid layer color, and to the error message otherwise. */
|
|
24
|
+
export type LayerColor<C extends number> = number extends C ? C : C extends Aci ? C : typeof COLOR;
|
|
25
|
+
/** Resolves to `N` when it is greater than 0, and to the error message otherwise. */
|
|
26
|
+
export type Positive<N extends number> = number extends N ? N : `${N}` extends `-${string}` | "0" ? typeof POSITIVE : N;
|
|
27
|
+
/** Returns what is wrong with a layer name for a DXF version, or undefined when it is valid. */
|
|
28
|
+
export declare function layerNameProblem(name: string, version: DxfVersion): string | undefined;
|
|
29
|
+
/** Returns what is wrong with a layer color, or undefined when it is valid. */
|
|
30
|
+
export declare function layerColorProblem(color: number): string | undefined;
|
|
31
|
+
/** Returns what is wrong with a single-line text value, or undefined when it is valid. */
|
|
32
|
+
export declare function textProblem(value: string): string | undefined;
|
|
33
|
+
export {};
|