tsdraft 0.2.0 → 0.2.2

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/rules.d.ts CHANGED
@@ -1,14 +1,19 @@
1
1
  import type { DxfVersion } from "./drawing.ts";
2
+ import { type REJECTED_COPTIC, type REJECTED_FORMAT, type REJECTED_GEORGIAN, type REJECTED_GLAGOLITIC, type REJECTED_LETTERLIKE, type REJECTED_PRIVATE } from "./layers.ts";
2
3
  declare const NAME_EMPTY = "Layer names cannot be empty";
3
4
  declare const NAME_CHARS = "Layer names cannot contain < > / \\ \" : ; ? * | = ` ^ , or line breaks";
5
+ declare const NAME_REJECTED = "Layer names cannot contain a character AutoCAD rejects or replaces in names, such as a zero-width space or the ohm sign U+2126. The README lists them";
4
6
  declare const NAME_EDGE = "Layer names cannot start or end with a space";
5
7
  declare const NAME_R12_CHARS = "R12 layer names can only use letters, digits, $, - and _. Use DXF version \"R2018\" for other characters";
6
8
  declare const NAME_R12_LENGTH = "R12 layer names cannot be longer than 31 characters. Use DXF version \"R2018\" for longer names";
7
9
  declare const COLOR = "A layer color must be an AutoCAD Color Index integer from 1 to 255";
8
10
  declare const POSITIVE = "This value must be greater than 0";
11
+ declare const WRITTEN_AS_ZERO = "This value is written as 0, because tsdraft writes 6 decimals. Use 0.000001 or more";
12
+ 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";
9
13
  type ForbiddenChar = "<" | ">" | "/" | "\\" | '"' | ":" | ";" | "?" | "*" | "|" | "=" | "`" | "^" | "," | "\n" | "\r" | "\t";
10
14
  type Chars<S extends string, Found = never> = S extends `${infer C}${infer Rest}` ? Chars<Rest, Found | C> : Found;
11
15
  type R12Char = Chars<"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$_-">;
16
+ type RejectedChar = Chars<typeof REJECTED_FORMAT> | Chars<typeof REJECTED_LETTERLIKE> | Chars<typeof REJECTED_GLAGOLITIC> | Chars<typeof REJECTED_COPTIC> | Chars<typeof REJECTED_GEORGIAN> | Chars<typeof REJECTED_PRIVATE>;
12
17
  /** Walks the name one character at a time, counting, so it can check both the characters and the length. */
13
18
  type R12Name<N extends string, Rest extends string = N, Count extends 0[] = []> = Count["length"] extends 32 ? typeof NAME_R12_LENGTH : Rest extends `${infer C}${infer Tail}` ? C extends R12Char ? R12Name<N, Tail, [...Count, 0]> : typeof NAME_R12_CHARS : N;
14
19
  /**
@@ -16,18 +21,99 @@ type R12Name<N extends string, Rest extends string = N, Count extends 0[] = []>
16
21
  * error message otherwise, so the compiler reports the reason. Names that are
17
22
  * not literals pass the compiler and are checked when the code runs.
18
23
  */
19
- export type LayerName<N extends string, V extends DxfVersion> = string extends N ? N : N extends "" ? typeof NAME_EMPTY : N extends `${string}${ForbiddenChar}${string}` ? typeof NAME_CHARS : N extends ` ${string}` | `${string} ` ? typeof NAME_EDGE : V extends "R12" ? R12Name<N> : N;
24
+ export type LayerName<N extends string, V extends DxfVersion> = string extends N ? N : N extends "" ? typeof NAME_EMPTY : N extends `${string}${ForbiddenChar}${string}` ? typeof NAME_CHARS : N extends `${string}${RejectedChar}${string}` ? typeof NAME_REJECTED : N extends ` ${string}` | `${string} ` ? typeof NAME_EDGE : V extends "R12" ? R12Name<N> : N;
20
25
  type Enumerate<N extends number, A extends number[] = []> = A["length"] extends N ? A[number] : Enumerate<N, [...A, A["length"]]>;
21
26
  /** AutoCAD Color Index: an integer from 1 to 255. */
22
27
  export type Aci = Exclude<Enumerate<256>, 0>;
23
28
  /** Resolves to `C` when it is a valid layer color, and to the error message otherwise. */
