@grafloria/element 0.4.2 → 0.4.4

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.
@@ -0,0 +1,128 @@
1
+ /**
2
+ * SPLIT LAYOUT — the DevExpress dashboard model, as pure arithmetic.
3
+ *
4
+ * A board is a tree: groups split their slot into children along one axis
5
+ * ('row' = left→right, 'column' = top→bottom), each child taking a WEIGHT of
6
+ * the group's length; leaves are the widgets. Every slot is covered exactly,
7
+ * so there is never an empty space — one widget fills the whole board, a
8
+ * second halves it, a third halves the larger half the other way. Measured
9
+ * on the DevExpress Web Dashboard designer (6 Sep 2026): one item 1481×823;
10
+ * two → 409/409; a third → the larger halved 738+738; a divider drag moved
11
+ * 409/409 to 286/532 (a percentage, not a pixel size); the item alone in its
12
+ * group has no divider of its own — its size IS the group's.
13
+ *
14
+ * Nothing in here touches the DOM or the model: the binder projects the tree
15
+ * onto a frame with `projectSplit`, hit-tests `dividersOf`, and persists the
16
+ * tree as plain JSON (`dashboardTree` on the board's group). One tree write
17
+ * per gesture is the whole undo story.
18
+ */
19
+ import type { CellRect, WorldRect } from './grid-mapping.js';
20
+ export type SplitDir = 'row' | 'column';
21
+ export interface SplitLeaf {
22
+ id: string;
23
+ weight: number;
24
+ }
25
+ export interface SplitGroup {
26
+ dir: SplitDir;
27
+ weight: number;
28
+ children: SplitNode[];
29
+ }
30
+ export type SplitNode = SplitLeaf | SplitGroup;
31
+ export type SplitSide = 'left' | 'right' | 'top' | 'bottom';
32
+ export declare const isSplitGroup: (n: SplitNode | null | undefined) => n is SplitGroup;
33
+ /** Every widget id in the tree, depth-first, in reading order. */
34
+ export declare function splitLeaves(root: SplitNode | null): string[];
35
+ /** A deep copy — commands keep the before/after trees, so they must not share. */
36
+ export declare function cloneSplit<T extends SplitNode | null>(root: T): T;
37
+ /** Index path from the root to the leaf with `id` (empty for the root itself), or null. */
38
+ export declare function pathToLeaf(root: SplitNode | null, id: string, path?: number[]): number[] | null;
39
+ export declare function nodeAt(root: SplitNode | null, path: readonly number[]): SplitNode | null;
40
+ /**
41
+ * Project the tree onto `frame`: every leaf's world rect, `gap` px between
42
+ * siblings, `padding` px inside the frame. A group's children share its
43
+ * length in proportion to their weights.
44
+ */
45
+ export declare function projectSplit(root: SplitNode | null, frame: WorldRect, gap?: number, padding?: number, rtl?: boolean): Map<string, WorldRect>;
46
+ /** Every GROUP's projected rect, keyed by its index path — drop targets for "under all of these". */
47
+ export interface SplitGroupRect {
48
+ path: number[];
49
+ dir: SplitDir;
50
+ rect: WorldRect;
51
+ }
52
+ export declare function groupRectsOf(root: SplitNode | null, frame: WorldRect, gap?: number, padding?: number, rtl?: boolean): SplitGroupRect[];
53
+ /** One draggable divider: the gap between two siblings of a group. */
54
+ export interface SplitDivider {
55
+ /** Index path to the GROUP that owns the divider. */
56
+ path: number[];
57
+ /** The divider sits after child `index` (between `index` and `index + 1`). */
58
+ index: number;
59
+ dir: SplitDir;
60
+ /** The gap's world rect — the hit zone the binder widens. */
61
+ rect: WorldRect;
62
+ /** The group's free length along its axis, so a px delta becomes a fraction. */
63
+ length: number;
64
+ }
65
+ export declare function dividersOf(root: SplitNode | null, frame: WorldRect, gap?: number, padding?: number, rtl?: boolean): SplitDivider[];
66
+ /**
67
+ * Split the leaf `targetId`'s slot to make room for `newId`: along `dir`,
68
+ * the newcomer `before` or after the target. If the target's parent already
69
+ * runs along `dir` the newcomer becomes a sibling and the two share the
70
+ * target's former weight; otherwise the target's slot becomes a new group of
71
+ * the two, equal halves. Returns the new root (never mutates the input).
72
+ */
73
+ export declare function splitLeaf(root: SplitNode | null, targetId: string, newId: string, dir: SplitDir, before?: boolean): SplitNode;
74
+ /**
75
+ * Split the slot of the node at `path` — a leaf OR a whole group — to make
76
+ * room for `newId` beside it: the DevExpress drop on a group's outer edge
77
+ * ("under all of these columns", not under one of them). Same weight rule as
78
+ * `splitLeaf`; a group target keeps its inner structure intact.
79
+ */
80
+ export declare function splitAt(root: SplitNode | null, path: readonly number[], newId: string, dir: SplitDir, before?: boolean): SplitNode;
81
+ /**
82
+ * ADD, the DevExpress way: the newcomer halves the LARGEST leaf, along that
83
+ * leaf's longer axis — so a wide board's first add gives left / right, and
84
+ * the halves, now taller than wide, split top / bottom under the next add.
85
+ */
86
+ export declare function addSplitLeaf(root: SplitNode | null, newId: string, frame: WorldRect, gap?: number, padding?: number): SplitNode;
87
+ /**
88
+ * Remove a leaf. Its slot goes to the NEIGHBOUR it was split from — the
89
+ * sibling before it, else the one after (DevExpress: deleting an item hands
90
+ * its space to the adjacent item, and an add followed by a remove gives the
91
+ * board back exactly). A group left with one child collapses into its
92
+ * parent, and a collapsed child running the same way as its new parent is
93
+ * spliced in so the tree never carries a redundant level.
94
+ */
95
+ export declare function removeSplitLeaf(root: SplitNode | null, id: string): SplitNode | null;
96
+ /** Fold single-child groups and same-direction nesting away. */
97
+ export declare function collapse(node: SplitNode | null): SplitNode | null;
98
+ /**
99
+ * Drop `id` on `side` of `targetId` (a move when `id` is already in the tree).
100
+ * left / right split the target's slot along a row, top / bottom along a
101
+ * column; the mover lands on the named side.
102
+ */
103
+ export declare function insertSplitLeaf(root: SplitNode | null, id: string, target: string | {
104
+ path: readonly number[];
105
+ }, side: SplitSide): SplitNode | null;
106
+ /**
107
+ * Move the divider after child `index` of the group at `path` by `fraction`
108
+ * of the group's free length (positive = towards the end). Neither neighbour
109
+ * may shrink below `min` of the group. Returns the new root.
110
+ */
111
+ export declare function moveSplitDivider(root: SplitNode, path: readonly number[], index: number, fraction: number, min?: number): SplitNode;
112
+ /** Weights as fractions summing to 1 in every group — tidy JSON, same picture. */
113
+ export declare function normalizeSplit<T extends SplitNode | null>(root: T): T;
114
+ /**
115
+ * A GRID layout (cells) as a split tree — guillotine cuts: every horizontal
116
+ * line no tile straddles becomes a row boundary (a column group), inside each
117
+ * band every vertical line no tile straddles becomes a column boundary (a row
118
+ * group), and so on. Weights are the bands' cell extents, so the tree paints
119
+ * the grid's proportions. Tiles that interleave with no clean cut (a pinwheel)
120
+ * fall back to a row in reading order.
121
+ */
122
+ export declare function splitFromCells(cells: ReadonlyMap<string, CellRect>): SplitNode | null;
123
+ /**
124
+ * A split tree as GRID cells: project onto a `columns` × `rows` unit frame and
125
+ * snap every edge to the nearest line (never below one cell). What a designer
126
+ * gets when switching a split board back to the grid.
127
+ */
128
+ export declare function cellsFromSplit(root: SplitNode | null, columns: number, rows: number): Map<string, CellRect>;
@@ -0,0 +1,425 @@
1
+ /**
2
+ * SPLIT LAYOUT — the DevExpress dashboard model, as pure arithmetic.
3
+ *
4
+ * A board is a tree: groups split their slot into children along one axis
5
+ * ('row' = left→right, 'column' = top→bottom), each child taking a WEIGHT of
6
+ * the group's length; leaves are the widgets. Every slot is covered exactly,
7
+ * so there is never an empty space — one widget fills the whole board, a
8
+ * second halves it, a third halves the larger half the other way. Measured
9
+ * on the DevExpress Web Dashboard designer (6 Sep 2026): one item 1481×823;
10
+ * two → 409/409; a third → the larger halved 738+738; a divider drag moved
11
+ * 409/409 to 286/532 (a percentage, not a pixel size); the item alone in its
12
+ * group has no divider of its own — its size IS the group's.
13
+ *
14
+ * Nothing in here touches the DOM or the model: the binder projects the tree
15
+ * onto a frame with `projectSplit`, hit-tests `dividersOf`, and persists the
16
+ * tree as plain JSON (`dashboardTree` on the board's group). One tree write
17
+ * per gesture is the whole undo story.
18
+ */
19
+ export const isSplitGroup = (n) => !!n && typeof n === 'object' && Array.isArray(n.children);
20
+ /** Every widget id in the tree, depth-first, in reading order. */
21
+ export function splitLeaves(root) {
22
+ if (!root)
23
+ return [];
24
+ return isSplitGroup(root) ? root.children.flatMap(splitLeaves) : [root.id];
25
+ }
26
+ /** A deep copy — commands keep the before/after trees, so they must not share. */
27
+ export function cloneSplit(root) {
28
+ if (!root)
29
+ return root;
30
+ if (isSplitGroup(root)) {
31
+ return { dir: root.dir, weight: root.weight, children: root.children.map((c) => cloneSplit(c)) };
32
+ }
33
+ return { id: root.id, weight: root.weight };
34
+ }
35
+ /** Index path from the root to the leaf with `id` (empty for the root itself), or null. */
36
+ export function pathToLeaf(root, id, path = []) {
37
+ if (!root)
38
+ return null;
39
+ if (!isSplitGroup(root))
40
+ return root.id === id ? path : null;
41
+ for (let i = 0; i < root.children.length; i++) {
42
+ const p = pathToLeaf(root.children[i], id, [...path, i]);
43
+ if (p)
44
+ return p;
45
+ }
46
+ return null;
47
+ }
48
+ export function nodeAt(root, path) {
49
+ var _a;
50
+ let n = root;
51
+ for (const i of path) {
52
+ if (!isSplitGroup(n))
53
+ return null;
54
+ n = (_a = n.children[i]) !== null && _a !== void 0 ? _a : null;
55
+ }
56
+ return n;
57
+ }
58
+ const sum = (children) => children.reduce((s, c) => s + Math.max(0, c.weight), 0) || 1;
59
+ /**
60
+ * Project the tree onto `frame`: every leaf's world rect, `gap` px between
61
+ * siblings, `padding` px inside the frame. A group's children share its
62
+ * length in proportion to their weights.
63
+ */
64
+ export function projectSplit(root, frame, gap = 0, padding = 0, rtl = false) {
65
+ const out = new Map();
66
+ if (!root)
67
+ return out;
68
+ const inner = {
69
+ x: frame.x + padding,
70
+ y: frame.y + padding,
71
+ width: Math.max(0, frame.width - 2 * padding),
72
+ height: Math.max(0, frame.height - 2 * padding),
73
+ };
74
+ const walk = (node, r) => {
75
+ if (!isSplitGroup(node)) {
76
+ out.set(node.id, Object.assign({}, r));
77
+ return;
78
+ }
79
+ const n = node.children.length;
80
+ if (!n)
81
+ return;
82
+ const total = sum(node.children);
83
+ const along = node.dir === 'row' ? r.width : r.height;
84
+ const free = Math.max(0, along - gap * (n - 1));
85
+ let cursor = 0;
86
+ const order = node.dir === 'row' && rtl ? [...node.children].reverse() : node.children;
87
+ for (const child of order) {
88
+ const size = (free * Math.max(0, child.weight)) / total;
89
+ const cr = node.dir === 'row'
90
+ ? { x: r.x + cursor, y: r.y, width: size, height: r.height }
91
+ : { x: r.x, y: r.y + cursor, width: r.width, height: size };
92
+ walk(child, cr);
93
+ cursor += size + gap;
94
+ }
95
+ };
96
+ walk(root, inner);
97
+ return out;
98
+ }
99
+ export function groupRectsOf(root, frame, gap = 0, padding = 0, rtl = false) {
100
+ const out = [];
101
+ if (!root)
102
+ return out;
103
+ const inner = {
104
+ x: frame.x + padding,
105
+ y: frame.y + padding,
106
+ width: Math.max(0, frame.width - 2 * padding),
107
+ height: Math.max(0, frame.height - 2 * padding),
108
+ };
109
+ const walk = (node, r, path) => {
110
+ if (!isSplitGroup(node))
111
+ return;
112
+ out.push({ path, dir: node.dir, rect: Object.assign({}, r) });
113
+ const n = node.children.length;
114
+ if (!n)
115
+ return;
116
+ const total = sum(node.children);
117
+ const along = node.dir === 'row' ? r.width : r.height;
118
+ const free = Math.max(0, along - gap * (n - 1));
119
+ let cursor = 0;
120
+ const mirrored = node.dir === 'row' && rtl;
121
+ for (let i = 0; i < n; i++) {
122
+ const idx = mirrored ? n - 1 - i : i;
123
+ const c = node.children[idx];
124
+ const size = (free * Math.max(0, c.weight)) / total;
125
+ const cr = node.dir === 'row'
126
+ ? { x: r.x + cursor, y: r.y, width: size, height: r.height }
127
+ : { x: r.x, y: r.y + cursor, width: r.width, height: size };
128
+ walk(c, cr, [...path, idx]);
129
+ cursor += size + gap;
130
+ }
131
+ };
132
+ walk(root, inner, []);
133
+ return out;
134
+ }
135
+ export function dividersOf(root, frame, gap = 0, padding = 0, rtl = false) {
136
+ const out = [];
137
+ if (!root)
138
+ return out;
139
+ const inner = {
140
+ x: frame.x + padding,
141
+ y: frame.y + padding,
142
+ width: Math.max(0, frame.width - 2 * padding),
143
+ height: Math.max(0, frame.height - 2 * padding),
144
+ };
145
+ const walk = (node, r, path) => {
146
+ if (!isSplitGroup(node))
147
+ return;
148
+ const n = node.children.length;
149
+ if (!n)
150
+ return;
151
+ const total = sum(node.children);
152
+ const along = node.dir === 'row' ? r.width : r.height;
153
+ const free = Math.max(0, along - gap * (n - 1));
154
+ let cursor = 0;
155
+ const mirrored = node.dir === 'row' && rtl;
156
+ node.children.forEach((child, i) => {
157
+ const idx = mirrored ? n - 1 - i : i;
158
+ const c = node.children[idx];
159
+ const size = (free * Math.max(0, c.weight)) / total;
160
+ const cr = node.dir === 'row'
161
+ ? { x: r.x + cursor, y: r.y, width: size, height: r.height }
162
+ : { x: r.x, y: r.y + cursor, width: r.width, height: size };
163
+ walk(c, cr, [...path, idx]);
164
+ if (i < n - 1) {
165
+ const gapRect = node.dir === 'row'
166
+ ? { x: r.x + cursor + size, y: r.y, width: gap, height: r.height }
167
+ : { x: r.x, y: r.y + cursor + size, width: r.width, height: gap };
168
+ // The divider belongs between the two children in TREE order.
169
+ const after = mirrored ? idx - 1 : idx;
170
+ out.push({ path, index: after, dir: node.dir, rect: gapRect, length: free });
171
+ }
172
+ cursor += size + gap;
173
+ void child;
174
+ });
175
+ };
176
+ walk(root, inner, []);
177
+ return out;
178
+ }
179
+ /**
180
+ * Split the leaf `targetId`'s slot to make room for `newId`: along `dir`,
181
+ * the newcomer `before` or after the target. If the target's parent already
182
+ * runs along `dir` the newcomer becomes a sibling and the two share the
183
+ * target's former weight; otherwise the target's slot becomes a new group of
184
+ * the two, equal halves. Returns the new root (never mutates the input).
185
+ */
186
+ export function splitLeaf(root, targetId, newId, dir, before = false) {
187
+ if (!root)
188
+ return { id: newId, weight: 1 };
189
+ const path = pathToLeaf(root, targetId);
190
+ return path ? splitAt(root, path, newId, dir, before) : cloneSplit(root);
191
+ }
192
+ /**
193
+ * Split the slot of the node at `path` — a leaf OR a whole group — to make
194
+ * room for `newId` beside it: the DevExpress drop on a group's outer edge
195
+ * ("under all of these columns", not under one of them). Same weight rule as
196
+ * `splitLeaf`; a group target keeps its inner structure intact.
197
+ */
198
+ export function splitAt(root, path, newId, dir, before = false) {
199
+ const leaf = { id: newId, weight: 1 };
200
+ if (!root)
201
+ return leaf;
202
+ const tree = cloneSplit(root);
203
+ const target = nodeAt(tree, path);
204
+ if (!target)
205
+ return tree;
206
+ const parentPath = path.slice(0, -1);
207
+ const idx = path[path.length - 1];
208
+ const parent = path.length ? nodeAt(tree, parentPath) : null;
209
+ if (parent && parent.dir === dir) {
210
+ const half = target.weight / 2;
211
+ target.weight = half;
212
+ leaf.weight = half;
213
+ parent.children.splice(before ? idx : idx + 1, 0, leaf);
214
+ return tree;
215
+ }
216
+ const group = {
217
+ dir,
218
+ weight: target.weight,
219
+ children: before ? [leaf, Object.assign(Object.assign({}, target), { weight: 1 })] : [Object.assign(Object.assign({}, target), { weight: 1 }), leaf],
220
+ };
221
+ if (!parent)
222
+ return group;
223
+ parent.children[idx] = group;
224
+ return tree;
225
+ }
226
+ /**
227
+ * ADD, the DevExpress way: the newcomer halves the LARGEST leaf, along that
228
+ * leaf's longer axis — so a wide board's first add gives left / right, and
229
+ * the halves, now taller than wide, split top / bottom under the next add.
230
+ */
231
+ export function addSplitLeaf(root, newId, frame, gap = 0, padding = 0) {
232
+ if (!root)
233
+ return { id: newId, weight: 1 };
234
+ if (pathToLeaf(root, newId))
235
+ return cloneSplit(root);
236
+ const rects = projectSplit(root, frame, gap, padding);
237
+ let bestId = null;
238
+ let best = null;
239
+ for (const [id, r] of rects) {
240
+ if (!best || r.width * r.height > best.width * best.height + 0.5) {
241
+ best = r;
242
+ bestId = id;
243
+ }
244
+ }
245
+ if (!bestId || !best)
246
+ return cloneSplit(root);
247
+ return splitLeaf(root, bestId, newId, best.width >= best.height ? 'row' : 'column', false);
248
+ }
249
+ /**
250
+ * Remove a leaf. Its slot goes to the NEIGHBOUR it was split from — the
251
+ * sibling before it, else the one after (DevExpress: deleting an item hands
252
+ * its space to the adjacent item, and an add followed by a remove gives the
253
+ * board back exactly). A group left with one child collapses into its
254
+ * parent, and a collapsed child running the same way as its new parent is
255
+ * spliced in so the tree never carries a redundant level.
256
+ */
257
+ export function removeSplitLeaf(root, id) {
258
+ var _a;
259
+ if (!root)
260
+ return null;
261
+ if (!isSplitGroup(root))
262
+ return root.id === id ? null : cloneSplit(root);
263
+ const tree = cloneSplit(root);
264
+ const path = pathToLeaf(tree, id);
265
+ if (!path)
266
+ return tree;
267
+ const parent = nodeAt(tree, path.slice(0, -1));
268
+ const i = path[path.length - 1];
269
+ const [gone] = parent.children.splice(i, 1);
270
+ const heir = (_a = parent.children[i - 1]) !== null && _a !== void 0 ? _a : parent.children[i];
271
+ if (heir)
272
+ heir.weight += Math.max(0, gone.weight);
273
+ return collapse(tree);
274
+ }
275
+ /** Fold single-child groups and same-direction nesting away. */
276
+ export function collapse(node) {
277
+ if (!node || !isSplitGroup(node))
278
+ return node;
279
+ const kids = node.children.map((c) => collapse(c)).filter((c) => !!c);
280
+ if (kids.length === 0)
281
+ return null;
282
+ if (kids.length === 1)
283
+ return Object.assign(Object.assign({}, kids[0]), { weight: node.weight });
284
+ const flat = [];
285
+ const total = sum(kids);
286
+ for (const k of kids) {
287
+ if (isSplitGroup(k) && k.dir === node.dir) {
288
+ const inner = sum(k.children);
289
+ for (const g of k.children)
290
+ flat.push(Object.assign(Object.assign({}, g), { weight: (k.weight / total) * (g.weight / inner) * total }));
291
+ }
292
+ else
293
+ flat.push(k);
294
+ }
295
+ return { dir: node.dir, weight: node.weight, children: flat };
296
+ }
297
+ /**
298
+ * Drop `id` on `side` of `targetId` (a move when `id` is already in the tree).
299
+ * left / right split the target's slot along a row, top / bottom along a
300
+ * column; the mover lands on the named side.
301
+ */
302
+ export function insertSplitLeaf(root, id, target, side) {
303
+ if (typeof target === 'string' && id === target)
304
+ return cloneSplit(root);
305
+ const without = pathToLeaf(root, id) ? removeSplitLeaf(root, id) : cloneSplit(root);
306
+ if (!without)
307
+ return { id, weight: 1 };
308
+ const dir = side === 'left' || side === 'right' ? 'row' : 'column';
309
+ const before = side === 'left' || side === 'top';
310
+ if (typeof target === 'string') {
311
+ if (!pathToLeaf(without, target))
312
+ return without;
313
+ return splitLeaf(without, target, id, dir, before);
314
+ }
315
+ // A GROUP target: its path was read from the tree WITHOUT the mover (the
316
+ // painted one), so it addresses the same node here.
317
+ if (!nodeAt(without, target.path))
318
+ return without;
319
+ return splitAt(without, target.path, id, dir, before);
320
+ }
321
+ /**
322
+ * Move the divider after child `index` of the group at `path` by `fraction`
323
+ * of the group's free length (positive = towards the end). Neither neighbour
324
+ * may shrink below `min` of the group. Returns the new root.
325
+ */
326
+ export function moveSplitDivider(root, path, index, fraction, min = 0.05) {
327
+ const tree = cloneSplit(root);
328
+ const group = nodeAt(tree, path);
329
+ if (!isSplitGroup(group) || index < 0 || index >= group.children.length - 1)
330
+ return tree;
331
+ const total = sum(group.children);
332
+ const a = group.children[index];
333
+ const b = group.children[index + 1];
334
+ const fa = a.weight / total;
335
+ const fb = b.weight / total;
336
+ const next = Math.min(Math.max(fa + fraction, min), fa + fb - min);
337
+ if (!Number.isFinite(next))
338
+ return tree;
339
+ a.weight = next * total;
340
+ b.weight = (fa + fb - next) * total;
341
+ return tree;
342
+ }
343
+ /** Weights as fractions summing to 1 in every group — tidy JSON, same picture. */
344
+ export function normalizeSplit(root) {
345
+ if (!root || !isSplitGroup(root))
346
+ return root;
347
+ const total = sum(root.children);
348
+ return {
349
+ dir: root.dir,
350
+ weight: root.weight,
351
+ children: root.children.map((c) => (Object.assign(Object.assign({}, normalizeSplit(c)), { weight: +(Math.max(0, c.weight) / total).toFixed(4) }))),
352
+ };
353
+ }
354
+ // ---- conversions ------------------------------------------------------------
355
+ /**
356
+ * A GRID layout (cells) as a split tree — guillotine cuts: every horizontal
357
+ * line no tile straddles becomes a row boundary (a column group), inside each
358
+ * band every vertical line no tile straddles becomes a column boundary (a row
359
+ * group), and so on. Weights are the bands' cell extents, so the tree paints
360
+ * the grid's proportions. Tiles that interleave with no clean cut (a pinwheel)
361
+ * fall back to a row in reading order.
362
+ */
363
+ export function splitFromCells(cells) {
364
+ const items = [...cells].map(([id, c]) => (Object.assign({ id }, c)));
365
+ if (!items.length)
366
+ return null;
367
+ const cutsAlong = (list, axis) => {
368
+ const lo = Math.min(...list.map((i) => (axis === 'y' ? i.y : i.x)));
369
+ const hi = Math.max(...list.map((i) => (axis === 'y' ? i.y + i.h : i.x + i.w)));
370
+ const lines = new Set();
371
+ for (const i of list) {
372
+ const a = axis === 'y' ? i.y : i.x;
373
+ const b = axis === 'y' ? i.y + i.h : i.x + i.w;
374
+ if (a > lo)
375
+ lines.add(a);
376
+ if (b < hi)
377
+ lines.add(b);
378
+ }
379
+ return [...lines]
380
+ .filter((line) => list.every((i) => (axis === 'y' ? i.y >= line || i.y + i.h <= line : i.x >= line || i.x + i.w <= line)))
381
+ .sort((p, q) => p - q);
382
+ };
383
+ const build = (list, prefer) => {
384
+ if (list.length === 1)
385
+ return { id: list[0].id, weight: 1 };
386
+ for (const axis of [prefer, prefer === 'y' ? 'x' : 'y']) {
387
+ const cuts = cutsAlong(list, axis);
388
+ if (!cuts.length)
389
+ continue;
390
+ const lo = Math.min(...list.map((i) => (axis === 'y' ? i.y : i.x)));
391
+ const hi = Math.max(...list.map((i) => (axis === 'y' ? i.y + i.h : i.x + i.w)));
392
+ const edges = [lo, ...cuts, hi];
393
+ const children = [];
394
+ for (let k = 0; k < edges.length - 1; k++) {
395
+ const band = list.filter((i) => (axis === 'y' ? i.y >= edges[k] && i.y + i.h <= edges[k + 1] : i.x >= edges[k] && i.x + i.w <= edges[k + 1]));
396
+ if (!band.length)
397
+ continue;
398
+ const child = build(band, axis === 'y' ? 'x' : 'y');
399
+ children.push(Object.assign(Object.assign({}, child), { weight: edges[k + 1] - edges[k] }));
400
+ }
401
+ return children.length === 1 ? children[0] : { dir: axis === 'y' ? 'column' : 'row', weight: 1, children };
402
+ }
403
+ const sorted = [...list].sort((p, q) => p.x - q.x || p.y - q.y);
404
+ return { dir: 'row', weight: 1, children: sorted.map((i) => ({ id: i.id, weight: i.w })) };
405
+ };
406
+ return collapse(build(items, 'y'));
407
+ }
408
+ /**
409
+ * A split tree as GRID cells: project onto a `columns` × `rows` unit frame and
410
+ * snap every edge to the nearest line (never below one cell). What a designer
411
+ * gets when switching a split board back to the grid.
412
+ */
413
+ export function cellsFromSplit(root, columns, rows) {
414
+ const out = new Map();
415
+ const rects = projectSplit(root, { x: 0, y: 0, width: columns, height: rows });
416
+ for (const [id, r] of rects) {
417
+ const x = Math.max(0, Math.min(columns - 1, Math.round(r.x)));
418
+ const y = Math.max(0, Math.round(r.y));
419
+ const x2 = Math.max(x + 1, Math.min(columns, Math.round(r.x + r.width)));
420
+ const y2 = Math.max(y + 1, Math.round(r.y + r.height));
421
+ out.set(id, { x, y, w: x2 - x, h: y2 - y });
422
+ }
423
+ return out;
424
+ }
425
+ //# sourceMappingURL=split-layout.js.map