tsdraft 0.3.0 → 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 +39 -16
- package/README.md +23 -5
- package/dist/callsite.d.ts +47 -0
- package/dist/callsite.js +82 -0
- package/dist/drawing.d.ts +37 -11
- package/dist/drawing.js +205 -101
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2 -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/lint.d.ts +17 -5
- package/dist/lint.js +37 -7
- package/dist/paper.d.ts +33 -0
- package/dist/paper.js +58 -0
- package/dist/rules.d.ts +59 -3
- package/dist/rules.js +79 -3
- package/dist/svg.d.ts +2 -1
- package/dist/svg.js +5 -3
- package/package.json +1 -2
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. */
|
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" | "linetype" | "duplicate-linetype" | "undefined-linetype" | "lineweight" | "layer-state" | "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. */
|
|
@@ -11,6 +12,14 @@ export interface Diagnostic {
|
|
|
11
12
|
readonly linetype?: string;
|
|
12
13
|
/** Index into `drawing.entities`, when it is an entity problem. */
|
|
13
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;
|
|
14
23
|
}
|
|
15
24
|
export interface LintOptions {
|
|
16
25
|
/** The DXF version to check against. Defaults to the drawing's own. */
|
|
@@ -27,15 +36,18 @@ export interface LintOptions {
|
|
|
27
36
|
export declare function lint(drawing: Drawing, options?: LintOptions): Diagnostic[];
|
|
28
37
|
/**
|
|
29
38
|
* An error that refuses a drawing because of what `lint` found in it. `toDXF`
|
|
30
|
-
* throws it when a drawing has lint errors for the version being written,
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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.
|
|
33
44
|
*/
|
|
34
45
|
export declare class DrawingError extends Error {
|
|
35
46
|
/** Every problem the drawing was refused for, including those the message leaves out. */
|
|
36
47
|
readonly diagnostics: readonly Diagnostic[];
|
|
37
48
|
/**
|
|
38
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.
|
|
39
51
|
*/
|
|
40
|
-
constructor(diagnostics: readonly Diagnostic[]);
|
|
52
|
+
constructor(diagnostics: readonly Diagnostic[], message?: string);
|
|
41
53
|
}
|
package/dist/lint.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { callOf } from "./callsite.js";
|
|
1
2
|
import { written } from "./format.js";
|
|
2
3
|
import { layerAuditKey, layerKey } from "./layers.js";
|
|
3
4
|
import { BUILT_IN_LINETYPES, builtInLinetype } from "./linetypes.js";
|
|
4
|
-
import { arcIsWrittenAsPoint, arcProblem, layerColorProblem, layerFlagProblem, layerLinetypeProblem, layerNameProblem, layerStateProblem, layerTwinProblem, lineweightProblem, linetypeNameProblem, linetypeProblem, linetypeScaleProblem, R12_ONLY, magnitudeProblem, sizeProblem, textProblem, textWarning, undefinedLinetypeProblem, } from "./rules.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";
|
|
5
6
|
/**
|
|
6
7
|
* Checks a drawing against the rules AutoCAD enforces when it opens DXF.
|
|
7
8
|
*
|
|
@@ -13,6 +14,9 @@ import { arcIsWrittenAsPoint, arcProblem, layerColorProblem, layerFlagProblem, l
|
|
|
13
14
|
export function lint(drawing, options = {}) {
|
|
14
15
|
const version = options.version ?? drawing.dxf;
|
|
15
16
|
const out = [];
|
|
17
|
+
const format = formatProblem(drawing.format);
|
|
18
|
+
if (format)
|
|
19
|
+
out.push({ rule: "format", severity: "error", message: format });
|
|
16
20
|
if (!["in", "ft", "mm", "cm", "m"].includes(drawing.units)) {
|
|
17
21
|
out.push({
|
|
18
22
|
rule: "units",
|
|
@@ -125,8 +129,31 @@ export function lint(drawing, options = {}) {
|
|
|
125
129
|
if (!seen.has("0")) {
|
|
126
130
|
out.push({ rule: "undefined-layer", severity: "error", message: 'Layer "0" is missing' });
|
|
127
131
|
}
|
|
132
|
+
/** The first spelling of each id, by `idKey`. */
|
|
133
|
+
const ids = new Map();
|
|
128
134
|
drawing.entities.forEach((entity, index) => {
|
|
129
|
-
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
|
+
}
|
|
130
157
|
if (!seen.has(layerKey(entity.layer))) {
|
|
131
158
|
// AutoCAD adds a layer for a name it does not know. When AUDIT takes that name for a
|
|
132
159
|
// declared layer's, it then reports the new layer as a duplicate of it.
|
|
@@ -172,21 +199,24 @@ export function lint(drawing, options = {}) {
|
|
|
172
199
|
}
|
|
173
200
|
/**
|
|
174
201
|
* An error that refuses a drawing because of what `lint` found in it. `toDXF`
|
|
175
|
-
* throws it when a drawing has lint errors for the version being written,
|
|
176
|
-
*
|
|
177
|
-
*
|
|
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.
|
|
178
207
|
*/
|
|
179
208
|
export class DrawingError extends Error {
|
|
180
209
|
/** Every problem the drawing was refused for, including those the message leaves out. */
|
|
181
210
|
diagnostics;
|
|
182
211
|
/**
|
|
183
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.
|
|
184
214
|
*/
|
|
185
|
-
constructor(diagnostics) {
|
|
215
|
+
constructor(diagnostics, message) {
|
|
186
216
|
const count = diagnostics.length === 1 ? "1 problem" : `${diagnostics.length} problems`;
|
|
187
217
|
const shown = diagnostics.slice(0, 10).map((d) => `- ${d.message}`);
|
|
188
218
|
const more = diagnostics.length > 10 ? [`- and ${diagnostics.length - 10} more`] : [];
|
|
189
|
-
super([`The drawing has ${count}:`, ...shown, ...more].join("\n"));
|
|
219
|
+
super(message ?? [`The drawing has ${count}:`, ...shown, ...more].join("\n"));
|
|
190
220
|
this.name = "DrawingError";
|
|
191
221
|
this.diagnostics = diagnostics;
|
|
192
222
|
}
|
package/dist/paper.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Units } from "./drawing.ts";
|
|
2
|
+
import { type PaperLength, type Ratio } from "./rules.ts";
|
|
3
|
+
/** A unit paper lengths are given in. */
|
|
4
|
+
export type PaperUnit = "in" | "mm";
|
|
5
|
+
/** The view a paper length is converted for. */
|
|
6
|
+
export interface PaperScale<R extends number = number> {
|
|
7
|
+
/**
|
|
8
|
+
* How many times larger the model is than its plot, as a plain number: 96
|
|
9
|
+
* for 1/8" = 1'-0", 240 for 1" = 20', 50 for 1:50.
|
|
10
|
+
*/
|
|
11
|
+
readonly ratio: Ratio<R>;
|
|
12
|
+
/** The drawing's units, as given to `createDrawing`. */
|
|
13
|
+
readonly units: Units;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Turns a length on paper into drawing units, for text, symbols and dashes
|
|
17
|
+
* that are sized by how large they plot. A 3/32 inch note on a sheet at
|
|
18
|
+
* 1/8" = 1'-0" is 9 inches in the model, which is 0.75 in a drawing in feet:
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* fromPaper(3 / 32, "in", { ratio: 96, units: "ft" }); // 0.75
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* A length or a ratio of 0 or less is a compile error when it is a literal.
|
|
25
|
+
*
|
|
26
|
+
* @param length - The length as it plots, greater than 0.
|
|
27
|
+
* @param unit - The unit of `length`: `"in"` or `"mm"`.
|
|
28
|
+
* @param view - The ratio the length plots at, and the drawing's units.
|
|
29
|
+
* @returns The length in drawing units.
|
|
30
|
+
* @throws When an argument is not one of the above, or when the result would
|
|
31
|
+
* be written as 0 at the 6 decimals tsdraft writes, or is 1e100 or more.
|
|
32
|
+
*/
|
|
33
|
+
export declare function fromPaper<L extends number, R extends number>(length: PaperLength<L>, unit: PaperUnit, view: PaperScale<R>): number;
|
package/dist/paper.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { paperLengthProblem } from "./rules.js";
|
|
2
|
+
/** Inches in one of each imperial unit, and millimetres in one of each metric unit. */
|
|
3
|
+
const SIZES = {
|
|
4
|
+
in: ["imperial", 1],
|
|
5
|
+
ft: ["imperial", 12],
|
|
6
|
+
mm: ["metric", 1],
|
|
7
|
+
cm: ["metric", 10],
|
|
8
|
+
m: ["metric", 1000],
|
|
9
|
+
};
|
|
10
|
+
const MM_PER_INCH = 25.4;
|
|
11
|
+
/**
|
|
12
|
+
* Turns a length on paper into drawing units, for text, symbols and dashes
|
|
13
|
+
* that are sized by how large they plot. A 3/32 inch note on a sheet at
|
|
14
|
+
* 1/8" = 1'-0" is 9 inches in the model, which is 0.75 in a drawing in feet:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* fromPaper(3 / 32, "in", { ratio: 96, units: "ft" }); // 0.75
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* A length or a ratio of 0 or less is a compile error when it is a literal.
|
|
21
|
+
*
|
|
22
|
+
* @param length - The length as it plots, greater than 0.
|
|
23
|
+
* @param unit - The unit of `length`: `"in"` or `"mm"`.
|
|
24
|
+
* @param view - The ratio the length plots at, and the drawing's units.
|
|
25
|
+
* @returns The length in drawing units.
|
|
26
|
+
* @throws When an argument is not one of the above, or when the result would
|
|
27
|
+
* be written as 0 at the 6 decimals tsdraft writes, or is 1e100 or more.
|
|
28
|
+
*/
|
|
29
|
+
export function fromPaper(length, unit, view) {
|
|
30
|
+
// The arguments are read as unknown, because a parameter that failed its type is a message.
|
|
31
|
+
const paper = length;
|
|
32
|
+
const { ratio, units } = view;
|
|
33
|
+
const from = unit === "in" || unit === "mm" ? SIZES[unit] : undefined;
|
|
34
|
+
const to = typeof units === "string" && Object.hasOwn(SIZES, units) ? SIZES[units] : undefined;
|
|
35
|
+
if (!from)
|
|
36
|
+
throw new Error(`The paper unit must be "in" or "mm", not ${JSON.stringify(unit)}`);
|
|
37
|
+
if (!to)
|
|
38
|
+
throw new Error(`Unknown units: ${JSON.stringify(units)}`);
|
|
39
|
+
if (typeof paper !== "number" || !Number.isFinite(paper)) {
|
|
40
|
+
throw new Error("The paper length must be a finite number");
|
|
41
|
+
}
|
|
42
|
+
if (typeof ratio !== "number" || !Number.isFinite(ratio)) {
|
|
43
|
+
throw new Error("The ratio must be a finite number");
|
|
44
|
+
}
|
|
45
|
+
// Multiplying before dividing keeps 3/32 in at 96 in feet at exactly 0.75.
|
|
46
|
+
const model = paper * ratio * from[1];
|
|
47
|
+
const sameSystem = from[0] === to[0];
|
|
48
|
+
const converted = sameSystem
|
|
49
|
+
? model
|
|
50
|
+
: from[0] === "imperial"
|
|
51
|
+
? model * MM_PER_INCH
|
|
52
|
+
: model / MM_PER_INCH;
|
|
53
|
+
const result = converted / to[1];
|
|
54
|
+
const problem = paperLengthProblem(paper, ratio, result);
|
|
55
|
+
if (problem)
|
|
56
|
+
throw new Error(problem);
|
|
57
|
+
return result;
|
|
58
|
+
}
|
package/dist/rules.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ declare const COLOR = "A layer color must be an AutoCAD Color Index integer from
|
|
|
22
22
|
declare const POSITIVE = "This value must be greater than 0";
|
|
23
23
|
declare const WRITTEN_AS_ZERO = "This value is written as 0, because tsdraft writes 6 decimals. Use 0.000001 or more";
|
|
24
24
|
declare const TOO_LARGE = "This value must be under 1e100, because AutoCAD reads only two exponent digits from DXF and would take 1e+100 for 1e+10";
|
|
25
|
+
declare const PAPER_LENGTH = "The paper length must be greater than 0";
|
|
26
|
+
declare const RATIO = "The ratio must be greater than 0";
|
|
25
27
|
declare const TEXT_CONTROL = "Text must be a single line with no control characters";
|
|
26
28
|
declare const TEXT_PRIVATE_USE = "Text cannot contain the private-use characters U+E800 to U+E8FF, because AutoCAD replaces them when it reads DXF";
|
|
27
29
|
declare const R12_LINETYPES = "R12 drawings have no linetypes but Continuous, and no linetype scale. Use DXF version \"R2018\" for linetypes";
|
|
@@ -34,6 +36,9 @@ declare const LAYER_LINETYPE = "A layer's linetype cannot be ByLayer or ByBlock.
|
|
|
34
36
|
declare const BUILT_IN = "Continuous, ByLayer and ByBlock are in every drawing and cannot be defined. AutoCAD keeps its own and drops the definition";
|
|
35
37
|
declare const PATTERN_COUNT = "A linetype pattern has 2 to 12 lengths, or none for a solid line. AutoCAD's AUDIT reports a pattern of one length, and AutoCAD discards a drawing with a pattern of more than 12";
|
|
36
38
|
declare const PATTERN_WRITTEN_AS_ZERO = "A dash or gap of this length is written as 0, because tsdraft writes 6 decimals, and would draw as a dot. Use 0 for a dot, or a length of 0.000001 or more";
|
|
39
|
+
declare const ID_EMPTY = "An id cannot be empty";
|
|
40
|
+
declare const ID_CHARS = "An id can only use the letters A to Z and a to z, digits, ., - and _";
|
|
41
|
+
declare const ID_LENGTH = "An id cannot be longer than 64 characters";
|
|
37
42
|
type ForbiddenChar = "<" | ">" | "/" | "\\" | '"' | ":" | ";" | "?" | "*" | "|" | "=" | "`" | "^" | "," | "\n" | "\r" | "\t";
|
|
38
43
|
type Chars<S extends string, Found = never> = S extends `${infer C}${infer Rest}` ? Chars<Rest, Found | C> : Found;
|
|
39
44
|
type R12Char = Chars<"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$_-">;
|
|
@@ -54,6 +59,17 @@ export type LayerName<N extends string, V extends DxfVersion> = string extends N
|
|
|
54
59
|
* layer names, apart from R12's, since R12 drawings have no linetypes.
|
|
55
60
|
*/
|
|
56
61
|
export type LinetypeName<N extends string> = string extends N ? N : NameCheck<N, typeof LINETYPE_NAMES, N>;
|
|
62
|
+
type IdChar = Exclude<R12Char, "$"> | ".";
|
|
63
|
+
/** Walks the id one character at a time, counting, as `R12Name` walks a layer name. */
|
|
64
|
+
type IdCheck<I extends string, Rest extends string = I, Count extends 0[] = []> = Count["length"] extends 65 ? typeof ID_LENGTH : Rest extends `${infer C}${infer Tail}` ? C extends IdChar ? IdCheck<I, Tail, [...Count, 0]> : typeof ID_CHARS : I;
|
|
65
|
+
/**
|
|
66
|
+
* Resolves to `I` when it is a valid entity id, and to the error message otherwise: 1 to 64
|
|
67
|
+
* characters, each a letter A to Z or a to z, a digit, `.`, `-` or `_`. An id that is not a
|
|
68
|
+
* literal, such as `` `door-${n}` ``, passes the compiler: the first branch takes every type
|
|
69
|
+
* that is not a union of literals. It is checked when the code runs, as is whether the drawing
|
|
70
|
+
* already has the id.
|
|
71
|
+
*/
|
|
72
|
+
export type EntityId<I extends string> = {} extends Record<I, never> ? I : I extends "" ? typeof ID_EMPTY : IdCheck<I>;
|
|
57
73
|
type Enumerate<N extends number, A extends number[] = []> = A["length"] extends N ? A[number] : Enumerate<N, [...A, A["length"]]>;
|
|
58
74
|
/** AutoCAD Color Index: an integer from 1 to 255. */
|
|
59
75
|
export type Aci = Exclude<Enumerate<256>, 0>;
|
|
@@ -74,6 +90,16 @@ type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
|
|
|
74
90
|
* prints a third exponent digit as the writers do.
|
|
75
91
|
*/
|
|
76
92
|
export type Positive<N extends number> = number extends N ? N : `${N}` extends `-${string}` | "0" ? typeof POSITIVE : WrittenAsZero<`${N}`> extends true ? typeof WRITTEN_AS_ZERO : `${N}` extends `${string}e+${Digit}${Digit}${Digit}` ? typeof TOO_LARGE : N;
|
|
93
|
+
/**
|
|
94
|
+
* Resolves to `N` when it is greater than 0, and to `Message` otherwise. For a
|
|
95
|
+
* number that is worked with before anything is written, where `Positive`
|
|
96
|
+
* would refuse a small value for the 6 decimals.
|
|
97
|
+
*/
|
|
98
|
+
type AboveZero<N extends number, Message extends string> = number extends N ? N : `${N}` extends `-${string}` | "0" ? Message : N;
|
|
99
|
+
/** Resolves to `N` when it is a length on paper, and to the error message otherwise. */
|
|
100
|
+
export type PaperLength<N extends number> = AboveZero<N, typeof PAPER_LENGTH>;
|
|
101
|
+
/** Resolves to `N` when it is a scale ratio, and to the error message otherwise. */
|
|
102
|
+
export type Ratio<N extends number> = AboveZero<N, typeof RATIO>;
|
|
77
103
|
type BuiltInName = Lowercase<(typeof BUILT_IN_LINETYPES)[number]>;
|
|
78
104
|
/**
|
|
79
105
|
* Resolves to `N` when it is a valid name for a new linetype, and to the error
|
|
@@ -131,9 +157,9 @@ export declare function linetypeNameProblem(name: string): string | undefined;
|
|
|
131
157
|
* layers to AutoCAD and one duplicate name to AUDIT: `ΟΔΟΣ` and `οδος`,
|
|
132
158
|
* `straße` and `strasse`, a composed é and a decomposed one, and a name with
|
|
133
159
|
* and without a soft hyphen. `layerAuditKey` lists the kinds this covers,
|
|
134
|
-
* which are
|
|
135
|
-
*
|
|
136
|
-
*
|
|
160
|
+
* which are not all that AUDIT reports. Names that are one layer to AutoCAD
|
|
161
|
+
* are not this function's concern. The rule compares two names, so it has no
|
|
162
|
+
* type.
|
|
137
163
|
*
|
|
138
164
|
* @param name - The name of the layer being checked.
|
|
139
165
|
* @param other - The name of another layer in the drawing.
|
|
@@ -198,6 +224,17 @@ export declare function layerColorProblem(color: number): string | undefined;
|
|
|
198
224
|
* @returns The problem, or undefined.
|
|
199
225
|
*/
|
|
200
226
|
export declare function sizeProblem(value: number): string | undefined;
|
|
227
|
+
/**
|
|
228
|
+
* Returns what is wrong with a paper length, the ratio it plots at, or the
|
|
229
|
+
* length in drawing units they give, or undefined when the result can be
|
|
230
|
+
* written. The result is a size, so it is held to the limits of `sizeProblem`.
|
|
231
|
+
*
|
|
232
|
+
* @param length - The length on paper, which must be a finite number.
|
|
233
|
+
* @param ratio - The scale ratio, which must be a finite number.
|
|
234
|
+
* @param result - The length in drawing units.
|
|
235
|
+
* @returns The problem, or undefined.
|
|
236
|
+
*/
|
|
237
|
+
export declare function paperLengthProblem(length: number, ratio: number, result: number): string | undefined;
|
|
201
238
|
/**
|
|
202
239
|
* Returns what is wrong with the size of a number a drawing holds, or
|
|
203
240
|
* undefined when AutoCAD can read it. This goes for every number that is
|
|
@@ -231,6 +268,25 @@ export declare function arcProblem(startAngle: number, endAngle: number): string
|
|
|
231
268
|
* @returns Whether the arc is written with no length.
|
|
232
269
|
*/
|
|
233
270
|
export declare function arcIsWrittenAsPoint(startAngle: number, endAngle: number): boolean;
|
|
271
|
+
/**
|
|
272
|
+
* Returns what is wrong with an entity id, or undefined when it is valid. It takes `unknown` so
|
|
273
|
+
* that `lint` can report a drawing built by hand with an id of the wrong type.
|
|
274
|
+
*/
|
|
275
|
+
export declare function idProblem(id: unknown): string | undefined;
|
|
276
|
+
/** The form two ids are compared in: `Door-1` and `door-1` are one id. */
|
|
277
|
+
export declare function idKey(id: string): string;
|
|
278
|
+
/**
|
|
279
|
+
* Returns what is wrong with a second entity that has an id the drawing already has. The rule
|
|
280
|
+
* depends on the drawing, so it has no type.
|
|
281
|
+
*
|
|
282
|
+
* @param id - The id of the second entity.
|
|
283
|
+
* @param first - The id as the first entity spells it.
|
|
284
|
+
*/
|
|
285
|
+
export declare function duplicateIdProblem(id: string, first: string): string;
|
|
286
|
+
/** Returns what is wrong with a drawing's format version, or undefined when tsdraft reads it. */
|
|
287
|
+
export declare function formatProblem(format: unknown): string | undefined;
|
|
288
|
+
/** A value of any type for a message: a string quoted, a number as it is, anything else by kind. */
|
|
289
|
+
export declare function shown(value: unknown): string;
|
|
234
290
|
/** Returns what is wrong with a single-line text value, or undefined when it is valid. */
|
|
235
291
|
export declare function textProblem(value: string): string | undefined;
|
|
236
292
|
/**
|