24
29
  export type LayerColor<C extends number> = number extends C ? C : C extends Aci ? C : typeof COLOR;
25
- /** Resolves to `N` when it is greater than 0, and to the error message otherwise. */
26
- export type Positive<N extends number> = number extends N ? N : `${N}` extends `-${string}` | "0" ? typeof POSITIVE : N;
30
+ /**
31
+ * True for a number the writers round to 0, given as the compiler prints it.
32
+ * A number under 0.000001 prints in exponent form, such as `4e-7`, and those
33
+ * under 0.0000005 round to 0. `5e-7` itself is stored a little below a half,
34
+ * so it rounds down too.
35
+ */
36
+ type WrittenAsZero<S extends string> = S extends `${infer Mantissa}e-${infer Exponent}` ? Exponent extends "7" ? Mantissa extends "5" ? true : Mantissa extends `${5 | 6 | 7 | 8 | 9}${string}` ? false : true : true : false;
37
+ type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
38
+ /**
39
+ * Resolves to `N` when it is a size the writers can keep, and to the error
40
+ * message otherwise: `N` must be greater than 0, must not round to 0 at the
41
+ * 6 decimals tsdraft writes, and must be under 1e100, where the compiler
42
+ * prints a third exponent digit as the writers do.
43
+ */
44
+ 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;
27
45
  /** Returns what is wrong with a layer name for a DXF version, or undefined when it is valid. */
28
46
  export declare function layerNameProblem(name: string, version: DxfVersion): string | undefined;
47
+ /**
48
+ * Returns what is wrong with a drawing that has both names as layers, or
49
+ * undefined when the two can stand together. AutoCAD looks layers up one way
50
+ * and its AUDIT compares their names another, so some pairs of names are two
51
+ * layers to AutoCAD and one duplicate name to AUDIT: `ΟΔΟΣ` and `οδος`,
52
+ * `straße` and `strasse`, a composed é and a decomposed one, and a name with
53
+ * and without a soft hyphen. `layerAuditKey` lists the kinds this covers,
54
+ * which are those measured in AutoCAD and not all that AUDIT reports. Names
55
+ * that are one layer to AutoCAD are not this function's concern. The rule
56
+ * compares two names, so it has no type.
57
+ *
58
+ * @param name - The name of the layer being checked.
59
+ * @param other - The name of another layer in the drawing.
60
+ * @returns The problem, or undefined.
61
+ */
62
+ export declare function layerTwinProblem(name: string, other: string): string | undefined;
29
63
  /** Returns what is wrong with a layer color, or undefined when it is valid. */
30
64
  export declare function layerColorProblem(color: number): string | undefined;
65
+ /**
66
+ * Returns what is wrong with a radius, a text height or a rectangle size, or
67
+ * undefined when it is valid. A size that is written as 0 is refused like 0
68
+ * itself: AutoCAD gives text of height 0 the drawing's default text height,
69
+ * and a circle of radius 0 is a point. A size of 1e100 or more is refused
70
+ * too, as `magnitudeProblem` explains.
71
+ *
72
+ * @param value - The size, which must be a finite number.
73
+ * @returns The problem, or undefined.
74
+ */
75
+ export declare function sizeProblem(value: number): string | undefined;
76
+ /**
77
+ * Returns what is wrong with the size of a number a drawing holds, or
78
+ * undefined when AutoCAD can read it. This goes for every number that is
79
+ * written: coordinates, sizes and text rotation. Only sizes can be checked as
80
+ * literals, through `Positive`, because points and rotations have no literal
81
+ * types.
82
+ *
83
+ * @param value - The number, which must be finite.
84
+ * @returns The problem, or undefined.
85
+ */
86
+ export declare function magnitudeProblem(value: number): string | undefined;
87
+ /**
88
+ * Returns what is wrong with an arc's angles once they are rounded for
89
+ * writing, or undefined when the arc can be written. AutoCAD holds an arc
90
+ * whose start and end angles are equal as an arc of no length, so an arc that
91
+ * is all but a full circle would vanish. The compiler cannot subtract two
92
+ * literals, so this rule has no type.
93
+ *
94
+ * @param startAngle - The start angle in degrees, from 0 up to 360.
95
+ * @param endAngle - The end angle in degrees, from 0 up to 360.
96
+ * @returns The problem, or undefined.
97
+ */
98
+ export declare function arcProblem(startAngle: number, endAngle: number): string | undefined;
99
+ /**
100
+ * True for an arc so short that its angles are the same once rounded, which
101
+ * is written as an arc of no length. That is within rounding of what was
102
+ * drawn, so it is worth a warning and not an error.
103
+ *
104
+ * @param startAngle - The start angle in degrees, from 0 up to 360.
105
+ * @param endAngle - The end angle in degrees, from 0 up to 360.
106
+ * @returns Whether the arc is written with no length.
107
+ */
108
+ export declare function arcIsWrittenAsPoint(startAngle: number, endAngle: number): boolean;
31
109
  /** Returns what is wrong with a single-line text value, or undefined when it is valid. */
