@rse/gradia 1.1.8 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -36
- package/dst/gradia-api-config.d.ts +7 -0
- package/dst/gradia-api-config.js +54 -24
- package/dst/gradia-api-parser.js +10 -4
- package/dst/gradia-api-render-base.d.ts +28 -0
- package/dst/gradia-api-render-base.js +3 -2
- package/dst/gradia-api-render-container.d.ts +31 -0
- package/dst/gradia-api-render-container.js +300 -0
- package/dst/gradia-api-render-edge.d.ts +1 -1
- package/dst/gradia-api-render-edge.js +106 -37
- package/dst/gradia-api-render-group.d.ts +3 -2
- package/dst/gradia-api-render-group.js +16 -27
- package/dst/gradia-api-render-node.d.ts +9 -1
- package/dst/gradia-api-render-node.js +28 -14
- package/dst/gradia-api-render-svg.d.ts +1 -21
- package/dst/gradia-api-render-svg.js +82 -20
- package/dst/gradia-api-type-graph.d.ts +3 -2
- package/dst/gradia-api-type-graph.js +282 -126
- package/dst/gradia-api-type-grid.d.ts +3 -2
- package/dst/gradia-api-type-grid.js +5 -3
- package/dst/gradia-api-type-hub.d.ts +3 -2
- package/dst/gradia-api-type-hub.js +20 -10
- package/dst/gradia-api.d.ts +12 -4
- package/dst/gradia-api.js +33 -13
- package/dst/gradia-api.umd.js +2 -2
- package/dst/gradia-cli.js +4 -21
- package/dst/gradia-mcp.js +47 -53
- package/package.json +1 -1
|
@@ -8,6 +8,9 @@ import { simplifyPoly, assignPorts, assignTracks } from "./gradia-api-render-edg
|
|
|
8
8
|
/* the separator between a node id and its placement suffix,
|
|
9
9
|
distinguishing the two clones of a twice-placed node */
|
|
10
10
|
const CLONE = "\u0000";
|
|
11
|
+
/* rendering geometry constants */
|
|
12
|
+
const CHAN_W1 = 28; /* width of a one-edge inter-column channel */
|
|
13
|
+
const CHAN_PAD = 16; /* cross-axis padding inside a channel */
|
|
11
14
|
/* validate the constrained input topology and classify the declared
|
|
12
15
|
nodes: exactly one node is annotated with "primary: true" and every
|
|
13
16
|
edge connects the primary with another node. Each other node is
|
|
@@ -69,8 +72,11 @@ const classifyTopology = (graph) => {
|
|
|
69
72
|
});
|
|
70
73
|
return { center, inputs, outputs, nodes: [center, ...inputs, ...outputs], edges };
|
|
71
74
|
};
|
|
72
|
-
/* lay out a hub graph model (N input nodes, one central hub node, M
|
|
73
|
-
|
|
75
|
+
/* lay out a hub graph model (N input nodes, one central hub node, M
|
|
76
|
+
output nodes), for a containment level with the container placeholders
|
|
77
|
+
at their fixed sizes and the edge ends attaching to them at their
|
|
78
|
+
fixed ports */
|
|
79
|
+
export const render = async (graph, config, level = {}) => {
|
|
74
80
|
/* validate the constrained input topology and classify the
|
|
75
81
|
declared nodes into the input and output columns */
|
|
76
82
|
const { center, inputs, outputs, nodes, edges } = classifyTopology(graph);
|
|
@@ -81,21 +87,25 @@ export const render = async (graph, config) => {
|
|
|
81
87
|
const scale = config["size-node-height-scale"];
|
|
82
88
|
/* the fixed three-column layout assignment */
|
|
83
89
|
const colOf = (id) => id === center.id ? 1 : (inputSet.has(id) ? 0 : 2);
|
|
84
|
-
/*
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
/* the attachment sides of every edge (east/west): every edge runs
|
|
91
|
+
from the input toward the output column (see classifyTopology),
|
|
92
|
+
hence leaves on the east and enters on the west side */
|
|
93
|
+
const sides = edges.map(() => ({ s: "e", t: "w" }));
|
|
87
94
|
/* determine node box sizes from their textual content (all boxes at
|
|
88
95
|
half height scale), then grow every box whose edge attachments
|
|
89
96
|
exceed the configured per-side maximum, step-wise by one port
|
|
90
97
|
separation per additional edge, so the edges keep enough
|
|
91
|
-
attachment room without a fixed height increase
|
|
92
|
-
|
|
98
|
+
attachment room without a fixed height increase (the fixed-size
|
|
99
|
+
boxes of a containment level are exempt) */
|
|
100
|
+
const { boxW, boxH, contentH } = measureNodes(nodes, config, () => scale / 2, level.fixedSize);
|
|
93
101
|
const portCnt = new Map();
|
|
94
102
|
edges.forEach((edge, i) => {
|
|
95
103
|
portCnt.set(`${sides[i].s}:${edge.source}`, (portCnt.get(`${sides[i].s}:${edge.source}`) ?? 0) + 1);
|
|
96
104
|
portCnt.set(`${sides[i].t}:${edge.target}`, (portCnt.get(`${sides[i].t}:${edge.target}`) ?? 0) + 1);
|
|
97
105
|
});
|
|
98
106
|
for (const node of nodes) {
|
|
107
|
+
if (level.fixedSize?.has(node.id))
|
|
108
|
+
continue;
|
|
99
109
|
const cnt = Math.max(portCnt.get(`w:${node.id}`) ?? 0, portCnt.get(`e:${node.id}`) ?? 0);
|
|
100
110
|
if (cnt > config["hub-node-degree-max"])
|
|
101
111
|
boxH.set(node.id, boxH.get(node.id) +
|
|
@@ -130,7 +140,7 @@ export const render = async (graph, config) => {
|
|
|
130
140
|
edges.filter((edge) => chanOf(edge) === 0).length,
|
|
131
141
|
edges.filter((edge) => chanOf(edge) === 1).length
|
|
132
142
|
];
|
|
133
|
-
const chanW = chanCnt.map((cnt) => Math.min(config["hub-channel-width-max"], Math.max(config["hub-channel-width-min"],
|
|
143
|
+
const chanW = chanCnt.map((cnt) => Math.min(config["hub-channel-width-max"], Math.max(config["hub-channel-width-min"], CHAN_W1 + (cnt - 1) * config["size-edge-track-gap"])));
|
|
134
144
|
const colLX = [];
|
|
135
145
|
let x = margin;
|
|
136
146
|
for (let c = 0; c < 3; c++) {
|
|
@@ -150,7 +160,7 @@ export const render = async (graph, config) => {
|
|
|
150
160
|
};
|
|
151
161
|
const cy = (id) => nodeCY.get(id);
|
|
152
162
|
/* distribute the edge attachment ports along each node side */
|
|
153
|
-
const portPos = assignPorts(edges, sides, cx, cy, boxW, boxH, config["size-edge-port-gap"]);
|
|
163
|
+
const portPos = assignPorts(edges, sides, cx, cy, boxW, boxH, config["size-edge-port-gap"], undefined, level.fixedPort);
|
|
154
164
|
/* assign the vertical tracks within each channel (see assignTracks
|
|
155
165
|
for the crossing-avoiding ordering scheme; a hub graph has no backward
|
|
156
166
|
edges, hence no channel user is ever mirrored) */
|
|
@@ -165,7 +175,7 @@ export const render = async (graph, config) => {
|
|
|
165
175
|
});
|
|
166
176
|
const chanOff = new Map();
|
|
167
177
|
chanUsers.forEach((users, c) => {
|
|
168
|
-
for (const [edge, off] of assignTracks(users, chanW[c],
|
|
178
|
+
for (const [edge, off] of assignTracks(users, chanW[c], CHAN_PAD, config["size-edge-track-gap"]))
|
|
169
179
|
chanOff.set(`${c}:${edge}`, off);
|
|
170
180
|
});
|
|
171
181
|
/* determine the X position of an edge's assigned track within a channel */
|
package/dst/gradia-api.d.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { Graph } from "./gradia-api-model.js";
|
|
2
2
|
import { Config } from "./gradia-api-config.js";
|
|
3
|
-
import { Layout } from "./gradia-api-render-svg.js";
|
|
4
3
|
export type { Attr, Node, Edge, Graph } from "./gradia-api-model.js";
|
|
5
4
|
export type { Config } from "./gradia-api-config.js";
|
|
6
5
|
declare const renderers: {
|
|
7
|
-
graph:
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
graph: {
|
|
7
|
+
render: (graph: Graph, config: Config, level?: import("./gradia-api-render-container.js").LevelContext) => Promise<import("./gradia-api-render-base.js").Layout>;
|
|
8
|
+
gated: true;
|
|
9
|
+
};
|
|
10
|
+
hub: {
|
|
11
|
+
render: (graph: Graph, config: Config, level?: import("./gradia-api-render-container.js").LevelContext) => Promise<import("./gradia-api-render-base.js").Layout>;
|
|
12
|
+
gated: false;
|
|
13
|
+
};
|
|
14
|
+
grid: {
|
|
15
|
+
render: (graph: Graph, config: Config, level?: import("./gradia-api-render-container.js").LevelContext) => Promise<import("./gradia-api-render-base.js").Layout>;
|
|
16
|
+
gated: false;
|
|
17
|
+
};
|
|
10
18
|
};
|
|
11
19
|
export type DiagramType = keyof typeof renderers;
|
|
12
20
|
export declare const diagramTypes: DiagramType[];
|
package/dst/gradia-api.js
CHANGED
|
@@ -4,17 +4,19 @@
|
|
|
4
4
|
** Distributed under MIT license <https://spdx.org/licenses/MIT.html>
|
|
5
5
|
*/
|
|
6
6
|
import { parse } from "./gradia-api-parser.js";
|
|
7
|
-
import { configDefaults, parseDirectives } from "./gradia-api-config.js";
|
|
7
|
+
import { configDefaults, parseDirectives, validateConfig } from "./gradia-api-config.js";
|
|
8
8
|
import { partitionGroups, composeGroups } from "./gradia-api-render-group.js";
|
|
9
|
+
import { containmentOf, renderContained } from "./gradia-api-render-container.js";
|
|
9
10
|
import { renderSVG } from "./gradia-api-render-svg.js";
|
|
10
11
|
import { render as renderGraph } from "./gradia-api-type-graph.js";
|
|
11
12
|
import { render as renderHub } from "./gradia-api-type-hub.js";
|
|
12
13
|
import { render as renderGrid } from "./gradia-api-type-grid.js";
|
|
13
|
-
/* the supported diagram types and
|
|
14
|
+
/* the supported diagram types, their renderers, and whether they let
|
|
15
|
+
the edges crossing a container boundary continue to the inner nodes */
|
|
14
16
|
const renderers = {
|
|
15
|
-
graph: renderGraph,
|
|
16
|
-
hub: renderHub,
|
|
17
|
-
grid: renderGrid
|
|
17
|
+
graph: { render: renderGraph, gated: true },
|
|
18
|
+
hub: { render: renderHub, gated: false },
|
|
19
|
+
grid: { render: renderGrid, gated: false }
|
|
18
20
|
};
|
|
19
21
|
export const diagramTypes = Object.keys(renderers);
|
|
20
22
|
export const diagramTypeDefault = "graph";
|
|
@@ -57,16 +59,34 @@ export class Gradia {
|
|
|
57
59
|
const format = options.format ?? diagramFormatDefault;
|
|
58
60
|
if (!diagramFormats.includes(format))
|
|
59
61
|
throw new Error(`invalid output format "${format}"`);
|
|
60
|
-
/* layer the rendering configuration
|
|
61
|
-
|
|
62
|
+
/* validate and layer the rendering configuration (an option
|
|
63
|
+
explicitly set to "undefined" is dropped, as it would otherwise
|
|
64
|
+
override its default instead of falling back to it) */
|
|
65
|
+
const explicit = validateConfig(options.config ?? {}, { trusted: true });
|
|
62
66
|
const config = { ...configDefaults, ...explicit };
|
|
63
|
-
/*
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
/* validate the graph model integrity: the map key and the id of
|
|
68
|
+
a node store the same identity (the layout code mixes lookups
|
|
69
|
+
by key and comparisons by id), and every edge endpoint is a
|
|
70
|
+
declared node (the layout code looks up endpoints unchecked) */
|
|
71
|
+
for (const [key, node] of graph.nodes)
|
|
72
|
+
if (node.id !== key)
|
|
73
|
+
throw new Error(`node id "${node.id}" does not match its map key "${key}"`);
|
|
74
|
+
for (const edge of graph.edges)
|
|
75
|
+
for (const id of [edge.source, edge.target])
|
|
76
|
+
if (!graph.nodes.has(id))
|
|
77
|
+
throw new Error(`endpoint "${id}" of edge ` +
|
|
78
|
+
`"${edge.source}" --> "${edge.target}" is not a declared node`);
|
|
79
|
+
/* determine (and validate) the containment tree of the graph */
|
|
80
|
+
const containment = containmentOf(graph, diagramTypes);
|
|
81
|
+
/* lay out the graph model (with its containers as nested
|
|
82
|
+
levels): either as a whole, or partitioned into its named
|
|
83
|
+
groups which are laid out individually and then stacked
|
|
84
|
+
vertically as decorated group boxes */
|
|
85
|
+
const render = (part) => renderContained(part, containment, type, renderers, config);
|
|
86
|
+
const parts = partitionGroups(graph, containment);
|
|
67
87
|
const layout = parts === null ?
|
|
68
|
-
await
|
|
69
|
-
composeGroups(parts, await Promise.all(parts.map((part) =>
|
|
88
|
+
await render(graph) :
|
|
89
|
+
composeGroups(parts, await Promise.all(parts.map((part) => render(part.graph))), config);
|
|
70
90
|
/* derive the identifier seed of the SVG document from everything
|
|
71
91
|
which determines the rendered output, so that regenerating an
|
|
72
92
|
unchanged diagram yields a byte-identical SVG (the explicitly
|