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/CHANGELOG.md +85 -0
- package/README.md +130 -7
- package/dist/drawing.d.ts +4 -0
- package/dist/drawing.js +40 -7
- package/dist/dxf-r12.d.ts +1 -0
- package/dist/dxf-r12.js +6 -4
- package/dist/dxf-r2018.js +17 -6
- package/dist/format.d.ts +32 -3
- package/dist/format.js +60 -6
- package/dist/layers.d.ts +66 -0
- package/dist/layers.js +255 -0
- package/dist/lint.d.ts +10 -1
- package/dist/lint.js +103 -18
- package/dist/rules.d.ts +89 -3
- package/dist/rules.js +112 -1
- package/dist/svg.js +9 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,90 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2
|
|
4
|
+
|
|
5
|
+
Found by opening tsdraft's output in AutoCAD 2025.
|
|
6
|
+
|
|
7
|
+
- Fixed: AutoCAD keeps two layers such as `straße` and `STRASSE` apart, but its
|
|
8
|
+
`AUDIT` reports them as a duplicate name. `lint` now reports the second layer
|
|
9
|
+
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:
|
|
12
|
+
- a letter and the letters it stands for, such as `ß` and `ss`, `æ` and `ae`,
|
|
13
|
+
`œ` and `oe`, `ij` and `ij`, `fi` and `fi`, `dž` and `dž`, and `þ` and `th`;
|
|
14
|
+
- a composed character and its decomposed form, such as `é`;
|
|
15
|
+
- a name with a soft hyphen inside it or in front of it and the same name
|
|
16
|
+
without.
|
|
17
|
+
|
|
18
|
+
This is not all that `AUDIT` reports. The README lists what is covered and
|
|
19
|
+
what is left out.
|
|
20
|
+
|
|
21
|
+
- When an entity names its layer by the other spelling of such a pair, the
|
|
22
|
+
undefined-layer error now says which declared layer AutoCAD does not take it
|
|
23
|
+
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
|
+
|
|
27
|
+
## 0.2.1
|
|
28
|
+
|
|
29
|
+
Every fix below was found by opening tsdraft's output in AutoCAD 2025.
|
|
30
|
+
|
|
31
|
+
- Fixed: text holding `\U+XXXX` or `\M+NXXXX` came out in AutoCAD as the
|
|
32
|
+
character those escapes stand for, so `\U+00B0` became a degree sign and
|
|
33
|
+
`\U+000A` put a line feed inside a single-line text. Both DXF versions now
|
|
34
|
+
write such a backslash so that AutoCAD holds the text as the drawing has it
|
|
35
|
+
when it opens the file. AutoCAD decodes the text after all once it has saved
|
|
36
|
+
the drawing itself and opened it again: `\M+NXXXX` after a save as DWG, and
|
|
37
|
+
`\U+XXXX` too after a save as DXF.
|
|
38
|
+
- Fixed: a size too small for the 6 decimals tsdraft writes was written as 0.
|
|
39
|
+
AutoCAD gave such a text its default height, and held such a circle or arc
|
|
40
|
+
with radius 0. A radius, text height or rectangle size that would be written
|
|
41
|
+
as 0 is now an error, in the compiler for a literal and when the code runs.
|
|
42
|
+
`rect()` also throws when a side is lost because two corners round together.
|
|
43
|
+
- Fixed: AutoCAD reads only two exponent digits from DXF, so it took a number
|
|
44
|
+
written as `1e+100` for `1e+10`, with no `AUDIT` error. A coordinate, size or
|
|
45
|
+
text rotation of 1e100 or more is now an error, in the compiler for a size
|
|
46
|
+
written as a literal and when the code runs. The R2018 writer also caps the
|
|
47
|
+
extents and the view height it works out, which could pass 1e100 for a
|
|
48
|
+
drawing whose own numbers are all smaller.
|
|
49
|
+
- Fixed: an arc within 0.000001 degrees of a full turn was written with equal
|
|
50
|
+
angles, which AutoCAD holds as an arc of no length. It is now an error. An arc
|
|
51
|
+
angle that rounds up to 360 is written as 0.
|
|
52
|
+
- Fixed: `toDXF` wrote layer `0` wherever the drawing listed it, and `AUDIT`
|
|
53
|
+
reported "Layer zero name found at incorrect index" when another layer came
|
|
54
|
+
first. Layer `0` is now written first.
|
|
55
|
+
- Fixed: `lint` matched layer names with JavaScript's `toLowerCase()`, which
|
|
56
|
+
AutoCAD does not use. It accepted an entity on `οδος` for a layer `ΟΔΟΣ`,
|
|
57
|
+
where AutoCAD makes a second layer. `lint` and the builder now compare names
|
|
58
|
+
as AutoCAD does. The README lists the pairs that differ.
|
|
59
|
+
- Fixed: two layers such as `ΟΔΟΣ` and `οδος`, which AutoCAD keeps apart but
|
|
60
|
+
its `AUDIT` reports as a duplicate name, are now an error.
|
|
61
|
+
- Fixed: AutoCAD discarded a drawing whose layer name had a zero-width space,
|
|
62
|
+
another invisible formatting character, a letterlike symbol such as the ohm
|
|
63
|
+
sign U+2126, or a Glagolitic, Coptic, Tifinagh, Georgian Supplement, Latin
|
|
64
|
+
Extended-C or Ethiopic Extended letter. Those names are now errors, in the
|
|
65
|
+
compiler for a literal and when the code runs. The README lists them.
|
|
66
|
+
- Fixed: `lint` threw a `RangeError` on a polyline of about 100,000 points or
|
|
67
|
+
more.
|
|
68
|
+
- Fixed: a layer name that starts or ends with a no-break space, or another
|
|
69
|
+
space outside ASCII, threw when the code ran, though the compiler accepted
|
|
70
|
+
the literal and AutoCAD accepts the name. Only an ASCII space is refused now.
|
|
71
|
+
- Fixed: AutoCAD replaced the private-use characters U+E800 to U+E8FF in text
|
|
72
|
+
and layer names. They are now errors.
|
|
73
|
+
- Fixed: `toSVG` drew nothing for an arc so close to a full turn that its ends
|
|
74
|
+
rounded to one point, and ignored a layer's color when an entity spelled the
|
|
75
|
+
layer's name in another case.
|
|
76
|
+
- Changed: a `DrawingError` message starts "The drawing has 3 problems:" and no
|
|
77
|
+
longer names DXF, because code other than `toDXF` uses it to refuse a drawing.
|
|
78
|
+
- Changed: `lint` messages for a bad radius or height read "radius: This value
|
|
79
|
+
must be greater than 0", the wording the compiler uses for a literal.
|
|
80
|
+
- `lint` warns about a polyline or an arc with no length, as it does for a line,
|
|
81
|
+
and about a line, polyline or arc that loses its length to rounding.
|
|
82
|
+
- `lint` warns about text that contains `%%`, which AutoCAD stores as written
|
|
83
|
+
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
|
+
|
|
3
88
|
## 0.2.0
|
|
4
89
|
|
|
5
90
|
0.1.1 was never published, so its changes ship here too.
|
package/README.md
CHANGED
|
@@ -46,6 +46,12 @@ Coordinates are in drawing units with Y pointing up, as in CAD. Angles are in
|
|
|
46
46
|
degrees, counterclockwise from +X. Layer colors are AutoCAD Color Index numbers.
|
|
47
47
|
Entities go on layer `0` unless they are drawn inside a `layer()` callback.
|
|
48
48
|
|
|
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.
|
|
54
|
+
|
|
49
55
|
## DXF versions
|
|
50
56
|
|
|
51
57
|
A drawing is written for one DXF version. `createDrawing()` targets R12, the
|
|
@@ -98,15 +104,131 @@ The rules:
|
|
|
98
104
|
- Layer names cannot be empty, start or end with a space, or contain a line
|
|
99
105
|
break or any of ``< > / \ " : ; ? * | = ` ^ ,``. R12 names have the tighter
|
|
100
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).
|
|
101
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).
|
|
102
113
|
- Layer colors are whole numbers from 1 to 255.
|
|
103
|
-
- Radii, text heights and rectangle sizes are greater than 0
|
|
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.
|
|
104
117
|
- Text is a single line. It cannot contain emoji or other characters above
|
|
105
|
-
U+FFFF, because AutoCAD cannot read them from DXF
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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.
|
|
110
232
|
|
|
111
233
|
## Development
|
|
112
234
|
|
|
@@ -124,7 +246,8 @@ pnpm example # writes a site plan to examples/out/ as DXF and SVG
|
|
|
124
246
|
that has AutoCAD. Each test drawing runs in AutoCAD's headless console, which
|
|
125
247
|
runs `AUDIT` and saves the drawing back as DXF. The script then compares that
|
|
126
248
|
file with what was drawn. Run it after any change to a DXF writer, because
|
|
127
|
-
AutoCAD rejects files that DXF parsing libraries accept.
|
|
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.
|
|
128
251
|
|
|
129
252
|
`pnpm test:package` packs the package, installs the tarball into a throwaway
|
|
130
253
|
project and checks that it type-checks and runs there. `pnpm build` compiles
|
package/dist/drawing.d.ts
CHANGED
|
@@ -105,6 +105,10 @@ export declare class DrawingBuilder<V extends DxfVersion = "R12"> {
|
|
|
105
105
|
* Arc from `startAngle` counterclockwise to `endAngle`. Angles are stored
|
|
106
106
|
* from 0 up to 360, so -90 becomes 270, because AutoCAD's AUDIT rejects
|
|
107
107
|
* angles outside that range.
|
|
108
|
+
*
|
|
109
|
+
* Angles are written with 6 decimals. An arc within 0.000001 degrees of a
|
|
110
|
+
* full turn would be written with equal angles, which AutoCAD holds as an
|
|
111
|
+
* arc of no length, so it throws: draw a circle instead.
|
|
108
112
|
*/
|
|
109
113
|
arc<R extends number>(center: Point, radius: Positive<R>, startAngle: number, endAngle: number): this;
|
|
110
114
|
text<H extends number = number>(at: Point, value: string, options?: TextOptions<H>): this;
|
package/dist/drawing.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fmt, written } from "./format.js";
|
|
2
|
+
import { layerAuditKey, layerKey } from "./layers.js";
|
|
3
|
+
import { arcProblem, layerColorProblem, magnitudeProblem, layerNameProblem, layerTwinProblem, sizeProblem, textProblem, } from "./rules.js";
|
|
2
4
|
const UNITS = ["in", "ft", "mm", "cm", "m"];
|
|
3
5
|
const DXF_VERSIONS = ["R12", "R2018"];
|
|
4
6
|
/**
|
|
@@ -12,8 +14,10 @@ const DXF_VERSIONS = ["R12", "R2018"];
|
|
|
12
14
|
export class DrawingBuilder {
|
|
13
15
|
units;
|
|
14
16
|
dxf;
|
|
15
|
-
/** Keyed by
|
|
17
|
+
/** Keyed by `layerKey`, because AutoCAD ignores case in layer names. */
|
|
16
18
|
#layers = new Map([["0", { name: "0", color: 7 }]]);
|
|
19
|
+
/** The first layer name for each `layerAuditKey`, to find names AUDIT would call duplicates. */
|
|
20
|
+
#audited = new Map([["0", "0"]]);
|
|
17
21
|
#entities = [];
|
|
18
22
|
#current = "0";
|
|
19
23
|
constructor(options = {}) {
|
|
@@ -37,14 +41,20 @@ export class DrawingBuilder {
|
|
|
37
41
|
const nameProblem = layerNameProblem(requested, this.dxf);
|
|
38
42
|
if (nameProblem)
|
|
39
43
|
throw new Error(`Layer ${JSON.stringify(requested)}: ${nameProblem}`);
|
|
40
|
-
const key = requested
|
|
44
|
+
const key = layerKey(requested);
|
|
41
45
|
const existing = this.#layers.get(key);
|
|
46
|
+
const auditKey = layerAuditKey(requested);
|
|
47
|
+
const twinProblem = layerTwinProblem(requested, this.#audited.get(auditKey) ?? requested);
|
|
48
|
+
if (twinProblem)
|
|
49
|
+
throw new Error(`Layer ${JSON.stringify(requested)}: ${twinProblem}`);
|
|
42
50
|
const color = options.color ?? existing?.color ?? 7;
|
|
43
51
|
const colorProblem = layerColorProblem(color);
|
|
44
52
|
if (colorProblem)
|
|
45
53
|
throw new Error(`Layer ${JSON.stringify(requested)}: ${colorProblem}`);
|
|
46
54
|
const layer = { name: existing?.name ?? requested, color };
|
|
47
55
|
this.#layers.set(key, layer);
|
|
56
|
+
if (!this.#audited.has(auditKey))
|
|
57
|
+
this.#audited.set(auditKey, layer.name);
|
|
48
58
|
if (draw) {
|
|
49
59
|
const previous = this.#current;
|
|
50
60
|
this.#current = layer.name;
|
|
@@ -70,6 +80,11 @@ export class DrawingBuilder {
|
|
|
70
80
|
const [x, y] = pt(corner);
|
|
71
81
|
const w = positive(width, "width");
|
|
72
82
|
const h = positive(height, "height");
|
|
83
|
+
// A side can still vanish in the file: a corner and the one across from it may round together.
|
|
84
|
+
if (written(x + w) === written(x))
|
|
85
|
+
throw new Error(lostSide("width", "x", x));
|
|
86
|
+
if (written(y + h) === written(y))
|
|
87
|
+
throw new Error(lostSide("height", "y", y));
|
|
73
88
|
return this.polyline([
|
|
74
89
|
[x, y],
|
|
75
90
|
[x + w, y],
|
|
@@ -89,12 +104,19 @@ export class DrawingBuilder {
|
|
|
89
104
|
* Arc from `startAngle` counterclockwise to `endAngle`. Angles are stored
|
|
90
105
|
* from 0 up to 360, so -90 becomes 270, because AutoCAD's AUDIT rejects
|
|
91
106
|
* angles outside that range.
|
|
107
|
+
*
|
|
108
|
+
* Angles are written with 6 decimals. An arc within 0.000001 degrees of a
|
|
109
|
+
* full turn would be written with equal angles, which AutoCAD holds as an
|
|
110
|
+
* arc of no length, so it throws: draw a circle instead.
|
|
92
111
|
*/
|
|
93
112
|
arc(center, radius, startAngle, endAngle) {
|
|
94
113
|
const start = turn(num(startAngle, "startAngle"));
|
|
95
114
|
const end = turn(num(endAngle, "endAngle"));
|
|
96
115
|
if (start === end)
|
|
97
116
|
throw new Error("An arc's start and end angles must differ");
|
|
117
|
+
const problem = arcProblem(start, end);
|
|
118
|
+
if (problem)
|
|
119
|
+
throw new Error(problem);
|
|
98
120
|
return this.#add({
|
|
99
121
|
kind: "arc",
|
|
100
122
|
layer: this.#current,
|
|
@@ -114,7 +136,7 @@ export class DrawingBuilder {
|
|
|
114
136
|
at: pt(at),
|
|
115
137
|
value,
|
|
116
138
|
height: positive(options.height ?? 0.125, "height"),
|
|
117
|
-
rotation:
|
|
139
|
+
rotation: kept(options.rotation ?? 0, "rotation"),
|
|
118
140
|
});
|
|
119
141
|
}
|
|
120
142
|
build() {
|
|
@@ -153,10 +175,21 @@ function positive(value, label) {
|
|
|
153
175
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
154
176
|
throw new Error(`${label} must be a finite number`);
|
|
155
177
|
}
|
|
156
|
-
|
|
157
|
-
|
|
178
|
+
const problem = sizeProblem(value);
|
|
179
|
+
if (problem)
|
|
180
|
+
throw new Error(`${label}: ${problem}`);
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
function lostSide(size, axis, at) {
|
|
184
|
+
return `${size}: This value is too small at ${axis} = ${at}. Both sides of the rectangle are written as ${axis} = ${fmt(at)}, because tsdraft writes 6 decimals`;
|
|
185
|
+
}
|
|
186
|
+
/** A number the drawing keeps and the writers print, which AutoCAD must be able to read. */
|
|
187
|
+
function kept(value, label) {
|
|
188
|
+
const problem = magnitudeProblem(num(value, label));
|
|
189
|
+
if (problem)
|
|
190
|
+
throw new Error(`${label}: ${problem}`);
|
|
158
191
|
return value;
|
|
159
192
|
}
|
|
160
193
|
function pt(point) {
|
|
161
|
-
return [
|
|
194
|
+
return [kept(point[0], "x"), kept(point[1], "y")];
|
|
162
195
|
}
|
package/dist/dxf-r12.d.ts
CHANGED
|
@@ -6,5 +6,6 @@ import type { Drawing } from "./drawing.ts";
|
|
|
6
6
|
* R12 has no header variable for drawing units, so coordinates are written as
|
|
7
7
|
* plain numbers in the drawing's units and `drawing.units` is not written.
|
|
8
8
|
* R12 predates Unicode DXF, so text outside ASCII is written as `\U+XXXX`.
|
|
9
|
+
* Layer `0` is written first, wherever it is in `drawing.layers`.
|
|
9
10
|
*/
|
|
10
11
|
export declare function toDXFR12(drawing: Drawing): string;
|
package/dist/dxf-r12.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { dxfText, fmt } from "./format.js";
|
|
1
|
+
import { dxfText, fmt, writtenAngle } from "./format.js";
|
|
2
|
+
import { layerZeroFirst } from "./layers.js";
|
|
2
3
|
/**
|
|
3
4
|
* Writes an ASCII DXF (AC1009, R12). R12 is the most widely readable DXF:
|
|
4
5
|
* AutoCAD, BricsCAD, LibreCAD, QCAD and most converters open it.
|
|
@@ -6,6 +7,7 @@ import { dxfText, fmt } from "./format.js";
|
|
|
6
7
|
* R12 has no header variable for drawing units, so coordinates are written as
|
|
7
8
|
* plain numbers in the drawing's units and `drawing.units` is not written.
|
|
8
9
|
* R12 predates Unicode DXF, so text outside ASCII is written as `\U+XXXX`.
|
|
10
|
+
* Layer `0` is written first, wherever it is in `drawing.layers`.
|
|
9
11
|
*/
|
|
10
12
|
export function toDXFR12(drawing) {
|
|
11
13
|
const out = [];
|
|
@@ -33,7 +35,7 @@ export function toDXFR12(drawing) {
|
|
|
33
35
|
g(0, "TABLE");
|
|
34
36
|
g(2, "LAYER");
|
|
35
37
|
g(70, String(drawing.layers.length));
|
|
36
|
-
for (const layer of drawing.layers) {
|
|
38
|
+
for (const layer of layerZeroFirst(drawing.layers)) {
|
|
37
39
|
g(0, "LAYER");
|
|
38
40
|
g(2, layer.name);
|
|
39
41
|
g(70, "0");
|
|
@@ -95,8 +97,8 @@ function writeEntity(e, g) {
|
|
|
95
97
|
g(20, e.center[1]);
|
|
96
98
|
g(30, 0);
|
|
97
99
|
g(40, e.radius);
|
|
98
|
-
g(50, e.startAngle);
|
|
99
|
-
g(51, e.endAngle);
|
|
100
|
+
g(50, writtenAngle(e.startAngle));
|
|
101
|
+
g(51, writtenAngle(e.endAngle));
|
|
100
102
|
return;
|
|
101
103
|
case "text":
|
|
102
104
|
g(0, "TEXT");
|
package/dist/dxf-r2018.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { extents } from "./extents.js";
|
|
2
|
-
import { dxfText, fmt } from "./format.js";
|
|
2
|
+
import { dxfText, fmt, readable, writtenAngle } from "./format.js";
|
|
3
|
+
import { layerZeroFirst } from "./layers.js";
|
|
3
4
|
/** `$INSUNITS` codes from the DXF Reference. */
|
|
4
5
|
const INSUNITS = { in: 1, ft: 2, mm: 4, cm: 5, m: 6 };
|
|
5
6
|
/**
|
|
@@ -21,7 +22,14 @@ export function toDXFR2018(drawing) {
|
|
|
21
22
|
const normalPlotStyle = handle();
|
|
22
23
|
const modelSpace = handle();
|
|
23
24
|
const paperSpace = handle();
|
|
24
|
-
|
|
25
|
+
// The extents are worked out here, so they are brought into the range AutoCAD reads.
|
|
26
|
+
const [left, bottom, right, top] = extents(drawing.entities);
|
|
27
|
+
const [minX, minY, maxX, maxY] = [
|
|
28
|
+
readable(left),
|
|
29
|
+
readable(bottom),
|
|
30
|
+
readable(right),
|
|
31
|
+
readable(top),
|
|
32
|
+
];
|
|
25
33
|
g(0, "SECTION");
|
|
26
34
|
g(2, "CLASSES");
|
|
27
35
|
g(0, "ENDSEC");
|
|
@@ -52,7 +60,9 @@ export function toDXFR2018(drawing) {
|
|
|
52
60
|
g(40, 0);
|
|
53
61
|
},
|
|
54
62
|
})));
|
|
55
|
-
writeTable(g, handle, "LAYER",
|
|
63
|
+
writeTable(g, handle, "LAYER",
|
|
64
|
+
// AUDIT reports "Layer zero name found at incorrect index" unless layer 0 comes first.
|
|
65
|
+
layerZeroFirst(drawing.layers).map((layer) => ({
|
|
56
66
|
type: "LAYER",
|
|
57
67
|
subclass: "AcDbLayerTableRecord",
|
|
58
68
|
name: layer.name,
|
|
@@ -222,7 +232,8 @@ function writeTable(g, handle, name, records) {
|
|
|
222
232
|
/** The *Active viewport, zoomed to the drawing's extents so the file opens on the drawing. */
|
|
223
233
|
function writeActiveViewport(g, minX, minY, maxX, maxY) {
|
|
224
234
|
const aspect = 1.6;
|
|
225
|
-
|
|
235
|
+
// A drawing can span more than AutoCAD can read as one number, so the height is capped.
|
|
236
|
+
const height = readable(Math.max(maxY - minY, (maxX - minX) / aspect, 1) * 1.1);
|
|
226
237
|
g(10, 0); // lower-left corner of the viewport on screen
|
|
227
238
|
g(20, 0);
|
|
228
239
|
g(11, 1); // upper-right corner
|
|
@@ -315,8 +326,8 @@ function writeEntity(g, e, handle, owner) {
|
|
|
315
326
|
point(g, 10, ...e.center);
|
|
316
327
|
g(40, e.radius);
|
|
317
328
|
g(100, "AcDbArc");
|
|
318
|
-
g(50, e.startAngle);
|
|
319
|
-
g(51, e.endAngle);
|
|
329
|
+
g(50, writtenAngle(e.startAngle));
|
|
330
|
+
g(51, writtenAngle(e.endAngle));
|
|
320
331
|
return;
|
|
321
332
|
case "text":
|
|
322
333
|
begin("TEXT", "AcDbText");
|
package/dist/format.d.ts
CHANGED
|
@@ -1,9 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A number as the writers print it: rounded to 6 decimals. Two values that are
|
|
3
|
+
* equal here are the same number in the file, whatever they were in the drawing.
|
|
4
|
+
*/
|
|
5
|
+
export declare function written(value: number): number;
|
|
6
|
+
/**
|
|
7
|
+
* An arc angle in degrees as the DXF writers print it: rounded to 6 decimals,
|
|
8
|
+
* with 0 in place of an angle that rounds up to 360.
|
|
9
|
+
*/
|
|
10
|
+
export declare function writtenAngle(degrees: number): number;
|
|
11
|
+
/**
|
|
12
|
+
* True for a number AutoCAD would misread from DXF, because it is 1e100 or
|
|
13
|
+
* more in size. `lint` refuses these wherever a drawing holds one.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isTooLarge(value: number): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Brings a number a writer works out for itself, such as an extent or the
|
|
18
|
+
* height of the view, into the range AutoCAD reads. `lint` cannot vouch for
|
|
19
|
+
* those: two coordinates under 1e100 can be more than 1e100 apart.
|
|
20
|
+
*/
|
|
21
|
+
export declare function readable(value: number): number;
|
|
1
22
|
/** Formats a coordinate with at most 6 decimals, no float noise, and no "-0". */
|
|
2
23
|
export declare function fmt(value: number): string;
|
|
3
24
|
/**
|
|
4
|
-
* Escapes a string value for DXF
|
|
5
|
-
*
|
|
6
|
-
* caret
|
|
25
|
+
* Escapes a string value for DXF, so that AutoCAD holds the string as given.
|
|
26
|
+
*
|
|
27
|
+
* A caret starts a control-character escape in DXF, and AutoCAD discards the
|
|
28
|
+
* drawing when it meets a bare one, so a literal caret is written as a caret
|
|
29
|
+
* and a space.
|
|
30
|
+
*
|
|
31
|
+
* AutoCAD decodes `\U+XXXX` and `\M+NXXXX` in a string, and it does so twice.
|
|
32
|
+
* A backslash that would start one is written as `\U+005CU+005C`: the first
|
|
33
|
+
* pass leaves `\U+005C` and the second leaves the backslash, with nothing left
|
|
34
|
+
* to decode what follows it. This lasts until AutoCAD saves the drawing:
|
|
35
|
+
* AutoCAD writes the text unescaped, so it decodes it on a later open.
|
|
7
36
|
*
|
|
8
37
|
* With `ascii`, characters outside ASCII become `\U+XXXX`, which is how DXF
|
|
9
38
|
* versions before 2007 carry them. Later versions are UTF-8 and need no escape.
|
package/dist/format.js
CHANGED
|
@@ -1,18 +1,72 @@
|
|
|
1
|
+
/** The number of decimals the writers keep. */
|
|
2
|
+
const DECIMALS = 6;
|
|
3
|
+
/**
|
|
4
|
+
* A number as the writers print it: rounded to 6 decimals. Two values that are
|
|
5
|
+
* equal here are the same number in the file, whatever they were in the drawing.
|
|
6
|
+
*/
|
|
7
|
+
export function written(value) {
|
|
8
|
+
const rounded = Number(value.toFixed(DECIMALS));
|
|
9
|
+
return Object.is(rounded, -0) ? 0 : rounded;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* An arc angle in degrees as the DXF writers print it: rounded to 6 decimals,
|
|
13
|
+
* with 0 in place of an angle that rounds up to 360.
|
|
14
|
+
*/
|
|
15
|
+
export function writtenAngle(degrees) {
|
|
16
|
+
const rounded = written(degrees);
|
|
17
|
+
return rounded >= 360 ? rounded - 360 : rounded;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The size from which a number prints with three exponent digits, as `1e+100`.
|
|
21
|
+
* AutoCAD reads only two from DXF: it takes `1e+100` for `1e+10` and
|
|
22
|
+
* `1e+308` for `1e+30`, with no AUDIT error. Writing the digits out does not
|
|
23
|
+
* help, because AutoCAD takes a number of 101 digits for 0.
|
|
24
|
+
*/
|
|
25
|
+
const TOO_LARGE = 1e100;
|
|
26
|
+
/** The largest number under `TOO_LARGE`. */
|
|
27
|
+
const LARGEST = 9.999999999999998e99;
|
|
28
|
+
/**
|
|
29
|
+
* True for a number AutoCAD would misread from DXF, because it is 1e100 or
|
|
30
|
+
* more in size. `lint` refuses these wherever a drawing holds one.
|
|
31
|
+
*/
|
|
32
|
+
export function isTooLarge(value) {
|
|
33
|
+
return Math.abs(value) >= TOO_LARGE;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Brings a number a writer works out for itself, such as an extent or the
|
|
37
|
+
* height of the view, into the range AutoCAD reads. `lint` cannot vouch for
|
|
38
|
+
* those: two coordinates under 1e100 can be more than 1e100 apart.
|
|
39
|
+
*/
|
|
40
|
+
export function readable(value) {
|
|
41
|
+
return Math.max(-LARGEST, Math.min(LARGEST, value));
|
|
42
|
+
}
|
|
1
43
|
/** Formats a coordinate with at most 6 decimals, no float noise, and no "-0". */
|
|
2
44
|
export function fmt(value) {
|
|
3
|
-
|
|
4
|
-
return Object.is(rounded, -0) ? "0" : String(rounded);
|
|
45
|
+
return String(written(value));
|
|
5
46
|
}
|
|
6
47
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
48
|
+
* A backslash that starts a `\U+XXXX` or `\M+NXXXX` sequence. AutoCAD decodes
|
|
49
|
+
* both forms in a DXF string, in either letter case.
|
|
50
|
+
*/
|
|
51
|
+
const ESCAPE_START = /\\(?=[Uu]\+[0-9A-Fa-f]{4}|[Mm]\+[1-5][0-9A-Fa-f]{4})/g;
|
|
52
|
+
/**
|
|
53
|
+
* Escapes a string value for DXF, so that AutoCAD holds the string as given.
|
|
54
|
+
*
|
|
55
|
+
* A caret starts a control-character escape in DXF, and AutoCAD discards the
|
|
56
|
+
* drawing when it meets a bare one, so a literal caret is written as a caret
|
|
57
|
+
* and a space.
|
|
58
|
+
*
|
|
59
|
+
* AutoCAD decodes `\U+XXXX` and `\M+NXXXX` in a string, and it does so twice.
|
|
60
|
+
* A backslash that would start one is written as `\U+005CU+005C`: the first
|
|
61
|
+
* pass leaves `\U+005C` and the second leaves the backslash, with nothing left
|
|
62
|
+
* to decode what follows it. This lasts until AutoCAD saves the drawing:
|
|
63
|
+
* AutoCAD writes the text unescaped, so it decodes it on a later open.
|
|
10
64
|
*
|
|
11
65
|
* With `ascii`, characters outside ASCII become `\U+XXXX`, which is how DXF
|
|
12
66
|
* versions before 2007 carry them. Later versions are UTF-8 and need no escape.
|
|
13
67
|
*/
|
|
14
68
|
export function dxfText(value, ascii = false) {
|
|
15
|
-
const escaped = value.replaceAll("^", "^ ");
|
|
69
|
+
const escaped = value.replace(ESCAPE_START, "\\U+005CU+005C").replaceAll("^", "^ ");
|
|
16
70
|
if (!ascii)
|
|
17
71
|
return escaped;
|
|
18
72
|
let out = "";
|