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/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. */
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, and
31
- * other code can throw it to refuse a drawing for its own purpose. Its message
32
- * 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.
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 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
+ }
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, and
176
- * other code can throw it to refuse a drawing for its own purpose. Its message
177
- * 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.
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
  }
@@ -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 those measured in AutoCAD and not all that AUDIT reports. Names
135
- * that are one layer to AutoCAD are not this function's concern. The rule
136
- * compares two names, so it has no type.
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
  /**