@graview/layout 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +96 -0
  2. package/README.md +20 -0
  3. package/dist/band.d.ts +103 -0
  4. package/dist/band.d.ts.map +1 -0
  5. package/dist/band.js +344 -0
  6. package/dist/band.js.map +1 -0
  7. package/dist/city.d.ts +161 -0
  8. package/dist/city.d.ts.map +1 -0
  9. package/dist/city.js +319 -0
  10. package/dist/city.js.map +1 -0
  11. package/dist/ids.d.ts +79 -0
  12. package/dist/ids.d.ts.map +1 -0
  13. package/dist/ids.js +115 -0
  14. package/dist/ids.js.map +1 -0
  15. package/dist/index.d.ts +19 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +10 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/interpolate.d.ts +27 -0
  20. package/dist/interpolate.d.ts.map +1 -0
  21. package/dist/interpolate.js +217 -0
  22. package/dist/interpolate.js.map +1 -0
  23. package/dist/label-fit.d.ts +115 -0
  24. package/dist/label-fit.d.ts.map +1 -0
  25. package/dist/label-fit.js +162 -0
  26. package/dist/label-fit.js.map +1 -0
  27. package/dist/layout.d.ts +99 -0
  28. package/dist/layout.d.ts.map +1 -0
  29. package/dist/layout.js +1391 -0
  30. package/dist/layout.js.map +1 -0
  31. package/dist/rank.d.ts +35 -0
  32. package/dist/rank.d.ts.map +1 -0
  33. package/dist/rank.js +84 -0
  34. package/dist/rank.js.map +1 -0
  35. package/dist/related.d.ts +42 -0
  36. package/dist/related.d.ts.map +1 -0
  37. package/dist/related.js +207 -0
  38. package/dist/related.js.map +1 -0
  39. package/dist/types.d.ts +404 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +27 -0
  42. package/dist/types.js.map +1 -0
  43. package/dist/view-state.d.ts +189 -0
  44. package/dist/view-state.d.ts.map +1 -0
  45. package/dist/view-state.js +350 -0
  46. package/dist/view-state.js.map +1 -0
  47. package/package.json +54 -0
