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 CHANGED
@@ -1,14 +1,97 @@
1
1
  # Changelog
2
2
 
3
- ## 0.2.2
3
+ ## 0.4.0
4
+
5
+ - `fromPaper(length, "in" | "mm", { ratio, units })` turns a length on paper
6
+ into drawing units, to size text, symbols and dashes by how large they plot:
7
+ `fromPaper(3 / 32, "in", { ratio: 96, units: "ft" })` is 0.75. A wrong unit,
8
+ and a length or ratio of 0 or less, are compile errors as literals and thrown
9
+ errors otherwise. A result that would be written as 0, or is 1e100 or more,
10
+ throws.
11
+ - `examples/site-plan.ts` is now the example sheet: a permit-style site plan for
12
+ an ARCH D sheet at 1" = 10', with property lines, setbacks, a building
13
+ footprint, dimensions, labels, site data, notes and a legend. Every text
14
+ height and annotation offset comes from `fromPaper`. Its default export is the drawing.
15
+ - Breaking: a `Drawing` has `format: 0`, its format version, which is separate
16
+ from `dxf`. The builder sets it. A drawing written by hand needs it, and
17
+ `lint` reports one without it under the new rule `format`. Format 0 carries
18
+ no promise of stability.
19
+ - Breaking: `LintRule` gained `format`, `id` and `duplicate-id`. Code that
20
+ switches over every rule, or uses `Record<LintRule, …>`, needs the three.
21
+ - `toJSON(drawing)` writes a drawing as canonical JSON: the same drawing gives
22
+ the same string, whatever order its keys were set in.
23
+ - `fromJSON(json)` reads a drawing from a JSON string or a parsed value. It
24
+ refuses a format version, a field or an entity kind it does not know, naming
25
+ each, then runs `lint`, and throws a `DrawingError`.
26
+ - Entities take an `id` in their options: 1 to 64 letters, digits, `.`, `-` and
27
+ `_`. A malformed id and a second entity with the same id are builder errors
28
+ and `lint` errors, under the new rules `id` and `duplicate-id`. `toSVG` writes
29
+ the id as `data-id`.
30
+ - A `Diagnostic` about an entity carries its `id`, and `call`, the file, line
31
+ and column of the call that drew it, when the drawing was built with
32
+ `createDrawing({ recordCalls: true })`. Setting
33
+ `globalThis.TSDRAFT_RECORD_CALLS = true` before a drawing's code runs turns
34
+ recording on for every builder that does not set the option, which is how a
35
+ tool that runs drawing files asks for it.
36
+ - The builder's `layer`, `linetype` and entity methods now throw a
37
+ `DrawingError` with the same message as before. Its diagnostic has the rule,
38
+ the `id` and the `call` that broke it.
39
+ - [docs/format.md](docs/format.md) describes all of the above.
4
40
 
5
- Found by opening tsdraft's output in AutoCAD 2025.
41
+ ## 0.3.0
42
+
43
+ - Breaking: `createDrawing()` now targets DXF R2018. Pass `{ dxf: "R12" }` for
44
+ R12, which gets no new features.
45
+ - Linetypes. `d.linetype({ name, description, pattern })` defines one, with
46
+ dashes as positive lengths, gaps as negative ones and dots as 0. `DASHED`,
47
+ `HIDDEN`, `CENTER`, `PHANTOM`, `DASHDOT` and `DOT` are exported with the
48
+ patterns AutoCAD loads from its `acad.lin`. `Continuous`, `ByLayer` and
49
+ `ByBlock` are in every drawing. `createDrawing({ linetypeScale })` sets
50
+ `$LTSCALE`.
51
+ - Lineweights in millimetres, from AutoCAD's 24.
52
+ - Layers take a `linetype`, a `lineweight`, and `off`, `frozen`, `locked` and
53
+ `plot`. Entities take a `linetype` and a `lineweight` of their own, and draw
54
+ ByLayer otherwise, as before.
55
+ - `toSVG` draws dashes and pen weights, and leaves out layers that are off or
56
+ frozen. Its `strokeWidth` is now the width of a 0.25 mm line.
57
+ - `toSVG` marks each entity's element with `data-entity`, its index in
58
+ `drawing.entities`, beside `data-layer`.
59
+ - New rules, each a builder error and a `lint` error, and a compile error where
60
+ the literal shows it:
61
+ - a lineweight outside AutoCAD's list, which makes AutoCAD discard the drawing
62
+ when an entity has it;
63
+ - a linetype the drawing does not define, which makes AutoCAD discard it;
64
+ - a pattern of one length, which `AUDIT` reports, or more than 12, which makes
65
+ AutoCAD discard the drawing;
66
+ - a pattern length that is not 0 but is written as 0, which would turn a dash
67
+ or a gap into a dot, and a pattern whose lengths add up to 1e100 or more,
68
+ because the total is written too;
69
+ - defining `Continuous`, `ByLayer` or `ByBlock`, a layer whose linetype is
70
+ `ByLayer` or `ByBlock`, and freezing layer `0`, which is the current layer;
71
+ - a linetype scale of 0 or less, or one written as 0, which AutoCAD reads as
72
+ 1, and a description over 249 characters, where AutoCAD cuts it;
73
+ - linetype names follow the rules of layer names, which AutoCAD holds them to,
74
+ and a name that `AUDIT` would report as a duplicate of `Continuous`,
75
+ `ByLayer` or `ByBlock`, such as one with a soft hyphen in it;
76
+ - a description with a control character, an emoji or a private-use
77
+ character from U+E800 to U+E8FF, as for text;
78
+ - an R12 drawing that defines a linetype, sets a linetype scale, or gives a
79
+ layer a linetype other than `Continuous`, a lineweight or a state, and an R12
80
+ entity with a linetype or lineweight of its own. Each message says to use
81
+ R2018.
82
+ - A layer or an entity can be given a definition in place of a linetype name.
83
+ It is checked as `linetype()` checks one, and is defined if the drawing does
84
+ not have the name. If the drawing has the name with another pattern or
85
+ description, the builder throws: change a linetype with `linetype()`.
86
+ - `lint` has the rules `linetype`, `duplicate-linetype`, `undefined-linetype`,
87
+ `lineweight` and `layer-state`, and a diagnostic names its `linetype`.
88
+
89
+ ## 0.2.2
6
90
 
