@weasel-js/diagram-elk 0.0.0-stage → 1.10.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/LICENSE +21 -0
- package/README.md +80 -2
- package/dist/index.d.ts +80 -0
- package/dist/index.js +253 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 orochi235
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,81 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @weasel-js/diagram-elk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Lays a `@weasel-js/diagram` graph out with [ELK](https://eclipse.dev/elk/), the
|
|
4
|
+
Eclipse Layout Kernel, through [elkjs](https://github.com/kieler/elkjs), and
|
|
5
|
+
answers with the diagram package's own `LayoutPlan`, so the result is applied
|
|
6
|
+
the way `layered` or `tree` is. ELK places ports, edge labels and nested groups
|
|
7
|
+
well; use it to lay a generated diagram out once, or to check one of the
|
|
8
|
+
diagram package's layouts against it.
|
|
9
|
+
|
|
10
|
+
Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
|
|
11
|
+
scene-graph canvas kit for React. See the
|
|
12
|
+
[API reference](https://orochi235.github.io/weasel/#pkg/diagram-elk).
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install @weasel-js/diagram-elk
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Laying a diagram out
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { applyLayout, buildGraph, routesOf } from '@weasel-js/diagram';
|
|
22
|
+
import { elkPlan } from '@weasel-js/diagram-elk';
|
|
23
|
+
|
|
24
|
+
const graph = buildGraph(source);
|
|
25
|
+
const plan = await elkPlan(graph, { direction: 'right', nodeGap: 32 });
|
|
26
|
+
applyLayout(scene, plan.moves, { routes: routesOf(graph, plan) });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
ELK answers asynchronously, so `elkPlan` returns a promise and is not a
|
|
30
|
+
`LayoutFn`: it cannot run inside a live layout, which recomputes every frame.
|
|
31
|
+
|
|
32
|
+
What goes in, and how ELK reads it:
|
|
33
|
+
|
|
34
|
+
| Diagram | ELK |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| A participant | A node of its own size. |
|
|
37
|
+
| A port an edge names | A fixed port where the diagram draws it, cast onto the node's outline. |
|
|
38
|
+
| A group | A compound node, nested the way the groups nest, padded by its `inset`. `minWidth` is its minimum width. |
|
|
39
|
+
| An edge to or from a group | One edge to the compound node, not one per member. |
|
|
40
|
+
| An edge label's room | An edge label of that size. |
|
|
41
|
+
| `direction`, `nodeGap`, `rankGap` | `elk.direction`, `elk.spacing.nodeNode`, `elk.layered.spacing.nodeNodeBetweenLayers`. |
|
|
42
|
+
| `order`, `chain`, `chainSide` | Not taken; ELK orders ranks itself. |
|
|
43
|
+
|
|
44
|
+
What comes back, beyond a `LayoutPlan`'s `moves`, `waypoints` and `labels`:
|
|
45
|
+
|
|
46
|
+
| Field | What it holds |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `ports` | For each edge, the port it leaves and arrives by: the one it names, or else the declared port nearest where ELK met the node. |
|
|
49
|
+
| `routes` | Each routed edge end to end. `waypoints` is the same route without its two ends. |
|
|
50
|
+
| `groups` | Each group's box as ELK sized it. Nothing writes these, since the diagram derives a group's box from its members. |
|
|
51
|
+
| `unheld` | Pinned nodes ELK put somewhere else. The layout is translated so the first pin stays where it is; ELK cannot be told about the rest. |
|
|
52
|
+
|
|
53
|
+
An edge the author routed (`GraphEdge.authored`) still shapes the layout but
|
|
54
|
+
gets no route back. Anything else ELK takes goes in `layoutOptions`, applied
|
|
55
|
+
over the options derived from the rest:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
await elkPlan(graph, { layoutOptions: { 'elk.algorithm': 'mrtree' } });
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Size, and where it runs
|
|
62
|
+
|
|
63
|
+
elkjs 0.12.0's bundled build, `elkjs/lib/elk.bundled.js`, is 1,609,707 bytes:
|
|
64
|
+
466,718 gzipped at level 9 and 348,494 under brotli. This package imports it
|
|
65
|
+
dynamically on the first layout, so a bundler splits it into a chunk of its own
|
|
66
|
+
that loads only when a layout runs. The adapter itself is about 10 KB.
|
|
67
|
+
|
|
68
|
+
The bundled build runs on the calling thread. To keep a large layout off it,
|
|
69
|
+
construct elkjs on its web worker and pass that in:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import ELK from 'elkjs/lib/elk-api.js';
|
|
73
|
+
|
|
74
|
+
const elk = new ELK({ workerUrl: new URL('elkjs/lib/elk-worker.min.js', import.meta.url).href });
|
|
75
|
+
await elkPlan(graph, { elk });
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
This package is MIT. elkjs, which it depends on, is EPL-2.0 OR
|
|
81
|
+
GPL-3.0-or-later.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Vec2 } from '@weasel-js/core/math';
|
|
2
|
+
import { LayoutOptions, LayoutPlan, Bounds, Graph, LayoutResult, EdgeLabelRoom, GraphNode, GraphGroup } from '@weasel-js/diagram/layout';
|
|
3
|
+
import { ELK, ElkNode } from 'elkjs/lib/elk-api';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The adapter: lay a diagram's `Graph` out with ELK and answer with a
|
|
7
|
+
* `LayoutPlan`, so the result goes through `applyLayout` and `routesOf` like
|
|
8
|
+
* any of the diagram package's own layouts.
|
|
9
|
+
*
|
|
10
|
+
* ELK answers asynchronously, so this is not a `LayoutFn` and cannot stand in
|
|
11
|
+
* one's place in a live run; it is for laying a diagram out once.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Anything with ELK's `layout`: the bundled instance, or one on a worker. */
|
|
15
|
+
type ElkRunner = Pick<ELK, 'layout'>;
|
|
16
|
+
/** {@link elkPlan}'s options: the diagram's own, read as ELK's nearest
|
|
17
|
+
* equivalents, and a way through to everything else ELK takes. */
|
|
18
|
+
interface ElkLayoutOptions extends Omit<LayoutOptions, 'order' | 'chain' | 'chainSide'> {
|
|
19
|
+
/** The ELK to run on. Default: elkjs's bundled build, loaded on first use
|
|
20
|
+
* and run on the calling thread. Pass one built on `elk-worker` to keep a
|
|
21
|
+
* large layout off it. */
|
|
22
|
+
elk?: ElkRunner;
|
|
23
|
+
/** ELK options for the root, applied over the ones this derives from the
|
|
24
|
+
* rest, under their full or `elk.`-prefixed ids. Default algorithm
|
|
25
|
+
* `elk.layered`. */
|
|
26
|
+
layoutOptions?: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
/** {@link elkPlan}'s answer: a layout plan, and what else ELK placed. */
|
|
29
|
+
interface ElkPlan extends LayoutPlan {
|
|
30
|
+
/** Each group's box, in world coordinates. The diagram derives a box from
|
|
31
|
+
* its members, so nothing writes these; they are what ELK left room for. */
|
|
32
|
+
groups: ReadonlyMap<string, Bounds>;
|
|
33
|
+
/** Each routed edge end to end, keyed by the edge as drawn. `waypoints` is
|
|
34
|
+
* the same route without its two ends. */
|
|
35
|
+
routes: ReadonlyMap<string, readonly Vec2[]>;
|
|
36
|
+
/** Pinned nodes ELK put somewhere else. Only the first pin holds: the
|
|
37
|
+
* layout is translated onto it, and ELK has no way to be told the rest. */
|
|
38
|
+
unheld: ReadonlySet<string>;
|
|
39
|
+
}
|
|
40
|
+
/** The ELK options a run starts from, before `opts.layoutOptions`. */
|
|
41
|
+
declare function rootOptions(opts?: ElkLayoutOptions): Record<string, string>;
|
|
42
|
+
/** Lay `graph` out with ELK: where each participant goes, each edge's route
|
|
43
|
+
* and the port at either end, each label's place, and each group's box. */
|
|
44
|
+
declare function elkPlan(graph: Graph, opts?: ElkLayoutOptions): Promise<ElkPlan>;
|
|
45
|
+
/** {@link elkPlan}'s moves alone. */
|
|
46
|
+
declare function elkLayout(graph: Graph, opts?: ElkLayoutOptions): Promise<LayoutResult>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A diagram `Graph` as an ELK graph: participants as leaf nodes, groups as
|
|
50
|
+
* compound nodes nested the way the groups are, the ports an edge names as
|
|
51
|
+
* fixed ports, and each drawn edge once.
|
|
52
|
+
*
|
|
53
|
+
* Every ELK id is minted here rather than reused from the graph, so nothing a
|
|
54
|
+
* caller names can collide across ELK's one id space for nodes, ports, edges
|
|
55
|
+
* and labels. `ElkIndex` maps them back.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/** One drawn edge: a stand-in `expandGroupEdges` made folds back into this. */
|
|
59
|
+
interface DrawnEdge {
|
|
60
|
+
id: string;
|
|
61
|
+
from: string;
|
|
62
|
+
to: string;
|
|
63
|
+
fromPort?: string;
|
|
64
|
+
toPort?: string;
|
|
65
|
+
label?: EdgeLabelRoom;
|
|
66
|
+
authored: boolean;
|
|
67
|
+
}
|
|
68
|
+
/** ELK ids back to the graph's. */
|
|
69
|
+
interface ElkIndex {
|
|
70
|
+
nodes: Map<string, GraphNode>;
|
|
71
|
+
groups: Map<string, GraphGroup>;
|
|
72
|
+
edges: Map<string, DrawnEdge>;
|
|
73
|
+
}
|
|
74
|
+
/** The ELK graph for `graph`, its root carrying `layoutOptions`. */
|
|
75
|
+
declare function toElk(graph: Graph, layoutOptions: Record<string, string>): {
|
|
76
|
+
root: ElkNode;
|
|
77
|
+
index: ElkIndex;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export { type DrawnEdge, type ElkIndex, type ElkLayoutOptions, type ElkPlan, type ElkRunner, elkLayout, elkPlan, rootOptions, toElk };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { DEFAULT_NODE_GAP, DEFAULT_RANK_GAP, pinnedSet, anchorOffset, translated, DEFAULT_PORTS, portsOnOutline } from '@weasel-js/diagram/layout';
|
|
2
|
+
import { nearestOnLine } from '@weasel-js/geom';
|
|
3
|
+
|
|
4
|
+
// src/elkPlan.ts
|
|
5
|
+
|
|
6
|
+
// src/fromElk.ts
|
|
7
|
+
function fromElk(root, index) {
|
|
8
|
+
const out = { nodes: /* @__PURE__ */ new Map(), groups: /* @__PURE__ */ new Map(), routes: /* @__PURE__ */ new Map(), labels: /* @__PURE__ */ new Map() };
|
|
9
|
+
const edges = [];
|
|
10
|
+
const visit = (n) => {
|
|
11
|
+
edges.push(...n.edges ?? []);
|
|
12
|
+
for (const child of n.children ?? []) {
|
|
13
|
+
const x = child.x ?? 0;
|
|
14
|
+
const y = child.y ?? 0;
|
|
15
|
+
const node = index.nodes.get(child.id);
|
|
16
|
+
if (node !== void 0) out.nodes.set(node.id, { x, y });
|
|
17
|
+
const group = index.groups.get(child.id);
|
|
18
|
+
if (group !== void 0) out.groups.set(group.id, { x, y, width: child.width ?? 0, height: child.height ?? 0 });
|
|
19
|
+
visit(child);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
visit(root);
|
|
23
|
+
for (const e of edges) {
|
|
24
|
+
const drawn = index.edges.get(e.id);
|
|
25
|
+
if (drawn === void 0) continue;
|
|
26
|
+
const route = [];
|
|
27
|
+
for (const s of e.sections ?? []) route.push(s.startPoint, ...s.bendPoints ?? [], s.endPoint);
|
|
28
|
+
if (route.length > 0) out.routes.set(drawn.id, dedupe(route));
|
|
29
|
+
const label = e.labels?.[0];
|
|
30
|
+
if (label !== void 0 && drawn.label !== void 0) {
|
|
31
|
+
out.labels.set(drawn.id, { x: label.x ?? 0, y: label.y ?? 0, width: label.width ?? 0, height: label.height ?? 0 });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
function dedupe(route) {
|
|
37
|
+
return route.filter((p, i) => i === 0 || p.x !== route[i - 1].x || p.y !== route[i - 1].y);
|
|
38
|
+
}
|
|
39
|
+
function portPoints(node, at) {
|
|
40
|
+
const bounds = { x: at.x, y: at.y, width: node.bounds.width, height: node.bounds.height };
|
|
41
|
+
const ports = (node.ports ?? DEFAULT_PORTS).map((spec) => ({
|
|
42
|
+
id: spec.id,
|
|
43
|
+
nodeId: node.id,
|
|
44
|
+
point: { x: at.x + spec.at.u * bounds.width, y: at.y + spec.at.v * bounds.height },
|
|
45
|
+
normal: null
|
|
46
|
+
}));
|
|
47
|
+
return node.outline === void 0 ? ports : portsOnOutline(ports, node.outline, bounds);
|
|
48
|
+
}
|
|
49
|
+
function drawnEdges(graph) {
|
|
50
|
+
const declares = (id, port) => {
|
|
51
|
+
const node = graph.node(id);
|
|
52
|
+
return port !== void 0 && node !== void 0 && (node.ports ?? DEFAULT_PORTS).some((p) => p.id === port);
|
|
53
|
+
};
|
|
54
|
+
const seen = /* @__PURE__ */ new Map();
|
|
55
|
+
for (const e of graph.edges) {
|
|
56
|
+
const id = e.origin?.id ?? e.id;
|
|
57
|
+
if (seen.has(id)) continue;
|
|
58
|
+
const from = e.origin?.from ?? e.from;
|
|
59
|
+
const to = e.origin?.to ?? e.to;
|
|
60
|
+
seen.set(id, {
|
|
61
|
+
id,
|
|
62
|
+
from,
|
|
63
|
+
to,
|
|
64
|
+
...declares(from, e.fromPort) ? { fromPort: e.fromPort } : {},
|
|
65
|
+
...declares(to, e.toPort) ? { toPort: e.toPort } : {},
|
|
66
|
+
...e.label !== void 0 ? { label: e.label } : {},
|
|
67
|
+
authored: e.authored === true
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return [...seen.values()];
|
|
71
|
+
}
|
|
72
|
+
function toElk(graph, layoutOptions) {
|
|
73
|
+
const index = { nodes: /* @__PURE__ */ new Map(), groups: /* @__PURE__ */ new Map(), edges: /* @__PURE__ */ new Map() };
|
|
74
|
+
const groups = graph.groups ?? [];
|
|
75
|
+
const elkIdOf = /* @__PURE__ */ new Map();
|
|
76
|
+
graph.nodes.forEach((n, i) => {
|
|
77
|
+
elkIdOf.set(n.id, `n${i}`);
|
|
78
|
+
index.nodes.set(`n${i}`, n);
|
|
79
|
+
});
|
|
80
|
+
groups.forEach((g, i) => {
|
|
81
|
+
elkIdOf.set(g.id, `g${i}`);
|
|
82
|
+
index.groups.set(`g${i}`, g);
|
|
83
|
+
});
|
|
84
|
+
const edges = drawnEdges(graph).filter((e) => elkIdOf.has(e.from) && elkIdOf.has(e.to));
|
|
85
|
+
const named = /* @__PURE__ */ new Map();
|
|
86
|
+
for (const e of edges) {
|
|
87
|
+
if (e.fromPort !== void 0) addTo(named, e.from, e.fromPort);
|
|
88
|
+
if (e.toPort !== void 0) addTo(named, e.to, e.toPort);
|
|
89
|
+
}
|
|
90
|
+
const leaves = /* @__PURE__ */ new Map();
|
|
91
|
+
for (const n of graph.nodes) {
|
|
92
|
+
pushTo(leaves, n.group, leafOf(n, elkIdOf.get(n.id), named.get(n.id)));
|
|
93
|
+
}
|
|
94
|
+
const transposed = minimumTransposed(layoutOptions);
|
|
95
|
+
const boxOf = (g) => ({
|
|
96
|
+
id: elkIdOf.get(g.id),
|
|
97
|
+
layoutOptions: groupOptions(g, transposed),
|
|
98
|
+
children: [...groups.filter((c) => c.parent === g.id).map(boxOf), ...leaves.get(g.id) ?? []]
|
|
99
|
+
});
|
|
100
|
+
const elkEdges = edges.map((e, i) => {
|
|
101
|
+
index.edges.set(`e${i}`, e);
|
|
102
|
+
const end = (id, port) => port !== void 0 ? portId(elkIdOf.get(id), port) : elkIdOf.get(id);
|
|
103
|
+
return {
|
|
104
|
+
id: `e${i}`,
|
|
105
|
+
sources: [end(e.from, e.fromPort)],
|
|
106
|
+
targets: [end(e.to, e.toPort)],
|
|
107
|
+
// ELK leaves a label with no text at the origin, unplaced, in some
|
|
108
|
+
// graphs; the text is never measured, since the size is given.
|
|
109
|
+
...e.label !== void 0 ? { labels: [{ id: `e${i}l`, text: e.id, width: e.label.width, height: e.label.height }] } : {}
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
const root = {
|
|
113
|
+
id: "root",
|
|
114
|
+
layoutOptions,
|
|
115
|
+
children: [...groups.filter((g) => g.parent === void 0).map(boxOf), ...leaves.get(void 0) ?? []],
|
|
116
|
+
edges: elkEdges
|
|
117
|
+
};
|
|
118
|
+
return { root, index };
|
|
119
|
+
}
|
|
120
|
+
function leafOf(node, id, ports) {
|
|
121
|
+
const leaf = { id, width: node.bounds.width, height: node.bounds.height };
|
|
122
|
+
if (ports === void 0) return leaf;
|
|
123
|
+
const elkPorts = portPoints(node, { x: 0, y: 0 }).filter((p) => ports.has(p.id)).map((p) => ({ id: portId(id, p.id), x: p.point.x, y: p.point.y, width: 0, height: 0 }));
|
|
124
|
+
return { ...leaf, ports: elkPorts, layoutOptions: { "elk.portConstraints": "FIXED_POS" } };
|
|
125
|
+
}
|
|
126
|
+
function groupOptions(g, transposed) {
|
|
127
|
+
const { top, right, bottom, left } = g.inset;
|
|
128
|
+
const options = { "elk.padding": `[top=${top},left=${left},bottom=${bottom},right=${right}]` };
|
|
129
|
+
if (g.minWidth !== void 0) {
|
|
130
|
+
options["elk.nodeSize.constraints"] = "MINIMUM_SIZE";
|
|
131
|
+
options["elk.nodeSize.minimum"] = transposed ? `(0, ${g.minWidth})` : `(${g.minWidth}, 0)`;
|
|
132
|
+
}
|
|
133
|
+
return options;
|
|
134
|
+
}
|
|
135
|
+
function minimumTransposed(options) {
|
|
136
|
+
const read = (key) => options[`elk.${key}`] ?? options[`org.eclipse.elk.${key}`];
|
|
137
|
+
const direction = read("direction");
|
|
138
|
+
return read("hierarchyHandling") === "INCLUDE_CHILDREN" && (direction === "DOWN" || direction === "UP");
|
|
139
|
+
}
|
|
140
|
+
function portId(nodeElkId, port) {
|
|
141
|
+
return `${nodeElkId}:${port}`;
|
|
142
|
+
}
|
|
143
|
+
function addTo(map, key, value) {
|
|
144
|
+
const set = map.get(key);
|
|
145
|
+
if (set === void 0) map.set(key, /* @__PURE__ */ new Set([value]));
|
|
146
|
+
else set.add(value);
|
|
147
|
+
}
|
|
148
|
+
function pushTo(map, key, value) {
|
|
149
|
+
const list = map.get(key);
|
|
150
|
+
if (list === void 0) map.set(key, [value]);
|
|
151
|
+
else list.push(value);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/elkPlan.ts
|
|
155
|
+
var DIRECTIONS = { down: "DOWN", up: "UP", right: "RIGHT", left: "LEFT" };
|
|
156
|
+
function rootOptions(opts = {}) {
|
|
157
|
+
const spacing = String(opts.nodeGap ?? DEFAULT_NODE_GAP);
|
|
158
|
+
return {
|
|
159
|
+
"elk.algorithm": "layered",
|
|
160
|
+
"elk.direction": DIRECTIONS[opts.direction ?? "down"],
|
|
161
|
+
"elk.spacing.nodeNode": spacing,
|
|
162
|
+
"elk.layered.spacing.nodeNodeBetweenLayers": String(opts.rankGap ?? DEFAULT_RANK_GAP),
|
|
163
|
+
"elk.hierarchyHandling": "INCLUDE_CHILDREN",
|
|
164
|
+
"elk.json.shapeCoords": "ROOT",
|
|
165
|
+
"elk.json.edgeCoords": "ROOT",
|
|
166
|
+
...opts.layoutOptions
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
var bundled;
|
|
170
|
+
function defaultElk() {
|
|
171
|
+
bundled ??= import('elkjs/lib/elk.bundled.js').then(({ default: ELKConstructor }) => new ELKConstructor());
|
|
172
|
+
return bundled;
|
|
173
|
+
}
|
|
174
|
+
async function elkPlan(graph, opts = {}) {
|
|
175
|
+
const { root, index } = toElk(graph, rootOptions(opts));
|
|
176
|
+
const elk = opts.elk ?? await defaultElk();
|
|
177
|
+
const placement = fromElk(await elk.layout(root), index);
|
|
178
|
+
return planFrom(graph, placement, opts);
|
|
179
|
+
}
|
|
180
|
+
async function elkLayout(graph, opts = {}) {
|
|
181
|
+
return (await elkPlan(graph, opts)).moves;
|
|
182
|
+
}
|
|
183
|
+
function planFrom(graph, placed, opts) {
|
|
184
|
+
const pinned = pinnedSet(graph, opts);
|
|
185
|
+
const tolerance = opts.tolerance ?? 1e-6;
|
|
186
|
+
const d = anchorOffset(graph, placed.nodes, pinned);
|
|
187
|
+
const shift = (p) => ({ x: p.x + d.x, y: p.y + d.y });
|
|
188
|
+
const unheld = /* @__PURE__ */ new Set();
|
|
189
|
+
for (const node of graph.nodes) {
|
|
190
|
+
const at = placed.nodes.get(node.id);
|
|
191
|
+
if (!pinned.has(node.id) || at === void 0) continue;
|
|
192
|
+
const to = shift(at);
|
|
193
|
+
if (Math.abs(to.x - node.bounds.x) > tolerance || Math.abs(to.y - node.bounds.y) > tolerance) unheld.add(node.id);
|
|
194
|
+
}
|
|
195
|
+
const groups = /* @__PURE__ */ new Map();
|
|
196
|
+
for (const [id, box] of placed.groups) groups.set(id, { ...box, ...shift(box) });
|
|
197
|
+
const routes = /* @__PURE__ */ new Map();
|
|
198
|
+
const waypoints = /* @__PURE__ */ new Map();
|
|
199
|
+
const labels = /* @__PURE__ */ new Map();
|
|
200
|
+
const ports = /* @__PURE__ */ new Map();
|
|
201
|
+
for (const edge of drawnEdges(graph)) {
|
|
202
|
+
const elkRoute = placed.routes.get(edge.id);
|
|
203
|
+
if (edge.authored || elkRoute === void 0 || elkRoute.length < 2) continue;
|
|
204
|
+
const route = elkRoute.map(shift);
|
|
205
|
+
routes.set(edge.id, route);
|
|
206
|
+
if (route.length > 2) waypoints.set(edge.id, route.slice(1, -1));
|
|
207
|
+
const box = placed.labels.get(edge.id);
|
|
208
|
+
if (box !== void 0) labels.set(edge.id, nearestOnRoute(route, shift({ x: box.x + box.width / 2, y: box.y + box.height / 2 })));
|
|
209
|
+
const named = endPorts(graph, edge, route, placed, shift);
|
|
210
|
+
if (named.from !== void 0 || named.to !== void 0) ports.set(edge.id, named);
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
moves: translated(graph, placed.nodes, pinned, tolerance),
|
|
214
|
+
waypoints,
|
|
215
|
+
labels,
|
|
216
|
+
ports,
|
|
217
|
+
groups,
|
|
218
|
+
routes,
|
|
219
|
+
unheld
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function endPorts(graph, edge, route, placed, shift) {
|
|
223
|
+
const nearest = (id, point) => {
|
|
224
|
+
const node = graph.node(id);
|
|
225
|
+
const at = placed.nodes.get(id);
|
|
226
|
+
if (node === void 0 || at === void 0) return void 0;
|
|
227
|
+
let best;
|
|
228
|
+
let bestDist = Infinity;
|
|
229
|
+
for (const port of portPoints(node, shift(at))) {
|
|
230
|
+
const dist = Math.hypot(port.point.x - point.x, port.point.y - point.y);
|
|
231
|
+
if (dist < bestDist) [best, bestDist] = [port.id, dist];
|
|
232
|
+
}
|
|
233
|
+
return best;
|
|
234
|
+
};
|
|
235
|
+
const from = edge.fromPort ?? nearest(edge.from, route[0]);
|
|
236
|
+
const to = edge.toPort ?? nearest(edge.to, route.at(-1));
|
|
237
|
+
return { ...from !== void 0 ? { from } : {}, ...to !== void 0 ? { to } : {} };
|
|
238
|
+
}
|
|
239
|
+
function nearestOnRoute(route, probe) {
|
|
240
|
+
let best = route[0];
|
|
241
|
+
let bestDist = Infinity;
|
|
242
|
+
for (let i = 1; i < route.length; i++) {
|
|
243
|
+
const a = route[i - 1];
|
|
244
|
+
const b = route[i];
|
|
245
|
+
const hit = nearestOnLine(probe.x, probe.y, a.x, a.y, b.x, b.y);
|
|
246
|
+
if (hit.dist < bestDist) [best, bestDist] = [{ x: hit.x, y: hit.y }, hit.dist];
|
|
247
|
+
}
|
|
248
|
+
return best;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export { elkLayout, elkPlan, rootOptions, toElk };
|
|
252
|
+
//# sourceMappingURL=index.js.map
|
|
253
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fromElk.ts","../src/toElk.ts","../src/elkPlan.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Reading a laid-out ELK graph back into the diagram's terms. The graph went\n * in asking for root coordinates (`elk.json.shapeCoords` and `edgeCoords`), so\n * every position read here is already in one frame, ELK's.\n */\nimport type { Vec2 } from '@weasel-js/core/math';\nimport type { Bounds } from '@weasel-js/diagram/layout';\nimport type { ElkExtendedEdge, ElkNode } from 'elkjs/lib/elk-api';\nimport type { ElkIndex } from './toElk';\n\n/** Everything ELK placed, keyed by the graph's ids, in ELK's frame. */\nexport interface ElkPlacement {\n /** Each participant's top-left. */\n nodes: Map<string, Vec2>;\n /** Each group's box. */\n groups: Map<string, Bounds>;\n /** Each drawn edge's route, end to end. */\n routes: Map<string, Vec2[]>;\n /** Each labeled edge's label box. */\n labels: Map<string, Bounds>;\n}\n\nexport function fromElk(root: ElkNode, index: ElkIndex): ElkPlacement {\n const out: ElkPlacement = { nodes: new Map(), groups: new Map(), routes: new Map(), labels: new Map() };\n const edges: ElkExtendedEdge[] = [];\n const visit = (n: ElkNode) => {\n edges.push(...(n.edges ?? []));\n for (const child of n.children ?? []) {\n const x = child.x ?? 0;\n const y = child.y ?? 0;\n const node = index.nodes.get(child.id);\n if (node !== undefined) out.nodes.set(node.id, { x, y });\n const group = index.groups.get(child.id);\n if (group !== undefined) out.groups.set(group.id, { x, y, width: child.width ?? 0, height: child.height ?? 0 });\n visit(child);\n }\n };\n visit(root);\n\n for (const e of edges) {\n const drawn = index.edges.get(e.id);\n if (drawn === undefined) continue;\n const route: Vec2[] = [];\n for (const s of e.sections ?? []) route.push(s.startPoint, ...(s.bendPoints ?? []), s.endPoint);\n if (route.length > 0) out.routes.set(drawn.id, dedupe(route));\n const label = e.labels?.[0];\n if (label !== undefined && drawn.label !== undefined) {\n out.labels.set(drawn.id, { x: label.x ?? 0, y: label.y ?? 0, width: label.width ?? 0, height: label.height ?? 0 });\n }\n }\n return out;\n}\n\n/** A route with no point repeated back to back, as consecutive sections of\n * one edge share their joining point. */\nfunction dedupe(route: readonly Vec2[]): Vec2[] {\n return route.filter((p, i) => i === 0 || p.x !== route[i - 1]!.x || p.y !== route[i - 1]!.y);\n}\n","/**\n * A diagram `Graph` as an ELK graph: participants as leaf nodes, groups as\n * compound nodes nested the way the groups are, the ports an edge names as\n * fixed ports, and each drawn edge once.\n *\n * Every ELK id is minted here rather than reused from the graph, so nothing a\n * caller names can collide across ELK's one id space for nodes, ports, edges\n * and labels. `ElkIndex` maps them back.\n */\nimport type { Vec2 } from '@weasel-js/core/math';\nimport {\n DEFAULT_PORTS,\n portsOnOutline,\n type EdgeLabelRoom,\n type Graph,\n type GraphGroup,\n type GraphNode,\n type Port,\n} from '@weasel-js/diagram/layout';\nimport type { ElkExtendedEdge, ElkNode, ElkPort } from 'elkjs/lib/elk-api';\n\n/** One drawn edge: a stand-in `expandGroupEdges` made folds back into this. */\nexport interface DrawnEdge {\n id: string;\n from: string;\n to: string;\n fromPort?: string;\n toPort?: string;\n label?: EdgeLabelRoom;\n authored: boolean;\n}\n\n/** ELK ids back to the graph's. */\nexport interface ElkIndex {\n nodes: Map<string, GraphNode>;\n groups: Map<string, GraphGroup>;\n edges: Map<string, DrawnEdge>;\n}\n\n/** Where each of a node's declared ports sits, cast onto its outline as\n * `portsOf` casts them, for a node whose top-left is `at`. */\nexport function portPoints(node: GraphNode, at: Vec2): Port[] {\n const bounds = { x: at.x, y: at.y, width: node.bounds.width, height: node.bounds.height };\n const ports: Port[] = (node.ports ?? DEFAULT_PORTS).map((spec) => ({\n id: spec.id,\n nodeId: node.id,\n point: { x: at.x + spec.at.u * bounds.width, y: at.y + spec.at.v * bounds.height },\n normal: null,\n }));\n return node.outline === undefined ? ports : portsOnOutline(ports, node.outline, bounds);\n}\n\n/** Each drawn edge once, in graph order, naming only the ports its node ends\n * declare: a group has none, and a port a node does not declare is no port. */\nexport function drawnEdges(graph: Graph): DrawnEdge[] {\n const declares = (id: string, port: string | undefined) => {\n const node = graph.node(id);\n return port !== undefined && node !== undefined && (node.ports ?? DEFAULT_PORTS).some((p) => p.id === port);\n };\n const seen = new Map<string, DrawnEdge>();\n for (const e of graph.edges) {\n const id = e.origin?.id ?? e.id;\n if (seen.has(id)) continue;\n const from = e.origin?.from ?? e.from;\n const to = e.origin?.to ?? e.to;\n seen.set(id, {\n id,\n from,\n to,\n ...(declares(from, e.fromPort) ? { fromPort: e.fromPort } : {}),\n ...(declares(to, e.toPort) ? { toPort: e.toPort } : {}),\n ...(e.label !== undefined ? { label: e.label } : {}),\n authored: e.authored === true,\n });\n }\n return [...seen.values()];\n}\n\n/** The ELK graph for `graph`, its root carrying `layoutOptions`. */\nexport function toElk(graph: Graph, layoutOptions: Record<string, string>): { root: ElkNode; index: ElkIndex } {\n const index: ElkIndex = { nodes: new Map(), groups: new Map(), edges: new Map() };\n const groups = graph.groups ?? [];\n const elkIdOf = new Map<string, string>();\n graph.nodes.forEach((n, i) => {\n elkIdOf.set(n.id, `n${i}`);\n index.nodes.set(`n${i}`, n);\n });\n groups.forEach((g, i) => {\n elkIdOf.set(g.id, `g${i}`);\n index.groups.set(`g${i}`, g);\n });\n\n const edges = drawnEdges(graph).filter((e) => elkIdOf.has(e.from) && elkIdOf.has(e.to));\n const named = new Map<string, Set<string>>();\n for (const e of edges) {\n if (e.fromPort !== undefined) addTo(named, e.from, e.fromPort);\n if (e.toPort !== undefined) addTo(named, e.to, e.toPort);\n }\n\n const leaves = new Map<string | undefined, ElkNode[]>();\n for (const n of graph.nodes) {\n pushTo(leaves, n.group, leafOf(n, elkIdOf.get(n.id)!, named.get(n.id)));\n }\n const transposed = minimumTransposed(layoutOptions);\n const boxOf = (g: GraphGroup): ElkNode => ({\n id: elkIdOf.get(g.id)!,\n layoutOptions: groupOptions(g, transposed),\n children: [...groups.filter((c) => c.parent === g.id).map(boxOf), ...(leaves.get(g.id) ?? [])],\n });\n\n const elkEdges: ElkExtendedEdge[] = edges.map((e, i) => {\n index.edges.set(`e${i}`, e);\n const end = (id: string, port: string | undefined) =>\n port !== undefined ? portId(elkIdOf.get(id)!, port) : elkIdOf.get(id)!;\n return {\n id: `e${i}`,\n sources: [end(e.from, e.fromPort)],\n targets: [end(e.to, e.toPort)],\n // ELK leaves a label with no text at the origin, unplaced, in some\n // graphs; the text is never measured, since the size is given.\n ...(e.label !== undefined ? { labels: [{ id: `e${i}l`, text: e.id, width: e.label.width, height: e.label.height }] } : {}),\n };\n });\n\n const root: ElkNode = {\n id: 'root',\n layoutOptions,\n children: [...groups.filter((g) => g.parent === undefined).map(boxOf), ...(leaves.get(undefined) ?? [])],\n edges: elkEdges,\n };\n return { root, index };\n}\n\nfunction leafOf(node: GraphNode, id: string, ports: ReadonlySet<string> | undefined): ElkNode {\n const leaf: ElkNode = { id, width: node.bounds.width, height: node.bounds.height };\n if (ports === undefined) return leaf;\n const elkPorts: ElkPort[] = portPoints(node, { x: 0, y: 0 })\n .filter((p) => ports.has(p.id))\n .map((p) => ({ id: portId(id, p.id), x: p.point.x, y: p.point.y, width: 0, height: 0 }));\n return { ...leaf, ports: elkPorts, layoutOptions: { 'elk.portConstraints': 'FIXED_POS' } };\n}\n\nfunction groupOptions(g: GraphGroup, transposed: boolean): Record<string, string> {\n const { top, right, bottom, left } = g.inset;\n const options: Record<string, string> = { 'elk.padding': `[top=${top},left=${left},bottom=${bottom},right=${right}]` };\n if (g.minWidth !== undefined) {\n options['elk.nodeSize.constraints'] = 'MINIMUM_SIZE';\n options['elk.nodeSize.minimum'] = transposed ? `(0, ${g.minWidth})` : `(${g.minWidth}, 0)`;\n }\n return options;\n}\n\n/** Whether elkjs 0.12 reads a compound node's minimum size with its axes\n * swapped, as it does laying out every level at once with ranks running\n * up or down. */\nexport function minimumTransposed(options: Readonly<Record<string, string>>): boolean {\n const read = (key: string) => options[`elk.${key}`] ?? options[`org.eclipse.elk.${key}`];\n const direction = read('direction');\n return read('hierarchyHandling') === 'INCLUDE_CHILDREN' && (direction === 'DOWN' || direction === 'UP');\n}\n\nfunction portId(nodeElkId: string, port: string): string {\n return `${nodeElkId}:${port}`;\n}\n\nfunction addTo(map: Map<string, Set<string>>, key: string, value: string): void {\n const set = map.get(key);\n if (set === undefined) map.set(key, new Set([value]));\n else set.add(value);\n}\n\nfunction pushTo<K, V>(map: Map<K, V[]>, key: K, value: V): void {\n const list = map.get(key);\n if (list === undefined) map.set(key, [value]);\n else list.push(value);\n}\n","/**\n * The adapter: lay a diagram's `Graph` out with ELK and answer with a\n * `LayoutPlan`, so the result goes through `applyLayout` and `routesOf` like\n * any of the diagram package's own layouts.\n *\n * ELK answers asynchronously, so this is not a `LayoutFn` and cannot stand in\n * one's place in a live run; it is for laying a diagram out once.\n */\nimport type { Vec2 } from '@weasel-js/core/math';\nimport {\n anchorOffset,\n DEFAULT_NODE_GAP,\n DEFAULT_RANK_GAP,\n pinnedSet,\n translated,\n type Bounds,\n type Graph,\n type LayoutDirection,\n type LayoutOptions,\n type LayoutPlan,\n type LayoutPorts,\n type LayoutResult,\n} from '@weasel-js/diagram/layout';\nimport { nearestOnLine } from '@weasel-js/geom';\nimport type { ELK } from 'elkjs/lib/elk-api';\nimport { fromElk, type ElkPlacement } from './fromElk';\nimport { drawnEdges, portPoints, toElk, type DrawnEdge } from './toElk';\n\n/** Anything with ELK's `layout`: the bundled instance, or one on a worker. */\nexport type ElkRunner = Pick<ELK, 'layout'>;\n\n/** {@link elkPlan}'s options: the diagram's own, read as ELK's nearest\n * equivalents, and a way through to everything else ELK takes. */\nexport interface ElkLayoutOptions extends Omit<LayoutOptions, 'order' | 'chain' | 'chainSide'> {\n /** The ELK to run on. Default: elkjs's bundled build, loaded on first use\n * and run on the calling thread. Pass one built on `elk-worker` to keep a\n * large layout off it. */\n elk?: ElkRunner;\n /** ELK options for the root, applied over the ones this derives from the\n * rest, under their full or `elk.`-prefixed ids. Default algorithm\n * `elk.layered`. */\n layoutOptions?: Record<string, string>;\n}\n\n/** {@link elkPlan}'s answer: a layout plan, and what else ELK placed. */\nexport interface ElkPlan extends LayoutPlan {\n /** Each group's box, in world coordinates. The diagram derives a box from\n * its members, so nothing writes these; they are what ELK left room for. */\n groups: ReadonlyMap<string, Bounds>;\n /** Each routed edge end to end, keyed by the edge as drawn. `waypoints` is\n * the same route without its two ends. */\n routes: ReadonlyMap<string, readonly Vec2[]>;\n /** Pinned nodes ELK put somewhere else. Only the first pin holds: the\n * layout is translated onto it, and ELK has no way to be told the rest. */\n unheld: ReadonlySet<string>;\n}\n\nconst DIRECTIONS: Record<LayoutDirection, string> = { down: 'DOWN', up: 'UP', right: 'RIGHT', left: 'LEFT' };\n\n/** The ELK options a run starts from, before `opts.layoutOptions`. */\nexport function rootOptions(opts: ElkLayoutOptions = {}): Record<string, string> {\n const spacing = String(opts.nodeGap ?? DEFAULT_NODE_GAP);\n return {\n 'elk.algorithm': 'layered',\n 'elk.direction': DIRECTIONS[opts.direction ?? 'down'],\n 'elk.spacing.nodeNode': spacing,\n 'elk.layered.spacing.nodeNodeBetweenLayers': String(opts.rankGap ?? DEFAULT_RANK_GAP),\n 'elk.hierarchyHandling': 'INCLUDE_CHILDREN',\n 'elk.json.shapeCoords': 'ROOT',\n 'elk.json.edgeCoords': 'ROOT',\n ...opts.layoutOptions,\n };\n}\n\nlet bundled: Promise<ElkRunner> | undefined;\n\n/** elkjs's bundled build, imported the first time a layout needs it. */\nfunction defaultElk(): Promise<ElkRunner> {\n bundled ??= import('elkjs/lib/elk.bundled.js').then(({ default: ELKConstructor }) => new ELKConstructor());\n return bundled;\n}\n\n/** Lay `graph` out with ELK: where each participant goes, each edge's route\n * and the port at either end, each label's place, and each group's box. */\nexport async function elkPlan(graph: Graph, opts: ElkLayoutOptions = {}): Promise<ElkPlan> {\n const { root, index } = toElk(graph, rootOptions(opts));\n const elk = opts.elk ?? (await defaultElk());\n const placement = fromElk(await elk.layout(root), index);\n return planFrom(graph, placement, opts);\n}\n\n/** {@link elkPlan}'s moves alone. */\nexport async function elkLayout(graph: Graph, opts: ElkLayoutOptions = {}): Promise<LayoutResult> {\n return (await elkPlan(graph, opts)).moves;\n}\n\nfunction planFrom(graph: Graph, placed: ElkPlacement, opts: ElkLayoutOptions): ElkPlan {\n const pinned = pinnedSet(graph, opts);\n const tolerance = opts.tolerance ?? 1e-6;\n const d = anchorOffset(graph, placed.nodes, pinned);\n const shift = (p: Vec2): Vec2 => ({ x: p.x + d.x, y: p.y + d.y });\n\n const unheld = new Set<string>();\n for (const node of graph.nodes) {\n const at = placed.nodes.get(node.id);\n if (!pinned.has(node.id) || at === undefined) continue;\n const to = shift(at);\n if (Math.abs(to.x - node.bounds.x) > tolerance || Math.abs(to.y - node.bounds.y) > tolerance) unheld.add(node.id);\n }\n\n const groups = new Map<string, Bounds>();\n for (const [id, box] of placed.groups) groups.set(id, { ...box, ...shift(box) });\n\n const routes = new Map<string, Vec2[]>();\n const waypoints = new Map<string, Vec2[]>();\n const labels = new Map<string, Vec2>();\n const ports = new Map<string, LayoutPorts>();\n for (const edge of drawnEdges(graph)) {\n const elkRoute = placed.routes.get(edge.id);\n if (edge.authored || elkRoute === undefined || elkRoute.length < 2) continue;\n const route = elkRoute.map(shift);\n routes.set(edge.id, route);\n if (route.length > 2) waypoints.set(edge.id, route.slice(1, -1));\n const box = placed.labels.get(edge.id);\n if (box !== undefined) labels.set(edge.id, nearestOnRoute(route, shift({ x: box.x + box.width / 2, y: box.y + box.height / 2 })));\n const named = endPorts(graph, edge, route, placed, shift);\n if (named.from !== undefined || named.to !== undefined) ports.set(edge.id, named);\n }\n\n return {\n moves: translated(graph, placed.nodes, pinned, tolerance),\n waypoints,\n labels,\n ports,\n groups,\n routes,\n unheld,\n };\n}\n\n/** The port an edge leaves and arrives by: the one it names, or else the\n * declared port nearest where ELK met the node. A group end has none. */\nfunction endPorts(\n graph: Graph,\n edge: DrawnEdge,\n route: readonly Vec2[],\n placed: ElkPlacement,\n shift: (p: Vec2) => Vec2,\n): LayoutPorts {\n const nearest = (id: string, point: Vec2): string | undefined => {\n const node = graph.node(id);\n const at = placed.nodes.get(id);\n if (node === undefined || at === undefined) return undefined;\n let best: string | undefined;\n let bestDist = Infinity;\n for (const port of portPoints(node, shift(at))) {\n const dist = Math.hypot(port.point.x - point.x, port.point.y - point.y);\n if (dist < bestDist) [best, bestDist] = [port.id, dist];\n }\n return best;\n };\n const from = edge.fromPort ?? nearest(edge.from, route[0]!);\n const to = edge.toPort ?? nearest(edge.to, route.at(-1)!);\n return { ...(from !== undefined ? { from } : {}), ...(to !== undefined ? { to } : {}) };\n}\n\nfunction nearestOnRoute(route: readonly Vec2[], probe: Vec2): Vec2 {\n let best = route[0]!;\n let bestDist = Infinity;\n for (let i = 1; i < route.length; i++) {\n const a = route[i - 1]!;\n const b = route[i]!;\n const hit = nearestOnLine(probe.x, probe.y, a.x, a.y, b.x, b.y);\n if (hit.dist < bestDist) [best, bestDist] = [{ x: hit.x, y: hit.y }, hit.dist];\n }\n return best;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/diagram-elk",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.10.1",
|
|
4
|
+
"description": "An ELK layout adapter for @weasel-js/diagram: lays a diagram's graph out once with elkjs, ports, labels and nested groups included, and answers with a diagram layout plan.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"author": "orochi235",
|
|
19
|
+
"homepage": "https://orochi235.github.io/weasel/",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/orochi235/weasel.git",
|
|
23
|
+
"directory": "packages/diagram-elk"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/orochi235/weasel/issues"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@weasel-js/core": "1.10.1",
|
|
41
|
+
"@weasel-js/diagram": "1.10.1",
|
|
42
|
+
"@weasel-js/geom": "1.10.1"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public",
|
|
46
|
+
"provenance": true
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"elkjs": "^0.12.0"
|
|
50
|
+
}
|
|
51
|
+
}
|