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/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export { createDrawing, DrawingBuilder, sweep, } from "./drawing.js";
2
2
  export { toDXF } from "./dxf.js";
3
+ export { fromJSON, toJSON } from "./json.js";
3
4
  export { DrawingError, lint } from "./lint.js";
5
+ export { CENTER, DASHDOT, DASHED, DOT, HIDDEN, LINEWEIGHTS, PHANTOM } from "./linetypes.js";
6
+ export { fromPaper } from "./paper.js";
4
7
  export { toSVG } from "./svg.js";
package/dist/json.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { Drawing } from "./drawing.ts";
2
+ /**
3
+ * Writes the drawing as canonical JSON: no white space, the keys of every object in code-unit
4
+ * order, and each number in the shortest form that reads back as the same number, with `-0`
5
+ * for negative zero. Two drawings that are equal give the same string, whatever order their
6
+ * keys were set in. A field that is `undefined` is left out.
7
+ *
8
+ * The drawing is written as it is and is not checked: `fromJSON` checks it when it is read.
9
+ * Throws when the drawing holds a value JSON cannot, such as `NaN`.
10
+ */
11
+ export declare function toJSON(drawing: Drawing): string;
12
+ /**
13
+ * Reads a drawing from JSON, as a string or as the value `JSON.parse` gives, which is returned
14
+ * as the drawing when it passes.
15
+ *
16
+ * It refuses a format version it does not know, a field or an entity kind it does not know, a
17
+ * missing field and a value of the wrong type, and then every error `lint` finds, so a drawing
18
+ * that was read has passed each check the builder makes. It throws a `DrawingError` whose
19
+ * diagnostics name each problem, with the rule `"format"` for the ones `lint` does not look for.
20
+ */
21
+ export declare function fromJSON(json: unknown): Drawing;
package/dist/json.js ADDED
@@ -0,0 +1,244 @@
1
+ import { DrawingError, lint } from "./lint.js";
2
+ import { formatProblem, shown } from "./rules.js";
3
+ /**
4
+ * Writes the drawing as canonical JSON: no white space, the keys of every object in code-unit
5
+ * order, and each number in the shortest form that reads back as the same number, with `-0`
6
+ * for negative zero. Two drawings that are equal give the same string, whatever order their
7
+ * keys were set in. A field that is `undefined` is left out.
8
+ *
9
+ * The drawing is written as it is and is not checked: `fromJSON` checks it when it is read.
10
+ * Throws when the drawing holds a value JSON cannot, such as `NaN`.
11
+ */
12
+ export function toJSON(drawing) {
13
+ let out = "";
14
+ const write = (value) => {
15
+ switch (typeof value) {
16
+ case "number":
17
+ if (!Number.isFinite(value))
18
+ break;
19
+ out += Object.is(value, -0) ? "-0" : value;
20
+ return;
21
+ case "string":
22
+ out += JSON.stringify(value);
23
+ return;
24
+ case "boolean":
25
+ out += value;
26
+ return;
27
+ case "object": {
28
+ if (value === null) {
29
+ out += "null";
30
+ return;
31
+ }
32
+ if (Array.isArray(value)) {
33
+ out += "[";
34
+ for (let i = 0; i < value.length; i++) {
35
+ if (i > 0)
36
+ out += ",";
37
+ write(value[i]);
38
+ }
39
+ out += "]";
40
+ return;
41
+ }
42
+ const fields = value;
43
+ let first = true;
44
+ for (const key of Object.keys(fields).sort()) {
45
+ if (fields[key] === undefined)
46
+ continue;
47
+ out += `${first ? "{" : ","}${JSON.stringify(key)}:`;
48
+ first = false;
49
+ write(fields[key]);
50
+ }
51
+ out += first ? "{}" : "}";
52
+ return;
53
+ }
54
+ }
55
+ const shown = typeof value === "number" ? String(value) : typeof value;
56
+ throw new Error(`A drawing with a value of ${shown} cannot be written as JSON`);
57
+ };
58
+ write(drawing);
59
+ return out;
60
+ }
61
+ /**
62
+ * Reads a drawing from JSON, as a string or as the value `JSON.parse` gives, which is returned
63
+ * as the drawing when it passes.
64
+ *
65
+ * It refuses a format version it does not know, a field or an entity kind it does not know, a
66
+ * missing field and a value of the wrong type, and then every error `lint` finds, so a drawing
67
+ * that was read has passed each check the builder makes. It throws a `DrawingError` whose
68
+ * diagnostics name each problem, with the rule `"format"` for the ones `lint` does not look for.
69
+ */
70
+ export function fromJSON(json) {
71
+ let value = json;
72
+ if (typeof json === "string") {
73
+ try {
74
+ value = JSON.parse(json);
75
+ }
76
+ catch (thrown) {
77
+ refuse([`The JSON cannot be parsed: ${thrown.message}`]);
78
+ }
79
+ }
80
+ if (!isRecord(value))
81
+ refuse([`The drawing ${PLAIN_OBJECT}`]);
82
+ // The fields of another format version cannot be judged, so its version is all that is said.
83
+ const format = formatProblem(Object.hasOwn(value, "format") ? value.format : undefined);
84
+ if (format)
85
+ refuse([format]);
86
+ const problems = [];
87
+ drawing(value, "", problems);
88
+ if (problems.length > 0)
89
+ refuse(problems);
90
+ const errors = lint(value).filter((d) => d.severity === "error");
91
+ if (errors.length > 0)
92
+ throw new DrawingError(errors);
93
+ return value;
94
+ }
95
+ function refuse(problems) {
96
+ throw new DrawingError(problems.map((message) => ({ rule: "format", severity: "error", message })));
97
+ }
98
+ const PLAIN_OBJECT = "must be a plain object, with no getter, setter or hidden field";
99
+ /**
100
+ * True for a plain object whose fields are all its own data: no class or other object behind it
101
+ * to inherit a field from, no symbol or hidden key, which `toJSON` would not write, and no
102
+ * getter or setter, which could give another value each time it is read. No field is read.
103
+ */
104
+ function isRecord(value) {
105
+ if (typeof value !== "object" || value === null || Array.isArray(value))
106
+ return false;
107
+ const proto = Object.getPrototypeOf(value);
108
+ if (proto !== Object.prototype && proto !== null)
109
+ return false;
110
+ for (const key of Reflect.ownKeys(value)) {
111
+ const field = Object.getOwnPropertyDescriptor(value, key);
112
+ if (typeof key !== "string" || !field?.enumerable || !("value" in field))
113
+ return false;
114
+ }
115
+ return true;
116
+ }
117
+ /**
118
+ * What is wrong with `value` as an array of plain data, or undefined: it must be an array with
119
+ * the methods of `Array`, each index its own plain value, and no key but its indexes. Stopping
120
+ * at the first empty slot keeps an array that is all slots, such as `new Array(1e9)`, cheap to
121
+ * refuse. No item is read.
122
+ */
123
+ function arrayProblem(value, at) {
124
+ if (!Array.isArray(value) || !(value instanceof Array))
125
+ return `${at} must be an array`;
126
+ for (let i = 0; i < value.length; i++) {
127
+ const item = Object.getOwnPropertyDescriptor(value, i);
128
+ if (!item)
129
+ return `${at} has no item at index ${i}. An array cannot have empty slots`;
130
+ if (!("value" in item))
131
+ return `${at}[${i}] must be a plain value, not a getter or setter`;
132
+ }
133
+ // Its indexes and `length` are all the keys a plain array has.
134
+ return Reflect.ownKeys(value).length === value.length + 1
135
+ ? undefined
136
+ : `${at} must have no key but its indexes`;
137
+ }
138
+ function isNumber(value) {
139
+ return typeof value === "number" && Number.isFinite(value);
140
+ }
141
+ const string = (value, at, problems) => {
142
+ if (typeof value !== "string")
143
+ problems.push(`${at} must be a string`);
144
+ };
145
+ const number = (value, at, problems) => {
146
+ if (!isNumber(value))
147
+ problems.push(`${at} must be a number`);
148
+ };
149
+ const boolean = (value, at, problems) => {
150
+ if (typeof value !== "boolean")
151
+ problems.push(`${at} must be true or false`);
152
+ };
153
+ const numberOrName = (value, at, problems) => {
154
+ if (typeof value !== "string")
155
+ number(value, at, problems);
156
+ };
157
+ const point = (value, at, problems) => {
158
+ const pair = value;
159
+ if (arrayProblem(value, at) || pair.length !== 2 || !isNumber(pair[0]) || !isNumber(pair[1])) {
160
+ problems.push(`${at} must be a point, [x, y]`);
161
+ }
162
+ };
163
+ const oneOf = (...values) => (value, at, problems) => {
164
+ if (!values.includes(value)) {
165
+ problems.push(`${at} must be ${values.map(shown).join(" or ")}`);
166
+ }
167
+ };
168
+ const list = (item) => (value, at, problems) => {
169
+ const problem = arrayProblem(value, at);
170
+ if (problem)
171
+ problems.push(problem);
172
+ else
173
+ value.forEach((v, i) => item(v, `${at}[${i}]`, problems));
174
+ };
175
+ /**
176
+ * The fields of an object with exactly the fields of `T`. The two tables must name every field
177
+ * of `T`, so a field added to a type is a compile error here until the parser knows it.
178
+ */
179
+ function fields(required, optional) {
180
+ const needed = required;
181
+ const known = { ...required, ...optional };
182
+ return (value, at, problems) => {
183
+ const label = at || "The drawing";
184
+ for (const key of Object.keys(needed)) {
185
+ if (!Object.hasOwn(value, key) || value[key] === undefined) {
186
+ problems.push(`${label}: the field "${key}" is missing`);
187
+ }
188
+ }
189
+ for (const [key, field] of Object.entries(value)) {
190
+ if (!Object.hasOwn(known, key)) {
191
+ problems.push(`${label}: unknown field ${JSON.stringify(key)}`);
192
+ }
193
+ else if (field !== undefined) {
194
+ known[key](field, at ? `${at}.${key}` : key, problems);
195
+ }
196
+ }
197
+ };
198
+ }
199
+ /** A plain object with the fields `check` knows. */
200
+ const record = (check) => (value, at, problems) => {
201
+ if (isRecord(value))
202
+ check(value, at, problems);
203
+ else
204
+ problems.push(`${at} ${PLAIN_OBJECT}`);
205
+ };
206
+ const style = { id: string, linetype: string, lineweight: numberOrName };
207
+ const kind = () => { };
208
+ const ENTITIES = {
209
+ line: fields({ kind, layer: string, from: point, to: point }, style),
210
+ polyline: fields({ kind, layer: string, points: list(point), closed: boolean }, style),
211
+ circle: fields({ kind, layer: string, center: point, radius: number }, style),
212
+ arc: fields({ kind, layer: string, center: point, radius: number, startAngle: number, endAngle: number }, style),
213
+ text: fields({ kind, layer: string, at: point, height: number, value: string, rotation: number }, style),
214
+ };
215
+ const entity = (value, at, problems) => {
216
+ if (!isRecord(value)) {
217
+ problems.push(`${at} ${PLAIN_OBJECT}`);
218
+ return;
219
+ }
220
+ const named = Object.hasOwn(value, "kind") ? value.kind : undefined;
221
+ if (typeof named === "string" && Object.hasOwn(ENTITIES, named)) {
222
+ ENTITIES[named](value, at, problems);
223
+ }
224
+ else {
225
+ problems.push(`${at}: unknown entity kind ${shown(named)}`);
226
+ }
227
+ };
228
+ const drawing = fields({
229
+ format: () => { },
230
+ units: string,
231
+ dxf: oneOf("R12", "R2018"),
232
+ layers: list(record(fields({ name: string, color: number }, {
233
+ linetype: string,
234
+ lineweight: numberOrName,
235
+ off: boolean,
236
+ frozen: boolean,
237
+ locked: boolean,
238
+ plot: boolean,
239
+ }))),
240
+ entities: list(entity),
241
+ }, {
242
+ linetypes: list(record(fields({ name: string, description: string, pattern: list(number) }, {}))),
243
+ linetypeScale: number,
244
+ });
package/dist/layers.d.ts CHANGED
@@ -38,7 +38,7 @@ export declare function layerKey(name: string): string;
38
38
  * error. Two names with the same key here are such a pair, unless they have
