nicevg 0.18.1 → 0.20.1
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/README.md +23 -9
- package/dist/cli.js +16 -5
- package/dist/cli.js.map +1 -1
- package/dist/draw.d.ts +6 -5
- package/dist/draw.js +27 -13
- package/dist/draw.js.map +1 -1
- package/dist/index.d.ts +20 -29
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/dist/style/simple.d.ts +26 -0
- package/dist/style/simple.js +80 -0
- package/dist/style/simple.js.map +1 -0
- package/native/darwin-arm64/libnicevg_ffi.dylib +0 -0
- package/native/darwin-x64/libnicevg_ffi.dylib +0 -0
- package/native/linux-arm64/libnicevg_ffi.so +0 -0
- package/native/linux-x64/libnicevg_ffi.so +0 -0
- package/native/win32-x64/nicevg_ffi.dll +0 -0
- package/package.json +7 -1
- package/src/cli.ts +16 -5
- package/src/draw.ts +27 -13
- package/src/index.ts +24 -34
- package/src/style/simple.ts +101 -0
package/README.md
CHANGED
|
@@ -33,14 +33,29 @@ It reads the SVG from stdin and writes the fixed one to stdout. The issues it co
|
|
|
33
33
|
### Library
|
|
34
34
|
|
|
35
35
|
```ts
|
|
36
|
-
import { render
|
|
36
|
+
import { render } from "nicevg";
|
|
37
|
+
import { box, circle } from "nicevg/style/simple";
|
|
37
38
|
|
|
38
39
|
const image = render({
|
|
39
40
|
nodes: [
|
|
40
|
-
{ id: "
|
|
41
|
-
{ id: "
|
|
41
|
+
{ id: "user", class: "circle", x: 20, y: 20, width: 60, height: 60, label: "User" },
|
|
42
|
+
{ id: "order", class: "box", x: 200, y: 22, width: 120, height: 56, label: "Order" },
|
|
42
43
|
],
|
|
43
|
-
edges: [{ from: "
|
|
44
|
+
edges: [{ from: "user", to: "order", label: "place" }],
|
|
45
|
+
style: { box, circle },
|
|
46
|
+
});
|
|
47
|
+
await image.write("diagram.svg");
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Styling
|
|
51
|
+
|
|
52
|
+
A style is a function that draws its nodes as SVG, with `svg` for the markup and `text` for labels measured the way nicevg measures them.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { render, svg, text } from "nicevg";
|
|
56
|
+
|
|
57
|
+
const image = render({
|
|
58
|
+
nodes: [{ id: "orders", class: "pill", x: 20, y: 20, label: "Orders" }],
|
|
44
59
|
style: {
|
|
45
60
|
pill: ({ label }) => {
|
|
46
61
|
const name = text(label);
|
|
@@ -71,11 +86,8 @@ Fixes the diagram, nudges its nodes into a cleaner layout and returns an `Svg`.
|
|
|
71
86
|
| Node field | |
|
|
72
87
|
|---|---|
|
|
73
88
|
| `id`, `x`, `y` | Its id and its top left corner. |
|
|
74
|
-
| `
|
|
75
|
-
| `label`, `fontSize` | Its text, lines split at `\n`. |
|
|
76
|
-
| `shape` | `"rect"` (the default) or `"circle"`. |
|
|
89
|
+
| `class` | The `style` that draws it; every node has one. Other fields you add are passed to that style. |
|
|
77
90
|
| `parent` | The id of the node it sits inside. |
|
|
78
|
-
| `class` | The `style` that draws it. Other fields you add are passed to that style. |
|
|
79
91
|
| `allowOverlap` | `true` if it overlaps other nodes on purpose. |
|
|
80
92
|
|
|
81
93
|
| Edge field | |
|
|
@@ -99,6 +111,8 @@ Fixes the diagram, nudges its nodes into a cleaner layout and returns an `Svg`.
|
|
|
99
111
|
| Function | |
|
|
100
112
|
|---|---|
|
|
101
113
|
| `` svg`...` `` | SVG markup. Embedded strings are escaped; `svg` fragments, texts and arrays of them go in as they are. |
|
|
102
|
-
| `text(content, size = 14)` |
|
|
114
|
+
| `text(content, size = 14)` | A text, lines split at `\n`, with the `width` and `height` nicevg measures it at: its widest line, and 1.2 font sizes a line. |
|
|
103
115
|
| `text(...).at({ left \| cx \| right, top \| cy \| bottom })` | Places the text so that point of its box sits at those coordinates. |
|
|
104
116
|
| `` svg`...`.at(x, y) `` | The fragment with its origin moved to `x`, `y`. |
|
|
117
|
+
|
|
118
|
+
`nicevg/style/simple` has `box` and `circle`, white shapes outlined in black with the label centred, growing to fit their label: `import { box, circle } from "nicevg/style/simple"`, then `style: { box, circle }`. Their nodes take `width`, `height`, `label` (lines split at `\n`) and `fontSize`; a circle sits in the middle of its `width` and `height`.
|
package/dist/cli.js
CHANGED
|
@@ -4,19 +4,30 @@
|
|
|
4
4
|
* native library, out on stdout. Issues left go to stderr, one per line;
|
|
5
5
|
* the exit status is 0 with none left, 1 with some, and 2 for bad input.
|
|
6
6
|
*/
|
|
7
|
-
import { readFileSync } from "node:fs";
|
|
7
|
+
import { readFileSync, writeSync } from "node:fs";
|
|
8
8
|
import { NicevgError, render } from "./index.js";
|
|
9
9
|
const ABOUT = "Repair mechanical layout errors in an SVG diagram read from stdin, move nodes a little so connectors bend, cross and crowd less, and write the result to stdout";
|
|
10
10
|
const USAGE = "Usage: nicevg";
|
|
11
|
+
/** Writes to stdout, as nothing once the reader has gone, as `head` does. */
|
|
12
|
+
function print(text) {
|
|
13
|
+
try {
|
|
14
|
+
writeSync(1, text);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error.code !== "EPIPE") {
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
11
22
|
function main(args) {
|
|
12
23
|
for (const arg of args) {
|
|
13
24
|
if (arg === "-h" || arg === "--help") {
|
|
14
|
-
|
|
25
|
+
print(`${ABOUT}\n\n${USAGE}\n\nOptions:\n -h, --help Print help\n -V, --version Print version\n`);
|
|
15
26
|
return 0;
|
|
16
27
|
}
|
|
17
28
|
if (arg === "-V" || arg === "--version") {
|
|
18
29
|
const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
19
|
-
|
|
30
|
+
print(`nicevg ${version}\n`);
|
|
20
31
|
return 0;
|
|
21
32
|
}
|
|
22
33
|
}
|
|
@@ -35,12 +46,12 @@ function main(args) {
|
|
|
35
46
|
}
|
|
36
47
|
throw error;
|
|
37
48
|
}
|
|
38
|
-
|
|
49
|
+
print(`${svg}\n`);
|
|
39
50
|
for (const issue of svg.report.issues) {
|
|
40
51
|
process.stderr.write(`${issue.code}: ${issue.message}\n`);
|
|
41
52
|
}
|
|
42
53
|
return svg.report.valid ? 0 : 1;
|
|
43
54
|
}
|
|
44
|
-
// Set rather than exit, so
|
|
55
|
+
// Set rather than exit, so stderr finishes writing first.
|
|
45
56
|
process.exitCode = main(process.argv.slice(2));
|
|
46
57
|
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;GAIG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;GAIG;AAEH,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAElD,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,KAAK,GACT,iKAAiK,CAAC;AACpK,MAAM,KAAK,GAAG,eAAe,CAAC;AAE9B,6EAA6E;AAC7E,SAAS,KAAK,CAAC,IAAY;IACzB,IAAI,CAAC;QACH,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IACrB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAK,KAA+B,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YACtD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,IAAI,CAAC,IAAc;IAC1B,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,QAAQ,EAAE,CAAC;YACrC,KAAK,CAAC,GAAG,KAAK,OAAO,KAAK,6EAA6E,CAAC,CAAC;YACzG,OAAO,CAAC,CAAC;QACX,CAAC;QACD,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,WAAW,EAAE,CAAC;YACxC,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;YAClG,KAAK,CAAC,UAAU,OAAO,IAAI,CAAC,CAAC;YAC7B,OAAO,CAAC,CAAC;QACX,CAAC;IACH,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,+BAA+B,IAAI,CAAC,CAAC,CAAC,cAAc,KAAK,2CAA2C,CAAC,CAAC;QAC3H,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,GAAG,CAAC;IACR,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,CAAC,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACjD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,WAAW,IAAI,CAAC,KAAK,YAAY,KAAK,IAAI,MAAM,IAAI,KAAK,CAAC,EAAE,CAAC;YAChF,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,gBAAgB,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;YACxD,OAAO,CAAC,CAAC;QACX,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;IACD,KAAK,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;IAClB,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACtC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAClC,CAAC;AAED,0DAA0D;AAC1D,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC"}
|
package/dist/draw.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export declare class SvgFragment {
|
|
|
23
23
|
}
|
|
24
24
|
/** SVG markup with values embedded: strings escaped, fragments, texts and arrays of them as they are. */
|
|
25
25
|
export declare function svg(strings: TemplateStringsArray, ...values: unknown[]): SvgFragment;
|
|
26
|
+
export declare const WIDE: RegExp;
|
|
26
27
|
/** Where a text goes: one of `left`, `cx`, `right` across, one of `top`, `cy`, `bottom` down. */
|
|
27
28
|
export type TextPlace = ({
|
|
28
29
|
left: number;
|
|
@@ -37,20 +38,20 @@ export type TextPlace = ({
|
|
|
37
38
|
} | {
|
|
38
39
|
bottom: number;
|
|
39
40
|
});
|
|
40
|
-
/** A
|
|
41
|
+
/** A text of one or more lines (split at `\n`), sized the way nicevg measures text, placed with `at`. */
|
|
41
42
|
export declare class Text {
|
|
42
43
|
readonly content: string;
|
|
43
44
|
readonly size: number;
|
|
44
45
|
readonly place: TextPlace;
|
|
45
|
-
/** Its width, wide characters one font size each and others 0.59. */
|
|
46
|
+
/** Its widest line's width, wide characters one font size each and others 0.59. */
|
|
46
47
|
readonly width: number;
|
|
47
|
-
/** Its
|
|
48
|
+
/** Its lines' height, 1.2 font sizes each. */
|
|
48
49
|
readonly height: number;
|
|
49
50
|
constructor(content: string, size?: number, place?: TextPlace);
|
|
50
51
|
/** This text with the point of its box named in `place` at the coordinates given. */
|
|
51
52
|
at(place: TextPlace): Text;
|
|
52
|
-
/** The `<text>` element, centred on
|
|
53
|
+
/** The `<text>` element, each line centred on itself by `dominant-baseline`, more than one as `<tspan>`s. */
|
|
53
54
|
toString(): string;
|
|
54
55
|
}
|
|
55
|
-
/** A
|
|
56
|
+
/** A text at `size` (14 by default), lines split at `\n`, to embed in `svg` once placed with `at`. */
|
|
56
57
|
export declare function text(content: string, size?: number): Text;
|
package/dist/draw.js
CHANGED
|
@@ -42,26 +42,29 @@ export function svg(strings, ...values) {
|
|
|
42
42
|
return new SvgFragment(strings.reduce((markup, string, index) => markup + embed(values[index - 1]) + string));
|
|
43
43
|
}
|
|
44
44
|
// shortcut: the common wide ranges (CJK, kana, hangul, fullwidth forms), not every East Asian Wide character; widen it if a script measures short.
|
|
45
|
-
const WIDE = /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-가-힣豈-︰-﹏-⦆¢-₩]|[\u{20000}-\u{3fffd}]/u;
|
|
46
|
-
/** A
|
|
45
|
+
export const WIDE = /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-가-힣豈-︰-﹏-⦆¢-₩]|[\u{20000}-\u{3fffd}]/u;
|
|
46
|
+
/** A text of one or more lines (split at `\n`), sized the way nicevg measures text, placed with `at`. */
|
|
47
47
|
export class Text {
|
|
48
48
|
content;
|
|
49
49
|
size;
|
|
50
50
|
place;
|
|
51
|
-
/** Its width, wide characters one font size each and others 0.59. */
|
|
51
|
+
/** Its widest line's width, wide characters one font size each and others 0.59. */
|
|
52
52
|
width;
|
|
53
|
-
/** Its
|
|
53
|
+
/** Its lines' height, 1.2 font sizes each. */
|
|
54
54
|
height;
|
|
55
55
|
constructor(content, size = 14, place = { left: 0, top: 0 }) {
|
|
56
56
|
this.content = content;
|
|
57
57
|
this.size = size;
|
|
58
58
|
this.place = place;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
const widths = content.split("\n").map((line) => {
|
|
60
|
+
let width = 0;
|
|
61
|
+
for (const character of line) {
|
|
62
|
+
width += WIDE.test(character) ? size : character.length * size * 0.59;
|
|
63
|
+
}
|
|
64
|
+
return width;
|
|
65
|
+
});
|
|
66
|
+
this.width = Math.ceil(Math.max(...widths));
|
|
67
|
+
this.height = Math.ceil(widths.length * size * 1.2);
|
|
65
68
|
}
|
|
66
69
|
/** This text with the point of its box named in `place` at the coordinates given. */
|
|
67
70
|
at(place) {
|
|
@@ -72,15 +75,26 @@ export class Text {
|
|
|
72
75
|
}
|
|
73
76
|
return new Text(this.content, this.size, place);
|
|
74
77
|
}
|
|
75
|
-
/** The `<text>` element, centred on
|
|
78
|
+
/** The `<text>` element, each line centred on itself by `dominant-baseline`, more than one as `<tspan>`s. */
|
|
76
79
|
toString() {
|
|
77
80
|
const place = this.place;
|
|
78
81
|
const [x, anchor] = "cx" in place ? [place.cx, "middle"] : "right" in place ? [place.right, "end"] : [place.left, "start"];
|
|
79
82
|
const y = "cy" in place ? place.cy : "top" in place ? place.top + this.height / 2 : place.bottom - this.height / 2;
|
|
80
|
-
|
|
83
|
+
const attributes = `font-size="${this.size}" text-anchor="${anchor}" dominant-baseline="central"`;
|
|
84
|
+
const lines = this.content.split("\n");
|
|
85
|
+
if (lines.length === 1) {
|
|
86
|
+
return `<text x="${x}" y="${y}" ${attributes}>${escape(this.content)}</text>`;
|
|
87
|
+
}
|
|
88
|
+
// A tspan with its own x and y is a line of its own, to nicevg as to the renderer.
|
|
89
|
+
const step = this.size * 1.2;
|
|
90
|
+
const tspans = lines.map((line, index) => {
|
|
91
|
+
const lineY = Math.round((y + (index - (lines.length - 1) / 2) * step) * 100) / 100;
|
|
92
|
+
return `<tspan x="${x}" y="${lineY}">${escape(line)}</tspan>`;
|
|
93
|
+
});
|
|
94
|
+
return `<text x="${x}" ${attributes}>${tspans.join("")}</text>`;
|
|
81
95
|
}
|
|
82
96
|
}
|
|
83
|
-
/** A
|
|
97
|
+
/** A text at `size` (14 by default), lines split at `\n`, to embed in `svg` once placed with `at`. */
|
|
84
98
|
export function text(content, size = 14) {
|
|
85
99
|
return new Text(content, size);
|
|
86
100
|
}
|
package/dist/draw.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"draw.js","sourceRoot":"","sources":["../src/draw.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,EAAE,CAC/B,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAEnG,4EAA4E;AAC5E,MAAM,OAAO,WAAW;IACb,OAAO,CAAS;IAEzB,YAAY,MAAc;QACxB,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;IACxB,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,uDAAuD;IACvD,EAAE,CAAC,CAAS,EAAE,CAAS;QACrB,OAAO,GAAG,CAAA,2BAA2B,CAAC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC;IAC9D,CAAC;CACF;AAED,SAAS,KAAK,CAAC,KAAc;IAC3B,IAAI,KAAK,YAAY,WAAW,IAAI,KAAK,YAAY,IAAI,EAAE,CAAC;QAC1D,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAC;IAC1B,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACnC,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;AAC/B,CAAC;AAED,yGAAyG;AACzG,MAAM,UAAU,GAAG,CAAC,OAA6B,EAAE,GAAG,MAAiB;IACrE,OAAO,IAAI,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC;AAChH,CAAC;AAED,mJAAmJ;AACnJ,MAAM,IAAI,GAAG,4DAA4D,CAAC;
|
|
1
|
+
{"version":3,"file":"draw.js","sourceRoot":"","sources":["../src/draw.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
*
|
|
6
6
|
* ```ts
|
|
7
7
|
* import { render } from "nicevg";
|
|
8
|
+
* import { box } from "nicevg/style/simple";
|
|
8
9
|
*
|
|
9
10
|
* const svg = render({
|
|
10
11
|
* nodes: [
|
|
11
|
-
* { id: "order", x: 20, y: 20, width: 120, height: 56, label: "Order" },
|
|
12
|
-
* { id: "bill", x: 300, y: 160, width: 120, height: 56, label: "Bill" },
|
|
12
|
+
* { id: "order", class: "box", x: 20, y: 20, width: 120, height: 56, label: "Order" },
|
|
13
|
+
* { id: "bill", class: "box", x: 300, y: 160, width: 120, height: 56, label: "Bill" },
|
|
13
14
|
* ],
|
|
14
|
-
*
|
|
15
|
+
* edges: [{ from: "order", to: "bill", label: "invoice" }],
|
|
16
|
+
* style: { box },
|
|
15
17
|
* });
|
|
16
18
|
* await svg.write("diagram.svg");
|
|
17
19
|
* svg.report.diagram.connectors; // where the routes ended up, in the order given
|
|
@@ -32,22 +34,6 @@ export interface Bounds {
|
|
|
32
34
|
width: number;
|
|
33
35
|
height: number;
|
|
34
36
|
}
|
|
35
|
-
/** A box, or a circle whose bounds are the square around it. */
|
|
36
|
-
export interface SketchNode {
|
|
37
|
-
id: string;
|
|
38
|
-
x: number;
|
|
39
|
-
y: number;
|
|
40
|
-
width: number;
|
|
41
|
-
height: number;
|
|
42
|
-
shape?: "rect" | "circle";
|
|
43
|
-
/** Its text, lines split at `\n`: at the top left of a node with children, centred in any other. */
|
|
44
|
-
label?: string;
|
|
45
|
-
fontSize?: number;
|
|
46
|
-
/** The node this one sits inside. */
|
|
47
|
-
parent?: string;
|
|
48
|
-
/** Whether it may overlap other nodes on purpose. */
|
|
49
|
-
allowOverlap?: boolean;
|
|
50
|
-
}
|
|
51
37
|
/**
|
|
52
38
|
* A connector from one node to another, along `points` when given. Without
|
|
53
39
|
* them it runs between the nodes' centres, which rendering reroutes from
|
|
@@ -64,10 +50,13 @@ export interface SketchConnector {
|
|
|
64
50
|
}
|
|
65
51
|
/**
|
|
66
52
|
* A node drawn by the style its `class` names, with its origin at `x`, `y`.
|
|
67
|
-
* It lies around what the style draws,
|
|
68
|
-
*
|
|
53
|
+
* It lies around what the style draws, or the rect or circle the style marks
|
|
54
|
+
* `data-bounds="true"`, and rendering moves it but never resizes it. Its
|
|
55
|
+
* other fields, such as `label`, are for the style to use: `box` and
|
|
56
|
+
* `circle` from `nicevg/style/simple` take `width`, `height`, `label` and
|
|
57
|
+
* `fontSize`.
|
|
69
58
|
*/
|
|
70
|
-
export interface
|
|
59
|
+
export interface SketchNode {
|
|
71
60
|
id: string;
|
|
72
61
|
x: number;
|
|
73
62
|
y: number;
|
|
@@ -78,16 +67,16 @@ export interface StyledNode {
|
|
|
78
67
|
allowOverlap?: boolean;
|
|
79
68
|
[field: string]: unknown;
|
|
80
69
|
}
|
|
81
|
-
/** Draws a
|
|
82
|
-
export type Style<Node extends
|
|
70
|
+
/** Draws a node's picture, from its own origin, with `svg` and `text`. */
|
|
71
|
+
export type Style<Node extends SketchNode = SketchNode> = (node: Node) => SvgFragment;
|
|
83
72
|
/** A style for each class the nodes name, each given the nodes of its class. */
|
|
84
|
-
export type Styles<Node extends SketchNode
|
|
85
|
-
[Class in
|
|
73
|
+
export type Styles<Node extends SketchNode> = {
|
|
74
|
+
[Class in Node["class"]]: Style<Extract<Node, {
|
|
86
75
|
class: Class;
|
|
87
76
|
}>>;
|
|
88
77
|
};
|
|
89
78
|
/** A diagram as nodes and the edges (connectors) between them. */
|
|
90
|
-
export interface Sketch<Node extends SketchNode
|
|
79
|
+
export interface Sketch<Node extends SketchNode = SketchNode> {
|
|
91
80
|
nodes: readonly Node[];
|
|
92
81
|
edges?: SketchConnector[];
|
|
93
82
|
/** The same as `edges`, its name before; pass one or the other. */
|
|
@@ -101,7 +90,7 @@ export interface Sketch<Node extends SketchNode | StyledNode = SketchNode | Styl
|
|
|
101
90
|
rootAttrs?: Record<string, string | number>;
|
|
102
91
|
}
|
|
103
92
|
/** A diagram to render: a sketch, SVG text following nicevg's SVG contract, or an Svg rendered before. */
|
|
104
|
-
export type Input<Node extends SketchNode
|
|
93
|
+
export type Input<Node extends SketchNode = SketchNode> = Sketch<Node> | {
|
|
105
94
|
svg: string;
|
|
106
95
|
} | Svg;
|
|
107
96
|
export interface DiagramNode {
|
|
@@ -111,6 +100,8 @@ export interface DiagramNode {
|
|
|
111
100
|
labelBounds: Bounds[];
|
|
112
101
|
parentId?: string;
|
|
113
102
|
allowOverlap?: boolean;
|
|
103
|
+
/** Drawn by a style: its texts are kept clear of, but not checked to fit. */
|
|
104
|
+
custom?: boolean;
|
|
114
105
|
}
|
|
115
106
|
export interface DiagramConnector {
|
|
116
107
|
/** Its id in an SVG; a sketch's connectors get one for their labels to refer to. */
|
|
@@ -177,4 +168,4 @@ export declare class NicevgError extends Error {
|
|
|
177
168
|
* routes through nodes, stray labels and more), routes the connectors given
|
|
178
169
|
* no points, and moves nodes a little where that reads better.
|
|
179
170
|
*/
|
|
180
|
-
export declare function render<const Node extends SketchNode
|
|
171
|
+
export declare function render<const Node extends SketchNode>(input: Input<Node>): Svg;
|
package/dist/index.js
CHANGED
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
*
|
|
6
6
|
* ```ts
|
|
7
7
|
* import { render } from "nicevg";
|
|
8
|
+
* import { box } from "nicevg/style/simple";
|
|
8
9
|
*
|
|
9
10
|
* const svg = render({
|
|
10
11
|
* nodes: [
|
|
11
|
-
* { id: "order", x: 20, y: 20, width: 120, height: 56, label: "Order" },
|
|
12
|
-
* { id: "bill", x: 300, y: 160, width: 120, height: 56, label: "Bill" },
|
|
12
|
+
* { id: "order", class: "box", x: 20, y: 20, width: 120, height: 56, label: "Order" },
|
|
13
|
+
* { id: "bill", class: "box", x: 300, y: 160, width: 120, height: 56, label: "Bill" },
|
|
13
14
|
* ],
|
|
14
|
-
*
|
|
15
|
+
* edges: [{ from: "order", to: "bill", label: "invoice" }],
|
|
16
|
+
* style: { box },
|
|
15
17
|
* });
|
|
16
18
|
* await svg.write("diagram.svg");
|
|
17
19
|
* svg.report.diagram.connectors; // where the routes ended up, in the order given
|
|
@@ -63,7 +65,7 @@ export function render(input) {
|
|
|
63
65
|
}
|
|
64
66
|
return new Svg(reply.ok.svg, reply.ok.changes, reply.ok.report);
|
|
65
67
|
}
|
|
66
|
-
/** The sketch as the native library reads it:
|
|
68
|
+
/** The sketch as the native library reads it: nodes drawn by their styles, edges as connectors. */
|
|
67
69
|
function sketchRequest({ nodes, edges, connectors, style, rootAttrs }) {
|
|
68
70
|
// Each style is called only with nodes of its own class.
|
|
69
71
|
const styles = (style ?? {});
|
|
@@ -72,8 +74,8 @@ function sketchRequest({ nodes, edges, connectors, style, rootAttrs }) {
|
|
|
72
74
|
}
|
|
73
75
|
return {
|
|
74
76
|
nodes: nodes.map((node, index) => {
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
+
if (typeof node.class !== "string") {
|
|
78
|
+
throw new NicevgError(`nodes[${index}].class: every node needs the class of the style that draws it`);
|
|
77
79
|
}
|
|
78
80
|
const draw = Object.hasOwn(styles, node.class) ? styles[node.class] : undefined;
|
|
79
81
|
if (!draw) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boxes and circles drawn the way nicevg draws a sketch's rect and circle
|
|
3
|
+
* nodes: white, outlined in black, the label centred in lines. A label
|
|
4
|
+
* too big for its node grows it as fixing grows those, so a diagram
|
|
5
|
+
* drawn with these styles arranges as the same diagram without them.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { render } from "nicevg";
|
|
9
|
+
* import { box, circle } from "nicevg/style/simple";
|
|
10
|
+
*
|
|
11
|
+
* render({ nodes: [{ id: "a", class: "box", x: 20, y: 20, width: 120, height: 56, label: "Order" }], style: { box, circle } });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import { type SvgFragment } from "../draw.ts";
|
|
15
|
+
/** A node a simple style draws: the box it takes, and its text. */
|
|
16
|
+
export interface SimpleNode {
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
/** Its text, lines split at `\n`, centred. */
|
|
20
|
+
label?: string;
|
|
21
|
+
fontSize?: number;
|
|
22
|
+
}
|
|
23
|
+
/** A box, grown to keep its label 12px inside it. */
|
|
24
|
+
export declare function box({ width, height, label: content, fontSize }: SimpleNode): SvgFragment;
|
|
25
|
+
/** A circle in the middle of the box it is given, grown to keep its label 12px inside it. */
|
|
26
|
+
export declare function circle({ width, height, label: content, fontSize }: SimpleNode): SvgFragment;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boxes and circles drawn the way nicevg draws a sketch's rect and circle
|
|
3
|
+
* nodes: white, outlined in black, the label centred in lines. A label
|
|
4
|
+
* too big for its node grows it as fixing grows those, so a diagram
|
|
5
|
+
* drawn with these styles arranges as the same diagram without them.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { render } from "nicevg";
|
|
9
|
+
* import { box, circle } from "nicevg/style/simple";
|
|
10
|
+
*
|
|
11
|
+
* render({ nodes: [{ id: "a", class: "box", x: 20, y: 20, width: 120, height: 56, label: "Order" }], style: { box, circle } });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import { WIDE, svg } from "../draw.js";
|
|
15
|
+
const LINE_HEIGHT = 1.2;
|
|
16
|
+
/** How far a label keeps inside its node. */
|
|
17
|
+
const PADDING = 12;
|
|
18
|
+
/** nicevg's rounding: half up. */
|
|
19
|
+
const round = (value) => (value - Math.floor(value) >= 0.5 ? Math.floor(value) + 1 : Math.floor(value));
|
|
20
|
+
/** A line's width as nicevg estimates it: wide characters one font size, others 0.59, rounded. */
|
|
21
|
+
function lineWidth(line, size) {
|
|
22
|
+
let wide = 0;
|
|
23
|
+
let narrow = 0;
|
|
24
|
+
for (const character of line) {
|
|
25
|
+
if (WIDE.test(character)) {
|
|
26
|
+
wide += 1;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
narrow += character.length;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return round(narrow * size * 0.59 + wide * size);
|
|
33
|
+
}
|
|
34
|
+
/** A label centred on `cx`, `cy` as nicevg writes one, and the box nicevg measures around it. */
|
|
35
|
+
function label(content, cx, cy, size) {
|
|
36
|
+
const lines = content.split("\n");
|
|
37
|
+
const first = cy - ((lines.length - 1) * size * LINE_HEIGHT) / 2 + size * 0.35;
|
|
38
|
+
const ys = lines.map((_, index) => first + index * size * LINE_HEIGHT);
|
|
39
|
+
const boxes = lines.map((line, index) => {
|
|
40
|
+
const width = lineWidth(line, size);
|
|
41
|
+
return { x: cx - width / 2, y: ys[index] - size, width, height: round(size * LINE_HEIGHT) };
|
|
42
|
+
});
|
|
43
|
+
const left = Math.min(...boxes.map((b) => b.x));
|
|
44
|
+
const top = Math.min(...boxes.map((b) => b.y));
|
|
45
|
+
const right = Math.max(...boxes.map((b) => b.x + b.width));
|
|
46
|
+
const bottom = Math.max(...boxes.map((b) => b.y + b.height));
|
|
47
|
+
return {
|
|
48
|
+
markup: svg `<text text-anchor="middle" font-size="${size}">${lines.map((line, index) => svg `<tspan x="${cx}" y="${ys[index]}">${line}</tspan>`)}</text>`,
|
|
49
|
+
bounds: { x: left, y: top, width: right - left, height: bottom - top },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** A box, grown to keep its label 12px inside it. */
|
|
53
|
+
export function box({ width, height, label: content, fontSize = 14 }) {
|
|
54
|
+
if (content === undefined) {
|
|
55
|
+
return svg `<rect data-bounds="true" x="0" y="0" width="${width}" height="${height}" fill="white" stroke="black"/>`;
|
|
56
|
+
}
|
|
57
|
+
const text = label(content, width / 2, height / 2, fontSize);
|
|
58
|
+
const left = Math.min(0, text.bounds.x - PADDING);
|
|
59
|
+
const top = Math.min(0, text.bounds.y - PADDING);
|
|
60
|
+
const right = Math.max(width, text.bounds.x + text.bounds.width + PADDING);
|
|
61
|
+
const bottom = Math.max(height, text.bounds.y + text.bounds.height + PADDING);
|
|
62
|
+
return svg `<rect data-bounds="true" x="${left}" y="${top}" width="${right - left}" height="${bottom - top}" fill="white" stroke="black"/>${text.markup}`;
|
|
63
|
+
}
|
|
64
|
+
/** A circle in the middle of the box it is given, grown to keep its label 12px inside it. */
|
|
65
|
+
export function circle({ width, height, label: content, fontSize = 14 }) {
|
|
66
|
+
const [cx, cy] = [width / 2, height / 2];
|
|
67
|
+
let radius = Math.min(width, height) / 2;
|
|
68
|
+
if (content === undefined) {
|
|
69
|
+
return svg `<circle data-bounds="true" cx="${cx}" cy="${cy}" r="${radius}" fill="white" stroke="black"/>`;
|
|
70
|
+
}
|
|
71
|
+
const text = label(content, cx, cy, fontSize);
|
|
72
|
+
const dx = Math.max(Math.abs(text.bounds.x - cx), Math.abs(text.bounds.x + text.bounds.width - cx));
|
|
73
|
+
const dy = Math.max(Math.abs(text.bounds.y - cy), Math.abs(text.bounds.y + text.bounds.height - cy));
|
|
74
|
+
const needed = Math.hypot(dx, dy) + PADDING;
|
|
75
|
+
if (needed > radius) {
|
|
76
|
+
radius = Math.ceil(needed);
|
|
77
|
+
}
|
|
78
|
+
return svg `<circle data-bounds="true" cx="${cx}" cy="${cy}" r="${radius}" fill="white" stroke="black"/>${text.markup}`;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=simple.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"simple.js","sourceRoot":"","sources":["../../src/style/simple.ts"],"names":[],"mappings":"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"}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nicevg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "Deterministic checks and repairs for diagrams, given as shapes or SVG, on Bun and Node through native FFI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"diagram",
|
|
@@ -28,11 +28,17 @@
|
|
|
28
28
|
"types": "./dist/index.d.ts",
|
|
29
29
|
"bun": "./src/index.ts",
|
|
30
30
|
"default": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./style/simple": {
|
|
33
|
+
"types": "./dist/style/simple.d.ts",
|
|
34
|
+
"bun": "./src/style/simple.ts",
|
|
35
|
+
"default": "./dist/style/simple.js"
|
|
31
36
|
}
|
|
32
37
|
},
|
|
33
38
|
"files": [
|
|
34
39
|
"dist",
|
|
35
40
|
"src/*.ts",
|
|
41
|
+
"src/style/*.ts",
|
|
36
42
|
"native"
|
|
37
43
|
],
|
|
38
44
|
"engines": {
|
package/src/cli.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* the exit status is 0 with none left, 1 with some, and 2 for bad input.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { readFileSync } from "node:fs";
|
|
8
|
+
import { readFileSync, writeSync } from "node:fs";
|
|
9
9
|
|
|
10
10
|
import { NicevgError, render } from "./index.ts";
|
|
11
11
|
|
|
@@ -13,15 +13,26 @@ const ABOUT =
|
|
|
13
13
|
"Repair mechanical layout errors in an SVG diagram read from stdin, move nodes a little so connectors bend, cross and crowd less, and write the result to stdout";
|
|
14
14
|
const USAGE = "Usage: nicevg";
|
|
15
15
|
|
|
16
|
+
/** Writes to stdout, as nothing once the reader has gone, as `head` does. */
|
|
17
|
+
function print(text: string): void {
|
|
18
|
+
try {
|
|
19
|
+
writeSync(1, text);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if ((error as NodeJS.ErrnoException).code !== "EPIPE") {
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
function main(args: string[]): number {
|
|
17
28
|
for (const arg of args) {
|
|
18
29
|
if (arg === "-h" || arg === "--help") {
|
|
19
|
-
|
|
30
|
+
print(`${ABOUT}\n\n${USAGE}\n\nOptions:\n -h, --help Print help\n -V, --version Print version\n`);
|
|
20
31
|
return 0;
|
|
21
32
|
}
|
|
22
33
|
if (arg === "-V" || arg === "--version") {
|
|
23
34
|
const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
24
|
-
|
|
35
|
+
print(`nicevg ${version}\n`);
|
|
25
36
|
return 0;
|
|
26
37
|
}
|
|
27
38
|
}
|
|
@@ -40,12 +51,12 @@ function main(args: string[]): number {
|
|
|
40
51
|
}
|
|
41
52
|
throw error;
|
|
42
53
|
}
|
|
43
|
-
|
|
54
|
+
print(`${svg}\n`);
|
|
44
55
|
for (const issue of svg.report.issues) {
|
|
45
56
|
process.stderr.write(`${issue.code}: ${issue.message}\n`);
|
|
46
57
|
}
|
|
47
58
|
return svg.report.valid ? 0 : 1;
|
|
48
59
|
}
|
|
49
60
|
|
|
50
|
-
// Set rather than exit, so
|
|
61
|
+
// Set rather than exit, so stderr finishes writing first.
|
|
51
62
|
process.exitCode = main(process.argv.slice(2));
|
package/src/draw.ts
CHANGED
|
@@ -51,17 +51,17 @@ export function svg(strings: TemplateStringsArray, ...values: unknown[]): SvgFra
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// shortcut: the common wide ranges (CJK, kana, hangul, fullwidth forms), not every East Asian Wide character; widen it if a script measures short.
|
|
54
|
-
const WIDE = /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-가-힣豈-︰-﹏-⦆¢-₩]|[\u{20000}-\u{3fffd}]/u;
|
|
54
|
+
export const WIDE = /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-가-힣豈-︰-﹏-⦆¢-₩]|[\u{20000}-\u{3fffd}]/u;
|
|
55
55
|
|
|
56
56
|
/** Where a text goes: one of `left`, `cx`, `right` across, one of `top`, `cy`, `bottom` down. */
|
|
57
57
|
export type TextPlace = ({ left: number } | { cx: number } | { right: number }) &
|
|
58
58
|
({ top: number } | { cy: number } | { bottom: number });
|
|
59
59
|
|
|
60
|
-
/** A
|
|
60
|
+
/** A text of one or more lines (split at `\n`), sized the way nicevg measures text, placed with `at`. */
|
|
61
61
|
export class Text {
|
|
62
|
-
/** Its width, wide characters one font size each and others 0.59. */
|
|
62
|
+
/** Its widest line's width, wide characters one font size each and others 0.59. */
|
|
63
63
|
readonly width: number;
|
|
64
|
-
/** Its
|
|
64
|
+
/** Its lines' height, 1.2 font sizes each. */
|
|
65
65
|
readonly height: number;
|
|
66
66
|
|
|
67
67
|
constructor(
|
|
@@ -69,12 +69,15 @@ export class Text {
|
|
|
69
69
|
readonly size = 14,
|
|
70
70
|
readonly place: TextPlace = { left: 0, top: 0 },
|
|
71
71
|
) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
const widths = content.split("\n").map((line) => {
|
|
73
|
+
let width = 0;
|
|
74
|
+
for (const character of line) {
|
|
75
|
+
width += WIDE.test(character) ? size : character.length * size * 0.59;
|
|
76
|
+
}
|
|
77
|
+
return width;
|
|
78
|
+
});
|
|
79
|
+
this.width = Math.ceil(Math.max(...widths));
|
|
80
|
+
this.height = Math.ceil(widths.length * size * 1.2);
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
/** This text with the point of its box named in `place` at the coordinates given. */
|
|
@@ -89,17 +92,28 @@ export class Text {
|
|
|
89
92
|
return new Text(this.content, this.size, place);
|
|
90
93
|
}
|
|
91
94
|
|
|
92
|
-
/** The `<text>` element, centred on
|
|
95
|
+
/** The `<text>` element, each line centred on itself by `dominant-baseline`, more than one as `<tspan>`s. */
|
|
93
96
|
toString(): string {
|
|
94
97
|
const place = this.place as Record<string, number>;
|
|
95
98
|
const [x, anchor] =
|
|
96
99
|
"cx" in place ? [place.cx!, "middle"] : "right" in place ? [place.right!, "end"] : [place.left!, "start"];
|
|
97
100
|
const y = "cy" in place ? place.cy! : "top" in place ? place.top! + this.height / 2 : place.bottom! - this.height / 2;
|
|
98
|
-
|
|
101
|
+
const attributes = `font-size="${this.size}" text-anchor="${anchor}" dominant-baseline="central"`;
|
|
102
|
+
const lines = this.content.split("\n");
|
|
103
|
+
if (lines.length === 1) {
|
|
104
|
+
return `<text x="${x}" y="${y}" ${attributes}>${escape(this.content)}</text>`;
|
|
105
|
+
}
|
|
106
|
+
// A tspan with its own x and y is a line of its own, to nicevg as to the renderer.
|
|
107
|
+
const step = this.size * 1.2;
|
|
108
|
+
const tspans = lines.map((line, index) => {
|
|
109
|
+
const lineY = Math.round((y + (index - (lines.length - 1) / 2) * step) * 100) / 100;
|
|
110
|
+
return `<tspan x="${x}" y="${lineY}">${escape(line)}</tspan>`;
|
|
111
|
+
});
|
|
112
|
+
return `<text x="${x}" ${attributes}>${tspans.join("")}</text>`;
|
|
99
113
|
}
|
|
100
114
|
}
|
|
101
115
|
|
|
102
|
-
/** A
|
|
116
|
+
/** A text at `size` (14 by default), lines split at `\n`, to embed in `svg` once placed with `at`. */
|
|
103
117
|
export function text(content: string, size = 14): Text {
|
|
104
118
|
return new Text(content, size);
|
|
105
119
|
}
|
package/src/index.ts
CHANGED
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
*
|
|
6
6
|
* ```ts
|
|
7
7
|
* import { render } from "nicevg";
|
|
8
|
+
* import { box } from "nicevg/style/simple";
|
|
8
9
|
*
|
|
9
10
|
* const svg = render({
|
|
10
11
|
* nodes: [
|
|
11
|
-
* { id: "order", x: 20, y: 20, width: 120, height: 56, label: "Order" },
|
|
12
|
-
* { id: "bill", x: 300, y: 160, width: 120, height: 56, label: "Bill" },
|
|
12
|
+
* { id: "order", class: "box", x: 20, y: 20, width: 120, height: 56, label: "Order" },
|
|
13
|
+
* { id: "bill", class: "box", x: 300, y: 160, width: 120, height: 56, label: "Bill" },
|
|
13
14
|
* ],
|
|
14
|
-
*
|
|
15
|
+
* edges: [{ from: "order", to: "bill", label: "invoice" }],
|
|
16
|
+
* style: { box },
|
|
15
17
|
* });
|
|
16
18
|
* await svg.write("diagram.svg");
|
|
17
19
|
* svg.report.diagram.connectors; // where the routes ended up, in the order given
|
|
@@ -40,23 +42,6 @@ export interface Bounds {
|
|
|
40
42
|
height: number;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
/** A box, or a circle whose bounds are the square around it. */
|
|
44
|
-
export interface SketchNode {
|
|
45
|
-
id: string;
|
|
46
|
-
x: number;
|
|
47
|
-
y: number;
|
|
48
|
-
width: number;
|
|
49
|
-
height: number;
|
|
50
|
-
shape?: "rect" | "circle";
|
|
51
|
-
/** Its text, lines split at `\n`: at the top left of a node with children, centred in any other. */
|
|
52
|
-
label?: string;
|
|
53
|
-
fontSize?: number;
|
|
54
|
-
/** The node this one sits inside. */
|
|
55
|
-
parent?: string;
|
|
56
|
-
/** Whether it may overlap other nodes on purpose. */
|
|
57
|
-
allowOverlap?: boolean;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
45
|
/**
|
|
61
46
|
* A connector from one node to another, along `points` when given. Without
|
|
62
47
|
* them it runs between the nodes' centres, which rendering reroutes from
|
|
@@ -74,10 +59,13 @@ export interface SketchConnector {
|
|
|
74
59
|
|
|
75
60
|
/**
|
|
76
61
|
* A node drawn by the style its `class` names, with its origin at `x`, `y`.
|
|
77
|
-
* It lies around what the style draws,
|
|
78
|
-
*
|
|
62
|
+
* It lies around what the style draws, or the rect or circle the style marks
|
|
63
|
+
* `data-bounds="true"`, and rendering moves it but never resizes it. Its
|
|
64
|
+
* other fields, such as `label`, are for the style to use: `box` and
|
|
65
|
+
* `circle` from `nicevg/style/simple` take `width`, `height`, `label` and
|
|
66
|
+
* `fontSize`.
|
|
79
67
|
*/
|
|
80
|
-
export interface
|
|
68
|
+
export interface SketchNode {
|
|
81
69
|
id: string;
|
|
82
70
|
x: number;
|
|
83
71
|
y: number;
|
|
@@ -89,16 +77,16 @@ export interface StyledNode {
|
|
|
89
77
|
[field: string]: unknown;
|
|
90
78
|
}
|
|
91
79
|
|
|
92
|
-
/** Draws a
|
|
93
|
-
export type Style<Node extends
|
|
80
|
+
/** Draws a node's picture, from its own origin, with `svg` and `text`. */
|
|
81
|
+
export type Style<Node extends SketchNode = SketchNode> = (node: Node) => SvgFragment;
|
|
94
82
|
|
|
95
83
|
/** A style for each class the nodes name, each given the nodes of its class. */
|
|
96
|
-
export type Styles<Node extends SketchNode
|
|
97
|
-
[Class in
|
|
84
|
+
export type Styles<Node extends SketchNode> = {
|
|
85
|
+
[Class in Node["class"]]: Style<Extract<Node, { class: Class }>>;
|
|
98
86
|
};
|
|
99
87
|
|
|
100
88
|
/** A diagram as nodes and the edges (connectors) between them. */
|
|
101
|
-
export interface Sketch<Node extends SketchNode
|
|
89
|
+
export interface Sketch<Node extends SketchNode = SketchNode> {
|
|
102
90
|
nodes: readonly Node[];
|
|
103
91
|
edges?: SketchConnector[];
|
|
104
92
|
/** The same as `edges`, its name before; pass one or the other. */
|
|
@@ -113,7 +101,7 @@ export interface Sketch<Node extends SketchNode | StyledNode = SketchNode | Styl
|
|
|
113
101
|
}
|
|
114
102
|
|
|
115
103
|
/** A diagram to render: a sketch, SVG text following nicevg's SVG contract, or an Svg rendered before. */
|
|
116
|
-
export type Input<Node extends SketchNode
|
|
104
|
+
export type Input<Node extends SketchNode = SketchNode> = Sketch<Node> | { svg: string } | Svg;
|
|
117
105
|
|
|
118
106
|
export interface DiagramNode {
|
|
119
107
|
id: string;
|
|
@@ -122,6 +110,8 @@ export interface DiagramNode {
|
|
|
122
110
|
labelBounds: Bounds[];
|
|
123
111
|
parentId?: string;
|
|
124
112
|
allowOverlap?: boolean;
|
|
113
|
+
/** Drawn by a style: its texts are kept clear of, but not checked to fit. */
|
|
114
|
+
custom?: boolean;
|
|
125
115
|
}
|
|
126
116
|
|
|
127
117
|
export interface DiagramConnector {
|
|
@@ -201,7 +191,7 @@ export class NicevgError extends Error {
|
|
|
201
191
|
* routes through nodes, stray labels and more), routes the connectors given
|
|
202
192
|
* no points, and moves nodes a little where that reads better.
|
|
203
193
|
*/
|
|
204
|
-
export function render<const Node extends SketchNode
|
|
194
|
+
export function render<const Node extends SketchNode>(input: Input<Node>): Svg {
|
|
205
195
|
const request = new TextEncoder().encode(
|
|
206
196
|
JSON.stringify(input instanceof Svg ? { svg: input.toString() } : "svg" in input ? input : sketchRequest(input)),
|
|
207
197
|
);
|
|
@@ -214,8 +204,8 @@ export function render<const Node extends SketchNode | StyledNode>(input: Input<
|
|
|
214
204
|
return new Svg(reply.ok.svg, reply.ok.changes, reply.ok.report);
|
|
215
205
|
}
|
|
216
206
|
|
|
217
|
-
/** The sketch as the native library reads it:
|
|
218
|
-
function sketchRequest<Node extends SketchNode
|
|
207
|
+
/** The sketch as the native library reads it: nodes drawn by their styles, edges as connectors. */
|
|
208
|
+
function sketchRequest<Node extends SketchNode>({ nodes, edges, connectors, style, rootAttrs }: Sketch<Node>) {
|
|
219
209
|
// Each style is called only with nodes of its own class.
|
|
220
210
|
const styles = (style ?? {}) as Record<string, Style>;
|
|
221
211
|
if (edges && connectors) {
|
|
@@ -223,8 +213,8 @@ function sketchRequest<Node extends SketchNode | StyledNode>({ nodes, edges, con
|
|
|
223
213
|
}
|
|
224
214
|
return {
|
|
225
215
|
nodes: nodes.map((node, index) => {
|
|
226
|
-
if (
|
|
227
|
-
|
|
216
|
+
if (typeof node.class !== "string") {
|
|
217
|
+
throw new NicevgError(`nodes[${index}].class: every node needs the class of the style that draws it`);
|
|
228
218
|
}
|
|
229
219
|
const draw = Object.hasOwn(styles, node.class) ? styles[node.class] : undefined;
|
|
230
220
|
if (!draw) {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boxes and circles drawn the way nicevg draws a sketch's rect and circle
|
|
3
|
+
* nodes: white, outlined in black, the label centred in lines. A label
|
|
4
|
+
* too big for its node grows it as fixing grows those, so a diagram
|
|
5
|
+
* drawn with these styles arranges as the same diagram without them.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { render } from "nicevg";
|
|
9
|
+
* import { box, circle } from "nicevg/style/simple";
|
|
10
|
+
*
|
|
11
|
+
* render({ nodes: [{ id: "a", class: "box", x: 20, y: 20, width: 120, height: 56, label: "Order" }], style: { box, circle } });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { type SvgFragment, WIDE, svg } from "../draw.ts";
|
|
16
|
+
|
|
17
|
+
/** A node a simple style draws: the box it takes, and its text. */
|
|
18
|
+
export interface SimpleNode {
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
/** Its text, lines split at `\n`, centred. */
|
|
22
|
+
label?: string;
|
|
23
|
+
fontSize?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const LINE_HEIGHT = 1.2;
|
|
27
|
+
/** How far a label keeps inside its node. */
|
|
28
|
+
const PADDING = 12;
|
|
29
|
+
|
|
30
|
+
/** nicevg's rounding: half up. */
|
|
31
|
+
const round = (value: number) => (value - Math.floor(value) >= 0.5 ? Math.floor(value) + 1 : Math.floor(value));
|
|
32
|
+
|
|
33
|
+
/** A line's width as nicevg estimates it: wide characters one font size, others 0.59, rounded. */
|
|
34
|
+
function lineWidth(line: string, size: number): number {
|
|
35
|
+
let wide = 0;
|
|
36
|
+
let narrow = 0;
|
|
37
|
+
for (const character of line) {
|
|
38
|
+
if (WIDE.test(character)) {
|
|
39
|
+
wide += 1;
|
|
40
|
+
} else {
|
|
41
|
+
narrow += character.length;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return round(narrow * size * 0.59 + wide * size);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface Box {
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
width: number;
|
|
51
|
+
height: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A label centred on `cx`, `cy` as nicevg writes one, and the box nicevg measures around it. */
|
|
55
|
+
function label(content: string, cx: number, cy: number, size: number): { markup: SvgFragment; bounds: Box } {
|
|
56
|
+
const lines = content.split("\n");
|
|
57
|
+
const first = cy - ((lines.length - 1) * size * LINE_HEIGHT) / 2 + size * 0.35;
|
|
58
|
+
const ys = lines.map((_, index) => first + index * size * LINE_HEIGHT);
|
|
59
|
+
const boxes = lines.map((line, index) => {
|
|
60
|
+
const width = lineWidth(line, size);
|
|
61
|
+
return { x: cx - width / 2, y: ys[index]! - size, width, height: round(size * LINE_HEIGHT) };
|
|
62
|
+
});
|
|
63
|
+
const left = Math.min(...boxes.map((b) => b.x));
|
|
64
|
+
const top = Math.min(...boxes.map((b) => b.y));
|
|
65
|
+
const right = Math.max(...boxes.map((b) => b.x + b.width));
|
|
66
|
+
const bottom = Math.max(...boxes.map((b) => b.y + b.height));
|
|
67
|
+
return {
|
|
68
|
+
markup: svg`<text text-anchor="middle" font-size="${size}">${lines.map((line, index) => svg`<tspan x="${cx}" y="${ys[index]!}">${line}</tspan>`)}</text>`,
|
|
69
|
+
bounds: { x: left, y: top, width: right - left, height: bottom - top },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A box, grown to keep its label 12px inside it. */
|
|
74
|
+
export function box({ width, height, label: content, fontSize = 14 }: SimpleNode): SvgFragment {
|
|
75
|
+
if (content === undefined) {
|
|
76
|
+
return svg`<rect data-bounds="true" x="0" y="0" width="${width}" height="${height}" fill="white" stroke="black"/>`;
|
|
77
|
+
}
|
|
78
|
+
const text = label(content, width / 2, height / 2, fontSize);
|
|
79
|
+
const left = Math.min(0, text.bounds.x - PADDING);
|
|
80
|
+
const top = Math.min(0, text.bounds.y - PADDING);
|
|
81
|
+
const right = Math.max(width, text.bounds.x + text.bounds.width + PADDING);
|
|
82
|
+
const bottom = Math.max(height, text.bounds.y + text.bounds.height + PADDING);
|
|
83
|
+
return svg`<rect data-bounds="true" x="${left}" y="${top}" width="${right - left}" height="${bottom - top}" fill="white" stroke="black"/>${text.markup}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A circle in the middle of the box it is given, grown to keep its label 12px inside it. */
|
|
87
|
+
export function circle({ width, height, label: content, fontSize = 14 }: SimpleNode): SvgFragment {
|
|
88
|
+
const [cx, cy] = [width / 2, height / 2];
|
|
89
|
+
let radius = Math.min(width, height) / 2;
|
|
90
|
+
if (content === undefined) {
|
|
91
|
+
return svg`<circle data-bounds="true" cx="${cx}" cy="${cy}" r="${radius}" fill="white" stroke="black"/>`;
|
|
92
|
+
}
|
|
93
|
+
const text = label(content, cx, cy, fontSize);
|
|
94
|
+
const dx = Math.max(Math.abs(text.bounds.x - cx), Math.abs(text.bounds.x + text.bounds.width - cx));
|
|
95
|
+
const dy = Math.max(Math.abs(text.bounds.y - cy), Math.abs(text.bounds.y + text.bounds.height - cy));
|
|
96
|
+
const needed = Math.hypot(dx, dy) + PADDING;
|
|
97
|
+
if (needed > radius) {
|
|
98
|
+
radius = Math.ceil(needed);
|
|
99
|
+
}
|
|
100
|
+
return svg`<circle data-bounds="true" cx="${cx}" cy="${cy}" r="${radius}" fill="white" stroke="black"/>${text.markup}`;
|
|
101
|
+
}
|