@n-uf/placement 26.11.12 → 26.11.13

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/CHANGELOG.md CHANGED
@@ -3,6 +3,34 @@
3
3
  Versions are calendar versions, `YY.M.R` (year, month of the release, counter within the month), shared with
4
4
  `@n-uf/motion`: both packages carry the same version, and a release bumps both.
5
5
 
6
+ ## 26.11.13
7
+
8
+ - `@n-uf/placement/stack`, floats (new exports; `fitStack`, `stackTargets` and the stack's stored form are unchanged, and
9
+ so are `./panels`, `./dock` and `./desk`). A float is an item above the pages, in the stage's own px (view space), so
10
+ the pages scrolling under it never move it; it has no pinned flag.
11
+ - `fitFloat(rect, stage, avoid?)`: the float's rect moved (never resized) inside the stage, then beside the docked
12
+ items it would cover, at the nearest clear place; `FloatFit` has `rect`, `clamped` (also when a position that wasn't a
13
+ number was replaced), `snapped`, `covers` (indexes into `avoid` it still covers; empty whenever a clear place exists,
14
+ which it finds, and not empty only when the stage has none) and `searchCapped`. A float larger than the stage keeps
15
+ its top-left corner on the stage's. The x's are tried nearest first and, for each, the stretches of y the rects
16
+ block are merged and skipped. Measured in a test run on a laptop: about 100 `avoid` rects 0.1 ms, 300 a few ms even
17
+ when no place is clear (6 ms median, 15 ms worst), 1,000 about 17 ms. A safety net stops the clear search after 3
18
+ million rect tests and sets `searchCapped` (the place found may then not be the nearest, or a clear place may exist that was not found); with no clear place
19
+ the least covered among the 2,048 nearest places is used.
20
+ - `stackFloatTarget({ id, stage, size, avoid?, label? })`: a free, priority 3 drop target in the shape
21
+ `@n-uf/motion/drag` takes, `kind: "float"`, id `float:<id>` (so it can't collide with `./desk`'s `"float"`; the
22
+ toolbar `floatTarget` in `./targets` keeps its name), whose `hit` covers the whole plane, so a pointer past the
23
+ stage's edge still hits it. `make(point, grab?)` gives the float's rect (clamped, clear of the docked items) for the
24
+ preview and for the drop: one answer, so a drop past the edge lands clamped and never cancels. A gap's `hit`
25
+ thinner than 8 px (twice the intent's 4 px breakthrough) can't take over from it.
26
+ - `storeFloats` / `readFloats`: `{ v: 1, floats: [{ id, x, y, width, height }] }` in stacking order; never throws,
27
+ drops what it can't use and lists it in `dropped`. `storeFloats` leaves out a float with a zero size or a bad number,
28
+ and of a repeated id keeps the last readable entry (the one on top); `readFloats` does the same and words a bad
29
+ version as the stack's reader does. It writes no `pinned`; one an old store has is read and ignored (listed as
30
+ `pinned:<id>`), and the float loads as a plain float.
31
+ - Exports: `fitFloat`, `stackFloatTarget`, `storeFloats`, `readFloats` and the types `StackFloat`, `FloatFit`,
32
+ `FloatTarget`, `StackFloatTargetInput`, `FloatDrop`, `StoredFloats`, `FloatsRead`.
33
+
6
34
  ## 26.11.12
7
35
 
8
36
  - New entry `@n-uf/placement/stack`: the items along one edge of a stage (a column or a row), each a panel or a control
package/README.md CHANGED
@@ -43,7 +43,8 @@ without importing each other: a target list from `@n-uf/placement/targets` is ex
43
43
  - **`/stack`**: the items along one edge, a column or a bar, each a panel or a control group with forms from largest to
44
44
  smallest (open, header, rail icon, » menu; row, stacked, icons, » menu): one form and rect per item, giving way by
45
45
  age, spare room to the panels that grow, a rail when nothing open sets the width; a drop target for every gap, each
46
- with the exact solve a drop makes; and a stored form. Not tied to `/panels`.
46
+ with the exact solve a drop makes; a stored form; and floats (an item above the pages, in the stage's own px, kept
47
+ inside the stage and clear of the docked items, with its drop target and stored form). Not tied to `/panels`.
47
48
 
48
49
  Rendering stays in the app: it draws the rects, measures its groups, and feeds pointer samples to a drag intent.
49
50
 
@@ -494,6 +495,71 @@ console.log(saved); // {"v":1,"order":["pages","tools"],"kept":["pages"],"closed
494
495
  console.log(readStack(JSON.parse(saved), ["pages", "tools", "findings"]).ok, readStack({ v: 2 }).ok); // true false
