@xenolithengine/graph-plugin-autolayout 0.7.0-beta.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/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/adapters/dagre.d.ts +9 -0
- package/dist/adapters/dagre.d.ts.map +1 -0
- package/dist/adapters/dagre.js +54 -0
- package/dist/adapters/dagre.js.map +1 -0
- package/dist/adapters/elk.d.ts +12 -0
- package/dist/adapters/elk.d.ts.map +1 -0
- package/dist/adapters/elk.js +85 -0
- package/dist/adapters/elk.js.map +1 -0
- package/dist/build-layout-graph.d.ts +9 -0
- package/dist/build-layout-graph.d.ts.map +1 -0
- package/dist/build-layout-graph.js +37 -0
- package/dist/build-layout-graph.js.map +1 -0
- package/dist/engine.d.ts +77 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +2 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +80 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 strelok2012
|
|
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
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# @xenolithengine/graph-plugin-autolayout
|
|
2
|
+
|
|
3
|
+
[](https://github.com/XenolithEngine/xenolith-graph#status)
|
|
4
|
+
[](https://github.com/XenolithEngine/xenolith-graph/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
Auto-layout plugin for XenolithGraph. Engine-agnostic: bring your own layout backend (dagre / elkjs / custom) via the `LayoutEngine` interface.
|
|
7
|
+
|
|
8
|
+
> **Beta** — public API in `STABLE-API.md` is the surface we plan to freeze, but it is **NOT frozen yet** — breaking changes can land at any point before v1.0. If you adopt now, pin an exact version.
|
|
9
|
+
|
|
10
|
+
Part of [XenolithGraph](https://github.com/XenolithEngine/xenolith-graph) — an AI-native, embeddable node-graph editor for the web with its own visual design language (Xen).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add @xenolithengine/graph-plugin-autolayout dagre
|
|
16
|
+
# or
|
|
17
|
+
pnpm add @xenolithengine/graph-plugin-autolayout elkjs
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Optional peer deps: `dagre@^0.8.5` (lighter, layered DAGs) **or** `elkjs@^0.9 || ^0.10 || ^0.11` (nested layouts, orthogonal routing, 9 algorithms). Install whichever backend you reach for; both engines lazy-load.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { autoLayoutPlugin } from '@xenolithengine/graph-plugin-autolayout'
|
|
26
|
+
import { dagreEngine } from '@xenolithengine/graph-plugin-autolayout/dagre'
|
|
27
|
+
|
|
28
|
+
const layout = autoLayoutPlugin({ engine: dagreEngine({ rankdir: 'LR' }) })
|
|
29
|
+
editor.use(layout)
|
|
30
|
+
|
|
31
|
+
await layout.arrange({ direction: 'LR', animate: { durationMs: 280 } })
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
ELK variant — same shape:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { elkEngine } from '@xenolithengine/graph-plugin-autolayout/elk'
|
|
38
|
+
|
|
39
|
+
const layout = autoLayoutPlugin({ engine: elkEngine({ algorithm: 'layered' }) })
|
|
40
|
+
editor.use(layout)
|
|
41
|
+
await layout.arrange()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`arrange()` commits all moves in a single undo step. Optional animated tween bypasses the command bus per frame and commits the final positions once.
|
|
45
|
+
|
|
46
|
+
## What's exported
|
|
47
|
+
|
|
48
|
+
- `autoLayoutPlugin(config)` → `AutoLayoutPlugin` with `arrange(opts?)`
|
|
49
|
+
- `AutoLayoutConfig`, `AutoLayoutPlugin`
|
|
50
|
+
- `LayoutEngine`, `LayoutGraph`, `LayoutNode`, `LayoutEdge`, `LayoutOpts`, `LayoutResult`
|
|
51
|
+
- Subpath `@xenolithengine/graph-plugin-autolayout/dagre` → `dagreEngine`, `DagreEngineOpts`
|
|
52
|
+
- Subpath `@xenolithengine/graph-plugin-autolayout/elk` → `elkEngine`, `ElkEngineOpts`
|
|
53
|
+
|
|
54
|
+
## Docs
|
|
55
|
+
|
|
56
|
+
- [API reference](https://xenolithengine.github.io/xenolith-graph/guides/api/) — every method exposed by `XenolithEditor`
|
|
57
|
+
- [GitHub](https://github.com/XenolithEngine/xenolith-graph)
|
|
58
|
+
|
|
59
|
+
MIT © XenolithEngine
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LayoutEngine } from '../engine.js';
|
|
2
|
+
/** Dagre adapter — fast, ~30 kB gzipped, single algorithm (Sugiyama layered). Good default for
|
|
3
|
+
* DAG-shaped graphs (ComfyUI, LLM flow, blueprint, DSP). Ignores port hints and hierarchy. */
|
|
4
|
+
export interface DagreEngineOpts {
|
|
5
|
+
/** Overrides applied to the dagre `setGraph()` call. See dagre docs for the full token list. */
|
|
6
|
+
graphOverrides?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
export declare function dagreEngine(opts?: DagreEngineOpts): LayoutEngine;
|
|
9
|
+
//# sourceMappingURL=dagre.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dagre.d.ts","sourceRoot":"","sources":["../../src/adapters/dagre.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAc,MAAM,cAAc,CAAA;AA8B5D;+FAC+F;AAC/F,MAAM,WAAW,eAAe;IAC9B,gGAAgG;IAChG,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACzC;AAED,wBAAgB,WAAW,CAAC,IAAI,GAAE,eAAoB,GAAG,YAAY,CAiCpE"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
let _dagre;
|
|
2
|
+
async function loadDagre() {
|
|
3
|
+
if (_dagre)
|
|
4
|
+
return _dagre;
|
|
5
|
+
try {
|
|
6
|
+
const mod = await import('dagre');
|
|
7
|
+
_dagre = (mod.default ?? mod);
|
|
8
|
+
return _dagre;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new Error('@xenolithengine/graph-plugin-autolayout/dagre: peer dependency `dagre` is not installed. Run `pnpm add dagre`.');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function mapDirection(d) {
|
|
15
|
+
return d ?? 'LR';
|
|
16
|
+
}
|
|
17
|
+
export function dagreEngine(opts = {}) {
|
|
18
|
+
return {
|
|
19
|
+
name: 'dagre',
|
|
20
|
+
async layout(g, layoutOpts) {
|
|
21
|
+
const dagre = await loadDagre();
|
|
22
|
+
const dg = new dagre.graphlib.Graph();
|
|
23
|
+
dg.setGraph({
|
|
24
|
+
rankdir: mapDirection(layoutOpts.direction),
|
|
25
|
+
nodesep: layoutOpts.spacing?.node ?? 50,
|
|
26
|
+
ranksep: layoutOpts.spacing?.layer ?? 80,
|
|
27
|
+
edgesep: layoutOpts.spacing?.edge ?? 20,
|
|
28
|
+
align: layoutOpts.align,
|
|
29
|
+
marginx: 0,
|
|
30
|
+
marginy: 0,
|
|
31
|
+
...opts.graphOverrides,
|
|
32
|
+
});
|
|
33
|
+
dg.setDefaultEdgeLabel(() => ({}));
|
|
34
|
+
for (const n of g.nodes)
|
|
35
|
+
dg.setNode(n.id, { width: n.width, height: n.height });
|
|
36
|
+
for (const e of g.edges)
|
|
37
|
+
dg.setEdge(e.from.node, e.to.node);
|
|
38
|
+
dagre.layout(dg);
|
|
39
|
+
// dagre returns the CENTRE of each node; we want top-left to feed MoveNode.
|
|
40
|
+
const positions = new Map();
|
|
41
|
+
for (const n of g.nodes) {
|
|
42
|
+
const placed = dg.node(n.id);
|
|
43
|
+
if (!placed)
|
|
44
|
+
continue;
|
|
45
|
+
positions.set(n.id, {
|
|
46
|
+
x: placed.x - n.width / 2,
|
|
47
|
+
y: placed.y - n.height / 2,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return { positions };
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=dagre.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dagre.js","sourceRoot":"","sources":["../../src/adapters/dagre.ts"],"names":[],"mappings":"AAgBA,IAAI,MAA+B,CAAA;AACnC,KAAK,UAAU,SAAS;IACtB,IAAI,MAAM;QAAE,OAAO,MAAM,CAAA;IACzB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,OAAiB,CAAC,CAAA;QAC3C,MAAM,GAAG,CAAC,GAAG,CAAC,OAAO,IAAI,GAAG,CAAgB,CAAA;QAC5C,OAAO,MAAM,CAAA;IACf,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,gHAAgH,CAAC,CAAA;IACnI,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,CAA0B;IAC9C,OAAO,CAAC,IAAI,IAAI,CAAA;AAClB,CAAC;AASD,MAAM,UAAU,WAAW,CAAC,OAAwB,EAAE;IACpD,OAAO;QACL,IAAI,EAAE,OAAO;QACb,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,UAAU;YACxB,MAAM,KAAK,GAAG,MAAM,SAAS,EAAE,CAAA;YAC/B,MAAM,EAAE,GAAG,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAA;YACrC,EAAE,CAAC,QAAQ,CAAC;gBACV,OAAO,EAAG,YAAY,CAAC,UAAU,CAAC,SAAS,CAAC;gBAC5C,OAAO,EAAG,UAAU,CAAC,OAAO,EAAE,IAAI,IAAK,EAAE;gBACzC,OAAO,EAAG,UAAU,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;gBACzC,OAAO,EAAG,UAAU,CAAC,OAAO,EAAE,IAAI,IAAK,EAAE;gBACzC,KAAK,EAAK,UAAU,CAAC,KAAK;gBAC1B,OAAO,EAAG,CAAC;gBACX,OAAO,EAAG,CAAC;gBACX,GAAG,IAAI,CAAC,cAAc;aACvB,CAAC,CAAA;YACF,EAAE,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;YAClC,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK;gBAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAA;YAC/E,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK;gBAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAA;YAC3D,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;YAChB,4EAA4E;YAC5E,MAAM,SAAS,GAAG,IAAI,GAAG,EAAoC,CAAA;YAC7D,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;gBACxB,MAAM,MAAM,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;gBAC5B,IAAI,CAAC,MAAM;oBAAE,SAAQ;gBACrB,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;oBAClB,CAAC,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC;oBACzB,CAAC,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC;iBAC3B,CAAC,CAAA;YACJ,CAAC;YACD,OAAO,EAAE,SAAS,EAAE,CAAA;QACtB,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LayoutEngine } from '../engine.js';
|
|
2
|
+
export interface ElkEngineOpts {
|
|
3
|
+
/** ELK algorithm. Default `'layered'` (Sugiyama — what dagre does, plus orthogonal routing).
|
|
4
|
+
* Others: `'mrtree'`, `'stress'`, `'force'`, `'radial'`, `'disco'`, `'sporeOverlap'`,
|
|
5
|
+
* `'sporeCompaction'`, `'rectpacking'`. See https://eclipse.dev/elk/reference/algorithms.html. */
|
|
6
|
+
algorithm?: 'layered' | 'mrtree' | 'stress' | 'force' | 'radial' | 'disco';
|
|
7
|
+
/** Merge these into ELK's `layoutOptions` after our defaults — last write wins.
|
|
8
|
+
* Example: `{ 'elk.layered.spacing.edgeNodeBetweenLayers': '40' }`. */
|
|
9
|
+
layoutOverrides?: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
export declare function elkEngine(opts?: ElkEngineOpts): LayoutEngine;
|
|
12
|
+
//# sourceMappingURL=elk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"elk.d.ts","sourceRoot":"","sources":["../../src/adapters/elk.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAqD,MAAM,cAAc,CAAA;AAqCnG,MAAM,WAAW,aAAa;IAC5B;;uGAEmG;IACnG,SAAS,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAA;IAC1E;4EACwE;IACxE,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACzC;AAED,wBAAgB,SAAS,CAAC,IAAI,GAAE,aAAkB,GAAG,YAAY,CAyDhE"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
let _ElkCtor;
|
|
2
|
+
async function loadElk() {
|
|
3
|
+
if (_ElkCtor)
|
|
4
|
+
return _ElkCtor;
|
|
5
|
+
try {
|
|
6
|
+
const mod = await import('elkjs/lib/elk.bundled.js');
|
|
7
|
+
_ElkCtor = (mod.default ?? mod);
|
|
8
|
+
return _ElkCtor;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new Error('@xenolithengine/graph-plugin-autolayout/elk: peer dependency `elkjs` is not installed. Run `pnpm add elkjs`.');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function mapDirection(d) {
|
|
15
|
+
switch (d ?? 'LR') {
|
|
16
|
+
case 'LR': return 'RIGHT';
|
|
17
|
+
case 'RL': return 'LEFT';
|
|
18
|
+
case 'TB': return 'DOWN';
|
|
19
|
+
case 'BT': return 'UP';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function elkEngine(opts = {}) {
|
|
23
|
+
const algorithm = opts.algorithm ?? 'layered';
|
|
24
|
+
return {
|
|
25
|
+
name: `elk:${algorithm}`,
|
|
26
|
+
async layout(g, layoutOpts) {
|
|
27
|
+
const Elk = await loadElk();
|
|
28
|
+
const elk = new Elk();
|
|
29
|
+
const layoutOptions = {
|
|
30
|
+
'elk.algorithm': algorithm,
|
|
31
|
+
'elk.direction': mapDirection(layoutOpts.direction),
|
|
32
|
+
'elk.spacing.nodeNode': String(layoutOpts.spacing?.node ?? 50),
|
|
33
|
+
'elk.layered.spacing.nodeNodeBetweenLayers': String(layoutOpts.spacing?.layer ?? 80),
|
|
34
|
+
'elk.spacing.edgeNode': String(layoutOpts.spacing?.edge ?? 20),
|
|
35
|
+
'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
|
|
36
|
+
...(opts.layoutOverrides ?? {}),
|
|
37
|
+
};
|
|
38
|
+
// Build nested ELK tree from flat (id, parent?) list. Roots: nodes without a parent we know
|
|
39
|
+
// about; children: bucket by their parent id. Each ElkNode declares only its OWN width/height
|
|
40
|
+
// — ELK computes parent sizes from contained children.
|
|
41
|
+
const byId = new Map(g.nodes.map((n) => [n.id, n]));
|
|
42
|
+
const childrenOf = new Map();
|
|
43
|
+
const roots = [];
|
|
44
|
+
for (const n of g.nodes) {
|
|
45
|
+
const parent = n.parent && byId.has(n.parent) ? n.parent : undefined;
|
|
46
|
+
if (parent) {
|
|
47
|
+
(childrenOf.get(parent) ?? (childrenOf.set(parent, []), childrenOf.get(parent))).push(n);
|
|
48
|
+
}
|
|
49
|
+
else
|
|
50
|
+
roots.push(n);
|
|
51
|
+
}
|
|
52
|
+
const toElk = (n) => {
|
|
53
|
+
const kids = childrenOf.get(n.id) ?? [];
|
|
54
|
+
return {
|
|
55
|
+
id: n.id,
|
|
56
|
+
width: n.width,
|
|
57
|
+
height: n.height,
|
|
58
|
+
...(kids.length > 0 ? { children: kids.map(toElk) } : {}),
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
const root = {
|
|
62
|
+
id: '$root',
|
|
63
|
+
children: roots.map(toElk),
|
|
64
|
+
edges: g.edges.map((e) => ({ id: e.id, sources: [e.from.node], targets: [e.to.node] })),
|
|
65
|
+
layoutOptions,
|
|
66
|
+
};
|
|
67
|
+
const laid = await elk.layout(root);
|
|
68
|
+
// Walk the laid-out tree and flatten ABSOLUTE positions. ELK reports each node's position
|
|
69
|
+
// relative to its parent, so accumulate offsets while descending. (top-left convention,
|
|
70
|
+
// matches dagre's normalised output — `MoveNode` consumes top-left.)
|
|
71
|
+
const positions = new Map();
|
|
72
|
+
const walk = (n, ox, oy) => {
|
|
73
|
+
const ax = ox + (n.x ?? 0);
|
|
74
|
+
const ay = oy + (n.y ?? 0);
|
|
75
|
+
if (n.id !== '$root')
|
|
76
|
+
positions.set(n.id, { x: ax, y: ay });
|
|
77
|
+
for (const c of n.children ?? [])
|
|
78
|
+
walk(c, ax, ay);
|
|
79
|
+
};
|
|
80
|
+
walk(laid, 0, 0);
|
|
81
|
+
return { positions };
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=elk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"elk.js","sourceRoot":"","sources":["../../src/adapters/elk.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Graph } from '@xenolithengine/graph-core';
|
|
2
|
+
import type { LayoutGraph } from './engine.js';
|
|
3
|
+
/** Pull an engine-neutral `LayoutGraph` out of the editor's live graph. Pure — never mutates.
|
|
4
|
+
* Nodes without an explicit `size` get a sane default so dagre/elk don't pack overlapping rects.
|
|
5
|
+
* Macro membership is surfaced as `node.parent` so hierarchical engines (ELK) lay out children
|
|
6
|
+
* inside their parent; flat engines (dagre) just ignore the field. No coordinate data is
|
|
7
|
+
* included — that's the engine's output, not its input. */
|
|
8
|
+
export declare function buildLayoutGraph(graph: Graph): LayoutGraph;
|
|
9
|
+
//# sourceMappingURL=build-layout-graph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-layout-graph.d.ts","sourceRoot":"","sources":["../src/build-layout-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAsB,MAAM,4BAA4B,CAAA;AAE3E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAE9C;;;;4DAI4D;AAC5D,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,WAAW,CAyB1D"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { isMacro, macroMembers } from '@xenolithengine/graph-core';
|
|
2
|
+
/** Pull an engine-neutral `LayoutGraph` out of the editor's live graph. Pure — never mutates.
|
|
3
|
+
* Nodes without an explicit `size` get a sane default so dagre/elk don't pack overlapping rects.
|
|
4
|
+
* Macro membership is surfaced as `node.parent` so hierarchical engines (ELK) lay out children
|
|
5
|
+
* inside their parent; flat engines (dagre) just ignore the field. No coordinate data is
|
|
6
|
+
* included — that's the engine's output, not its input. */
|
|
7
|
+
export function buildLayoutGraph(graph) {
|
|
8
|
+
const nodes = [...graph.nodes()];
|
|
9
|
+
const edges = [...graph.edges()];
|
|
10
|
+
const parentOf = new Map();
|
|
11
|
+
for (const n of nodes) {
|
|
12
|
+
if (!isMacro(n))
|
|
13
|
+
continue;
|
|
14
|
+
for (const m of macroMembers(n))
|
|
15
|
+
parentOf.set(String(m), String(n.id));
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
nodes: nodes.map((n) => {
|
|
19
|
+
const parent = parentOf.get(String(n.id));
|
|
20
|
+
return {
|
|
21
|
+
id: String(n.id),
|
|
22
|
+
width: Math.max(1, n.size?.x ?? DEFAULT_W),
|
|
23
|
+
height: Math.max(1, n.size?.y ?? DEFAULT_H),
|
|
24
|
+
...(parent !== undefined ? { parent } : {}),
|
|
25
|
+
ports: n.pins.map((p) => ({ id: String(p.id), side: p.direction === 'in' ? 'in' : 'out' })),
|
|
26
|
+
};
|
|
27
|
+
}),
|
|
28
|
+
edges: edges.map((e) => ({
|
|
29
|
+
id: String(e.id),
|
|
30
|
+
from: { node: String(e.from.node), port: String(e.from.pin) },
|
|
31
|
+
to: { node: String(e.to.node), port: String(e.to.pin) },
|
|
32
|
+
})),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const DEFAULT_W = 180;
|
|
36
|
+
const DEFAULT_H = 80;
|
|
37
|
+
//# sourceMappingURL=build-layout-graph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-layout-graph.js","sourceRoot":"","sources":["../src/build-layout-graph.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAGlE;;;;4DAI4D;AAC5D,MAAM,UAAU,gBAAgB,CAAC,KAAY;IAC3C,MAAM,KAAK,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,EAAE,CAAW,CAAA;IAC1C,MAAM,KAAK,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,EAAE,CAAW,CAAA;IAC1C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAA;IAC1C,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;YAAE,SAAQ;QACzB,KAAK,MAAM,CAAC,IAAI,YAAY,CAAC,CAAC,CAAa;YAAE,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IACpF,CAAC;IACD,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACrB,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;YACzC,OAAO;gBACL,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;gBAChB,KAAK,EAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,SAAS,CAAC;gBAC3C,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,SAAS,CAAC;gBAC3C,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC3C,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAc,EAAE,CAAC,CAAC;aACrG,CAAA;QACH,CAAC,CAAC;QACF,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACvB,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;YAChB,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;YAC7D,EAAE,EAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,EAAI,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,EAAI;SAC9D,CAAC,CAAC;KACJ,CAAA;AACH,CAAC;AAED,MAAM,SAAS,GAAG,GAAG,CAAA;AACrB,MAAM,SAAS,GAAG,EAAE,CAAA"}
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** The contract every layout backend (dagre / elkjs / WebCola / custom) must implement. Pure: a
|
|
2
|
+
* function from the structural sub-graph the plugin extracts to a per-node position map. The
|
|
3
|
+
* plugin handles everything else — pulling the snapshot, applying positions through MoveNode in
|
|
4
|
+
* one transaction, undo, events. Adapters live next door (`/dagre`, `/elk`) and are ~50 lines each.
|
|
5
|
+
* Swap engine without touching the plugin or the editor. */
|
|
6
|
+
export interface LayoutEngine {
|
|
7
|
+
/** Human-readable backend tag, e.g. `'dagre'`, `'elk:layered'`. Surfaced in logs / UI. */
|
|
8
|
+
readonly name: string;
|
|
9
|
+
/** Compute final positions for every node in `graph`. Async because some engines (ELK) are
|
|
10
|
+
* worker-backed. May honour `opts.signal` to bail out on cancellation. */
|
|
11
|
+
layout(graph: LayoutGraph, opts: LayoutOpts): Promise<LayoutResult>;
|
|
12
|
+
}
|
|
13
|
+
/** Engine-neutral structural view of the graph. Coordinates intentionally absent — that's the
|
|
14
|
+
* engine's job. Per-node `parent` lets nested layouts (macros, comments) inform engines that
|
|
15
|
+
* understand hierarchy (ELK); engines that don't (dagre) just ignore it. */
|
|
16
|
+
export interface LayoutGraph {
|
|
17
|
+
nodes: ReadonlyArray<LayoutNode>;
|
|
18
|
+
edges: ReadonlyArray<LayoutEdge>;
|
|
19
|
+
}
|
|
20
|
+
export interface LayoutNode {
|
|
21
|
+
id: string;
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
/** Containment for hierarchical engines. Omit for flat layout. */
|
|
25
|
+
parent?: string;
|
|
26
|
+
/** Optional port hints — engines that route per-port (ELK) use them; dagre ignores. */
|
|
27
|
+
ports?: ReadonlyArray<{
|
|
28
|
+
id: string;
|
|
29
|
+
side: 'in' | 'out';
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
export interface LayoutEdge {
|
|
33
|
+
id: string;
|
|
34
|
+
from: {
|
|
35
|
+
node: string;
|
|
36
|
+
port?: string;
|
|
37
|
+
};
|
|
38
|
+
to: {
|
|
39
|
+
node: string;
|
|
40
|
+
port?: string;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export interface LayoutAnimateOpts {
|
|
44
|
+
/** Total animation duration in milliseconds. Skip / set to 0 for an instant move. */
|
|
45
|
+
durationMs?: number;
|
|
46
|
+
/** Easing function `t ∈ [0,1] → eased ∈ [0,1]`. Default: cubic ease-in-out. */
|
|
47
|
+
easing?: (t: number) => number;
|
|
48
|
+
}
|
|
49
|
+
export interface LayoutOpts {
|
|
50
|
+
/** Flow direction. Default `'LR'` (left-to-right) — matches DAG editors. */
|
|
51
|
+
direction?: 'LR' | 'RL' | 'TB' | 'BT';
|
|
52
|
+
spacing?: {
|
|
53
|
+
/** Min gap between sibling nodes (same layer / branch). Default 50. */
|
|
54
|
+
node?: number;
|
|
55
|
+
/** Min gap between layers / rows. Default 80. */
|
|
56
|
+
layer?: number;
|
|
57
|
+
/** Min gap between routed edges (engines that route). Default 20. */
|
|
58
|
+
edge?: number;
|
|
59
|
+
};
|
|
60
|
+
/** Alignment within a layer for engines that support it (dagre). */
|
|
61
|
+
align?: 'UL' | 'UR' | 'DL' | 'DR' | 'center';
|
|
62
|
+
/** Cancel a long-running layout (ELK on big nested graphs can take seconds). */
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
/** When set, tween from current positions to the target over `durationMs` instead of snapping.
|
|
65
|
+
* Each per-frame step is committed ephemerally (no undo entry per frame); only the FINAL
|
|
66
|
+
* positions land as one MoveNode-per-node transaction (still ONE undo for the whole arrange).
|
|
67
|
+
* Rete `TransitionApplier` parity. */
|
|
68
|
+
animate?: LayoutAnimateOpts;
|
|
69
|
+
}
|
|
70
|
+
export interface LayoutResult {
|
|
71
|
+
/** Top-left position per node id. The plugin translates these into MoveNode commands. */
|
|
72
|
+
positions: ReadonlyMap<string, {
|
|
73
|
+
x: number;
|
|
74
|
+
y: number;
|
|
75
|
+
}>;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;6DAI6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,0FAA0F;IAC1F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;+EAC2E;IAC3E,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;CACpE;AAED;;6EAE6E;AAC7E,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,aAAa,CAAC,UAAU,CAAC,CAAA;IAChC,KAAK,EAAE,aAAa,CAAC,UAAU,CAAC,CAAA;CACjC;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,uFAAuF;IACvF,KAAK,CAAC,EAAE,aAAa,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,IAAI,GAAG,KAAK,CAAA;KAAE,CAAC,CAAA;CAC1D;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACrC,EAAE,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CACpC;AAED,MAAM,WAAW,iBAAiB;IAChC,qFAAqF;IACrF,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;CAC/B;AAED,MAAM,WAAW,UAAU;IACzB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;IACrC,OAAO,CAAC,EAAE;QACR,uEAAuE;QACvE,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,iDAAiD;QACjD,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,qEAAqE;QACrE,IAAI,CAAC,EAAE,MAAM,CAAA;KACd,CAAA;IACD,oEAAoE;IACpE,KAAK,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,QAAQ,CAAA;IAC5C,gFAAgF;IAChF,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;2CAGuC;IACvC,OAAO,CAAC,EAAE,iBAAiB,CAAA;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B,yFAAyF;IACzF,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACzD"}
|
package/dist/engine.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":""}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { XenolithPlugin } from '@xenolithengine/graph-editor';
|
|
2
|
+
import type { LayoutEngine, LayoutOpts, LayoutResult } from './engine.js';
|
|
3
|
+
export type { LayoutEngine, LayoutGraph, LayoutNode, LayoutEdge, LayoutOpts, LayoutResult, } from './engine.js';
|
|
4
|
+
export interface AutoLayoutConfig {
|
|
5
|
+
engine: LayoutEngine;
|
|
6
|
+
/** Override defaults applied when `arrange()` is called without explicit opts. */
|
|
7
|
+
defaults?: LayoutOpts;
|
|
8
|
+
}
|
|
9
|
+
/** The plugin object returned by `autoLayoutPlugin()`. Keep the reference handy — `arrange()` is
|
|
10
|
+
* the method you call to do the work. Multiple plugin instances can coexist on one editor under
|
|
11
|
+
* different `name`s (e.g. one configured for LR / DAG, another for TB / mind-map). */
|
|
12
|
+
export interface AutoLayoutPlugin extends XenolithPlugin {
|
|
13
|
+
/** Run the configured engine against the current graph and apply the resulting positions in a
|
|
14
|
+
* single transaction (= one undo step). Throws if the plugin wasn't installed yet, or if the
|
|
15
|
+
* engine rejects (e.g. AbortSignal fired). Resolves with the raw `LayoutResult` so the caller
|
|
16
|
+
* can also inspect edge routes when the engine returns them. */
|
|
17
|
+
arrange(opts?: LayoutOpts): Promise<LayoutResult>;
|
|
18
|
+
}
|
|
19
|
+
export declare function autoLayoutPlugin(config: AutoLayoutConfig & {
|
|
20
|
+
name?: string;
|
|
21
|
+
}): AutoLayoutPlugin;
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAiB,cAAc,EAAE,MAAM,8BAA8B,CAAA;AACjF,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAGzE,YAAY,EACV,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,GAC5E,MAAM,aAAa,CAAA;AAEpB,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,YAAY,CAAA;IACpB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,UAAU,CAAA;CACtB;AAED;;uFAEuF;AACvF,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD;;;qEAGiE;IACjE,OAAO,CAAC,IAAI,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;CAClD;AAID,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,gBAAgB,CAuC/F"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { MoveNode } from '@xenolithengine/graph-core';
|
|
2
|
+
import { buildLayoutGraph } from './build-layout-graph.js';
|
|
3
|
+
const DEFAULT_NAME = 'autolayout';
|
|
4
|
+
export function autoLayoutPlugin(config) {
|
|
5
|
+
let ctx = null;
|
|
6
|
+
return {
|
|
7
|
+
name: config.name ?? DEFAULT_NAME,
|
|
8
|
+
install(c) {
|
|
9
|
+
ctx = c;
|
|
10
|
+
return () => { ctx = null; };
|
|
11
|
+
},
|
|
12
|
+
async arrange(opts) {
|
|
13
|
+
if (!ctx)
|
|
14
|
+
throw new Error('autolayout: arrange() called before editor.use(plugin)');
|
|
15
|
+
const merged = { ...(config.defaults ?? {}), ...(opts ?? {}) };
|
|
16
|
+
const layoutGraph = buildLayoutGraph(ctx.graph);
|
|
17
|
+
if (layoutGraph.nodes.length === 0)
|
|
18
|
+
return { positions: new Map() };
|
|
19
|
+
const result = await config.engine.layout(layoutGraph, merged);
|
|
20
|
+
if (result.positions.size === 0)
|
|
21
|
+
return result;
|
|
22
|
+
// Capture START positions BEFORE we commit the final ones. Used by the animation path AND
|
|
23
|
+
// by the no-animation path (latter needs them to skip identity moves).
|
|
24
|
+
const starts = new Map();
|
|
25
|
+
for (const id of result.positions.keys()) {
|
|
26
|
+
const live = ctx.graph.getNode(id);
|
|
27
|
+
if (live)
|
|
28
|
+
starts.set(id, { x: live.position.x, y: live.position.y });
|
|
29
|
+
}
|
|
30
|
+
const animateMs = merged.animate?.durationMs ?? 0;
|
|
31
|
+
if (animateMs > 0)
|
|
32
|
+
await tweenPositions(ctx, starts, result.positions, animateMs, merged.animate?.easing ?? easeInOutCubic);
|
|
33
|
+
ctx.commandBus.transaction(() => {
|
|
34
|
+
for (const [id, pos] of result.positions) {
|
|
35
|
+
const live = ctx.graph.getNode(id);
|
|
36
|
+
if (!live)
|
|
37
|
+
continue;
|
|
38
|
+
const s = starts.get(id);
|
|
39
|
+
if (s && s.x === pos.x && s.y === pos.y)
|
|
40
|
+
continue;
|
|
41
|
+
ctx.commandBus.apply(new MoveNode(id, { x: pos.x, y: pos.y }));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return result;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const easeInOutCubic = (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2);
|
|
49
|
+
/** Per-frame ephemeral position updates from `starts` → `targets`. Bypasses the command bus
|
|
50
|
+
* (`MoveNode` would push N×frames undo entries for nothing — see #propagateToDisplayConsumers
|
|
51
|
+
* pattern for ephemeral writes). The plugin commits the FINAL positions in one transaction once
|
|
52
|
+
* the tween settles. */
|
|
53
|
+
async function tweenPositions(ctx, starts, targets, durationMs, easing) {
|
|
54
|
+
const start = performance.now();
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
const step = () => {
|
|
57
|
+
const elapsed = performance.now() - start;
|
|
58
|
+
const t = Math.min(1, elapsed / durationMs);
|
|
59
|
+
const e = easing(t);
|
|
60
|
+
for (const [id, end] of targets) {
|
|
61
|
+
const s = starts.get(id);
|
|
62
|
+
if (!s)
|
|
63
|
+
continue;
|
|
64
|
+
const live = ctx.graph.getNode(id);
|
|
65
|
+
if (!live)
|
|
66
|
+
continue;
|
|
67
|
+
// Ephemeral move — syncs node data + view container + incident edges + requests render.
|
|
68
|
+
// setNodePositionEphemeral is the editor's contract for "I'll commit the final position
|
|
69
|
+
// through MoveNode at the end; in the meantime, just paint here".
|
|
70
|
+
ctx.setNodePositionEphemeral(id, s.x + (end.x - s.x) * e, s.y + (end.y - s.y) * e);
|
|
71
|
+
}
|
|
72
|
+
if (t < 1)
|
|
73
|
+
requestAnimationFrame(step);
|
|
74
|
+
else
|
|
75
|
+
resolve();
|
|
76
|
+
};
|
|
77
|
+
requestAnimationFrame(step);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xenolithengine/graph-plugin-autolayout",
|
|
3
|
+
"version": "0.7.0-beta.0",
|
|
4
|
+
"description": "Auto-layout plugin for XenolithGraph. Engine-agnostic: bring your own layout backend (dagre / elkjs / custom) via the LayoutEngine interface.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./dagre": {
|
|
16
|
+
"types": "./dist/adapters/dagre.d.ts",
|
|
17
|
+
"import": "./dist/adapters/dagre.js"
|
|
18
|
+
},
|
|
19
|
+
"./elk": {
|
|
20
|
+
"types": "./dist/adapters/elk.d.ts",
|
|
21
|
+
"import": "./dist/adapters/elk.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@xenolithengine/graph-core": "0.7.0-beta.0",
|
|
31
|
+
"@xenolithengine/graph-editor": "0.7.0-beta.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"dagre": "^0.8.5",
|
|
35
|
+
"elkjs": "^0.9.0 || ^0.10.0 || ^0.11.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"dagre": {
|
|
39
|
+
"optional": true
|
|
40
|
+
},
|
|
41
|
+
"elkjs": {
|
|
42
|
+
"optional": true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/dagre": "^0.7.54",
|
|
47
|
+
"dagre": "^0.8.5",
|
|
48
|
+
"typescript": "^5.7.0",
|
|
49
|
+
"vitest": "^3.0.0"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"node-editor",
|
|
53
|
+
"auto-layout",
|
|
54
|
+
"dagre",
|
|
55
|
+
"elk",
|
|
56
|
+
"layout"
|
|
57
|
+
],
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
60
|
+
},
|
|
61
|
+
"repository": {
|
|
62
|
+
"type": "git",
|
|
63
|
+
"url": "git+https://github.com/XenolithEngine/xenolith-graph.git",
|
|
64
|
+
"directory": "packages/plugin-autolayout"
|
|
65
|
+
},
|
|
66
|
+
"homepage": "https://xenolithengine.github.io/xenolith-graph/",
|
|
67
|
+
"bugs": {
|
|
68
|
+
"url": "https://github.com/XenolithEngine/xenolith-graph/issues"
|
|
69
|
+
},
|
|
70
|
+
"author": "XenolithEngine",
|
|
71
|
+
"scripts": {
|
|
72
|
+
"build": "tsc -b",
|
|
73
|
+
"dev": "tsc -b --watch",
|
|
74
|
+
"test": "vitest run",
|
|
75
|
+
"test:watch": "vitest",
|
|
76
|
+
"lint": "eslint src",
|
|
77
|
+
"typecheck": "tsc --noEmit"
|
|
78
|
+
}
|
|
79
|
+
}
|