@mark1russell7/highways 0.6.0 → 0.8.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.6.0",
12
+ "version": "0.8.0",
13
13
  "type": "module",
14
14
  "main": "./build/index.js",
15
15
  "types": "./build/index.d.ts",
@@ -29,7 +29,7 @@
29
29
  "access": "public"
30
30
  },
31
31
  "dependencies": {
32
- "@mark1russell7/layout-contract": "^0.2.7"
32
+ "@mark1russell7/layout-contract": "^0.2.8"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@mark1russell7/cue": "github:mark1russell7/cue",
@@ -1,5 +1,6 @@
1
1
  // The portfolio combinator (PORTFOLIO-API): `portfolio`, `selectBest` and `pickMoves` come from `@mark1russell7/layout-contract` 0.2.6 (PORTFOLIO-FIT).
2
- // The rules of the pick, the time limit and the trace are in `docs/PORTFOLIO.md` of the contract.
2
+ // The option `hashes` of `PortfolioOptions` comes from the contract 0.2.8 (HASH-LAZY).
3
+ // The rules of the pick, the time limit, the hashes and the trace are in `docs/PORTFOLIO.md` of the contract.
3
4
  export {
4
5
  pickMoves,
5
6
  portfolio,
package/src/compare.ts ADDED
@@ -0,0 +1,17 @@
1
+ // The named comparators of the core (LANE-ORDER). `comparators.test.ts` checks that each one is a total order.
2
+ // The package index does not export this module.
3
+
4
+ /** This function compares two texts by their UTF-16 code units. It gives 0 only for equal texts. */
5
+ export const byText = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
6
+
7
+ /** An edge id with its hash. */
8
+ export interface Hashed {
9
+ readonly h: number;
10
+ readonly e: { readonly id: string };
11
+ }
12
+
13
+ /** This function compares two edges by the hash of their ids, and then by their ids. Thus the order of two edges does not depend on the input. */
14
+ export function byHash(a: Hashed, b: Hashed): number {
15
+ if (a.h !== b.h) return a.h - b.h;
16
+ return byText(a.e.id, b.e.id);
17
+ }
@@ -0,0 +1,149 @@
1
+ // The lane order of a run (LANE-ORDER): the relation of two lanes, and a total order from it. `orderLanes` of `segments.ts` uses this module.
2
+ // The package index does not export this module.
3
+ import { byText } from "./compare.ts";
4
+ import { dist, type Point } from "./geom.ts";
5
+
6
+ /** Two points nearer than this distance are the same point. The value is the same as in `segments.ts`. */
7
+ const SAME = 1e-6;
8
+
9
+ /** The place of a route on a run. The points go in the direction of the run. `s` and `e` are the indexes of the ends of the run. */
10
+ export interface Placed {
11
+ readonly edge: string;
12
+ readonly p: readonly Point[];
13
+ readonly s: number;
14
+ readonly e: number;
15
+ }
16
+
17
+ /** The place where two routes part past one end of their common stretch, and the side of the first route there. */
18
+ interface Parting {
19
+ /** The last common point. */
20
+ readonly at: Point;
21
+ /** This value is below 0 when the first route goes to the left of the second route. It is above 0 when it goes to the right. */
22
+ readonly side: number;
23
+ }
24
+
25
+ /** The angle, from -π to π, of the turn at `base` from the direction `from` → `base` to the point `to`. */
26
+ function turn(from: Point, base: Point, to: Point): number {
27
+ const dx = base.x - from.x;
28
+ const dy = base.y - from.y;
29
+ const vx = to.x - base.x;
30
+ const vy = to.y - base.y;
31
+ return Math.atan2(dx * vy - dy * vx, dx * vx + dy * vy);
32
+ }
33
+
34
+ /** This function gives the place where the routes `P` and `Q` part after the last point of the run, or nothing. */
35
+ function partAfter(P: Placed, Q: Placed): Parting | undefined {
36
+ for (let j = 1; ; j++) {
37
+ const a = P.p[P.e + j];
38
+ const b = Q.p[Q.e + j];
39
+ if (a === undefined || b === undefined) return undefined;
40
+ if (dist(a, b) > SAME) {
41
+ const from = P.p[P.e + j - 2]!;
42
+ const at = P.p[P.e + j - 1]!;
43
+ const side = turn(from, at, a) - turn(from, at, b);
44
+ return side === 0 ? undefined : { at, side };
45
+ }
46
+ }
47
+ }
48
+
49
+ /** This function gives the place where the routes `P` and `Q` part before the first point of the run, or nothing. */
50
+ function partBefore(P: Placed, Q: Placed): Parting | undefined {
51
+ for (let j = 1; ; j++) {
52
+ const a = P.p[P.s - j];
53
+ const b = Q.p[Q.s - j];
54
+ if (a === undefined || b === undefined) return undefined;
55
+ if (dist(a, b) > SAME) {
56
+ const from = P.p[P.s - j + 2]!;
57
+ const at = P.p[P.s - j + 1]!;
58
+ // Seen backward, the left side is the right side. Thus the order turns around.
59
+ const side = turn(from, at, b) - turn(from, at, a);
60
+ return side === 0 ? undefined : { at, side };
61
+ }
62
+ }
63
+ }
64
+
65
+ /** This function compares two placed routes by their edge ids. */
66
+ export const byEdge = (P: Placed, Q: Placed): number => byText(P.edge, Q.edge);
67
+
68
+ /** This function puts the point `a` before the point `b`: the smaller x first, then the smaller y. */
69
+ const firstPoint = (a: Point, b: Point): boolean => a.x < b.x - SAME || (Math.abs(a.x - b.x) <= SAME && a.y < b.y);
70
+
71
+ /**
72
+ * This function is the relation of the order `run`. The side after the run tells the order first, then the side before the run, then the edges.
73
+ * A value below 0 puts `P` before `Q`. The relation is not transitive: three lanes can make a cycle.
74
+ */
75
+ export const byRun = (P: Placed, Q: Placed): number => partAfter(P, Q)?.side ?? partBefore(P, Q)?.side ?? byEdge(P, Q);
76
+
77
+ /**
78
+ * This function is the relation of the order `bundle`. When the two ends disagree, the end with the smaller x, then the smaller y, tells the side.
79
+ * A value below 0 puts `P` before `Q`. The relation is not transitive: three lanes can make a cycle.
80
+ */
81
+ export const byBundle = (P: Placed, Q: Placed): number => {
82
+ const after = partAfter(P, Q);
83
+ const before = partBefore(P, Q);
84
+ if (after && before && Math.sign(after.side) !== Math.sign(before.side)) return firstPoint(after.at, before.at) ? after.side : before.side;
85
+ return after?.side ?? before?.side ?? byEdge(P, Q);
86
+ };
87
+
88
+ /**
89
+ * This function sorts a list with a stable merge sort of the package, not with `Array.prototype.sort`.
90
+ * The pairs that it gives to `compare` depend only on the list and on the results of `compare`.
91
+ * Thus the result is the same on each JavaScript engine, also when `compare` is not a total order.
92
+ * A value of `compare(a, b)` above 0 puts `b` before `a`. Any other value (below 0, 0 or NaN) keeps `a` first.
93
+ */
94
+ export function mergeSort<T>(list: readonly T[], compare: (a: T, b: T) => number): T[] {
95
+ if (list.length < 2) return [...list];
96
+ const mid = list.length >> 1;
97
+ const left = mergeSort(list.slice(0, mid), compare);
98
+ const right = mergeSort(list.slice(mid), compare);
99
+ const out: T[] = [];
100
+ let i = 0;
101
+ let j = 0;
102
+ while (i < left.length && j < right.length) {
103
+ if (compare(left[i]!, right[j]!) > 0) out.push(right[j++]!);
104
+ else out.push(left[i++]!);
105
+ }
106
+ while (i < left.length) out.push(left[i++]!);
107
+ while (j < right.length) out.push(right[j++]!);
108
+ return out;
109
+ }
110
+
111
+ /**
112
+ * This function gives true when the relation of `n` items has a cycle. For `i < j`, a value of `before(i, j)` above 0 puts `j` before `i`.
113
+ * Any other value puts `i` before `j`. The relation has no cycle when the counts of the items that each item comes before are all different.
114
+ */
115
+ export function hasCycle(n: number, before: (i: number, j: number) => number): boolean {
116
+ const wins = new Int32Array(n);
117
+ for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) wins[before(i, j) > 0 ? j : i]!++;
118
+ const seen = new Uint8Array(n);
119
+ for (const w of wins) seen[w] = 1;
120
+ return seen.includes(0);
121
+ }
122
+
123
+ /**
124
+ * The function that `observeLanes` installs. It gets the lanes of each run in their start order, the relation, and the lane order.
125
+ * The measure scripts use it to compare the lane order with the sort of the engine.
126
+ */
127
+ export type LaneObserver = <T>(start: readonly T[], compare: (a: T, b: T) => number, lanes: readonly T[]) => void;
128
+
129
+ let observer: LaneObserver | undefined;
130
+
131
+ /** This function installs an observer of the lane orders, or removes it with `undefined`. The measure scripts use it. The routes do not change. */
132
+ export function observeLanes(fn: LaneObserver | undefined): void {
133
+ observer = fn;
134
+ }
135
+
136
+ /** This function gives the lane order of a run: the merge sort of `start` with the relation `compare`. It tells the observer of `observeLanes`. */
137
+ export function orderRun<T>(start: readonly T[], compare: (a: T, b: T) => number): T[] {
138
+ const lanes = mergeSort(start, compare);
139
+ observer?.(start, compare, lanes);
140
+ return lanes;
141
+ }
142
+
143
+ /**
144
+ * This function gives the lane order of the placed routes of a run. `order` selects the relation.
145
+ * With `run`, the sort starts from the order of `placed`. With `bundle`, it starts from the order of the edges, so that each run sorts the same edges the same way.
146
+ */
147
+ export function orderPlaced(placed: readonly Placed[], order: "run" | "bundle"): Placed[] {
148
+ return order === "run" ? orderRun(placed, byRun) : orderRun([...placed].sort(byEdge), byBundle);
149
+ }
@@ -1,14 +1,9 @@
1
1
  import { fnv1a32 } from "../bench/screen/hash.ts";
2
+ import { byHash } from "../compare.ts";
2
3
  import { rng } from "../fixtures.ts";
3
- import type { Edge, Graph, Result, Router } from "../model.ts";
4
+ import type { Graph, Result, Router } from "../model.ts";
4
5
  import { defineStrategy, type Strategy } from "../strategies/model.ts";
5
6
 
6
- /** This function compares two edges by the hash of their ids, and then by their ids. Thus the order of two edges does not depend on the input. */
7
- function byHash(a: { readonly h: number; readonly e: Edge }, b: { readonly h: number; readonly e: Edge }): number {
8
- if (a.h !== b.h) return a.h - b.h;
9
- return a.e.id < b.e.id ? -1 : a.e.id > b.e.id ? 1 : 0;
10
- }
11
-
12
7
  /**
13
8
  * This function gives the graph with its edges in the stable order of WILD-053: by the FNV-1a hash of the id, then by the id.
14
9
  * The nodes and the edges do not change. Two orders of the same edges give the same graph.
@@ -1,41 +1,41 @@
1
1
  // This file is generated by `scripts/portfolio-sets.ts` (`pnpm portfolio:sets`). Do not edit it. The test `sets.test.ts` checks that it is current.
2
- // The times come from the CI run 40670252502 (the workflow `Portfolio`, `portfolio:time --members`).
2
+ // The times come from the CI run 40681241949 (the workflow `Portfolio`, `portfolio:time --members`).
3
3
  import type { PortfolioCap, PortfolioRule } from "./bands.ts";
4
4
 
5
5
  /**
6
6
  * The fit of each rule, cap and size band, by the key `<rule>|<cap>|<band>`: the members of the default of the portfolio router, in their order.
7
- * Each member has a node limit: it runs only on an input with that many nodes or fewer. The limits keep the model time at or below 42 ms.
7
+ * Each member has a node limit: it runs only on an input with that many nodes or fewer. The limits keep the model time at or below 45 ms.
8
8
  * The base `shared` is not in a fit: the portfolio always runs it first.
9
9
  */
10
10
  export const PORTFOLIO_FIT: Readonly<Record<`${PortfolioRule}|${PortfolioCap}|${string}`, readonly (readonly [string, number])[]>> = {
11
11
  "collection|1.1|n1-12": [["corners-side", 12], ["w2c:select+stretch", 12], ["trunks-gilbert-a05", 12], ["w3a:spiral-a15", 12], ["trunks-tree-bf08", 12], ["hw:zipper", 12], ["w2c:level-links+stretch-b1.1", 12], ["hw:stubs", 12]],
12
- "collection|1.1|n13-24": [["w3e:crossbars+stretch", 24], ["trunks-tree-bf08", 24], ["corners-side", 24], ["shared-p2", 24], ["w2c:zipper+stretch-b1.1", 18]],
13
- "collection|1.1|n25-40": [["w2a:shared-ways+breakeven", 40], ["w2b:weiszfeld-a1", 32]],
12
+ "collection|1.1|n13-24": [["w3e:crossbars+stretch", 24], ["trunks-tree-bf08", 24], ["corners-side", 24], ["shared-p2", 24], ["w2c:zipper+stretch-b1.1", 18], ["hw:weiszfeld", 18], ["trunks-gilbert-a07", 18]],
13
+ "collection|1.1|n25-40": [["w2a:shared-ways+breakeven", 40], ["w2b:weiszfeld-a1", 40]],
14
14
  "collection|1.1|n41-64": [],
15
15
  "collection|1.1|n65-128": [],
16
16
  "collection|1.25|n1-12": [["corners-side", 12], ["trunks-tree-bf06", 12], ["w3f:p-hub-a0.2", 12], ["hw:stubs", 12], ["hw:p-hub", 12], ["hw:spiral", 12], ["w2c:level-links+stretch", 12], ["w3f:spectral-c0.3", 12]],
17
- "collection|1.25|n13-24": [["w2a:shared-ways+stretch", 24], ["shared-p3", 24], ["trunks-gilbert-a07", 24], ["w3e:crossbars+stretch", 24], ["trunks-tree-bf06", 24], ["w3c:hdbscan+stretch", 18], ["shared-wind-p2-r1-e2", 18]],
18
- "collection|1.25|n25-40": [["w2a:shared-ways+stretch", 40], ["shared-p3", 40], ["shared-order-shortest", 32]],
17
+ "collection|1.25|n13-24": [["w2a:shared-ways+stretch", 24], ["shared-p3", 24], ["trunks-gilbert-a07", 24], ["w3e:crossbars+stretch", 24], ["trunks-tree-bf06", 24], ["w3c:hdbscan+stretch", 18], ["shared-wind-p2-r1-e2", 18], ["hw:stubs", 18]],
18
+ "collection|1.25|n25-40": [["w2a:shared-ways+stretch", 40], ["shared-p3", 40], ["shared-order-shortest", 40], ["w2b:weiszfeld-a1", 32]],
19
19
  "collection|1.25|n41-64": [],
20
20
  "collection|1.25|n65-128": [],
21
21
  "collection|1.5|n1-12": [["trunks-gilbert-a03", 12], ["w3e:areas-keys", 12], ["trunks-tree-bf02", 12], ["w3f:p-hub-a0.2", 12], ["w2b:zipper-a1", 12], ["w3a:spiral-a35", 12], ["w3f:spectral-c0.3", 12], ["w2c:level-links+stretch", 12]],
22
- "collection|1.5|n13-24": [["shared-wind-p2-r0.5", 24], ["trunks-gilbert-a03", 24], ["w3e:crossbars+stretch", 24], ["shared-wind-p2-r1-e2", 24], ["trunks-tree-bf06", 24], ["shared-flow-a06-p3", 24], ["shared-wind-p3-r1", 18], ["trunks-gilbert-a07", 18]],
23
- "collection|1.5|n25-40": [["shared-p3", 40], ["hw:lanes", 40], ["shared-wind-p2-r1-e2", 32]],
22
+ "collection|1.5|n13-24": [["shared-wind-p2-r0.5", 24], ["trunks-gilbert-a03", 24], ["w3e:crossbars+stretch", 24], ["shared-wind-p2-r1-e2", 24], ["trunks-tree-bf06", 24], ["shared-flow-a06-p3", 24], ["shared-wind-p3-r1", 24], ["trunks-gilbert-a07", 24]],
23
+ "collection|1.5|n25-40": [["shared-p3", 40], ["hw:lanes", 40], ["shared-wind-p2-r1-e2", 40], ["hw:crossbars", 32]],
24
24
  "collection|1.5|n41-64": [["shared-flow-a06-p3", 64]],
25
25
  "collection|1.5|n65-128": [],
26
- "all|1.1|n1-12": [["w2b:zipper-p2", 12], ["hw:cubu", 12], ["w3d:kdeeb+stretch", 12], ["w3c:hough-wide", 12], ["w3d:kdeeb-routes", 12], ["hw:stubs", 10], ["hw:spiral", 10], ["w3b:growth-s0.7", 10]],
27
- "all|1.1|n13-24": [["w3e:frechet-e0.5", 24], ["w3a:diffusion-wide", 24], ["w2c:zipper+stretch-b1.1", 24], ["w3d:cubu+stretch", 18]],
26
+ "all|1.1|n1-12": [["w2b:zipper-p2", 12], ["hw:cubu", 12], ["w3d:kdeeb+stretch", 12], ["w3c:hough-wide", 12], ["w3d:kdeeb-routes", 12], ["hw:stubs", 12], ["hw:spiral", 12], ["w3b:growth-s0.7", 10]],
27
+ "all|1.1|n13-24": [["w3e:frechet-e0.5", 24], ["w3a:diffusion-wide", 24], ["w2c:zipper+stretch-b1.1", 24], ["w3d:cubu+stretch", 18], ["w3a:spiral-a15", 18]],
28
28
  "all|1.1|n25-40": [["w2c:zipper+stretch-b1.1", 40], ["w3a:spiral-a15", 32]],
29
- "all|1.1|n41-64": [],
29
+ "all|1.1|n41-64": [["hw:diffusion", 64]],
30
30
  "all|1.1|n65-128": [],
31
- "all|1.25|n1-12": [["w3d:kdeeb-routes", 12], ["w3e:mlseb+stretch", 12], ["hw:growth", 12], ["hw:level-links", 10], ["w3b:growth-input", 10], ["w3e:confluent+stretch", 10]],
32
- "all|1.25|n13-24": [["w3d:kdeeb-routes", 24], ["w3f:p-hub-a0.2", 18], ["w3d:kdeeb-nomask", 18]],
33
- "all|1.25|n25-40": [],
31
+ "all|1.25|n1-12": [["w3d:kdeeb-routes", 12], ["w3e:mlseb+stretch", 12], ["hw:growth", 12], ["hw:level-links", 12], ["w3b:growth-input", 10], ["w3e:confluent+stretch", 10], ["w3d:kdeeb-nomask", 10]],
32
+ "all|1.25|n13-24": [["w3d:kdeeb-routes", 24], ["w3f:p-hub-a0.2", 24], ["w3d:kdeeb-nomask", 18], ["w3a:spiral-n3", 18]],
33
+ "all|1.25|n25-40": [["w3d:kdeeb-routes", 32]],
34
34
  "all|1.25|n41-64": [],
35
35
  "all|1.25|n65-128": [],
36
36
  "all|1.5|n1-12": [["w3b:growth-s0.3", 12], ["w3e:mlseb-a0", 12], ["w3e:mlseb-routes", 10], ["w3c:edge-paths-k1.5", 10], ["w3f:territories-r2", 10]],
37
37
  "all|1.5|n13-24": [["hw:growth", 24], ["w3b:growth-input", 24]],
38
- "all|1.5|n25-40": [["hw:growth", 40], ["w3b:growth-s0.7", 40]],
38
+ "all|1.5|n25-40": [["hw:growth", 40], ["w3b:growth-s0.7", 40], ["w3b:growth-s0.3", 32]],
39
39
  "all|1.5|n41-64": [],
40
40
  "all|1.5|n65-128": [],
41
41
  };
@@ -30,8 +30,20 @@ export interface PortfolioRouterOptions {
30
30
  readonly maxMembers?: number;
31
31
  /** The clock in milliseconds. The default is `performance.now`. */
32
32
  readonly now?: () => number;
33
+ /**
34
+ * The members that get an output hash in the trace (HASH-LAZY). The pick does not use the hashes, so the winner and the output do not change.
35
+ * - With `pick` (the default), only the base and the winner get a hash, and only when the winner is not the base.
36
+ * - With `all`, each member that ran gets a hash, as before HASH-LAZY. Use it when you need the `outputHash` of each member.
37
+ */
38
+ readonly hashes?: PortfolioHashes;
33
39
  }
34
40
 
41
+ /** The values of the option `hashes` of the portfolio router. */
42
+ export type PortfolioHashes = "all" | "pick";
43
+
44
+ /** The values of the option `hashes`, in order. The default is `pick`. */
45
+ export const PORTFOLIO_HASHES: readonly PortfolioHashes[] = ["pick", "all"];
46
+
35
47
  /** The base member of each portfolio: the router `shared` with the rule `byCollection`. It is the fallback output above the cap. */
36
48
  export const PORTFOLIO_BASE_MEMBER = "shared";
37
49
 
@@ -49,14 +61,16 @@ export interface PortfolioRoute {
49
61
  }
50
62
 
51
63
  /** This function checks the options of the portfolio router, and gives them with their defaults. It throws a `RangeError` for a bad value. */
52
- function checked(o: PortfolioRouterOptions): { rule: PortfolioRule; cap: PortfolioCap; k: 3 | 5 | 8 | undefined } {
64
+ function checked(o: PortfolioRouterOptions): { rule: PortfolioRule; cap: PortfolioCap; k: 3 | 5 | 8 | undefined; hashes: PortfolioHashes } {
53
65
  const rule = o.rule ?? "collection";
54
66
  const cap = o.cap ?? 1.5;
55
67
  const k = o.k;
68
+ const hashes = o.hashes ?? "pick";
56
69
  if (!PORTFOLIO_RULES.includes(rule)) throw new RangeError(`The portfolio router has the rule "${String(rule)}". It must be ${PORTFOLIO_RULES.join(" or ")}.`);
57
70
  if (!PORTFOLIO_CAPS.includes(cap)) throw new RangeError(`The portfolio router has the cap ${String(cap)}. It must be ${PORTFOLIO_CAPS.join(", ")}.`);
58
71
  if (k !== undefined && !PORTFOLIO_KS_API.includes(k)) throw new RangeError(`The portfolio router has the k ${String(k)}. It must be ${PORTFOLIO_KS_API.join(", ")}.`);
59
- return { rule, cap, k };
72
+ if (!PORTFOLIO_HASHES.includes(hashes)) throw new RangeError(`The portfolio router has the hashes "${String(hashes)}". It must be ${PORTFOLIO_HASHES.join(" or ")}.`);
73
+ return { rule, cap, k, hashes };
60
74
  }
61
75
 
62
76
  /** This function gives the name of the portfolio router with the options `options`: `portfolio-<rule>-<cap>-k<k>`, or `portfolio-<rule>-<cap>-fit` without `k`. */
@@ -83,11 +97,12 @@ export function portfolioMembers(nodes: number, options: PortfolioRouterOptions
83
97
  * The key is `portfolioKey`: a member wins only when it passes the guard against the base. Of those members, the winner has the most ink saved.
84
98
  * A tie within the `minEffect` of B1 goes to the first member (`selectBest`). A member that throws does not win.
85
99
  * The result carries the trace of the pick and the move count `move.pick`.
100
+ * With the option `hashes` "pick" (the default), only the base and the winner get an `outputHash` in the trace. They get it only when the winner is not the base.
86
101
  */
87
102
  export function routePortfolio(graph: Graph, options: PortfolioRouterOptions = {}): PortfolioRoute {
88
103
  // A member with a node limit below the size of the graph does not run. The base `shared` has no such limit below the limit of the router.
89
104
  const names = portfolioMembers(graph.nodes.length, options).filter((name) => name === PORTFOLIO_BASE_MEMBER || (portfolioMember(name)?.maxNodes ?? Infinity) >= graph.nodes.length);
90
- const { rule, cap } = checked(options);
105
+ const { rule, cap, hashes } = checked(options);
91
106
  // The key compares each member with the output of the base, so the run of the base keeps its output.
92
107
  let base: Result | undefined;
93
108
  const members: Member<Graph, Result>[] = names.map((name) => {
@@ -104,6 +119,7 @@ export function routePortfolio(graph: Graph, options: PortfolioRouterOptions = {
104
119
  };
105
120
  });
106
121
  const p = portfolio(portfolioName(options), members, portfolioKey(graph, rule, cap, () => base), {
122
+ hashes,
107
123
  ...(options.budgetMs === undefined ? {} : { budgetMs: options.budgetMs }),
108
124
  ...(options.maxMembers === undefined ? {} : { maxMembers: options.maxMembers }),
109
125
  ...(options.now === undefined ? {} : { now: options.now }),
@@ -1,6 +1,8 @@
1
1
  import { center, dist, type Point } from "../geom.ts";
2
2
  import { nodeIndex, type Edge, type Graph, type GroupRule, type Result, type Route, type Router, type SharedRun } from "../model.ts";
3
3
  import { channelWidth } from "../channels.ts";
4
+ import { orderRun } from "../lane-order.ts";
5
+ import { byText } from "../compare.ts";
4
6
  import { placePlug, type Plug } from "../plugs.ts";
5
7
  import { buildSections, type Section } from "../sections.ts";
6
8
  import { orderLanes, type RunOrder } from "../segments.ts";
@@ -85,8 +87,6 @@ interface Run {
85
87
  readonly ways: Way[];
86
88
  }
87
89
 
88
- const byText = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
89
-
90
90
  /** The angle, from -π to π, of the turn at `base` from the direction `from` → `base` to the point `to`. */
91
91
  function turn(from: Point, base: Point, to: Point): number {
92
92
  const dx = base.x - from.x;
@@ -376,5 +376,6 @@ function laneOrderOfRun(run: Run): string[] {
376
376
  }
377
377
  return byText(p.edge.id, q.edge.id);
378
378
  };
379
- return [...run.ways].sort(compare).map((w) => w.edge.id);
379
+ // This relation is not transitive, so the merge sort of the package gives the same order on each engine (LANE-ORDER).
380
+ return orderRun(run.ways, compare).map((w) => w.edge.id);
380
381
  }
package/src/segments.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { dist, type Point } from "./geom.ts";
2
+ import { orderPlaced, type Placed } from "./lane-order.ts";
2
3
  import type { Result, Route } from "./model.ts";
3
4
 
4
5
  /** A piece of drawn way. A shared segment carries several edges, in lane order. A private segment carries one edge. */
@@ -193,15 +194,6 @@ function lanePaths(graph: SegmentGraph, spacingOf: (s: Segment) => number): Rout
193
194
  return routes;
194
195
  }
195
196
 
196
- /** The angle, from -π to π, of the turn at `base` from the direction `from` → `base` to the point `to`. */
197
- function turn(from: Point, base: Point, to: Point): number {
198
- const dx = base.x - from.x;
199
- const dy = base.y - from.y;
200
- const vx = to.x - base.x;
201
- const vy = to.y - base.y;
202
- return Math.atan2(dx * vy - dy * vx, dx * vx + dy * vy);
203
- }
204
-
205
197
  /**
206
198
  * The lane order of the shared ways of a router.
207
199
  * With `bundle`, two edges keep one order on all the shared ways of their common stretch.
@@ -209,58 +201,6 @@ function turn(from: Point, base: Point, to: Point): number {
209
201
  */
210
202
  export type RunOrder = "bundle" | "run";
211
203
 
212
- /** The place of a route on a run. The points go in the direction of the run. `s` and `e` are the indexes of the ends of the run. */
213
- interface Placed {
214
- readonly edge: string;
215
- readonly p: readonly Point[];
216
- readonly s: number;
217
- readonly e: number;
218
- }
219
-
220
- /** The place where two routes part past one end of their common stretch, and the side of the first route there. */
221
- interface Parting {
222
- /** The last common point. */
223
- readonly at: Point;
224
- /** This value is below 0 when the first route goes to the left of the second route. It is above 0 when it goes to the right. */
225
- readonly side: number;
226
- }
227
-
228
- /** This function gives the place where the routes `P` and `Q` part after the last point of the run, or nothing. */
229
- function partAfter(P: Placed, Q: Placed): Parting | undefined {
230
- for (let j = 1; ; j++) {
231
- const a = P.p[P.e + j];
232
- const b = Q.p[Q.e + j];
233
- if (a === undefined || b === undefined) return undefined;
234
- if (dist(a, b) > SAME) {
235
- const from = P.p[P.e + j - 2]!;
236
- const at = P.p[P.e + j - 1]!;
237
- const side = turn(from, at, a) - turn(from, at, b);
238
- return side === 0 ? undefined : { at, side };
239
- }
240
- }
241
- }
242
-
243
- /** This function gives the place where the routes `P` and `Q` part before the first point of the run, or nothing. */
244
- function partBefore(P: Placed, Q: Placed): Parting | undefined {
245
- for (let j = 1; ; j++) {
246
- const a = P.p[P.s - j];
247
- const b = Q.p[Q.s - j];
248
- if (a === undefined || b === undefined) return undefined;
249
- if (dist(a, b) > SAME) {
250
- const from = P.p[P.s - j + 2]!;
251
- const at = P.p[P.s - j + 1]!;
252
- // Seen backward, the left side is the right side. Thus the order turns around.
253
- const side = turn(from, at, b) - turn(from, at, a);
254
- return side === 0 ? undefined : { at, side };
255
- }
256
- }
257
- }
258
-
259
- const byEdge = (P: Placed, Q: Placed): number => (P.edge < Q.edge ? -1 : P.edge > Q.edge ? 1 : 0);
260
-
261
- /** This function puts the point `a` before the point `b`: the smaller x first, then the smaller y. */
262
- const firstPoint = (a: Point, b: Point): boolean => a.x < b.x - SAME || (Math.abs(a.x - b.x) <= SAME && a.y < b.y);
263
-
264
204
  /**
265
205
  * This function gives the lane order of the edges on a shared way `run` (metro-line order).
266
206
  * The routes are polylines that hold the points of the run in a row, in either direction. A route that does not hold the run goes last.
@@ -271,6 +211,11 @@ const firstPoint = (a: Point, b: Point): boolean => a.x < b.x - SAME || (Math.ab
271
211
  * With the order `bundle`, two routes that part on one side at both ends keep that side.
272
212
  * When the two ends disagree, the two routes must cross. The end with the smaller x, then the smaller y, tells the side.
273
213
  * Thus each shared way of a common stretch gives two routes the same order, and the crossing is where they part.
214
+ *
215
+ * This relation of two lanes is not transitive, so three lanes can make a cycle. The sort of the engine then gives another order on another engine.
216
+ * Thus the function sorts with a merge sort of the package (LANE-ORDER). The order is the same on each JavaScript engine.
217
+ * On a run with no cycle, the order is the order of each correct sort, for each order of `routes`.
218
+ * On a run with a cycle, the order depends on the order of `routes`. The item LANE-STRETCH is to remove the cycles.
274
219
  */
275
220
  export function orderLanes(run: readonly Point[], routes: readonly Route[], order: RunOrder = "run"): string[] {
276
221
  const placed: Placed[] = [];
@@ -285,16 +230,6 @@ export function orderLanes(run: readonly Point[], routes: readonly Route[], orde
285
230
  else lost.push(r.edge);
286
231
  }
287
232
  }
288
- if (order === "run") placed.sort((P, Q) => partAfter(P, Q)?.side ?? partBefore(P, Q)?.side ?? byEdge(P, Q));
289
- else {
290
- // The sort starts from the order of the edges, so that each run sorts the same edges the same way.
291
- placed.sort(byEdge);
292
- placed.sort((P, Q) => {
293
- const after = partAfter(P, Q);
294
- const before = partBefore(P, Q);
295
- if (after && before && Math.sign(after.side) !== Math.sign(before.side)) return firstPoint(after.at, before.at) ? after.side : before.side;
296
- return after?.side ?? before?.side ?? byEdge(P, Q);
297
- });
298
- }
299
- return [...placed.map((x) => x.edge), ...lost.sort()];
233
+ const lanes = orderPlaced(placed, order);
234
+ return [...lanes.map((x) => x.edge), ...lost.sort()];
300
235
  }