@mark1russell7/highways 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "directory": "packages/core"
10
10
  },
11
11
  "homepage": "https://mark1russell7.github.io/highways/",
12
- "version": "0.1.4",
12
+ "version": "0.2.0",
13
13
  "type": "module",
14
14
  "main": "./build/index.js",
15
15
  "types": "./build/index.d.ts",
@@ -1,17 +1,22 @@
1
- import { curveOf, CURVE_TYPES, curveSpace, sampleCurve, type CurveType } from "../curves/index.ts";
1
+ import { CURVE_TYPES, curveSpace, sampleCurve, type CurveType } from "../curves/index.ts";
2
2
  import { polylineLength } from "../geom.ts";
3
3
  import { nodeIndex, type Graph, type Router } from "../model.ts";
4
4
  import { nearestSide, SIDE_NORMAL } from "../plugs.ts";
5
+ import { curveStrategies } from "../strategies/builtins.ts";
6
+ import type { Strategy } from "../strategies/model.ts";
5
7
  import { measureLinks, type MeasuredLink } from "./links.ts";
6
8
 
7
9
  // The core runs in any host, so it reads the clock without the types of Node or the browser.
8
10
  const now = (): number => (globalThis as { performance?: { now(): number } }).performance?.now() ?? Date.now();
9
11
 