package/dist/ids.js ADDED
@@ -0,0 +1,115 @@
1
+ import { toggleExpanded, withFocus, withOverview, withWithin, withZoom } from "./view-state.js";
2
+ /*
3
+ * WHAT AN ID MEANS in the picture. A node's id is its own; the picture adds
4
+ * ids of its own for the things only it draws — a group (`aggregate:`) and a
5
+ * kind's card on the kinds plane (`kind:`) — and these say how to make one,
6
+ * read one, and go into one.
7
+ */
8
+ export const AGGREGATE_PREFIX = "aggregate:";
9
+ /**
10
+ * The kinds plane's cards have ids of their own.
11
+ *
12
+ * They cannot share `aggregate:<kind>` with a focusable group, because an app
13
+ * whose primary view IS one kind — the coaching example's formation is
14
+ * `aggregate:position` — would then place the same id twice: once as the
15
+ * focus and once as its own card in the strip. The strip is a MAP of kinds,
16
+ * not a set of groups you focus, so it gets its own namespace.
17
+ */
18
+ export const KIND_PREFIX = "kind:";
19
+ export function kindCardId(kind) {
20
+ return `${KIND_PREFIX}${kind}`;
21
+ }
22
+ export function kindOfCard(id) {
23
+ return id.startsWith(KIND_PREFIX) ? id.slice(KIND_PREFIX.length) : null;
24
+ }
25
+ /**
26
+ * The card that stands for the districts the row could not hold.
27
+ *
28
+ * Its own id rather than a kind's, because it is not a kind: it is the row
29
+ * saying what it had to leave out, and naming them.
30
+ */
31
+ export const BEYOND_CARD = "kinds:beyond";
32
+ /** The kinds an id stands for, whichever namespace it is in. */
33
+ export function kindsOf(id) {
34
+ const card = kindOfCard(id);
35
+ return card ? [card] : kindsOfAggregate(id);
36
+ }
37
+ /**
38
+ * The id of a group standing in for one or more kinds.
39
+ *
40
+ * Several kinds because a group is a view of a SET of kinds, not a synonym
41
+ * for one: the household example's week is its blocks and its runs together, and no node
42
+ * kind called "week" exists or should. Kinds are sorted so the same group is
43
+ * always the same id, which keeps it stable in a URL.
44
+ */
45
+ export function aggregateId(...kinds) {
46
+ return `${AGGREGATE_PREFIX}${[...kinds].sort().join("+")}`;
47
+ }
48
+ export function isAggregateId(id) {
49
+ return id.startsWith(AGGREGATE_PREFIX);
50
+ }
51
+ /** The kinds a group id names, or an empty list if it is not a group id. */
52
+ export function kindsOfAggregate(id) {
53
+ // A band's group ("aggregate:song|by|in|…") stands for SOME members, not a kind.
54
+ if (!isAggregateId(id) || id.includes("|"))
55
+ return [];
56
+ return id.slice(AGGREGATE_PREFIX.length).split("+").filter(Boolean);
57
+ }
58
+ /**
59
+ * Going deeper into a card, as view state.
60
+ *
61
+ * A record ZOOMS: the same scene with the record grown to most of it, and
62
+ * the same gesture on the zoomed record zooms back out. A KIND CARD stands
63
+ * for a group, so deeper means the group: on the ground it zooms into the
64
+ * group as a place (`aggregate:<kind>`), and from altitude it opens the
65
+ * district in place — the exploded view — and the same gesture closes it.
66
+ *
67
+ * UNLESS THE KIND HAS A PICTURE OF ITS OWN. A district explodes into a ring
68
+ * of chips because a bag of names is the best a generic card can do with its
69
+ * members. A kind with a lens registered over it has something better, and
70
+ * the card already says so with its ◆ — so "deeper" there means that
71
+ * picture, and the district stays shut. Exploding it replaced a designed
72
+ * view with the fallback it exists to improve on.
73
+ *
74
+ * The card's own id (`kind:<kind>`) is never made the focus. A focus
75
+ * resolves to a node or to a group's kinds, and a kind card is neither: a
76
+ * focus on one laid out an empty scene with the card's name in the URL,
77
+ * which is what double-clicking a group looked like before this existed.
78
+ */
79
+ export function withJackIn(state, id, options = {}) {
80
+ const kind = kindOfCard(id);
81
+ if (kind !== null && state.overview && options.ownPicture !== true) {
82
+ return toggleExpanded(state, id);
83
+ }
84
+ const target = kind !== null ? aggregateId(kind) : id;
85
+ /*
86
+ * Carrying words is never the way back out. Standing zoomed in the very
87
+ * district the search lit, its count or its row narrows it where you are;
88
+ * only a bare jack-in on the zoomed district is the toggle out.
89
+ */
90
+ if (state.zoom && state.focusId === target && !(kind !== null && options.carry))
91
+ return withZoom(state, false);
92
+ if (kind !== null && options.carry) {
93
+ /*
94
+ * A SEARCH'S DESCENT COMES DOWN. From altitude every village still
95
+ * stands, every member a building, and a district "opened narrowed"
96
+ * up there reads as not narrowed at all. The words took you to it, so
97
+ * it opens on the ground, close, with only what they found.
98
+ */
99
+ const down = withOverview({ ...withFocus(state, target), relation: null }, false);
100
+ return withWithin(withZoom(down, true), "q", options.carry);
101
+ }
102
+ return withZoom({ ...withFocus(state, target), relation: null }, true);
103
+ }
104
+ /**
105
+ * Ranks by a STABLE key — the node id — and never by a mutable count.
106
+ *
107
+ * This is the single rule that protects spatial memory. Ordering people by
108
+ * "how many runs they have" would reshuffle the whole plane the moment
109
+ * anything is reassigned, and the picture you remember would stop being the
110
+ * picture you get.
111
+ */
112
+ export function byStableKey(a, b) {
113
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
114
+ }
115
+ //# sourceMappingURL=ids.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ids.js","sourceRoot":"","sources":["../src/ids.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ export { aggregateId, isAggregateId, kindCardId, kindOfCard, kindsOf, kindsOfAggregate, KIND_PREFIX, withJackIn, AGGREGATE_PREFIX, } from "./ids.js";
2
+ export { holdLayout, layout, marqueeHeightFor, panLayout, planeOf, ROSTER_MOST, ROSTER_ROW, rosterHeight, rosterRows, SCREEN_LEASH_CELLS } from "./layout.js";
3
+ export { easeInOut, interpolate } from "./interpolate.js";
4
+ export { rankKinds } from "./rank.js";
5
+ export type { KindRank, KindRanking } from "./rank.js";
6
+ export type { InterpolatedLayout, InterpolatedNode } from "./interpolate.js";
7
+ export { DEFAULT_OPTIONS } from "./types.js";
8
+ export type { Aggregate, Connector, Layout, LayoutNode, LayoutOptions, Plane, Via, } from "./types.js";
9
+ export { EDGE_SELECTION_PREFIX, edgeOfSelection, edgeSelectionId, EMPTY_VIEW, fromUrl, sameView, toggleExpanded, toUrl, withFocus, withOverview, withPan, withPast, withQuery, withoutSearch, withShown, withPin, withoutMoves, withRelation, withWithin, withSelection, withZoom, } from "./view-state.js";
10
+ export type { EdgeRef, Pin, ViewState } from "./view-state.js";
11
+ export { areaOf, boxOf, centroidOf, estimateWidth, fitLabel, overlaps, spanAt } from "./label-fit.js";
12
+ export type { FitOptions, FitPoint, FittedLabel, LabelBox, Measure } from "./label-fit.js";
13
+ export { cameraLimit, collides, districtsPastTheEdge, panForZoom, placeCity } from "./city.js";
14
+ export type { CityCard, PastTheEdge, PlacedCard } from "./city.js";
15
+ export type { CityFrame } from "./types.js";
16
+ export { bandAggregateWords, bandCaps, bandOf, chooseGrouping, isBandAggregate, packRuns, runOf, shares } from "./band.js";
17
+ export type { BandItem, BandOptions, Relevance } from "./band.js";
18
+ export type { Opens } from "./types.js";
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,aAAa,EACb,UAAU,EACV,UAAU,EACV,OAAO,EACP,gBAAgB,EAChB,WAAW,EACX,UAAU,EACV,gBAAgB,GACjB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAC9J,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACvD,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,YAAY,EACV,SAAS,EACT,SAAS,EACT,MAAM,EACN,UAAU,EACV,aAAa,EACb,KAAK,EACL,GAAG,GACJ,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,eAAe,EACf,UAAU,EACV,OAAO,EACP,QAAQ,EACR,cAAc,EACd,KAAK,EACL,SAAS,EACT,YAAY,EACZ,OAAO,EACP,QAAQ,EACR,SAAS,EACT,aAAa,EACb,SAAS,EACT,OAAO,EACP,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,aAAa,EACb,QAAQ,GACT,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACtG,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAC3F,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,oBAAoB,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC/F,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACnE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,eAAe,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC3H,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAClE,YAAY,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { aggregateId, isAggregateId, kindCardId, kindOfCard, kindsOf, kindsOfAggregate, KIND_PREFIX, withJackIn, AGGREGATE_PREFIX, } from "./ids.js";
2
+ export { holdLayout, layout, marqueeHeightFor, panLayout, planeOf, ROSTER_MOST, ROSTER_ROW, rosterHeight, rosterRows, SCREEN_LEASH_CELLS } from "./layout.js";
3
+ export { easeInOut, interpolate } from "./interpolate.js";
4
+ export { rankKinds } from "./rank.js";
5
+ export { DEFAULT_OPTIONS } from "./types.js";
6
+ export { EDGE_SELECTION_PREFIX, edgeOfSelection, edgeSelectionId, EMPTY_VIEW, fromUrl, sameView, toggleExpanded, toUrl, withFocus, withOverview, withPan, withPast, withQuery, withoutSearch, withShown, withPin, withoutMoves, withRelation, withWithin, withSelection, withZoom, } from "./view-state.js";
7
+ export { areaOf, boxOf, centroidOf, estimateWidth, fitLabel, overlaps, spanAt } from "./label-fit.js";
8
+ export { cameraLimit, collides, districtsPastTheEdge, panForZoom, placeCity } from "./city.js";
9
+ export { bandAggregateWords, bandCaps, bandOf, chooseGrouping, isBandAggregate, packRuns, runOf, shares } from "./band.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,aAAa,EACb,UAAU,EACV,UAAU,EACV,OAAO,EACP,gBAAgB,EAChB,WAAW,EACX,UAAU,EACV,gBAAgB,GACjB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAC9J,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAGtC,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAU7C,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,eAAe,EACf,UAAU,EACV,OAAO,EACP,QAAQ,EACR,cAAc,EACd,KAAK,EACL,SAAS,EACT,YAAY,EACZ,OAAO,EACP,QAAQ,EACR,SAAS,EACT,aAAa,EACb,SAAS,EACT,OAAO,EACP,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,aAAa,EACb,QAAQ,GACT,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAEtG,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,oBAAoB,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAG/F,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,eAAe,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC"}
@@ -0,0 +1,27 @@
1
+ import type { CityFrame, Connector, Layout, LayoutNode } from "./types.js";
2
+ export interface InterpolatedNode extends Omit<LayoutNode, "plane"> {
3
+ /** Fractional plane, so the renderer can mix two plane styles. */
4
+ readonly plane: number;
5
+ /** 0 while fully absent, 1 while fully present. */
6
+ readonly opacity: number;
7
+ }
8
+ export interface InterpolatedLayout {
9
+ readonly nodes: readonly InterpolatedNode[];
10
+ readonly connectors: readonly (Connector & {
11
+ opacity: number;
12
+ })[];
13
+ readonly width: number;
14
+ readonly height: number;
15
+ readonly t: number;
16
+ /** The destination's lattice, when it has one. */
17
+ readonly city?: CityFrame;
18
+ }
19
+ /**
20
+ * Interpolates two layouts. Because layout is a pure function of the view,
21
+ * ANY two states can be tweened — there is no special-cased transition list,
22
+ * and a new kind of navigation animates for free.
23
+ */
24
+ export declare function interpolate(from: Layout, to: Layout, t: number): InterpolatedLayout;
25
+ /** Ease-in-out, so a transition reads as motion rather than a slide. */
26
+ export declare function easeInOut(t: number): number;
27
+ //# sourceMappingURL=interpolate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interpolate.d.ts","sourceRoot":"","sources":["../src/interpolate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE3E,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC;IACjE,kEAAkE;IAClE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,mDAAmD;IACnD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC5C,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC,SAAS,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,CAAC;IAClE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC;CAC3B;AAkGD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAwDnF;AAyED,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAK3C"}
@@ -0,0 +1,217 @@
1
+ import { aggregateId, isAggregateId, kindCardId, kindOfCard } from "./ids.js";
2
+ const lerp = (a, b, t) => a + (b - a) * t;
3
+ function centre(node) {
4
+ return { x: node.x + node.width / 2, y: node.y + node.height / 2 };
5
+ }
6
+ const indexed = new WeakMap();
7
+ function indexOf(layout) {
8
+ let index = indexed.get(layout);
9
+ if (index)
10
+ return index;
11
+ const byId = new Map();
12
+ const groupsOf = new Map();
13
+ for (const node of layout.nodes) {
14
+ byId.set(node.id, node);
15
+ for (const member of node.aggregate?.memberIds ?? []) {
16
+ const groups = groupsOf.get(member);
17
+ if (groups)
18
+ groups.push(node);
19
+ else
20
+ groupsOf.set(member, [node]);
21
+ }
22
+ }
23
+ index = { byId, groupsOf };
24
+ indexed.set(layout, index);
25
+ return index;
26
+ }
27
+ /*
28
+ * The same node and the same far layout give the same stand-in, and a tween
29
+ * asks sixty times a second: a group of a thousand songs was a thousand
30
+ * lookups and a centroid per frame. Answered once per pair.
31
+ */
32
+ const stoodIn = new WeakMap();
33
+ function standIn(node, other) {
34
+ let known = stoodIn.get(other);
35
+ if (!known)
36
+ stoodIn.set(other, (known = new WeakMap()));
37
+ if (known.has(node))
38
+ return known.get(node);
39
+ const answer = standInFresh(node, other);
40
+ known.set(node, answer);
41
+ return answer;
42
+ }
43
+ function standInFresh(node, other) {
44
+ const { byId, groupsOf } = indexOf(other);
45
+ /*
46
+ * A BILLBOARD SINKS INTO ITS VILLAGE, and rises out of it. The picture on
47
+ * a kind's plot is an aggregate node, and an aggregate's stand-in is the
48
+ * centroid of its members — but from altitude the members are buildings
49
+ * on the ground, not nodes, so a billboard leaving had nowhere to go and
50
+ * faded where it stood, at full size, under the billboard replacing it:
51
+ * two pictures on top of each other for the length of the tween, and
52
+ * the old one read as left up. Its kind's card is where it goes — the
53
+ * signpost and board at the front of the village, where its own small
54
+ * picture is — and where the next one comes from.
55
+ */
56
+ if (node.screenOf !== undefined) {
57
+ const card = byId.get(kindCardId(node.screenOf));
58
+ if (card)
59
+ return { ...node, x: card.x, y: card.y, width: card.width, height: card.height };
60
+ }
61
+ if (isAggregateId(node.id) || kindOfCard(node.id) !== null) {
62
+ // A group vanishing: its members are the thing it becomes. Collapse to
63
+ // the centroid of wherever they went.
64
+ const members = (node.aggregate?.memberIds ?? [])
65
+ .map((id) => byId.get(id))
66
+ .filter((n) => n !== undefined);
67
+ if (members.length === 0)
68
+ return null;
69
+ const points = members.map(centre);
70
+ const x = points.reduce((sum, p) => sum + p.x, 0) / points.length;
71
+ const y = points.reduce((sum, p) => sum + p.y, 0) / points.length;
72
+ return { ...node, x: x - node.width / 2, y: y - node.height / 2 };
73
+ }
74
+ const group = groupsOf
75
+ .get(node.id)
76
+ ?.find((candidate) => candidate.id === kindCardId(node.kind) || candidate.id === aggregateId(node.kind));
77
+ if (!group)
78
+ return null;
79
+ const point = centre(group);
80
+ return { ...node, x: point.x - node.width / 2, y: point.y - node.height / 2 };
81
+ }
82
+ /**
83
+ * Interpolates two layouts. Because layout is a pure function of the view,
84
+ * ANY two states can be tweened — there is no special-cased transition list,
85
+ * and a new kind of navigation animates for free.
86
+ */
87
+ export function interpolate(from, to, t) {
88
+ const clamped = Math.max(0, Math.min(1, t));
89
+ const fromById = new Map(from.nodes.map((node) => [node.id, node]));
90
+ const toById = new Map(to.nodes.map((node) => [node.id, node]));
91
+ const nodes = [];
92
+ for (const [id, start] of fromById) {
93
+ const end = toById.get(id);
94
+ if (end) {
95
+ nodes.push(mix(start, end, clamped, 1));
96
+ continue;
97
+ }
98
+ // Leaving: shrink into whatever now stands in for it, and fade.
99
+ const target = standIn(start, to) ?? start;
100
+ nodes.push(mix(start, target, clamped, 1 - clamped));
101
+ }
102
+ for (const [id, end] of toById) {
103
+ if (fromById.has(id))
104
+ continue;
105
+ // Entering: grow out of whatever stood in for it, and fade in.
106
+ const source = standIn(end, from) ?? end;
107
+ nodes.push(mix(source, end, clamped, clamped));
108
+ }
109
+ const fromConnectors = new Map(from.connectors.map((c) => [c.id, c]));
110
+ const toConnectors = new Map(to.connectors.map((c) => [c.id, c]));
111
+ const connectors = [];
112
+ for (const [id, start] of fromConnectors) {
113
+ const end = toConnectors.get(id);
114
+ connectors.push(end
115
+ ? { ...mixConnector(start, end, clamped), opacity: 1 }
116
+ : { ...start, opacity: 1 - clamped });
117
+ }
118
+ for (const [id, end] of toConnectors) {
119
+ if (fromConnectors.has(id))
120
+ continue;
121
+ connectors.push({ ...end, opacity: clamped });
122
+ }
123
+ return {
124
+ // A node that has finished fading out is gone. Leaving it in the list at
125
+ // opacity 0 means the renderer keeps a texture, a placement and a draw
126
+ // call for something nobody can see — and it never leaves, because no
127
+ // further frames are emitted once the tween settles.
128
+ nodes: nodes
129
+ .filter((node) => node.opacity > 0)
130
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)),
131
+ connectors: connectors
132
+ .filter((connector) => connector.opacity > 0)
133
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)),
134
+ width: lerp(from.width, to.width, clamped),
135
+ height: lerp(from.height, to.height, clamped),
136
+ t: clamped,
137
+ ...(mixCity(from.city, to.city, clamped) ? { city: mixCity(from.city, to.city, clamped) } : {}),
138
+ };
139
+ }
140
+ /**
141
+ * THE GROUND MOVES WITH THE CITY. Between two cities — the camera flying
142
+ * closer, a district pinned elsewhere — the cell and the origin tween, so
143
+ * the lattice, the plots and the roads slide and grow with the cards on
144
+ * them rather than snapping to the destination while the cards are still
145
+ * on their way. Rising, there is no city to start from, so the lattice
146
+ * arrives with the destination and the ground crossfades to it by the
147
+ * altitude number; a half-scaled lattice would be no grid. Descending, the
148
+ * city stays as it was while the ground fades out under the landing
149
+ * picture, instead of vanishing on the first frame of the way down.
150
+ */
151
+ function mixCity(from, to, t) {
152
+ if (!from)
153
+ return to;
154
+ if (!to)
155
+ return from;
156
+ return {
157
+ cell: lerp(from.cell, to.cell, t),
158
+ originX: lerp(from.originX, to.originX, t),
159
+ originY: lerp(from.originY, to.originY, t),
160
+ pan: { x: lerp(from.pan.x, to.pan.x, t), y: lerp(from.pan.y, to.pan.y, t) },
161
+ extent: {
162
+ x: lerp(from.extent.x, to.extent.x, t),
163
+ y: lerp(from.extent.y, to.extent.y, t),
164
+ width: lerp(from.extent.width, to.extent.width, t),
165
+ height: lerp(from.extent.height, to.extent.height, t),
166
+ },
167
+ };
168
+ }
169
+ function mix(a, b, t, opacity) {
170
+ return {
171
+ id: b.id,
172
+ kind: b.kind,
173
+ plane: lerp(a.plane, b.plane, t),
174
+ x: lerp(a.x, b.x, t),
175
+ y: lerp(a.y, b.y, t),
176
+ width: lerp(a.width, b.width, t),
177
+ height: lerp(a.height, b.height, t),
178
+ pinned: b.pinned,
179
+ opacity,
180
+ ...(b.aggregate ? { aggregate: b.aggregate } : {}),
181
+ ...(b.via ? { via: b.via } : {}),
182
+ ...(b.raised ? { raised: true } : {}),
183
+ ...(b.focused ? { focused: true } : {}),
184
+ ...(b.opened ? { opened: true } : {}),
185
+ ...(b.openedRows !== undefined ? { openedRows: b.openedRows } : {}),
186
+ ...(b.compact ? { compact: true } : {}),
187
+ ...(b.rank ? { rank: b.rank } : {}),
188
+ ...(b.nestedUnder ? { nestedUnder: b.nestedUnder } : {}),
189
+ /* What a card STANDS FOR travels with it, or the row's overflow card
190
+ arrives mid-transition as an empty box with no kind and nothing in it. */
191
+ ...(b.beyond ? { beyond: b.beyond } : {}),
192
+ /* An address is not a position: it does not tween, it travels. Dropped
193
+ here, a district mid-flight to altitude has no plot, and the ground
194
+ under it and the roads to it have nothing to stand on. */
195
+ ...(b.plot ? { plot: b.plot } : {}),
196
+ ...(b.screenOf ? { screenOf: b.screenOf } : {}),
197
+ ...(b.depth === undefined ? {} : { depth: lerp(a.depth ?? 1, b.depth, t) }),
198
+ ...(b.natural ? { natural: b.natural } : {}),
199
+ };
200
+ }
201
+ function mixConnector(a, b, t) {
202
+ return {
203
+ ...b,
204
+ x1: lerp(a.x1, b.x1, t),
205
+ y1: lerp(a.y1, b.y1, t),
206
+ x2: lerp(a.x2, b.x2, t),
207
+ y2: lerp(a.y2, b.y2, t),
208
+ };
209
+ }
210
+ /** Ease-in-out, so a transition reads as motion rather than a slide. */
211
+ export function easeInOut(t) {
212
+ const clamped = Math.max(0, Math.min(1, t));
213
+ return clamped < 0.5
214
+ ? 4 * clamped * clamped * clamped
215
+ : 1 - (-2 * clamped + 2) ** 3 / 2;
216
+ }
217
+ //# sourceMappingURL=interpolate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interpolate.js","sourceRoot":"","sources":["../src/interpolate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * A NAME THAT FITS THE THING IT NAMES.
3
+ *
4
+ * Every drawn surface in this framework eventually has to write a word
5
+ * inside a shape — a district, a region on a plan, a band on a calendar —
6
+ * and every one of them starts by centring the text at one size and hoping.
7
+ * That works for as long as the names are short, which is for as long as a
8
+ * person is typing them.
9
+ *
10
+ * A product asked a model to survey a garden and got seven areas back
11
+ * called things like "Pea-gravel corner with river-rock border, log seats
12
+ * and a fire bowl". The map drew each at one size across the middle of its
13
+ * shape: three ran off the canvas, two were struck through by a
14
+ * neighbour's outline, and nothing failed — every test passed, the
15
+ * accessibility tree was perfect, and the picture said "rick patio with
16
+ * gravel joints". Agent-authored labels are not an edge case; they are the
17
+ * normal case for anything with an intelligence provider in it.
18
+ *
19
+ * So this is the arithmetic, apart from any drawing, where it can be
20
+ * tested: how much room a shape actually has, and what of a name will fit
21
+ * in it. Nothing here knows what a zone is.
22
+ */
23
+ export interface FitPoint {
24
+ readonly x: number;
25
+ readonly y: number;
26
+ }
27
+ /**
28
+ * How wide a shape is AT A HEIGHT — not its bounding box.
29
+ *
30
+ * The shapes that overflow worst are the long thin ones, whose box is
31
+ * generous exactly where the shape is not. So this is a scanline: cross the
32
+ * polygon with a horizontal line and take the widest run that is genuinely
33
+ * inside it. For an L, a crescent or an hourglass waist that is much less
34
+ * than the box, which is the point.
35
+ */
36
+ export declare function spanAt(outline: readonly FitPoint[], y: number): {
37
+ readonly x0: number;
38
+ readonly x1: number;
39
+ };
40
+ /** The corner-to-corner extent, for the vertical room a label has. */
41
+ export declare function boxOf(outline: readonly FitPoint[]): {
42
+ readonly top: number;
43
+ readonly bottom: number;
44
+ };
45
+ /** How much a shape covers, by the shoelace. For ordering, not for display. */
46
+ export declare function areaOf(outline: readonly FitPoint[]): number;
47
+ /**
48
+ * The area-weighted centroid, which is NOT the mean of the corners.
49
+ *
50
+ * An L-shaped region with six corners has four of them bunched at one end,
51
+ * and averaging drags the label off the shape entirely — onto the
52
+ * neighbour, where it reads as that neighbour's name. The fallback keeps a
53
+ * degenerate shape from producing a NaN that silently removes the label.
54
+ */
55
+ export declare function centroidOf(outline: readonly FitPoint[]): FitPoint;
56
+ /**
57
+ * HOW WIDE A STRING WILL BE.
58
+ *
59
+ * Injected, because the honest answer depends on where you are. A browser
60
+ * has a 2D canvas context that will measure text synchronously with the same
61
+ * engine that will draw it; a test and a server have nothing, and have to
62
+ * estimate. The estimate is what this defaults to and it is deliberately
63
+ * PESSIMISTIC: the failure it guards is overflow, and being a little small
64
+ * is not a failure at all.
65
+ *
66
+ * The first version only estimated, at 0.52 of the size per character, and
67
+ * that was wrong by enough to matter — a display serif runs from 0.28 for an
68
+ * "i" to 0.86 for a "W", so a name full of wide letters measured short and
69
+ * was drawn running off the picture. Which is the exact fault the fitting
70
+ * exists to prevent, arrived at from the other side.
71
+ */
72
+ export type Measure = (text: string, fontSize: number) => number;
73
+ export declare const estimateWidth: Measure;
74
+ export interface FittedLabel {
75
+ readonly lines: readonly string[];
76
+ readonly fontSize: number;
77
+ /** True when the whole name is on the drawing, so nothing has to say it again. */
78
+ readonly whole: boolean;
79
+ }
80
+ export interface FitOptions {
81
+ /** The widest a line may be. */
82
+ readonly room: number;
83
+ /** The tallest the whole label may be. */
84
+ readonly height: number;
85
+ /** The size to draw at when there is room. */
86
+ readonly size: number;
87
+ /** The size below which nothing is legible anyway. */
88
+ readonly floor: number;
89
+ readonly measure?: Measure;
90
+ /** How many lines to break onto. One or two; more is a paragraph. */
91
+ readonly lines?: 1 | 2;
92
+ }
93
+ /**
94
+ * A name that fits, or no name at all.
95
+ *
96
+ * In order: the whole thing on one line; the whole thing broken at the space
97
+ * nearest the middle, which is what a person lettering a plan does; the same
98
+ * shrunk, down to the floor; and then cut, with an ellipsis, so the shape is
99
+ * at least identified.
100
+ *
101
+ * A shape too small for four characters gets NOTHING. A two-letter stub over
102
+ * a flower bed is worse than a clean shape, because it reads as somebody
103
+ * else's label clipped — and `whole: false` is how the caller knows to say
104
+ * the name somewhere a person can still read it.
105
+ */
106
+ export declare function fitLabel(text: string, options: FitOptions): FittedLabel | null;
107
+ export interface LabelBox {
108
+ readonly x0: number;
109
+ readonly y0: number;
110
+ readonly x1: number;
111
+ readonly y1: number;
112
+ }
113
+ /** Whether two boxes touch. The whole of collision avoidance, in one line. */
114
+ export declare const overlaps: (a: LabelBox, b: LabelBox) => boolean;
115
+ //# sourceMappingURL=label-fit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"label-fit.d.ts","sourceRoot":"","sources":["../src/label-fit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAmB5G;AAED,sEAAsE;AACtE,wBAAgB,KAAK,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,GAAG;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAQrG;AAED,+EAA+E;AAC/E,wBAAgB,MAAM,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,GAAG,MAAM,CAQ3D;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,GAAG,QAAQ,CAiBjE;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;AAGjE,eAAO,MAAM,aAAa,EAAE,OAA8D,CAAC;AAE3F,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,kFAAkF;IAClF,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,gCAAgC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0CAA0C;IAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,qEAAqE;IACrE,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;CACxB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,WAAW,GAAG,IAAI,CAyC9E;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,8EAA8E;AAC9E,eAAO,MAAM,QAAQ,GAAI,GAAG,QAAQ,EAAE,GAAG,QAAQ,KAAG,OACM,CAAC"}