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/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 { arcProblem, layerColorProblem, magnitudeProblem, layerNameProblem, layerTwinProblem, sizeProblem, textProblem, } from "./rules.js";
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 ?? "R12";
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
- throw new Error(`Layer ${JSON.stringify(requested)}: ${nameProblem}`);
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
- throw new Error(`Layer ${JSON.stringify(requested)}: ${twinProblem}`);
50
- const color = options.color ?? existing?.color ?? 7;
150
+ fail("duplicate-layer", twinProblem);
151
+ const color = o.color ?? existing?.color ?? 7;
51
152
  const colorProblem = layerColorProblem(color);
52
153
  if (colorProblem)
53
- throw new Error(`Layer ${JSON.stringify(requested)}: ${colorProblem}`);
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
- if (draw) {
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({ kind: "line", layer: this.#current, from: pt(from), to: pt(to) });
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
- if (points.length < 2)
75
- throw new Error("A polyline needs at least 2 points");
76
- return this.#add({ kind: "polyline", layer: this.#current, points: points.map(pt), closed });
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
- const [x, y] = pt(corner);
81
- const w = positive(width, "width");
82
- const h = positive(height, "height");
83
- // A side can still vanish in the file: a corner and the one across from it may round together.
84
- if (written(x + w) === written(x))
85
- throw new Error(lostSide("width", "x", x));
86
- if (written(y + h) === written(y))
87
- throw new Error(lostSide("height", "y", y));
88
- return this.polyline([
89
- [x, y],
90
- [x + w, y],
91
- [x + w, y + h],
92
- [x, y + h],
93
- ], true);
94
- }
95
- circle(center, radius) {
96
- return this.#add({
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
- const start = turn(num(startAngle, "startAngle"));
114
- const end = turn(num(endAngle, "endAngle"));
115
- if (start === end)
116
- throw new Error("An arc's start and end angles must differ");
117
- const problem = arcProblem(start, end);
118
- if (problem)
119
- throw new Error(problem);
120
- return this.#add({
121
- kind: "arc",
122
- layer: this.#current,
123
- center: pt(center),
124
- radius: positive(radius, "radius"),
125
- startAngle: start,
126
- endAngle: end,
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
- const problem = textProblem(value);
131
- if (problem)
132
- throw new Error(problem);
133
- return this.#add({
134
- kind: "text",
135
- layer: this.#current,
136
- at: pt(at),
137
- value,
138
- height: positive(options.height ?? 0.125, "height"),
139
- rotation: kept(options.rotation ?? 0, "rotation"),
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: [...this.#entities],
293
+ entities,
294
+ linetypes: [...this.#linetypes.values()],
295
+ linetypeScale: this.linetypeScale,
148
296
  };
149
297
  }
150
- #add(entity) {
151
- this.#entities.push(entity);
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
- throw new Error(`${label} must be a finite number`);
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
- throw new Error(`${label} must be a finite number`);
440
+ return refuse("geometry", `${label} must be a finite number`);
177
441
  }
178
442
  const problem = sizeProblem(value);
179
443
  if (problem)
180
- throw new Error(`${label}: ${problem}`);
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
- throw new Error(`${label}: ${problem}`);
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
- writeTable(g, handle, "LTYPE", [
48
- ["ByBlock", ""],
49
- ["ByLayer", ""],
50
- ["Continuous", "Solid line"],
51
- ].map(([name = "", description = ""]) => ({
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, 0);
60
- g(40, 0);
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
- g(62, layer.color);
72
- g(6, "Continuous");
73
- g(370, -3); // lineweight: default
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 `"R12"` unless set. */
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 { 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";
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 type { Aci, LayerColor, LayerName, Positive } from "./rules.ts";
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";