10
- /** One row of the benchmark of the curves: one curve type on the routes of one router on one fixture. */
11
- export interface CurveBenchRow {
12
+ /**
13
+ * One row of the benchmark of the curves: one curve type on the routes of one router on one fixture.
14
+ * The type `T` of the name of the curve is `CurveType` for `benchCurves`, and `string` for `benchCurveStrategies`.
15
+ */
16
+ export interface CurveBenchRow<T extends string = CurveType> {
12
17
  readonly router: string;
13
18
  readonly fixture: string;
14
- readonly type: CurveType;
19
+ readonly type: T;
15
20
  /** The number of routes. */
16
21
  readonly links: number;
17
22
  /** The number of polylines that go through a box that is not one of their ends (`measureLinks`). */
@@ -40,6 +45,27 @@ export interface CurveBenchOptions {
40
45
  * The type `spline` leaves each plug along the normal of the nearest side of its node.
41
46
  */
42
47
  export function benchCurves(router: Router, fixture: string, graph: Graph, options: CurveBenchOptions = {}): CurveBenchRow[] {
48
+ // Each built-in curve strategy has the name of its curve type, so each row has a `CurveType`.
49
+ return benchCurveStrategies(
50
+ router,
51
+ fixture,
52
+ graph,
53
+ CURVE_TYPES.map((t) => curveStrategies[t]),
54
+ options,
55
+ ) as CurveBenchRow[];
56
+ }
57
+
58
+ /**
59
+ * This function does the work of `benchCurves` with a list of curve strategies, in place of the curve types.
60
+ * The option `radius` goes to each strategy. A strategy without such an option does not read it.
61
+ */
62
+ export function benchCurveStrategies(
63
+ router: Router,
64
+ fixture: string,
65
+ graph: Graph,
66
+ curves: readonly Strategy<"curve">[],
67
+ options: CurveBenchOptions = {},
68
+ ): CurveBenchRow<string>[] {
43
69
  const tolerance = options.tolerance ?? 0.25;
44
70
  const result = router.route(graph);
45
71
  const byId = nodeIndex(graph);
@@ -60,13 +86,14 @@ export function benchCurves(router: Router, fixture: string, graph: Graph, optio
60
86
  });
61
87
  const base = measureLinks(boxes, routes);
62
88
  const ink = routes.reduce((sum, r) => sum + polylineLength(r.points), 0);
63
- return CURVE_TYPES.map((type): CurveBenchRow => {
89
+ const radius = { radius: options.radius ?? 12 };
90
+ return curves.map((curve): CurveBenchRow<string> => {
64
91
  const make = (withSpace: boolean): MeasuredLink[] =>
65
92
  routes.map((r) => ({
66
93
  source: r.source,
67
94
  target: r.target,
68
95
  points: sampleCurve(
69
- curveOf(r.points, { type, radius: options.radius ?? 12, startNormal: r.startNormal, endNormal: r.endNormal, ...(withSpace ? { space } : {}) }),
96
+ curve.run({ points: r.points, startNormal: r.startNormal, endNormal: r.endNormal, ...(withSpace ? { space } : {}) }, radius),
70
97
  tolerance,
71
98
  ),
72
99
  }));
@@ -78,7 +105,7 @@ export function benchCurves(router: Router, fixture: string, graph: Graph, optio
78
105
  return {
79
106
  router: router.name,
80
107
  fixture,
81
- type,
108
+ type: curve.name,
82
109
  links: routes.length,
83
110
  through: base.through,
84
111
  free: free.through,
@@ -90,7 +117,7 @@ export function benchCurves(router: Router, fixture: string, graph: Graph, optio
90
117
  }
91
118
 
92
119
  /** This function writes the rows of `benchCurves` as a Markdown table. */
93
- export function formatCurves(rows: readonly CurveBenchRow[]): string {
120
+ export function formatCurves(rows: readonly CurveBenchRow<string>[]): string {
94
121
  const head = "| router | fixture | curve | routes | through, polyline | through, free | through, clamped | length | ms |";
95
122
  const line = "|---|---|---|---:|---:|---:|---:|---:|---:|";
96
123
  const body = rows.map((r) => `| ${r.router} | ${r.fixture} | ${r.type} | ${r.links} | ${r.through} | ${r.free} | ${r.clamped} | ${r.length.toFixed(3)} | ${r.ms.toFixed(1)} |`);
package/src/bench/room.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import { levelLinks } from "../levels.ts";
2
2
  import type { Graph, Node, Result } from "../model.ts";
3
- import { makeRoom, roomLinks, type RoomOptions } from "../room/index.ts";
3
+ import { makeRoom, openGaps, roomLinks, type RoomOptions } from "../room/index.ts";
4
+ import { gapLanes, LANE_ORDERS, type LaneOrder } from "../room/lanes.ts";
4
5
  import { sectionsRouter } from "../routers/sections.ts";
5
6
  import { laneRoutes, runKey, segmentsOf } from "../segments.ts";
7
+ import { roomStrategies } from "../strategies/builtins.ts";
8
+ import type { Strategy } from "../strategies/model.ts";
6
9
  import { measureLinks, measureRouteFaults, type LinkFaults } from "./links.ts";
7
10
  import { measureQuality } from "./quality.ts";
8
11
 
@@ -50,15 +53,18 @@ function areaOf(nodes: readonly Node[]): number {
50
53
 
51
54
  /**
52
55
  * This function measures the links of one board. It measures the links of the far view, with each node of the graph as one item of a level.
53
- * It also measures the lane routes of the router `sections` at the lane width `spacing`.
56
+ * With a lane order, each link of the far view gets its own lane in each gap (`gapLanes` at the lane width `spacing`).
57
+ * Without it, the links of a gap share the lines of the level links.
58
+ * The function also measures the lane routes of the router `sections` at the lane width `spacing`.
54
59
  */
55
- export function measureBoard(graph: Graph, spacing: number): Omit<RoomBenchRow, "fixture" | "board" | "area" | "ms"> {
60
+ export function measureBoard(graph: Graph, spacing: number, order?: LaneOrder): Omit<RoomBenchRow, "fixture" | "board" | "area" | "ms"> {
56
61
  const links = roomLinks(graph);
57
62
  const items = graph.nodes.map((n) => ({ id: n.id, box: n.box, nodes: [n.id] }));
58
- const drawn = levelLinks(
63
+ const level = levelLinks(
59
64
  items,
60
65
  links.map((l) => ({ source: l.source, target: l.target, count: l.count ?? 1 })),
61
66
  );
67
+ const drawn = order ? gapLanes(graph.nodes, level, { spacing, order }) : level;
62
68
  const boxes = new Map(graph.nodes.map((n) => [n.id, n.box]));
63
69
  // `measureQuality` reads routes of edges, so each link of the far view stands for the first edge of its pair.
64
70
  const edgeOf = new Map<string, string>();
@@ -82,10 +88,15 @@ export function measureBoard(graph: Graph, spacing: number): Omit<RoomBenchRow,
82
88
  }
83
89
 
84
90
  /**
85
- * This function measures a packed board, the board with each gap open to the gutter only, and the board that `makeRoom` opens with each method.
86
- * The options of `makeRoom` are the same for each method, except `method`.
91
+ * This function measures a packed board, the board with each gap open to the gutter only, and the board that each room strategy opens.
92
+ * The options are the same for each strategy, except `method`. The default strategies are the methods `routes` and `lines` of `roomDemand`.
87
93
  */
88
- export function benchRoom(fixture: string, graph: Graph, options: RoomOptions = {}): RoomBenchRow[] {
94
+ export function benchRoom(
95
+ fixture: string,
96
+ graph: Graph,
97
+ options: RoomOptions = {},
98
+ methods: readonly Strategy<"room">[] = [roomStrategies.routes, roomStrategies.lines],
99
+ ): RoomBenchRow[] {
89
100
  const spacing = options.spacing ?? 4;
90
101
  const area0 = areaOf(graph.nodes);
91
102
  const links = roomLinks(graph);
@@ -97,19 +108,76 @@ export function benchRoom(fixture: string, graph: Graph, options: RoomOptions =
97
108
  ...measureBoard({ nodes, edges: graph.edges }, spacing),
98
109
  });
99
110
  const rows = [row("packed", graph.nodes, 0)];
100
- const states: [string, RoomOptions][] = [
101
- ["gutter", { ...options, spacing: 0, method: "routes" }],
102
- ["routes", { ...options, method: "routes" }],
103
- ["lines", { ...options, method: "lines" }],
104
- ];
105
- for (const [board, o] of states) {
111
+ const t0 = now();
112
+ const gutter = makeRoom(graph.nodes, links, { ...options, spacing: 0, method: "routes" });
113
+ rows.push(row("gutter", gutter.items, now() - t0));
114
+ const { method: _method, ...preset } = options;
115
+ for (const s of methods) {
116
+ const t1 = now();
117
+ const gaps = s.run({ items: graph.nodes, links }, preset);
118
+ const nodes = openGaps(graph.nodes, gaps, options.snap);
119
+ rows.push(row(s.name, nodes, now() - t1));
120
+ }
121
+ return rows;
122
+ }
123
+
124
+ /** One setting of a sweep of the room: the options of `makeRoom` and the lane order of the far view. */
125
+ export interface RoomSetting {
126
+ /** The name of the setting in the column `board`. */
127
+ readonly name: string;
128
+ readonly options: RoomOptions;
129
+ /** The lane order of the far view. Without it, the links of a gap share the lines of the level links. */
130
+ readonly lanes?: LaneOrder;
131
+ }
132
+
133
+ /**
134
+ * This function measures the board that `makeRoom` opens with each setting, with the method `routes`.
135
+ * The first row is the fair baseline: the board with each gap open to the gutter of `baseline` only, measured at the spacing of `baseline`.
136
+ */
137
+ export function sweepRoom(fixture: string, graph: Graph, settings: readonly RoomSetting[], baseline: RoomOptions = { gutter: 8, spacing: 4 }): RoomBenchRow[] {
138
+ const area0 = areaOf(graph.nodes);
139
+ const links = roomLinks(graph);
140
+ const row = (board: string, nodes: readonly Node[], ms: number, spacing: number, lanes?: LaneOrder): RoomBenchRow => ({
141
+ fixture,
142
+ board,
143
+ area: area0 > 0 ? areaOf(nodes) / area0 : 1,
144
+ ms,
145
+ ...measureBoard({ nodes, edges: graph.edges }, spacing, lanes),
146
+ });
147
+ const fair = makeRoom(graph.nodes, links, { ...baseline, spacing: 0, method: "routes", maxGrowth: Infinity, passes: 1 });
148
+ const rows = [row("gutter", fair.items, 0, baseline.spacing ?? 4)];
149
+ for (const s of settings) {
106
150
  const t0 = now();
107
- const room = makeRoom(graph.nodes, links, o);
108
- rows.push(row(board, room.items, now() - t0));
151
+ const open = makeRoom(graph.nodes, links, { ...s.options, method: "routes" });
152
+ rows.push(row(s.name, open.items, now() - t0, s.options.spacing ?? 4, s.lanes));
109
153
  }
110
154
  return rows;
111
155
  }
112
156
 
157
+ /** The values of `maxGrowth` of the sweep of ROOM-A. */
158
+ export const GROWTH_SWEEP: readonly number[] = [0, 0.06, 0.09, 0.12, 0.15, 0.18, 0.21, 0.25, 0.3, 0.35, 0.5, Infinity];
159
+
160
+ /** This function gives the settings of ROOM-A: the area and the faults for each `maxGrowth`, at gutter 8 and spacing 4. */
161
+ export function growthSettings(values: readonly number[] = GROWTH_SWEEP): RoomSetting[] {
162
+ return values.map((m) => ({ name: `maxGrowth ${m}`, options: { gutter: 8, spacing: 4, maxGrowth: m } }));
163
+ }
164
+
165
+ /**
166
+ * This function gives the settings of ROOM-C: each lane order of the far view at each spacing, at gutter 8.
167
+ * The order `shared` is the far view without lanes, as before ROOM-C.
168
+ */
169
+ export function laneSettings(spacings: readonly number[] = [2, 4, 6, 8], orders: readonly LaneOrder[] = LANE_ORDERS): RoomSetting[] {
170
+ return spacings.flatMap((spacing) => [
171
+ { name: `shared spacing ${spacing}`, options: { gutter: 8, spacing } },
172
+ ...orders.map((lanes) => ({ name: `${lanes} spacing ${spacing}`, options: { gutter: 8, spacing }, lanes })),
173
+ ]);
174
+ }
175
+
176
+ /** This function gives the settings of ROOM-D: each gutter with each number of passes, at spacing 4. */
177
+ export function passSettings(gutters: readonly number[] = [4, 8, 12, 16], passes: readonly number[] = [1, 2]): RoomSetting[] {
178
+ return gutters.flatMap((gutter) => passes.map((p) => ({ name: `gutter ${gutter} passes ${p}`, options: { gutter, spacing: 4, passes: p } })));
179
+ }
180
+
113
181
  /** This function writes the rows of `benchRoom` as a Markdown table. */
114
182
  export function formatRoom(rows: readonly RoomBenchRow[]): string {
115
183
  const head = "| fixture | board | area | links | through | outside | out and back | worst detour | crossings | ink | detour | lane through | ms |";
@@ -0,0 +1,121 @@
1
+ import type { Graph, Result } from "../model.ts";
2
+ import { sectionsRouter, type SectionsOptions } from "../routers/sections.ts";
3
+ import { groupStrategies } from "../strategies/builtins.ts";
4
+ import type { Strategy } from "../strategies/model.ts";
5
+ import { sameData } from "./run.ts";
6
+ import { measureLevelFaults, measureRouteFaults, sumFaults, type LinkFaults } from "./links.ts";
7
+
8
+ // The core runs in any host, so it reads the clock without the types of Node or the browser.
9
+ const now = (): number => (globalThis as { performance?: { now(): number } }).performance?.now() ?? Date.now();
10
+
11
+ /** The options of one setting of the router `sections`, without the name. */
12
+ export type SectionsSettingOptions = Omit<SectionsOptions, "name" | "maxNodes">;
13
+
14
+ /** One setting of a sweep of the router `sections`. */
15
+ export interface SectionsSetting {
16
+ /** The name of the setting in the column `setting`. */
17
+ readonly name: string;
18
+ readonly options: SectionsSettingOptions;
19
+ }
20
+
21
+ /** One row of a sweep of the router `sections`: one setting on one board, and the measures of its routes and of its zoom levels. */
22
+ export interface SectionsBenchRow {
23
+ readonly fixture: string;
24
+ readonly setting: string;
25
+ /** The faults of the routes of the edges (`measureRouteFaults`). */
26
+ readonly faults: LinkFaults;
27
+ /** The sum of the faults of the levels with groups (`measureLevelFaults` and `sumFaults`). */
28
+ readonly levelFaults: LinkFaults;
29
+ /** The sum of the overlaps of the levels with groups. */
30
+ readonly overlaps: number;
31
+ /** The number of routes that go through hubs: the routes that are not the same as the direct route of their edge. */
32
+ readonly hubRoutes: number;
33
+ /** The number of shared stretches of way of the result (`runs`). */
34
+ readonly runs: number;
35
+ /** The number of zoom levels, with the last level of single nodes. */
36
+ readonly levels: number;
37
+ /** The number of items of the far view: the first level with more than one item. */
38
+ readonly farItems: number;
39
+ /** The time of one route, in milliseconds. */
40
+ readonly ms: number;
41
+ }
42
+
43
+ /** This function gives the number of items of the first level with more than one item, or 0 for a result without levels. */
44
+ function farItems(result: Result): number {
45
+ return result.levels?.find((l) => l.items.length > 1)?.items.length ?? 0;
46
+ }
47
+
48
+ /**
49
+ * This function measures the router `sections` on one board with each setting. The first row is the fair baseline: the defaults, with no option.
50
+ * The direct routes come from the hub limit 0, so `hubRoutes` counts the routes that use the hubs.
51
+ */
52
+ export function sweepSections(fixture: string, graph: Graph, settings: readonly SectionsSetting[]): SectionsBenchRow[] {
53
+ const all: SectionsSetting[] = [{ name: "defaults", options: {} }, ...settings];
54
+ return all.map((s) => {
55
+ const direct = sectionsRouter({ ...s.options, name: "direct", stretch: 0, stretchPad: 0 }).route(graph);
56
+ const t0 = now();
57
+ const result = sectionsRouter({ ...s.options, name: s.name }).route(graph);
58
+ const ms = now() - t0;
59
+ const levelFaults = measureLevelFaults(result);
60
+ return {
61
+ fixture,
62
+ setting: s.name,
63
+ faults: measureRouteFaults(graph, result),
64
+ levelFaults: sumFaults(levelFaults),
65
+ overlaps: levelFaults.reduce((n, f) => n + f.overlaps, 0),
66
+ hubRoutes: result.routes.filter((r, i) => !sameData(r.points, direct.routes[i]?.points)).length,
67
+ runs: result.runs?.length ?? 0,
68
+ levels: result.levels?.length ?? 0,
69
+ farItems: farItems(result),
70
+ ms,
71
+ };
72
+ });
73
+ }
74
+
75
+ /** The values of the hub limit (`stretch`) of the sweep of CUSTOM. The default is 2. */
76
+ export const STRETCH_SWEEP: readonly number[] = [1.25, 1.5, 2, 3, Infinity];
77
+
78
+ /** This function gives the settings of the sweep of the hub limit: one setting for each value of `stretch`. */
79
+ export function stretchSettings(values: readonly number[] = STRETCH_SWEEP): SectionsSetting[] {
80
+ return values.map((stretch) => ({ name: `stretch ${stretch}`, options: { stretch } }));
81
+ }
82
+
83
+ /** The values of `hubSlack` of the sweep of CUSTOM. The default is 80. */
84
+ export const SLACK_SWEEP: readonly number[] = [0, 40, 80, 160, Infinity];
85
+
86
+ /** This function gives the settings of the sweep of the slack of the hubs: one setting for each value of `hubSlack`. */
87
+ export function slackSettings(values: readonly number[] = SLACK_SWEEP): SectionsSetting[] {
88
+ return values.map((hubSlack) => ({ name: `hubSlack ${hubSlack}`, options: { hubSlack } }));
89
+ }
90
+
91
+ /** The values of the section limit (`cap`) of the sweep of CUSTOM. The default is 8. */
92
+ export const CAP_SWEEP: readonly number[] = [4, 6, 8, 12, 16];
93
+
94
+ /** This function gives the settings of the sweep of the section limit: one setting for each value of `cap`. */
95
+ export function capSettings(values: readonly number[] = CAP_SWEEP): SectionsSetting[] {
96
+ return values.map((cap) => ({ name: `cap ${cap}`, options: { cap } }));
97
+ }
98
+
99
+ /**
100
+ * This function gives one setting for each grouping rule: the router `sections` with the strategy as its option `group`.
101
+ * The map `options` gives the options of a strategy by its name.
102
+ */
103
+ export function groupSettings(
104
+ strategies: readonly Strategy<"group">[] = Object.values(groupStrategies),
105
+ options: Readonly<Record<string, object>> = {},
106
+ ): SectionsSetting[] {
107
+ return strategies.map((group) => ({ name: `group ${group.name}`, options: { group, ...(options[group.name] ? { groupOptions: options[group.name] } : {}) } }));
108
+ }
109
+
110
+ /** This function writes the rows of `sweepSections` as a Markdown table. */
111
+ export function formatSections(rows: readonly SectionsBenchRow[]): string {
112
+ const head =
113
+ "| fixture | setting | through | out and back | worst detour | outside | hub routes | runs | levels | far items | level through | level out and back | level outside | level overlaps | ms |";
114
+ const line = "|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|";
115
+ const body = rows.map((r) => {
116
+ const f = r.faults;
117
+ const l = r.levelFaults;
118
+ return `| ${r.fixture} | ${r.setting} | ${f.through} | ${f.backtrack} | ${f.worstDetour.toFixed(2)} | ${f.outside} | ${r.hubRoutes} | ${r.runs} | ${r.levels} | ${r.farItems} | ${l.through} | ${l.backtrack} | ${l.outside} | ${r.overlaps} | ${r.ms.toFixed(1)} |`;
119
+ });
120
+ return [head, line, ...body].join("\n");
121
+ }
@@ -0,0 +1,2 @@
1
+ export { quadtreeGroups } from "./quadtree/index.ts";
2
+ export { byDataGroup, keyedSections, keyGroups, type KeyOptions, type NodeKey } from "./keys/index.ts";
@@ -0,0 +1,81 @@
1
+ import type { Box } from "../../geom.ts";
2
+ import type { Graph, Node } from "../../model.ts";
3
+ import { buildSections, type Section, type SectionOptions } from "../../sections.ts";
4
+
5
+ /** A key rule gives the key of the group of a node. A node without a key is a group alone. */
6
+ export type NodeKey = (node: Node, index: number) => string | undefined;
7
+
8
+ /** The options of the grouping rule `keys`. */
9
+ export interface KeyOptions {
10
+ /** This function gives the key of the group of each node. The default is `byDataGroup`. */
11
+ readonly key?: NodeKey;
12
+ /**
13
+ * With this value, a group with more nodes than its `cap` splits into the cells of a quadtree, as `buildSections` does.
14
+ * Without it, each group is one leaf. The default is no split.
15
+ */
16
+ readonly split?: SectionOptions;
17
+ }
18
+
19
+ /** This key rule gives the string `data.group` of the node, or `undefined` when the node has no such string. */
20
+ export const byDataGroup: NodeKey = (node) => {
21
+ const group = node.data?.["group"];
22
+ return typeof group === "string" ? group : undefined;
23
+ };
24
+
25
+ /** This function gives the bounding box of the boxes of the nodes of the list. */
26
+ function boundsOf(nodes: readonly Node[], list: readonly number[]): Box {
27
+ let x0 = Infinity;
28
+ let y0 = Infinity;
29
+ let x1 = -Infinity;
30
+ let y1 = -Infinity;
31
+ for (const i of list) {
32
+ const b = nodes[i]!.box;
33
+ x0 = Math.min(x0, b.x);
34
+ y0 = Math.min(y0, b.y);
35
+ x1 = Math.max(x1, b.x + b.w);
36
+ y1 = Math.max(y1, b.y + b.h);
37
+ }
38
+ return list.length ? { x: x0, y: y0, w: x1 - x0, h: y1 - y0 } : { x: 0, y: 0, w: 0, h: 0 };
39
+ }
40
+
41
+ /**
42
+ * This function gives the tree of sections of the grouping rule `keys`. The root holds each node. Each child of the root holds the nodes of one key,
43
+ * in the order of their first node. The id of a group is `s:` and its key. A node without a key is a group alone, with the id `s#` and its index.
44
+ * Thus the far view of the router `sections` shows one item for each group.
45
+ */
46
+ export function keyedSections(nodes: readonly Node[], options: KeyOptions = {}): Section {
47
+ const key = options.key ?? byDataGroup;
48
+ const groups = new Map<string, number[]>();
49
+ nodes.forEach((n, i) => {
50
+ const k = key(n, i);
51
+ const id = k === undefined ? `s#${i}` : `s:${k}`;
52
+ const list = groups.get(id);
53
+ if (list) list.push(i);
54
+ else groups.set(id, [i]);
55
+ });
56
+ const children = [...groups].map(([id, list]) => group(nodes, id, list, options.split));
57
+ return { id: "s", depth: 0, cell: boundsOf(nodes, nodes.map((_, i) => i)), nodes: nodes.map((_, i) => i), children };
58
+ }
59
+
60
+ /** This function gives the section of one group at depth 1. With `split`, the section has the quadtree of its nodes below it. */
61
+ function group(nodes: readonly Node[], id: string, list: readonly number[], split: SectionOptions | undefined): Section {
62
+ if (!split) return { id, depth: 1, cell: boundsOf(nodes, list), nodes: list, children: [] };
63
+ const tree = buildSections(
64
+ list.map((i) => nodes[i]!),
65
+ split,
66
+ );
67
+ // The quadtree numbers the nodes of the group from 0. Each section gets the indices of the graph, one more level and the id of the group before its path.
68
+ const rebase = (s: Section): Section => ({
69
+ id: s.id === "s" ? id : `${id}/${s.id.slice(1)}`,
70
+ depth: s.depth + 1,
71
+ cell: s.cell,
72
+ nodes: s.nodes.map((j) => list[j]!),
73
+ children: s.children.map(rebase),
74
+ });
75
+ return rebase(tree);
76
+ }
77
+
78
+ /** This function gives the tree of sections of the grouping rule `keys` for a graph. */
79
+ export function keyGroups(graph: Graph, options: KeyOptions = {}): Section {
80
+ return keyedSections(graph.nodes, options);
81
+ }
@@ -0,0 +1,10 @@
1
+ import type { Graph } from "../../model.ts";
2
+ import { buildSections, type Section, type SectionOptions } from "../../sections.ts";
3
+
4
+ /**
5
+ * This function gives the tree of sections of the grouping rule `quadtree`: the cells of a quadtree of the plane (`buildSections`).
6
+ * It is the default grouping rule of the router `sections`.
7
+ */
8
+ export function quadtreeGroups(graph: Graph, options: SectionOptions = {}): Section {
9
+ return buildSections(graph.nodes, options);
10
+ }
package/src/index.ts CHANGED
@@ -10,11 +10,16 @@ export * from "./visibility.ts";
10
10
  export * from "./ways.ts";
11
11
  export * from "./segments.ts";
12
12
  export * from "./sections.ts";
13
+ export * from "./groups/index.ts";
14
+ export * from "./pick.ts";
13
15
  export * from "./routers/index.ts";
14
16
  export * from "./room/index.ts";
17
+ export * from "./room/lanes.ts";
15
18
  export * from "./bench/properties.ts";
16
19
  export * from "./bench/quality.ts";
17
20
  export * from "./bench/links.ts";
18
21
  export * from "./bench/run.ts";
19
22
  export * from "./bench/curves.ts";
20
23
  export * from "./bench/room.ts";
24
+ export * from "./bench/sections.ts";
25
+ export * from "./strategies/index.ts";
package/src/levels.ts CHANGED
@@ -28,8 +28,19 @@ interface LevelWays extends Ways {
28
28
  readonly free: Map<Node, FreePlugs>;
29
29
  }
30
30
 
31
- /** The margin of the second corner graph. A gap between two boxes wider than approximately 1.5 times this margin lets a link through. */
32
- const TIGHT_MARGIN = 1;
31
+ /** The options of the links of a level: the options of the ways, and two limits of the level links. */
32
+ export interface LevelLinkOptions extends WayOptions {
33
+ /**
34
+ * The margin of the second corner graph. A gap between two boxes wider than approximately 1.5 times this margin lets a link through.
35
+ * The second graph uses the smaller of this value and `margin`. The default is 1.
36
+ */
37
+ readonly tightMargin?: number;
38
+ /** A plug stays at least this far from the corners of its side. The level links use the smaller of this value and `margin`. The default is 6. */
39
+ readonly plugInset?: number;
40
+ }
41
+
42
+ /** The default values of the two limits of `LevelLinkOptions`. */
43
+ export const LEVEL_LINK_DEFAULTS: Readonly<Required<Pick<LevelLinkOptions, "tightMargin" | "plugInset">>> = Object.freeze({ tightMargin: 1, plugInset: 6 });
33
44
 
34
45
  const EPS = 1e-6;
35
46
 
@@ -60,12 +71,12 @@ function tidy(p: readonly Point[]): Point[] {
60
71
  * A link starts on the boundary of the box of its source and ends on the boundary of the box of its target.
61
72
  * It goes around the boxes of the other items. Where there is space, its legs are parallel to the axes.
62
73
  */
63
- export function levelLinks(items: readonly LevelItem[], pairs: readonly ItemPair[], options: WayOptions = {}): LevelLink[] {
74
+ export function levelLinks(items: readonly LevelItem[], pairs: readonly ItemPair[], options: LevelLinkOptions = {}): LevelLink[] {
64
75
  if (pairs.length === 0) return [];
65
76
  const nodes: Node[] = items.map((i) => ({ id: i.id, box: i.box }));
66
77
  const byId = new Map(nodes.map((n) => [n.id, n]));
67
78
  const ways = levelWays(nodes, options);
68
- const inset = Math.min(6, ways.margin);
79
+ const inset = Math.min(options.plugInset ?? LEVEL_LINK_DEFAULTS.plugInset, ways.margin);
69
80
  const out: LevelLink[] = [];
70
81
  for (const p of pairs) {
71
82
  const s = byId.get(p.source);
@@ -101,7 +112,7 @@ function sideWay(ways: LevelWays, s: Node, t: Node, inset: number): Point[] {
101
112
  * The buckets of the obstacles grow with the items, so a long link visits few buckets.
102
113
  * The corner graphs use `cones`, so a link goes along the corridors between the groups, not around the outer edge of the board.
103
114
  */
104
- function levelWays(nodes: readonly Node[], options: WayOptions): LevelWays {
115
+ function levelWays(nodes: readonly Node[], options: LevelLinkOptions): LevelWays {
105
116
  const margin = options.margin ?? 5;
106
117
  const k = options.k ?? 8;
107
118
  let size = 0;
@@ -110,7 +121,7 @@ function levelWays(nodes: readonly Node[], options: WayOptions): LevelWays {
110
121
  const corners = new CornerGraph(nodes, obstacles, { margin, k, cones: true });
111
122
  const out = (p: Plug): Point => ({ x: p.point.x + p.normal.x * margin, y: p.point.y + p.normal.y * margin });
112
123
  let tight: CornerGraph | undefined;
113
- const tightGraph = (): CornerGraph => (tight ??= new CornerGraph(nodes, obstacles, { margin: Math.min(margin, TIGHT_MARGIN), k, cones: true }));
124
+ const tightGraph = (): CornerGraph => (tight ??= new CornerGraph(nodes, obstacles, { margin: Math.min(margin, options.tightMargin ?? LEVEL_LINK_DEFAULTS.tightMargin), k, cones: true }));
114
125
  return { obstacles, corners, margin, out, tight: tightGraph, free: new Map() };
115
126
  }
116
127
 
package/src/model.ts CHANGED
@@ -20,6 +20,8 @@ export interface Node {
20
20
  readonly id: string;
21
21
  readonly box: Box;
22
22
  readonly ports?: readonly Port[];
23
+ /** This data is free for the grouping rules of the nodes. The rule `keys` reads the string `data.group` by default. */
24
+ readonly data?: Readonly<Record<string, unknown>>;
23
25
  }
24
26
 
25
27
  /** One end of an edge: a node, and optionally one plug of that node. */
package/src/pick.ts ADDED
@@ -0,0 +1,36 @@
1
+ import type { Level } from "./model.ts";
2
+
3
+ /** This function gives the median width of the shown items of each level, in units of the plane. */
4
+ export function itemSizes(levels: readonly Level[]): number[] {
5
+ return levels.map((level) => {
6
+ const sizes = level.items.map((i) => Math.max(i.box.w, i.box.h)).sort((a, b) => a - b);
7
+ return sizes.length ? sizes[Math.floor(sizes.length / 2)]! : 0;
8
+ });
9
+ }
10
+
11
+ /** The far-view thresholds of `pickLevel`, in pixels on the screen. */
12
+ export interface PickOptions {
13
+ /** When a node is at least this wide on the screen, the view shows each route. */
14
+ readonly routesAt: number;
15
+ /** A level is fine enough when its median item is at least this wide on the screen. */
16
+ readonly itemAt: number;
17
+ }
18
+
19
+ /** The default thresholds: a node of 22 pixels shows its routes, and a group of 120 pixels shows its parts. */
20
+ export const PICK: PickOptions = Object.freeze({ routesAt: 22, itemAt: 120 });
21
+
22
+ /**
23
+ * This function picks the level that a camera shows at a scale, in pixels for each unit of the plane. The sizes come from `itemSizes`.
24
+ * The value `sizes.length` stands for the routes of each edge. The last level holds the nodes alone.
25
+ * Each threshold that `options` does not give gets its value from `PICK`.
26
+ */
27
+ export function pickLevel(sizes: readonly number[], scale: number, options: Partial<PickOptions> = PICK): number {
28
+ const routesAt = options.routesAt ?? PICK.routesAt;
29
+ const itemAt = options.itemAt ?? PICK.itemAt;
30
+ const n = sizes.length;
31
+ if (n === 0) return 0;
32
+ if (sizes[n - 1]! * scale >= routesAt) return n;
33
+ let pick = 0;
34
+ for (let d = 0; d < n - 1; d++) if (sizes[d]! * scale >= itemAt) pick = d;
35
+ return pick;
36
+ }
package/src/room/index.ts CHANGED
@@ -64,6 +64,12 @@ export interface RoomOptions {
64
64
  readonly lanes?: "link" | "count";
65
65
  /** Two box edges nearer than this distance make one boundary of the sweep. The default is 0.5. */
66
66
  readonly snap?: number;
67
+ /**
68
+ * The number of passes of the demand. The default is 1.
69
+ * Each pass after the first finds the lanes again on the board that the passes before it opened, and adds the growth that it needs.
70
+ * The limit `maxGrowth` applies to the sum of the passes.
71
+ */
72
+ readonly passes?: number;
67
73
  }
68
74
 
69
75
  /** The demand of a board and the board with its gaps open. */
@@ -147,14 +153,28 @@ export function openGaps<T extends Boxed>(items: readonly T[], gaps: readonly Ga
147
153
  * The lanes of a gap come from the `method` of the options. The need of a gap is `gutter + lanes × spacing`, and not less than its current width.
148
154
  */
149
155
  export function roomDemand(items: readonly Boxed[], links: readonly RoomLink[], options: RoomOptions = {}): Gap[] {
156
+ const gutter = options.gutter ?? DEFAULT_GUTTER;
157
+ const snap = options.snap ?? DEFAULT_SNAP;
158
+ const passes = Math.max(1, Math.floor(options.passes ?? 1));
159
+ let total = demandOnce(items, links, options);
160
+ for (let p = 1; p < passes; p++) {
161
+ // The next pass measures the board that the passes before it opened. Its gaps are the same gaps, in the same order.
162
+ const next = demandOnce(openGaps(items, total, snap), links, options);
163
+ if (next.length !== total.length || next.some((g, i) => g.axis !== total[i]!.axis)) break;
164
+ total = total.map((g, i) => ({ ...g, lanes: Math.max(g.lanes, next[i]!.lanes), need: g.need + Math.max(0, next[i]!.need - next[i]!.width) }));
165
+ }
166
+ return capGrowth(items, total, gutter, options.maxGrowth ?? Infinity);
167
+ }
168
+
169
+ /** This function gives the demand of one pass, without the limit `maxGrowth`. */
170
+ function demandOnce(items: readonly Boxed[], links: readonly RoomLink[], options: RoomOptions): Gap[] {
150
171
  const gutter = options.gutter ?? DEFAULT_GUTTER;
151
172
  const spacing = options.spacing ?? DEFAULT_SPACING;
152
173
  const snap = options.snap ?? DEFAULT_SNAP;
153
174
  const weight = (l: RoomLink): number => (options.lanes === "count" ? Math.max(1, l.count ?? 1) : 1);
154
175
  const gaps = boardGaps(items, snap);
155
176
  const lanes = (options.method ?? "routes") === "lines" ? lineLanes(items, links, gaps, weight) : routeLanes(items, links, gaps, gutter, snap, weight, options.router);
156
- const raw = gaps.map((g, i) => ({ ...g, lanes: lanes[i]!, need: Math.max(g.width, gutter + lanes[i]! * spacing) }));
157
- return capGrowth(items, raw, gutter, options.maxGrowth ?? Infinity);
177
+ return gaps.map((g, i) => ({ ...g, lanes: lanes[i]!, need: Math.max(g.width, gutter + lanes[i]! * spacing) }));
158
178
  }
159
179
 
160
180
  /** This function gives the demand of the board and the board with each gap open to its need. */