495
496
  ```
496
497
 
498
+ ### Floats
499
+
500
+ An item that isn't docked is a float. It lies above the pages in the stage's own px (view space), so the pages scrolling
501
+ under it never move it, and it has no pinned flag: there is one kind of float, `{ id, rect }`. `./stack` keeps floats
502
+ apart from the stacks (an item is in a stack's order or in the floats, the app's choice) and offers three parts, all
503
+ pure.
504
+
505
+ - `fitFloat(rect, stage, avoid?)` gives the rect a float gets: moved (never resized) to lie inside the `stage`, then
506
+ beside any of the `avoid` rects (the docked items' rects, and the » entries', in the same px) it would cover, at the
507
+ nearest place that is clear and inside the stage. A float larger than the stage keeps its top-left corner on the
508
+ stage's. A clear place is found whenever one exists, and then `covers` is empty. Only when the stage has no clear
509
+ place for it, the float stays where it covers the least (among the 2,048 nearest candidate places) and `covers` lists
510
+ the `avoid` indexes it still covers. `FloatFit`: `rect`, `clamped` (also true when a position that wasn't a number was
511
+ replaced), `snapped`, `covers`, `searchCapped`. Fit the floats again whenever the stacks or the stage change: a stack
512
+ that grew may lie under one.
513
+ Cost: the candidate x's (every edge of the stage and of an `avoid` rect) are tried nearest first; for each, the
514
+ stretches of y the rects block are merged and skipped, so no clear place is missed however many lie between, and the
515
+ search stops when no later x can be nearer. Measured in a test run on a laptop: about 100 `avoid` rects 0.1 ms, 300
516
+ a few ms even when no place is clear (6 ms median, 15 ms worst), 1,000 about 17 ms. A safety net stops the clear
517
+ search after 3 million rect tests (thousands of rects) and sets `searchCapped`: the place found may then not be the
518
+ nearest, or a clear place may exist that was not found. Keep `avoid` to the docked items, not to every pixel.
519
+ - `stackFloatTarget({ id, stage, size, avoid?, label? })` is a drop target in the shape `@n-uf/motion/drag` takes: `id`
520
+ `float:<id>` (not `"float"`, which `./desk` uses) and `kind: "float"`, priority 3 (a stack's gaps, priority 2, win
521
+ where they are), `free` (no room is made for it) and a `hit` that covers the whole plane, so a pointer past the
522
+ stage's edge, above, below or beside it, still hits it and a float drop never cancels. `make(point, grab?)` is the one
523
+ answer for the preview and the drop: the float's rect with the pointer at `point` holding it at `grab` (its offset in
524
+ the float; default the middle), run through `fitFloat`. Draw `make(...).rect` as the preview, and land the float on
525
+ the same call at the release. The intent lets a higher priority target take over from the current one only when the
526
+ pointer is `breakthrough` px (4) inside it, so a gap zone thinner than 8 px (2 × the breakthrough) along either axis
527
+ can never take over from the float target: give the gaps' `hit` at least that much room (the lab widens a thin
528
+ stack's zone across, and the first and last gap's past the stack's ends).
529
+ - `storeFloats(floats)` and `readFloats(data, known?)`: `{ v: 1, floats: [{ id, x, y, width, height }] }`, in stacking
530
+ order (the last is on top). It writes no `pinned`. `storeFloats` leaves out, silently, a float whose position isn't a
531
+ finite number or whose size isn't over 0, and keeps the last readable entry of a repeated id (the one on top, at its
532
+ own place in the order). Reading never throws: anything but an object of version 1 with a `floats` array is
533
+ `{ ok: false, error }` (the version worded as the stack's reader words it, a string quoted); inside one, a float that
534
+ isn't an object, has no string id, isn't one the consumer has (with `known`) or has a position that isn't a finite
535
+ number or a size that isn't over 0 is dropped and listed in `dropped` as `float:<id>`; of a repeated id the last
536
+ readable entry stays and the earlier ones are listed the same way; a field this version doesn't know is listed as
537
+ `"<field>"` (a float's own as `float:<id>:<field>`). A `pinned` key, which an older store may have, is read and
538
+ ignored: the float loads as a plain float, at the rect it was stored with, and `pinned:<id>` is listed.
539
+
540
+ ```ts
541
+ import { fitFloat, readFloats, stackFloatTarget, storeFloats } from "@n-uf/placement/stack";
542
+
543
+ const stage = { x: 0, y: 0, width: 900, height: 500 };
544
+ const docked = [{ x: 0, y: 32, width: 240, height: 468 }]; // a column of docked items, in the stage's px
545
+
546
+ const fit = fitFloat({ x: 100, y: 200, width: 200, height: 150 }, stage, docked);
547
+ console.log(fit.rect, fit.snapped); // { x: 240, y: 200, width: 200, height: 150 } true
548
+
549
+ const target = stackFloatTarget({ id: "notes", stage, size: { width: 200, height: 150 }, avoid: docked });
550
+ const drop = target.make({ x: 500, y: -400 }, { x: 20, y: 10 }); // the pointer is above the stage
551
+ console.log(drop.rect, drop.fit.clamped); // { x: 480, y: 0, width: 200, height: 150 } true
552
+
553
+ console.log(JSON.stringify(storeFloats([{ id: "notes", rect: drop.rect }]))); // {"v":1,"floats":[{"id":"notes","x":480,"y":0,"width":200,"height":150}]}
554
+ const old = readFloats({ v: 1, floats: [{ id: "notes", x: 480, y: 0, width: 200, height: 150, pinned: true }] });
555
+ console.log(old.ok && old.dropped); // [ 'pinned:notes' ]
556
+ ```
557
+
558
+ Freezing the layout needs nothing from the library: the freeze is the app's flag, and the app reads it once at the press.
559
+ A press on a grip while frozen (and Alt not held) never becomes a drag, so no target is built and nothing is dropped;
560
+ holding Alt shows the grips and everything works, a drag that began finishes. `fitStack` reads sizes, not what an item
561
+ says, so a count or a phase that leaves its forms' sizes alone moves no other item.
562
+
497
563
  ## Pane zones
498
564
 
499
565
  `paneZone(rect, point, previous, options?)` says which of a pane's five zones a point is in: the centre rectangle
package/dist/stack.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { R as Rect } from './geometry-mL-GTRti.js';
1
+ import { R as Rect, P as Point, S as Size } from './geometry-mL-GTRti.js';
2
2
  import { T as Target } from './target-D3PiLg44.js';
3
3
 
4
4
  /**
@@ -234,4 +234,118 @@ declare const createStack: (order: readonly string[]) => StackState;
234
234
  declare function storeStack(state: StackState): StoredStack;
235
235
  declare function readStack(data: unknown, known?: readonly string[]): StackRead;
236
236
 
237
- export { type CrossLimits, DEFAULT_MENU, DEFAULT_RAIL, type GroupFormKind, type MainSize, type PanelFormKind, type PlacedStackItem, type StackAxis, type StackFit, type StackForm, type StackFormKind, type StackInput, type StackItem, type StackMenu, type StackNote, type StackNoteCode, type StackPlace, type StackRead, type StackState, type StackTarget, type StoredStack, createStack, fitStack, formSetsCross, readStack, stackTargets, storeStack, withItem };
237
+ /**
238
+ * Floats for the edge stack: an item that isn't docked lies above the pages in the stage's own px (view space), so the
239
+ * pages scrolling under it never move it. A float has no pinned flag: there is one kind of float, and it is in view
240
+ * space. Pure, like the rest of `./stack`.
241
+ *
242
+ * - `fitFloat(rect, stage, avoid)`: the rect a float gets. It is moved (never resized) to lie inside the stage, then
243
+ * beside whatever docked content it would cover: the nearest place inside the stage that is clear of every `avoid`
244
+ * rect (the docked items' rects, in the same px). One larger than the stage keeps its top-left corner on the
245
+ * stage's; if no clear place exists the float stays where it covers the least and `covers` says which rects.
246
+ * A clear place is found whenever one exists: the x's are tried nearest first and, for each, the blocked stretches of
247
+ * y are merged and skipped, so no clear place is missed however many places lie between. Cost (measured in a test
248
+ * run on a laptop): about 100 docked rects 0.1 ms, 300 a few ms even when no place is clear (6 ms median, 15 ms
249
+ * worst), 1,000 about 17 ms; a safety net stops the clear search after 3 million rect tests and says so in
250
+ * `searchCapped`; a stage with no clear place looks at the 2,048 nearest places for the least covered.
251
+ * - `stackFloatTarget(input)`: a drop target in the shape `@n-uf/motion/drag` takes, free (no room is made for it),
252
+ * `kind` `"float"`, id `float:<item>`, whose `hit` covers the whole plane, so a pointer past the stage's edge still
253
+ * hits it (a float drop never cancels), at priority 3 so a docked stack's gaps win where they are. `make(point,
254
+ * grab)` is the one answer for the preview and the drop: the float's rect with the pointer holding it at `grab`,
255
+ * clamped and clear of the docked content.
256
+ * - `storeFloats` / `readFloats`: the stored form, `{ v: 1, floats: [{ id, x, y, width, height }] }`, in stacking
257
+ * order (the last is on top). It writes no `pinned`; one an old store has is read, ignored and listed.
258
+ */
259
+
260
+ /** A float: the item's id and its rect in view space (the stage's px). */
261
+ interface StackFloat {
262
+ readonly id: string;
263
+ readonly rect: Rect;
264
+ }
265
+ interface FloatFit {
266
+ /** The float's rect: inside the stage and clear of the docked content where a clear place exists. */
267
+ readonly rect: Rect;
268
+ /** It was moved to lie inside the stage. */
269
+ readonly clamped: boolean;
270
+ /** It was moved beside the docked content it would have covered. */
271
+ readonly snapped: boolean;
272
+ /**
273
+ * Indexes into `avoid` of the rects it still covers. Empty whenever a clear place was found, which is whenever one
274
+ * exists (unless `searchCapped`); not empty only when the stage has no clear place for the float, and then it stays
275
+ * where it covers the least among the 2,048 nearest candidate places.
276
+ */
277
+ readonly covers: readonly number[];
278
+ /** The safety net on the clear search was hit (thousands of rects): the place found may not be the nearest, or a clear place may exist that was not found. Normally false. */
279
+ readonly searchCapped: boolean;
280
+ }
281
+ declare function fitFloat(want: Rect, stage: Rect, avoid?: readonly Rect[]): FloatFit;
282
+ /** What a float drop makes: the same answer for the preview and for the release. */
283
+ interface FloatDrop {
284
+ /** The float's rect in view space. */
285
+ readonly rect: Rect;
286
+ readonly fit: FloatFit;
287
+ }
288
+ interface StackFloatTargetInput {
289
+ /** The item dragged. */
290
+ readonly id: string;
291
+ /** The stage, in view space. */
292
+ readonly stage: Rect;
293
+ /** The float's size: it is never resized. */
294
+ readonly size: Size;
295
+ /** The docked content to stay clear of: the docked items' rects (and the » entries'), in view space. */
296
+ readonly avoid?: readonly Rect[];
297
+ /** What the target is called (default "Float here"). */
298
+ readonly label?: string;
299
+ }
300
+ interface FloatTarget extends Target {
301
+ /** What kind of place it is, for a consumer that holds targets of several kinds. */
302
+ readonly kind: "float";
303
+ readonly priority: 3;
304
+ readonly free: true;
305
+ /** The item dragged. */
306
+ readonly item: string;
307
+ /**
308
+ * The drop with the pointer at `point` holding the float at `grab` (its offset from the float's top-left corner,
309
+ * default the middle): `rect` is the preview and the landing, clamped into the stage and clear of the docked content.
310
+ */
311
+ make(point: Point, grab?: Point): FloatDrop;
312
+ }
313
+ declare function stackFloatTarget(input: StackFloatTargetInput): FloatTarget;
314
+ interface StoredFloats {
315
+ readonly v: 1;
316
+ readonly floats: {
317
+ id: string;
318
+ x: number;
319
+ y: number;
320
+ width: number;
321
+ height: number;
322
+ }[];
323
+ }
324
+ type FloatsRead = {
325
+ readonly ok: true;
326
+ readonly floats: StackFloat[];
327
+ readonly dropped: readonly string[];
328
+ } | {
329
+ readonly ok: false;
330
+ readonly error: string;
331
+ };
332
+ /**
333
+ * The floats as plain JSON-safe data, in the order given (the stacking order, the last on top). Never a `pinned`.
334
+ * What it can't read back is left out, silently: a float whose position isn't a finite number or whose size isn't over
335
+ * 0 (a zero-size float), and an earlier entry of a repeated id (the last readable entry of an id is the one on top,
336
+ * so it stays, at its own place in the order).
337
+ */
338
+ declare function storeFloats(floats: readonly StackFloat[]): StoredFloats;
339
+ /**
340
+ * Reads parsed data back; never throws. Anything but an object of version 1 with a `floats` array is `{ ok: false,
341
+ * error }`. Inside one it is lenient: a float that isn't an object, has no string id, isn't one the
342
+ * consumer has (with `known`) or has a position or size that isn't a finite number (a size over 0) is dropped and
343
+ * listed in `dropped` as `float:<id>` (or `a <type> where a float goes`); a top-level field this version doesn't
344
+ * know is listed as `"<field>"`, and so is a float's own (`float:<id>:<field>`) while the float itself is kept.
345
+ * A `pinned` key (an old store's flag, whatever its value) is ignored: the float loads as a plain float, in view
346
+ * space as stored, and `pinned:<id>` is listed. A repeated id keeps its last readable entry (the list is in stacking
347
+ * order, the last on top); the earlier ones are dropped and listed as `float:<id>`.
348
+ */
349
+ declare function readFloats(data: unknown, known?: readonly string[]): FloatsRead;
350
+
351
+ export { type CrossLimits, DEFAULT_MENU, DEFAULT_RAIL, type FloatDrop, type FloatFit, type FloatTarget, type FloatsRead, type GroupFormKind, type MainSize, type PanelFormKind, type PlacedStackItem, type StackAxis, type StackFit, type StackFloat, type StackFloatTargetInput, type StackForm, type StackFormKind, type StackInput, type StackItem, type StackMenu, type StackNote, type StackNoteCode, type StackPlace, type StackRead, type StackState, type StackTarget, type StoredFloats, type StoredStack, createStack, fitFloat, fitStack, formSetsCross, readFloats, readStack, stackFloatTarget, stackTargets, storeFloats, storeStack, withItem };
package/dist/stack.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import {
2
+ clampInto,
2
3
  rect
3
4
  } from "./chunk-QMTNB2C3.js";
4
5
 
@@ -258,14 +259,232 @@ function read(data, known) {
258
259
  }
259
260
  return { ok: true, state: { order, kept, closed, used, forms }, dropped };
260
261
  }
262
+
263
+ // src/stack/floats.ts
264
+ var EPS2 = 1e-3;
265
+ var finite3 = (x, fallback) => Number.isFinite(x) ? x : fallback;
266
+ var overlaps = (a, b) => Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x) > EPS2 && Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y) > EPS2;
267
+ var shared = (a, b) => Math.max(0, Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y));
268
+ var clean = (r, at) => rect(finite3(r.x, at.x), finite3(r.y, at.y), Math.max(0, finite3(r.width, 0)), Math.max(0, finite3(r.height, 0)));
269
+ var WORK = 3e6;
270
+ var COVERED_PLACES = 2048;
271
+ var Nearest = class {
272
+ h = [];
273
+ less(a, b) {
274
+ return a[2] !== b[2] ? a[2] < b[2] : a[0] !== b[0] ? a[0] < b[0] : a[1] < b[1];
275
+ }
276
+ get size() {
277
+ return this.h.length;
278
+ }
279
+ push(t) {
280
+ const h = this.h;
281
+ let i = h.push(t) - 1;
282
+ while (i > 0) {
283
+ const p = i - 1 >> 1;
284
+ if (!this.less(h[i], h[p])) break;
285
+ [h[i], h[p]] = [h[p], h[i]];
286
+ i = p;
287
+ }
288
+ }
289
+ pop() {
290
+ const h = this.h;
291
+ const top = h[0];
292
+ const last = h.pop();
293
+ if (h.length > 0) {
294
+ h[0] = last;
295
+ let i = 0;
296
+ for (; ; ) {
297
+ const l = 2 * i + 1;
298
+ const r = l + 1;
299
+ let m = i;
300
+ if (l < h.length && this.less(h[l], h[m])) m = l;
301
+ if (r < h.length && this.less(h[r], h[m])) m = r;
302
+ if (m === i) break;
303
+ [h[i], h[m]] = [h[m], h[i]];
304
+ i = m;
305
+ }
306
+ }
307
+ return top;
308
+ }
309
+ };
310
+ function* byDistance(xs, ys, fx, fy) {
311
+ const sx = [...xs].sort((a, b) => Math.abs(a - fx) - Math.abs(b - fx) || a - b);
312
+ const sy = [...ys].sort((a, b) => Math.abs(a - fy) - Math.abs(b - fy) || a - b);
313
+ const q = new Nearest();
314
+ const d = (i, j) => Math.hypot(sx[i] - fx, sy[j] - fy);
315
+ if (sy.length > 0) for (let i = 0; i < sx.length; i++) q.push([i, 0, d(i, 0)]);
316
+ while (q.size > 0) {
317
+ const [i, j, far] = q.pop();
318
+ yield { x: sx[i], y: sy[j], far };
319
+ if (j + 1 < sy.length) q.push([i, j + 1, d(i, j + 1)]);
320
+ }
321
+ }
322
+ function fitFloat(want, stage, avoid = []) {
323
+ const area = clean(stage, rect(0, 0, 0, 0));
324
+ const r = clean(want, area);
325
+ const inside = clampInto(r, area);
326
+ const clamped = inside.x !== want.x || inside.y !== want.y;
327
+ const reach = rect(area.x, area.y, Math.max(area.width, r.width), Math.max(area.height, r.height));
328
+ const near = [];
329
+ avoid.forEach((a, index) => {
330
+ const at = clean(a, area);
331
+ if (at.width > EPS2 && at.height > EPS2 && overlaps(at, reach)) near.push({ at, index });
332
+ });
333
+ const under = (cand) => near.filter((n) => overlaps(cand, n.at)).map((n) => n.index);
334
+ const blocked = (cand) => near.some((n) => overlaps(cand, n.at));
335
+ if (!blocked(inside)) return { rect: inside, clamped, snapped: false, covers: [], searchCapped: false };
336
+ const lo = (v, from, to) => Math.min(Math.max(v, from), Math.max(from, to));
337
+ const xs = [inside.x, area.x, area.x + area.width - r.width];
338
+ const ys = [inside.y, area.y, area.y + area.height - r.height];
339
+ for (const { at } of near) {
340
+ xs.push(at.x - r.width, at.x + at.width);
341
+ ys.push(at.y - r.height, at.y + at.height);
342
+ }
343
+ const places = (list, from, size, extent) => [...new Set(list.map((v) => lo(v, from, from + extent - size)))];
344
+ const px = places(xs, area.x, r.width, area.width);
345
+ const py = places(ys, area.y, r.height, area.height);
346
+ const ylo = area.y;
347
+ const yhi = Math.max(area.y, area.y + area.height - r.height);
348
+ const byX = [...px].sort((a, b) => Math.abs(a - inside.x) - Math.abs(b - inside.x) || a - b);
349
+ let clear = null;
350
+ let work = 0;
351
+ let searchCapped = false;
352
+ for (const x of byX) {
353
+ const dx = Math.abs(x - inside.x);
354
+ if (clear && dx >= clear.far) break;
355
+ work += near.length + 1;
356
+ if (work > WORK) {
357
+ searchCapped = true;
358
+ break;
359
+ }
360
+ const stretches = [];
361
+ for (const { at } of near) {
362
+ if (Math.min(x + r.width, at.x + at.width) - Math.max(x, at.x) > EPS2) stretches.push([at.y - r.height, at.y + at.height]);
363
+ }
364
+ stretches.sort((a, b) => a[0] - b[0]);
365
+ let cursor = ylo;
366
+ let bestY = null;
367
+ const gap = (from, to) => {
368
+ if (from > to) return;
369
+ const y = Math.min(Math.max(inside.y, from), to);
370
+ if (bestY === null || Math.abs(y - inside.y) < Math.abs(bestY - inside.y)) bestY = y;
371
+ };
372
+ for (const [a, b] of stretches) {
373
+ gap(cursor, Math.min(a, yhi));
374
+ cursor = Math.max(cursor, b);
375
+ if (cursor > yhi) break;
376
+ }
377
+ if (cursor <= yhi) gap(cursor, yhi);
378
+ if (bestY === null) continue;
379
+ const far = Math.hypot(dx, bestY - inside.y);
380
+ if (!clear || far < clear.far) clear = { x, y: bestY, far };
381
+ }
382
+ if (clear) return { rect: rect(clear.x, clear.y, r.width, r.height), clamped, snapped: clear.far > EPS2, covers: [], searchCapped };
383
+ let best = null;
384
+ let tried = 0;
385
+ for (const p of byDistance(px, py, inside.x, inside.y)) {
386
+ if (tried++ >= COVERED_PLACES) break;
387
+ const cand = rect(p.x, p.y, r.width, r.height);
388
+ const left = near.reduce((s, n) => s + shared(cand, n.at), 0);
389
+ if (!best || left < best.left - EPS2) best = { rect: cand, left, far: p.far, hits: under(cand) };
390
+ }
391
+ const out = best ?? { rect: inside, far: 0, hits: under(inside) };
392
+ return { rect: out.rect, clamped, snapped: out.far > EPS2, covers: out.hits, searchCapped };
393
+ }
394
+ var PLANE = rect(-1e9, -1e9, 2e9, 2e9);
395
+ function stackFloatTarget(input) {
396
+ const { stage, size } = input;
397
+ const avoid = input.avoid ?? [];
398
+ const w = Math.max(0, finite3(size.width, 0));
399
+ const h = Math.max(0, finite3(size.height, 0));
400
+ return {
401
+ // Not "float": `./desk` has a target of that id, and a consumer may hold both.
402
+ id: `float:${input.id}`,
403
+ kind: "float",
404
+ priority: 3,
405
+ free: true,
406
+ hit: PLANE,
407
+ label: input.label ?? "Float here",
408
+ item: input.id,
409
+ make(point, grab) {
410
+ const g = grab ?? { x: w / 2, y: h / 2 };
411
+ const fit = fitFloat(rect(point.x - g.x, point.y - g.y, w, h), stage, avoid);
412
+ return { rect: fit.rect, fit };
413
+ }
414
+ };
415
+ }
416
+ var isSize = (n) => typeof n === "number" && Number.isFinite(n) && n > 0;
417
+ var isAt = (n) => typeof n === "number" && Number.isFinite(n);
418
+ function storeFloats(floats) {
419
+ const last = /* @__PURE__ */ new Map();
420
+ floats.forEach((f, i) => {
421
+ const r = f.rect;
422
+ if (isAt(r.x) && isAt(r.y) && isSize(r.width) && isSize(r.height)) last.set(f.id, i);
423
+ });
424
+ const out = [];
425
+ floats.forEach((f, i) => {
426
+ if (last.get(f.id) === i) out.push({ id: f.id, x: f.rect.x, y: f.rect.y, width: f.rect.width, height: f.rect.height });
427
+ });
428
+ return { v: 1, floats: out };
429
+ }
430
+ var isObject2 = (x) => typeof x === "object" && x !== null && !Array.isArray(x);
431
+ var FIELDS = ["id", "x", "y", "width", "height"];
432
+ function readFloats(data, known) {
433
+ try {
434
+ return read2(data, known);
435
+ } catch (e) {
436
+ return { ok: false, error: `unreadable: ${e instanceof Error ? e.message : typeof e}` };
437
+ }
438
+ }
439
+ function read2(data, known) {
440
+ if (!isObject2(data)) return { ok: false, error: "not an object" };
441
+ if (data.v !== 1) return { ok: false, error: `unknown version ${versionWords(data.v)} (this reads version 1)` };
442
+ if (!Array.isArray(data.floats)) return { ok: false, error: "floats is missing" };
443
+ const dropped = [];
444
+ const drop = (what) => {
445
+ if (!dropped.includes(what)) dropped.push(what);
446
+ };
447
+ for (const k of Object.keys(data)) if (k !== "v" && k !== "floats") drop(k);
448
+ const floats = [];
449
+ for (const x of data.floats) {
450
+ if (!isObject2(x)) {
451
+ drop(`a ${x === null ? "null" : Array.isArray(x) ? "list" : typeof x} where a float goes`);
452
+ continue;
453
+ }
454
+ const id = x.id;
455
+ if (typeof id !== "string") {
456
+ drop("a float with no id");
457
+ continue;
458
+ }
459
+ if (known && !known.includes(id) || !isAt(x.x) || !isAt(x.y) || !isSize(x.width) || !isSize(x.height)) {
460
+ drop(`float:${id}`);
461
+ continue;
462
+ }
463
+ for (const k of Object.keys(x)) {
464
+ if (k === "pinned") drop(`pinned:${id}`);
465
+ else if (!FIELDS.includes(k)) drop(`float:${id}:${k}`);
466
+ }
467
+ const before = floats.findIndex((f) => f.id === id);
468
+ if (before >= 0) {
469
+ floats.splice(before, 1);
470
+ drop(`float:${id}`);
471
+ }
472
+ floats.push({ id, rect: rect(x.x, x.y, x.width, x.height) });
473
+ }
474
+ return { ok: true, floats, dropped };
475
+ }
261
476
  export {
262
477
  DEFAULT_MENU,
263
478
  DEFAULT_RAIL,
264
479
  createStack,
480
+ fitFloat,
265
481
  fitStack,
266
482
  formSetsCross,
483
+ readFloats,
267
484
  readStack,
485
+ stackFloatTarget,
268
486
  stackTargets,
487
+ storeFloats,
269
488
  storeStack,
270
489
  withItem
271
490
  };
package/dist/stack.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/stack/model.ts","../src/stack/layout.ts","../src/stack/targets.ts","../src/stack/store.ts"],"sourcesContent":["/**\n * The edge stack's shapes. A stack is the items along one edge of a stage: a column (vertical, a side region) or a\n * row (horizontal, a bar). An item is a panel or a control group; it declares its forms, largest first, and the\n * solver (`fitStack`) picks one per item. Plain data in and out: no words, no time, no DOM.\n *\n * The form kinds are structural labels the app draws by; the solver reads only `menu` (a form with no room of its own)\n * and, for the cross size, `open`. Panel and group kinds may be mixed on one item.\n */\nimport type { Rect } from \"../geometry\";\n\n/** `\"column\"` runs the stack down (its main size is a height, its cross size a width); `\"row\"` runs it across. */\nexport type StackAxis = \"column\" | \"row\";\n\n/** How a panel draws: open (header and body), a header only, a rail icon, or an entry in the » menu. */\nexport type PanelFormKind = \"open\" | \"header\" | \"rail\" | \"menu\";\n/** How a control group draws: a row of words, the words stacked, icons only, or an entry in the » menu. */\nexport type GroupFormKind = \"row\" | \"stacked\" | \"icons\" | \"menu\";\nexport type StackFormKind = PanelFormKind | GroupFormKind;\n\n/** Along the stack: `min` is the least it can be drawn, `natural` what it asks for; `grows` takes spare length. */\nexport interface MainSize {\n readonly min: number;\n readonly natural: number;\n readonly grows?: boolean;\n}\n\n/** One way to draw an item. A `menu` form takes no room in the stack (the » entry stands for it). */\nexport interface StackForm {\n readonly kind: StackFormKind;\n readonly main: MainSize;\n /**\n * Across the stack: `natural` is what this form asks for (it sets the stack's cross size when `setsCross`); `min`\n * is the least it can be drawn across (default `natural`). A form that doesn't set the cross size takes its next\n * form only when its `min` is over the stack's cross size, so a header stays in a column an open panel made\n * narrow when it has a small `min`.\n */\n readonly cross: { readonly natural: number; readonly min?: number };\n /**\n * Whether this form sets the stack's cross size (the column's width, the bar's height). Default: true for a\n * panel's `open`, false for everything else (a group never sets a column's width).\n */\n readonly setsCross?: boolean;\n}\n\nexport interface StackItem {\n readonly id: string;\n /** Largest first. The solver starts at `from` and steps down one at a time. An item with none is left out. */\n readonly forms: readonly StackForm[];\n /** How recently it was used: the smaller, the longer ago, the sooner it gives way. */\n readonly used: number;\n /** Kept: gives way after every item that isn't. */\n readonly kept?: boolean;\n /**\n * The first form the item may take, an index into `forms` (default 0; a panel folded by hand starts at its header).\n * The solver never goes above it. `form` in the result and in the notes are indexes into `forms`, not into what is\n * left after `from`.\n */\n readonly from?: number;\n}\n\n/** The cross size a stack may take. With no form setting it, the stack is a `rail` wide (default {@link DEFAULT_RAIL}). */\nexport interface CrossLimits {\n readonly min: number;\n readonly max: number;\n readonly rail?: number;\n}\n\nexport interface StackInput {\n readonly axis: StackAxis;\n /** The stack's length along the axis. */\n readonly length: number;\n readonly cross: CrossLimits;\n /** In order, from the start of the stack. */\n readonly items: readonly StackItem[];\n /** The » menu entry's size along the stack, taken only while an item is in the menu (default {@link DEFAULT_MENU}). */\n readonly menu?: { readonly main: number };\n}\n\n/** The width of a stack nothing open holds, px. */\nexport const DEFAULT_RAIL = 30;\n/** The » menu entry's size along the stack, px. */\nexport const DEFAULT_MENU = 28;\n\nconst finite = (x: number, fallback: number): number => (Number.isFinite(x) ? x : fallback);\n\n/**\n * The cross limits with a non-number replaced: `min` and `rail` that aren't finite become 0 and the default rail, a\n * `max` that is NaN becomes no limit (Infinity is one already), and `max` is never under `min`.\n */\nexport function limitsOf(input: StackInput): { readonly min: number; readonly max: number; readonly rail: number } {\n const min = Math.max(0, finite(input.cross.min, 0));\n const max = Number.isNaN(input.cross.max) ? Infinity : input.cross.max;\n return { min, max: Math.max(min, max), rail: Math.max(0, finite(input.cross.rail ?? DEFAULT_RAIL, DEFAULT_RAIL)) };\n}\nexport const menuMain = (input: StackInput): number => Math.max(0, finite(input.menu?.main ?? DEFAULT_MENU, DEFAULT_MENU));\n\n/** Whether a form sets the stack's cross size: its own say, else true for a panel's `open`. */\nexport const formSetsCross = (f: StackForm): boolean => f.setsCross ?? f.kind === \"open\";\n\n/** `input` with `item` inserted at index `at` (clamped to the items' range). */\nexport function withItem(input: StackInput, item: StackItem, at: number): StackInput {\n const k = Math.min(Math.max(0, Math.floor(at)), input.items.length);\n return { ...input, items: [...input.items.slice(0, k), item, ...input.items.slice(k)] };\n}\n\n/** An item drawn: the form it took, its kind and its rect (stack-local px). One in the menu has the menu entry's rect. */\nexport interface PlacedStackItem {\n readonly id: string;\n /** Index into the item's `forms`. */\n readonly form: number;\n readonly kind: StackFormKind;\n readonly rect: Rect;\n}\n\nexport interface StackMenu {\n /** The » entry's rect. */\n readonly rect: Rect;\n /** The items in it, in the stack's order: exactly the items whose kind is `menu`. */\n readonly items: readonly string[];\n}\n\n/**\n * Why a stack looks as it does:\n * - `form-down`: an item took a smaller form than the one it started in (`from`, an index into its forms), by length\n * or by cross size.\n * - `to-menu`: an item is in the » menu.\n * - `too-wide`: an item's form doesn't set the cross size and its `cross.min` is over the stack's. Cross size never\n * puts an item in the menu, so it keeps its last drawn form and the app clips it.\n * - `no-forms`: an item with no forms, left out of `items`.\n * - `overflow`: no item can give more way and the stack is still `over` px too long (forms that don't end in a `menu`\n * form can't give more; neither can a stack where going into the menu would only make it longer).\n */\nexport type StackNote =\n | { readonly code: \"form-down\"; readonly id: string; readonly from: number; readonly to: number }\n | { readonly code: \"to-menu\"; readonly id: string }\n | { readonly code: \"too-wide\"; readonly id: string }\n | { readonly code: \"no-forms\"; readonly id: string }\n | { readonly code: \"overflow\"; readonly over: number };\nexport type StackNoteCode = StackNote[\"code\"];\n\nexport interface StackFit {\n readonly axis: StackAxis;\n readonly length: number;\n /** The stack's size across: the widest `open` (clamped to the limits), or the rail. */\n readonly cross: number;\n /** One per input item that has forms, in order. */\n readonly items: readonly PlacedStackItem[];\n /** The » entry, or null while no item is in the menu. */\n readonly menu: StackMenu | null;\n /** `no-forms` first, then `form-down` per item in the order they gave way, `to-menu`, `too-wide`, and `overflow`. */\n readonly notes: readonly StackNote[];\n}\n","/**\n * `fitStack`: which form each item of a stack takes, and where it goes.\n *\n * 1. Every item starts in its largest form it may take (`forms[from]`, default the first).\n * 2. The stack's cross size is the largest `cross.natural` among the items whose form sets it (an open panel's, by\n * default), clamped to the limits; with none, the rail width.\n * 3. Cross-fit: an item whose form doesn't set the cross size and whose `cross.min` is over it steps down until it\n * fits (a group never sets a column's width, so a group alone becomes a rail of icons). Cross size never steps\n * an item into the menu: when only the menu is left it keeps its last drawn form and a `too-wide` note says so.\n * 4. Give-way: while the smallest sizes (`main.min`) don't fit the length, the item used longest ago steps down one\n * form; unkept before kept, the later in the stack first on a tie. The same item keeps giving way until its forms\n * end, then the next does. A `menu` form takes no room; the » entry does, while any item is in it (and an item\n * doesn't go into the menu when that would only make the stack longer and nothing else can give way).\n * Steps 2 to 4 repeat until nothing changes: a panel folding can narrow the column, which asks more of the groups.\n * 5. The room the items leave goes to the forms that grow, shared by their natural sizes. With none, the items keep\n * their natural sizes from the start of the stack. Between the minimum and natural sizes every item shrinks by the\n * same share of what it can give.\n *\n * The solver has no open or closed state: it lays out the items it is given, and a consumer closes a panel by not\n * passing it (or by starting it at a later form with `from`). An item with no forms is left out (`no-forms`). Pure:\n * same input, same fit.\n */\nimport type { Rect } from \"../geometry\";\nimport { formSetsCross, limitsOf, menuMain, type PlacedStackItem, type StackFit, type StackForm, type StackInput, type StackItem, type StackMenu, type StackNote } from \"./model\";\n\nconst EPS = 1e-6;\nconst isMenu = (f: StackForm): boolean => f.kind === \"menu\";\nconst finite = (x: number, fallback = 0): number => (Number.isFinite(x) ? x : fallback);\nconst mainOf = (f: StackForm, which: \"min\" | \"natural\"): number => (isMenu(f) ? 0 : Math.max(0, finite(f.main[which])));\n/** The least a form can be drawn across the stack. */\nconst crossMin = (f: StackForm): number => finite(f.cross.min ?? f.cross.natural);\n\n/** The item that gives way next: unkept before kept, used longest ago, the later in the stack on a tie. */\nfunction victim(items: readonly StackItem[], at: readonly number[]): number {\n let best = -1;\n for (let i = 0; i < items.length; i++) {\n if (at[i]! + 1 >= items[i]!.forms.length) continue;\n if (best < 0) {\n best = i;\n continue;\n }\n const a = items[i]!;\n const b = items[best]!;\n const ka = a.kept ? 1 : 0;\n const kb = b.kept ? 1 : 0;\n if (ka !== kb ? ka < kb : a.used !== b.used ? a.used < b.used : true) best = i;\n }\n return best;\n}\n\nexport function fitStack(input: StackInput): StackFit {\n const length = Math.max(0, finite(input.length));\n const lim = limitsOf(input);\n const items = input.items.filter((it) => it.forms.length > 0);\n const start = items.map((it) => Math.min(Math.max(0, Math.floor(finite(it.from ?? 0))), it.forms.length - 1));\n const at = [...start];\n const form = (i: number): StackForm => items[i]!.forms[at[i]!]!;\n const idx = items.map((_, i) => i);\n\n const firstStep: number[] = [];\n const menuOrder: number[] = [];\n const step = (i: number): void => {\n if (!firstStep.includes(i)) firstStep.push(i);\n at[i]!++;\n if (isMenu(form(i)) && !menuOrder.includes(i)) menuOrder.push(i);\n };\n // An item that starts in the menu is in it from the start.\n for (const i of idx) if (isMenu(form(i))) menuOrder.push(i);\n\n const crossOf = (): number => {\n const setters = idx.filter((i) => !isMenu(form(i)) && formSetsCross(form(i)));\n if (setters.length === 0) return lim.rail;\n const want = Math.max(...setters.map((i) => finite(form(i).cross.natural)));\n return Math.min(Math.max(want, lim.min), lim.max);\n };\n const inMenu = (): boolean => idx.some((i) => isMenu(form(i)));\n const minSum = (): number => idx.reduce((s, i) => s + mainOf(form(i), \"min\"), 0) + (inMenu() ? menuMain(input) : 0);\n const canStep = (i: number): boolean => at[i]! + 1 < items[i]!.forms.length;\n const tooWide = (i: number, cross: number): boolean => !isMenu(form(i)) && !formSetsCross(form(i)) && crossMin(form(i)) > cross + EPS;\n\n let cross = crossOf();\n let overflow = 0;\n const guard = items.reduce((s, it) => s + it.forms.length, 0) + 1;\n for (let n = 0; n <= guard; n++) {\n cross = crossOf();\n // Too wide for the stack: step down, but never into the menu (that is the length's call).\n let stepped = false;\n for (const i of idx) {\n if (tooWide(i, cross) && canStep(i) && !isMenu(items[i]!.forms[at[i]! + 1]!)) {\n step(i);\n stepped = true;\n }\n }\n if (stepped) continue;\n const over = minSum() - length;\n if (over <= EPS) break;\n const v = victim(items, at);\n if (v < 0) {\n overflow = over;\n break;\n }\n // Into the menu only when that helps, or when something else can still give way.\n const intoMenu = isMenu(items[v]!.forms[at[v]! + 1]!);\n const gain = mainOf(form(v), \"min\") - (inMenu() ? 0 : menuMain(input));\n if (intoMenu && gain < 0 && idx.filter(canStep).length === 1) {\n overflow = over;\n break;\n }\n step(v);\n }\n cross = crossOf();\n\n // Sizes along the stack.\n const menuSize = inMenu() ? menuMain(input) : 0;\n const avail = Math.max(0, length - menuSize);\n const drawn = idx.filter((i) => !isMenu(form(i)));\n const min = (i: number): number => mainOf(form(i), \"min\");\n const nat = (i: number): number => Math.max(min(i), mainOf(form(i), \"natural\"));\n const sumMin = drawn.reduce((s, i) => s + min(i), 0);\n const sumNat = drawn.reduce((s, i) => s + nat(i), 0);\n const size = new Map<number, number>();\n if (sumNat <= avail + EPS) {\n const growers = drawn.filter((i) => form(i).main.grows);\n const spare = Math.max(0, avail - sumNat);\n const weight = growers.reduce((s, i) => s + nat(i), 0);\n for (const i of drawn) {\n const share = growers.includes(i) ? (weight > EPS ? nat(i) / weight : 1 / growers.length) : 0;\n size.set(i, nat(i) + spare * share);\n }\n } else {\n const t = avail > sumMin && sumNat > sumMin ? (avail - sumMin) / (sumNat - sumMin) : 0;\n for (const i of drawn) size.set(i, min(i) + (nat(i) - min(i)) * t);\n }\n\n const rectAt = (from: number, main: number): Rect => (input.axis === \"column\" ? { x: 0, y: from, width: cross, height: main } : { x: from, y: 0, width: main, height: cross });\n const rects = new Map<number, Rect>();\n let cursor = 0;\n for (const i of drawn) {\n const s = size.get(i)!;\n rects.set(i, rectAt(cursor, s));\n cursor += s;\n }\n const menuRect = rectAt(Math.max(cursor, length - menuSize), menuSize);\n const inside = idx.filter((i) => isMenu(form(i)));\n const menu: StackMenu | null = inside.length > 0 ? { rect: menuRect, items: inside.map((i) => items[i]!.id) } : null;\n\n const placed: PlacedStackItem[] = idx.map((i) => ({ id: items[i]!.id, form: at[i]!, kind: form(i).kind, rect: isMenu(form(i)) ? menuRect : rects.get(i)! }));\n\n const notes: StackNote[] = [];\n for (const it of input.items) if (it.forms.length === 0) notes.push({ code: \"no-forms\", id: it.id });\n for (const i of firstStep) if (at[i]! > start[i]!) notes.push({ code: \"form-down\", id: items[i]!.id, from: start[i]!, to: at[i]! });\n for (const i of menuOrder) if (isMenu(form(i))) notes.push({ code: \"to-menu\", id: items[i]!.id });\n for (const i of idx) if (tooWide(i, cross)) notes.push({ code: \"too-wide\", id: items[i]!.id });\n if (overflow > EPS) notes.push({ code: \"overflow\", over: overflow });\n return { axis: input.axis, length, cross, items: placed, menu, notes };\n}\n","/**\n * The drop targets for dragging an item into a stack, in the shape @n-uf/motion/drag takes. `stackTargets(input,\n * dragged)` gives one target for every gap of the stack without the dragged item (an item of the input with its id\n * is lifted out first, so a reorder within a stack works): before each item and after the last, so an empty stack has\n * one. An item with no forms has no place of its own, but its gaps count.\n *\n * - `id` `gap:<k>`, priority 2, `place` `{ kind: \"gap\", at: k }` (k counts the input's items, the dragged one lifted\n * out, including any with no forms; it is not an index into `fit.items`, which leaves those out: look the dragged\n * item up there by its id, as `slot` already does).\n * - `hit`: the gap's zone along the axis, the stack split at its items' middles (an item in the menu has no place of\n * its own: the gaps next to it split the stretch where the stack's drawn items end; with every item in the menu only\n * the last gap, \"at the end\", has any length, and the others are a point at the start). Across, it is the larger of\n * the stack's current size and the size the drop gives it, so the pointer over the preview is over the target.\n * - `fit`: the full `fitStack` of the stack with the dragged item inserted at k, and `input` the stack that was\n * solved. The preview is `slot`, the dragged item's rect in `fit`: exactly the rect it gets after the drop, because\n * the drop is that same solve. `marker` is the line where its leading edge lands: a rect with no thickness across\n * the stack's cross size (the app draws it with one).\n *\n * The dragged item counts as just used: it is newer than every item in the stack, so the others give way before it.\n * A group lands in the form that fits; a panel that would set a wider column makes room by that solve too. A dragged\n * item with no forms has no targets.\n */\nimport { rect, type Rect } from \"../geometry\";\nimport type { Target } from \"../targets/target\";\nimport { fitStack } from \"./layout\";\nimport { withItem, type StackFit, type StackInput, type StackItem } from \"./model\";\n\nexport type StackPlace = { readonly kind: \"gap\"; readonly at: number };\n\nexport interface StackTarget extends Target {\n readonly priority: 2;\n /** The item dragged. */\n readonly item: string;\n readonly place: StackPlace;\n /** The line where the dragged item's leading edge lands. */\n readonly marker: Rect;\n /** The stack with the dragged item inserted: what a drop here makes. */\n readonly input: StackInput;\n /** Its fit: the preview and the drop are both its rect for the dragged item. */\n readonly fit: StackFit;\n}\n\n/** The gap boundaries along the axis: `b[0]` is the start, `b[k]` the middle of item k - 1, `b[n + 1]` the length. */\nfunction bounds(input: StackInput, fit: StackFit): number[] {\n const column = input.axis === \"column\";\n const start = (r: Rect): number => (column ? r.y : r.x);\n const size = (r: Rect): number => (column ? r.height : r.width);\n const placed = new Map(fit.items.map((p) => [p.id, p]));\n const b = [0];\n // Where the drawn items have ended so far: an item in the menu (or with no forms) sits there.\n let end = 0;\n for (const it of input.items) {\n const p = placed.get(it.id);\n if (!p || p.kind === \"menu\") {\n b.push(end);\n continue;\n }\n b.push(start(p.rect) + size(p.rect) / 2);\n end = start(p.rect) + size(p.rect);\n }\n b.push(fit.length);\n // Monotone, whatever the sizes did (a stack that overflows).\n for (let i = 1; i < b.length; i++) b[i] = Math.min(Math.max(b[i]!, b[i - 1]!), fit.length);\n return b;\n}\n\nexport function stackTargets(input: StackInput, dragged: StackItem): StackTarget[] {\n if (dragged.forms.length === 0) return [];\n const rest: StackInput = { ...input, items: input.items.filter((it) => it.id !== dragged.id) };\n const base = fitStack(rest);\n const b = bounds(rest, base);\n const column = rest.axis === \"column\";\n const newest = rest.items.reduce((m, it) => Math.max(m, it.used), dragged.used) + 1;\n const item: StackItem = { ...dragged, used: newest };\n const out: StackTarget[] = [];\n for (let k = 0; k <= rest.items.length; k++) {\n const solved = withItem(rest, item, k);\n const fit = fitStack(solved);\n const slot = fit.items.find((p) => p.id === dragged.id)!.rect;\n const from = b[k]!;\n const to = b[k + 1]!;\n const across = Math.max(base.cross, fit.cross);\n out.push({\n id: `gap:${k}`,\n priority: 2,\n item: dragged.id,\n place: { kind: \"gap\", at: k },\n hit: column ? rect(0, from, across, to - from) : rect(from, 0, to - from, across),\n slot,\n marker: column ? rect(slot.x, slot.y, slot.width, 0) : rect(slot.x, slot.y, 0, slot.height),\n input: solved,\n fit,\n });\n }\n return out;\n}\n","/**\n * The stack's stored form: `storeStack(state)` gives plain JSON-safe data (`{ v: 1, order, kept, closed, used, forms\n * }`); `readStack(data, known?)` reads parsed data back and never throws. Anything but an object of version 1 is\n * `{ ok: false, error }`. Inside one it is lenient, as the panels' reader is: an id that isn't a string, is repeated\n * or (with `known`) isn't one the consumer has is dropped and listed in `dropped`; a `kept`, `closed`, `used` or\n * `forms` entry for an id that isn't in `order` is dropped; a form that isn't a whole number from 1 up is dropped;\n * the key `__proto__` in `forms` is dropped (the other keys, `constructor` and `prototype` included, are ids like any\n * other: `forms` is an object with no prototype, written and read); a field this version doesn't know is skipped and\n * reported as `\"<field>\"`. An item the consumer has that the stored order lacks is no error: the consumer places it.\n *\n * - `kept`: items kept open (they give way last).\n * - `closed`: items the user closed; their place in `order` is kept for when they open.\n * - `used`: the use order, oldest to newest, so a reload restores who gives way first (an item's `used` is its index in\n * it; one the list lacks is older than all). An id listed twice counts at its last place: the newest use wins.\n * - `forms`: the user's chosen form per item, the index of the largest form it may take (`StackItem.from`). 0, the\n * first, is not stored.\n */\n\nexport interface StackState {\n /** The items' order along the stack. */\n readonly order: readonly string[];\n /** Items kept open: they give way last. */\n readonly kept: readonly string[];\n /** Items the user closed. */\n readonly closed: readonly string[];\n /** The use order, oldest first. */\n readonly used: readonly string[];\n /** The form index the user chose per item (only those that chose one; absent means the first). */\n readonly forms: Readonly<Record<string, number>>;\n}\n\nexport interface StoredStack {\n readonly v: 1;\n readonly order: string[];\n readonly kept: string[];\n readonly closed: string[];\n readonly used: string[];\n readonly forms: Record<string, number>;\n}\n\nexport type StackRead = { readonly ok: true; readonly state: StackState; readonly dropped: readonly string[] } | { readonly ok: false; readonly error: string };\n\n/** A state with these items in this order, none kept or closed, used in that order, every form the first. */\nexport const createStack = (order: readonly string[]): StackState => ({ order: [...order], kept: [], closed: [], used: [...order], forms: {} });\n\nconst isForm = (f: unknown): f is number => typeof f === \"number\" && Number.isInteger(f) && f >= 1;\n/** Ids that are in `order`, each once, at the first place it was listed. */\nconst uniq = (ids: readonly string[], order: readonly string[]): string[] => [...new Set(ids)].filter((id) => order.includes(id));\n/** Ids that are in `order`, each once, at the last place it was listed (the newest use wins). */\nconst uniqLast = (ids: readonly string[], order: readonly string[]): string[] => ids.filter((id, i) => order.includes(id) && ids.lastIndexOf(id) === i);\n/** Forms live in an object with no prototype, so no key (`constructor`, `prototype`) is more than a key; only `__proto__` is refused. */\nconst noForms = (): Record<string, number> => Object.create(null) as Record<string, number>;\n\nexport function storeStack(state: StackState): StoredStack {\n const forms = noForms();\n for (const id of state.order) {\n const f = state.forms[id];\n if (id !== \"__proto__\" && Object.hasOwn(state.forms, id) && isForm(f)) forms[id] = f;\n }\n return { v: 1, order: [...state.order], kept: uniq(state.kept, state.order), closed: uniq(state.closed, state.order), used: uniqLast(state.used, state.order), forms };\n}\n\nconst isObject = (x: unknown): x is Record<string, unknown> => typeof x === \"object\" && x !== null && !Array.isArray(x);\n\nexport function readStack(data: unknown, known?: readonly string[]): StackRead {\n try {\n return read(data, known);\n } catch (e) {\n // A getter that throws, a proxy: still no throw.\n return { ok: false, error: `unreadable: ${e instanceof Error ? e.message : typeof e}` };\n }\n}\n\nconst versionWords = (v: unknown): string => (typeof v === \"number\" ? String(v) : typeof v === \"string\" ? JSON.stringify(v.length > 24 ? `${v.slice(0, 24)}…` : v) : typeof v);\n\nfunction read(data: unknown, known?: readonly string[]): StackRead {\n if (!isObject(data)) return { ok: false, error: \"not an object\" };\n if (data.v !== 1) return { ok: false, error: `unknown version ${versionWords(data.v)} (this reads version 1)` };\n if (!Array.isArray(data.order)) return { ok: false, error: \"order is missing\" };\n const dropped: string[] = [];\n const drop = (what: string): void => {\n if (!dropped.includes(what)) dropped.push(what);\n };\n for (const k of Object.keys(data)) if (![\"v\", \"order\", \"kept\", \"closed\", \"used\", \"forms\"].includes(k)) drop(k);\n const order: string[] = [];\n for (const x of data.order as unknown[]) {\n if (typeof x !== \"string\") drop(`a ${typeof x} where an id goes`);\n else if ((known && !known.includes(x)) || order.includes(x)) drop(x);\n else order.push(x);\n }\n // A list of ids that are in `order`, each once: at the first place it was listed, or for `used` the last (the newest use wins).\n const list = (key: \"kept\" | \"closed\" | \"used\"): string[] => {\n const out: string[] = [];\n const x = data[key];\n if (x === undefined) return out;\n if (!Array.isArray(x)) {\n drop(key);\n return out;\n }\n for (const id of x as unknown[]) {\n if (typeof id === \"string\" && order.includes(id)) {\n if (key === \"used\" && out.includes(id)) out.splice(out.indexOf(id), 1);\n if (!out.includes(id)) out.push(id);\n } else drop(typeof id === \"string\" ? `${key}:${id}` : `a ${typeof id} in ${key}`);\n }\n return out;\n };\n const kept = list(\"kept\");\n const closed = list(\"closed\");\n const used = list(\"used\");\n const forms = noForms();\n if (data.forms !== undefined) {\n if (!isObject(data.forms)) drop(\"forms\");\n else {\n for (const id of Object.keys(data.forms)) {\n const f = data.forms[id];\n if (id === \"__proto__\" || !Object.hasOwn(data.forms, id) || !order.includes(id) || !isForm(f)) drop(`forms:${id}`);\n else forms[id] = f;\n }\n }\n }\n return { ok: true, state: { order, kept, closed, used, forms }, dropped };\n}\n"],"mappings":"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","names":["finite","cross"]}
1
+ {"version":3,"sources":["../src/stack/model.ts","../src/stack/layout.ts","../src/stack/targets.ts","../src/stack/store.ts","../src/stack/floats.ts"],"sourcesContent":["/**\n * The edge stack's shapes. A stack is the items along one edge of a stage: a column (vertical, a side region) or a\n * row (horizontal, a bar). An item is a panel or a control group; it declares its forms, largest first, and the\n * solver (`fitStack`) picks one per item. Plain data in and out: no words, no time, no DOM.\n *\n * The form kinds are structural labels the app draws by; the solver reads only `menu` (a form with no room of its own)\n * and, for the cross size, `open`. Panel and group kinds may be mixed on one item.\n */\nimport type { Rect } from \"../geometry\";\n\n/** `\"column\"` runs the stack down (its main size is a height, its cross size a width); `\"row\"` runs it across. */\nexport type StackAxis = \"column\" | \"row\";\n\n/** How a panel draws: open (header and body), a header only, a rail icon, or an entry in the » menu. */\nexport type PanelFormKind = \"open\" | \"header\" | \"rail\" | \"menu\";\n/** How a control group draws: a row of words, the words stacked, icons only, or an entry in the » menu. */\nexport type GroupFormKind = \"row\" | \"stacked\" | \"icons\" | \"menu\";\nexport type StackFormKind = PanelFormKind | GroupFormKind;\n\n/** Along the stack: `min` is the least it can be drawn, `natural` what it asks for; `grows` takes spare length. */\nexport interface MainSize {\n readonly min: number;\n readonly natural: number;\n readonly grows?: boolean;\n}\n\n/** One way to draw an item. A `menu` form takes no room in the stack (the » entry stands for it). */\nexport interface StackForm {\n readonly kind: StackFormKind;\n readonly main: MainSize;\n /**\n * Across the stack: `natural` is what this form asks for (it sets the stack's cross size when `setsCross`); `min`\n * is the least it can be drawn across (default `natural`). A form that doesn't set the cross size takes its next\n * form only when its `min` is over the stack's cross size, so a header stays in a column an open panel made\n * narrow when it has a small `min`.\n */\n readonly cross: { readonly natural: number; readonly min?: number };\n /**\n * Whether this form sets the stack's cross size (the column's width, the bar's height). Default: true for a\n * panel's `open`, false for everything else (a group never sets a column's width).\n */\n readonly setsCross?: boolean;\n}\n\nexport interface StackItem {\n readonly id: string;\n /** Largest first. The solver starts at `from` and steps down one at a time. An item with none is left out. */\n readonly forms: readonly StackForm[];\n /** How recently it was used: the smaller, the longer ago, the sooner it gives way. */\n readonly used: number;\n /** Kept: gives way after every item that isn't. */\n readonly kept?: boolean;\n /**\n * The first form the item may take, an index into `forms` (default 0; a panel folded by hand starts at its header).\n * The solver never goes above it. `form` in the result and in the notes are indexes into `forms`, not into what is\n * left after `from`.\n */\n readonly from?: number;\n}\n\n/** The cross size a stack may take. With no form setting it, the stack is a `rail` wide (default {@link DEFAULT_RAIL}). */\nexport interface CrossLimits {\n readonly min: number;\n readonly max: number;\n readonly rail?: number;\n}\n\nexport interface StackInput {\n readonly axis: StackAxis;\n /** The stack's length along the axis. */\n readonly length: number;\n readonly cross: CrossLimits;\n /** In order, from the start of the stack. */\n readonly items: readonly StackItem[];\n /** The » menu entry's size along the stack, taken only while an item is in the menu (default {@link DEFAULT_MENU}). */\n readonly menu?: { readonly main: number };\n}\n\n/** The width of a stack nothing open holds, px. */\nexport const DEFAULT_RAIL = 30;\n/** The » menu entry's size along the stack, px. */\nexport const DEFAULT_MENU = 28;\n\nconst finite = (x: number, fallback: number): number => (Number.isFinite(x) ? x : fallback);\n\n/**\n * The cross limits with a non-number replaced: `min` and `rail` that aren't finite become 0 and the default rail, a\n * `max` that is NaN becomes no limit (Infinity is one already), and `max` is never under `min`.\n */\nexport function limitsOf(input: StackInput): { readonly min: number; readonly max: number; readonly rail: number } {\n const min = Math.max(0, finite(input.cross.min, 0));\n const max = Number.isNaN(input.cross.max) ? Infinity : input.cross.max;\n return { min, max: Math.max(min, max), rail: Math.max(0, finite(input.cross.rail ?? DEFAULT_RAIL, DEFAULT_RAIL)) };\n}\nexport const menuMain = (input: StackInput): number => Math.max(0, finite(input.menu?.main ?? DEFAULT_MENU, DEFAULT_MENU));\n\n/** Whether a form sets the stack's cross size: its own say, else true for a panel's `open`. */\nexport const formSetsCross = (f: StackForm): boolean => f.setsCross ?? f.kind === \"open\";\n\n/** `input` with `item` inserted at index `at` (clamped to the items' range). */\nexport function withItem(input: StackInput, item: StackItem, at: number): StackInput {\n const k = Math.min(Math.max(0, Math.floor(at)), input.items.length);\n return { ...input, items: [...input.items.slice(0, k), item, ...input.items.slice(k)] };\n}\n\n/** An item drawn: the form it took, its kind and its rect (stack-local px). One in the menu has the menu entry's rect. */\nexport interface PlacedStackItem {\n readonly id: string;\n /** Index into the item's `forms`. */\n readonly form: number;\n readonly kind: StackFormKind;\n readonly rect: Rect;\n}\n\nexport interface StackMenu {\n /** The » entry's rect. */\n readonly rect: Rect;\n /** The items in it, in the stack's order: exactly the items whose kind is `menu`. */\n readonly items: readonly string[];\n}\n\n/**\n * Why a stack looks as it does:\n * - `form-down`: an item took a smaller form than the one it started in (`from`, an index into its forms), by length\n * or by cross size.\n * - `to-menu`: an item is in the » menu.\n * - `too-wide`: an item's form doesn't set the cross size and its `cross.min` is over the stack's. Cross size never\n * puts an item in the menu, so it keeps its last drawn form and the app clips it.\n * - `no-forms`: an item with no forms, left out of `items`.\n * - `overflow`: no item can give more way and the stack is still `over` px too long (forms that don't end in a `menu`\n * form can't give more; neither can a stack where going into the menu would only make it longer).\n */\nexport type StackNote =\n | { readonly code: \"form-down\"; readonly id: string; readonly from: number; readonly to: number }\n | { readonly code: \"to-menu\"; readonly id: string }\n | { readonly code: \"too-wide\"; readonly id: string }\n | { readonly code: \"no-forms\"; readonly id: string }\n | { readonly code: \"overflow\"; readonly over: number };\nexport type StackNoteCode = StackNote[\"code\"];\n\nexport interface StackFit {\n readonly axis: StackAxis;\n readonly length: number;\n /** The stack's size across: the widest `open` (clamped to the limits), or the rail. */\n readonly cross: number;\n /** One per input item that has forms, in order. */\n readonly items: readonly PlacedStackItem[];\n /** The » entry, or null while no item is in the menu. */\n readonly menu: StackMenu | null;\n /** `no-forms` first, then `form-down` per item in the order they gave way, `to-menu`, `too-wide`, and `overflow`. */\n readonly notes: readonly StackNote[];\n}\n","/**\n * `fitStack`: which form each item of a stack takes, and where it goes.\n *\n * 1. Every item starts in its largest form it may take (`forms[from]`, default the first).\n * 2. The stack's cross size is the largest `cross.natural` among the items whose form sets it (an open panel's, by\n * default), clamped to the limits; with none, the rail width.\n * 3. Cross-fit: an item whose form doesn't set the cross size and whose `cross.min` is over it steps down until it\n * fits (a group never sets a column's width, so a group alone becomes a rail of icons). Cross size never steps\n * an item into the menu: when only the menu is left it keeps its last drawn form and a `too-wide` note says so.\n * 4. Give-way: while the smallest sizes (`main.min`) don't fit the length, the item used longest ago steps down one\n * form; unkept before kept, the later in the stack first on a tie. The same item keeps giving way until its forms\n * end, then the next does. A `menu` form takes no room; the » entry does, while any item is in it (and an item\n * doesn't go into the menu when that would only make the stack longer and nothing else can give way).\n * Steps 2 to 4 repeat until nothing changes: a panel folding can narrow the column, which asks more of the groups.\n * 5. The room the items leave goes to the forms that grow, shared by their natural sizes. With none, the items keep\n * their natural sizes from the start of the stack. Between the minimum and natural sizes every item shrinks by the\n * same share of what it can give.\n *\n * The solver has no open or closed state: it lays out the items it is given, and a consumer closes a panel by not\n * passing it (or by starting it at a later form with `from`). An item with no forms is left out (`no-forms`). Pure:\n * same input, same fit.\n */\nimport type { Rect } from \"../geometry\";\nimport { formSetsCross, limitsOf, menuMain, type PlacedStackItem, type StackFit, type StackForm, type StackInput, type StackItem, type StackMenu, type StackNote } from \"./model\";\n\nconst EPS = 1e-6;\nconst isMenu = (f: StackForm): boolean => f.kind === \"menu\";\nconst finite = (x: number, fallback = 0): number => (Number.isFinite(x) ? x : fallback);\nconst mainOf = (f: StackForm, which: \"min\" | \"natural\"): number => (isMenu(f) ? 0 : Math.max(0, finite(f.main[which])));\n/** The least a form can be drawn across the stack. */\nconst crossMin = (f: StackForm): number => finite(f.cross.min ?? f.cross.natural);\n\n/** The item that gives way next: unkept before kept, used longest ago, the later in the stack on a tie. */\nfunction victim(items: readonly StackItem[], at: readonly number[]): number {\n let best = -1;\n for (let i = 0; i < items.length; i++) {\n if (at[i]! + 1 >= items[i]!.forms.length) continue;\n if (best < 0) {\n best = i;\n continue;\n }\n const a = items[i]!;\n const b = items[best]!;\n const ka = a.kept ? 1 : 0;\n const kb = b.kept ? 1 : 0;\n if (ka !== kb ? ka < kb : a.used !== b.used ? a.used < b.used : true) best = i;\n }\n return best;\n}\n\nexport function fitStack(input: StackInput): StackFit {\n const length = Math.max(0, finite(input.length));\n const lim = limitsOf(input);\n const items = input.items.filter((it) => it.forms.length > 0);\n const start = items.map((it) => Math.min(Math.max(0, Math.floor(finite(it.from ?? 0))), it.forms.length - 1));\n const at = [...start];\n const form = (i: number): StackForm => items[i]!.forms[at[i]!]!;\n const idx = items.map((_, i) => i);\n\n const firstStep: number[] = [];\n const menuOrder: number[] = [];\n const step = (i: number): void => {\n if (!firstStep.includes(i)) firstStep.push(i);\n at[i]!++;\n if (isMenu(form(i)) && !menuOrder.includes(i)) menuOrder.push(i);\n };\n // An item that starts in the menu is in it from the start.\n for (const i of idx) if (isMenu(form(i))) menuOrder.push(i);\n\n const crossOf = (): number => {\n const setters = idx.filter((i) => !isMenu(form(i)) && formSetsCross(form(i)));\n if (setters.length === 0) return lim.rail;\n const want = Math.max(...setters.map((i) => finite(form(i).cross.natural)));\n return Math.min(Math.max(want, lim.min), lim.max);\n };\n const inMenu = (): boolean => idx.some((i) => isMenu(form(i)));\n const minSum = (): number => idx.reduce((s, i) => s + mainOf(form(i), \"min\"), 0) + (inMenu() ? menuMain(input) : 0);\n const canStep = (i: number): boolean => at[i]! + 1 < items[i]!.forms.length;\n const tooWide = (i: number, cross: number): boolean => !isMenu(form(i)) && !formSetsCross(form(i)) && crossMin(form(i)) > cross + EPS;\n\n let cross = crossOf();\n let overflow = 0;\n const guard = items.reduce((s, it) => s + it.forms.length, 0) + 1;\n for (let n = 0; n <= guard; n++) {\n cross = crossOf();\n // Too wide for the stack: step down, but never into the menu (that is the length's call).\n let stepped = false;\n for (const i of idx) {\n if (tooWide(i, cross) && canStep(i) && !isMenu(items[i]!.forms[at[i]! + 1]!)) {\n step(i);\n stepped = true;\n }\n }\n if (stepped) continue;\n const over = minSum() - length;\n if (over <= EPS) break;\n const v = victim(items, at);\n if (v < 0) {\n overflow = over;\n break;\n }\n // Into the menu only when that helps, or when something else can still give way.\n const intoMenu = isMenu(items[v]!.forms[at[v]! + 1]!);\n const gain = mainOf(form(v), \"min\") - (inMenu() ? 0 : menuMain(input));\n if (intoMenu && gain < 0 && idx.filter(canStep).length === 1) {\n overflow = over;\n break;\n }\n step(v);\n }\n cross = crossOf();\n\n // Sizes along the stack.\n const menuSize = inMenu() ? menuMain(input) : 0;\n const avail = Math.max(0, length - menuSize);\n const drawn = idx.filter((i) => !isMenu(form(i)));\n const min = (i: number): number => mainOf(form(i), \"min\");\n const nat = (i: number): number => Math.max(min(i), mainOf(form(i), \"natural\"));\n const sumMin = drawn.reduce((s, i) => s + min(i), 0);\n const sumNat = drawn.reduce((s, i) => s + nat(i), 0);\n const size = new Map<number, number>();\n if (sumNat <= avail + EPS) {\n const growers = drawn.filter((i) => form(i).main.grows);\n const spare = Math.max(0, avail - sumNat);\n const weight = growers.reduce((s, i) => s + nat(i), 0);\n for (const i of drawn) {\n const share = growers.includes(i) ? (weight > EPS ? nat(i) / weight : 1 / growers.length) : 0;\n size.set(i, nat(i) + spare * share);\n }\n } else {\n const t = avail > sumMin && sumNat > sumMin ? (avail - sumMin) / (sumNat - sumMin) : 0;\n for (const i of drawn) size.set(i, min(i) + (nat(i) - min(i)) * t);\n }\n\n const rectAt = (from: number, main: number): Rect => (input.axis === \"column\" ? { x: 0, y: from, width: cross, height: main } : { x: from, y: 0, width: main, height: cross });\n const rects = new Map<number, Rect>();\n let cursor = 0;\n for (const i of drawn) {\n const s = size.get(i)!;\n rects.set(i, rectAt(cursor, s));\n cursor += s;\n }\n const menuRect = rectAt(Math.max(cursor, length - menuSize), menuSize);\n const inside = idx.filter((i) => isMenu(form(i)));\n const menu: StackMenu | null = inside.length > 0 ? { rect: menuRect, items: inside.map((i) => items[i]!.id) } : null;\n\n const placed: PlacedStackItem[] = idx.map((i) => ({ id: items[i]!.id, form: at[i]!, kind: form(i).kind, rect: isMenu(form(i)) ? menuRect : rects.get(i)! }));\n\n const notes: StackNote[] = [];\n for (const it of input.items) if (it.forms.length === 0) notes.push({ code: \"no-forms\", id: it.id });\n for (const i of firstStep) if (at[i]! > start[i]!) notes.push({ code: \"form-down\", id: items[i]!.id, from: start[i]!, to: at[i]! });\n for (const i of menuOrder) if (isMenu(form(i))) notes.push({ code: \"to-menu\", id: items[i]!.id });\n for (const i of idx) if (tooWide(i, cross)) notes.push({ code: \"too-wide\", id: items[i]!.id });\n if (overflow > EPS) notes.push({ code: \"overflow\", over: overflow });\n return { axis: input.axis, length, cross, items: placed, menu, notes };\n}\n","/**\n * The drop targets for dragging an item into a stack, in the shape @n-uf/motion/drag takes. `stackTargets(input,\n * dragged)` gives one target for every gap of the stack without the dragged item (an item of the input with its id\n * is lifted out first, so a reorder within a stack works): before each item and after the last, so an empty stack has\n * one. An item with no forms has no place of its own, but its gaps count.\n *\n * - `id` `gap:<k>`, priority 2, `place` `{ kind: \"gap\", at: k }` (k counts the input's items, the dragged one lifted\n * out, including any with no forms; it is not an index into `fit.items`, which leaves those out: look the dragged\n * item up there by its id, as `slot` already does).\n * - `hit`: the gap's zone along the axis, the stack split at its items' middles (an item in the menu has no place of\n * its own: the gaps next to it split the stretch where the stack's drawn items end; with every item in the menu only\n * the last gap, \"at the end\", has any length, and the others are a point at the start). Across, it is the larger of\n * the stack's current size and the size the drop gives it, so the pointer over the preview is over the target.\n * - `fit`: the full `fitStack` of the stack with the dragged item inserted at k, and `input` the stack that was\n * solved. The preview is `slot`, the dragged item's rect in `fit`: exactly the rect it gets after the drop, because\n * the drop is that same solve. `marker` is the line where its leading edge lands: a rect with no thickness across\n * the stack's cross size (the app draws it with one).\n *\n * The dragged item counts as just used: it is newer than every item in the stack, so the others give way before it.\n * A group lands in the form that fits; a panel that would set a wider column makes room by that solve too. A dragged\n * item with no forms has no targets.\n */\nimport { rect, type Rect } from \"../geometry\";\nimport type { Target } from \"../targets/target\";\nimport { fitStack } from \"./layout\";\nimport { withItem, type StackFit, type StackInput, type StackItem } from \"./model\";\n\nexport type StackPlace = { readonly kind: \"gap\"; readonly at: number };\n\nexport interface StackTarget extends Target {\n readonly priority: 2;\n /** The item dragged. */\n readonly item: string;\n readonly place: StackPlace;\n /** The line where the dragged item's leading edge lands. */\n readonly marker: Rect;\n /** The stack with the dragged item inserted: what a drop here makes. */\n readonly input: StackInput;\n /** Its fit: the preview and the drop are both its rect for the dragged item. */\n readonly fit: StackFit;\n}\n\n/** The gap boundaries along the axis: `b[0]` is the start, `b[k]` the middle of item k - 1, `b[n + 1]` the length. */\nfunction bounds(input: StackInput, fit: StackFit): number[] {\n const column = input.axis === \"column\";\n const start = (r: Rect): number => (column ? r.y : r.x);\n const size = (r: Rect): number => (column ? r.height : r.width);\n const placed = new Map(fit.items.map((p) => [p.id, p]));\n const b = [0];\n // Where the drawn items have ended so far: an item in the menu (or with no forms) sits there.\n let end = 0;\n for (const it of input.items) {\n const p = placed.get(it.id);\n if (!p || p.kind === \"menu\") {\n b.push(end);\n continue;\n }\n b.push(start(p.rect) + size(p.rect) / 2);\n end = start(p.rect) + size(p.rect);\n }\n b.push(fit.length);\n // Monotone, whatever the sizes did (a stack that overflows).\n for (let i = 1; i < b.length; i++) b[i] = Math.min(Math.max(b[i]!, b[i - 1]!), fit.length);\n return b;\n}\n\nexport function stackTargets(input: StackInput, dragged: StackItem): StackTarget[] {\n if (dragged.forms.length === 0) return [];\n const rest: StackInput = { ...input, items: input.items.filter((it) => it.id !== dragged.id) };\n const base = fitStack(rest);\n const b = bounds(rest, base);\n const column = rest.axis === \"column\";\n const newest = rest.items.reduce((m, it) => Math.max(m, it.used), dragged.used) + 1;\n const item: StackItem = { ...dragged, used: newest };\n const out: StackTarget[] = [];\n for (let k = 0; k <= rest.items.length; k++) {\n const solved = withItem(rest, item, k);\n const fit = fitStack(solved);\n const slot = fit.items.find((p) => p.id === dragged.id)!.rect;\n const from = b[k]!;\n const to = b[k + 1]!;\n const across = Math.max(base.cross, fit.cross);\n out.push({\n id: `gap:${k}`,\n priority: 2,\n item: dragged.id,\n place: { kind: \"gap\", at: k },\n hit: column ? rect(0, from, across, to - from) : rect(from, 0, to - from, across),\n slot,\n marker: column ? rect(slot.x, slot.y, slot.width, 0) : rect(slot.x, slot.y, 0, slot.height),\n input: solved,\n fit,\n });\n }\n return out;\n}\n","/**\n * The stack's stored form: `storeStack(state)` gives plain JSON-safe data (`{ v: 1, order, kept, closed, used, forms\n * }`); `readStack(data, known?)` reads parsed data back and never throws. Anything but an object of version 1 is\n * `{ ok: false, error }`. Inside one it is lenient, as the panels' reader is: an id that isn't a string, is repeated\n * or (with `known`) isn't one the consumer has is dropped and listed in `dropped`; a `kept`, `closed`, `used` or\n * `forms` entry for an id that isn't in `order` is dropped; a form that isn't a whole number from 1 up is dropped;\n * the key `__proto__` in `forms` is dropped (the other keys, `constructor` and `prototype` included, are ids like any\n * other: `forms` is an object with no prototype, written and read); a field this version doesn't know is skipped and\n * reported as `\"<field>\"`. An item the consumer has that the stored order lacks is no error: the consumer places it.\n *\n * - `kept`: items kept open (they give way last).\n * - `closed`: items the user closed; their place in `order` is kept for when they open.\n * - `used`: the use order, oldest to newest, so a reload restores who gives way first (an item's `used` is its index in\n * it; one the list lacks is older than all). An id listed twice counts at its last place: the newest use wins.\n * - `forms`: the user's chosen form per item, the index of the largest form it may take (`StackItem.from`). 0, the\n * first, is not stored.\n */\n\nexport interface StackState {\n /** The items' order along the stack. */\n readonly order: readonly string[];\n /** Items kept open: they give way last. */\n readonly kept: readonly string[];\n /** Items the user closed. */\n readonly closed: readonly string[];\n /** The use order, oldest first. */\n readonly used: readonly string[];\n /** The form index the user chose per item (only those that chose one; absent means the first). */\n readonly forms: Readonly<Record<string, number>>;\n}\n\nexport interface StoredStack {\n readonly v: 1;\n readonly order: string[];\n readonly kept: string[];\n readonly closed: string[];\n readonly used: string[];\n readonly forms: Record<string, number>;\n}\n\nexport type StackRead = { readonly ok: true; readonly state: StackState; readonly dropped: readonly string[] } | { readonly ok: false; readonly error: string };\n\n/** A state with these items in this order, none kept or closed, used in that order, every form the first. */\nexport const createStack = (order: readonly string[]): StackState => ({ order: [...order], kept: [], closed: [], used: [...order], forms: {} });\n\nconst isForm = (f: unknown): f is number => typeof f === \"number\" && Number.isInteger(f) && f >= 1;\n/** Ids that are in `order`, each once, at the first place it was listed. */\nconst uniq = (ids: readonly string[], order: readonly string[]): string[] => [...new Set(ids)].filter((id) => order.includes(id));\n/** Ids that are in `order`, each once, at the last place it was listed (the newest use wins). */\nconst uniqLast = (ids: readonly string[], order: readonly string[]): string[] => ids.filter((id, i) => order.includes(id) && ids.lastIndexOf(id) === i);\n/** Forms live in an object with no prototype, so no key (`constructor`, `prototype`) is more than a key; only `__proto__` is refused. */\nconst noForms = (): Record<string, number> => Object.create(null) as Record<string, number>;\n\nexport function storeStack(state: StackState): StoredStack {\n const forms = noForms();\n for (const id of state.order) {\n const f = state.forms[id];\n if (id !== \"__proto__\" && Object.hasOwn(state.forms, id) && isForm(f)) forms[id] = f;\n }\n return { v: 1, order: [...state.order], kept: uniq(state.kept, state.order), closed: uniq(state.closed, state.order), used: uniqLast(state.used, state.order), forms };\n}\n\nconst isObject = (x: unknown): x is Record<string, unknown> => typeof x === \"object\" && x !== null && !Array.isArray(x);\n\nexport function readStack(data: unknown, known?: readonly string[]): StackRead {\n try {\n return read(data, known);\n } catch (e) {\n // A getter that throws, a proxy: still no throw.\n return { ok: false, error: `unreadable: ${e instanceof Error ? e.message : typeof e}` };\n }\n}\n\n/** A stored version in words for an error (a string is quoted and cut); shared with the floats' reader, not exported from the entry. */\nexport const versionWords = (v: unknown): string => (typeof v === \"number\" ? String(v) : typeof v === \"string\" ? JSON.stringify(v.length > 24 ? `${v.slice(0, 24)}…` : v) : typeof v);\n\nfunction read(data: unknown, known?: readonly string[]): StackRead {\n if (!isObject(data)) return { ok: false, error: \"not an object\" };\n if (data.v !== 1) return { ok: false, error: `unknown version ${versionWords(data.v)} (this reads version 1)` };\n if (!Array.isArray(data.order)) return { ok: false, error: \"order is missing\" };\n const dropped: string[] = [];\n const drop = (what: string): void => {\n if (!dropped.includes(what)) dropped.push(what);\n };\n for (const k of Object.keys(data)) if (![\"v\", \"order\", \"kept\", \"closed\", \"used\", \"forms\"].includes(k)) drop(k);\n const order: string[] = [];\n for (const x of data.order as unknown[]) {\n if (typeof x !== \"string\") drop(`a ${typeof x} where an id goes`);\n else if ((known && !known.includes(x)) || order.includes(x)) drop(x);\n else order.push(x);\n }\n // A list of ids that are in `order`, each once: at the first place it was listed, or for `used` the last (the newest use wins).\n const list = (key: \"kept\" | \"closed\" | \"used\"): string[] => {\n const out: string[] = [];\n const x = data[key];\n if (x === undefined) return out;\n if (!Array.isArray(x)) {\n drop(key);\n return out;\n }\n for (const id of x as unknown[]) {\n if (typeof id === \"string\" && order.includes(id)) {\n if (key === \"used\" && out.includes(id)) out.splice(out.indexOf(id), 1);\n if (!out.includes(id)) out.push(id);\n } else drop(typeof id === \"string\" ? `${key}:${id}` : `a ${typeof id} in ${key}`);\n }\n return out;\n };\n const kept = list(\"kept\");\n const closed = list(\"closed\");\n const used = list(\"used\");\n const forms = noForms();\n if (data.forms !== undefined) {\n if (!isObject(data.forms)) drop(\"forms\");\n else {\n for (const id of Object.keys(data.forms)) {\n const f = data.forms[id];\n if (id === \"__proto__\" || !Object.hasOwn(data.forms, id) || !order.includes(id) || !isForm(f)) drop(`forms:${id}`);\n else forms[id] = f;\n }\n }\n }\n return { ok: true, state: { order, kept, closed, used, forms }, dropped };\n}\n","/**\n * Floats for the edge stack: an item that isn't docked lies above the pages in the stage's own px (view space), so the\n * pages scrolling under it never move it. A float has no pinned flag: there is one kind of float, and it is in view\n * space. Pure, like the rest of `./stack`.\n *\n * - `fitFloat(rect, stage, avoid)`: the rect a float gets. It is moved (never resized) to lie inside the stage, then\n * beside whatever docked content it would cover: the nearest place inside the stage that is clear of every `avoid`\n * rect (the docked items' rects, in the same px). One larger than the stage keeps its top-left corner on the\n * stage's; if no clear place exists the float stays where it covers the least and `covers` says which rects.\n * A clear place is found whenever one exists: the x's are tried nearest first and, for each, the blocked stretches of\n * y are merged and skipped, so no clear place is missed however many places lie between. Cost (measured in a test\n * run on a laptop): about 100 docked rects 0.1 ms, 300 a few ms even when no place is clear (6 ms median, 15 ms\n * worst), 1,000 about 17 ms; a safety net stops the clear search after 3 million rect tests and says so in\n * `searchCapped`; a stage with no clear place looks at the 2,048 nearest places for the least covered.\n * - `stackFloatTarget(input)`: a drop target in the shape `@n-uf/motion/drag` takes, free (no room is made for it),\n * `kind` `\"float\"`, id `float:<item>`, whose `hit` covers the whole plane, so a pointer past the stage's edge still\n * hits it (a float drop never cancels), at priority 3 so a docked stack's gaps win where they are. `make(point,\n * grab)` is the one answer for the preview and the drop: the float's rect with the pointer holding it at `grab`,\n * clamped and clear of the docked content.\n * - `storeFloats` / `readFloats`: the stored form, `{ v: 1, floats: [{ id, x, y, width, height }] }`, in stacking\n * order (the last is on top). It writes no `pinned`; one an old store has is read, ignored and listed.\n */\nimport { clampInto, rect, type Point, type Rect, type Size } from \"../geometry\";\nimport type { Target } from \"../targets/target\";\nimport { versionWords } from \"./store\";\n\n/** A float: the item's id and its rect in view space (the stage's px). */\nexport interface StackFloat {\n readonly id: string;\n readonly rect: Rect;\n}\n\nexport interface FloatFit {\n /** The float's rect: inside the stage and clear of the docked content where a clear place exists. */\n readonly rect: Rect;\n /** It was moved to lie inside the stage. */\n readonly clamped: boolean;\n /** It was moved beside the docked content it would have covered. */\n readonly snapped: boolean;\n /**\n * Indexes into `avoid` of the rects it still covers. Empty whenever a clear place was found, which is whenever one\n * exists (unless `searchCapped`); not empty only when the stage has no clear place for the float, and then it stays\n * where it covers the least among the 2,048 nearest candidate places.\n */\n readonly covers: readonly number[];\n /** The safety net on the clear search was hit (thousands of rects): the place found may not be the nearest, or a clear place may exist that was not found. Normally false. */\n readonly searchCapped: boolean;\n}\n\nconst EPS = 1e-3;\nconst finite = (x: number, fallback: number): number => (Number.isFinite(x) ? x : fallback);\n\n/** Whether two rects share an area (touching edges don't). */\nconst overlaps = (a: Rect, b: Rect): boolean => Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x) > EPS && Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y) > EPS;\nconst shared = (a: Rect, b: Rect): number =>\n Math.max(0, Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y));\n\n/** `r` with a non-number replaced and no negative size. */\nconst clean = (r: Rect, at: Rect): Rect => rect(finite(r.x, at.x), finite(r.y, at.y), Math.max(0, finite(r.width, 0)), Math.max(0, finite(r.height, 0)));\n\n/** A safety net on the clear search, in rect tests (only thousands of rects reach it), and the places the least-covered search looks at. */\nconst WORK = 3_000_000;\nconst COVERED_PLACES = 2048;\n\ntype Triple = [number, number, number];\n\n/** A binary min-heap of `[index, index, distance]` triples, by distance then indexes (a fixed order). */\nclass Nearest {\n private readonly h: Triple[] = [];\n private less(a: Triple, b: Triple): boolean {\n return a[2] !== b[2] ? a[2] < b[2] : a[0] !== b[0] ? a[0] < b[0] : a[1] < b[1];\n }\n get size(): number {\n return this.h.length;\n }\n push(t: Triple): void {\n const h = this.h;\n let i = h.push(t) - 1;\n while (i > 0) {\n const p = (i - 1) >> 1;\n if (!this.less(h[i]!, h[p]!)) break;\n [h[i], h[p]] = [h[p]!, h[i]!];\n i = p;\n }\n }\n pop(): Triple {\n const h = this.h;\n const top = h[0]!;\n const last = h.pop()!;\n if (h.length > 0) {\n h[0] = last;\n let i = 0;\n for (;;) {\n const l = 2 * i + 1;\n const r = l + 1;\n let m = i;\n if (l < h.length && this.less(h[l]!, h[m]!)) m = l;\n if (r < h.length && this.less(h[r]!, h[m]!)) m = r;\n if (m === i) break;\n [h[i], h[m]] = [h[m]!, h[i]!];\n i = m;\n }\n }\n return top;\n }\n}\n\n/** The places `xs` crossed with `ys`, nearest to `(fx, fy)` first (Euclidean), without building the cross product. */\nfunction* byDistance(xs: readonly number[], ys: readonly number[], fx: number, fy: number): Generator<{ x: number; y: number; far: number }> {\n const sx = [...xs].sort((a, b) => Math.abs(a - fx) - Math.abs(b - fx) || a - b);\n const sy = [...ys].sort((a, b) => Math.abs(a - fy) - Math.abs(b - fy) || a - b);\n const q = new Nearest();\n const d = (i: number, j: number): number => Math.hypot(sx[i]! - fx, sy[j]! - fy);\n // Each x starts at its nearest y; popping a place pushes the same x's next y.\n if (sy.length > 0) for (let i = 0; i < sx.length; i++) q.push([i, 0, d(i, 0)]);\n while (q.size > 0) {\n const [i, j, far] = q.pop();\n yield { x: sx[i]!, y: sy[j]!, far };\n if (j + 1 < sy.length) q.push([i, j + 1, d(i, j + 1)]);\n }\n}\n\nexport function fitFloat(want: Rect, stage: Rect, avoid: readonly Rect[] = []): FloatFit {\n const area = clean(stage, rect(0, 0, 0, 0));\n const r = clean(want, area);\n const inside = clampInto(r, area);\n // Compared with what was asked, so a position that wasn't a number (and was replaced) counts as moved.\n const clamped = inside.x !== want.x || inside.y !== want.y;\n // Only rects that can touch the float wherever the stage lets it be: its reach.\n const reach = rect(area.x, area.y, Math.max(area.width, r.width), Math.max(area.height, r.height));\n const near: { at: Rect; index: number }[] = [];\n avoid.forEach((a, index) => {\n const at = clean(a, area);\n if (at.width > EPS && at.height > EPS && overlaps(at, reach)) near.push({ at, index });\n });\n const under = (cand: Rect): number[] => near.filter((n) => overlaps(cand, n.at)).map((n) => n.index);\n const blocked = (cand: Rect): boolean => near.some((n) => overlaps(cand, n.at));\n if (!blocked(inside)) return { rect: inside, clamped, snapped: false, covers: [], searchCapped: false };\n\n // The places to look at: every x that puts the float against an edge of the stage or of a rect (or where it is),\n // crossed with every y that does; a clear place exists if one does at all, as a float slid against edges.\n const lo = (v: number, from: number, to: number): number => Math.min(Math.max(v, from), Math.max(from, to));\n const xs = [inside.x, area.x, area.x + area.width - r.width];\n const ys = [inside.y, area.y, area.y + area.height - r.height];\n for (const { at } of near) {\n xs.push(at.x - r.width, at.x + at.width);\n ys.push(at.y - r.height, at.y + at.height);\n }\n const places = (list: number[], from: number, size: number, extent: number): number[] => [...new Set(list.map((v) => lo(v, from, from + extent - size)))];\n const px = places(xs, area.x, r.width, area.width);\n const py = places(ys, area.y, r.height, area.height);\n // Nearest clear place: the x's nearest first; for each, the rects that overlap that x block open stretches of y, and\n // the nearest clear y is the wanted one if it is clear, else the end of the stretch it is in (the stretches are\n // merged, so a whole blocked run is skipped at once). A later x can't beat a clear place at least as far as its x\n // alone, so the search stops there. `WORK` bounds the rect tests as a safety net.\n const ylo = area.y;\n const yhi = Math.max(area.y, area.y + area.height - r.height);\n const byX = [...px].sort((a, b) => Math.abs(a - inside.x) - Math.abs(b - inside.x) || a - b);\n let clear: { x: number; y: number; far: number } | null = null;\n let work = 0;\n let searchCapped = false;\n for (const x of byX) {\n const dx = Math.abs(x - inside.x);\n if (clear && dx >= clear.far) break;\n work += near.length + 1;\n if (work > WORK) {\n searchCapped = true;\n break;\n }\n const stretches: [number, number][] = [];\n for (const { at } of near) {\n if (Math.min(x + r.width, at.x + at.width) - Math.max(x, at.x) > EPS) stretches.push([at.y - r.height, at.y + at.height]);\n }\n stretches.sort((a, b) => a[0] - b[0]);\n // The clear y's are the closed gaps between the merged open stretches, within the stage.\n let cursor = ylo;\n let bestY: number | null = null;\n const gap = (from: number, to: number): void => {\n if (from > to) return;\n const y = Math.min(Math.max(inside.y, from), to);\n if (bestY === null || Math.abs(y - inside.y) < Math.abs(bestY - inside.y)) bestY = y;\n };\n for (const [a, b] of stretches) {\n gap(cursor, Math.min(a, yhi));\n cursor = Math.max(cursor, b);\n if (cursor > yhi) break;\n }\n if (cursor <= yhi) gap(cursor, yhi);\n if (bestY === null) continue;\n const far = Math.hypot(dx, bestY - inside.y);\n if (!clear || far < clear.far) clear = { x, y: bestY, far };\n }\n if (clear) return { rect: rect(clear.x, clear.y, r.width, r.height), clamped, snapped: clear.far > EPS, covers: [], searchCapped };\n // None is clear: the nearest places that cover the least.\n let best: { rect: Rect; left: number; far: number; hits: number[] } | null = null;\n let tried = 0;\n for (const p of byDistance(px, py, inside.x, inside.y)) {\n if (tried++ >= COVERED_PLACES) break;\n const cand = rect(p.x, p.y, r.width, r.height);\n const left = near.reduce((s, n) => s + shared(cand, n.at), 0);\n if (!best || left < best.left - EPS) best = { rect: cand, left, far: p.far, hits: under(cand) };\n }\n const out = best ?? { rect: inside, far: 0, hits: under(inside) };\n return { rect: out.rect, clamped, snapped: out.far > EPS, covers: out.hits, searchCapped };\n}\n\n// --- the drop target ---------------------------------------------------------------------------------------------\n\n/** What a float drop makes: the same answer for the preview and for the release. */\nexport interface FloatDrop {\n /** The float's rect in view space. */\n readonly rect: Rect;\n readonly fit: FloatFit;\n}\n\nexport interface StackFloatTargetInput {\n /** The item dragged. */\n readonly id: string;\n /** The stage, in view space. */\n readonly stage: Rect;\n /** The float's size: it is never resized. */\n readonly size: Size;\n /** The docked content to stay clear of: the docked items' rects (and the » entries'), in view space. */\n readonly avoid?: readonly Rect[];\n /** What the target is called (default \"Float here\"). */\n readonly label?: string;\n}\n\nexport interface FloatTarget extends Target {\n /** What kind of place it is, for a consumer that holds targets of several kinds. */\n readonly kind: \"float\";\n readonly priority: 3;\n readonly free: true;\n /** The item dragged. */\n readonly item: string;\n /**\n * The drop with the pointer at `point` holding the float at `grab` (its offset from the float's top-left corner,\n * default the middle): `rect` is the preview and the landing, clamped into the stage and clear of the docked content.\n */\n make(point: Point, grab?: Point): FloatDrop;\n}\n\n/** A zone that covers any pointer the page can see: large and finite (an infinite rect has no far edge to compare). */\nconst PLANE = rect(-1e9, -1e9, 2e9, 2e9);\n\nexport function stackFloatTarget(input: StackFloatTargetInput): FloatTarget {\n const { stage, size } = input;\n const avoid = input.avoid ?? [];\n const w = Math.max(0, finite(size.width, 0));\n const h = Math.max(0, finite(size.height, 0));\n return {\n // Not \"float\": `./desk` has a target of that id, and a consumer may hold both.\n id: `float:${input.id}`,\n kind: \"float\",\n priority: 3,\n free: true,\n hit: PLANE,\n label: input.label ?? \"Float here\",\n item: input.id,\n make(point, grab) {\n const g = grab ?? { x: w / 2, y: h / 2 };\n const fit = fitFloat(rect(point.x - g.x, point.y - g.y, w, h), stage, avoid);\n return { rect: fit.rect, fit };\n },\n };\n}\n\n// --- the stored form ---------------------------------------------------------------------------------------------\n\nexport interface StoredFloats {\n readonly v: 1;\n readonly floats: { id: string; x: number; y: number; width: number; height: number }[];\n}\n\nexport type FloatsRead = { readonly ok: true; readonly floats: StackFloat[]; readonly dropped: readonly string[] } | { readonly ok: false; readonly error: string };\n\nconst isSize = (n: unknown): n is number => typeof n === \"number\" && Number.isFinite(n) && n > 0;\nconst isAt = (n: unknown): n is number => typeof n === \"number\" && Number.isFinite(n);\n\n/**\n * The floats as plain JSON-safe data, in the order given (the stacking order, the last on top). Never a `pinned`.\n * What it can't read back is left out, silently: a float whose position isn't a finite number or whose size isn't over\n * 0 (a zero-size float), and an earlier entry of a repeated id (the last readable entry of an id is the one on top,\n * so it stays, at its own place in the order).\n */\nexport function storeFloats(floats: readonly StackFloat[]): StoredFloats {\n const last = new Map<string, number>();\n floats.forEach((f, i) => {\n const r = f.rect;\n if (isAt(r.x) && isAt(r.y) && isSize(r.width) && isSize(r.height)) last.set(f.id, i);\n });\n const out: StoredFloats[\"floats\"] = [];\n floats.forEach((f, i) => {\n if (last.get(f.id) === i) out.push({ id: f.id, x: f.rect.x, y: f.rect.y, width: f.rect.width, height: f.rect.height });\n });\n return { v: 1, floats: out };\n}\n\nconst isObject = (x: unknown): x is Record<string, unknown> => typeof x === \"object\" && x !== null && !Array.isArray(x);\nconst FIELDS = [\"id\", \"x\", \"y\", \"width\", \"height\"];\n\n/**\n * Reads parsed data back; never throws. Anything but an object of version 1 with a `floats` array is `{ ok: false,\n * error }`. Inside one it is lenient: a float that isn't an object, has no string id, isn't one the\n * consumer has (with `known`) or has a position or size that isn't a finite number (a size over 0) is dropped and\n * listed in `dropped` as `float:<id>` (or `a <type> where a float goes`); a top-level field this version doesn't\n * know is listed as `\"<field>\"`, and so is a float's own (`float:<id>:<field>`) while the float itself is kept.\n * A `pinned` key (an old store's flag, whatever its value) is ignored: the float loads as a plain float, in view\n * space as stored, and `pinned:<id>` is listed. A repeated id keeps its last readable entry (the list is in stacking\n * order, the last on top); the earlier ones are dropped and listed as `float:<id>`.\n */\nexport function readFloats(data: unknown, known?: readonly string[]): FloatsRead {\n try {\n return read(data, known);\n } catch (e) {\n // A getter that throws, a proxy: still no throw.\n return { ok: false, error: `unreadable: ${e instanceof Error ? e.message : typeof e}` };\n }\n}\n\nfunction read(data: unknown, known?: readonly string[]): FloatsRead {\n if (!isObject(data)) return { ok: false, error: \"not an object\" };\n if (data.v !== 1) return { ok: false, error: `unknown version ${versionWords(data.v)} (this reads version 1)` };\n if (!Array.isArray(data.floats)) return { ok: false, error: \"floats is missing\" };\n const dropped: string[] = [];\n const drop = (what: string): void => {\n if (!dropped.includes(what)) dropped.push(what);\n };\n for (const k of Object.keys(data)) if (k !== \"v\" && k !== \"floats\") drop(k);\n const floats: StackFloat[] = [];\n for (const x of data.floats as unknown[]) {\n if (!isObject(x)) {\n drop(`a ${x === null ? \"null\" : Array.isArray(x) ? \"list\" : typeof x} where a float goes`);\n continue;\n }\n const id = x.id;\n if (typeof id !== \"string\") {\n drop(\"a float with no id\");\n continue;\n }\n if ((known && !known.includes(id)) || !isAt(x.x) || !isAt(x.y) || !isSize(x.width) || !isSize(x.height)) {\n drop(`float:${id}`);\n continue;\n }\n for (const k of Object.keys(x)) {\n if (k === \"pinned\") drop(`pinned:${id}`);\n else if (!FIELDS.includes(k)) drop(`float:${id}:${k}`);\n }\n // A repeated id: the last readable entry is the one on top (the list is in stacking order), the earlier is dropped.\n const before = floats.findIndex((f) => f.id === id);\n if (before >= 0) {\n floats.splice(before, 1);\n drop(`float:${id}`);\n }\n floats.push({ id, rect: rect(x.x, x.y, x.width, x.height) });\n }\n return { ok: true, floats, dropped };\n}\n"],"mappings":"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","names":["finite","cross","EPS","finite","isObject","read"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@n-uf/placement",
3
- "version": "26.11.12",
3
+ "version": "26.11.13",
4
4
  "description": "Where things go: desk docks, rails and palettes, panel columns, toolbar groups, and the drop targets for each. Pure state in, rects out. No DOM, no framework, no motion.",
5
5
  "keywords": [
6
6
  "layout",