39
39
  * the same `layerKey` too.
40
40
  *
41
- * The key follows the kinds of pair measured in AutoCAD 2025:
41
+ * The key follows these kinds of pair:
42
42
  *
43
43
  * - A few single letters, such as σ and ς.
44
44
  * - A letter and the letters it stands for, such as ß and `ss`.
package/dist/layers.js CHANGED
@@ -207,7 +207,7 @@ export function layerKey(name) {
207
207
  * error. Two names with the same key here are such a pair, unless they have
208
208
  * the same `layerKey` too.
209
209
  *
210
- * The key follows the kinds of pair measured in AutoCAD 2025:
210
+ * The key follows these kinds of pair:
211
211
  *
212
212
  * - A few single letters, such as σ and ς.
213
213
  * - A letter and the letters it stands for, such as ß and `ss`.
@@ -232,8 +232,7 @@ export function layerAuditKey(name) {
232
232
  // AUDIT does not report `coop` and `coop` with a soft hyphen after it as duplicates, though it
233
233
  // does report some other pairs that differ by a soft hyphen at the end. A mark for one at the
234
234
  // end keeps a name that ends with a soft hyphen apart from every name that does not. Two names
235
- // that both end with one are compared like any others, as the pairs in `auditPairs` in
236
- // `scripts/cad-check.ts` check.
235
+ // that both end with one are compared like any others.
237
236
  return name.endsWith(SOFT_HYPHEN) ? key + SOFT_HYPHEN : key;
238
237
  }
239
238
  /** Decomposes every composed character that AUDIT takes for its decomposed form. */
@@ -0,0 +1,27 @@
1
+ import type { Entity, Layer, Lineweight, Linetype } from "./drawing.ts";
2
+ /** Every lineweight AutoCAD offers, in millimetres, from thinnest to widest. */
3
+ export declare const LINEWEIGHTS: readonly Lineweight[];
4
+ /** Dashes: 0.5 on, 0.25 off. */
5
+ export declare const DASHED: Linetype;
6
+ /** Short dashes, half the size of `DASHED`. */
7
+ export declare const HIDDEN: Linetype;
8
+ /** Long dash, short dash. */
9
+ export declare const CENTER: Linetype;
10
+ /** Long dash, two short dashes. */
11
+ export declare const PHANTOM: Linetype;
12
+ /** Dash, dot. */
13
+ export declare const DASHDOT: Linetype;
14
+ /** Dots, 0.25 apart. */
15
+ export declare const DOT: Linetype;
16
+ /** The linetypes every drawing has, in the spelling AutoCAD gives them. */
17
+ export declare const BUILT_IN_LINETYPES: readonly ["ByBlock", "ByLayer", "Continuous"];
18
+ /** The spelling AutoCAD gives a built-in linetype, or undefined for any other name. */
19
+ export declare function builtInLinetype(name: string): (typeof BUILT_IN_LINETYPES)[number] | undefined;
20
+ /** The linetype name a writer gives an entity, or undefined when it draws ByLayer. */
21
+ export declare function entityLinetype(e: Entity): string | undefined;
22
+ /** The linetype name a writer gives a layer. */
23
+ export declare function layerLinetype(layer: Layer): string;
24
+ /** A lineweight as DXF group 370 holds it: hundredths of a millimetre, or -1, -2 or -3. */
25
+ export declare function lineweightCode(value: number | "ByLayer" | "ByBlock" | "Default"): number;
26
+ /** The total length of a pattern, as AutoCAD stores it: the sum of the lengths, unsigned. */
27
+ export declare function patternLength(pattern: readonly number[]): number;
@@ -0,0 +1,76 @@
1
+ import { layerKey } from "./layers.js";
2
+ /** Every lineweight AutoCAD offers, in millimetres, from thinnest to widest. */
3
+ export const LINEWEIGHTS = [
4
+ 0, 0.05, 0.09, 0.13, 0.15, 0.18, 0.2, 0.25, 0.3, 0.35, 0.4, 0.5, 0.53, 0.6, 0.7, 0.8, 0.9, 1,
5
+ 1.06, 1.2, 1.4, 1.58, 2, 2.11,
6
+ ];
7
+ // The linetypes plans use most, with the names, descriptions and patterns that AutoCAD 2025
8
+ // loads from its acad.lin, so a drawing looks the same as one drafted by hand. The lengths are
9
+ // in drawing units: scale them with the drawing's linetypeScale.
10
+ /** Dashes: 0.5 on, 0.25 off. */
11
+ export const DASHED = {
12
+ name: "DASHED",
13
+ description: "Dashed __ __ __ __ __ __ __ __ __ __ __ __ __ _",
14
+ pattern: [0.5, -0.25],
15
+ };
16
+ /** Short dashes, half the size of `DASHED`. */
17
+ export const HIDDEN = {
18
+ name: "HIDDEN",
19
+ description: "Hidden __ __ __ __ __ __ __ __ __ __ __ __ __ _",
20
+ pattern: [0.25, -0.125],
21
+ };
22
+ /** Long dash, short dash. */
23
+ export const CENTER = {
24
+ name: "CENTER",
25
+ description: "Center ____ _ ____ _ ____ _ ____ _ ____ _ ____",
26
+ pattern: [1.25, -0.25, 0.25, -0.25],
27
+ };
28
+ /** Long dash, two short dashes. */
29
+ export const PHANTOM = {
30
+ name: "PHANTOM",
31
+ description: "Phantom ______ __ __ ______ __ __ ______",
32
+ pattern: [1.25, -0.25, 0.25, -0.25, 0.25, -0.25],
33
+ };
34
+ /** Dash, dot. */
35
+ export const DASHDOT = {
36
+ name: "DASHDOT",
37
+ description: "Dash dot __ . __ . __ . __ . __ . __ . __ . __",
38
+ pattern: [0.5, -0.25, 0, -0.25],
39
+ };
40
+ /** Dots, 0.25 apart. */
41
+ export const DOT = {
42
+ name: "DOT",
43
+ description: "Dot . . . . . . . . . . . . . . . . . . . . . .",
44
+ pattern: [0, -0.25],
45
+ };
46
+ /** The linetypes every drawing has, in the spelling AutoCAD gives them. */
47
+ export const BUILT_IN_LINETYPES = ["ByBlock", "ByLayer", "Continuous"];
48
+ /** The spelling AutoCAD gives a built-in linetype, or undefined for any other name. */
49
+ export function builtInLinetype(name) {
50
+ const key = layerKey(name);
51
+ return BUILT_IN_LINETYPES.find((n) => layerKey(n) === key);
52
+ }
53
+ /** The linetype name a writer gives an entity, or undefined when it draws ByLayer. */
54
+ export function entityLinetype(e) {
55
+ if (e.linetype === undefined)
56
+ return undefined;
57
+ return builtInLinetype(e.linetype) === "ByLayer" ? undefined : e.linetype;
58
+ }
59
+ /** The linetype name a writer gives a layer. */
60
+ export function layerLinetype(layer) {
61
+ return layer.linetype ?? "Continuous";
62
+ }
63
+ /** A lineweight as DXF group 370 holds it: hundredths of a millimetre, or -1, -2 or -3. */
64
+ export function lineweightCode(value) {
65
+ if (value === "ByLayer")
66
+ return -1;
67
+ if (value === "ByBlock")
68
+ return -2;
69
+ if (value === "Default")
70
+ return -3;
71
+ return Math.round(value * 100);
72
+ }
73
+ /** The total length of a pattern, as AutoCAD stores it: the sum of the lengths, unsigned. */
74
+ export function patternLength(pattern) {
75
+ return pattern.reduce((sum, length) => sum + Math.abs(length), 0);
76
+ }
package/dist/lint.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { type CallSite } from "./callsite.ts";
1
2
  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 type LintRule = "format" | "units" | "layer-name" | "duplicate-layer" | "layer-color" | "undefined-layer" | "linetype" | "duplicate-linetype" | "undefined-linetype" | "lineweight" | "layer-state" | "geometry" | "text" | "id" | "duplicate-id" | "zero-length";
3
4
  export interface Diagnostic {
4
5
  readonly rule: LintRule;
5
6
  /** An error means AutoCAD rejects, repairs or garbles the output. `toDXF` refuses to write it. */
@@ -7,8 +8,18 @@ export interface Diagnostic {
7
8
  readonly message: string;
8
9
  /** The layer the problem is in, when it is a layer problem. */
9
10
  readonly layer?: string;
11
+ /** The linetype the problem is in, when it is a problem with a linetype's definition. */
12
+ readonly linetype?: string;
10
13
  /** Index into `drawing.entities`, when it is an entity problem. */
11
14
  readonly entity?: number;
15
+ /** The `id` of the entity the problem is in, when it has one. */
16
+ readonly id?: string;
17
+ /**
18
+ * The call in the drawing's code that the problem comes from. `lint` gives the call that drew
19
+ * the entity when the drawing's builder recorded calls, and an error the builder throws gives
20
+ * the call that threw. Missing where the JavaScript engine has no stack to read.
21
+ */
22
+ readonly call?: CallSite;
12
23
  }
13
24
  export interface LintOptions {
14
25
  /** The DXF version to check against. Defaults to the drawing's own. */
@@ -25,15 +36,18 @@ export interface LintOptions {
25
36
  export declare function lint(drawing: Drawing, options?: LintOptions): Diagnostic[];
26
37
  /**
27
38
  * An error that refuses a drawing because of what `lint` found in it. `toDXF`
28
- * throws it when a drawing has lint errors for the version being written, and
29
- * other code can throw it to refuse a drawing for its own purpose. Its message
30
- * lists the first 10 problems and names no file format.
39
+ * throws it when a drawing has lint errors for the version being written, `fromJSON`
40
+ * when the JSON is not a drawing it can read, and the builder's `layer`, `linetype`
41
+ * and entity methods at a call that breaks a rule, with that one problem as the
42
+ * message. Other code can throw it to refuse a drawing for its own purpose. Its
43
+ * message lists the first 10 problems and names no file format.
31
44
  */
32
45
  export declare class DrawingError extends Error {
33
46
  /** Every problem the drawing was refused for, including those the message leaves out. */
34
47
  readonly diagnostics: readonly Diagnostic[];
35
48
  /**
36
49
  * @param diagnostics - The problems to refuse the drawing for, as `lint` returns them.
50
+ * @param message - Defaults to a list of the first 10 problems.
37
51
  */
38
- constructor(diagnostics: readonly Diagnostic[]);
52
+ constructor(diagnostics: readonly Diagnostic[], message?: string);
39
53
  }
package/dist/lint.js CHANGED
@@ -1,6 +1,8 @@
1
+ import { callOf } from "./callsite.js";
1
2
  import { written } from "./format.js";
2
3
  import { layerAuditKey, layerKey } from "./layers.js";
3
- import { arcIsWrittenAsPoint, arcProblem, layerColorProblem, layerNameProblem, layerTwinProblem, magnitudeProblem, sizeProblem, textProblem, textWarning, } from "./rules.js";
4
+ import { BUILT_IN_LINETYPES, builtInLinetype } from "./linetypes.js";
5
+ import { arcIsWrittenAsPoint, arcProblem, duplicateIdProblem, formatProblem, idKey, idProblem, layerColorProblem, layerFlagProblem, layerLinetypeProblem, layerNameProblem, layerStateProblem, layerTwinProblem, lineweightProblem, linetypeNameProblem, linetypeProblem, linetypeScaleProblem, R12_ONLY, magnitudeProblem, sizeProblem, textProblem, textWarning, undefinedLinetypeProblem, } from "./rules.js";
4
6
  /**
5
7
  * Checks a drawing against the rules AutoCAD enforces when it opens DXF.
6
8
  *
@@ -12,6 +14,9 @@ import { arcIsWrittenAsPoint, arcProblem, layerColorProblem, layerNameProblem, l
12
14
  export function lint(drawing, options = {}) {
13
15
  const version = options.version ?? drawing.dxf;
14
16
  const out = [];
17
+ const format = formatProblem(drawing.format);
18
+ if (format)
19
+ out.push({ rule: "format", severity: "error", message: format });
15
20
  if (!["in", "ft", "mm", "cm", "m"].includes(drawing.units)) {
16
21
  out.push({
17
22
  rule: "units",
@@ -19,6 +24,52 @@ export function lint(drawing, options = {}) {
19
24
  message: `Unknown units: ${JSON.stringify(drawing.units)}`,
20
25
  });
21
26
  }
27
+ const r12 = version === "R12";
28
+ const scale = drawing.linetypeScale;
29
+ if (scale !== undefined && (r12 ? scale !== 1 : linetypeScaleProblem(scale))) {
30
+ const problem = r12 ? R12_ONLY.linetypes : linetypeScaleProblem(scale);
31
+ out.push({ rule: "linetype", severity: "error", message: `linetypeScale: ${problem}` });
32
+ }
33
+ // AutoCAD compares linetype names as it compares layer names, and AUDIT reports a twin of a
34
+ // built-in linetype as it reports any other.
35
+ const linetypes = new Set();
36
+ const linetypesAudited = new Map(BUILT_IN_LINETYPES.map((name) => [layerAuditKey(name), name]));
37
+ for (const linetype of drawing.linetypes ?? []) {
38
+ const error = (rule, message) => out.push({ rule, severity: "error", message, linetype: linetype.name });
39
+ const label = `Linetype ${JSON.stringify(linetype.name)}`;
40
+ if (r12) {
41
+ error("linetype", `${label}: ${R12_ONLY.linetypes}`);
42
+ continue;
43
+ }
44
+ const problem = linetypeProblem(linetype.name, linetype.description, linetype.pattern);
45
+ if (problem)
46
+ error("linetype", `${label}: ${problem}`);
47
+ if (typeof linetype.name !== "string")
48
+ continue;
49
+ const key = layerKey(linetype.name);
50
+ const auditKey = layerAuditKey(linetype.name);
51
+ const twin = layerTwinProblem(linetype.name, linetypesAudited.get(auditKey) ?? linetype.name, "linetype");
52
+ if (linetypes.has(key)) {
53
+ error("duplicate-linetype", `${label} is defined twice. CAD ignores case in linetype names and keeps the first`);
54
+ }
55
+ else if (twin) {
56
+ error("duplicate-linetype", `${label}: ${twin}`);
57
+ }
58
+ linetypes.add(key);
59
+ if (!linetypesAudited.has(auditKey))
60
+ linetypesAudited.set(auditKey, linetype.name);
61
+ }
62
+ /** What is wrong with a reference to a linetype, which AutoCAD discards a drawing over. */
63
+ const missing = (name) => {
64
+ if (typeof name !== "string")
65
+ return "a linetype is named by a string";
66
+ const problem = linetypeNameProblem(name);
67
+ if (problem)
68
+ return `Linetype ${JSON.stringify(name)}: ${problem}`;
69
+ return builtInLinetype(name) || linetypes.has(layerKey(name))
70
+ ? undefined
71
+ : undefinedLinetypeProblem(name);
72
+ };
22
73
  // AutoCAD looks layers up by one form of their names, and AUDIT compares another.
23
74
  const seen = new Set();
24
75
  const audited = new Map();
@@ -30,6 +81,38 @@ export function lint(drawing, options = {}) {
30
81
  const colorProblem = layerColorProblem(layer.color);
31
82
  if (colorProblem)
32
83
  error("layer-color", `Layer ${JSON.stringify(layer.name)}: ${colorProblem}`);
84
+ const label = `Layer ${JSON.stringify(layer.name)}`;
85
+ if (layer.linetype !== undefined) {
86
+ const name = layer.linetype;
87
+ if (r12) {
88
+ if (typeof name !== "string" || builtInLinetype(name) !== "Continuous") {
89
+ error("linetype", `${label}: ${R12_ONLY.linetypes}`);
90
+ }
91
+ }
92
+ else {
93
+ const absent = missing(name);
94
+ if (absent)
95
+ error("undefined-linetype", `${label}: ${absent}`);
96
+ const problem = typeof name === "string" ? layerLinetypeProblem(name) : undefined;
97
+ if (problem)
98
+ error("linetype", `${label}: ${problem}`);
99
+ }
100
+ }
101
+ if (layer.lineweight !== undefined) {
102
+ const problem = lineweightProblem(layer.lineweight, "layer") ??
103
+ (r12 && layer.lineweight !== "Default" ? R12_ONLY.lineweights : undefined);
104
+ if (problem)
105
+ error("lineweight", `${label}: ${problem}`);
106
+ }
107
+ for (const state of ["off", "frozen", "locked", "plot"]) {
108
+ const flagProblem = layerFlagProblem(state, layer[state]);
109
+ if (flagProblem)
110
+ error("layer-state", `${label}: ${flagProblem}`);
111
+ }
112
+ const states = layer.off || layer.frozen || layer.locked || layer.plot === false;
113
+ const stateProblem = r12 && states ? R12_ONLY.layerStates : layerStateProblem(layer.name, layer.frozen);
114
+ if (stateProblem)
115
+ error("layer-state", `${label}: ${stateProblem}`);
33
116
  const key = layerKey(layer.name);
34
117
  const auditKey = layerAuditKey(layer.name);
35
118
  const twinProblem = layerTwinProblem(layer.name, audited.get(auditKey) ?? layer.name);
@@ -46,8 +129,31 @@ export function lint(drawing, options = {}) {
46
129
  if (!seen.has("0")) {
47
130
  out.push({ rule: "undefined-layer", severity: "error", message: 'Layer "0" is missing' });
48
131
  }
132
+ /** The first spelling of each id, by `idKey`. */
133
+ const ids = new Map();
49
134
  drawing.entities.forEach((entity, index) => {
50
- const add = (rule, severity, problem) => out.push({ rule, severity, message: `${entity.kind} #${index}: ${problem}`, entity: index });
135
+ const id = entity.id;
136
+ const add = (rule, severity, problem) => {
137
+ const call = callOf(drawing.entities, index);
138
+ out.push({
139
+ rule,
140
+ severity,
141
+ message: `${entity.kind} #${index}: ${problem}`,
142
+ entity: index,
143
+ ...(typeof id === "string" ? { id } : {}),
144
+ ...(call ? { call } : {}),
145
+ });
146
+ };
147
+ if (id !== undefined) {
148
+ const problem = idProblem(id);
149
+ const first = problem ? undefined : ids.get(idKey(id));
150
+ if (problem)
151
+ add("id", "error", problem);
152
+ else if (first !== undefined)
153
+ add("duplicate-id", "error", duplicateIdProblem(id, first));
154
+ else
155
+ ids.set(idKey(id), id);
156
+ }
51
157
  if (!seen.has(layerKey(entity.layer))) {
52
158
  // AutoCAD adds a layer for a name it does not know. When AUDIT takes that name for a
53
159
  // declared layer's, it then reports the new layer as a duplicate of it.
@@ -57,6 +163,22 @@ export function lint(drawing, options = {}) {
57
163
  : "";
58
164
  add("undefined-layer", "error", `layer ${JSON.stringify(entity.layer)} is not defined${hint}`);
59
165
  }
166
+ if (entity.linetype !== undefined) {
167
+ const name = entity.linetype;
168
+ const problem = r12
169
+ ? typeof name === "string" && builtInLinetype(name) === "ByLayer"
170
+ ? undefined
171
+ : R12_ONLY.entityStyle
172
+ : missing(name);
173
+ if (problem)
174
+ add(r12 ? "linetype" : "undefined-linetype", "error", problem);
175
+ }
176
+ if (entity.lineweight !== undefined) {
177
+ const problem = lineweightProblem(entity.lineweight, "entity") ??
178
+ (r12 && entity.lineweight !== "ByLayer" ? R12_ONLY.entityStyle : undefined);
179
+ if (problem)
180
+ add("lineweight", "error", problem);
181
+ }
60
182
  const problems = geometryProblems(entity);
61
183
  for (const problem of problems)
62
184
  add("geometry", "error", problem);
@@ -77,21 +199,24 @@ export function lint(drawing, options = {}) {
77
199
  }
78
200
  /**
79
201
  * An error that refuses a drawing because of what `lint` found in it. `toDXF`
80
- * throws it when a drawing has lint errors for the version being written, and
81
- * other code can throw it to refuse a drawing for its own purpose. Its message
82
- * lists the first 10 problems and names no file format.
202
+ * throws it when a drawing has lint errors for the version being written, `fromJSON`
203
+ * when the JSON is not a drawing it can read, and the builder's `layer`, `linetype`
204
+ * and entity methods at a call that breaks a rule, with that one problem as the
205
+ * message. Other code can throw it to refuse a drawing for its own purpose. Its
206
+ * message lists the first 10 problems and names no file format.
83
207
  */
84
208
  export class DrawingError extends Error {
85
209
  /** Every problem the drawing was refused for, including those the message leaves out. */
86
210
  diagnostics;
87
211
  /**
88
212
  * @param diagnostics - The problems to refuse the drawing for, as `lint` returns them.
213
+ * @param message - Defaults to a list of the first 10 problems.
89
214
  */
90
- constructor(diagnostics) {
215
+ constructor(diagnostics, message) {
91
216
  const count = diagnostics.length === 1 ? "1 problem" : `${diagnostics.length} problems`;
92
217
  const shown = diagnostics.slice(0, 10).map((d) => `- ${d.message}`);
93
218
  const more = diagnostics.length > 10 ? [`- and ${diagnostics.length - 10} more`] : [];
94
- super([`The drawing has ${count}:`, ...shown, ...more].join("\n"));
219
+ super(message ?? [`The drawing has ${count}:`, ...shown, ...more].join("\n"));
95
220
  this.name = "DrawingError";
96
221
  this.diagnostics = diagnostics;
97
222
  }