nicevg 0.19.0 → 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 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, svg, text } from "nicevg";
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: "order", x: 20, y: 20, width: 120, height: 56, label: "Order" },
41
- { id: "orders", class: "pill", x: 300, y: 160, label: "Orders" },
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: "order", to: "orders", label: "save" }],
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
- | `width`, `height` | Its size. Not used with `class`. |
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)` | One line of text, with the `width` and `height` nicevg measures it at. |
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/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 line of text, sized the way nicevg measures text, placed with `at`. */
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 line's height, 1.2 font sizes. */
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 its line by `dominant-baseline`. */
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 line of text at `size` (14 by default), to embed in `svg` once placed with `at`. */
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 line of text, sized the way nicevg measures text, placed with `at`. */
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 line's height, 1.2 font sizes. */
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
- let width = 0;
60
- for (const character of content) {
61
- width += WIDE.test(character) ? size : character.length * size * 0.59;
62
- }
63
- this.width = Math.ceil(width);
64
- this.height = Math.ceil(size * 1.2);
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 its line by `dominant-baseline`. */
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
- return `<text x="${x}" y="${y}" font-size="${this.size}" text-anchor="${anchor}" dominant-baseline="central">${escape(this.content)}</text>`;
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 line of text at `size` (14 by default), to embed in `svg` once placed with `at`. */
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":"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"}
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
- * connectors: [{ from: "order", to: "bill", label: "invoice" }],
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, and rendering moves it but never
68
- * resizes it. Its other fields, such as `label`, are for the style to use.
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 StyledNode {
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 styled node's picture, from its own origin, with `svg` and `text`. */
82
- export type Style<Node extends StyledNode = StyledNode> = (node: Node) => SvgFragment;
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 | StyledNode> = {
85
- [Class in Extract<Node, StyledNode>["class"]]: Style<Extract<Node, {
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 | StyledNode = SketchNode | StyledNode> {
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 | StyledNode = SketchNode | StyledNode> = Sketch<Node> | {
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 | StyledNode>(input: Input<Node>): Svg;
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
- * connectors: [{ from: "order", to: "bill", label: "invoice" }],
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: styled nodes drawn, edges as connectors. */
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 (!("class" in node)) {
76
- return node;
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":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAG7C,OAAO,EAAE,IAAI,IAAI,MAAM,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,EAAE,WAAW,EAAE,IAAI,EAAkB,GAAG,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAgIzE,gGAAgG;AAChG,MAAM,OAAO,GAAG;IAMH;IAEA;IAPF,KAAK,CAAS;IAEvB,YACE,IAAY;IACZ,mDAAmD;IAC1C,OAAiB;IAC1B,8FAA8F;IACrF,MAAsB;QAFtB,YAAO,GAAP,OAAO,CAAU;QAEjB,WAAM,GAAN,MAAM,CAAgB;QAE/B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACpB,CAAC;IAED,oBAAoB;IACpB,QAAQ;QACN,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,qCAAqC;IACrC,KAAK,CAAC,IAAkB;QACtB,OAAO,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IACrC,CAAC;CACF;AAED,mEAAmE;AACnE,MAAM,OAAO,WAAY,SAAQ,KAAK;IAC3B,IAAI,GAAG,aAAa,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,UAAU,MAAM,CAAgC,KAAkB;IACtE,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CACtC,IAAI,CAAC,SAAS,CAAC,KAAK,YAAY,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CACjH,CAAC;IACF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAEnB,CAAC;IACtB,IAAI,OAAO,IAAI,KAAK,EAAE,CAAC;QACrB,MAAM,IAAI,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC;AAClE,CAAC;AAED,mGAAmG;AACnG,SAAS,aAAa,CAA0B,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAgB;IAC1G,yDAAyD;IACzD,MAAM,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE,CAA0B,CAAC;IACtD,IAAI,KAAK,IAAI,UAAU,EAAE,CAAC;QACxB,MAAM,IAAI,WAAW,CAAC,qDAAqD,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;YAC/B,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;gBACnC,MAAM,IAAI,WAAW,CAAC,SAAS,KAAK,gEAAgE,CAAC,CAAC;YACxG,CAAC;YACD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAChF,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,MAAM,IAAI,WAAW,CAAC,SAAS,KAAK,4BAA4B,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;YACjF,CAAC;YACD,MAAM,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;YAChD,OAAO,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QACrE,CAAC,CAAC;QACF,UAAU,EAAE,KAAK,IAAI,UAAU;QAC/B,SAAS,EAAE,SAAS,IAAI,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;KACpH,CAAC;AACJ,CAAC"}
@@ -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":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAoB,IAAI,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAWzD,MAAM,WAAW,GAAG,GAAG,CAAC;AACxB,6CAA6C;AAC7C,MAAM,OAAO,GAAG,EAAE,CAAC;AAEnB,kCAAkC;AAClC,MAAM,KAAK,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;AAEhH,kGAAkG;AAClG,SAAS,SAAS,CAAC,IAAY,EAAE,IAAY;IAC3C,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,KAAK,MAAM,SAAS,IAAI,IAAI,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;YACzB,IAAI,IAAI,CAAC,CAAC;QACZ,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,SAAS,CAAC,MAAM,CAAC;QAC7B,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC;AACnD,CAAC;AASD,iGAAiG;AACjG,SAAS,KAAK,CAAC,OAAe,EAAE,EAAU,EAAE,EAAU,EAAE,IAAY;IAClE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,KAAK,GAAG,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IAC/E,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,KAAK,GAAG,IAAI,GAAG,WAAW,CAAC,CAAC;IACvE,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QACtC,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACpC,OAAO,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,CAAE,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,GAAG,WAAW,CAAC,EAAE,CAAC;IAC/F,CAAC,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3D,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;IAC7D,OAAO;QACL,MAAM,EAAE,GAAG,CAAA,yCAAyC,IAAI,KAAK,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,CAAA,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,CAAE,KAAK,IAAI,UAAU,CAAC,SAAS;QACzJ,MAAM,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,EAAE;KACvE,CAAC;AACJ,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,GAAG,EAAE,EAAc;IAC9E,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,GAAG,CAAA,+CAA+C,KAAK,aAAa,MAAM,iCAAiC,CAAC;IACrH,CAAC;IACD,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,EAAE,KAAK,GAAG,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC;IAC7D,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC;IAClD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,GAAG,OAAO,CAAC,CAAC;IAC3E,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,OAAO,CAAC,CAAC;IAC9E,OAAO,GAAG,CAAA,+BAA+B,IAAI,QAAQ,GAAG,YAAY,KAAK,GAAG,IAAI,aAAa,MAAM,GAAG,GAAG,kCAAkC,IAAI,CAAC,MAAM,EAAE,CAAC;AAC3J,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,GAAG,EAAE,EAAc;IACjF,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,CAAC;IACzC,IAAI,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,GAAG,CAAA,kCAAkC,EAAE,SAAS,EAAE,QAAQ,MAAM,iCAAiC,CAAC;IAC3G,CAAC;IACD,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC;IAC9C,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC,CAAC;IACpG,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC;IACrG,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC;IAC5C,IAAI,MAAM,GAAG,MAAM,EAAE,CAAC;QACpB,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC7B,CAAC;IACD,OAAO,GAAG,CAAA,kCAAkC,EAAE,SAAS,EAAE,QAAQ,MAAM,kCAAkC,IAAI,CAAC,MAAM,EAAE,CAAC;AACzH,CAAC"}
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nicevg",
3
- "version": "0.19.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/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 line of text, sized the way nicevg measures text, placed with `at`. */
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 line's height, 1.2 font sizes. */
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
- let width = 0;
73
- for (const character of content) {
74
- width += WIDE.test(character) ? size : character.length * size * 0.59;
75
- }
76
- this.width = Math.ceil(width);
77
- this.height = Math.ceil(size * 1.2);
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 its line by `dominant-baseline`. */
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
- return `<text x="${x}" y="${y}" font-size="${this.size}" text-anchor="${anchor}" dominant-baseline="central">${escape(this.content)}</text>`;
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 line of text at `size` (14 by default), to embed in `svg` once placed with `at`. */
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
- * connectors: [{ from: "order", to: "bill", label: "invoice" }],
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, and rendering moves it but never
78
- * resizes it. Its other fields, such as `label`, are for the style to use.
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 StyledNode {
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 styled node's picture, from its own origin, with `svg` and `text`. */
93
- export type Style<Node extends StyledNode = StyledNode> = (node: Node) => SvgFragment;
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 | StyledNode> = {
97
- [Class in Extract<Node, StyledNode>["class"]]: Style<Extract<Node, { class: Class }>>;
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 | StyledNode = SketchNode | StyledNode> {
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 | StyledNode = SketchNode | StyledNode> = Sketch<Node> | { svg: string } | Svg;
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 | StyledNode>(input: Input<Node>): Svg {
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: styled nodes drawn, edges as connectors. */
218
- function sketchRequest<Node extends SketchNode | StyledNode>({ nodes, edges, connectors, style, rootAttrs }: Sketch<Node>) {
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 (!("class" in node)) {
227
- return node;
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
+ }