7
91
  - Fixed: AutoCAD keeps two layers such as `straße` and `STRASSE` apart, but its
8
92
  `AUDIT` reports them as a duplicate name. `lint` now reports the second layer
9
93
  as a duplicate, and the builder throws at the `layer()` call. It does so for
10
- the kinds of pair measured in AutoCAD, in either letter case and inside longer
11
- names:
94
+ these kinds of pair, in either letter case and inside longer names:
12
95
  - a letter and the letters it stands for, such as `ß` and `ss`, `æ` and `ae`,
13
96
  `œ` and `oe`, `ij` and `ij`, `fi` and `fi`, `dž` and `dž`, and `þ` and `th`;
14
97
  - a composed character and its decomposed form, such as `é`;
@@ -21,13 +104,9 @@ Found by opening tsdraft's output in AutoCAD 2025.
21
104
  - When an entity names its layer by the other spelling of such a pair, the
22
105
  undefined-layer error now says which declared layer AutoCAD does not take it
23
106
  for. AutoCAD adds a layer for the entity, and `AUDIT` reports a duplicate name.
24
- - `pnpm test:cad` checks each kind of pair against AutoCAD, and 600 longer names
25
- beside respellings of themselves.
26
107
 
27
108
  ## 0.2.1
28
109
 
29
- Every fix below was found by opening tsdraft's output in AutoCAD 2025.
30
-
31
110
  - Fixed: text holding `\U+XXXX` or `\M+NXXXX` came out in AutoCAD as the
32
111
  character those escapes stand for, so `\U+00B0` became a degree sign and
33
112
  `\U+000A` put a line feed inside a single-line text. Both DXF versions now
@@ -81,16 +160,11 @@ Every fix below was found by opening tsdraft's output in AutoCAD 2025.
81
160
  and about a line, polyline or arc that loses its length to rounding.
82
161
  - `lint` warns about text that contains `%%`, which AutoCAD stores as written
83
162
  but draws as a control code: `%%d` is drawn as a degree sign.
84
- - `pnpm test:cad` covers each fix above, and checks every character `lint`
85
- accepts in text and in layer names, and each form of number the writers
86
- print, against AutoCAD.
87
163
 
88
164
  ## 0.2.0
89
165
 
90
166
  0.1.1 was never published, so its changes ship here too.
91
167
 
92
- Every fix below comes from opening tsdraft's output in AutoCAD 2025.
93
-
94
168
  - `createDrawing({ dxf: "R2018" })` targets AC1032, the DXF version AutoCAD has
95
169
  used since 2018. It writes handles, owner links, the standard tables, model
96
170
  and paper space blocks, plot styles and `$INSUNITS`. R12 stays the default.
@@ -119,7 +193,6 @@ Every fix below comes from opening tsdraft's output in AutoCAD 2025.
119
193
  - Unknown units, and rectangle sizes of 0 or less, are rejected.
120
194
  - Breaking: a `Drawing` now has a required `dxf` field, and R12 layer names
121
195
  AutoCAD rejects now throw (see above).
