tsdraft 0.0.0-stage → 0.2.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 +50 -0
- package/LICENSE +21 -0
- package/README.md +150 -2
- package/dist/drawing.d.ts +116 -0
- package/dist/drawing.js +162 -0
- package/dist/dxf-r12.d.ts +10 -0
- package/dist/dxf-r12.js +113 -0
- package/dist/dxf-r2018.d.ts +10 -0
- package/dist/dxf-r2018.js +333 -0
- package/dist/dxf.d.ts +11 -0
- package/dist/dxf.js +18 -0
- package/dist/extents.d.ts +3 -0
- package/dist/extents.js +42 -0
- package/dist/format.d.ts +11 -0
- package/dist/format.js +24 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/lint.d.ts +30 -0
- package/dist/lint.js +109 -0
- package/dist/rules.d.ts +33 -0
- package/dist/rules.js +56 -0
- package/dist/svg.d.ts +9 -0
- package/dist/svg.js +70 -0
- package/package.json +61 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
0.1.1 was never published, so its changes ship here too.
|
|
6
|
+
|
|
7
|
+
Every fix below comes from opening tsdraft's output in AutoCAD 2025.
|
|
8
|
+
|
|
9
|
+
- `createDrawing({ dxf: "R2018" })` targets AC1032, the DXF version AutoCAD has
|
|
10
|
+
used since 2018. It writes handles, owner links, the standard tables, model
|
|
11
|
+
and paper space blocks, plot styles and `$INSUNITS`. R12 stays the default.
|
|
12
|
+
`toDXF(drawing)` writes the drawing's version, and
|
|
13
|
+
`toDXF(drawing, { version })` writes another.
|
|
14
|
+
- Layer names, layer colors, radii, text heights and rectangle sizes written as
|
|
15
|
+
literals are checked by the compiler. The same checks run when the code runs.
|
|
16
|
+
- `lint(drawing, { version })` returns every problem in a drawing. `toDXF`
|
|
17
|
+
throws a `DrawingError` listing the errors instead of writing a file AutoCAD
|
|
18
|
+
would reject.
|
|
19
|
+
- Fixed: AutoCAD discarded R12 drawings whose layer names had spaces, dots or
|
|
20
|
+
other punctuation, or more than 31 characters. Those names are now errors in
|
|
21
|
+
an R12 drawing and allowed in an R2018 drawing.
|
|
22
|
+
- Fixed: a layer name containing a line break wrote extra records into the
|
|
23
|
+
file. Commas, carets and leading or trailing spaces are rejected too.
|
|
24
|
+
- Fixed: AutoCAD discarded any drawing with a `^` in its text. A caret is now
|
|
25
|
+
escaped.
|
|
26
|
+
- Fixed: R12 text outside ASCII came out garbled. It is now written as
|
|
27
|
+
`\U+XXXX` escapes.
|
|
28
|
+
- Fixed: arc angles outside 0 to 360 failed `AUDIT`. Angles are now stored from
|
|
29
|
+
0 up to 360.
|
|
30
|
+
- Fixed: `wall` and `WALL` were two layers, and AutoCAD kept only the first.
|
|
31
|
+
They are now one layer.
|
|
32
|
+
- Text with control characters, or with emoji and other characters above
|
|
33
|
+
U+FFFF, is rejected, because AutoCAD cannot read it.
|
|
34
|
+
- Unknown units, and rectangle sizes of 0 or less, are rejected.
|
|
35
|
+
- Breaking: a `Drawing` now has a required `dxf` field, and R12 layer names
|
|
36
|
+
AutoCAD rejects now throw (see above).
|
|
37
|
+
- `pnpm test:cad` runs the output through AutoCAD over ssh.
|
|
38
|
+
- Published from GitHub Actions through npm trusted publishing, with a
|
|
39
|
+
provenance attestation linking the package to its source commit.
|
|
40
|
+
- `tsdraft/package.json` can be imported or resolved.
|
|
41
|
+
- The package now includes this changelog.
|
|
42
|
+
|
|
43
|
+
## 0.1.0
|
|
44
|
+
|
|
45
|
+
First release.
|
|
46
|
+
|
|
47
|
+
- `createDrawing()` builds a drawing with layers (AutoCAD Color Index colors),
|
|
48
|
+
lines, polylines, rectangles, circles, arcs and single-line text.
|
|
49
|
+
- `toDXF()` writes ASCII DXF R12 (AC1009) with layer and linetype tables.
|
|
50
|
+
- `toSVG()` writes an SVG preview with model Y pointing up.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stephen Frederick
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,151 @@
|
|
|
1
|
-
#
|
|
1
|
+
# tsdraft
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { createDrawing, toDXF, toSVG } from "tsdraft";
|
|
29
|
+
|
|
30
|
+
const d = createDrawing({ units: "ft" });
|
|
31
|
+
|
|
32
|
+
d.layer("C-PROP", { color: 3 }, () => {
|
|
33
|
+
d.rect([0, 0], 75, 120);
|
|
34
|
+
});
|
|
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 });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const drawing = d.build();
|
|
41
|
+
const dxf = toDXF(drawing);
|
|
42
|
+
const svg = toSVG(drawing);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Coordinates are in drawing units with Y pointing up, as in CAD. Angles are in
|
|
46
|
+
degrees, counterclockwise from +X. Layer colors are AutoCAD Color Index numbers.
|
|
47
|
+
Entities go on layer `0` unless they are drawn inside a `layer()` callback.
|
|
48
|
+
|
|
49
|
+
## DXF versions
|
|
50
|
+
|
|
51
|
+
A drawing is written for one DXF version. `createDrawing()` targets R12, the
|
|
52
|
+
oldest version still in common use, which nearly every CAD program can open.
|
|
53
|
+
`createDrawing({ dxf: "R2018" })` targets the version AutoCAD has used since
|
|
54
|
+
2018, which is the newest one Autodesk has published. `toDXF(drawing)` writes
|
|
55
|
+
the drawing's version, and `toDXF(drawing, { version })` writes another one.
|
|
56
|
+
|
|
57
|
+
| | `"R12"`, the default | `"R2018"` |
|
|
58
|
+
| ------------- | -------------------------------------------------- | ------------------------------------------------------ |
|
|
59
|
+
| Format code | AC1009 | AC1032 |
|
|
60
|
+
| Drawing units | Not stored. Set them in CAD with `UNITS`. | Stored in `$INSUNITS`. |
|
|
61
|
+
| Polylines | `POLYLINE` with `VERTEX` records | `LWPOLYLINE` |
|
|
62
|
+
| Text | ASCII, with `\U+XXXX` escapes for other characters | UTF-8 |
|
|
63
|
+
| Layer names | Up to 31 letters, digits, `$`, `-` and `_` | Up to 255 characters, with the exceptions listed below |
|
|
64
|
+
|
|
65
|
+
Choose R2018 when the drawing will be inserted into another drawing, because CAD
|
|
66
|
+
needs the stored units to scale it correctly. AutoCAD shows layer names from an
|
|
67
|
+
R12 file in upper case.
|
|
68
|
+
|
|
69
|
+
## Checks
|
|
70
|
+
|
|
71
|
+
AutoCAD discards a whole drawing over one bad layer name, and it reports no
|
|
72
|
+
line of your code when it does. tsdraft checks the rules AutoCAD enforces at
|
|
73
|
+
three points, so the mistake shows up where you made it.
|
|
74
|
+
|
|
75
|
+
1. **In the editor.** Layer names, colors and sizes written as literals are
|
|
76
|
+
checked by the TypeScript compiler, and the error message says what is wrong.
|
|
77
|
+
2. **When the code runs.** The builder throws at the call that breaks a rule.
|
|
78
|
+
This covers values the compiler cannot see, such as a name read from a file.
|
|
79
|
+
3. **Before export.** `lint(drawing)` returns every problem in a drawing, and
|
|
80
|
+
`toDXF` throws a `DrawingError` that lists them instead of writing a file
|
|
81
|
+
AutoCAD would reject.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const d = createDrawing({ units: "ft" });
|
|
85
|
+
|
|
86
|
+
d.layer("A WALL");
|
|
87
|
+
// error TS2345: Argument of type '"A WALL"' is not assignable to parameter of
|
|
88
|
+
// type '"R12 layer names can only use letters, digits, $, - and _. Use DXF
|
|
89
|
+
// version \"R2018\" for other characters"'.
|
|
90
|
+
|
|
91
|
+
createDrawing({ dxf: "R2018" }).layer("A WALL"); // fine
|
|
92
|
+
|
|
93
|
+
lint(drawing, { version: "R12" }); // check a drawing against another version
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The rules:
|
|
97
|
+
|
|
98
|
+
- Layer names cannot be empty, start or end with a space, or contain a line
|
|
99
|
+
break or any of ``< > / \ " : ; ? * | = ` ^ ,``. R12 names have the tighter
|
|
100
|
+
limits in the table above.
|
|
101
|
+
- `wall` and `WALL` are the same layer, as in CAD. The first spelling is kept.
|
|
102
|
+
- Layer colors are whole numbers from 1 to 255.
|
|
103
|
+
- Radii, text heights and rectangle sizes are greater than 0.
|
|
104
|
+
- Text is a single line. It cannot contain emoji or other characters above
|
|
105
|
+
U+FFFF, because AutoCAD cannot read them from DXF.
|
|
106
|
+
- Arc angles are stored from 0 up to 360, so -90 becomes 270.
|
|
107
|
+
|
|
108
|
+
`lint` also returns warnings, such as a line with no length. Warnings do not
|
|
109
|
+
stop an export.
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
Contributors need Node 24 or later and pnpm. Node runs the TypeScript source
|
|
114
|
+
directly, so tests and examples need no build step.
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
pnpm install
|
|
118
|
+
pnpm verify # format check, lint, type check, tests, package smoke test
|
|
119
|
+
pnpm example # writes a site plan to examples/out/ as DXF and SVG
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`pnpm test:cad` opens the output in AutoCAD. It needs
|
|
123
|
+
`TSDRAFT_CAD_SSH=user@host`, an ssh login to a WSL shell on a Windows machine
|
|
124
|
+
that has AutoCAD. Each test drawing runs in AutoCAD's headless console, which
|
|
125
|
+
runs `AUDIT` and saves the drawing back as DXF. The script then compares that
|
|
126
|
+
file with what was drawn. Run it after any change to a DXF writer, because
|
|
127
|
+
AutoCAD rejects files that DXF parsing libraries accept.
|
|
128
|
+
|
|
129
|
+
`pnpm test:package` packs the package, installs the tarball into a throwaway
|
|
130
|
+
project and checks that it type-checks and runs there. `pnpm build` compiles
|
|
131
|
+
`src/` to `dist/`, and `npm pack` and `npm publish` run it automatically.
|
|
132
|
+
|
|
133
|
+
The official TypeScript 7 compiler checks and builds the package.
|
|
134
|
+
[tsrs](https://github.com/oxc-project/tsrs), the Rust port of TypeScript, runs
|
|
135
|
+
as `pnpm check:tsrs` and as an informational CI step. It tracks TypeScript 7.1
|
|
136
|
+
beta, so it does not gate releases yet. Its WebAssembly build is the planned
|
|
137
|
+
route for type-checking drawings in a browser playground.
|
|
138
|
+
|
|
139
|
+
## Releasing
|
|
140
|
+
|
|
141
|
+
Bump `version` in `package.json` and add a CHANGELOG entry in a pull request.
|
|
142
|
+
After it merges, publish a GitHub release tagged `v<version>` on the merge
|
|
143
|
+
commit. The Release workflow checks that the tag matches the version, runs
|
|
144
|
+
`pnpm verify` and publishes to npm through
|
|
145
|
+
[trusted publishing](https://docs.npmjs.com/trusted-publishers), so no npm token
|
|
146
|
+
is stored anywhere. npm adds a provenance attestation that links each version to
|
|
147
|
+
the commit it was built from.
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { type LayerColor, type LayerName, type Positive } from "./rules.ts";
|
|
2
|
+
/** A model-space point, in drawing units. Y points up, as in CAD. */
|
|
3
|
+
export type Point = readonly [x: number, y: number];
|
|
4
|
+
/**
|
|
5
|
+
* The unit one drawing unit stands for. It is kept on the drawing for callers;
|
|
6
|
+
* the DXF R12 writer has no field for it, so set units in CAD after opening.
|
|
7
|
+
*/
|
|
8
|
+
export type Units = "in" | "ft" | "mm" | "cm" | "m";
|
|
9
|
+
/**
|
|
10
|
+
* DXF versions tsdraft writes.
|
|
11
|
+
*
|
|
12
|
+
* - `"R12"` (AC1009) opens in nearly every CAD program, but stores no units and
|
|
13
|
+
* limits layer names to 31 letters, digits, `$`, `-` and `_`.
|
|
14
|
+
* - `"R2018"` (AC1032) is the newest published version and the one AutoCAD has
|
|
15
|
+
* used since 2018. It records the drawing's units.
|
|
16
|
+
*/
|
|
17
|
+
export type DxfVersion = "R12" | "R2018";
|
|
18
|
+
export interface Layer {
|
|
19
|
+
readonly name: string;
|
|
20
|
+
/** AutoCAD Color Index, 1 to 255. 7 is white on dark backgrounds, black on light. */
|
|
21
|
+
readonly color: number;
|
|
22
|
+
}
|
|
23
|
+
interface EntityBase {
|
|
24
|
+
readonly layer: string;
|
|
25
|
+
}
|
|
26
|
+
export interface Line extends EntityBase {
|
|
27
|
+
readonly kind: "line";
|
|
28
|
+
readonly from: Point;
|
|
29
|
+
readonly to: Point;
|
|
30
|
+
}
|
|
31
|
+
export interface Polyline extends EntityBase {
|
|
32
|
+
readonly kind: "polyline";
|
|
33
|
+
readonly points: readonly Point[];
|
|
34
|
+
readonly closed: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface Circle extends EntityBase {
|
|
37
|
+
readonly kind: "circle";
|
|
38
|
+
readonly center: Point;
|
|
39
|
+
readonly radius: number;
|
|
40
|
+
}
|
|
41
|
+
/** Angles are in degrees from 0 up to 360, counterclockwise from +X, as in DXF. */
|
|
42
|
+
export interface Arc extends EntityBase {
|
|
43
|
+
readonly kind: "arc";
|
|
44
|
+
readonly center: Point;
|
|
45
|
+
readonly radius: number;
|
|
46
|
+
readonly startAngle: number;
|
|
47
|
+
readonly endAngle: number;
|
|
48
|
+
}
|
|
49
|
+
export interface Text extends EntityBase {
|
|
50
|
+
readonly kind: "text";
|
|
51
|
+
/** Left end of the text baseline. */
|
|
52
|
+
readonly at: Point;
|
|
53
|
+
readonly height: number;
|
|
54
|
+
readonly value: string;
|
|
55
|
+
/** Degrees counterclockwise. */
|
|
56
|
+
readonly rotation: number;
|
|
57
|
+
}
|
|
58
|
+
export type Entity = Line | Polyline | Circle | Arc | Text;
|
|
59
|
+
export interface Drawing {
|
|
60
|
+
readonly units: Units;
|
|
61
|
+
/** The DXF version the drawing is written for. `toDXF` uses it unless told otherwise. */
|
|
62
|
+
readonly dxf: DxfVersion;
|
|
63
|
+
readonly layers: readonly Layer[];
|
|
64
|
+
readonly entities: readonly Entity[];
|
|
65
|
+
}
|
|
66
|
+
export interface DrawingOptions<V extends DxfVersion = DxfVersion> {
|
|
67
|
+
/** Defaults to `"in"`. */
|
|
68
|
+
readonly units?: Units;
|
|
69
|
+
/** The DXF version to check the drawing against and export. Defaults to `"R12"`. */
|
|
70
|
+
readonly dxf?: V;
|
|
71
|
+
}
|
|
72
|
+
export interface LayerOptions<C extends number = number> {
|
|
73
|
+
/** AutoCAD Color Index, 1 to 255. */
|
|
74
|
+
readonly color?: LayerColor<C>;
|
|
75
|
+
}
|
|
76
|
+
export interface TextOptions<H extends number = number> {
|
|
77
|
+
readonly height?: Positive<H>;
|
|
78
|
+
/** Degrees counterclockwise. */
|
|
79
|
+
readonly rotation?: number;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Builds a drawing. Entities go on the current layer, which starts as "0".
|
|
83
|
+
* `layer(name, opts, draw)` scopes everything drawn inside `draw` to that layer.
|
|
84
|
+
*
|
|
85
|
+
* Mistakes written as literals, such as a layer name the DXF version does not
|
|
86
|
+
* allow or a negative radius, are compile errors. The same checks run again
|
|
87
|
+
* when the code runs, for values the compiler cannot see.
|
|
88
|
+
*/
|
|
89
|
+
export declare class DrawingBuilder<V extends DxfVersion = "R12"> {
|
|
90
|
+
#private;
|
|
91
|
+
readonly units: Units;
|
|
92
|
+
readonly dxf: V;
|
|
93
|
+
constructor(options?: DrawingOptions<V>);
|
|
94
|
+
/**
|
|
95
|
+
* Defines or updates a layer. With `draw`, entities drawn inside it go on this layer.
|
|
96
|
+
* `wall` and `WALL` are the same layer, as in CAD, and keep the first spelling.
|
|
97
|
+
*/
|
|
98
|
+
layer<N extends string, C extends number = number>(name: LayerName<N, V>, options?: LayerOptions<C>, draw?: () => void): this;
|
|
99
|
+
line(from: Point, to: Point): this;
|
|
100
|
+
polyline(points: readonly Point[], closed?: boolean): this;
|
|
101
|
+
/** Closed rectangle from its lower-left corner. */
|
|
102
|
+
rect<W extends number, H extends number>(corner: Point, width: Positive<W>, height: Positive<H>): this;
|
|
103
|
+
circle<R extends number>(center: Point, radius: Positive<R>): this;
|
|
104
|
+
/**
|
|
105
|
+
* Arc from `startAngle` counterclockwise to `endAngle`. Angles are stored
|
|
106
|
+
* from 0 up to 360, so -90 becomes 270, because AutoCAD's AUDIT rejects
|
|
107
|
+
* angles outside that range.
|
|
108
|
+
*/
|
|
109
|
+
arc<R extends number>(center: Point, radius: Positive<R>, startAngle: number, endAngle: number): this;
|
|
110
|
+
text<H extends number = number>(at: Point, value: string, options?: TextOptions<H>): this;
|
|
111
|
+
build(): Drawing;
|
|
112
|
+
}
|
|
113
|
+
export declare function createDrawing<V extends DxfVersion = "R12">(options?: DrawingOptions<V>): DrawingBuilder<V>;
|
|
114
|
+
/** Counterclockwise sweep from start to end, in degrees, within [0, 360). */
|
|
115
|
+
export declare function sweep(startAngle: number, endAngle: number): number;
|
|
116
|
+
export {};
|
package/dist/drawing.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { layerColorProblem, layerNameProblem, textProblem, } from "./rules.js";
|
|
2
|
+
const UNITS = ["in", "ft", "mm", "cm", "m"];
|
|
3
|
+
const DXF_VERSIONS = ["R12", "R2018"];
|
|
4
|
+
/**
|
|
5
|
+
* Builds a drawing. Entities go on the current layer, which starts as "0".
|
|
6
|
+
* `layer(name, opts, draw)` scopes everything drawn inside `draw` to that layer.
|
|
7
|
+
*
|
|
8
|
+
* Mistakes written as literals, such as a layer name the DXF version does not
|
|
9
|
+
* allow or a negative radius, are compile errors. The same checks run again
|
|
10
|
+
* when the code runs, for values the compiler cannot see.
|
|
11
|
+
*/
|
|
12
|
+
export class DrawingBuilder {
|
|
13
|
+
units;
|
|
14
|
+
dxf;
|
|
15
|
+
/** Keyed by lower-cased name, because CAD ignores case in layer names. */
|
|
16
|
+
#layers = new Map([["0", { name: "0", color: 7 }]]);
|
|
17
|
+
#entities = [];
|
|
18
|
+
#current = "0";
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
const units = options.units ?? "in";
|
|
21
|
+
if (!UNITS.includes(units))
|
|
22
|
+
throw new Error(`Unknown units: ${JSON.stringify(units)}`);
|
|
23
|
+
const dxf = options.dxf ?? "R12";
|
|
24
|
+
if (!DXF_VERSIONS.includes(dxf)) {
|
|
25
|
+
throw new Error(`Unsupported DXF version: ${JSON.stringify(dxf)}`);
|
|
26
|
+
}
|
|
27
|
+
this.units = units;
|
|
28
|
+
this.dxf = dxf;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Defines or updates a layer. With `draw`, entities drawn inside it go on this layer.
|
|
32
|
+
* `wall` and `WALL` are the same layer, as in CAD, and keep the first spelling.
|
|
33
|
+
*/
|
|
34
|
+
layer(name, options = {}, draw) {
|
|
35
|
+
// The parameter type is the error message when a literal name is wrong, so read it as a string.
|
|
36
|
+
const requested = name;
|
|
37
|
+
const nameProblem = layerNameProblem(requested, this.dxf);
|
|
38
|
+
if (nameProblem)
|
|
39
|
+
throw new Error(`Layer ${JSON.stringify(requested)}: ${nameProblem}`);
|
|
40
|
+
const key = requested.toLowerCase();
|
|
41
|
+
const existing = this.#layers.get(key);
|
|
42
|
+
const color = options.color ?? existing?.color ?? 7;
|
|
43
|
+
const colorProblem = layerColorProblem(color);
|
|
44
|
+
if (colorProblem)
|
|
45
|
+
throw new Error(`Layer ${JSON.stringify(requested)}: ${colorProblem}`);
|
|
46
|
+
const layer = { name: existing?.name ?? requested, color };
|
|
47
|
+
this.#layers.set(key, layer);
|
|
48
|
+
if (draw) {
|
|
49
|
+
const previous = this.#current;
|
|
50
|
+
this.#current = layer.name;
|
|
51
|
+
try {
|
|
52
|
+
draw();
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
this.#current = previous;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
line(from, to) {
|
|
61
|
+
return this.#add({ kind: "line", layer: this.#current, from: pt(from), to: pt(to) });
|
|
62
|
+
}
|
|
63
|
+
polyline(points, closed = false) {
|
|
64
|
+
if (points.length < 2)
|
|
65
|
+
throw new Error("A polyline needs at least 2 points");
|
|
66
|
+
return this.#add({ kind: "polyline", layer: this.#current, points: points.map(pt), closed });
|
|
67
|
+
}
|
|
68
|
+
/** Closed rectangle from its lower-left corner. */
|
|
69
|
+
rect(corner, width, height) {
|
|
70
|
+
const [x, y] = pt(corner);
|
|
71
|
+
const w = positive(width, "width");
|
|
72
|
+
const h = positive(height, "height");
|
|
73
|
+
return this.polyline([
|
|
74
|
+
[x, y],
|
|
75
|
+
[x + w, y],
|
|
76
|
+
[x + w, y + h],
|
|
77
|
+
[x, y + h],
|
|
78
|
+
], true);
|
|
79
|
+
}
|
|
80
|
+
circle(center, radius) {
|
|
81
|
+
return this.#add({
|
|
82
|
+
kind: "circle",
|
|
83
|
+
layer: this.#current,
|
|
84
|
+
center: pt(center),
|
|
85
|
+
radius: positive(radius, "radius"),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Arc from `startAngle` counterclockwise to `endAngle`. Angles are stored
|
|
90
|
+
* from 0 up to 360, so -90 becomes 270, because AutoCAD's AUDIT rejects
|
|
91
|
+
* angles outside that range.
|
|
92
|
+
*/
|
|
93
|
+
arc(center, radius, startAngle, endAngle) {
|
|
94
|
+
const start = turn(num(startAngle, "startAngle"));
|
|
95
|
+
const end = turn(num(endAngle, "endAngle"));
|
|
96
|
+
if (start === end)
|
|
97
|
+
throw new Error("An arc's start and end angles must differ");
|
|
98
|
+
return this.#add({
|
|
99
|
+
kind: "arc",
|
|
100
|
+
layer: this.#current,
|
|
101
|
+
center: pt(center),
|
|
102
|
+
radius: positive(radius, "radius"),
|
|
103
|
+
startAngle: start,
|
|
104
|
+
endAngle: end,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
text(at, value, options = {}) {
|
|
108
|
+
const problem = textProblem(value);
|
|
109
|
+
if (problem)
|
|
110
|
+
throw new Error(problem);
|
|
111
|
+
return this.#add({
|
|
112
|
+
kind: "text",
|
|
113
|
+
layer: this.#current,
|
|
114
|
+
at: pt(at),
|
|
115
|
+
value,
|
|
116
|
+
height: positive(options.height ?? 0.125, "height"),
|
|
117
|
+
rotation: num(options.rotation ?? 0, "rotation"),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
build() {
|
|
121
|
+
return {
|
|
122
|
+
units: this.units,
|
|
123
|
+
dxf: this.dxf,
|
|
124
|
+
layers: [...this.#layers.values()],
|
|
125
|
+
entities: [...this.#entities],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
#add(entity) {
|
|
129
|
+
this.#entities.push(entity);
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export function createDrawing(options) {
|
|
134
|
+
return new DrawingBuilder(options);
|
|
135
|
+
}
|
|
136
|
+
/** Counterclockwise sweep from start to end, in degrees, within [0, 360). */
|
|
137
|
+
export function sweep(startAngle, endAngle) {
|
|
138
|
+
return turn(endAngle - startAngle);
|
|
139
|
+
}
|
|
140
|
+
/** Brings an angle in degrees into [0, 360). */
|
|
141
|
+
function turn(degrees) {
|
|
142
|
+
const wrapped = ((degrees % 360) + 360) % 360;
|
|
143
|
+
// A tiny negative angle rounds up to exactly 360.
|
|
144
|
+
return wrapped === 360 ? 0 : wrapped;
|
|
145
|
+
}
|
|
146
|
+
function num(value, label) {
|
|
147
|
+
if (!Number.isFinite(value))
|
|
148
|
+
throw new Error(`${label} must be a finite number`);
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
/** Takes `unknown` because a `Positive<N>` parameter is a message string when the literal is wrong. */
|
|
152
|
+
function positive(value, label) {
|
|
153
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
154
|
+
throw new Error(`${label} must be a finite number`);
|
|
155
|
+
}
|
|
156
|
+
if (value <= 0)
|
|
157
|
+
throw new Error(`${label} must be greater than 0`);
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
function pt(point) {
|
|
161
|
+
return [num(point[0], "x"), num(point[1], "y")];
|
|
162
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Drawing } from "./drawing.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Writes an ASCII DXF (AC1009, R12). R12 is the most widely readable DXF:
|
|
4
|
+
* AutoCAD, BricsCAD, LibreCAD, QCAD and most converters open it.
|
|
5
|
+
*
|
|
6
|
+
* R12 has no header variable for drawing units, so coordinates are written as
|
|
7
|
+
* plain numbers in the drawing's units and `drawing.units` is not written.
|
|
8
|
+
* R12 predates Unicode DXF, so text outside ASCII is written as `\U+XXXX`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function toDXFR12(drawing: Drawing): string;
|
package/dist/dxf-r12.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { dxfText, fmt } from "./format.js";
|
|
2
|
+
/**
|
|
3
|
+
* Writes an ASCII DXF (AC1009, R12). R12 is the most widely readable DXF:
|
|
4
|
+
* AutoCAD, BricsCAD, LibreCAD, QCAD and most converters open it.
|
|
5
|
+
*
|
|
6
|
+
* R12 has no header variable for drawing units, so coordinates are written as
|
|
7
|
+
* plain numbers in the drawing's units and `drawing.units` is not written.
|
|
8
|
+
* R12 predates Unicode DXF, so text outside ASCII is written as `\U+XXXX`.
|
|
9
|
+
*/
|
|
10
|
+
export function toDXFR12(drawing) {
|
|
11
|
+
const out = [];
|
|
12
|
+
const g = (code, value) => {
|
|
13
|
+
out.push(String(code), typeof value === "number" ? fmt(value) : value);
|
|
14
|
+
};
|
|
15
|
+
g(0, "SECTION");
|
|
16
|
+
g(2, "HEADER");
|
|
17
|
+
g(9, "$ACADVER");
|
|
18
|
+
g(1, "AC1009");
|
|
19
|
+
g(0, "ENDSEC");
|
|
20
|
+
g(0, "SECTION");
|
|
21
|
+
g(2, "TABLES");
|
|
22
|
+
g(0, "TABLE");
|
|
23
|
+
g(2, "LTYPE");
|
|
24
|
+
g(70, "1");
|
|
25
|
+
g(0, "LTYPE");
|
|
26
|
+
g(2, "CONTINUOUS");
|
|
27
|
+
g(70, "0");
|
|
28
|
+
g(3, "Solid line");
|
|
29
|
+
g(72, "65");
|
|
30
|
+
g(73, "0");
|
|
31
|
+
g(40, "0.0");
|
|
32
|
+
g(0, "ENDTAB");
|
|
33
|
+
g(0, "TABLE");
|
|
34
|
+
g(2, "LAYER");
|
|
35
|
+
g(70, String(drawing.layers.length));
|
|
36
|
+
for (const layer of drawing.layers) {
|
|
37
|
+
g(0, "LAYER");
|
|
38
|
+
g(2, layer.name);
|
|
39
|
+
g(70, "0");
|
|
40
|
+
g(62, String(layer.color));
|
|
41
|
+
g(6, "CONTINUOUS");
|
|
42
|
+
}
|
|
43
|
+
g(0, "ENDTAB");
|
|
44
|
+
g(0, "ENDSEC");
|
|
45
|
+
g(0, "SECTION");
|
|
46
|
+
g(2, "ENTITIES");
|
|
47
|
+
for (const entity of drawing.entities)
|
|
48
|
+
writeEntity(entity, g);
|
|
49
|
+
g(0, "ENDSEC");
|
|
50
|
+
g(0, "EOF");
|
|
51
|
+
return out.join("\r\n") + "\r\n";
|
|
52
|
+
}
|
|
53
|
+
function writeEntity(e, g) {
|
|
54
|
+
switch (e.kind) {
|
|
55
|
+
case "line":
|
|
56
|
+
g(0, "LINE");
|
|
57
|
+
g(8, e.layer);
|
|
58
|
+
g(10, e.from[0]);
|
|
59
|
+
g(20, e.from[1]);
|
|
60
|
+
g(30, 0);
|
|
61
|
+
g(11, e.to[0]);
|
|
62
|
+
g(21, e.to[1]);
|
|
63
|
+
g(31, 0);
|
|
64
|
+
return;
|
|
65
|
+
case "polyline":
|
|
66
|
+
g(0, "POLYLINE");
|
|
67
|
+
g(8, e.layer);
|
|
68
|
+
g(66, "1");
|
|
69
|
+
g(10, 0);
|
|
70
|
+
g(20, 0);
|
|
71
|
+
g(30, 0);
|
|
72
|
+
g(70, e.closed ? "1" : "0");
|
|
73
|
+
for (const [x, y] of e.points) {
|
|
74
|
+
g(0, "VERTEX");
|
|
75
|
+
g(8, e.layer);
|
|
76
|
+
g(10, x);
|
|
77
|
+
g(20, y);
|
|
78
|
+
g(30, 0);
|
|
79
|
+
}
|
|
80
|
+
g(0, "SEQEND");
|
|
81
|
+
g(8, e.layer);
|
|
82
|
+
return;
|
|
83
|
+
case "circle":
|
|
84
|
+
g(0, "CIRCLE");
|
|
85
|
+
g(8, e.layer);
|
|
86
|
+
g(10, e.center[0]);
|
|
87
|
+
g(20, e.center[1]);
|
|
88
|
+
g(30, 0);
|
|
89
|
+
g(40, e.radius);
|
|
90
|
+
return;
|
|
91
|
+
case "arc":
|
|
92
|
+
g(0, "ARC");
|
|
93
|
+
g(8, e.layer);
|
|
94
|
+
g(10, e.center[0]);
|
|
95
|
+
g(20, e.center[1]);
|
|
96
|
+
g(30, 0);
|
|
97
|
+
g(40, e.radius);
|
|
98
|
+
g(50, e.startAngle);
|
|
99
|
+
g(51, e.endAngle);
|
|
100
|
+
return;
|
|
101
|
+
case "text":
|
|
102
|
+
g(0, "TEXT");
|
|
103
|
+
g(8, e.layer);
|
|
104
|
+
g(10, e.at[0]);
|
|
105
|
+
g(20, e.at[1]);
|
|
106
|
+
g(30, 0);
|
|
107
|
+
g(40, e.height);
|
|
108
|
+
g(1, dxfText(e.value, true));
|
|
109
|
+
if (e.rotation !== 0)
|
|
110
|
+
g(50, e.rotation);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Drawing } from "./drawing.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Writes an ASCII DXF for AutoCAD 2018 and later (AC1032), the newest DXF
|
|
4
|
+
* version Autodesk has published. AutoCAD 2027 still reads and writes AC1032.
|
|
5
|
+
*
|
|
6
|
+
* Unlike R12, this version has handles, owner links, an OBJECTS section and the
|
|
7
|
+
* `$INSUNITS` header, so the drawing's units reach CAD and xref and block
|
|
8
|
+
* insertion scale correctly. Text is UTF-8.
|
|
9
|
+
*/
|
|
10
|
+
export declare function toDXFR2018(drawing: Drawing): string;
|