@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.
@@ -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 output nodes) */
73
- export const render = async (graph, config) => {
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
- /* determine the attachment sides of every edge (east/west),
85
- derived from the column relation of its endpoint nodes */
86
- const sides = edges.map((edge) => colOf(edge.source) < colOf(edge.target) ? { s: "e", t: "w" } : { s: "w", t: "e" });
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
- const { boxW, boxH, contentH } = measureNodes(nodes, config, () => scale / 2);
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"], 28 + (cnt - 1) * config["size-edge-track-gap"])));
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], 16, config["size-edge-track-gap"]))
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 */
@@ -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: (graph: Graph, config: Config) => Promise<Layout>;
8
- hub: (graph: Graph, config: Config) => Promise<Layout>;
9
- grid: (graph: Graph, config: Config) => Promise<Layout>;
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 their renderers */
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
- const explicit = options.config ?? {};
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
- /* lay out the graph model: either as a whole, or partitioned
64
- into its named groups which are laid out individually and
65
- then stacked vertically as decorated group boxes */
66
- const parts = partitionGroups(graph);
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 renderers[type](graph, config) :
69
- composeGroups(parts, await Promise.all(parts.map((part) => renderers[type](part.graph, config))), config);
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