32
110
  export declare function textProblem(value: string): string | undefined;
111
+ /**
112
+ * Returns a warning for text that AutoCAD stores as written but draws
113
+ * differently, or undefined when it draws what it stores.
114
+ *
115
+ * @param value - The text value.
116
+ * @returns The warning, or undefined.
117
+ */
118
+ export declare function textWarning(value: string): string | undefined;
33
119
  export {};
package/dist/rules.js CHANGED
@@ -1,18 +1,30 @@
1
+ import { isTooLarge, written, writtenAngle } from "./format.js";
2
+ import { layerAuditKey, layerKey, rejectedNameCharacter, } from "./layers.js";
1
3
  const NAME_EMPTY = "Layer names cannot be empty";
2
4
  const NAME_CHARS = 'Layer names cannot contain < > / \\ " : ; ? * | = ` ^ , or line breaks';
5
+ const NAME_REJECTED = "Layer names cannot contain a character AutoCAD rejects or replaces in names, such as a zero-width space or the ohm sign U+2126. The README lists them";
3
6
  const NAME_EDGE = "Layer names cannot start or end with a space";
4
7
  const NAME_LENGTH = "Layer names cannot be longer than 255 characters";
5
8
  const NAME_R12_CHARS = 'R12 layer names can only use letters, digits, $, - and _. Use DXF version "R2018" for other characters';
6
9
  const NAME_R12_LENGTH = 'R12 layer names cannot be longer than 31 characters. Use DXF version "R2018" for longer names';
7
10
  const COLOR = "A layer color must be an AutoCAD Color Index integer from 1 to 255";
8
11
  const POSITIVE = "This value must be greater than 0";
12
+ const WRITTEN_AS_ZERO = "This value is written as 0, because tsdraft writes 6 decimals. Use 0.000001 or more";
13
+ 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";
14
+ const ARC_LOST = "An arc of nearly a full turn cannot be written: its angles are the same once rounded to 6 decimals, so AutoCAD would hold an arc of no length. Draw a circle instead";
15
+ const TEXT_PRIVATE_USE = "Text cannot contain the private-use characters U+E800 to U+E8FF, because AutoCAD replaces them when it reads DXF";
16
+ const TEXT_PERCENT = 'AutoCAD reads "%%" in text as the start of a control code: it draws "%%d" as a degree sign and "%%%" as one percent sign. The text is stored as written';
9
17
  /** Returns what is wrong with a layer name for a DXF version, or undefined when it is valid. */
