tsdraft 0.2.2 → 0.4.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 +87 -14
- package/README.md +141 -212
- package/dist/callsite.d.ts +47 -0
- package/dist/callsite.js +82 -0
- package/dist/drawing.d.ts +164 -16
- package/dist/drawing.js +339 -75
- package/dist/dxf-r2018.js +36 -15
- package/dist/dxf.d.ts +1 -1
- package/dist/format.d.ts +7 -0
- package/dist/format.js +9 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -0
- package/dist/json.d.ts +21 -0
- package/dist/json.js +244 -0
- package/dist/layers.d.ts +1 -1
- package/dist/layers.js +2 -3
- package/dist/linetypes.d.ts +27 -0
- package/dist/linetypes.js +76 -0
- package/dist/lint.d.ts +19 -5
- package/dist/lint.js +132 -7
- package/dist/paper.d.ts +33 -0
- package/dist/paper.js +58 -0
- package/dist/rules.d.ts +190 -9
- package/dist/rules.js +239 -22
- package/dist/svg.d.ts +11 -2
- package/dist/svg.js +143 -10
- package/package.json +6 -4
package/dist/drawing.js
CHANGED
|
@@ -1,104 +1,242 @@
|
|
|
1
|
+
import { holdCall, readCall, recordCalls } from "./callsite.js";
|
|
1
2
|
import { fmt, written } from "./format.js";
|
|
2
3
|
import { layerAuditKey, layerKey } from "./layers.js";
|
|
3
|
-
import {
|
|
4
|
+
import { BUILT_IN_LINETYPES, builtInLinetype } from "./linetypes.js";
|
|
5
|
+
import { DrawingError } from "./lint.js";
|
|
6
|
+
import { arcProblem, duplicateIdProblem, idKey, idProblem, layerColorProblem, layerFlagProblem, layerLinetypeProblem, layerStateProblem, lineweightProblem, linetypeNameProblem, linetypeProblem, linetypeScaleProblem, magnitudeProblem, layerNameProblem, layerTwinProblem, R12_ONLY, sizeProblem, textProblem, undefinedLinetypeProblem, } from "./rules.js";
|
|
4
7
|
const UNITS = ["in", "ft", "mm", "cm", "m"];
|
|
5
8
|
const DXF_VERSIONS = ["R12", "R2018"];
|
|
9
|
+
/** A broken rule, thrown inside a builder method and turned into a `DrawingError` on its way out. */
|
|
10
|
+
class Problem {
|
|
11
|
+
rule;
|
|
12
|
+
message;
|
|
13
|
+
constructor(rule, message) {
|
|
14
|
+
this.rule = rule;
|
|
15
|
+
this.message = message;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function refuse(rule, message) {
|
|
19
|
+
throw new Problem(rule, message);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* What a builder method throws for something thrown inside it: for a `Problem`, a `DrawingError`
|
|
23
|
+
* whose one diagnostic names the caller's call of `method` and what the call was `about`.
|
|
24
|
+
*/
|
|
25
|
+
function located(thrown, method, about) {
|
|
26
|
+
if (!(thrown instanceof Problem))
|
|
27
|
+
return thrown;
|
|
28
|
+
const call = readCall(holdCall(method));
|
|
29
|
+
const diagnostic = {
|
|
30
|
+
rule: thrown.rule,
|
|
31
|
+
severity: "error",
|
|
32
|
+
message: thrown.message,
|
|
33
|
+
...about,
|
|
34
|
+
...(call ? { call } : {}),
|
|
35
|
+
};
|
|
36
|
+
return new DrawingError([diagnostic], thrown.message);
|
|
37
|
+
}
|
|
38
|
+
/** Whether a tool that runs drawing files has asked every builder to record calls. */
|
|
39
|
+
function recordsCalls() {
|
|
40
|
+
return globalThis.TSDRAFT_RECORD_CALLS === true;
|
|
41
|
+
}
|
|
6
42
|
/**
|
|
7
43
|
* Builds a drawing. Entities go on the current layer, which starts as "0".
|
|
8
44
|
* `layer(name, opts, draw)` scopes everything drawn inside `draw` to that layer.
|
|
9
45
|
*
|
|
10
46
|
* Mistakes written as literals, such as a layer name the DXF version does not
|
|
11
47
|
* allow or a negative radius, are compile errors. The same checks run again
|
|
12
|
-
* when the code runs, for values the compiler cannot see
|
|
48
|
+
* when the code runs, for values the compiler cannot see: `layer`, `linetype`
|
|
49
|
+
* and the entity methods then throw a `DrawingError` whose diagnostic has the
|
|
50
|
+
* rule, the entity's `id` and the `call` that broke it.
|
|
13
51
|
*/
|
|
14
52
|
export class DrawingBuilder {
|
|
15
53
|
units;
|
|
16
54
|
dxf;
|
|
55
|
+
linetypeScale;
|
|
17
56
|
/** Keyed by `layerKey`, because AutoCAD ignores case in layer names. */
|
|
18
57
|
#layers = new Map([["0", { name: "0", color: 7 }]]);
|
|
19
58
|
/** The first layer name for each `layerAuditKey`, to find names AUDIT would call duplicates. */
|
|
20
59
|
#audited = new Map([["0", "0"]]);
|
|
60
|
+
/** Keyed by `layerKey`: AutoCAD compares linetype names as it compares layer names. */
|
|
61
|
+
#linetypes = new Map();
|
|
62
|
+
/** Seeded with the built-in linetypes, whose twins AUDIT reports as it reports any other. */
|
|
63
|
+
#linetypesAudited = new Map(BUILT_IN_LINETYPES.map((name) => [layerAuditKey(name), name]));
|
|
21
64
|
#entities = [];
|
|
65
|
+
/** The first spelling of each entity id, by `idKey`. */
|
|
66
|
+
#ids = new Map();
|
|
67
|
+
/** The call that drew each entity, when calls are recorded. */
|
|
68
|
+
#calls;
|
|
22
69
|
#current = "0";
|
|
23
70
|
constructor(options = {}) {
|
|
24
71
|
const units = options.units ?? "in";
|
|
25
72
|
if (!UNITS.includes(units))
|
|
26
73
|
throw new Error(`Unknown units: ${JSON.stringify(units)}`);
|
|
27
|
-
const dxf = options.dxf ?? "
|
|
74
|
+
const dxf = options.dxf ?? "R2018";
|
|
28
75
|
if (!DXF_VERSIONS.includes(dxf)) {
|
|
29
76
|
throw new Error(`Unsupported DXF version: ${JSON.stringify(dxf)}`);
|
|
30
77
|
}
|
|
78
|
+
const scale = options.linetypeScale;
|
|
79
|
+
if (scale !== undefined) {
|
|
80
|
+
if (typeof scale !== "number")
|
|
81
|
+
throw new Error("linetypeScale must be a finite number");
|
|
82
|
+
if (dxf === "R12" && scale !== 1)
|
|
83
|
+
throw new Error(`linetypeScale: ${R12_ONLY.linetypes}`);
|
|
84
|
+
const problem = linetypeScaleProblem(scale);
|
|
85
|
+
if (problem)
|
|
86
|
+
throw new Error(`linetypeScale: ${problem}`);
|
|
87
|
+
}
|
|
31
88
|
this.units = units;
|
|
32
89
|
this.dxf = dxf;
|
|
90
|
+
this.linetypeScale = scale ?? 1;
|
|
91
|
+
this.#calls = (options.recordCalls ?? recordsCalls()) ? [] : undefined;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Defines a linetype, or updates the one of the same name. `dashed` and
|
|
95
|
+
* `DASHED` are the same linetype, as in CAD, and keep the first spelling.
|
|
96
|
+
* tsdraft exports the linetypes plans use, such as `DASHED` and `HIDDEN`.
|
|
97
|
+
*/
|
|
98
|
+
linetype(linetype) {
|
|
99
|
+
const definition = linetype;
|
|
100
|
+
try {
|
|
101
|
+
if (this.dxf === "R12")
|
|
102
|
+
refuse("linetype", R12_ONLY.linetypes);
|
|
103
|
+
this.#define(definition);
|
|
104
|
+
}
|
|
105
|
+
catch (thrown) {
|
|
106
|
+
const name = definition?.name;
|
|
107
|
+
const about = typeof name === "string" ? { linetype: name } : {};
|
|
108
|
+
throw located(thrown, DrawingBuilder.prototype.linetype, about);
|
|
109
|
+
}
|
|
110
|
+
return this;
|
|
33
111
|
}
|
|
34
112
|
/**
|
|
35
113
|
* Defines or updates a layer. With `draw`, entities drawn inside it go on this layer.
|
|
36
114
|
* `wall` and `WALL` are the same layer, as in CAD, and keep the first spelling.
|
|
115
|
+
* Options left out keep the values the layer has.
|
|
37
116
|
*/
|
|
38
117
|
layer(name, options = {}, draw) {
|
|
39
118
|
// The parameter type is the error message when a literal name is wrong, so read it as a string.
|
|
40
119
|
const requested = name;
|
|
120
|
+
let layer;
|
|
121
|
+
try {
|
|
122
|
+
layer = this.#layer(requested, options);
|
|
123
|
+
}
|
|
124
|
+
catch (thrown) {
|
|
125
|
+
throw located(thrown, DrawingBuilder.prototype.layer, { layer: requested });
|
|
126
|
+
}
|
|
127
|
+
if (draw) {
|
|
128
|
+
const previous = this.#current;
|
|
129
|
+
this.#current = layer.name;
|
|
130
|
+
try {
|
|
131
|
+
draw();
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
this.#current = previous;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
/** Checks a layer and stores it. */
|
|
140
|
+
#layer(requested, o) {
|
|
141
|
+
const fail = (rule, problem) => refuse(rule, `Layer ${JSON.stringify(requested)}: ${problem}`);
|
|
41
142
|
const nameProblem = layerNameProblem(requested, this.dxf);
|
|
42
143
|
if (nameProblem)
|
|
43
|
-
|
|
144
|
+
fail("layer-name", nameProblem);
|
|
44
145
|
const key = layerKey(requested);
|
|
45
146
|
const existing = this.#layers.get(key);
|
|
46
147
|
const auditKey = layerAuditKey(requested);
|
|
47
148
|
const twinProblem = layerTwinProblem(requested, this.#audited.get(auditKey) ?? requested);
|
|
48
149
|
if (twinProblem)
|
|
49
|
-
|
|
50
|
-
const color =
|
|
150
|
+
fail("duplicate-layer", twinProblem);
|
|
151
|
+
const color = o.color ?? existing?.color ?? 7;
|
|
51
152
|
const colorProblem = layerColorProblem(color);
|
|
52
153
|
if (colorProblem)
|
|
53
|
-
|
|
154
|
+
fail("layer-color", colorProblem);
|
|
54
155
|
const layer = { name: existing?.name ?? requested, color };
|
|
156
|
+
const set = (field, value, plain) => {
|
|
157
|
+
const kept = value === undefined ? existing?.[field] : value;
|
|
158
|
+
if (kept !== undefined && kept !== plain)
|
|
159
|
+
layer[field] = kept;
|
|
160
|
+
};
|
|
161
|
+
const r12 = this.dxf === "R12";
|
|
162
|
+
let linetype;
|
|
163
|
+
if (o.linetype !== undefined) {
|
|
164
|
+
if (r12 && !isBuiltIn(o.linetype, "Continuous"))
|
|
165
|
+
fail("linetype", R12_ONLY.linetypes);
|
|
166
|
+
linetype = this.#linetypeName(o.linetype, fail);
|
|
167
|
+
const problem = layerLinetypeProblem(linetype);
|
|
168
|
+
if (problem)
|
|
169
|
+
fail("linetype", problem);
|
|
170
|
+
}
|
|
171
|
+
set("linetype", linetype === undefined ? undefined : (builtInLinetype(linetype) ?? linetype), "Continuous");
|
|
172
|
+
if (o.lineweight !== undefined) {
|
|
173
|
+
const problem = lineweightProblem(o.lineweight, "layer");
|
|
174
|
+
if (problem)
|
|
175
|
+
fail("lineweight", problem);
|
|
176
|
+
if (r12 && o.lineweight !== "Default")
|
|
177
|
+
fail("lineweight", R12_ONLY.lineweights);
|
|
178
|
+
}
|
|
179
|
+
set("lineweight", o.lineweight, "Default");
|
|
180
|
+
for (const state of ["off", "frozen", "locked", "plot"]) {
|
|
181
|
+
const value = o[state];
|
|
182
|
+
const flagProblem = layerFlagProblem(state, value);
|
|
183
|
+
if (flagProblem)
|
|
184
|
+
fail("layer-state", flagProblem);
|
|
185
|
+
if (r12 && value !== undefined && value !== (state === "plot")) {
|
|
186
|
+
fail("layer-state", R12_ONLY.layerStates);
|
|
187
|
+
}
|
|
188
|
+
set(state, value, state === "plot");
|
|
189
|
+
}
|
|
190
|
+
const stateProblem = layerStateProblem(layer.name, layer.frozen);
|
|
191
|
+
if (stateProblem)
|
|
192
|
+
fail("layer-state", stateProblem);
|
|
55
193
|
this.#layers.set(key, layer);
|
|
56
194
|
if (!this.#audited.has(auditKey))
|
|
57
195
|
this.#audited.set(auditKey, layer.name);
|
|
58
|
-
|
|
59
|
-
const previous = this.#current;
|
|
60
|
-
this.#current = layer.name;
|
|
61
|
-
try {
|
|
62
|
-
draw();
|
|
63
|
-
}
|
|
64
|
-
finally {
|
|
65
|
-
this.#current = previous;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return this;
|
|
196
|
+
return layer;
|
|
69
197
|
}
|
|
70
|
-
line(from, to) {
|
|
71
|
-
return this.#add(
|
|
198
|
+
line(from, to, options = {}) {
|
|
199
|
+
return this.#add(DrawingBuilder.prototype.line, options, () => ({
|
|
200
|
+
kind: "line",
|
|
201
|
+
layer: this.#current,
|
|
202
|
+
from: pt(from),
|
|
203
|
+
to: pt(to),
|
|
204
|
+
}));
|
|
72
205
|
}
|
|
73
|
-
polyline(points, closed = false) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
206
|
+
polyline(points, closed = false, options = {}) {
|
|
207
|
+
return this.#add(DrawingBuilder.prototype.polyline, options, () => {
|
|
208
|
+
if (points.length < 2)
|
|
209
|
+
refuse("geometry", "A polyline needs at least 2 points");
|
|
210
|
+
return { kind: "polyline", layer: this.#current, points: points.map(pt), closed };
|
|
211
|
+
});
|
|
77
212
|
}
|
|
78
213
|
/** Closed rectangle from its lower-left corner. */
|
|
79
|
-
rect(corner, width, height) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
[
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
214
|
+
rect(corner, width, height, options = {}) {
|
|
215
|
+
return this.#add(DrawingBuilder.prototype.rect, options, () => {
|
|
216
|
+
const [x, y] = pt(corner);
|
|
217
|
+
const w = positive(width, "width");
|
|
218
|
+
const h = positive(height, "height");
|
|
219
|
+
// A side can still vanish in the file: a corner and the one across from it may round together.
|
|
220
|
+
if (written(x + w) === written(x))
|
|
221
|
+
refuse("geometry", lostSide("width", "x", x));
|
|
222
|
+
if (written(y + h) === written(y))
|
|
223
|
+
refuse("geometry", lostSide("height", "y", y));
|
|
224
|
+
const corners = [
|
|
225
|
+
[x, y],
|
|
226
|
+
[x + w, y],
|
|
227
|
+
[x + w, y + h],
|
|
228
|
+
[x, y + h],
|
|
229
|
+
];
|
|
230
|
+
return { kind: "polyline", layer: this.#current, points: corners.map(pt), closed: true };
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
circle(center, radius, options = {}) {
|
|
234
|
+
return this.#add(DrawingBuilder.prototype.circle, options, () => ({
|
|
97
235
|
kind: "circle",
|
|
98
236
|
layer: this.#current,
|
|
99
237
|
center: pt(center),
|
|
100
238
|
radius: positive(radius, "radius"),
|
|
101
|
-
});
|
|
239
|
+
}));
|
|
102
240
|
}
|
|
103
241
|
/**
|
|
104
242
|
* Arc from `startAngle` counterclockwise to `endAngle`. Angles are stored
|
|
@@ -109,52 +247,178 @@ export class DrawingBuilder {
|
|
|
109
247
|
* full turn would be written with equal angles, which AutoCAD holds as an
|
|
110
248
|
* arc of no length, so it throws: draw a circle instead.
|
|
111
249
|
*/
|
|
112
|
-
arc(center, radius, startAngle, endAngle) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
250
|
+
arc(center, radius, startAngle, endAngle, options = {}) {
|
|
251
|
+
return this.#add(DrawingBuilder.prototype.arc, options, () => {
|
|
252
|
+
const start = turn(num(startAngle, "startAngle"));
|
|
253
|
+
const end = turn(num(endAngle, "endAngle"));
|
|
254
|
+
if (start === end)
|
|
255
|
+
refuse("geometry", "An arc's start and end angles must differ");
|
|
256
|
+
const problem = arcProblem(start, end);
|
|
257
|
+
if (problem)
|
|
258
|
+
refuse("geometry", problem);
|
|
259
|
+
return {
|
|
260
|
+
kind: "arc",
|
|
261
|
+
layer: this.#current,
|
|
262
|
+
center: pt(center),
|
|
263
|
+
radius: positive(radius, "radius"),
|
|
264
|
+
startAngle: start,
|
|
265
|
+
endAngle: end,
|
|
266
|
+
};
|
|
127
267
|
});
|
|
128
268
|
}
|
|
129
269
|
text(at, value, options = {}) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
270
|
+
return this.#add(DrawingBuilder.prototype.text, options, () => {
|
|
271
|
+
const problem = textProblem(value);
|
|
272
|
+
if (problem)
|
|
273
|
+
refuse("text", problem);
|
|
274
|
+
return {
|
|
275
|
+
kind: "text",
|
|
276
|
+
layer: this.#current,
|
|
277
|
+
at: pt(at),
|
|
278
|
+
value,
|
|
279
|
+
height: positive(options.height ?? 0.125, "height"),
|
|
280
|
+
rotation: kept(options.rotation ?? 0, "rotation"),
|
|
281
|
+
};
|
|
140
282
|
});
|
|
141
283
|
}
|
|
142
284
|
build() {
|
|
285
|
+
const entities = [...this.#entities];
|
|
286
|
+
if (this.#calls)
|
|
287
|
+
recordCalls(entities, [...this.#calls]);
|
|
143
288
|
return {
|
|
289
|
+
format: 0,
|
|
144
290
|
units: this.units,
|
|
145
291
|
dxf: this.dxf,
|
|
146
292
|
layers: [...this.#layers.values()],
|
|
147
|
-
entities
|
|
293
|
+
entities,
|
|
294
|
+
linetypes: [...this.#linetypes.values()],
|
|
295
|
+
linetypeScale: this.linetypeScale,
|
|
148
296
|
};
|
|
149
297
|
}
|
|
150
|
-
|
|
151
|
-
|
|
298
|
+
/**
|
|
299
|
+
* Checks the entity that `make` gives and the `EntityOptions` it was drawn with, whose type
|
|
300
|
+
* parameters differ from method to method, and stores it. `method` is the builder method the
|
|
301
|
+
* caller called, which a diagnostic names the call of.
|
|
302
|
+
*/
|
|
303
|
+
#add(method, options, make) {
|
|
304
|
+
const o = options;
|
|
305
|
+
const id = o.id;
|
|
306
|
+
try {
|
|
307
|
+
const key = id === undefined ? undefined : this.#idKey(id);
|
|
308
|
+
const named = id === undefined ? {} : { id: id };
|
|
309
|
+
this.#entities.push({ ...make(), ...named, ...this.#style(o) });
|
|
310
|
+
if (key !== undefined)
|
|
311
|
+
this.#ids.set(key, id);
|
|
312
|
+
}
|
|
313
|
+
catch (thrown) {
|
|
314
|
+
throw located(thrown, method, typeof id === "string" ? { id } : {});
|
|
315
|
+
}
|
|
316
|
+
this.#calls?.push(holdCall(method));
|
|
152
317
|
return this;
|
|
153
318
|
}
|
|
319
|
+
/** Checks an id the drawing does not have yet, and returns the key it is kept by. */
|
|
320
|
+
#idKey(id) {
|
|
321
|
+
const problem = idProblem(id);
|
|
322
|
+
if (problem)
|
|
323
|
+
refuse("id", problem);
|
|
324
|
+
const key = idKey(id);
|
|
325
|
+
const first = this.#ids.get(key);
|
|
326
|
+
if (first !== undefined)
|
|
327
|
+
refuse("duplicate-id", duplicateIdProblem(id, first));
|
|
328
|
+
return key;
|
|
329
|
+
}
|
|
330
|
+
/** The linetype and lineweight an entity is stored with. */
|
|
331
|
+
#style(options) {
|
|
332
|
+
const style = {};
|
|
333
|
+
if (options.linetype !== undefined) {
|
|
334
|
+
if (this.dxf === "R12" && !isBuiltIn(options.linetype, "ByLayer")) {
|
|
335
|
+
refuse("linetype", R12_ONLY.entityStyle);
|
|
336
|
+
}
|
|
337
|
+
const name = this.#linetypeName(options.linetype, refuse);
|
|
338
|
+
const builtIn = builtInLinetype(name);
|
|
339
|
+
if (builtIn !== "ByLayer")
|
|
340
|
+
style.linetype = builtIn ?? name;
|
|
341
|
+
}
|
|
342
|
+
const weight = options.lineweight;
|
|
343
|
+
if (weight !== undefined) {
|
|
344
|
+
const problem = lineweightProblem(weight, "entity");
|
|
345
|
+
if (problem)
|
|
346
|
+
refuse("lineweight", problem);
|
|
347
|
+
if (this.dxf === "R12" && weight !== "ByLayer")
|
|
348
|
+
refuse("lineweight", R12_ONLY.entityStyle);
|
|
349
|
+
if (weight !== "ByLayer")
|
|
350
|
+
style.lineweight = weight;
|
|
351
|
+
}
|
|
352
|
+
return style;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* The name of the linetype a layer or entity asks for, in the spelling the
|
|
356
|
+
* drawing has it. A definition is defined first if the drawing does not have it.
|
|
357
|
+
*/
|
|
358
|
+
#linetypeName(choice, fail) {
|
|
359
|
+
if (typeof choice === "object" && choice !== null) {
|
|
360
|
+
return this.#define(choice, true);
|
|
361
|
+
}
|
|
362
|
+
if (typeof choice !== "string") {
|
|
363
|
+
return fail("undefined-linetype", "A linetype is a name or a LinetypeDefinition");
|
|
364
|
+
}
|
|
365
|
+
const problem = linetypeNameProblem(choice);
|
|
366
|
+
if (problem)
|
|
367
|
+
fail("undefined-linetype", `Linetype ${JSON.stringify(choice)}: ${problem}`);
|
|
368
|
+
const known = builtInLinetype(choice) ?? this.#linetypes.get(layerKey(choice))?.name;
|
|
369
|
+
if (known === undefined) {
|
|
370
|
+
const absent = `${undefinedLinetypeProblem(choice)}. Define it with linetype() first`;
|
|
371
|
+
fail("undefined-linetype", absent);
|
|
372
|
+
}
|
|
373
|
+
return known;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Checks a definition and stores it, and returns the name the drawing has it by. A
|
|
377
|
+
* definition given to a layer or an entity must match the one the drawing has of its name,
|
|
378
|
+
* where `linetype()` replaces it.
|
|
379
|
+
*/
|
|
380
|
+
#define(definition, inline = false) {
|
|
381
|
+
const label = `Linetype ${JSON.stringify(definition.name)}`;
|
|
382
|
+
const problem = linetypeProblem(definition.name, definition.description ?? "", definition.pattern);
|
|
383
|
+
if (problem)
|
|
384
|
+
refuse("linetype", `${label}: ${problem}`);
|
|
385
|
+
// linetypeProblem has checked the types.
|
|
386
|
+
const name = definition.name;
|
|
387
|
+
const description = (definition.description ?? "");
|
|
388
|
+
const lengths = definition.pattern;
|
|
389
|
+
const key = layerKey(name);
|
|
390
|
+
const auditKey = layerAuditKey(name);
|
|
391
|
+
const twin = layerTwinProblem(name, this.#linetypesAudited.get(auditKey) ?? name, "linetype");
|
|
392
|
+
if (twin)
|
|
393
|
+
refuse("duplicate-linetype", `${label}: ${twin}`);
|
|
394
|
+
const existing = this.#linetypes.get(key);
|
|
395
|
+
if (inline && existing) {
|
|
396
|
+
const same = existing.description === description &&
|
|
397
|
+
existing.pattern.length === lengths.length &&
|
|
398
|
+
existing.pattern.every((length, i) => length === lengths[i]);
|
|
399
|
+
if (!same) {
|
|
400
|
+
refuse("linetype", `${label}: the drawing has a linetype of this name with another pattern or description. Pass its name, or change it with linetype()`);
|
|
401
|
+
}
|
|
402
|
+
return existing.name;
|
|
403
|
+
}
|
|
404
|
+
const stored = existing?.name ?? name;
|
|
405
|
+
this.#linetypes.set(key, {
|
|
406
|
+
name: stored,
|
|
407
|
+
description,
|
|
408
|
+
pattern: [...lengths],
|
|
409
|
+
});
|
|
410
|
+
if (!this.#linetypesAudited.has(auditKey))
|
|
411
|
+
this.#linetypesAudited.set(auditKey, name);
|
|
412
|
+
return stored;
|
|
413
|
+
}
|
|
154
414
|
}
|
|
155
415
|
export function createDrawing(options) {
|
|
156
416
|
return new DrawingBuilder(options);
|
|
157
417
|
}
|
|
418
|
+
/** True when `choice` names the built-in linetype `name`, in any case. */
|
|
419
|
+
function isBuiltIn(choice, name) {
|
|
420
|
+
return typeof choice === "string" && builtInLinetype(choice) === name;
|
|
421
|
+
}
|
|
158
422
|
/** Counterclockwise sweep from start to end, in degrees, within [0, 360). */
|
|
159
423
|
export function sweep(startAngle, endAngle) {
|
|
160
424
|
return turn(endAngle - startAngle);
|
|
@@ -167,17 +431,17 @@ function turn(degrees) {
|
|
|
167
431
|
}
|
|
168
432
|
function num(value, label) {
|
|
169
433
|
if (!Number.isFinite(value))
|
|
170
|
-
|
|
434
|
+
refuse("geometry", `${label} must be a finite number`);
|
|
171
435
|
return value;
|
|
172
436
|
}
|
|
173
437
|
/** Takes `unknown` because a `Positive<N>` parameter is a message string when the literal is wrong. */
|
|
174
438
|
function positive(value, label) {
|
|
175
439
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
176
|
-
|
|
440
|
+
return refuse("geometry", `${label} must be a finite number`);
|
|
177
441
|
}
|
|
178
442
|
const problem = sizeProblem(value);
|
|
179
443
|
if (problem)
|
|
180
|
-
|
|
444
|
+
refuse("geometry", `${label}: ${problem}`);
|
|
181
445
|
return value;
|
|
182
446
|
}
|
|
183
447
|
function lostSide(size, axis, at) {
|
|
@@ -187,7 +451,7 @@ function lostSide(size, axis, at) {
|
|
|
187
451
|
function kept(value, label) {
|
|
188
452
|
const problem = magnitudeProblem(num(value, label));
|
|
189
453
|
if (problem)
|
|
190
|
-
|
|
454
|
+
refuse("geometry", `${label}: ${problem}`);
|
|
191
455
|
return value;
|
|
192
456
|
}
|
|
193
457
|
function pt(point) {
|
package/dist/dxf-r2018.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { extents } from "./extents.js";
|
|
2
|
-
import { dxfText, fmt, readable, writtenAngle } from "./format.js";
|
|
2
|
+
import { dxfOnce, dxfText, fmt, readable, written, writtenAngle } from "./format.js";
|
|
3
3
|
import { layerZeroFirst } from "./layers.js";
|
|
4
|
+
import { entityLinetype, layerLinetype, lineweightCode, patternLength } from "./linetypes.js";
|
|
4
5
|
/** `$INSUNITS` codes from the DXF Reference. */
|
|
5
6
|
const INSUNITS = { in: 1, ft: 2, mm: 4, cm: 5, m: 6 };
|
|
6
7
|
/**
|
|
@@ -44,20 +45,26 @@ export function toDXFR2018(drawing) {
|
|
|
44
45
|
fields: (g) => writeActiveViewport(g, minX, minY, maxX, maxY),
|
|
45
46
|
},
|
|
46
47
|
]);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
const linetypes = [
|
|
49
|
+
{ name: "ByBlock", description: "", pattern: [] },
|
|
50
|
+
{ name: "ByLayer", description: "", pattern: [] },
|
|
51
|
+
{ name: "Continuous", description: "Solid line", pattern: [] },
|
|
52
|
+
...(drawing.linetypes ?? []),
|
|
53
|
+
];
|
|
54
|
+
writeTable(g, handle, "LTYPE", linetypes.map((linetype) => ({
|
|
52
55
|
type: "LTYPE",
|
|
53
56
|
subclass: "AcDbLinetypeTableRecord",
|
|
54
|
-
name,
|
|
57
|
+
name: linetype.name,
|
|
55
58
|
handle: handle(),
|
|
56
59
|
fields: (g) => {
|
|
57
|
-
g(3, description);
|
|
58
|
-
g(72, 65);
|
|
59
|
-
g(73,
|
|
60
|
-
g(40,
|
|
60
|
+
g(3, dxfOnce(linetype.description));
|
|
61
|
+
g(72, 65); // alignment: "A"
|
|
62
|
+
g(73, linetype.pattern.length);
|
|
63
|
+
g(40, patternLength(linetype.pattern.map(written)));
|
|
64
|
+
for (const length of linetype.pattern) {
|
|
65
|
+
g(49, length);
|
|
66
|
+
g(74, 0); // a plain dash, gap or dot
|
|
67
|
+
}
|
|
61
68
|
},
|
|
62
69
|
})));
|
|
63
70
|
writeTable(g, handle, "LAYER",
|
|
@@ -67,10 +74,14 @@ export function toDXFR2018(drawing) {
|
|
|
67
74
|
subclass: "AcDbLayerTableRecord",
|
|
68
75
|
name: layer.name,
|
|
69
76
|
handle: handle(),
|
|
77
|
+
flags: (layer.frozen ? 1 : 0) | (layer.locked ? 4 : 0),
|
|
70
78
|
fields: (g) => {
|
|
71
|
-
|
|
72
|
-
g(
|
|
73
|
-
g(
|
|
79
|
+
// A layer that is off has its color negated.
|
|
80
|
+
g(62, layer.off ? -layer.color : layer.color);
|
|
81
|
+
g(6, layerLinetype(layer));
|
|
82
|
+
if (layer.plot === false)
|
|
83
|
+
g(290, 0);
|
|
84
|
+
g(370, lineweightCode(layer.lineweight ?? "Default"));
|
|
74
85
|
g(390, normalPlotStyle); // AutoCAD rejects a layer without a plot style
|
|
75
86
|
},
|
|
76
87
|
})));
|
|
@@ -177,6 +188,10 @@ export function toDXFR2018(drawing) {
|
|
|
177
188
|
h(70, INSUNITS[drawing.units]);
|
|
178
189
|
h(9, "$MEASUREMENT");
|
|
179
190
|
h(70, drawing.units === "in" || drawing.units === "ft" ? 0 : 1);
|
|
191
|
+
if (drawing.linetypeScale !== undefined && drawing.linetypeScale !== 1) {
|
|
192
|
+
h(9, "$LTSCALE");
|
|
193
|
+
h(40, drawing.linetypeScale);
|
|
194
|
+
}
|
|
180
195
|
h(9, "$CLAYER");
|
|
181
196
|
h(8, "0");
|
|
182
197
|
h(9, "$HANDSEED");
|
|
@@ -224,7 +239,7 @@ function writeTable(g, handle, name, records) {
|
|
|
224
239
|
g(100, "AcDbSymbolTableRecord");
|
|
225
240
|
g(100, r.subclass);
|
|
226
241
|
g(2, r.name);
|
|
227
|
-
g(70, 0);
|
|
242
|
+
g(70, r.flags ?? 0);
|
|
228
243
|
r.fields?.(g);
|
|
229
244
|
}
|
|
230
245
|
g(0, "ENDTAB");
|
|
@@ -298,6 +313,12 @@ function writeEntity(g, e, handle, owner) {
|
|
|
298
313
|
g(330, owner);
|
|
299
314
|
g(100, "AcDbEntity");
|
|
300
315
|
g(8, e.layer);
|
|
316
|
+
const linetype = entityLinetype(e);
|
|
317
|
+
if (linetype !== undefined)
|
|
318
|
+
g(6, linetype);
|
|
319
|
+
if (e.lineweight !== undefined && e.lineweight !== "ByLayer") {
|
|
320
|
+
g(370, lineweightCode(e.lineweight));
|
|
321
|
+
}
|
|
301
322
|
g(100, subclass);
|
|
302
323
|
};
|
|
303
324
|
switch (e.kind) {
|
package/dist/dxf.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Drawing, DxfVersion } from "./drawing.ts";
|
|
2
2
|
export interface DxfOptions {
|
|
3
|
-
/** Defaults to the version the drawing was created for, which is `"
|
|
3
|
+
/** Defaults to the version the drawing was created for, which is `"R2018"` unless set. */
|
|
4
4
|
readonly version?: DxfVersion;
|
|
5
5
|
}
|
|
6
6
|
/**
|
package/dist/format.d.ts
CHANGED
|
@@ -38,3 +38,10 @@ export declare function fmt(value: number): string;
|
|
|
38
38
|
* versions before 2007 carry them. Later versions are UTF-8 and need no escape.
|
|
39
39
|
*/
|
|
40
40
|
export declare function dxfText(value: string, ascii?: boolean): string;
|
|
41
|
+
/**
|
|
42
|
+
* Escapes a string that AutoCAD decodes once when it reads DXF, as it does a linetype's
|
|
43
|
+
* description, so that AutoCAD holds the string as given. `dxfText` explains the escapes; a
|
|
44
|
+
* text is decoded twice, and a description once, so here a backslash that would start an
|
|
45
|
+
* escape is written as `\U+005C`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function dxfOnce(value: string): string;
|
package/dist/format.js
CHANGED
|
@@ -76,3 +76,12 @@ export function dxfText(value, ascii = false) {
|
|
|
76
76
|
}
|
|
77
77
|
return out;
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Escapes a string that AutoCAD decodes once when it reads DXF, as it does a linetype's
|
|
81
|
+
* description, so that AutoCAD holds the string as given. `dxfText` explains the escapes; a
|
|
82
|
+
* text is decoded twice, and a description once, so here a backslash that would start an
|
|
83
|
+
* escape is written as `\U+005C`.
|
|
84
|
+
*/
|
|
85
|
+
export function dxfOnce(value) {
|
|
86
|
+
return value.replace(ESCAPE_START, "\\U+005C").replaceAll("^", "^ ");
|
|
87
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type { CallSite } from "./callsite.ts";
|
|
2
|
+
export { createDrawing, DrawingBuilder, sweep, type Arc, type Circle, type Drawing, type DrawingOptions, type DxfVersion, type Entity, type EntityOptions, type Layer, type LayerOptions, type Line, type Linetype, type LinetypeChoice, type LinetypeDefinition, type Lineweight, type Point, type Polyline, type Text, type TextOptions, type Units, } from "./drawing.ts";
|
|
2
3
|
export { toDXF, type DxfOptions } from "./dxf.ts";
|
|
4
|
+
export { fromJSON, toJSON } from "./json.ts";
|
|
3
5
|
export { DrawingError, lint, type Diagnostic, type LintOptions, type LintRule } from "./lint.ts";
|
|
4
|
-
export
|
|
6
|
+
export { CENTER, DASHDOT, DASHED, DOT, HIDDEN, LINEWEIGHTS, PHANTOM } from "./linetypes.ts";
|
|
7
|
+
export { fromPaper, type PaperScale, type PaperUnit } from "./paper.ts";
|
|
8
|
+
export type { Aci, Description, EntityId, LayerColor, LayerLinetypeName, LayerName, LineweightOf, LinetypeName, NewLinetypeName, PaperLength, Pattern, Positive, Ratio, } from "./rules.ts";
|
|
5
9
|
export { toSVG, type SvgOptions } from "./svg.ts";
|