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/dist/rules.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const NAME_EMPTY = "Layer names cannot be empty";
|
|
2
|
+
const NAME_CHARS = 'Layer names cannot contain < > / \\ " : ; ? * | = ` ^ , or line breaks';
|
|
3
|
+
const NAME_EDGE = "Layer names cannot start or end with a space";
|
|
4
|
+
const NAME_LENGTH = "Layer names cannot be longer than 255 characters";
|
|
5
|
+
const NAME_R12_CHARS = 'R12 layer names can only use letters, digits, $, - and _. Use DXF version "R2018" for other characters';
|
|
6
|
+
const NAME_R12_LENGTH = 'R12 layer names cannot be longer than 31 characters. Use DXF version "R2018" for longer names';
|
|
7
|
+
const COLOR = "A layer color must be an AutoCAD Color Index integer from 1 to 255";
|
|
8
|
+
const POSITIVE = "This value must be greater than 0";
|
|
9
|
+
/** Returns what is wrong with a layer name for a DXF version, or undefined when it is valid. */
|
|
10
|
+
export function layerNameProblem(name, version) {
|
|
11
|
+
if (name.length === 0)
|
|
12
|
+
return NAME_EMPTY;
|
|
13
|
+
if (/[<>/\\":;?*|=`^,]/.test(name) || hasControl(name) || hasAstral(name))
|
|
14
|
+
return NAME_CHARS;
|
|
15
|
+
if (name !== name.trim())
|
|
16
|
+
return NAME_EDGE;
|
|
17
|
+
if (name.length > 255)
|
|
18
|
+
return NAME_LENGTH;
|
|
19
|
+
if (version === "R12") {
|
|
20
|
+
if (!/^[A-Za-z0-9$_-]+$/.test(name))
|
|
21
|
+
return NAME_R12_CHARS;
|
|
22
|
+
if (name.length > 31)
|
|
23
|
+
return NAME_R12_LENGTH;
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
/** Returns what is wrong with a layer color, or undefined when it is valid. */
|
|
28
|
+
export function layerColorProblem(color) {
|
|
29
|
+
return Number.isInteger(color) && color >= 1 && color <= 255 ? undefined : COLOR;
|
|
30
|
+
}
|
|
31
|
+
/** Returns what is wrong with a single-line text value, or undefined when it is valid. */
|
|
32
|
+
export function textProblem(value) {
|
|
33
|
+
if (hasControl(value))
|
|
34
|
+
return "Text must be a single line with no control characters";
|
|
35
|
+
if (hasAstral(value)) {
|
|
36
|
+
return "Text cannot contain emoji or other characters above U+FFFF, because AutoCAD cannot read them from DXF";
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
function hasControl(value) {
|
|
41
|
+
for (let i = 0; i < value.length; i++) {
|
|
42
|
+
const code = value.charCodeAt(i);
|
|
43
|
+
if (code < 0x20 || code === 0x7f)
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
/** True when the string has a character above U+FFFF, which JavaScript stores as a surrogate pair. */
|
|
49
|
+
function hasAstral(value) {
|
|
50
|
+
for (let i = 0; i < value.length; i++) {
|
|
51
|
+
const code = value.charCodeAt(i);
|
|
52
|
+
if (code >= 0xd800 && code <= 0xdfff)
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
package/dist/svg.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Drawing } from "./drawing.ts";
|
|
2
|
+
export interface SvgOptions {
|
|
3
|
+
/** Margin around the drawing's extents, in drawing units. Defaults to 2% of the larger side. */
|
|
4
|
+
readonly margin?: number;
|
|
5
|
+
/** Stroke width in screen pixels; it stays constant when the SVG scales. */
|
|
6
|
+
readonly strokeWidth?: number;
|
|
7
|
+
}
|
|
8
|
+
/** Renders a preview SVG. Model Y points up, so the drawing group flips Y. */
|
|
9
|
+
export declare function toSVG(drawing: Drawing, options?: SvgOptions): string;
|
package/dist/svg.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { sweep } from "./drawing.js";
|
|
2
|
+
import { extents } from "./extents.js";
|
|
3
|
+
import { fmt } from "./format.js";
|
|
4
|
+
/** Screen colors for the standard ACI colors 1 to 7. Other indexes draw in currentColor. */
|
|
5
|
+
const ACI = {
|
|
6
|
+
1: "#ff0000",
|
|
7
|
+
2: "#ffff00",
|
|
8
|
+
3: "#00ff00",
|
|
9
|
+
4: "#00ffff",
|
|
10
|
+
5: "#0000ff",
|
|
11
|
+
6: "#ff00ff",
|
|
12
|
+
7: "currentColor",
|
|
13
|
+
};
|
|
14
|
+
/** Renders a preview SVG. Model Y points up, so the drawing group flips Y. */
|
|
15
|
+
export function toSVG(drawing, options = {}) {
|
|
16
|
+
const [minX, minY, maxX, maxY] = extents(drawing.entities);
|
|
17
|
+
const margin = options.margin ?? Math.max(maxX - minX, maxY - minY, 1) * 0.02;
|
|
18
|
+
const x = minX - margin;
|
|
19
|
+
const y = -(maxY + margin);
|
|
20
|
+
const w = maxX - minX + margin * 2;
|
|
21
|
+
const h = maxY - minY + margin * 2;
|
|
22
|
+
const colors = new Map(drawing.layers.map((l) => [l.name, ACI[l.color] ?? "currentColor"]));
|
|
23
|
+
const stroke = options.strokeWidth ?? 1;
|
|
24
|
+
const body = drawing.entities
|
|
25
|
+
.map((e) => entityToSvg(e, colors.get(e.layer) ?? "currentColor"))
|
|
26
|
+
.join("\n ");
|
|
27
|
+
return [
|
|
28
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${fmt(x)} ${fmt(y)} ${fmt(w)} ${fmt(h)}">`,
|
|
29
|
+
` <g transform="scale(1 -1)" fill="none" stroke-width="${fmt(stroke)}" vector-effect="non-scaling-stroke">`,
|
|
30
|
+
` ${body}`,
|
|
31
|
+
` </g>`,
|
|
32
|
+
`</svg>`,
|
|
33
|
+
``,
|
|
34
|
+
].join("\n");
|
|
35
|
+
}
|
|
36
|
+
function entityToSvg(e, color) {
|
|
37
|
+
const s = `stroke="${color}" vector-effect="non-scaling-stroke" data-layer="${esc(e.layer)}"`;
|
|
38
|
+
switch (e.kind) {
|
|
39
|
+
case "line":
|
|
40
|
+
return `<line x1="${fmt(e.from[0])}" y1="${fmt(e.from[1])}" x2="${fmt(e.to[0])}" y2="${fmt(e.to[1])}" ${s}/>`;
|
|
41
|
+
case "polyline": {
|
|
42
|
+
const tag = e.closed ? "polygon" : "polyline";
|
|
43
|
+
const points = e.points.map(([px, py]) => `${fmt(px)},${fmt(py)}`).join(" ");
|
|
44
|
+
return `<${tag} points="${points}" ${s}/>`;
|
|
45
|
+
}
|
|
46
|
+
case "circle":
|
|
47
|
+
return `<circle cx="${fmt(e.center[0])}" cy="${fmt(e.center[1])}" r="${fmt(e.radius)}" ${s}/>`;
|
|
48
|
+
case "arc": {
|
|
49
|
+
const [cx, cy] = e.center;
|
|
50
|
+
const a0 = rad(e.startAngle);
|
|
51
|
+
const a1 = rad(e.endAngle);
|
|
52
|
+
const large = sweep(e.startAngle, e.endAngle) > 180 ? 1 : 0;
|
|
53
|
+
const start = `${fmt(cx + e.radius * Math.cos(a0))} ${fmt(cy + e.radius * Math.sin(a0))}`;
|
|
54
|
+
const end = `${fmt(cx + e.radius * Math.cos(a1))} ${fmt(cy + e.radius * Math.sin(a1))}`;
|
|
55
|
+
// sweep-flag 1 is the positive-angle direction in local (Y-up) coordinates: counterclockwise.
|
|
56
|
+
return `<path d="M ${start} A ${fmt(e.radius)} ${fmt(e.radius)} 0 ${large} 1 ${end}" ${s}/>`;
|
|
57
|
+
}
|
|
58
|
+
case "text": {
|
|
59
|
+
// Undo the group's Y flip so the text reads upright, then apply CAD rotation.
|
|
60
|
+
const t = `translate(${fmt(e.at[0])} ${fmt(e.at[1])}) scale(1 -1)${e.rotation ? ` rotate(${fmt(-e.rotation)})` : ""}`;
|
|
61
|
+
return `<text transform="${t}" font-size="${fmt(e.height)}" font-family="monospace" fill="${color}" stroke="none" data-layer="${esc(e.layer)}">${esc(e.value)}</text>`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function rad(degrees) {
|
|
66
|
+
return (degrees * Math.PI) / 180;
|
|
67
|
+
}
|
|
68
|
+
function esc(value) {
|
|
69
|
+
return value.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c] ?? c);
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tsdraft",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "2D CAD drawings as TypeScript: write the drawing in code, export DXF and SVG.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"2d",
|
|
7
|
+
"autocad",
|
|
8
|
+
"bricscad",
|
|
9
|
+
"cad",
|
|
10
|
+
"drafting",
|
|
11
|
+
"drawing",
|
|
12
|
+
"dxf"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/Surfside-Software/tsdraft#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/Surfside-Software/tsdraft/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Stephen Frederick",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/Surfside-Software/tsdraft.git"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"CHANGELOG.md"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
39
|
+
"build": "pnpm clean && tsc -p tsconfig.build.json",
|
|
40
|
+
"prepack": "pnpm build",
|
|
41
|
+
"check": "tsc --noEmit",
|
|
42
|
+
"check:tsrs": "tsrs --noEmit",
|
|
43
|
+
"lint": "oxlint",
|
|
44
|
+
"fmt": "oxfmt",
|
|
45
|
+
"fmt:check": "oxfmt --check",
|
|
46
|
+
"test": "node --test test/*.test.ts",
|
|
47
|
+
"test:package": "node scripts/package-smoke.ts",
|
|
48
|
+
"test:cad": "node scripts/cad-check.ts",
|
|
49
|
+
"example": "node examples/site-plan.ts",
|
|
50
|
+
"verify": "pnpm fmt:check && pnpm lint && pnpm check && pnpm test && pnpm test:package"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^24.0.0",
|
|
54
|
+
"oxfmt": "0.77.0",
|
|
55
|
+
"oxlint": "1.92.0",
|
|
56
|
+
"tsrs": "0.23.0-ts7.1.0-beta",
|
|
57
|
+
"typescript": "7.0.2"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=22"
|
|
61
|
+
},
|
|
62
|
+
"packageManager": "pnpm@12.11.2"
|
|
63
|
+
}
|