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/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
|
|
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
|
|
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
|
|
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,
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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 {
|
|
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
|
|
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,
|
|
81
|
-
*
|
|
82
|
-
*
|
|
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
|
}
|