122
- - `pnpm test:cad` runs the output through AutoCAD over ssh.
123
196
  - Published from GitHub Actions through npm trusted publishing, with a
124
197
  provenance attestation linking the package to its source commit.
125
198
  - `tsdraft/package.json` can be imported or resolved.
package/README.md CHANGED
@@ -1,40 +1,66 @@
1
- # tsdraft
2
-
3
- Write 2D CAD drawings in TypeScript and export them as DXF, the exchange format
4
- that AutoCAD, BricsCAD and most other CAD programs open, or as SVG for a quick
5
- preview. A drawing is a program, so dimensions can come from variables, drawings
6
- can share code, and changes show up as diffs.
7
-
8
- ## Status
9
-
10
- tsdraft is at an early stage. It draws layers, lines, polylines, rectangles,
11
- circles, arcs and single-line text. It does not yet have dimensions, hatches,
12
- blocks, linetypes, lineweights or paper space layouts. It writes DXF, not DWG.
13
-
14
- The DXF output is tested by opening it in AutoCAD 2025 and running `AUDIT`. It
15
- has not been tested in BricsCAD or any other CAD program yet.
16
-
17
- ## Install
18
-
19
- ```sh
20
- npm install tsdraft
21
- ```
22
-
23
- tsdraft is an ES module and needs Node 22 or later.
24
-
25
- ## Example
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Surfside-Software/tsdraft/main/docs/assets/tsdraft-logo-dark.svg">
3
+ <img src="https://raw.githubusercontent.com/Surfside-Software/tsdraft/main/docs/assets/tsdraft-logo-light.svg" alt="tsdraft" width="292" height="64">
4
+ </picture>
5
+
6
+ Write 2D CAD drawings in TypeScript and export them as DXF for CAD programs, or
7
+ as SVG for a quick preview.
8
+
9
+ [![npm](https://img.shields.io/npm/v/tsdraft?color=1aafc8)](https://www.npmjs.com/package/tsdraft)
10
+ [![CI](https://img.shields.io/github/actions/workflow/status/Surfside-Software/tsdraft/ci.yml?branch=main&label=CI)](https://github.com/Surfside-Software/tsdraft/actions/workflows/ci.yml)
11
+ [![License](https://img.shields.io/npm/l/tsdraft?color=1aafc8)](LICENSE)
12
+
13
+ <a href="https://github.com/Surfside-Software/tsdraft/tree/main/examples">
14
+ <picture>
15
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Surfside-Software/tsdraft/main/docs/assets/outdoor-kitchen-dark.svg">
16
+ <img src="https://raw.githubusercontent.com/Surfside-Software/tsdraft/main/docs/assets/outdoor-kitchen-light.svg" alt="An outdoor kitchen drawn by tsdraft at 3/8 inch to the foot. On the left, an elevation of a stucco wall with an undercounter refrigerator, a built-in gas grill under a vent hood, a sink below a window and two shelves, with dimension strings and notes. On the right, a wall section through the grill from the footing to the attic: the reinforced masonry wall on its footing, the patio slab, the cabinet, the grill, the hood and its duct, and the ceilings with insulation." width="100%">
17
+ </picture>
18
+ </a>
19
+
20
+ The picture is what `toSVG` writes for
21
+ [`examples/outdoor-kitchen.ts`](examples/outdoor-kitchen.ts), restyled for this
22
+ page with a color for each layer. Its dimensions, leaders and stipple
23
+ are small functions in the example that draw lines and text, and every size
24
+ comes from a few parameters at the top of the file.
25
+ [`examples`](examples) has both drawings and how to run them.
26
+
27
+ A smaller drawing, start to finish:
26
28
 
27
29
  ```ts
28
- import { createDrawing, toDXF, toSVG } from "tsdraft";
30
+ import { createDrawing, DASHED, PHANTOM, toDXF, toSVG } from "tsdraft";
31
+
32
+ const lot = { width: 120, depth: 80 }; // ft
33
+ const setbacks = { front: 20, side: 10, rear: 15 };
34
+ const house = { width: 56, depth: 32 };
35
+ const x = (lot.width - house.width) / 2;
36
+ const y = setbacks.front + 5;
29
37
 
30
- const d = createDrawing({ units: "ft" });
38
+ // Linetype patterns are in drawing units. At a scale of 4, DASHED draws dashes of 2 ft and
39
+ // PHANTOM long dashes of 5 ft.
40
+ const d = createDrawing({ units: "ft", linetypeScale: 4 });
31
41
 
32
- d.layer("C-PROP", { color: 3 }, () => {
33
- d.rect([0, 0], 75, 120);
42
+ d.layer("C-PROP", { color: 3, linetype: PHANTOM, lineweight: 0.5 }, () => {
43
+ d.rect([0, 0], lot.width, lot.depth);
44
+ d.text([2, -6], `LOT ${lot.width}' x ${lot.depth}'`, { height: 3 });
34
45
  });
35
- d.layer("A-BLDG", { color: 1 }, () => {
36
- d.rect([17.5, 30], 40, 50);
37
- d.text([19.5, 55], "PROPOSED RESIDENCE", { height: 2 });
46
+ d.layer("C-SETB", { color: 4, linetype: DASHED, lineweight: 0.25 }, () => {
47
+ const { front, side, rear } = setbacks;
48
+ d.rect([side, front], lot.width - side * 2, lot.depth - front - rear);
49
+ });
50
+ d.layer("A-BLDG", { color: 1, lineweight: 0.7 }, () => {
51
+ d.rect([x, y], house.width, house.depth);
52
+ d.text([x + 2, y + house.depth / 2], "PROPOSED RESIDENCE", { height: 3, lineweight: 0.35 });
53
+ });
54
+ d.layer("C-PAVE", { color: 8, lineweight: 0.25 }, () => {
55
+ d.line([x + 40, y], [x + 40, -12]);
56
+ d.line([x + 52, y], [x + 52, -12]);
57
+ });
58
+ d.layer("C-ROAD", { color: 2, lineweight: 0.35 }, () => {
59
+ d.line([-10, -12], [lot.width + 10, -12]);
60
+ d.text([lot.width / 2 - 10, -18], "MAIN STREET", { height: 3 });
61
+ });
62
+ d.layer("V-TREE", { color: 3, lineweight: 0.18 }, () => {
63
+ for (let i = 0; i < 6; i++) d.circle([15 + i * 18, 72], 5);
38
64
  });
39
65
 
40
66
  const drawing = d.build();
@@ -42,35 +68,76 @@ const dxf = toDXF(drawing);
42
68
  const svg = toSVG(drawing);
43
69
  ```
44
70
 
71
+ A drawing is a program, so dimensions can come from variables, drawings can
72
+ share code, and changes show up as diffs.
73
+
45
74
  Coordinates are in drawing units with Y pointing up, as in CAD. Angles are in
46
75
  degrees, counterclockwise from +X. Layer colors are AutoCAD Color Index numbers.
47
76
  Entities go on layer `0` unless they are drawn inside a `layer()` callback.
77
+ Linetypes and lineweights are set on a layer, and an entity draws ByLayer unless
78
+ it is given its own. `DASHED`, `HIDDEN`, `CENTER`, `PHANTOM`, `DASHDOT` and `DOT`
79
+ are AutoCAD's own definitions; `d.linetype({ name, pattern })` defines another,
80
+ with dashes as positive lengths, gaps as negative ones and dots as 0. A
81
+ lineweight is in millimetres.
48
82
 
49
- Numbers are written with 6 decimals, in DXF and in SVG. The checks below judge
50
- a drawing as it will be written, so a size too small to survive the rounding is
51
- an error and a line that rounds down to a point is a warning. Every number must
52
- also be under 1e100 in size. AutoCAD reads only two exponent digits from DXF, so
53
- it would take `1e+100` for `1e+10` and report nothing.
83
+ Text, symbols and dashes are sized by how large they plot. `fromPaper` turns a
84
+ length on paper into drawing units for one scale, given as a plain ratio:
85
+
86
+ ```ts
87
+ import { fromPaper } from "tsdraft";
88
+
89
+ const view = { ratio: 120, units: "ft" } as const; // 1" = 10'
90
+ const note = fromPaper(3 / 32, "in", view); // 0.9375 ft
91
+ d.text([0, 0], "PROPERTY LINE", { height: note });
92
+ ```
93
+
94
+ A paper unit other than `"in"` or `"mm"`, and a length or a ratio of 0 or less,
95
+ are compile errors as literals and thrown errors otherwise. `fromPaper` also
96
+ throws when the result would be written as 0 or is 1e100 or more.
97
+ [`examples/site-plan.ts`](examples/site-plan.ts) is a whole sheet sized this
98
+ way, and the file to copy to start a drawing.
99
+
100
+ ## Status
101
+
102
+ tsdraft is at an early stage. It draws layers, lines, polylines, rectangles,
103
+ circles, arcs and single-line text, with linetypes and lineweights, and layers
104
+ that are off, frozen, locked or not plotted. It does not yet have dimensions,
105
+ hatches, blocks or paper space layouts. It writes DXF, not DWG. The drawing at
106
+ the top builds its dimensions, leaders and stipple from lines and text, so in
107
+ CAD they are plain lines and text, not dimension or hatch objects.
108
+
109
+ The rules tsdraft checks are AutoCAD's. Other CAD programs may hold a drawing to
110
+ other rules.
111
+
112
+ ## Install
113
+
114
+ ```sh
115
+ npm install tsdraft
116
+ ```
117
+
118
+ tsdraft is an ES module and needs Node 22 or later.
54
119
 
55
120
  ## DXF versions
56
121
 
57
- A drawing is written for one DXF version. `createDrawing()` targets R12, the
58
- oldest version still in common use, which nearly every CAD program can open.
59
- `createDrawing({ dxf: "R2018" })` targets the version AutoCAD has used since
60
- 2018, which is the newest one Autodesk has published. `toDXF(drawing)` writes
122
+ A drawing is written for one DXF version. `createDrawing()` targets R2018, the
123
+ version AutoCAD has used since 2018, which is the newest one Autodesk has
124
+ published. `createDrawing({ dxf: "R12" })` targets R12, the oldest version still
125
+ in common use, which nearly every CAD program can open. `toDXF(drawing)` writes
61
126
  the drawing's version, and `toDXF(drawing, { version })` writes another one.
62
127
 
63
- | | `"R12"`, the default | `"R2018"` |
64
- | ------------- | -------------------------------------------------- | ------------------------------------------------------ |
65
- | Format code | AC1009 | AC1032 |
66
- | Drawing units | Not stored. Set them in CAD with `UNITS`. | Stored in `$INSUNITS`. |
67
- | Polylines | `POLYLINE` with `VERTEX` records | `LWPOLYLINE` |
68
- | Text | ASCII, with `\U+XXXX` escapes for other characters | UTF-8 |
69
- | Layer names | Up to 31 letters, digits, `$`, `-` and `_` | Up to 255 characters, with the exceptions listed below |
128
+ | | `"R2018"`, the default | `"R12"` |
129
+ | ------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------- |
130
+ | Format code | AC1032 | AC1009 |
131
+ | Drawing units | Stored in `$INSUNITS`. | Not stored. Set them in CAD with `UNITS`. |
132
+ | Polylines | `LWPOLYLINE` | `POLYLINE` with `VERTEX` records |
133
+ | Text | UTF-8 | ASCII, with `\U+XXXX` escapes for other characters |
134
+ | Layer names | Up to 255 characters, except those in [the rules](docs/rules.md#errors) | Up to 31 letters, digits, `$`, `-` and `_` |
135
+ | Linetypes, lineweights | Yes | No: only `Continuous`, and no lineweights |
136
+ | Layer off, frozen, locked | Yes, and not plotted | No |
70
137
 
71
- Choose R2018 when the drawing will be inserted into another drawing, because CAD
72
- needs the stored units to scale it correctly. AutoCAD shows layer names from an
73
- R12 file in upper case.
138
+ R12 gets no new features. Choose it only for a program that cannot read R2018.
139
+ CAD needs the stored units to scale a drawing inserted into another, and AutoCAD
140
+ shows layer names from an R12 file in upper case.
74
141
 
75
142
  ## Checks
76
143
 
@@ -87,148 +154,28 @@ three points, so the mistake shows up where you made it.
87
154
  AutoCAD would reject.
88
155
 
89
156
  ```ts
90
- const d = createDrawing({ units: "ft" });
157
+ const d = createDrawing({ units: "ft", dxf: "R12" });
91
158
 
92
159
  d.layer("A WALL");
93
160
  // error TS2345: Argument of type '"A WALL"' is not assignable to parameter of
94
161
  // type '"R12 layer names can only use letters, digits, $, - and _. Use DXF
95
162
  // version \"R2018\" for other characters"'.
96
163
 
97
- createDrawing({ dxf: "R2018" }).layer("A WALL"); // fine
164
+ createDrawing().layer("A WALL"); // fine
98
165
 
99
166
  lint(drawing, { version: "R12" }); // check a drawing against another version
100
167
  ```
101
168
 
102
- The rules:
103
-
104
- - Layer names cannot be empty, start or end with a space, or contain a line
105
- break or any of ``< > / \ " : ; ? * | = ` ^ ,``. R12 names have the tighter
106
- limits in the table above.
107
- - Layer names cannot contain the other characters AutoCAD refuses in a name. It
108
- discards the drawing over any of them, and they are listed under
109
- [Layer name characters](#layer-name-characters).
110
- - `wall` and `WALL` are the same layer, as in CAD. The first spelling is kept.
111
- tsdraft compares names the way AutoCAD does, which is not the way JavaScript
112
- does: see [Layer names and case](#layer-names-and-case).
113
- - Layer colors are whole numbers from 1 to 255.
114
- - Radii, text heights and rectangle sizes are greater than 0, and large enough
115
- not to be written as 0. Anything from 0.000001 up is safe. AutoCAD gives text
116
- of height 0 its default height.
117
- - Text is a single line. It cannot contain emoji or other characters above
118
- U+FFFF, because AutoCAD cannot read them from DXF, or the private-use
119
- characters U+E800 to U+E8FF, which AutoCAD replaces.
120
- - Coordinates, sizes and text rotations are under 1e100 in size, because
121
- AutoCAD reads only two exponent digits from DXF.
122
- - Arc angles are stored from 0 up to 360, so -90 becomes 270. An arc cannot come
123
- within 0.000001 degrees of a full turn, because its two angles would be
124
- written the same and AutoCAD holds such an arc as having no length. Draw a
125
- circle instead.
126
-
127
- `lint` also returns warnings. Warnings do not stop an export. They are:
128
-
129
- - A line, a polyline or an arc that has no length, or has none once its numbers
130
- are rounded.
131
- - Text that contains `%%`. AutoCAD stores the text as written but reads `%%` as
132
- the start of a control code when it draws it: `%%d` is drawn as a degree sign
133
- and `%%%` as one percent sign.
134
-
135
- `toDXF` throws a `DrawingError` for errors only. Its `diagnostics` property holds
136
- them, and other code may throw one to refuse a drawing for its own reasons.
137
-
138
- ### Text
139
-
140
- When AutoCAD opens tsdraft's DXF, it holds each text exactly as the drawing has
141
- it. tsdraft escapes what DXF would otherwise read as something else: a caret,
142
- and a backslash that starts `\U+XXXX` or `\M+NXXXX`, which AutoCAD decodes into
143
- a character. A text `\U+00B0` reaches AutoCAD as those seven characters and not
144
- as a degree sign. Write the character itself, `°`, to get the character.
145
-
146
- That lasts until AutoCAD saves the drawing, because AutoCAD does not escape these
147
- sequences in what it writes. After it saves as DWG and opens the file again, a
148
- text `\M+18140` holds the character U+3000. After it saves as DXF and opens
149
- that, `\U+00B0` holds a degree sign as well. tsdraft cannot change this.
150
-
151
- ### Layer names and case
152
-
153
- AutoCAD lower-cases a layer name one character at a time, from a table of its
154
- own that is older and smaller than Unicode's. An entity on `mixedcase`, `café`,
155
- `αβγ` or `СЛОЙ` goes on a layer declared as `MixedCase`, `CAFÉ`, `ΑΒΓ` or `Слой`,
156
- and `lint` accepts it. AutoCAD does not pair these, and `lint` reports the
157
- entity's layer as undefined:
158
-
159
- - `ΟΔΟΣ` and `οδος`, because of the final sigma `ς`. `οδοσ` does match.
160
- - `STRASSE` and `straße`, and `TITLE` and `tıtle` with a dotless `ı`.
161
- - `Ÿ` and `ÿ`, `İ` and `i`, and the title-case digraphs such as `Dž`.
162
- - Georgian, Cherokee and most other letters that gained a case after Unicode
163
- 5.1.
164
-
165
- Where `AUDIT` would take the entity's spelling for the declared layer's, as it
166
- does for `STRASSE` and `straße`, the message says so. AutoCAD adds a layer for
167
- the entity, and `AUDIT` then reports a duplicate name.
168
-
169
- ### Layer names that AUDIT calls duplicates
170
-
171
- AutoCAD's `AUDIT` compares layer names another way than AutoCAD looks them up.
172
- It reports "Duplicate name" for some pairs of layers that AutoCAD keeps as two.
173
- `lint` reports the second layer of such a pair as a duplicate, which is an
174
- error, for these kinds of pair. Each kind holds in either letter case and
175
- inside a longer name, so `Hauptstraße 12` and `HAUPTSTRASSE 12` are a pair.
176
-
177
- - Single letters: `σ` and `ς`, `Ÿ` and `ÿ`, `ß` and `ẞ`, a title-case digraph
178
- such as `Dž` and `dž`, and a few others.
179
- - A letter and the letters it stands for: `ß` and `ss`, `æ` and `ae`, `œ` and
180
- `oe`, `ij` and `ij`, `þ` and `th`, `dž` and `dž`, `lj` and `lj`, `nj` and `nj`,
181
- `dz` and `dz`, the ligatures `ff`, `fi`, `fl`, `ffi`, `ffl` and `st`, and `ȸ`, `ȹ`,
182
- `ʪ`, `ʫ`, `ꜳ`, `ꜵ`, `ꜷ`, `ꜹ`, `ꜽ`, `ꝏ` and `ᴂ`.
183
- - A composed character and its decomposed form, such as `é` and an `e` with a
184
- combining acute accent, with its marks in any order Unicode counts as the
185
- same, such as a dot below before or after a circumflex. Hangul syllables are
186
- an exception, and so are 40 Arabic, Indic, Myanmar, kana, Hebrew and CJK
187
- compatibility characters: `AUDIT` keeps those apart from their decomposed
188
- forms.
189
- - A name with a soft hyphen, U+00AD, inside it or in front of it, and the same
190
- name without.
191
-
192
- This is what was measured in AutoCAD 2025. It is not AutoCAD's whole comparison,
193
- so a drawing that passes `lint` can still get a "Duplicate name" from `AUDIT`.
194
- The rule is kept narrow so that it does not go the other way: in over 20,000
195
- pairs tried, `lint` reported none that `AUDIT` accepts. What it leaves out:
196
-
197
- - A name that ends with a soft hyphen and the same name without it, and a soft
198
- hyphen in front of a combining mark or a Hangul jamo. `AUDIT` reports some of
199
- those pairs and not others. It does not report `coop` and `coop` followed by a
200
- soft hyphen. Two names that both end with a soft hyphen are compared like any
201
- others.
202
- - Compatibility characters that `AUDIT` takes for their plain forms, such as a
203
- modifier letter `ʰ` and `h`, `㎏` and `kg`, and Arabic presentation forms.
204
- - Characters that `AUDIT` cannot tell from an unassigned one, such as `Ԥ`,
205
- U+0524, and `Ꙡ`, U+A660. Two names that differ only in such characters are
206
- duplicates to it.
207
-
208
- ### Layer name characters
209
-
210
- AutoCAD refuses these characters in a layer name, so `lint` does too. Over any
211
- of them but the last row's, AutoCAD discards the whole drawing. All are outside
212
- ASCII, so they only concern R2018 drawings.
213
-
214
- | Characters | What they are |
215
- | -------------------------------------------------------------------------- | ---------------------------------------------------------- |
216
- | U+0080 to U+009F | C1 control characters |
217
- | U+0600 to U+0603, U+06DD, U+070F, U+17B4, U+17B5 | Arabic, Syriac and Khmer formatting characters |
218
- | U+200B to U+200F, U+202A to U+202E, U+2060 to U+2064, U+206A to U+206F | Zero-width spaces and joiners, and text direction controls |
219
- | U+FEFF, U+FFF9 to U+FFFB | The byte order mark and annotation characters |
220
- | U+2071, U+207F, U+2090 to U+2094 | Superscript and subscript letters |
221
- | Letters in U+2102 to U+214E, such as `ℓ`, `ℕ`, the ohm sign and the kelvin | Letterlike symbols. Use the Greek `Ω`, U+03A9, for ohms. |
222
- | U+2180 to U+2188 | Roman numeral characters |
223
- | U+2C00 to U+2C5E, except U+2C2F | Glagolitic |
224
- | U+2C60 to U+2C7D, except U+2C70 | Latin Extended-C |
225
- | U+2C80 to U+2CE4 | Coptic |
226
- | U+2D00 to U+2D25 | Georgian Supplement |
227
- | U+2D30 to U+2D65, U+2D6F | Tifinagh |
228
- | Letters in U+2D80 to U+2DDE | Ethiopic Extended |
229
- | U+E800 to U+E8FF | Private use. AutoCAD replaces these and keeps the drawing. |
230
-
231
- `src/layers.ts` has the exact list.
169
+ ## Reference
170
+
171
+ - [Rules](docs/rules.md) lists what `lint` refuses and what it warns about.
172
+ - [The Drawing format](docs/format.md) covers format 0, entity ids, `toJSON`
173
+ and `fromJSON`, and what a diagnostic carries.
174
+ - [Layer names](docs/layer-names.md) explains how AutoCAD compares layer and
175
+ linetype names, and lists the pairs `AUDIT` calls duplicates and the
176
+ characters it refuses.
177
+ - [Text](docs/text.md) explains what tsdraft escapes in text, and what AutoCAD
178
+ does with it after a save.
232
179
 
233
180
  ## Development
234
181
 
@@ -237,37 +184,19 @@ directly, so tests and examples need no build step.
237
184
 
238
185
  ```sh
239
186
  pnpm install
240
- pnpm verify # format check, lint, type check, tests, package smoke test
241
- pnpm example # writes a site plan to examples/out/ as DXF and SVG
187
+ pnpm verify # format check, lint, type check, tests, package smoke test
188
+ pnpm example # writes both examples to examples/out/ as DXF and SVG
189
+ pnpm readme:drawing # redraws the pictures in docs/assets/ from the examples
242
190
  ```
243
191
 
244
- `pnpm test:cad` opens the output in AutoCAD. It needs
245
- `TSDRAFT_CAD_SSH=user@host`, an ssh login to a WSL shell on a Windows machine
246
- that has AutoCAD. Each test drawing runs in AutoCAD's headless console, which
247
- runs `AUDIT` and saves the drawing back as DXF. The script then compares that
248
- file with what was drawn. Run it after any change to a DXF writer, because
249
- AutoCAD rejects files that DXF parsing libraries accept. Some of its cases write
250
- a file `toDXF` would refuse, to show that AutoCAD does what the rule says.
251
-
252
- `pnpm test:package` packs the package, installs the tarball into a throwaway
253
- project and checks that it type-checks and runs there. `pnpm build` compiles
254
- `src/` to `dist/`, and `npm pack` and `npm publish` run it automatically.
255
-
256
- The official TypeScript 7 compiler checks and builds the package.
257
- [tsrs](https://github.com/oxc-project/tsrs), the Rust port of TypeScript, runs
258
- as `pnpm check:tsrs` and as an informational CI step. It tracks TypeScript 7.1
259
- beta, so it does not gate releases yet. Its WebAssembly build is the planned
260
- route for type-checking drawings in a browser playground.
261
-
262
- ## Releasing
263
-
264
- Bump `version` in `package.json` and add a CHANGELOG entry in a pull request.
265
- After it merges, publish a GitHub release tagged `v<version>` on the merge
266
- commit. The Release workflow checks that the tag matches the version, runs
267
- `pnpm verify` and publishes to npm through
268
- [trusted publishing](https://docs.npmjs.com/trusted-publishers), so no npm token
269
- is stored anywhere. npm adds a provenance attestation that links each version to
270
- the commit it was built from.
192
+ [docs/development.md](docs/development.md) covers the package smoke test, the
193
+ TypeScript compilers and releasing.
194
+
195
+ ## Contributing
196
+
197
+ tsdraft is developed in a private repository and published here through pull
198
+ requests from the `export` branch. Issues are welcome.
199
+ [`CONTRIBUTING.md`](CONTRIBUTING.md) says how changes arrive.
271
200
 
272
201
  ## License
273
202
 
@@ -0,0 +1,47 @@
1
+ /** A place in a source file, as the JavaScript engine names it in a stack. */
2
+ export interface CallSite {
3
+ /** A `file://` URL for an ES module in Node, a path for CommonJS, and a URL in a browser. */
4
+ readonly file: string;
5
+ /** Counted from 1. */
6
+ readonly line: number;
7
+ /** Counted from 1. */
8
+ readonly column: number;
9
+ }
10
+ /**
11
+ * A stack that the engine has captured and formats when `stack` is first read. `whole` is true
12
+ * for a stack that starts inside tsdraft, and false for one cut down to the caller's frame.
13
+ */
14
+ export interface HeldCall {
15
+ readonly stack?: unknown;
16
+ readonly whole: boolean;
17
+ }
18
+ type Method = (...args: never[]) => unknown;
19
+ /**
20
+ * Captures the call of the builder method `method` that is running now, to be read later, or
21
+ * returns undefined where the engine gives no stack. It must be called from the builder's file.
22
+ *
23
+ * With `Error.captureStackTrace` the engine leaves out every frame down to `method`, so only the
24
+ * caller's frame is captured. Without it, the whole stack of a new error is kept.
25
+ */
26
+ export declare function holdCall(method: Method): HeldCall | undefined;
27
+ /** The location a held call names, or undefined when its stack has none. */
28
+ export declare function readCall(held: HeldCall | undefined): CallSite | undefined;
29
+ /**
30
+ * The caller's frame in a whole stack captured by `holdCall`: the first frame in neither of
31
+ * tsdraft's two files on it, which are those of the first frame, `holdCall`'s own, and of the
32
+ * second, the builder's. Frames are told apart by file and not counted, because an engine may
33
+ * leave a frame out. When the caller's code is in one file with tsdraft, as in a bundle, no
34
+ * frame qualifies and there is no location, which is better than a line inside tsdraft.
35
+ */
36
+ export declare function callerInStack(stack: string): CallSite | undefined;
37
+ /** Keeps the calls that drew `entities`, one for each entity in order. */
38
+ export declare function recordCalls(entities: readonly object[], calls: readonly (HeldCall | undefined)[]): void;
39
+ /** The call that drew the entity at `index` of `entities`, when the builder recorded it. */
40
+ export declare function callOf(entities: readonly object[], index: number): CallSite | undefined;
41
+ /**
42
+ * The location of each frame of a stack, innermost first. V8 writes a frame as
43
+ * `at name (file:1:2)` or `at file:1:2`, and Firefox and Safari as `name@file:1:2`. A line that
44
+ * is no frame, such as the error's message, is left out.
45
+ */
46
+ export declare function parseStack(stack: string): CallSite[];
47
+ export {};