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 +87 -14
- package/README.md +141 -212
- package/dist/callsite.d.ts +47 -0
- package/dist/callsite.js +82 -0
- package/dist/drawing.d.ts +164 -16
- package/dist/drawing.js +339 -75
- package/dist/dxf-r2018.js +36 -15
- package/dist/dxf.d.ts +1 -1
- package/dist/format.d.ts +7 -0
- package/dist/format.js +9 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -0
- package/dist/json.d.ts +21 -0
- package/dist/json.js +244 -0
- package/dist/layers.d.ts +1 -1
- package/dist/layers.js +2 -3
- package/dist/linetypes.d.ts +27 -0
- package/dist/linetypes.js +76 -0
- package/dist/lint.d.ts +19 -5
- package/dist/lint.js +132 -7
- package/dist/paper.d.ts +33 -0
- package/dist/paper.js +58 -0
- package/dist/rules.d.ts +190 -9
- package/dist/rules.js +239 -22
- package/dist/svg.d.ts +11 -2
- package/dist/svg.js +143 -10
- package/package.json +6 -4
package/dist/rules.js
CHANGED
|
@@ -1,39 +1,85 @@
|
|
|
1
1
|
import { isTooLarge, written, writtenAngle } from "./format.js";
|
|
2
|
+
import { BUILT_IN_LINETYPES, builtInLinetype, LINEWEIGHTS, patternLength } from "./linetypes.js";
|
|
2
3
|
import { layerAuditKey, layerKey, rejectedNameCharacter, } from "./layers.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
// AutoCAD holds layer names and linetype names to the same rules, so each rule has a message
|
|
5
|
+
// for each kind of name.
|
|
6
|
+
const LAYER_NAMES = {
|
|
7
|
+
empty: "Layer names cannot be empty",
|
|
8
|
+
chars: 'Layer names cannot contain < > / \\ " : ; ? * | = ` ^ , or line breaks',
|
|
9
|
+
rejected: "Layer names cannot contain a character AutoCAD rejects or replaces in names, such as a zero-width space or the ohm sign U+2126. docs/layer-names.md in the tsdraft repository lists them",
|
|
10
|
+
edge: "Layer names cannot start or end with a space",
|
|
11
|
+
length: "Layer names cannot be longer than 255 characters",
|
|
12
|
+
};
|
|
13
|
+
const LINETYPE_NAMES = {
|
|
14
|
+
empty: "Linetype names cannot be empty",
|
|
15
|
+
chars: 'Linetype names cannot contain < > / \\ " : ; ? * | = ` ^ , or line breaks',
|
|
16
|
+
rejected: "Linetype names cannot contain a character AutoCAD rejects or replaces in names, such as a zero-width space or the ohm sign U+2126. docs/layer-names.md in the tsdraft repository lists them",
|
|
17
|
+
edge: "Linetype names cannot start or end with a space",
|
|
18
|
+
length: "Linetype names cannot be longer than 255 characters",
|
|
19
|
+
};
|
|
8
20
|
const NAME_R12_CHARS = 'R12 layer names can only use letters, digits, $, - and _. Use DXF version "R2018" for other characters';
|
|
9
21
|
const NAME_R12_LENGTH = 'R12 layer names cannot be longer than 31 characters. Use DXF version "R2018" for longer names';
|
|
10
22
|
const COLOR = "A layer color must be an AutoCAD Color Index integer from 1 to 255";
|
|
11
23
|
const POSITIVE = "This value must be greater than 0";
|
|
12
24
|
const WRITTEN_AS_ZERO = "This value is written as 0, because tsdraft writes 6 decimals. Use 0.000001 or more";
|
|
13
25
|
const TOO_LARGE = "This value must be under 1e100, because AutoCAD reads only two exponent digits from DXF and would take 1e+100 for 1e+10";
|
|
26
|
+
const PAPER_LENGTH = "The paper length must be greater than 0";
|
|
27
|
+
const RATIO = "The ratio must be greater than 0";
|
|
14
28
|
const ARC_LOST = "An arc of nearly a full turn cannot be written: its angles are the same once rounded to 6 decimals, so AutoCAD would hold an arc of no length. Draw a circle instead";
|
|
29
|
+
const TEXT_CONTROL = "Text must be a single line with no control characters";
|
|
15
30
|
const TEXT_PRIVATE_USE = "Text cannot contain the private-use characters U+E800 to U+E8FF, because AutoCAD replaces them when it reads DXF";
|
|
31
|
+
const R12_LINETYPES = 'R12 drawings have no linetypes but Continuous, and no linetype scale. Use DXF version "R2018" for linetypes';
|
|
32
|
+
const R12_LINEWEIGHTS = 'R12 drawings have no lineweights. Use DXF version "R2018" for lineweights';
|
|
33
|
+
const R12_ENTITY_STYLE = 'R12 entities take no linetype or lineweight of their own. Use DXF version "R2018" for an entity linetype or lineweight';
|
|
34
|
+
const R12_LAYER_STATES = 'tsdraft writes no layer states to R12 drawings. Use DXF version "R2018" for layers that are off, frozen, locked or not plotted';
|
|
35
|
+
const LINEWEIGHT = "A lineweight must be one of AutoCAD's, in millimetres: 0, 0.05, 0.09, 0.13, 0.15, 0.18, 0.2, 0.25, 0.3, 0.35, 0.4, 0.5, 0.53, 0.6, 0.7, 0.8, 0.9, 1, 1.06, 1.2, 1.4, 1.58, 2 or 2.11";
|
|
36
|
+
const LAYER_LINEWEIGHT = `${LINEWEIGHT}, or "Default"`;
|
|
37
|
+
const ENTITY_LINEWEIGHT = `${LINEWEIGHT}, or "ByLayer", "ByBlock" or "Default"`;
|
|
38
|
+
const FROZEN_CURRENT = 'Layer "0" cannot be frozen: it is the current layer, and AutoCAD does not freeze the current layer';
|
|
39
|
+
const LAYER_LINETYPE = "A layer's linetype cannot be ByLayer or ByBlock. AutoCAD's AUDIT reports a layer with either";
|
|
40
|
+
const BUILT_IN = "Continuous, ByLayer and ByBlock are in every drawing and cannot be defined. AutoCAD keeps its own and drops the definition";
|
|
41
|
+
const PATTERN_COUNT = "A linetype pattern has 2 to 12 lengths, or none for a solid line. AutoCAD's AUDIT reports a pattern of one length, and AutoCAD discards a drawing with a pattern of more than 12";
|
|
42
|
+
const PATTERN_WRITTEN_AS_ZERO = "A dash or gap of this length is written as 0, because tsdraft writes 6 decimals, and would draw as a dot. Use 0 for a dot, or a length of 0.000001 or more";
|
|
43
|
+
const PATTERN_TOTAL = "A linetype pattern's lengths, added up without their signs, must be under 1e100. The total is written too, and AutoCAD reads only two exponent digits from DXF";
|
|
44
|
+
const PATTERN_TYPE = "A linetype's pattern must be an array of finite numbers";
|
|
45
|
+
const DESCRIPTION_LENGTH = "A linetype description cannot be longer than 249 characters, because AutoCAD cuts it there when it reads DXF";
|
|
46
|
+
const ID_EMPTY = "An id cannot be empty";
|
|
47
|
+
const ID_CHARS = "An id can only use the letters A to Z and a to z, digits, ., - and _";
|
|
48
|
+
const ID_LENGTH = "An id cannot be longer than 64 characters";
|
|
16
49
|
const TEXT_PERCENT = 'AutoCAD reads "%%" in text as the start of a control code: it draws "%%d" as a degree sign and "%%%" as one percent sign. The text is stored as written';
|
|
17
50
|
/** Returns what is wrong with a layer name for a DXF version, or undefined when it is valid. */
|
|
18
51
|
export function layerNameProblem(name, version) {
|
|
52
|
+
const problem = nameProblem(name, LAYER_NAMES);
|
|
53
|
+
if (problem)
|
|
54
|
+
return problem;
|
|
55
|
+
if (version === "R12") {
|
|
56
|
+
if (!/^[A-Za-z0-9$_-]+$/.test(name))
|
|
57
|
+
return NAME_R12_CHARS;
|
|
58
|
+
if (name.length > 31)
|
|
59
|
+
return NAME_R12_LENGTH;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Returns what is wrong with a linetype name, or undefined when it is valid.
|
|
65
|
+
* The name may be one of the linetypes every drawing has.
|
|
66
|
+
*/
|
|
67
|
+
export function linetypeNameProblem(name) {
|
|
68
|
+
return nameProblem(name, LINETYPE_NAMES);
|
|
69
|
+
}
|
|
70
|
+
function nameProblem(name, messages) {
|
|
19
71
|
if (name.length === 0)
|
|
20
|
-
return
|
|
72
|
+
return messages.empty;
|
|
21
73
|
if (/[<>/\\":;?*|=`^,]/.test(name) || hasControl(name) || hasAstral(name))
|
|
22
|
-
return
|
|
74
|
+
return messages.chars;
|
|
23
75
|
const rejected = rejectedNameCharacter(name);
|
|
24
76
|
if (rejected !== undefined)
|
|
25
|
-
return `${
|
|
77
|
+
return `${messages.rejected}. This name has ${codePoint(rejected)}`;
|
|
26
78
|
// Only the ASCII space: AutoCAD takes a no-break space or any other at either end.
|
|
27
79
|
if (name.startsWith(" ") || name.endsWith(" "))
|
|
28
|
-
return
|
|
80
|
+
return messages.edge;
|
|
29
81
|
if (name.length > 255)
|
|
30
|
-
return
|
|
31
|
-
if (version === "R12") {
|
|
32
|
-
if (!/^[A-Za-z0-9$_-]+$/.test(name))
|
|
33
|
-
return NAME_R12_CHARS;
|
|
34
|
-
if (name.length > 31)
|
|
35
|
-
return NAME_R12_LENGTH;
|
|
36
|
-
}
|
|
82
|
+
return messages.length;
|
|
37
83
|
return undefined;
|
|
38
84
|
}
|
|
39
85
|
/**
|
|
@@ -43,20 +89,120 @@ export function layerNameProblem(name, version) {
|
|
|
43
89
|
* layers to AutoCAD and one duplicate name to AUDIT: `ΟΔΟΣ` and `οδος`,
|
|
44
90
|
* `straße` and `strasse`, a composed é and a decomposed one, and a name with
|
|
45
91
|
* and without a soft hyphen. `layerAuditKey` lists the kinds this covers,
|
|
46
|
-
* which are
|
|
47
|
-
*
|
|
48
|
-
*
|
|
92
|
+
* which are not all that AUDIT reports. Names that are one layer to AutoCAD
|
|
93
|
+
* are not this function's concern. The rule compares two names, so it has no
|
|
94
|
+
* type.
|
|
49
95
|
*
|
|
50
96
|
* @param name - The name of the layer being checked.
|
|
51
97
|
* @param other - The name of another layer in the drawing.
|
|
98
|
+
* @param kind - Whether the names are of layers or of linetypes, which AutoCAD compares alike.
|
|
52
99
|
* @returns The problem, or undefined.
|
|
53
100
|
*/
|
|
54
|
-
export function layerTwinProblem(name, other) {
|
|
101
|
+
export function layerTwinProblem(name, other, kind = "layer") {
|
|
55
102
|
if (layerKey(name) === layerKey(other) || layerAuditKey(name) !== layerAuditKey(other)) {
|
|
56
103
|
return undefined;
|
|
57
104
|
}
|
|
58
|
-
return `AutoCAD keeps this
|
|
105
|
+
return `AutoCAD keeps this ${kind} and ${JSON.stringify(other)} as two ${kind}s, but its AUDIT reports them as one duplicate name`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Returns what is wrong with a linetype definition, or undefined when it is
|
|
109
|
+
* valid. It takes `unknown`s so that `lint` can report a drawing built by hand
|
|
110
|
+
* with a field of the wrong type.
|
|
111
|
+
*/
|
|
112
|
+
export function linetypeProblem(name, description, pattern) {
|
|
113
|
+
if (typeof name !== "string")
|
|
114
|
+
return "A linetype's name must be a string";
|
|
115
|
+
const nameError = linetypeNameProblem(name);
|
|
116
|
+
if (nameError)
|
|
117
|
+
return nameError;
|
|
118
|
+
if (builtInLinetype(name))
|
|
119
|
+
return BUILT_IN;
|
|
120
|
+
if (typeof description !== "string")
|
|
121
|
+
return "A linetype's description must be a string";
|
|
122
|
+
const descriptionError = textProblem(description);
|
|
123
|
+
if (descriptionError)
|
|
124
|
+
return `its description: ${descriptionError}`;
|
|
125
|
+
if (description.length > 249)
|
|
126
|
+
return DESCRIPTION_LENGTH;
|
|
127
|
+
if (!Array.isArray(pattern))
|
|
128
|
+
return PATTERN_TYPE;
|
|
129
|
+
if (pattern.length === 1 || pattern.length > 12)
|
|
130
|
+
return PATTERN_COUNT;
|
|
131
|
+
for (const length of pattern) {
|
|
132
|
+
if (typeof length !== "number" || !Number.isFinite(length))
|
|
133
|
+
return PATTERN_TYPE;
|
|
134
|
+
if (length !== 0 && written(length) === 0)
|
|
135
|
+
return `its pattern: ${PATTERN_WRITTEN_AS_ZERO}`;
|
|
136
|
+
const problem = magnitudeProblem(length);
|
|
137
|
+
if (problem)
|
|
138
|
+
return `its pattern: ${problem}`;
|
|
139
|
+
}
|
|
140
|
+
// The writers write the total of the lengths as they write each one, rounded.
|
|
141
|
+
if (isTooLarge(patternLength(pattern.map(written)))) {
|
|
142
|
+
return `its pattern: ${PATTERN_TOTAL}`;
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Returns what is wrong with a layer or an entity that names a linetype the
|
|
148
|
+
* drawing does not have. The rule depends on the drawing, so it has no type.
|
|
149
|
+
*/
|
|
150
|
+
export function undefinedLinetypeProblem(name) {
|
|
151
|
+
return `Linetype ${JSON.stringify(name)} is not defined, and AutoCAD discards a drawing that uses a linetype it does not have`;
|
|
152
|
+
}
|
|
153
|
+
/** Returns what is wrong with the linetype a layer names, or undefined when it can be a layer's. */
|
|
154
|
+
export function layerLinetypeProblem(name) {
|
|
155
|
+
const builtInName = builtInLinetype(name);
|
|
156
|
+
return builtInName === "ByLayer" || builtInName === "ByBlock" ? LAYER_LINETYPE : undefined;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Returns what is wrong with a lineweight, or undefined when it is valid. A
|
|
160
|
+
* layer's lineweight may also be "Default", and an entity's "ByLayer",
|
|
161
|
+
* "ByBlock" or "Default". AutoCAD discards a drawing with an entity of any
|
|
162
|
+
* other lineweight, and gives a layer of another lineweight the default.
|
|
163
|
+
*
|
|
164
|
+
* @param value - The lineweight, in millimetres or by name.
|
|
165
|
+
* @param of - Whose lineweight it is.
|
|
166
|
+
* @returns The problem, or undefined.
|
|
167
|
+
*/
|
|
168
|
+
export function lineweightProblem(value, of) {
|
|
169
|
+
if (value === "Default" || (of === "entity" && (value === "ByLayer" || value === "ByBlock"))) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
if (typeof value === "number" && LINEWEIGHTS.includes(value))
|
|
173
|
+
return undefined;
|
|
174
|
+
return of === "layer" ? LAYER_LINEWEIGHT : ENTITY_LINEWEIGHT;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Returns what is wrong with a layer's states, or undefined when AutoCAD takes
|
|
178
|
+
* them. tsdraft writes layer "0" as the current layer, and AutoCAD's AUDIT
|
|
179
|
+
* reports a drawing whose current layer is frozen.
|
|
180
|
+
*/
|
|
181
|
+
export function layerStateProblem(name, frozen) {
|
|
182
|
+
return frozen && name === "0" ? FROZEN_CURRENT : undefined;
|
|
183
|
+
}
|
|
184
|
+
/** Returns what is wrong with the value of a layer's `off`, `frozen`, `locked` or `plot`. */
|
|
185
|
+
export function layerFlagProblem(state, value) {
|
|
186
|
+
return value === undefined || typeof value === "boolean"
|
|
187
|
+
? undefined
|
|
188
|
+
: `${state} must be true or false`;
|
|
59
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Returns what is wrong with the global linetype scale, or undefined when it is
|
|
192
|
+
* valid. AutoCAD reads a scale of 0 or less, or one written as 0, as 1.
|
|
193
|
+
*/
|
|
194
|
+
export function linetypeScaleProblem(value) {
|
|
195
|
+
if (!Number.isFinite(value))
|
|
196
|
+
return "This value must be a finite number";
|
|
197
|
+
return sizeProblem(value);
|
|
198
|
+
}
|
|
199
|
+
/** The R12 message for a feature, for `lint` and the builder. */
|
|
200
|
+
export const R12_ONLY = {
|
|
201
|
+
linetypes: R12_LINETYPES,
|
|
202
|
+
lineweights: R12_LINEWEIGHTS,
|
|
203
|
+
layerStates: R12_LAYER_STATES,
|
|
204
|
+
entityStyle: R12_ENTITY_STYLE,
|
|
205
|
+
};
|
|
60
206
|
/** Returns what is wrong with a layer color, or undefined when it is valid. */
|
|
61
207
|
export function layerColorProblem(color) {
|
|
62
208
|
return Number.isInteger(color) && color >= 1 && color <= 255 ? undefined : COLOR;
|
|
@@ -76,6 +222,29 @@ export function sizeProblem(value) {
|
|
|
76
222
|
return POSITIVE;
|
|
77
223
|
return written(value) === 0 ? WRITTEN_AS_ZERO : magnitudeProblem(value);
|
|
78
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Returns what is wrong with a paper length, the ratio it plots at, or the
|
|
227
|
+
* length in drawing units they give, or undefined when the result can be
|
|
228
|
+
* written. The result is a size, so it is held to the limits of `sizeProblem`.
|
|
229
|
+
*
|
|
230
|
+
* @param length - The length on paper, which must be a finite number.
|
|
231
|
+
* @param ratio - The scale ratio, which must be a finite number.
|
|
232
|
+
* @param result - The length in drawing units.
|
|
233
|
+
* @returns The problem, or undefined.
|
|
234
|
+
*/
|
|
235
|
+
export function paperLengthProblem(length, ratio, result) {
|
|
236
|
+
if (!(length > 0))
|
|
237
|
+
return PAPER_LENGTH;
|
|
238
|
+
if (!(ratio > 0))
|
|
239
|
+
return RATIO;
|
|
240
|
+
if (written(result) === 0) {
|
|
241
|
+
return `The result, ${result} drawing units, is written as 0, because tsdraft writes 6 decimals. Use a longer paper length or a larger ratio`;
|
|
242
|
+
}
|
|
243
|
+
if (isTooLarge(result)) {
|
|
244
|
+
return "The result is 1e100 drawing units or more, and AutoCAD reads only two exponent digits from DXF. Use a shorter paper length or a smaller ratio";
|
|
245
|
+
}
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
79
248
|
/**
|
|
80
249
|
* Returns what is wrong with the size of a number a drawing holds, or
|
|
81
250
|
* undefined when AutoCAD can read it. This goes for every number that is
|
|
@@ -117,10 +286,58 @@ export function arcProblem(startAngle, endAngle) {
|
|
|
117
286
|
export function arcIsWrittenAsPoint(startAngle, endAngle) {
|
|
118
287
|
return roundsToOneAngle(startAngle, endAngle) && isShortArc(startAngle, endAngle);
|
|
119
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* Returns what is wrong with an entity id, or undefined when it is valid. It takes `unknown` so
|
|
291
|
+
* that `lint` can report a drawing built by hand with an id of the wrong type.
|
|
292
|
+
*/
|
|
293
|
+
export function idProblem(id) {
|
|
294
|
+
if (typeof id !== "string")
|
|
295
|
+
return "An id must be a string";
|
|
296
|
+
if (id.length === 0)
|
|
297
|
+
return ID_EMPTY;
|
|
298
|
+
if (!/^[A-Za-z0-9._-]+$/.test(id))
|
|
299
|
+
return ID_CHARS;
|
|
300
|
+
return id.length > 64 ? ID_LENGTH : undefined;
|
|
301
|
+
}
|
|
302
|
+
/** The form two ids are compared in: `Door-1` and `door-1` are one id. */
|
|
303
|
+
export function idKey(id) {
|
|
304
|
+
return id.toLowerCase();
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Returns what is wrong with a second entity that has an id the drawing already has. The rule
|
|
308
|
+
* depends on the drawing, so it has no type.
|
|
309
|
+
*
|
|
310
|
+
* @param id - The id of the second entity.
|
|
311
|
+
* @param first - The id as the first entity spells it.
|
|
312
|
+
*/
|
|
313
|
+
export function duplicateIdProblem(id, first) {
|
|
314
|
+
const spelled = first === id ? "" : `, as ${JSON.stringify(first)}`;
|
|
315
|
+
return `The id ${JSON.stringify(id)} is already used${spelled}. An id names one entity, and ids are compared without regard to case`;
|
|
316
|
+
}
|
|
317
|
+
/** Returns what is wrong with a drawing's format version, or undefined when tsdraft reads it. */
|
|
318
|
+
export function formatProblem(format) {
|
|
319
|
+
// Negative zero is refused: it would be written as -0, and so change the drawing's JSON.
|
|
320
|
+
if (Object.is(format, 0))
|
|
321
|
+
return undefined;
|
|
322
|
+
return format === undefined
|
|
323
|
+
? "The drawing has no format version. A Drawing has format: 0"
|
|
324
|
+
: `Unknown Drawing format ${shown(format)}. This version of tsdraft reads format 0`;
|
|
325
|
+
}
|
|
326
|
+
/** A value of any type for a message: a string quoted, a number as it is, anything else by kind. */
|
|
327
|
+
export function shown(value) {
|
|
328
|
+
if (typeof value === "string")
|
|
329
|
+
return JSON.stringify(value);
|
|
330
|
+
if (Object.is(value, -0))
|
|
331
|
+
return "-0";
|
|
332
|
+
if (typeof value === "number" || typeof value === "boolean" || value == null) {
|
|
333
|
+
return String(value);
|
|
334
|
+
}
|
|
335
|
+
return Array.isArray(value) ? "an array" : `a value of type ${typeof value}`;
|
|
336
|
+
}
|
|
120
337
|
/** Returns what is wrong with a single-line text value, or undefined when it is valid. */
|
|
121
338
|
export function textProblem(value) {
|
|
122
339
|
if (hasControl(value))
|
|
123
|
-
return
|
|
340
|
+
return TEXT_CONTROL;
|
|
124
341
|
if (hasAstral(value)) {
|
|
125
342
|
return "Text cannot contain emoji or other characters above U+FFFF, because AutoCAD cannot read them from DXF";
|
|
126
343
|
}
|
package/dist/svg.d.ts
CHANGED
|
@@ -2,8 +2,17 @@ import { type Drawing } from "./drawing.ts";
|
|
|
2
2
|
export interface SvgOptions {
|
|
3
3
|
/** Margin around the drawing's extents, in drawing units. Defaults to 2% of the larger side. */
|
|
4
4
|
readonly margin?: number;
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Stroke width in screen pixels of a line of the default lineweight, 0.25 mm. Other
|
|
7
|
+
* lineweights are drawn in proportion. It stays constant when the SVG scales.
|
|
8
|
+
*/
|
|
6
9
|
readonly strokeWidth?: number;
|
|
7
10
|
}
|
|
8
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Renders a preview SVG. Model Y points up, so the drawing group flips Y. Entities on layers
|
|
13
|
+
* that are off or frozen are left out. Dashed lines are cut into their dashes, scaled by the
|
|
14
|
+
* drawing's linetype scale, and each line is as wide as its lineweight. Each entity's element
|
|
15
|
+
* has its layer in `data-layer`, its index in `drawing.entities` in `data-entity`, and its `id`,
|
|
16
|
+
* when it has one, in `data-id`.
|
|
17
|
+
*/
|
|
9
18
|
export declare function toSVG(drawing: Drawing, options?: SvgOptions): string;
|
package/dist/svg.js
CHANGED
|
@@ -2,6 +2,7 @@ import { sweep } from "./drawing.js";
|
|
|
2
2
|
import { extents } from "./extents.js";
|
|
3
3
|
import { fmt } from "./format.js";
|
|
4
4
|
import { layerKey } from "./layers.js";
|
|
5
|
+
import { builtInLinetype, lineweightCode } from "./linetypes.js";
|
|
5
6
|
/** Screen colors for the standard ACI colors 1 to 7. Other indexes draw in currentColor. */
|
|
6
7
|
const ACI = {
|
|
7
8
|
1: "#ff0000",
|
|
@@ -12,20 +13,53 @@ const ACI = {
|
|
|
12
13
|
6: "#ff00ff",
|
|
13
14
|
7: "currentColor",
|
|
14
15
|
};
|
|
15
|
-
/**
|
|
16
|
+
/** AutoCAD draws a lineweight of "Default" at its LWDEFAULT, 0.25 mm. */
|
|
17
|
+
const DEFAULT_LINEWEIGHT = 25;
|
|
18
|
+
/**
|
|
19
|
+
* The most dashes one entity is cut into. AutoCAD draws a line solid when its pattern is too
|
|
20
|
+
* fine to show, and so does the preview.
|
|
21
|
+
*/
|
|
22
|
+
const MAX_DASHES = 10000;
|
|
23
|
+
/**
|
|
24
|
+
* Renders a preview SVG. Model Y points up, so the drawing group flips Y. Entities on layers
|
|
25
|
+
* that are off or frozen are left out. Dashed lines are cut into their dashes, scaled by the
|
|
26
|
+
* drawing's linetype scale, and each line is as wide as its lineweight. Each entity's element
|
|
27
|
+
* has its layer in `data-layer`, its index in `drawing.entities` in `data-entity`, and its `id`,
|
|
28
|
+
* when it has one, in `data-id`.
|
|
29
|
+
*/
|
|
16
30
|
export function toSVG(drawing, options = {}) {
|
|
17
|
-
|
|
31
|
+
// Keyed as AutoCAD looks layers up, so an entity on `wall` takes the color of layer `WALL`.
|
|
32
|
+
const layers = new Map(drawing.layers.map((l) => [layerKey(l.name), l]));
|
|
33
|
+
// Each entity keeps its index in `drawing.entities`, for its element's data-entity.
|
|
34
|
+
const shown = [...drawing.entities.entries()].filter(([, e]) => {
|
|
35
|
+
const layer = layers.get(layerKey(e.layer));
|
|
36
|
+
return !layer?.off && !layer?.frozen;
|
|
37
|
+
});
|
|
38
|
+
const [minX, minY, maxX, maxY] = extents(shown.map(([, e]) => e));
|
|
18
39
|
const margin = options.margin ?? Math.max(maxX - minX, maxY - minY, 1) * 0.02;
|
|
19
40
|
const x = minX - margin;
|
|
20
41
|
const y = -(maxY + margin);
|
|
21
42
|
const w = maxX - minX + margin * 2;
|
|
22
43
|
const h = maxY - minY + margin * 2;
|
|
23
|
-
|
|
24
|
-
const
|
|
44
|
+
const patterns = new Map((drawing.linetypes ?? []).map((t) => [layerKey(t.name), alternating(t.pattern)]));
|
|
45
|
+
const scale = drawing.linetypeScale ?? 1;
|
|
25
46
|
const stroke = options.strokeWidth ?? 1;
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
.
|
|
47
|
+
const pen = (e) => {
|
|
48
|
+
const layer = layers.get(layerKey(e.layer));
|
|
49
|
+
const byLayer = e.linetype === undefined || builtInLinetype(e.linetype) === "ByLayer";
|
|
50
|
+
const linetype = byLayer ? layer?.linetype : e.linetype;
|
|
51
|
+
const pattern = linetype === undefined ? undefined : patterns.get(layerKey(linetype));
|
|
52
|
+
const weight = e.lineweight === undefined || e.lineweight === "ByLayer"
|
|
53
|
+
? (layer?.lineweight ?? "Default")
|
|
54
|
+
: e.lineweight;
|
|
55
|
+
const code = lineweightCode(weight);
|
|
56
|
+
return {
|
|
57
|
+
color: ACI[layer?.color ?? 7] ?? "currentColor",
|
|
58
|
+
dashes: pattern?.map((length) => length * scale),
|
|
59
|
+
lineweight: code < 0 ? DEFAULT_LINEWEIGHT : code,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
const body = shown.map(([i, e]) => entityToSvg(e, i, pen(e), stroke)).join("\n ");
|
|
29
63
|
return [
|
|
30
64
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${fmt(x)} ${fmt(y)} ${fmt(w)} ${fmt(h)}">`,
|
|
31
65
|
` <g transform="scale(1 -1)" fill="none" stroke-width="${fmt(stroke)}" vector-effect="non-scaling-stroke">`,
|
|
@@ -35,8 +69,20 @@ export function toSVG(drawing, options = {}) {
|
|
|
35
69
|
``,
|
|
36
70
|
].join("\n");
|
|
37
71
|
}
|
|
38
|
-
function entityToSvg(e,
|
|
39
|
-
|
|
72
|
+
function entityToSvg(e, index, pen, stroke) {
|
|
73
|
+
// A lineweight of 0 draws as the thinnest line, as in AutoCAD.
|
|
74
|
+
const width = pen.lineweight === DEFAULT_LINEWEIGHT
|
|
75
|
+
? ""
|
|
76
|
+
: ` stroke-width="${fmt((stroke * Math.max(pen.lineweight, 5)) / DEFAULT_LINEWEIGHT)}"`;
|
|
77
|
+
const mark = `data-layer="${esc(e.layer)}" data-entity="${index}"${e.id === undefined ? "" : ` data-id="${esc(e.id)}"`}`;
|
|
78
|
+
const s = `stroke="${pen.color}"${width} vector-effect="non-scaling-stroke" ${mark}`;
|
|
79
|
+
if (e.kind !== "text" && pen.dashes) {
|
|
80
|
+
const d = dashedPath(e, pen.dashes);
|
|
81
|
+
// A pattern with a dot draws it as a round cap on a path of no length.
|
|
82
|
+
const cap = pen.dashes.includes(0) ? ` stroke-linecap="round"` : "";
|
|
83
|
+
if (d !== undefined)
|
|
84
|
+
return `<path d="${d}"${cap} ${s}/>`;
|
|
85
|
+
}
|
|
40
86
|
switch (e.kind) {
|
|
41
87
|
case "line":
|
|
42
88
|
return `<line x1="${fmt(e.from[0])}" y1="${fmt(e.from[1])}" x2="${fmt(e.to[0])}" y2="${fmt(e.to[1])}" ${s}/>`;
|
|
@@ -65,9 +111,96 @@ function entityToSvg(e, color) {
|
|
|
65
111
|
case "text": {
|
|
66
112
|
// Undo the group's Y flip so the text reads upright, then apply CAD rotation.
|
|
67
113
|
const t = `translate(${fmt(e.at[0])} ${fmt(e.at[1])}) scale(1 -1)${e.rotation ? ` rotate(${fmt(-e.rotation)})` : ""}`;
|
|
68
|
-
return `<text transform="${t}" font-size="${fmt(e.height)}" font-family="monospace" fill="${color}" stroke="none"
|
|
114
|
+
return `<text transform="${t}" font-size="${fmt(e.height)}" font-family="monospace" fill="${pen.color}" stroke="none" ${mark}>${esc(e.value)}</text>`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A linetype pattern as SVG thinks of dashes: dash, gap, dash, gap, starting with a dash and
|
|
120
|
+
* of even length. Dashes or gaps side by side are joined, and a pattern that starts with a gap
|
|
121
|
+
* is turned to start with the dash after it. Undefined for a pattern with no gap, with no dash
|
|
122
|
+
* or dot, or with no length, and the caller then draws the entity solid.
|
|
123
|
+
*/
|
|
124
|
+
function alternating(pattern) {
|
|
125
|
+
const first = pattern.findIndex((length) => length >= 0);
|
|
126
|
+
if (first < 0)
|
|
127
|
+
return undefined;
|
|
128
|
+
const runs = [];
|
|
129
|
+
// A dot is a dash of no length. Runs alternate dash, gap, so an odd index is a gap.
|
|
130
|
+
for (const length of [...pattern.slice(first), ...pattern.slice(0, first)]) {
|
|
131
|
+
if (runs.length % 2 === (length < 0 ? 0 : 1))
|
|
132
|
+
runs[runs.length - 1] += Math.abs(length);
|
|
133
|
+
else
|
|
134
|
+
runs.push(Math.abs(length));
|
|
135
|
+
}
|
|
136
|
+
if (runs.length < 2)
|
|
137
|
+
return undefined;
|
|
138
|
+
if (runs.length % 2 === 1)
|
|
139
|
+
runs[0] += runs.pop();
|
|
140
|
+
return runs.reduce((a, b) => a + b, 0) > 0 ? runs : undefined;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* An entity cut into the dashes of its pattern, as path data, or undefined when the pattern
|
|
144
|
+
* is too fine for the entity and it draws solid. A polyline starts the pattern again at each
|
|
145
|
+
* vertex, as AutoCAD does unless told otherwise.
|
|
146
|
+
*/
|
|
147
|
+
function dashedPath(e, dashes) {
|
|
148
|
+
const total = dashes.reduce((a, b) => a + b, 0);
|
|
149
|
+
const out = [];
|
|
150
|
+
/** Cuts a stretch of the given length, and draws each dash from `from` to `to` along it. */
|
|
151
|
+
const cut = (length, draw) => {
|
|
152
|
+
if (length / total > MAX_DASHES)
|
|
153
|
+
return false;
|
|
154
|
+
let at = 0;
|
|
155
|
+
for (let i = 0; at < length; i = (i + 1) % dashes.length) {
|
|
156
|
+
const end = Math.min(at + dashes[i], length);
|
|
157
|
+
if (i % 2 === 0)
|
|
158
|
+
out.push(draw(at, end));
|
|
159
|
+
at += dashes[i];
|
|
69
160
|
}
|
|
161
|
+
return true;
|
|
162
|
+
};
|
|
163
|
+
const straight = (a, b) => {
|
|
164
|
+
const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
165
|
+
const along = (t) => `${fmt(a[0] + ((b[0] - a[0]) * t) / length)} ${fmt(a[1] + ((b[1] - a[1]) * t) / length)}`;
|
|
166
|
+
return length === 0 || cut(length, (from, to) => `M ${along(from)} L ${along(to)}`);
|
|
167
|
+
};
|
|
168
|
+
const round = (center, radius, start, turn) => {
|
|
169
|
+
const along = (t) => {
|
|
170
|
+
const angle = rad(start) + t / radius;
|
|
171
|
+
return `${fmt(center[0] + radius * Math.cos(angle))} ${fmt(center[1] + radius * Math.sin(angle))}`;
|
|
172
|
+
};
|
|
173
|
+
return cut(radius * rad(turn), (from, to) => {
|
|
174
|
+
// SVG draws no arc between two equal points, so a dot is a line of no length.
|
|
175
|
+
if (to === from)
|
|
176
|
+
return `M ${along(from)} L ${along(to)}`;
|
|
177
|
+
const arc = `A ${fmt(radius)} ${fmt(radius)} 0`;
|
|
178
|
+
// A dash of a full turn ends where it starts, so it is drawn as two halves.
|
|
179
|
+
if ((to - from) / radius > Math.PI && along(from) === along(to)) {
|
|
180
|
+
return `M ${along(from)} ${arc} 0 1 ${along((from + to) / 2)} ${arc} 0 1 ${along(to)}`;
|
|
181
|
+
}
|
|
182
|
+
const large = (to - from) / radius > Math.PI ? 1 : 0;
|
|
183
|
+
return `M ${along(from)} ${arc} ${large} 1 ${along(to)}`;
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
let ok;
|
|
187
|
+
switch (e.kind) {
|
|
188
|
+
case "line":
|
|
189
|
+
ok = straight(e.from, e.to);
|
|
190
|
+
break;
|
|
191
|
+
case "polyline": {
|
|
192
|
+
const points = e.closed ? [...e.points, e.points[0]] : e.points;
|
|
193
|
+
ok = points.slice(1).every((p, i) => straight(points[i], p));
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case "circle":
|
|
197
|
+
ok = round(e.center, e.radius, 0, 360);
|
|
198
|
+
break;
|
|
199
|
+
case "arc":
|
|
200
|
+
ok = round(e.center, e.radius, e.startAngle, sweep(e.startAngle, e.endAngle));
|
|
201
|
+
break;
|
|
70
202
|
}
|
|
203
|
+
return ok && out.length > 0 ? out.join(" ") : undefined;
|
|
71
204
|
}
|
|
72
205
|
function rad(degrees) {
|
|
73
206
|
return (degrees * Math.PI) / 180;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tsdraft",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "2D CAD drawings as TypeScript: write the drawing in code, export DXF and SVG.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"2d",
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
"cad",
|
|
10
10
|
"drafting",
|
|
11
11
|
"drawing",
|
|
12
|
-
"dxf"
|
|
12
|
+
"dxf",
|
|
13
|
+
"svg",
|
|
14
|
+
"typescript"
|
|
13
15
|
],
|
|
14
16
|
"homepage": "https://github.com/Surfside-Software/tsdraft#readme",
|
|
15
17
|
"bugs": {
|
|
@@ -45,8 +47,8 @@
|
|
|
45
47
|
"fmt:check": "oxfmt --check",
|
|
46
48
|
"test": "node --test test/*.test.ts",
|
|
47
49
|
"test:package": "node scripts/package-smoke.ts",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
+
"example": "node examples/site-plan.ts && node examples/outdoor-kitchen.ts",
|
|
51
|
+
"readme:drawing": "node scripts/readme-drawing.ts",
|
|
50
52
|
"verify": "pnpm fmt:check && pnpm lint && pnpm check && pnpm test && pnpm test:package"
|
|
51
53
|
},
|
|
52
54
|
"devDependencies": {
|