10
18
  export function layerNameProblem(name, version) {
11
19
  if (name.length === 0)
12
20
  return NAME_EMPTY;
13
21
  if (/[<>/\\":;?*|=`^,]/.test(name) || hasControl(name) || hasAstral(name))
14
22
  return NAME_CHARS;
15
- if (name !== name.trim())
23
+ const rejected = rejectedNameCharacter(name);
24
+ if (rejected !== undefined)
25
+ return `${NAME_REJECTED}. This name has ${codePoint(rejected)}`;
26
+ // Only the ASCII space: AutoCAD takes a no-break space or any other at either end.
27
+ if (name.startsWith(" ") || name.endsWith(" "))
16
28
  return NAME_EDGE;
17
29
  if (name.length > 255)
18
30
  return NAME_LENGTH;
@@ -24,10 +36,87 @@ export function layerNameProblem(name, version) {
24
36
  }
25
37
  return undefined;
26
38
  }
39
+ /**
40
+ * Returns what is wrong with a drawing that has both names as layers, or
41
+ * undefined when the two can stand together. AutoCAD looks layers up one way
42
+ * and its AUDIT compares their names another, so some pairs of names are two
43
+ * layers to AutoCAD and one duplicate name to AUDIT: `ΟΔΟΣ` and `οδος`,
44
+ * `straße` and `strasse`, a composed é and a decomposed one, and a name with
45
+ * and without a soft hyphen. `layerAuditKey` lists the kinds this covers,
46
+ * which are those measured in AutoCAD and not all that AUDIT reports. Names
47
+ * that are one layer to AutoCAD are not this function's concern. The rule
48
+ * compares two names, so it has no type.
49
+ *
50
+ * @param name - The name of the layer being checked.
51
+ * @param other - The name of another layer in the drawing.
52
+ * @returns The problem, or undefined.
53
+ */
54
+ export function layerTwinProblem(name, other) {
55
+ if (layerKey(name) === layerKey(other) || layerAuditKey(name) !== layerAuditKey(other)) {
56
+ return undefined;
57
+ }
58
+ return `AutoCAD keeps this layer and ${JSON.stringify(other)} as two layers, but its AUDIT reports them as one duplicate name`;
59
+ }
27
60
  /** Returns what is wrong with a layer color, or undefined when it is valid. */
28
61
  export function layerColorProblem(color) {
29
62
  return Number.isInteger(color) && color >= 1 && color <= 255 ? undefined : COLOR;
30
63
  }
64
+ /**
65
+ * Returns what is wrong with a radius, a text height or a rectangle size, or
66
+ * undefined when it is valid. A size that is written as 0 is refused like 0
67
+ * itself: AutoCAD gives text of height 0 the drawing's default text height,
68
+ * and a circle of radius 0 is a point. A size of 1e100 or more is refused
69
+ * too, as `magnitudeProblem` explains.
70
+ *
71
+ * @param value - The size, which must be a finite number.
72
+ * @returns The problem, or undefined.
73
+ */
74
+ export function sizeProblem(value) {
75
+ if (!(value > 0))
76
+ return POSITIVE;
77
+ return written(value) === 0 ? WRITTEN_AS_ZERO : magnitudeProblem(value);
78
+ }
79
+ /**
80
+ * Returns what is wrong with the size of a number a drawing holds, or
81
+ * undefined when AutoCAD can read it. This goes for every number that is
82
+ * written: coordinates, sizes and text rotation. Only sizes can be checked as
83
+ * literals, through `Positive`, because points and rotations have no literal
84
+ * types.
85
+ *
86
+ * @param value - The number, which must be finite.
87
+ * @returns The problem, or undefined.
88
+ */
89
+ export function magnitudeProblem(value) {
90
+ return isTooLarge(value) ? TOO_LARGE : undefined;
91
+ }
92
+ /**
93
+ * Returns what is wrong with an arc's angles once they are rounded for
94
+ * writing, or undefined when the arc can be written. AutoCAD holds an arc
95
+ * whose start and end angles are equal as an arc of no length, so an arc that
96
+ * is all but a full circle would vanish. The compiler cannot subtract two
97
+ * literals, so this rule has no type.
98
+ *
99
+ * @param startAngle - The start angle in degrees, from 0 up to 360.
100
+ * @param endAngle - The end angle in degrees, from 0 up to 360.
101
+ * @returns The problem, or undefined.
102
+ */
103
+ export function arcProblem(startAngle, endAngle) {
104
+ return roundsToOneAngle(startAngle, endAngle) && !isShortArc(startAngle, endAngle)
105
+ ? ARC_LOST
106
+ : undefined;
107
+ }
108
+ /**
109
+ * True for an arc so short that its angles are the same once rounded, which
110
+ * is written as an arc of no length. That is within rounding of what was
111
+ * drawn, so it is worth a warning and not an error.
112
+ *
113
+ * @param startAngle - The start angle in degrees, from 0 up to 360.
114
+ * @param endAngle - The end angle in degrees, from 0 up to 360.
115
+ * @returns Whether the arc is written with no length.
116
+ */
117
+ export function arcIsWrittenAsPoint(startAngle, endAngle) {
118
+ return roundsToOneAngle(startAngle, endAngle) && isShortArc(startAngle, endAngle);
119
+ }
31
120
  /** Returns what is wrong with a single-line text value, or undefined when it is valid. */
32
121
  export function textProblem(value) {
33
122
  if (hasControl(value))
@@ -35,8 +124,30 @@ export function textProblem(value) {
35
124
  if (hasAstral(value)) {
36
125
  return "Text cannot contain emoji or other characters above U+FFFF, because AutoCAD cannot read them from DXF";
37
126
  }
127
+ if (/[\ue800-\ue8ff]/.test(value))
128
+ return TEXT_PRIVATE_USE;
38
129
  return undefined;
39
130
  }
131
+ /**
132
+ * Returns a warning for text that AutoCAD stores as written but draws
133
+ * differently, or undefined when it draws what it stores.
134
+ *
135
+ * @param value - The text value.
136
+ * @returns The warning, or undefined.
137
+ */
138
+ export function textWarning(value) {
139
+ return value.includes("%%") ? TEXT_PERCENT : undefined;
140
+ }
141
+ function roundsToOneAngle(startAngle, endAngle) {
142
+ return writtenAngle(startAngle) === writtenAngle(endAngle);
143
+ }
144
+ /** True when the counterclockwise sweep from start to end is half a turn or less. */
145
+ function isShortArc(startAngle, endAngle) {
146
+ return (((endAngle - startAngle) % 360) + 360) % 360 <= 180;
147
+ }
148
+ function codePoint(character) {
149
+ return `U+${character.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}`;
150
+ }
40
151
  function hasControl(value) {
41
152
  for (let i = 0; i < value.length; i++) {
42
153
  const code = value.charCodeAt(i);
package/dist/svg.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { sweep } from "./drawing.js";
2
2
  import { extents } from "./extents.js";
3
3
  import { fmt } from "./format.js";
4
+ import { layerKey } from "./layers.js";
4
5
  /** Screen colors for the standard ACI colors 1 to 7. Other indexes draw in currentColor. */
5
6
  const ACI = {
6
7
  1: "#ff0000",
@@ -19,10 +20,11 @@ export function toSVG(drawing, options = {}) {
19
20
  const y = -(maxY + margin);
20
21
  const w = maxX - minX + margin * 2;
21
22
  const h = maxY - minY + margin * 2;
22
- const colors = new Map(drawing.layers.map((l) => [l.name, ACI[l.color] ?? "currentColor"]));
23
+ // Keyed as AutoCAD looks layers up, so an entity on `wall` takes the color of layer `WALL`.
24
+ const colors = new Map(drawing.layers.map((l) => [layerKey(l.name), ACI[l.color] ?? "currentColor"]));
23
25
  const stroke = options.strokeWidth ?? 1;
24
26
  const body = drawing.entities
25
- .map((e) => entityToSvg(e, colors.get(e.layer) ?? "currentColor"))
27
+ .map((e) => entityToSvg(e, colors.get(layerKey(e.layer)) ?? "currentColor"))
26
28
  .join("\n ");
27
29
  return [
28
30
  `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${fmt(x)} ${fmt(y)} ${fmt(w)} ${fmt(h)}">`,
@@ -52,6 +54,11 @@ function entityToSvg(e, color) {
52
54
  const large = sweep(e.startAngle, e.endAngle) > 180 ? 1 : 0;
53
55
  const start = `${fmt(cx + e.radius * Math.cos(a0))} ${fmt(cy + e.radius * Math.sin(a0))}`;
54
56
  const end = `${fmt(cx + e.radius * Math.cos(a1))} ${fmt(cy + e.radius * Math.sin(a1))}`;
57
+ // SVG draws nothing for an arc that ends where it starts, which is where rounding can
58
+ // put the ends of an arc that is all but a full turn.
59
+ if (start === end && large === 1) {
60
+ return `<circle cx="${fmt(cx)}" cy="${fmt(cy)}" r="${fmt(e.radius)}" ${s}/>`;
61
+ }
55
62
  // sweep-flag 1 is the positive-angle direction in local (Y-up) coordinates: counterclockwise.
56
63
  return `<path d="M ${start} A ${fmt(e.radius)} ${fmt(e.radius)} 0 ${large} 1 ${end}" ${s}/>`;
57
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsdraft",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "2D CAD drawings as TypeScript: write the drawing in code, export DXF and SVG.",
5
5
  "keywords": [
6
6
  "2d",