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.
@@ -0,0 +1,82 @@
1
+ // Where in the caller's code a builder method was called. The location comes from the engine's
2
+ // stack, so it holds a path of the machine the drawing was built on. It is kept beside the
3
+ // drawing, never in it.
4
+ /** The calls that drew a drawing's entities, by its `entities` array and each entity's index. */
5
+ const recorded = new WeakMap();
6
+ /**
7
+ * Captures the call of the builder method `method` that is running now, to be read later, or
8
+ * returns undefined where the engine gives no stack. It must be called from the builder's file.
9
+ *
10
+ * With `Error.captureStackTrace` the engine leaves out every frame down to `method`, so only the
11
+ * caller's frame is captured. Without it, the whole stack of a new error is kept.
12
+ */
13
+ export function holdCall(method) {
14
+ const errors = Error;
15
+ try {
16
+ if (typeof errors.captureStackTrace !== "function") {
17
+ return { stack: new Error().stack, whole: true };
18
+ }
19
+ const held = { whole: false };
20
+ const limit = errors.stackTraceLimit;
21
+ const limited = typeof limit === "number";
22
+ if (limited)
23
+ errors.stackTraceLimit = 1;
24
+ try {
25
+ errors.captureStackTrace(held, method);
26
+ }
27
+ finally {
28
+ if (limited)
29
+ errors.stackTraceLimit = limit;
30
+ }
31
+ return held;
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ /** The location a held call names, or undefined when its stack has none. */
38
+ export function readCall(held) {
39
+ if (!held)
40
+ return undefined;
41
+ try {
42
+ const stack = held.stack;
43
+ if (typeof stack !== "string")
44
+ return undefined;
45
+ return held.whole ? callerInStack(stack) : parseStack(stack)[0];
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ }
51
+ /**
52
+ * The caller's frame in a whole stack captured by `holdCall`: the first frame in neither of
53
+ * tsdraft's two files on it, which are those of the first frame, `holdCall`'s own, and of the
54
+ * second, the builder's. Frames are told apart by file and not counted, because an engine may
55
+ * leave a frame out. When the caller's code is in one file with tsdraft, as in a bundle, no
56
+ * frame qualifies and there is no location, which is better than a line inside tsdraft.
57
+ */
58
+ export function callerInStack(stack) {
59
+ const [capture, builder, ...rest] = parseStack(stack);
60
+ return rest.find((frame) => frame.file !== capture?.file && frame.file !== builder?.file);
61
+ }
62
+ /** Keeps the calls that drew `entities`, one for each entity in order. */
63
+ export function recordCalls(entities, calls) {
64
+ recorded.set(entities, calls);
65
+ }
66
+ /** The call that drew the entity at `index` of `entities`, when the builder recorded it. */
67
+ export function callOf(entities, index) {
68
+ return readCall(recorded.get(entities)?.[index]);
69
+ }
70
+ /**
71
+ * The location of each frame of a stack, innermost first. V8 writes a frame as
72
+ * `at name (file:1:2)` or `at file:1:2`, and Firefox and Safari as `name@file:1:2`. A line that
73
+ * is no frame, such as the error's message, is left out.
74
+ */
75
+ export function parseStack(stack) {
76
+ return stack.split("\n").flatMap((line) => {
77
+ const frame = /^\s*at .*?\((.+):(\d+):(\d+)\)$/.exec(line) ??
78
+ /^\s*at (.+):(\d+):(\d+)$/.exec(line) ??
79
+ /^[^@]*@(.+):(\d+):(\d+)$/.exec(line);
80
+ return frame ? [{ file: frame[1], line: Number(frame[2]), column: Number(frame[3]) }] : [];
81
+ });
82
+ }
package/dist/drawing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type LayerColor, type LayerName, type Positive } from "./rules.ts";
1
+ import { type Description, type EntityId, type FrozenCurrent, type LayerLinetypeName, type LineweightMm, type LineweightOf, type LinetypeName, type NewLinetypeName, type Pattern, type R12EntityStyle, type R12LayerStates, type R12Lineweights, type R12Linetypes, type LayerColor, type LayerName, type Positive } from "./rules.ts";
2
2
  /** A model-space point, in drawing units. Y points up, as in CAD. */
3
3
  export type Point = readonly [x: number, y: number];
4
4
  /**
@@ -15,13 +15,53 @@ export type Units = "in" | "ft" | "mm" | "cm" | "m";
15
15
  * used since 2018. It records the drawing's units.
16
16
  */
17
17
  export type DxfVersion = "R12" | "R2018";
18
+ /**
19
+ * A simple linetype: a repeating pattern of dashes, gaps and dots. The lengths are in drawing
20
+ * units, and CAD multiplies them by the drawing's `linetypeScale`.
21
+ */
22
+ export interface Linetype {
23
+ readonly name: string;
24
+ readonly description: string;
25
+ /** A dash is a positive length, a gap a negative one, and a dot 0. Empty for a solid line. */
26
+ readonly pattern: readonly number[];
27
+ }
28
+ /**
29
+ * The lineweights AutoCAD offers, in millimetres. A lineweight is how wide a line plots, and
30
+ * how wide AutoCAD shows it when LWDISPLAY is on.
31
+ */
32
+ export type Lineweight = LineweightMm;
18
33
  export interface Layer {
19
34
  readonly name: string;
20
35
  /** AutoCAD Color Index, 1 to 255. 7 is white on dark backgrounds, black on light. */
21
36
  readonly color: number;
37
+ /** The linetype of the layer's entities that draw ByLayer. Defaults to `"Continuous"`. */
38
+ readonly linetype?: string;
39
+ /**
40
+ * In millimetres, one of `Lineweight`, or `"Default"`, which AutoCAD draws at its LWDEFAULT
41
+ * of 0.25 mm. Defaults to `"Default"`.
42
+ */
43
+ readonly lineweight?: number | "Default";
44
+ /** An off layer is not shown or plotted, but CAD still regenerates it. */
45
+ readonly off?: boolean;
46
+ /** A frozen layer is not shown, plotted or regenerated. */
47
+ readonly frozen?: boolean;
48
+ /** A locked layer is shown and plotted, and CAD does not let its entities be edited. */
49
+ readonly locked?: boolean;
50
+ /** Whether the layer plots. Defaults to `true`. */
51
+ readonly plot?: boolean;
22
52
  }
23
53
  interface EntityBase {
54
+ /**
55
+ * A name for this entity that stays the same when the drawing's code changes around it: 1 to
56
+ * 64 letters A to Z or a to z, digits, `.`, `-` and `_`. No two entities of a drawing share
57
+ * one, whatever its case.
58
+ */
59
+ readonly id?: string;
24
60
  readonly layer: string;
61
+ /** A linetype of the drawing, `"Continuous"`, `"ByBlock"` or `"ByLayer"`, the default. */
62
+ readonly linetype?: string;
63
+ /** In millimetres, one of `Lineweight`, or `"ByLayer"`, the default, `"ByBlock"` or `"Default"`. */
64
+ readonly lineweight?: number | "ByLayer" | "ByBlock" | "Default";
25
65
  }
26
66
  export interface Line extends EntityBase {
27
67
  readonly kind: "line";
@@ -57,23 +97,121 @@ export interface Text extends EntityBase {
57
97
  }
58
98
  export type Entity = Line | Polyline | Circle | Arc | Text;
59
99
  export interface Drawing {
100
+ /**
101
+ * The version of the `Drawing` format, which is separate from the DXF version in `dxf`.
102
+ * Format 0 carries no promise of stability: a later tsdraft may change it without reading it.
103
+ */
104
+ readonly format: 0;
60
105
  readonly units: Units;
61
106
  /** The DXF version the drawing is written for. `toDXF` uses it unless told otherwise. */
62
107
  readonly dxf: DxfVersion;
63
108
  readonly layers: readonly Layer[];
64
109
  readonly entities: readonly Entity[];
110
+ /** Linetypes beyond `Continuous`, `ByLayer` and `ByBlock`, which every drawing has. */
111
+ readonly linetypes?: readonly Linetype[];
112
+ /** The global linetype scale, `$LTSCALE`, which multiplies every pattern. Defaults to 1. */
113
+ readonly linetypeScale?: number;
65
114
  }
66
- export interface DrawingOptions<V extends DxfVersion = DxfVersion> {
115
+ export interface DrawingOptions<V extends DxfVersion = DxfVersion, S extends number = number> {
67
116
  /** Defaults to `"in"`. */
68
117
  readonly units?: Units;
69
- /** The DXF version to check the drawing against and export. Defaults to `"R12"`. */
118
+ /** The DXF version to check the drawing against and export. Defaults to `"R2018"`. */
70
119
  readonly dxf?: V;
120
+ /** The global linetype scale, which multiplies every linetype's pattern. Defaults to 1. */
121
+ readonly linetypeScale?: V extends "R12" ? 1 | R12Linetypes : Positive<S>;
122
+ /**
123
+ * Whether to record the call that draws each entity, so that a `lint` diagnostic names it in
124
+ * `call`. It more than doubles the time the builder takes. Defaults to `false`, or to `true` when
125
+ * `globalThis.TSDRAFT_RECORD_CALLS` is `true`, which a tool that runs drawing files sets
126
+ * before it runs them.
127
+ */
128
+ readonly recordCalls?: boolean;
129
+ }
130
+ /** A linetype to define: a `Linetype`, whose description may be left out. */
131
+ export interface LinetypeDefinition<N extends string = string, P extends readonly number[] = readonly number[], D extends string = string> {
132
+ readonly name: NewLinetypeName<N>;
133
+ /** Shown beside the name in CAD's linetype lists. Defaults to `""`. */
134
+ readonly description?: Description<D>;
135
+ /** A dash is a positive length, a gap a negative one, and a dot 0. Empty for a solid line. */
136
+ readonly pattern: Pattern<P>;
71
137
  }
72
- export interface LayerOptions<C extends number = number> {
138
+ /** What a layer or an entity may be given as its linetype: a name, or a definition. */
139
+ type LinetypeValue = string | LinetypeDefinition;
140
+ /** Each definition in `L`, checked as `linetype()` checks one. A name in `L` adds nothing. */
141
+ type CheckedDefinition<L> = L extends LinetypeDefinition ? LinetypeDefinition<L["name"], L["pattern"], DescriptionOf<L>> : never;
142
+ type DescriptionOf<L> = L extends {
143
+ readonly description: infer D extends string;
144
+ } ? D : string;
145
+ /**
146
+ * A linetype for a layer or an entity: the name `T` of one the drawing has, or a
147
+ * `LinetypeDefinition`, which is checked as `linetype()` checks one and defined first if the
148
+ * drawing does not have it yet.
149
+ *
150
+ * `L` is the whole value given, a name or a definition, so the compiler infers it in one piece
151
+ * even from a conditional that mixes the two. That is why this is one conditional type and not a
152
+ * union: the compiler infers a union's members apart. The false branch is never taken: it is
153
+ * there so the compiler infers `L`, and the tuple keeps `const L` from widening it.
154
+ */
155
+ export type LinetypeChoice<T extends string, L extends LinetypeValue = LinetypeDefinition> = [
156
+ L
157
+ ] extends [LinetypeValue] ? LinetypeName<T> | CheckedDefinition<L> : L;
158
+ /**
159
+ * A linetype on an R12 drawing, which can only be the built-in `Allowed`. Any other literal name,
160
+ * and any definition, resolves to `Message`. `L` holds a definition when it has an object in it
161
+ * and is not left at its default, `LinetypeDefinition`. The last branch, never taken, lets `L` be
162
+ * inferred, as in `LinetypeChoice`.
163
+ */
164
+ type R12LinetypeChoice<T extends string, L extends LinetypeValue, Allowed extends string, Message> = [L] extends [LinetypeValue] ? LinetypeDefinition extends L ? R12LinetypeName<T, Allowed, Message> : [Extract<L, object>] extends [never] ? R12LinetypeName<T, Allowed, Message> : Message : L;
165
+ type R12LinetypeName<T extends string, Allowed extends string, Message> = string extends T ? T : Lowercase<T> extends Allowed ? T : Message;
166
+ /**
167
+ * What `layer()` sets on a layer.
168
+ *
169
+ * @typeParam L - The linetype as written, a name or a definition. A definition's literal mistakes
170
+ * are compile errors.
171
+ */
172
+ export interface LayerOptions<C extends number = number, W extends number = number, T extends string = string, V extends DxfVersion = "R2018", N extends string = string, L extends LinetypeValue = LinetypeDefinition> {
73
173
  /** AutoCAD Color Index, 1 to 255. */
74
174
  readonly color?: LayerColor<C>;
175
+ /** Defaults to `"Continuous"`. It cannot be ByLayer or ByBlock. R2018 only. */
176
+ readonly linetype?: V extends "R12" ? R12LinetypeChoice<T, L, "continuous", R12Linetypes> : [
177
+ L
178
+ ] extends [LinetypeValue] ? LayerLinetypeName<T> | CheckedDefinition<L> : L;
179
+ /** In millimetres, or `"Default"`, which is 0.25 mm. Defaults to `"Default"`. R2018 only. */
180
+ readonly lineweight?: V extends "R12" ? "Default" | R12Lineweights : LineweightOf<W> | "Default";
181
+ /** Hides the layer. R2018 only. */
182
+ readonly off?: V extends "R12" ? false | R12LayerStates : boolean;
183
+ /** Hides the layer and leaves it out of regeneration. Layer "0" cannot be frozen. R2018 only. */
184
+ readonly frozen?: V extends "R12" ? false | R12LayerStates : N extends "0" ? false | FrozenCurrent : boolean;
185
+ /** Keeps the layer's entities from being edited in CAD. R2018 only. */
186
+ readonly locked?: V extends "R12" ? false | R12LayerStates : boolean;
187
+ /** Whether the layer plots. Defaults to `true`. R2018 only. */
188
+ readonly plot?: V extends "R12" ? true | R12LayerStates : boolean;
75
189
  }
76
- export interface TextOptions<H extends number = number> {
190
+ /**
191
+ * The id an entity may have, and the linetype and lineweight it may have of its own. Both
192
+ * default to ByLayer.
193
+ *
194
+ * @typeParam L - The linetype as written, a name or a definition. A definition's literal mistakes
195
+ * are compile errors.
196
+ */
197
+ export interface EntityOptions<V extends DxfVersion = "R2018", T extends string = string, W extends number = number, L extends LinetypeValue = LinetypeDefinition, I extends string = string> {
198
+ /**
199
+ * A name for the entity that no other entity of the drawing has: 1 to 64 letters A to Z or a
200
+ * to z, digits, `.`, `-` and `_`. Diagnostics and `toSVG` carry it.
201
+ */
202
+ readonly id?: EntityId<I>;
203
+ /** A linetype of the drawing, `"Continuous"`, `"ByBlock"` or `"ByLayer"`. R2018 only. */
204
+ readonly linetype?: V extends "R12" ? R12LinetypeChoice<T, L, "bylayer", R12EntityStyle> : LinetypeChoice<T, L>;
205
+ /** In millimetres, or `"ByLayer"`, `"ByBlock"` or `"Default"`. R2018 only. */
206
+ readonly lineweight?: V extends "R12" ? "ByLayer" | R12EntityStyle : LineweightOf<W> | "ByLayer" | "ByBlock" | "Default";
207
+ }
208
+ /**
209
+ * The options of `text()`: its height and rotation, and the options of every entity.
210
+ *
211
+ * @typeParam L - The linetype as written, a name or a definition. A definition's literal mistakes
212
+ * are compile errors.
213
+ */
214
+ export interface TextOptions<H extends number = number, V extends DxfVersion = "R2018", T extends string = string, W extends number = number, L extends LinetypeValue = LinetypeDefinition, I extends string = string> extends EntityOptions<V, T, W, L, I> {
77
215
  readonly height?: Positive<H>;
78
216
  /** Degrees counterclockwise. */
79
217
  readonly rotation?: number;
@@ -84,23 +222,33 @@ export interface TextOptions<H extends number = number> {
84
222
  *
85
223
  * Mistakes written as literals, such as a layer name the DXF version does not
86
224
  * allow or a negative radius, are compile errors. The same checks run again
87
- * when the code runs, for values the compiler cannot see.
225
+ * when the code runs, for values the compiler cannot see: `layer`, `linetype`
226
+ * and the entity methods then throw a `DrawingError` whose diagnostic has the
227
+ * rule, the entity's `id` and the `call` that broke it.
88
228
  */
89
- export declare class DrawingBuilder<V extends DxfVersion = "R12"> {
229
+ export declare class DrawingBuilder<V extends DxfVersion = "R2018", S extends number = number> {
90
230
  #private;
91
231
  readonly units: Units;
92
232
  readonly dxf: V;
93
- constructor(options?: DrawingOptions<V>);
233
+ readonly linetypeScale: number;
234
+ constructor(options?: DrawingOptions<V, S>);
235
+ /**
236
+ * Defines a linetype, or updates the one of the same name. `dashed` and
237
+ * `DASHED` are the same linetype, as in CAD, and keep the first spelling.
238
+ * tsdraft exports the linetypes plans use, such as `DASHED` and `HIDDEN`.
239
+ */
240
+ linetype<N extends string, const P extends readonly number[], D extends string = string>(linetype: V extends "R12" ? R12Linetypes : LinetypeDefinition<N, P, D>): this;
94
241
  /**
95
242
  * Defines or updates a layer. With `draw`, entities drawn inside it go on this layer.
96
243
  * `wall` and `WALL` are the same layer, as in CAD, and keep the first spelling.
244
+ * Options left out keep the values the layer has.
97
245
  */
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;
246
+ layer<N extends string, C extends number = number, W extends number = number, T extends string = string, const L extends LinetypeValue = LinetypeDefinition>(name: LayerName<N, V>, options?: LayerOptions<C, W, T, V, N, L>, draw?: () => void): this;
247
+ line<T extends string = string, W extends number = number, const L extends LinetypeValue = LinetypeDefinition, I extends string = string>(from: Point, to: Point, options?: EntityOptions<V, T, W, L, I>): this;
248
+ polyline<T extends string = string, W extends number = number, const L extends LinetypeValue = LinetypeDefinition, I extends string = string>(points: readonly Point[], closed?: boolean, options?: EntityOptions<V, T, W, L, I>): this;
101
249
  /** 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;
250
+ rect<W extends number, H extends number, T extends string = string, LW extends number = number, const L extends LinetypeValue = LinetypeDefinition, I extends string = string>(corner: Point, width: Positive<W>, height: Positive<H>, options?: EntityOptions<V, T, LW, L, I>): this;
251
+ circle<R extends number, T extends string = string, W extends number = number, const L extends LinetypeValue = LinetypeDefinition, I extends string = string>(center: Point, radius: Positive<R>, options?: EntityOptions<V, T, W, L, I>): this;
104
252
  /**
105
253
  * Arc from `startAngle` counterclockwise to `endAngle`. Angles are stored
106
254
  * from 0 up to 360, so -90 becomes 270, because AutoCAD's AUDIT rejects
@@ -110,11 +258,11 @@ export declare class DrawingBuilder<V extends DxfVersion = "R12"> {
110
258
  * full turn would be written with equal angles, which AutoCAD holds as an
111
259
  * arc of no length, so it throws: draw a circle instead.
112
260
  */
113
- arc<R extends number>(center: Point, radius: Positive<R>, startAngle: number, endAngle: number): this;
114
- text<H extends number = number>(at: Point, value: string, options?: TextOptions<H>): this;
261
+ arc<R extends number, T extends string = string, W extends number = number, const L extends LinetypeValue = LinetypeDefinition, I extends string = string>(center: Point, radius: Positive<R>, startAngle: number, endAngle: number, options?: EntityOptions<V, T, W, L, I>): this;
262
+ text<H extends number = number, T extends string = string, W extends number = number, const L extends LinetypeValue = LinetypeDefinition, I extends string = string>(at: Point, value: string, options?: TextOptions<H, V, T, W, L, I>): this;
115
263
  build(): Drawing;
116
264
  }
117
- export declare function createDrawing<V extends DxfVersion = "R12">(options?: DrawingOptions<V>): DrawingBuilder<V>;
265
+ export declare function createDrawing<V extends DxfVersion = "R2018", S extends number = number>(options?: DrawingOptions<V, S>): DrawingBuilder<V>;
118
266
  /** Counterclockwise sweep from start to end, in degrees, within [0, 360). */
119
267
  export declare function sweep(startAngle: number, endAngle: number): number;
120
268
  export {};