@mark1russell7/highways 0.9.4 → 0.9.6

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/build/index.js CHANGED
@@ -351,10 +351,20 @@ function portOffset(box, port) {
351
351
  along: port.along
352
352
  });
353
353
  }
354
- /** This function throws a `RangeError` when the `along` of a plug is bad: not finite, outside 0 to 1, without `side` or with `at`. */
354
+ /**
355
+ * This function throws a `RangeError` when the `along` of a plug is bad: not finite, outside 0 to 1, without `side` or with `at`.
356
+ * It also throws a `RangeError` for a bad `range` of a plug. A bad range has a value that is not finite or is outside 0 to 1, or a start after its end.
357
+ */
355
358
  function checkPort(node, port) {
356
- if (port.along === void 0) return;
357
359
  const at = `The plug "${port.id}" of the node "${node.id}"`;
360
+ if (port.range !== void 0) {
361
+ const r = port.range;
362
+ if (!Array.isArray(r) || r.length !== 2 || !r.every((v) => typeof v === "number" && Number.isFinite(v))) throw new RangeError(`${at} has the range ${JSON.stringify(r)}. It must be two finite numbers.`);
363
+ const [lo, hi] = r;
364
+ if (lo < 0 || hi > 1) throw new RangeError(`${at} has the range [${lo}, ${hi}]. It must be in the side, from 0 to 1.`);
365
+ if (lo > hi) throw new RangeError(`${at} has the range [${lo}, ${hi}]. Its start must not be after its end.`);
366
+ }
367
+ if (port.along === void 0) return;
358
368
  if (typeof port.along !== "number" || !Number.isFinite(port.along) || port.along < 0 || port.along > 1) throw new RangeError(`${at} has the along value ${String(port.along)}. It must be a finite number from 0 to 1.`);
359
369
  if (port.side === void 0) throw new RangeError(`${at} has an along value and no side. A plug with along needs a side.`);
360
370
  if (port.at !== void 0) throw new RangeError(`${at} has an along value and an at point. Give one of the two.`);
@@ -362,7 +372,7 @@ function checkPort(node, port) {
362
372
  /**
363
373
  * This function checks the plugs of a graph, and gives each plug with `side` and `along` a fixed `at`. The plug keeps its `side`.
364
374
  * Each router calls it at the start of `route`, so all routers agree. A graph without `along` comes back as the same object.
365
- * It throws a `RangeError` for a bad `along` (`checkPort`).
375
+ * It throws a `RangeError` for a bad `along` or a bad `range` (`checkPort`).
366
376
  */
367
377
  function resolvePorts(graph) {
368
378
  let found = false;
@@ -913,6 +923,115 @@ const CASES = [
913
923
  make: () => manyGroupsBoard()
914
924
  }
915
925
  ];
926
+ // src/plugs/range.ts
927
+ /** The tolerance of the comparisons of places along a side. */
928
+ const EPS$9 = 1e-9;
929
+ /** This function gives the axis along a side. */
930
+ function alongAxis(side) {
931
+ return side === "east" || side === "west" ? "y" : "x";
932
+ }
933
+ /** This function gives the port of an end, or `undefined` when the end has no port or the node has no such port. */
934
+ function portOfEnd(node, end) {
935
+ return end.port === void 0 ? void 0 : node.ports?.find((p) => p.id === end.port);
936
+ }
937
+ /**
938
+ * This function gives the interval of a plug with a range, on the axis along its side, or `undefined` for a port without a range.
939
+ * The interval is the range of the port, without the corner insets of the side.
940
+ * When the range is all in a corner inset, the interval is the range.
941
+ */
942
+ function rangeInterval(box, side, port, inset = 6) {
943
+ const range = port?.range;
944
+ if (range === void 0) return void 0;
945
+ const along = alongAxis(side);
946
+ const start = along === "y" ? box.y : box.x;
947
+ const len = along === "y" ? box.h : box.w;
948
+ const m = Math.min(inset, len / 4);
949
+ const lo = start + range[0] * len;
950
+ const hi = start + range[1] * len;
951
+ const a = Math.max(lo, start + m);
952
+ const b = Math.min(hi, start + len - m);
953
+ return a <= b ? [a, b] : [lo, hi];
954
+ }
955
+ /** This function moves a point of a side into the interval of its range, on the axis along the side. A point of a port without a range stays. */
956
+ function clampToRange(box, side, port, point, inset = 6) {
957
+ const r = rangeInterval(box, side, port, inset);
958
+ if (!r) return point;
959
+ const along = alongAxis(side);
960
+ const v = Math.max(r[0], Math.min(r[1], point[along]));
961
+ return along === "y" ? {
962
+ x: point.x,
963
+ y: v
964
+ } : {
965
+ x: v,
966
+ y: point.y
967
+ };
968
+ }
969
+ /**
970
+ * This function places `ideal.length` plugs of one side in their given order. The plug `i` stays in the interval from `lo[i]` to `hi[i]`.
971
+ * Two neighbours are at least `gap` apart. When the intervals are too short for the gap, the gap shrinks evenly to the largest gap that they let.
972
+ * The sum of the squares of the moves from the ideal places is the least.
973
+ *
974
+ * When the order cannot keep each plug in its interval, the plugs take the order of their ideal places in their intervals. Thus the intervals win over the order.
975
+ * The result is in the order of `ideal`.
976
+ */
977
+ function placeInIntervals(ideal, lo, hi, gap) {
978
+ const k = ideal.length;
979
+ if (k === 0) return [];
980
+ let order = ideal.map((_, i) => i);
981
+ let most = -Infinity;
982
+ let broken = false;
983
+ for (const i of order) {
984
+ if (hi[i] < most - EPS$9) broken = true;
985
+ most = Math.max(most, lo[i]);
986
+ }
987
+ if (broken) {
988
+ const c = order.map((i) => Math.max(lo[i], Math.min(hi[i], ideal[i])));
989
+ order = [...order].sort((a, b) => c[a] - c[b] || a - b);
990
+ }
991
+ let s = Math.max(0, gap);
992
+ for (let i = 1; i < k; i++) {
993
+ const hiI = hi[order[i]];
994
+ for (let j = 0; j < i; j++) s = Math.min(s, (hiI - lo[order[j]]) / (i - j));
995
+ }
996
+ s = Math.max(0, s);
997
+ const lower = new Array(k);
998
+ const upper = new Array(k);
999
+ let run = -Infinity;
1000
+ for (let i = 0; i < k; i++) lower[i] = run = Math.max(run, lo[order[i]] - i * s);
1001
+ run = Infinity;
1002
+ for (let i = k - 1; i >= 0; i--) upper[i] = run = Math.min(run, hi[order[i]] - i * s);
1003
+ const sum = [];
1004
+ const size = [];
1005
+ const blockLo = [];
1006
+ const blockHi = [];
1007
+ const value = [];
1008
+ const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
1009
+ for (let i = 0; i < k; i++) {
1010
+ sum.push(ideal[order[i]] - i * s);
1011
+ size.push(1);
1012
+ blockLo.push(lower[i]);
1013
+ blockHi.push(upper[i]);
1014
+ value.push(clamp(sum[sum.length - 1], lower[i], upper[i]));
1015
+ while (value.length > 1 && value[value.length - 2] > value[value.length - 1]) {
1016
+ const t = value.length - 1;
1017
+ sum[t - 1] += sum[t];
1018
+ size[t - 1] += size[t];
1019
+ blockLo[t - 1] = blockLo[t];
1020
+ value[t - 1] = clamp(sum[t - 1] / size[t - 1], blockLo[t - 1], blockHi[t - 1]);
1021
+ sum.pop();
1022
+ size.pop();
1023
+ blockLo.pop();
1024
+ blockHi.pop();
1025
+ value.pop();
1026
+ }
1027
+ }
1028
+ const out = new Array(k);
1029
+ let i = 0;
1030
+ value.forEach((v, b) => {
1031
+ for (let j = 0; j < size[b]; j++, i++) out[order[i]] = v + i * s;
1032
+ });
1033
+ return out;
1034
+ }
916
1035
  // src/plugs.ts
917
1036
  /** The outward normal of each side, in a frame with the y axis down. */
918
1037
  const SIDE_NORMAL = {
@@ -983,7 +1102,7 @@ function pointOnSide(box, side, toward, inset = 6) {
983
1102
  }
984
1103
  /**
985
1104
  * This function places the plug of one end of an edge. A fixed plug stays at its point. A plug with a side stays on that side.
986
- * The router places a free plug on the side that faces `toward`.
1105
+ * The router places a free plug on the side that faces `toward`. A free plug of a port with a `range` stays in that range (`clampToRange`).
987
1106
  */
988
1107
  function placePlug(node, end, toward) {
989
1108
  const fixed = fixedPortPoint(node, end);
@@ -993,8 +1112,9 @@ function placePlug(node, end, toward) {
993
1112
  normal: SIDE_NORMAL[port?.side ?? nearestSide(node.box, fixed)]
994
1113
  };
995
1114
  const side = port?.side ?? sideToward(node.box, toward);
1115
+ const point = pointOnSide(node.box, side, toward);
996
1116
  return {
997
- point: pointOnSide(node.box, side, toward),
1117
+ point: port?.range === void 0 ? point : clampToRange(node.box, side, port, point),
998
1118
  normal: SIDE_NORMAL[side]
999
1119
  };
1000
1120
  }
@@ -4321,7 +4441,7 @@ const byDataGroup = (node) => {
4321
4441
  return typeof group === "string" ? group : void 0;
4322
4442
  };
4323
4443
  /** This function gives the bounding box of the boxes of the nodes of the list. */
4324
- function boundsOf(nodes, list) {
4444
+ function boundsOf$2(nodes, list) {
4325
4445
  let x0 = Infinity;
4326
4446
  let y0 = Infinity;
4327
4447
  let x1 = -Infinity;
@@ -4364,7 +4484,7 @@ function keyedSections(nodes, options = {}) {
4364
4484
  return {
4365
4485
  id: "s",
4366
4486
  depth: 0,
4367
- cell: boundsOf(nodes, nodes.map((_, i) => i)),
4487
+ cell: boundsOf$2(nodes, nodes.map((_, i) => i)),
4368
4488
  nodes: nodes.map((_, i) => i),
4369
4489
  children
4370
4490
  };
@@ -4374,7 +4494,7 @@ function group(nodes, id, list, split) {
4374
4494
  if (!split) return {
4375
4495
  id,
4376
4496
  depth: 1,
4377
- cell: boundsOf(nodes, list),
4497
+ cell: boundsOf$2(nodes, list),
4378
4498
  nodes: list,
4379
4499
  children: []
4380
4500
  };
@@ -4953,6 +5073,7 @@ function isotonic(v) {
4953
5073
  * This function moves the free plugs of each side of each node, so that two of them are at least `gap` apart (`justifyPlugs`).
4954
5074
  * The plugs keep their order on the side. Two plugs at one place take the order of their other ends. A fixed plug does not move.
4955
5075
  * A plug stays `inset` from the corners of its side, or a quarter of the side when the side is short.
5076
+ * A plug of a port with a `range` stays in its range (PORT-RANGE), and the plugs keep their order and the gap (`placeInIntervals`).
4956
5077
  */
4957
5078
  function spacePlugs(ends, gap, inset = 6) {
4958
5079
  const sides = /* @__PURE__ */ new Map();
@@ -4985,18 +5106,21 @@ function spacePlugs(ends, gap, inset = 6) {
4985
5106
  side.items.push([
4986
5107
  i,
4987
5108
  k,
4988
- other.point
5109
+ other.point,
5110
+ portOfEnd(node, end)
4989
5111
  ]);
4990
5112
  }
4991
5113
  });
4992
5114
  for (const { node, normal, items } of sides.values()) {
4993
- if (items.length < 2) continue;
5115
+ const ranged = items.some((x) => x[3]?.range !== void 0);
5116
+ if (items.length < 2 && !ranged) continue;
4994
5117
  const along = normal.x !== 0 ? "y" : "x";
4995
5118
  const b = node.box;
4996
5119
  const start = along === "y" ? b.y : b.x;
4997
5120
  const len = along === "y" ? b.h : b.w;
4998
5121
  const m = Math.min(inset, len / 4);
4999
- const placed = justifyPlugs(items.map(([i, k]) => ends[i][k].point[along]), start + m, start + len - m, gap, (j) => items[j][2][along]);
5122
+ const ideal = items.map(([i, k]) => ends[i][k].point[along]);
5123
+ const placed = ranged ? spaceInRanges(b, normal, items, ideal, start + m, start + len - m, gap, inset) : justifyPlugs(ideal, start + m, start + len - m, gap, (j) => items[j][2][along]);
5000
5124
  items.forEach(([i, k], j) => {
5001
5125
  const old = ends[i][k];
5002
5126
  ends[i][k] = {
@@ -5013,6 +5137,28 @@ function spacePlugs(ends, gap, inset = 6) {
5013
5137
  }
5014
5138
  }
5015
5139
  /**
5140
+ * This function places the free plugs of one side with ranges (PORT-RANGE). The order is the order of `justifyPlugs`.
5141
+ * Each plug stays in the interval of its range, or in the room of the side without a range.
5142
+ */
5143
+ function spaceInRanges(box, normal, items, ideal, lo, hi, gap, inset) {
5144
+ const side = sideOfNormal(normal);
5145
+ const along = alongAxis(side);
5146
+ const order = ideal.map((_, i) => i).sort((a, b) => ideal[a] - ideal[b] || items[a][2][along] - items[b][2][along] || a - b);
5147
+ const span = order.map((j) => rangeInterval(box, side, items[j][3], inset) ?? [lo, hi]);
5148
+ const at = placeInIntervals(order.map((j) => ideal[j]), span.map((r) => r[0]), span.map((r) => r[1]), gap);
5149
+ const out = new Array(ideal.length);
5150
+ order.forEach((j, i) => {
5151
+ out[j] = at[i];
5152
+ });
5153
+ return out;
5154
+ }
5155
+ /** This function gives the side of an outward normal. */
5156
+ function sideOfNormal(n) {
5157
+ if (n.x > 0) return "east";
5158
+ if (n.x < 0) return "west";
5159
+ return n.y > 0 ? "south" : "north";
5160
+ }
5161
+ /**
5016
5162
  * This function gives the plugs of the plug strategy `justify` (WILD-002): the plugs of `facing`, then the gaps of `spacePlugs`.
5017
5163
  * The router `corners` with the option `plugGap` gives the same plugs.
5018
5164
  */
@@ -5048,7 +5194,8 @@ function justifyEnds(input, options = {}) {
5048
5194
  /** This function gives the free side of an end, or `undefined` when the plug of the end is fixed. */
5049
5195
  function freeSide(node, end, toward, inset) {
5050
5196
  if (fixedPortPoint(node, end)) return void 0;
5051
- const side = (end.port === void 0 ? void 0 : node.ports?.find((p) => p.id === end.port))?.side ?? sideToward(node.box, toward);
5197
+ const port = end.port === void 0 ? void 0 : node.ports?.find((p) => p.id === end.port);
5198
+ const side = port?.side ?? sideToward(node.box, toward);
5052
5199
  const b = node.box;
5053
5200
  const along = side === "east" || side === "west" ? "y" : "x";
5054
5201
  const start = along === "y" ? b.y : b.x;
@@ -5056,6 +5203,23 @@ function freeSide(node, end, toward, inset) {
5056
5203
  const m = Math.min(inset, len / 4);
5057
5204
  const at = side === "east" ? b.x + b.w : side === "west" ? b.x : side === "south" ? b.y + b.h : b.y;
5058
5205
  const point = pointOnSide(b, side, toward, inset);
5206
+ const range = port?.range === void 0 ? void 0 : rangeInterval(b, side, port, inset);
5207
+ if (range) {
5208
+ const facing = clampToRange(b, side, port, point, inset);
5209
+ return {
5210
+ node,
5211
+ side,
5212
+ normal: SIDE_NORMAL[side],
5213
+ along,
5214
+ at,
5215
+ lo: range[0],
5216
+ hi: range[1],
5217
+ facing: {
5218
+ point: facing,
5219
+ normal: SIDE_NORMAL[side]
5220
+ }
5221
+ };
5222
+ }
5059
5223
  return {
5060
5224
  node,
5061
5225
  side,
@@ -5215,7 +5379,7 @@ function sideEnds(input, options = {}) {
5215
5379
  // src/routers/gutter.ts
5216
5380
  /** The default cost of one bend of the gutter mode. */
5217
5381
  const GUTTER_BEND_COST = 16;
5218
- const EPS$6 = 1e-6;
5382
+ const EPS$8 = 1e-6;
5219
5383
  /** The directions of a step: east, south, west and north, with the y axis down. */
5220
5384
  const DX = [
5221
5385
  1,
@@ -5239,7 +5403,7 @@ const DY = [
5239
5403
  */
5240
5404
  function gutterRoutes(frame, nodes, edges, options = {}) {
5241
5405
  const bend = options.bendCost ?? 16;
5242
- const boxes = nodes.map((n) => n.box).filter((b) => b.w > EPS$6 && b.h > EPS$6);
5406
+ const boxes = nodes.map((n) => n.box).filter((b) => b.w > EPS$8 && b.h > EPS$8);
5243
5407
  const entries = edges.map(entriesOf);
5244
5408
  const xs = /* @__PURE__ */ new Set();
5245
5409
  const ys = /* @__PURE__ */ new Set();
@@ -5257,7 +5421,7 @@ function gutterRoutes(frame, nodes, edges, options = {}) {
5257
5421
  const cuts = [lo, lo + size];
5258
5422
  for (const b of boxes) for (const v of key === "x" ? [b.x, b.x + b.w] : [b.y, b.y + b.h]) if (v > lo && v < lo + size) cuts.push(v);
5259
5423
  cuts.sort((a, b) => a - b);
5260
- for (let i = 1; i < cuts.length; i++) if (cuts[i] - cuts[i - 1] > EPS$6) lines.add((cuts[i] + cuts[i - 1]) / 2);
5424
+ for (let i = 1; i < cuts.length; i++) if (cuts[i] - cuts[i - 1] > EPS$8) lines.add((cuts[i] + cuts[i - 1]) / 2);
5261
5425
  }
5262
5426
  edges.forEach((e, i) => {
5263
5427
  for (const { point, normal } of [{
@@ -5320,19 +5484,19 @@ function stubOnly(e, boxes) {
5320
5484
  const n = e.plug.normal;
5321
5485
  let q;
5322
5486
  if (n.x !== 0) {
5323
- if (!(p.y > b.y + EPS$6 && p.y < b.y + b.h - EPS$6)) return void 0;
5487
+ if (!(p.y > b.y + EPS$8 && p.y < b.y + b.h - EPS$8)) return void 0;
5324
5488
  q = {
5325
5489
  x: n.x > 0 ? b.x + b.w : b.x,
5326
5490
  y: p.y
5327
5491
  };
5328
- if ((q.x - p.x) * n.x > EPS$6) return void 0;
5492
+ if ((q.x - p.x) * n.x > EPS$8) return void 0;
5329
5493
  } else {
5330
- if (!(p.x > b.x + EPS$6 && p.x < b.x + b.w - EPS$6)) return void 0;
5494
+ if (!(p.x > b.x + EPS$8 && p.x < b.x + b.w - EPS$8)) return void 0;
5331
5495
  q = {
5332
5496
  x: p.x,
5333
5497
  y: n.y > 0 ? b.y + b.h : b.y
5334
5498
  };
5335
- if ((q.y - p.y) * n.y > EPS$6) return void 0;
5499
+ if ((q.y - p.y) * n.y > EPS$8) return void 0;
5336
5500
  }
5337
5501
  for (const o of boxes) if (o !== b && blocks(p, q, o)) return void 0;
5338
5502
  return [p, q];
@@ -5340,11 +5504,11 @@ function stubOnly(e, boxes) {
5340
5504
  /** This function tells if the axis-parallel segment `ab` meets the closed box `o` along a part longer than a tiny length. A segment along a side meets the box. */
5341
5505
  function blocks(a, b, o) {
5342
5506
  if (a.y === b.y) {
5343
- if (a.y < o.y - EPS$6 || a.y > o.y + o.h + EPS$6) return false;
5344
- return Math.min(Math.max(a.x, b.x), o.x + o.w) - Math.max(Math.min(a.x, b.x), o.x) > EPS$6;
5507
+ if (a.y < o.y - EPS$8 || a.y > o.y + o.h + EPS$8) return false;
5508
+ return Math.min(Math.max(a.x, b.x), o.x + o.w) - Math.max(Math.min(a.x, b.x), o.x) > EPS$8;
5345
5509
  }
5346
- if (a.x < o.x - EPS$6 || a.x > o.x + o.w + EPS$6) return false;
5347
- return Math.min(Math.max(a.y, b.y), o.y + o.h) - Math.max(Math.min(a.y, b.y), o.y) > EPS$6;
5510
+ if (a.x < o.x - EPS$8 || a.x > o.x + o.w + EPS$8) return false;
5511
+ return Math.min(Math.max(a.y, b.y), o.y + o.h) - Math.max(Math.min(a.y, b.y), o.y) > EPS$8;
5348
5512
  }
5349
5513
  /** This function gives the direction index of a unit normal: 0 east, 1 south, 2 west, 3 north. */
5350
5514
  function dirOf(n) {
@@ -5363,12 +5527,12 @@ var Grid$1 = class {
5363
5527
  this.#free = new Uint8Array(xs.length * ys.length);
5364
5528
  for (let i = 0; i < ys.length; i++) {
5365
5529
  const y = ys[i];
5366
- if (y <= frame.y + EPS$6 || y >= frame.y + frame.h - EPS$6) continue;
5530
+ if (y <= frame.y + EPS$8 || y >= frame.y + frame.h - EPS$8) continue;
5367
5531
  for (let j = 0; j < xs.length; j++) {
5368
5532
  const x = xs[j];
5369
- if (x <= frame.x + EPS$6 || x >= frame.x + frame.w - EPS$6) continue;
5533
+ if (x <= frame.x + EPS$8 || x >= frame.x + frame.w - EPS$8) continue;
5370
5534
  let free = true;
5371
- for (const b of boxes) if (x >= b.x - EPS$6 && x <= b.x + b.w + EPS$6 && y >= b.y - EPS$6 && y <= b.y + b.h + EPS$6) {
5535
+ for (const b of boxes) if (x >= b.x - EPS$8 && x <= b.x + b.w + EPS$8 && y >= b.y - EPS$8 && y <= b.y + b.h + EPS$8) {
5372
5536
  free = false;
5373
5537
  break;
5374
5538
  }
@@ -5405,11 +5569,11 @@ var Grid$1 = class {
5405
5569
  const sign = d < 2 ? 1 : -1;
5406
5570
  let k = -1;
5407
5571
  if (sign > 0) {
5408
- for (let m = 0; m < list.length; m++) if (list[m] > v + EPS$6) {
5572
+ for (let m = 0; m < list.length; m++) if (list[m] > v + EPS$8) {
5409
5573
  k = m;
5410
5574
  break;
5411
5575
  }
5412
- } else for (let m = list.length - 1; m >= 0; m--) if (list[m] < v - EPS$6) {
5576
+ } else for (let m = list.length - 1; m >= 0; m--) if (list[m] < v - EPS$8) {
5413
5577
  k = m;
5414
5578
  break;
5415
5579
  }
@@ -5608,7 +5772,7 @@ function lanes(frame, boxes, edges, entries, routes, laneGap) {
5608
5772
  const b = legs[j];
5609
5773
  if (a.route === b.route || a.horizontal !== b.horizontal) continue;
5610
5774
  if (Math.min(a.s1, b.s1) - Math.max(a.s0, b.s0) <= -laneGap) continue;
5611
- if (a.across < b.lo - EPS$6 || a.across > b.hi + EPS$6 || b.across < a.lo - EPS$6 || b.across > a.hi + EPS$6) continue;
5775
+ if (a.across < b.lo - EPS$8 || a.across > b.hi + EPS$8 || b.across < a.lo - EPS$8 || b.across > a.hi + EPS$8) continue;
5612
5776
  parent[root(i)] = root(j);
5613
5777
  }
5614
5778
  const groups = /* @__PURE__ */ new Map();
@@ -5648,9 +5812,9 @@ function free(frame, boxes, horizontal, across, s0, s1) {
5648
5812
  b.x,
5649
5813
  b.w
5650
5814
  ];
5651
- if (Math.min(s1, bs + bl) - Math.max(s0, bs) <= EPS$6) continue;
5652
- if (cs + cl <= across + EPS$6) lo = Math.max(lo, cs + cl);
5653
- if (cs >= across - EPS$6) hi = Math.min(hi, cs);
5815
+ if (Math.min(s1, bs + bl) - Math.max(s0, bs) <= EPS$8) continue;
5816
+ if (cs + cl <= across + EPS$8) lo = Math.max(lo, cs + cl);
5817
+ if (cs >= across - EPS$8) hi = Math.min(hi, cs);
5654
5818
  }
5655
5819
  return [lo, hi];
5656
5820
  }
@@ -5661,8 +5825,8 @@ function want(a, b) {
5661
5825
  if (inner(sb, sa)) return -tb;
5662
5826
  return ta === tb ? 0 : Math.sign(ta - tb);
5663
5827
  };
5664
- const left = side(a.s0, b.s0, a.t0, b.t0, (x, y) => x > y + EPS$6);
5665
- const right = side(a.s1, b.s1, a.t1, b.t1, (x, y) => x < y - EPS$6);
5828
+ const left = side(a.s0, b.s0, a.t0, b.t0, (x, y) => x > y + EPS$8);
5829
+ const right = side(a.s1, b.s1, a.t1, b.t1, (x, y) => x < y - EPS$8);
5666
5830
  if (left !== 0 && right !== 0 && left !== right) return left;
5667
5831
  return left || right;
5668
5832
  }
@@ -5680,7 +5844,7 @@ function place(group, laneGap) {
5680
5844
  const a = group[i];
5681
5845
  const b = group[j];
5682
5846
  if (a.pinned && b.pinned) {
5683
- if (Math.abs(a.across - b.across) <= EPS$6) continue;
5847
+ if (Math.abs(a.across - b.across) <= EPS$8) continue;
5684
5848
  const [x, y] = a.across < b.across ? [i, j] : [j, i];
5685
5849
  hard[x].push(y);
5686
5850
  hardIn[y]++;
@@ -5698,7 +5862,7 @@ function place(group, laneGap) {
5698
5862
  let best = -1;
5699
5863
  for (let i = 0; i < n; i++) {
5700
5864
  if (done[i] || hardIn[i] > 0) continue;
5701
- if (best < 0 || softIn[i] < softIn[best] || softIn[i] === softIn[best] && group[i].across < group[best].across - EPS$6) best = i;
5865
+ if (best < 0 || softIn[i] < softIn[best] || softIn[i] === softIn[best] && group[i].across < group[best].across - EPS$8) best = i;
5702
5866
  }
5703
5867
  done[best] = true;
5704
5868
  order.push(best);
@@ -5711,7 +5875,7 @@ function place(group, laneGap) {
5711
5875
  let to = k + 1;
5712
5876
  for (let m = k + 1; m < order.length; m++) {
5713
5877
  const b = group[order[m]];
5714
- if (b.pinned && Math.abs(b.across - a.across) <= EPS$6) order.splice(to++, 0, ...order.splice(m, 1));
5878
+ if (b.pinned && Math.abs(b.across - a.across) <= EPS$8) order.splice(to++, 0, ...order.splice(m, 1));
5715
5879
  }
5716
5880
  k = to - 1;
5717
5881
  }
@@ -5731,7 +5895,7 @@ function place(group, laneGap) {
5731
5895
  const width = (run.length - 1) * s;
5732
5896
  const mean = run.reduce((t, r) => t + group[r].across, 0) / run.length;
5733
5897
  const first = Math.min(Math.max(mean - width / 2, lo + s), hi - s - width);
5734
- run.forEach((r, m) => at[r] = room > EPS$6 && s > 0 ? first + m * s : group[r].across);
5898
+ run.forEach((r, m) => at[r] = room > EPS$8 && s > 0 ? first + m * s : group[r].across);
5735
5899
  }
5736
5900
  if (k < n) at[i] = group[i].across;
5737
5901
  start = k + 1;
@@ -5741,7 +5905,7 @@ function place(group, laneGap) {
5741
5905
  }
5742
5906
  // src/routers/frames.ts
5743
5907
  /** The tiny gap between the bounds and the band of a wall, so that a way along the bounds stays clear of the wall. */
5744
- const EPS$5 = 1e-6;
5908
+ const EPS$7 = 1e-6;
5745
5909
  /**
5746
5910
  * This function gives the inner routes of the inner edges (`splitFrames`). Each route stays in the bounds of its frame:
5747
5911
  * the box of the frame, smaller by `frameInset`. It goes around the other nodes of the frame.
@@ -5866,7 +6030,7 @@ function makeRoom$1(f, chain, children, byId, inset, options) {
5866
6030
  const margin = options.margin ?? 5;
5867
6031
  const bounds = shrink(f.box, inset);
5868
6032
  const nodes = roomNodes(f, chain, children, byId, bounds, options);
5869
- const w = shrink(bounds, -(margin / 2 + EPS$5));
6033
+ const w = shrink(bounds, -(margin / 2 + EPS$7));
5870
6034
  const t = 2 * margin + 1;
5871
6035
  const walls = [
5872
6036
  {
@@ -6225,14 +6389,14 @@ const LEVEL_LINK_DEFAULTS = Object.freeze({
6225
6389
  plugInset: 6,
6226
6390
  throughCost: 8
6227
6391
  });
6228
- const EPS$4 = 1e-6;
6392
+ const EPS$6 = 1e-6;
6229
6393
  /**
6230
6394
  * This function removes the repeated points, the points in the middle of a straight run,
6231
6395
  * and the points where the way turns back on itself.
6232
6396
  * A way of two points or more keeps two points: its first point and its last point.
6233
6397
  * Two items that touch can get a link of zero length, and such a link keeps an end on each item.
6234
6398
  */
6235
- function tidy(p) {
6399
+ function tidy$1(p) {
6236
6400
  const out = tidyPoints(p);
6237
6401
  return out.length === 1 && p.length > 1 ? [p[0], p[p.length - 1]] : out;
6238
6402
  }
@@ -6240,15 +6404,15 @@ function tidy(p) {
6240
6404
  function tidyPoints(p) {
6241
6405
  const out = [];
6242
6406
  for (const q of p) {
6243
- if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS$4) continue;
6407
+ if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS$6) continue;
6244
6408
  while (out.length >= 2) {
6245
6409
  const a = out[out.length - 2];
6246
6410
  const b = out[out.length - 1];
6247
6411
  const cross = (b.x - a.x) * (q.y - b.y) - (b.y - a.y) * (q.x - b.x);
6248
- if (Math.abs(cross) > EPS$4 * (1 + dist(a, b) * dist(b, q))) break;
6412
+ if (Math.abs(cross) > EPS$6 * (1 + dist(a, b) * dist(b, q))) break;
6249
6413
  out.pop();
6250
6414
  }
6251
- if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS$4) continue;
6415
+ if (out.length > 0 && dist(q, out[out.length - 1]) <= EPS$6) continue;
6252
6416
  out.push(q);
6253
6417
  }
6254
6418
  return out;
@@ -6390,7 +6554,7 @@ function fewerFar(ways, s, t, way, cost, limit) {
6390
6554
  const far = cost * ways.size;
6391
6555
  const clipped = clip(ways.soft().path(center(s.box), center(t.box), (n) => n === s || n === t ? 0 : near.has(n) ? ways.size : far), s.box, t.box);
6392
6556
  if (!clipped) return way;
6393
- const soft = tidy(pullThrough(ways, clipped, s, t));
6557
+ const soft = tidy$1(pullThrough(ways, clipped, s, t));
6394
6558
  if (goesOutAndBack(soft) && !goesOutAndBack(way)) return way;
6395
6559
  if (detour(soft) > limit) return way;
6396
6560
  return boxesThrough(ways, soft, s, t, near).far < before.far ? soft : way;
@@ -6621,7 +6785,7 @@ function freeSpans(lo, hi, covered) {
6621
6785
  if (x >= hi) break;
6622
6786
  }
6623
6787
  if (x < hi) out.push([x, hi]);
6624
- return out.filter(([a, b]) => b - a > EPS$4);
6788
+ return out.filter(([a, b]) => b - a > EPS$6);
6625
6789
  }
6626
6790
  /**
6627
6791
  * This function gives the shortest way between the free plugs of the two nodes, or `undefined` when the second corner graph has none.
@@ -6707,7 +6871,7 @@ function quickWay(ways, e) {
6707
6871
  const near = Math.min(dist(e.ps.point, e.a), dist(e.pt.point, e.b));
6708
6872
  const pad = Math.max(0, Math.min(ways.margin / 2, near - 1));
6709
6873
  const legs = square(e.a, e.b, ways.obstacles, pad, axisOf(e.ps.normal), axisOf(e.pt.normal));
6710
- return legs && tidy([
6874
+ return legs && tidy$1([
6711
6875
  e.ps.point,
6712
6876
  e.a,
6713
6877
  ...legs,
@@ -6720,7 +6884,7 @@ function quickWay(ways, e) {
6720
6884
  */
6721
6885
  function pulled(ways, ps, path, pt) {
6722
6886
  const legs = squarePull(path, ways.obstacles, ways.margin / 2, axisOf(ps.normal), axisOf(pt.normal));
6723
- return tidy([
6887
+ return tidy$1([
6724
6888
  ps.point,
6725
6889
  ...legs,
6726
6890
  pt.point
@@ -6753,7 +6917,7 @@ function squarePull(p, obstacles, pad, first, last, lookahead = 16) {
6753
6917
  out.push(...step.legs);
6754
6918
  const end = out[out.length - 1];
6755
6919
  const before = out[out.length - 2];
6756
- axis = Math.abs(end.y - before.y) < EPS$4 ? "x" : Math.abs(end.x - before.x) < EPS$4 ? "y" : void 0;
6920
+ axis = Math.abs(end.y - before.y) < EPS$6 ? "x" : Math.abs(end.x - before.x) < EPS$6 ? "y" : void 0;
6757
6921
  i = step.j;
6758
6922
  }
6759
6923
  return out;
@@ -6763,7 +6927,7 @@ function squarePull(p, obstacles, pad, first, last, lookahead = 16) {
6763
6927
  * Of two clear corners, it picks the corner that continues the axis `from` and arrives along the axis `to`.
6764
6928
  */
6765
6929
  function square(a, b, obstacles, pad, from, to) {
6766
- if (Math.abs(a.x - b.x) < EPS$4 || Math.abs(a.y - b.y) < EPS$4) return obstacles.clear(a, b, pad) ? [b] : void 0;
6930
+ if (Math.abs(a.x - b.x) < EPS$6 || Math.abs(a.y - b.y) < EPS$6) return obstacles.clear(a, b, pad) ? [b] : void 0;
6767
6931
  const options = [{
6768
6932
  c: {
6769
6933
  x: b.x,
@@ -8154,7 +8318,8 @@ function sharedRouter(options, negotiation, hooks) {
8154
8318
  seq: way,
8155
8319
  ids: way.map((p, i) => i === 0 || i === way.length - 1 ? endId(p) : index.get(p))
8156
8320
  };
8157
- }
8321
+ },
8322
+ width: (i, j) => widthOf(i, j)
8158
8323
  };
8159
8324
  const sorted = orderWays(all, options.order ?? "span", graph);
8160
8325
  const order = hooks?.order ? hooks.order(sorted, search) : sorted;
@@ -8931,6 +9096,1020 @@ const annealingStrategy = defineStrategy({
8931
9096
  rule: byCollection
8932
9097
  }).route(graph)
8933
9098
  });
9099
+ // src/routers/canals/index.ts
9100
+ /** The default options of the router `canals`. */
9101
+ const CANAL_DEFAULTS = Object.freeze({
9102
+ mu: 2,
9103
+ steps: 60,
9104
+ decay: 1,
9105
+ minMembers: 3
9106
+ });
9107
+ /** The declarations of the options of the router `canals` (NAT-004). */
9108
+ const CANAL_SCHEMA = {
9109
+ mu: {
9110
+ type: "number",
9111
+ min: 1,
9112
+ max: 3,
9113
+ step: .5,
9114
+ marks: [
9115
+ 1,
9116
+ 2,
9117
+ 3
9118
+ ],
9119
+ default: 2,
9120
+ describe: "The exponent of the flux in the growth of a conductance. A value above 1 gives a tree."
9121
+ },
9122
+ steps: {
9123
+ type: "integer",
9124
+ min: 30,
9125
+ max: 150,
9126
+ step: 10,
9127
+ marks: [
9128
+ 30,
9129
+ 60,
9130
+ 150
9131
+ ],
9132
+ default: 60,
9133
+ describe: "The largest number of updates of the conductances."
9134
+ },
9135
+ decay: {
9136
+ type: "number",
9137
+ min: .5,
9138
+ max: 2,
9139
+ step: .25,
9140
+ marks: [
9141
+ .5,
9142
+ 1,
9143
+ 2
9144
+ ],
9145
+ default: 1,
9146
+ describe: "The decay of a conductance."
9147
+ },
9148
+ minMembers: {
9149
+ type: "integer",
9150
+ min: 2,
9151
+ max: 8,
9152
+ step: 1,
9153
+ marks: [
9154
+ 2,
9155
+ 3,
9156
+ 5,
9157
+ 8
9158
+ ],
9159
+ default: 3,
9160
+ describe: "A hub has this number of edges of one key or more."
9161
+ }
9162
+ };
9163
+ /** The time step of one update of the conductances. */
9164
+ const DT = .5;
9165
+ /** The gain of a conductance in the cost of a link: a link with the conductance `D` costs `d / (1 + GAIN × D)`. */
9166
+ const GAIN = 4;
9167
+ /** The cost share of a link that another way of the key uses, as the default `discount` of `shared`. */
9168
+ const SHARE = .5;
9169
+ /** The updates with no change of a way, after which the updates stop. */
9170
+ const PATIENCE = 3;
9171
+ /**
9172
+ * This function gives `x^mu` for `x` from 0 to 1 and `mu` in steps of 0.5, with only `*` and `Math.sqrt`.
9173
+ * These operations are exact to the last bit on each platform, so a decision does not depend on the engine.
9174
+ */
9175
+ function halfPower(x, mu) {
9176
+ const twice = Math.max(2, Math.min(6, Math.round(mu * 2)));
9177
+ let out = 1;
9178
+ for (let i = 0; i < Math.floor(twice / 2); i++) out *= x;
9179
+ return twice % 2 === 1 ? out * Math.sqrt(x) : out;
9180
+ }
9181
+ /**
9182
+ * This function gives the hubs of the ways. In each key, the node with the most ends is a hub when it has `minMembers` edges or more.
9183
+ * Its edges leave the key, and the next hub of the key comes from the edges that stay. A tie goes to the smaller node id.
9184
+ * An edge with no key is in no hub.
9185
+ */
9186
+ function canalHubs(ways, minMembers) {
9187
+ const byKey = /* @__PURE__ */ new Map();
9188
+ for (const w of ways) {
9189
+ if (!w.found || w.ids.length < 2 || w.key.startsWith("\0")) continue;
9190
+ const list = byKey.get(w.key);
9191
+ if (list) list.push(w);
9192
+ else byKey.set(w.key, [w]);
9193
+ }
9194
+ const out = [];
9195
+ for (const key of [...byKey.keys()].sort()) {
9196
+ let left = byKey.get(key);
9197
+ for (;;) {
9198
+ const count = /* @__PURE__ */ new Map();
9199
+ for (const w of left) for (const n of /* @__PURE__ */ new Set([w.edge.source.node, w.edge.target.node])) count.set(n, (count.get(n) ?? 0) + 1);
9200
+ let node;
9201
+ let most = 0;
9202
+ for (const [n, c] of count) if (c > most || c === most && node !== void 0 && n < node) [node, most] = [n, c];
9203
+ if (node === void 0 || most < Math.max(2, minMembers)) break;
9204
+ const hub = node;
9205
+ const members = left.filter((w) => w.edge.source.node === hub || w.edge.target.node === hub);
9206
+ out.push({
9207
+ node: hub,
9208
+ key,
9209
+ ways: [...members].sort((a, b) => a.edge.id < b.edge.id ? -1 : a.edge.id > b.edge.id ? 1 : 0)
9210
+ });
9211
+ left = left.filter((w) => !members.includes(w));
9212
+ }
9213
+ }
9214
+ return out;
9215
+ }
9216
+ /**
9217
+ * This function grows a feeder tree to each hub (NAT-004, after the canalization of Sachs 1981 and Mitchison 1980).
9218
+ * Each member edge is a source of one unit of flux, and the hub is the sink. The flux of a link is the share of the members whose ways use it.
9219
+ * At each update, each conductance changes by `DT × (flux^mu - decay × D)`, and each member finds its way again with the cost `d / (1 + GAIN × D)`.
9220
+ * A link that another way of the key uses costs half, as in `shared`.
9221
+ * With `mu` above 1, a link with more flux grows faster, so the ways gather into one tree. The updates stop after `steps`, or when no way changes for 3 updates.
9222
+ */
9223
+ function canalWays(ways, search, options = {}) {
9224
+ const o = {
9225
+ ...CANAL_DEFAULTS,
9226
+ ...options
9227
+ };
9228
+ const n = search.corners;
9229
+ const linksOf = (w) => {
9230
+ const out = /* @__PURE__ */ new Set();
9231
+ for (let i = 1; i < w.ids.length; i++) if (w.ids[i - 1] < n && w.ids[i] < n) out.add(search.link(w.ids[i - 1], w.ids[i]));
9232
+ return [...out];
9233
+ };
9234
+ const usage = /* @__PURE__ */ new Map();
9235
+ const use = (w, add) => {
9236
+ let m = usage.get(w.key);
9237
+ if (!m) usage.set(w.key, m = /* @__PURE__ */ new Map());
9238
+ for (const k of linksOf(w)) m.set(k, (m.get(k) ?? 0) + add);
9239
+ };
9240
+ for (const w of ways) if (w.found) use(w, 1);
9241
+ for (const hub of canalHubs(ways, Math.floor(o.minMembers))) {
9242
+ const members = hub.ways;
9243
+ const used = usage.get(hub.key);
9244
+ const conductance = /* @__PURE__ */ new Map();
9245
+ let calm = 0;
9246
+ for (let step = 0; step < Math.max(1, Math.floor(o.steps)); step++) {
9247
+ const flux = /* @__PURE__ */ new Map();
9248
+ for (const w of members) {
9249
+ const seen = /* @__PURE__ */ new Set();
9250
+ for (let i = 1; i < w.ids.length; i++) {
9251
+ const a = w.ids[i - 1];
9252
+ const b = w.ids[i];
9253
+ if (a >= n || b >= n) continue;
9254
+ const k = search.link(a, b);
9255
+ if (seen.has(k)) continue;
9256
+ seen.add(k);
9257
+ flux.set(k, (flux.get(k) ?? 0) + 1);
9258
+ }
9259
+ }
9260
+ for (const k of /* @__PURE__ */ new Set([...conductance.keys(), ...flux.keys()])) {
9261
+ const phi = (flux.get(k) ?? 0) / members.length;
9262
+ const d = conductance.get(k) ?? 0;
9263
+ const next = d + DT * (halfPower(phi, o.mu) - o.decay * d);
9264
+ if (next > 1e-6) conductance.set(k, next);
9265
+ else conductance.delete(k);
9266
+ }
9267
+ let top = 0;
9268
+ for (const d of conductance.values()) top = Math.max(top, d);
9269
+ const floor = SHARE / (1 + GAIN * top);
9270
+ let changed = 0;
9271
+ for (const w of members) {
9272
+ use(w, -1);
9273
+ const next = search.find(w, (i, j, d) => {
9274
+ const k = search.link(i, j);
9275
+ return ((used.get(k) ?? 0) > 0 ? SHARE * d : d) / (1 + GAIN * (conductance.get(k) ?? 0));
9276
+ }, floor);
9277
+ if (next) {
9278
+ if (next.ids.length !== w.ids.length || next.ids.some((id, i) => id !== w.ids[i])) changed++;
9279
+ w.seq = next.seq;
9280
+ w.ids = next.ids;
9281
+ }
9282
+ use(w, 1);
9283
+ }
9284
+ calm = changed === 0 ? calm + 1 : 0;
9285
+ if (calm >= PATIENCE) break;
9286
+ }
9287
+ }
9288
+ }
9289
+ /**
9290
+ * This function makes the router `canals` (NAT-004). It is the router `shared`, then the feeder trees of `canalWays` to the hubs of each key.
9291
+ * The runs and the bundles come from the ways after the trees.
9292
+ */
9293
+ function canalRouter(options) {
9294
+ const o = {
9295
+ ...CANAL_DEFAULTS,
9296
+ ...options
9297
+ };
9298
+ return sharedRouter({
9299
+ name: o.name,
9300
+ ...o.rule === void 0 ? {} : { rule: o.rule },
9301
+ ...o.maxNodes === void 0 ? {} : { maxNodes: o.maxNodes }
9302
+ }, void 0, { refine: (ways, search) => canalWays(ways, search, o) });
9303
+ }
9304
+ /**
9305
+ * The router strategy `canals` (NAT-004), with the rule `byCollection` of `shared`. It is not a built-in router, so the benchmark and the hashes of `ROUTERS` do not change.
9306
+ * A screen module or a consumer adds it to a registry.
9307
+ */
9308
+ const canalStrategy = defineStrategy({
9309
+ step: "router",
9310
+ name: "canals",
9311
+ describe: "Canalization feeder trees (NAT-004). The members of a hub are sources of flux, and the links with more flux get cheaper until the ways make one tree.",
9312
+ defaults: { ...CANAL_DEFAULTS },
9313
+ schema: CANAL_SCHEMA,
9314
+ maxNodes: 1e4,
9315
+ run: (graph, o) => canalRouter({
9316
+ ...o,
9317
+ name: "canals",
9318
+ rule: byCollection
9319
+ }).route(graph)
9320
+ });
9321
+ // src/bench/quality.ts
9322
+ /** The clearance stops at this distance. */
9323
+ const CLEARANCE_CAP = 40;
9324
+ const TIGHT = 4;
9325
+ const EXACT_CROSSINGS$1 = 2e4;
9326
+ const SAMPLE$1 = 2e3;
9327
+ const key$1 = (p) => `${Math.round(p.x * 8)},${Math.round(p.y * 8)}`;
9328
+ /** This function measures the quality of a result. */
9329
+ function measureQuality(graph, result) {
9330
+ const byId = nodeIndex(graph);
9331
+ const nodes = new Grid(180);
9332
+ for (const n of graph.nodes) nodes.add(n, n.box);
9333
+ const segs = [];
9334
+ const seen = /* @__PURE__ */ new Set();
9335
+ let bends = 0;
9336
+ let infl = 0;
9337
+ let clear = 0;
9338
+ let tight = 0;
9339
+ let detour = 0;
9340
+ let detourN = 0;
9341
+ let angle = 0;
9342
+ let angleN = 0;
9343
+ for (const r of result.routes) {
9344
+ const p = r.points;
9345
+ for (let i = 1; i < p.length; i++) {
9346
+ const ka = key$1(p[i - 1]);
9347
+ const kb = key$1(p[i]);
9348
+ if (ka === kb) continue;
9349
+ const k = ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
9350
+ if (seen.has(k)) continue;
9351
+ seen.add(k);
9352
+ segs.push([p[i - 1], p[i]]);
9353
+ }
9354
+ bends += bendCount(p);
9355
+ infl += inflectionCount(p);
9356
+ const ends = endNodes(graph, byId, r.edge);
9357
+ let c = 40;
9358
+ for (let i = 1; i < p.length; i++) {
9359
+ const a = p[i - 1];
9360
+ const b = p[i];
9361
+ nodes.querySegment(a, b, c, (n) => {
9362
+ if (n !== ends.source && n !== ends.target) c = Math.min(c, segmentBoxDistance(a, b, n.box));
9363
+ });
9364
+ }
9365
+ clear += c;
9366
+ if (c < TIGHT) tight++;
9367
+ if (p.length >= 2) {
9368
+ const d = dist(p[0], p[p.length - 1]);
9369
+ if (d > 1) {
9370
+ detour += polylineLength(p) / d;
9371
+ detourN++;
9372
+ }
9373
+ for (const [at, next, node] of [[
9374
+ p[0],
9375
+ p[1],
9376
+ ends.source
9377
+ ], [
9378
+ p[p.length - 1],
9379
+ p[p.length - 2],
9380
+ ends.target
9381
+ ]]) {
9382
+ if (!node) continue;
9383
+ const a = sideAngle(node.box, at, next);
9384
+ if (a !== void 0) {
9385
+ angle += a;
9386
+ angleN++;
9387
+ }
9388
+ }
9389
+ }
9390
+ }
9391
+ const n = Math.max(1, result.routes.length);
9392
+ let ink = 0;
9393
+ for (const [a, b] of segs) ink += dist(a, b);
9394
+ const [crossings, crossingsEstimated] = countCrossings(segs);
9395
+ return {
9396
+ ink,
9397
+ crossings,
9398
+ crossingsEstimated,
9399
+ bends: bends / n,
9400
+ inflections: infl / n,
9401
+ clearance: clear / n,
9402
+ tight: tight / n,
9403
+ detour: detourN ? detour / detourN : 1,
9404
+ plugAngle: angleN ? angle / angleN : 0
9405
+ };
9406
+ }
9407
+ function endNodes(graph, byId, edgeId) {
9408
+ const e = edgeIndex(graph).get(edgeId);
9409
+ return {
9410
+ source: e && byId.get(e.source.node),
9411
+ target: e && byId.get(e.target.node)
9412
+ };
9413
+ }
9414
+ const edgeIndexes = /* @__PURE__ */ new WeakMap();
9415
+ function edgeIndex(graph) {
9416
+ let m = edgeIndexes.get(graph);
9417
+ if (!m) {
9418
+ m = /* @__PURE__ */ new Map();
9419
+ for (const e of graph.edges) m.set(e.id, e);
9420
+ edgeIndexes.set(graph, m);
9421
+ }
9422
+ return m;
9423
+ }
9424
+ /**
9425
+ * This function gives an angle in degrees: the angle between the segment from `at` to `next` and the outward normal.
9426
+ * The normal is the normal of the side of the box nearest to `at`. A route that starts inside the box (an interior route) gives no angle.
9427
+ */
9428
+ function sideAngle(box, at, next) {
9429
+ const d = [
9430
+ [
9431
+ Math.abs(at.y - box.y),
9432
+ 0,
9433
+ -1
9434
+ ],
9435
+ [
9436
+ Math.abs(at.x - (box.x + box.w)),
9437
+ 1,
9438
+ 0
9439
+ ],
9440
+ [
9441
+ Math.abs(at.y - (box.y + box.h)),
9442
+ 0,
9443
+ 1
9444
+ ],
9445
+ [
9446
+ Math.abs(at.x - box.x),
9447
+ -1,
9448
+ 0
9449
+ ]
9450
+ ].sort((a, b) => a[0] - b[0])[0];
9451
+ if (d[0] > 1) return void 0;
9452
+ const vx = next.x - at.x;
9453
+ const vy = next.y - at.y;
9454
+ const l = Math.hypot(vx, vy);
9455
+ if (l === 0) return void 0;
9456
+ const cos = Math.max(-1, Math.min(1, (vx * d[1] + vy * d[2]) / l));
9457
+ return Math.acos(cos) * 180 / Math.PI;
9458
+ }
9459
+ /**
9460
+ * This function counts the crossings of a set of segments with a grid of buckets.
9461
+ * Above 20 000 segments, it counts the crossings of a seeded sample of 2 000 segments and scales the count.
9462
+ */
9463
+ function countCrossings(segs) {
9464
+ if (segs.length < 2) return [0, false];
9465
+ const len = segs.reduce((s, [a, b]) => s + dist(a, b), 0) / segs.length;
9466
+ const grid = new Grid(Math.max(20, len));
9467
+ segs.forEach(([a, b], i) => grid.addSegment(i, a, b));
9468
+ const exact = segs.length <= EXACT_CROSSINGS$1;
9469
+ const R = rng(segs.length);
9470
+ const pick = exact ? segs.map((_, i) => i) : Array.from({ length: SAMPLE$1 }, () => Math.floor(R() * segs.length));
9471
+ let count = 0;
9472
+ for (const i of pick) {
9473
+ const [a, b] = segs[i];
9474
+ grid.querySegment(a, b, 0, (j) => {
9475
+ if (exact && j <= i) return;
9476
+ if (j === i) return;
9477
+ const [c, d] = segs[j];
9478
+ if (segmentsCross(a, b, c, d)) count++;
9479
+ });
9480
+ }
9481
+ return exact ? [count, false] : [Math.round(count * segs.length / (2 * pick.length)), true];
9482
+ }
9483
+ /** This function measures how much the routes changed between two results. */
9484
+ function measureStability(before, after) {
9485
+ const prev = new Map(before.routes.map((r) => [r.edge, r.points]));
9486
+ let sum = 0;
9487
+ let max = 0;
9488
+ let moved = 0;
9489
+ let n = 0;
9490
+ for (const r of after.routes) {
9491
+ const p = prev.get(r.edge);
9492
+ if (!p) continue;
9493
+ const f = frechet(p, r.points);
9494
+ sum += f;
9495
+ max = Math.max(max, f);
9496
+ if (f > 1) moved++;
9497
+ n++;
9498
+ }
9499
+ return {
9500
+ mean: n ? sum / n : 0,
9501
+ max,
9502
+ moved: n ? moved / n : 0
9503
+ };
9504
+ }
9505
+ // src/routers/adam/index.ts
9506
+ /** The default options of the router `adam`. */
9507
+ const ADAM_DEFAULTS = Object.freeze({
9508
+ eta: .1,
9509
+ samples: 8,
9510
+ steps: 200,
9511
+ seed: 1,
9512
+ clearance: 8
9513
+ });
9514
+ /** The declarations of the options of the router `adam` (RULE-020). */
9515
+ const ADAM_SCHEMA = {
9516
+ eta: {
9517
+ type: "number",
9518
+ min: .01,
9519
+ max: 1,
9520
+ step: .01,
9521
+ marks: [
9522
+ .01,
9523
+ .1,
9524
+ 1
9525
+ ],
9526
+ default: .1,
9527
+ describe: "The first step size, in units of the clearance. It falls to a tenth at the last step."
9528
+ },
9529
+ samples: {
9530
+ type: "integer",
9531
+ min: 4,
9532
+ max: 32,
9533
+ step: 1,
9534
+ marks: [
9535
+ 4,
9536
+ 8,
9537
+ 16,
9538
+ 32
9539
+ ],
9540
+ default: 8,
9541
+ describe: "The number of other legs that each leg samples at each step."
9542
+ },
9543
+ steps: {
9544
+ type: "integer",
9545
+ min: 100,
9546
+ max: 2e3,
9547
+ step: 100,
9548
+ marks: [
9549
+ 100,
9550
+ 200,
9551
+ 500,
9552
+ 2e3
9553
+ ],
9554
+ default: 200,
9555
+ describe: "The number of steps: the budget of the search."
9556
+ },
9557
+ seed: {
9558
+ type: "integer",
9559
+ min: 0,
9560
+ max: 1e6,
9561
+ step: 1,
9562
+ marks: [
9563
+ 1,
9564
+ 2,
9565
+ 3
9566
+ ],
9567
+ default: 1,
9568
+ describe: "The seed of the samples. The same seed gives the same routes."
9569
+ },
9570
+ clearance: {
9571
+ type: "number",
9572
+ min: 4,
9573
+ max: 32,
9574
+ step: 1,
9575
+ marks: [
9576
+ 4,
9577
+ 8,
9578
+ 16,
9579
+ 32
9580
+ ],
9581
+ default: 8,
9582
+ describe: "A leg nearer than this distance to another leg or to a box gets a push."
9583
+ }
9584
+ };
9585
+ /** The moment factors of Adam (Kingma and Ba 2015), as the record gives them. */
9586
+ const BETA1 = .9;
9587
+ const BETA2 = .999;
9588
+ const EPS$5 = 1e-8;
9589
+ /** The weight of the term that holds a leg near its first place. */
9590
+ const HOLD = .05;
9591
+ /** This function gives the point of the segment `ab` nearest to `p`. */
9592
+ function nearestOn(p, a, b) {
9593
+ const dx = b.x - a.x;
9594
+ const dy = b.y - a.y;
9595
+ const l2 = dx * dx + dy * dy;
9596
+ const t = l2 > 0 ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / l2)) : 0;
9597
+ return {
9598
+ x: a.x + t * dx,
9599
+ y: a.y + t * dy
9600
+ };
9601
+ }
9602
+ /** This function gives the point of a box nearest to `p`. A point in the box gives the nearest point of its border. */
9603
+ function nearestOfBox(p, b) {
9604
+ const qx = Math.max(b.x, Math.min(b.x + b.w, p.x));
9605
+ const qy = Math.max(b.y, Math.min(b.y + b.h, p.y));
9606
+ if (qx !== p.x || qy !== p.y) return {
9607
+ x: qx,
9608
+ y: qy
9609
+ };
9610
+ const sides = [
9611
+ p.x - b.x,
9612
+ b.x + b.w - p.x,
9613
+ p.y - b.y,
9614
+ b.y + b.h - p.y
9615
+ ];
9616
+ const m = Math.min(...sides);
9617
+ if (m === sides[0]) return {
9618
+ x: b.x,
9619
+ y: p.y
9620
+ };
9621
+ if (m === sides[1]) return {
9622
+ x: b.x + b.w,
9623
+ y: p.y
9624
+ };
9625
+ if (m === sides[2]) return {
9626
+ x: p.x,
9627
+ y: b.y
9628
+ };
9629
+ return {
9630
+ x: p.x,
9631
+ y: b.y + b.h
9632
+ };
9633
+ }
9634
+ /** This function gives the crossings of the route `a` with itself and with the routes of `others`. It skips the route with the index `skip`. */
9635
+ function crossingsOf(a, others, skip) {
9636
+ const [ax0, ay0, ax1, ay1] = boundsOf$1(a);
9637
+ let n = 0;
9638
+ for (let i = 1; i < a.length; i++) for (let k = i + 2; k < a.length; k++) if (segmentsCross(a[i - 1], a[i], a[k - 1], a[k])) n++;
9639
+ others.forEach((b, j) => {
9640
+ if (j === skip) return;
9641
+ const [bx0, by0, bx1, by1] = boundsOf$1(b);
9642
+ if (bx0 > ax1 || bx1 < ax0 || by0 > ay1 || by1 < ay0) return;
9643
+ for (let i = 1; i < a.length; i++) for (let k = 1; k < b.length; k++) if (segmentsCross(a[i - 1], a[i], b[k - 1], b[k])) n++;
9644
+ });
9645
+ return n;
9646
+ }
9647
+ /** The bounds of each polyline, once for each polyline object. */
9648
+ const BOUNDS = /* @__PURE__ */ new WeakMap();
9649
+ /** This function gives the bounds of a polyline: the least x and y, and the largest x and y. */
9650
+ function boundsOf$1(p) {
9651
+ let b = BOUNDS.get(p);
9652
+ if (b) return b;
9653
+ let x0 = Infinity;
9654
+ let y0 = Infinity;
9655
+ let x1 = -Infinity;
9656
+ let y1 = -Infinity;
9657
+ for (const q of p) {
9658
+ x0 = Math.min(x0, q.x);
9659
+ y0 = Math.min(y0, q.y);
9660
+ x1 = Math.max(x1, q.x);
9661
+ y1 = Math.max(y1, q.y);
9662
+ }
9663
+ b = [
9664
+ x0,
9665
+ y0,
9666
+ x1,
9667
+ y1
9668
+ ];
9669
+ BOUNDS.set(p, b);
9670
+ return b;
9671
+ }
9672
+ /**
9673
+ * This function moves the free legs of the routes of `result` with Adam (RULE-020, after Kingma and Ba 2015, and the SGD layouts of Zheng, Pawar and Goodman 2019).
9674
+ * A leg is free when its two points are inside its route, and no other route, run or trunk holds them. The variable of a leg is a shift along its normal.
9675
+ * The energy of a leg is `Σ (1 - d / c)²` over a seeded sample of `samples` other legs nearer than `c`. The near boxes add the same term times 4.
9676
+ * A term `0.05 × (t / c)²` holds each leg near its first place. The step size falls from `eta` to a tenth of it, with only `+`, `*` and `/`.
9677
+ * A moved route goes back to its first points when it has more crossings than before, or a break of a hard rule.
9678
+ * When the output still has more crossings than the routes of `shared`, it is the routes of `shared`.
9679
+ */
9680
+ function adamRoutes(input, result, options = {}) {
9681
+ const o = {
9682
+ ...ADAM_DEFAULTS,
9683
+ ...options
9684
+ };
9685
+ const c = Math.max(1e-6, o.clearance);
9686
+ const graph = outerGraph(input);
9687
+ const first = result.routes.map((r) => r.points);
9688
+ const pinned = /* @__PURE__ */ new Set();
9689
+ const keyOf = (p) => `${p.x},${p.y}`;
9690
+ for (const run of result.runs ?? []) for (const p of run.points) pinned.add(keyOf(p));
9691
+ for (const b of result.bundles ?? []) for (const p of b.trunk) pinned.add(keyOf(p));
9692
+ const owners = /* @__PURE__ */ new Map();
9693
+ first.forEach((pts, route) => {
9694
+ for (const p of pts) {
9695
+ const k = keyOf(p);
9696
+ const o = owners.get(k);
9697
+ if (o === void 0) owners.set(k, route);
9698
+ else if (o !== route) pinned.add(k);
9699
+ }
9700
+ });
9701
+ const boxes = graph.nodes.map((n) => n.box);
9702
+ const legs = [];
9703
+ first.forEach((pts, route) => {
9704
+ for (let k = 1; k + 2 < pts.length; k++) {
9705
+ const a = pts[k];
9706
+ const b = pts[k + 1];
9707
+ if (pinned.has(keyOf(a)) || pinned.has(keyOf(b))) continue;
9708
+ const dx = b.x - a.x;
9709
+ const dy = b.y - a.y;
9710
+ const len = Math.sqrt(dx * dx + dy * dy);
9711
+ if (len <= 1e-9) continue;
9712
+ const reach = 3 * c;
9713
+ const near = boxes.filter((bx) => Math.min(a.x, b.x) - reach <= bx.x + bx.w && Math.max(a.x, b.x) + reach >= bx.x && Math.min(a.y, b.y) - reach <= bx.y + bx.h && Math.max(a.y, b.y) + reach >= bx.y);
9714
+ legs.push({
9715
+ route,
9716
+ k,
9717
+ nx: -dy / len,
9718
+ ny: dx / len,
9719
+ boxes: near
9720
+ });
9721
+ }
9722
+ });
9723
+ if (legs.length === 0) return result;
9724
+ const shift = new Float64Array(legs.length);
9725
+ const m1 = new Float64Array(legs.length);
9726
+ const m2 = new Float64Array(legs.length);
9727
+ const legAt = /* @__PURE__ */ new Map();
9728
+ legs.forEach((l, i) => {
9729
+ for (const k of [l.k, l.k + 1]) {
9730
+ const key = `${l.route}:${k}`;
9731
+ const list = legAt.get(key);
9732
+ if (list) list.push(i);
9733
+ else legAt.set(key, [i]);
9734
+ }
9735
+ });
9736
+ const pointOf = (route, k) => {
9737
+ const p = first[route][k];
9738
+ const list = legAt.get(`${route}:${k}`);
9739
+ if (!list) return p;
9740
+ let x = p.x;
9741
+ let y = p.y;
9742
+ for (const i of list) {
9743
+ x += shift[i] * legs[i].nx;
9744
+ y += shift[i] * legs[i].ny;
9745
+ }
9746
+ return {
9747
+ x,
9748
+ y
9749
+ };
9750
+ };
9751
+ const all = [];
9752
+ first.forEach((pts, route) => {
9753
+ for (let k = 0; k + 1 < pts.length; k++) all.push({
9754
+ route,
9755
+ k
9756
+ });
9757
+ });
9758
+ const random = rng(Math.max(0, Math.floor(o.seed)));
9759
+ const steps = Math.max(1, Math.floor(o.steps));
9760
+ const count = Math.max(1, Math.floor(o.samples));
9761
+ let b1 = 1;
9762
+ let b2 = 1;
9763
+ for (let t = 0; t < steps; t++) {
9764
+ b1 *= BETA1;
9765
+ b2 *= BETA2;
9766
+ const eta = o.eta * c / (1 + 9 * t / Math.max(1, steps - 1));
9767
+ const grads = new Float64Array(legs.length);
9768
+ legs.forEach((l, i) => {
9769
+ const a = pointOf(l.route, l.k);
9770
+ const b = pointOf(l.route, l.k + 1);
9771
+ const mid = {
9772
+ x: (a.x + b.x) / 2,
9773
+ y: (a.y + b.y) / 2
9774
+ };
9775
+ let g = 2 * HOLD * shift[i] / (c * c);
9776
+ for (let s = 0; s < count; s++) {
9777
+ const other = all[Math.floor(random() * all.length)];
9778
+ if (other.route === l.route) continue;
9779
+ const q = nearestOn(mid, pointOf(other.route, other.k), pointOf(other.route, other.k + 1));
9780
+ const dx = mid.x - q.x;
9781
+ const dy = mid.y - q.y;
9782
+ const d = Math.sqrt(dx * dx + dy * dy);
9783
+ if (d >= c || d <= 1e-9) continue;
9784
+ g += -2 * (1 - d / c) / c * ((l.nx * dx + l.ny * dy) / d);
9785
+ }
9786
+ for (const bx of l.boxes) for (const p of [
9787
+ a,
9788
+ mid,
9789
+ b
9790
+ ]) {
9791
+ const q = nearestOfBox(p, bx);
9792
+ const inside = p.x >= bx.x && p.x <= bx.x + bx.w && p.y >= bx.y && p.y <= bx.y + bx.h;
9793
+ const dx = (inside ? q.x - p.x : p.x - q.x) || 0;
9794
+ const dy = (inside ? q.y - p.y : p.y - q.y) || 0;
9795
+ const d0 = Math.sqrt(dx * dx + dy * dy);
9796
+ if (d0 <= 1e-9) continue;
9797
+ const d = inside ? -d0 : d0;
9798
+ if (d >= c) continue;
9799
+ g += -8 * (1 - d / c) / c * ((l.nx * dx + l.ny * dy) / d0);
9800
+ }
9801
+ grads[i] = g;
9802
+ });
9803
+ for (let i = 0; i < legs.length; i++) {
9804
+ const g = grads[i];
9805
+ m1[i] = BETA1 * m1[i] + .09999999999999998 * g;
9806
+ m2[i] = BETA2 * m2[i] + .0010000000000000009 * g * g;
9807
+ const mh = m1[i] / (1 - b1);
9808
+ const vh = m2[i] / (1 - b2);
9809
+ shift[i] = Math.max(-2 * c, Math.min(2 * c, shift[i] - eta * mh / (Math.sqrt(vh) + EPS$5)));
9810
+ }
9811
+ }
9812
+ const moved = new Set(legs.map((l) => l.route));
9813
+ const points = first.map((pts, route) => moved.has(route) ? pts.map((_, k) => pointOf(route, k)) : pts);
9814
+ for (let round = 0; round < 8; round++) {
9815
+ let back = 0;
9816
+ for (const route of moved) {
9817
+ if (points[route] === first[route] || crossingsOf(points[route], points, route) <= crossingsOf(first[route], points, route)) continue;
9818
+ points[route] = first[route];
9819
+ back++;
9820
+ }
9821
+ if (back === 0) break;
9822
+ }
9823
+ const build = (pts) => ({
9824
+ ...result,
9825
+ routes: result.routes.map((r, i) => pts[i] === r.points ? r : {
9826
+ ...r,
9827
+ points: pts[i]
9828
+ })
9829
+ });
9830
+ let out = build(points);
9831
+ for (let round = 0; round < 3; round++) {
9832
+ const faulty = new Set(checkProperties(input, out, 1e6).map((v) => v.edge));
9833
+ const fix = result.routes.map((r, i) => faulty.has(r.edge) && points[i] !== first[i] ? i : -1).filter((i) => i >= 0);
9834
+ if (fix.length === 0) break;
9835
+ for (const i of fix) points[i] = first[i];
9836
+ out = build(points);
9837
+ }
9838
+ if (checkProperties(input, out, 1).length > checkProperties(input, result, 1).length) return result;
9839
+ return measureQuality(input, out).crossings > measureQuality(input, result).crossings ? result : out;
9840
+ }
9841
+ /**
9842
+ * This function makes the router `adam` (RULE-020). It is the router `shared`, then the steps of Adam of `adamRoutes`.
9843
+ * The runs and the bundles of `shared` stay, because no point of a run or of a trunk moves.
9844
+ */
9845
+ function adamRouter(options) {
9846
+ const o = {
9847
+ ...ADAM_DEFAULTS,
9848
+ ...options
9849
+ };
9850
+ const base = sharedRouter({
9851
+ name: o.name,
9852
+ ...o.rule === void 0 ? {} : { rule: o.rule },
9853
+ ...o.maxNodes === void 0 ? {} : { maxNodes: o.maxNodes }
9854
+ });
9855
+ return {
9856
+ name: o.name,
9857
+ ...o.maxNodes === void 0 ? {} : { maxNodes: o.maxNodes },
9858
+ route: (graph) => adamRoutes(graph, base.route(graph), o)
9859
+ };
9860
+ }
9861
+ /**
9862
+ * The router strategy `adam` (RULE-020), with the rule `byCollection` of `shared`. It is not a built-in router, so the benchmark and the hashes of `ROUTERS` do not change.
9863
+ * A screen module or a consumer adds it to a registry.
9864
+ */
9865
+ const adamStrategy = defineStrategy({
9866
+ step: "router",
9867
+ name: "adam",
9868
+ describe: "Adam with a seeded sample of pairs (RULE-020). The free legs of the routes of `shared` move along their normals, away from near legs and boxes.",
9869
+ defaults: { ...ADAM_DEFAULTS },
9870
+ schema: ADAM_SCHEMA,
9871
+ maxNodes: 1e4,
9872
+ run: (graph, o) => adamRouter({
9873
+ ...o,
9874
+ name: "adam",
9875
+ rule: byCollection
9876
+ }).route(graph)
9877
+ });
9878
+ // src/routers/reroute/index.ts
9879
+ /** The sine of 1 degree, as a literal. A literal gives the same bits on each platform, so a decision does not depend on `Math.sin`. */
9880
+ const SIN_1 = .01745240643728351;
9881
+ /** The default options of the router `reroute`. */
9882
+ const REROUTE_DEFAULTS = Object.freeze({
9883
+ alpha: 1,
9884
+ beta: .5,
9885
+ gamma: .5,
9886
+ tau: .25,
9887
+ rounds: 2,
9888
+ q: .15,
9889
+ clearance: 16
9890
+ });
9891
+ /** The declarations of the options of the router `reroute` (RULE-044). */
9892
+ const REROUTE_SCHEMA = {
9893
+ alpha: {
9894
+ type: "number",
9895
+ min: 0,
9896
+ max: 2,
9897
+ step: .05,
9898
+ marks: [
9899
+ 0,
9900
+ 1,
9901
+ 2
9902
+ ],
9903
+ default: 1,
9904
+ describe: "The price of a link in a narrow channel."
9905
+ },
9906
+ beta: {
9907
+ type: "number",
9908
+ min: 0,
9909
+ max: 2,
9910
+ step: .05,
9911
+ marks: [
9912
+ 0,
9913
+ .5,
9914
+ 1
9915
+ ],
9916
+ default: .5,
9917
+ describe: "The price of each edge of another key on a link."
9918
+ },
9919
+ gamma: {
9920
+ type: "number",
9921
+ min: 0,
9922
+ max: 2,
9923
+ step: .05,
9924
+ marks: [
9925
+ 0,
9926
+ .5,
9927
+ 1
9928
+ ],
9929
+ default: .5,
9930
+ describe: "The discount of a link that an edge of the same key uses."
9931
+ },
9932
+ tau: {
9933
+ type: "number",
9934
+ min: 0,
9935
+ max: 2,
9936
+ step: .05,
9937
+ marks: [
9938
+ 0,
9939
+ .25,
9940
+ 1
9941
+ ],
9942
+ default: .25,
9943
+ describe: "The price of a turn, in units of the clearance."
9944
+ },
9945
+ rounds: {
9946
+ type: "integer",
9947
+ min: 1,
9948
+ max: 5,
9949
+ step: 1,
9950
+ marks: [
9951
+ 1,
9952
+ 2,
9953
+ 3,
9954
+ 5
9955
+ ],
9956
+ default: 2,
9957
+ describe: "The number of rounds of rip-up and reroute."
9958
+ },
9959
+ q: {
9960
+ type: "number",
9961
+ min: .05,
9962
+ max: .3,
9963
+ step: .05,
9964
+ marks: [
9965
+ .05,
9966
+ .15,
9967
+ .3
9968
+ ],
9969
+ default: .15,
9970
+ describe: "The share of the edges that each round rips up: the edges with the largest price over their direct length."
9971
+ },
9972
+ clearance: {
9973
+ type: "number",
9974
+ min: 4,
9975
+ max: 64,
9976
+ step: 1,
9977
+ marks: [
9978
+ 8,
9979
+ 16,
9980
+ 32
9981
+ ],
9982
+ default: 16,
9983
+ describe: "A channel narrower than this width gets the price alpha."
9984
+ }
9985
+ };
9986
+ /** This function gives the number of turns of a way: the points where the direction changes by more than 1 degree. */
9987
+ function turnsOf(seq) {
9988
+ let turns = 0;
9989
+ for (let i = 1; i + 1 < seq.length; i++) {
9990
+ const a = seq[i - 1];
9991
+ const b = seq[i];
9992
+ const c = seq[i + 1];
9993
+ const ux = b.x - a.x;
9994
+ const uy = b.y - a.y;
9995
+ const vx = c.x - b.x;
9996
+ const vy = c.y - b.y;
9997
+ const lu = Math.sqrt(ux * ux + uy * uy);
9998
+ const lv = Math.sqrt(vx * vx + vy * vy);
9999
+ if (lu <= 1e-9 || lv <= 1e-9) continue;
10000
+ if (Math.abs(ux * vy - uy * vx) > SIN_1 * lu * lv || ux * vx + uy * vy < 0) turns++;
10001
+ }
10002
+ return turns;
10003
+ }
10004
+ /**
10005
+ * This function rips up and reroutes the ways of `shared` (RULE-044, after the rip-up and reroute of PathFinder, McMurchie 1995).
10006
+ * The price factor of a link is `max(0.05, 1 + alpha × narrow + beta × others - gamma × same)`.
10007
+ * `narrow` is `max(0, 1 - width / clearance)`. `others` counts the edges of the other keys on the link.
10008
+ * `same` is 1 when an edge of the same key uses the link.
10009
+ * The price of a way is the sum of the price factor times the length of each link, plus `tau × clearance` for each turn.
10010
+ * Each round takes the share `q` of the ways with the largest price over their direct length. Each one gets a new search with the prices of the links.
10011
+ * The new way stays only when its price is lower. The search has no state for the turns, so only the comparison reads `tau`.
10012
+ */
10013
+ function rerouteWays(ways, search, options = {}) {
10014
+ const o = {
10015
+ ...REROUTE_DEFAULTS,
10016
+ ...options
10017
+ };
10018
+ const n = search.corners;
10019
+ const use = /* @__PURE__ */ new Map();
10020
+ const linksOf = (w) => {
10021
+ const out = [];
10022
+ for (let i = 1; i < w.ids.length; i++) {
10023
+ const a = w.ids[i - 1];
10024
+ const b = w.ids[i];
10025
+ if (a < n && b < n) out.push(search.link(a, b));
10026
+ }
10027
+ return out;
10028
+ };
10029
+ const change = (w, by) => {
10030
+ for (const k of linksOf(w)) {
10031
+ let m = use.get(k);
10032
+ if (!m) use.set(k, m = /* @__PURE__ */ new Map());
10033
+ const next = (m.get(w.key) ?? 0) + by;
10034
+ if (next > 0) m.set(w.key, next);
10035
+ else m.delete(w.key);
10036
+ }
10037
+ };
10038
+ const found = ways.filter((w) => w.found && w.ids.length >= 2);
10039
+ for (const w of found) change(w, 1);
10040
+ const factor = (i, j, key) => {
10041
+ const m = use.get(search.link(i, j));
10042
+ let others = 0;
10043
+ let same = 0;
10044
+ for (const [k, c] of m ?? []) if (k === key) same = 1;
10045
+ else others += c;
10046
+ const narrow = o.alpha > 0 ? Math.max(0, 1 - search.width(i, j) / o.clearance) : 0;
10047
+ return Math.max(.05, 1 + o.alpha * narrow + o.beta * others - o.gamma * same);
10048
+ };
10049
+ const priceOf = (seq, ids, key) => {
10050
+ let p = 0;
10051
+ for (let i = 1; i < seq.length; i++) {
10052
+ const a = ids[i - 1];
10053
+ const b = ids[i];
10054
+ const d = dist(seq[i - 1], seq[i]);
10055
+ p += a < n && b < n ? d * factor(a, b, key) : d;
10056
+ }
10057
+ return p + o.tau * o.clearance * turnsOf(seq);
10058
+ };
10059
+ const floor = Math.max(.05, 1 - o.gamma);
10060
+ const count = (share) => Math.max(1, Math.round(share * found.length));
10061
+ for (let round = 0; round < Math.max(1, Math.floor(o.rounds)); round++) {
10062
+ const ranked = found.map((w) => ({
10063
+ w,
10064
+ score: priceOf(w.seq, w.ids, w.key) / Math.max(1e-9, dist(w.ps.point, w.pt.point))
10065
+ })).sort((a, b) => b.score - a.score || (a.w.edge.id < b.w.edge.id ? -1 : a.w.edge.id > b.w.edge.id ? 1 : 0));
10066
+ let moved = 0;
10067
+ for (const { w } of ranked.slice(0, count(Math.min(.3, Math.max(.05, o.q))))) {
10068
+ change(w, -1);
10069
+ const before = priceOf(w.seq, w.ids, w.key);
10070
+ const next = search.find(w, (i, j, d) => d * factor(i, j, w.key), floor);
10071
+ if (next && priceOf(next.seq, next.ids, w.key) < before - 1e-9) {
10072
+ w.seq = next.seq;
10073
+ w.ids = next.ids;
10074
+ moved++;
10075
+ }
10076
+ change(w, 1);
10077
+ }
10078
+ if (moved === 0) break;
10079
+ }
10080
+ }
10081
+ /**
10082
+ * This function makes the router `reroute` (RULE-044). It is the router `shared`, then the rounds of rip-up and reroute of `rerouteWays`.
10083
+ * The runs and the bundles come from the ways after the rounds.
10084
+ */
10085
+ function rerouteRouter(options) {
10086
+ const o = {
10087
+ ...REROUTE_DEFAULTS,
10088
+ ...options
10089
+ };
10090
+ return sharedRouter({
10091
+ name: o.name,
10092
+ ...o.rule === void 0 ? {} : { rule: o.rule },
10093
+ ...o.maxNodes === void 0 ? {} : { maxNodes: o.maxNodes }
10094
+ }, void 0, { refine: (ways, search) => rerouteWays(ways, search, o) });
10095
+ }
10096
+ /**
10097
+ * The router strategy `reroute` (RULE-044), with the rule `byCollection` of `shared`. It is not a built-in router, so the benchmark and the hashes of `ROUTERS` do not change.
10098
+ * A screen module or a consumer adds it to a registry.
10099
+ */
10100
+ const rerouteStrategy = defineStrategy({
10101
+ step: "router",
10102
+ name: "reroute",
10103
+ describe: "Rule prices and rip-up (RULE-044). The ways of `shared` with the largest prices of narrow channels, other keys and turns get a new search.",
10104
+ defaults: { ...REROUTE_DEFAULTS },
10105
+ schema: REROUTE_SCHEMA,
10106
+ maxNodes: 1e4,
10107
+ run: (graph, o) => rerouteRouter({
10108
+ ...o,
10109
+ name: "reroute",
10110
+ rule: byCollection
10111
+ }).route(graph)
10112
+ });
8934
10113
  // src/routers/tight/index.ts
8935
10114
  /** The values of `TightFirst`. */
8936
10115
  const TIGHT_FIRSTS = Object.freeze(["tight", "loose"]);
@@ -9201,7 +10380,7 @@ function jogLength(r, shift) {
9201
10380
  return 2 * r * Math.sin(Math.acos(1 - d / (2 * r)));
9202
10381
  }
9203
10382
  /** This function gives the legs of the route of each edge, with the lanes and the offset of the lane on each leg. */
9204
- function legsOf$1(graph, spacing, maxWidth) {
10383
+ function legsOf$2(graph, spacing, maxWidth) {
9205
10384
  const byId = new Map(graph.segments.map((s) => [s.id, s]));
9206
10385
  return graph.paths.map((path) => {
9207
10386
  const legs = [];
@@ -9318,7 +10497,7 @@ function bendGrowth(items, links, gaps, bends, snap) {
9318
10497
  source: { node: l.source },
9319
10498
  target: { node: l.target }
9320
10499
  })));
9321
- const short = shortLegs(legsOf$1(segmentsOf(bends.router.route({
10500
+ const short = shortLegs(legsOf$2(segmentsOf(bends.router.route({
9322
10501
  nodes: items,
9323
10502
  edges
9324
10503
  })), spacing, maxWidth), minRadius);
@@ -9689,8 +10868,8 @@ const LANE_TRACKS = [
9689
10868
  "left-edge",
9690
10869
  "free"
9691
10870
  ];
9692
- const EPS$3 = 1e-6;
9693
- const ROUNDS = 8;
10871
+ const EPS$4 = 1e-6;
10872
+ const ROUNDS$1 = 8;
9694
10873
  /**
9695
10874
  * This function gives each link that runs along a gap its own lane in the gap. The lanes of a gap are `spacing` apart, around its middle line.
9696
10875
  * A leg of a link moves across its gap only, so the link keeps its plugs and the axes of its legs.
@@ -9714,7 +10893,7 @@ function laneGaps(items, links, options) {
9714
10893
  })));
9715
10894
  const counts = [];
9716
10895
  for (const g of boardGaps(items, options.snap)) {
9717
- if (g.width <= EPS$3) continue;
10896
+ if (g.width <= EPS$4) continue;
9718
10897
  const across = g.axis;
9719
10898
  const along = g.axis === "x" ? "y" : "x";
9720
10899
  const lo = g.at - g.width;
@@ -9725,8 +10904,8 @@ function laneGaps(items, links, options) {
9725
10904
  for (let i = 2; i <= p.length - 2; i++) {
9726
10905
  const a = p[i - 1];
9727
10906
  const b = p[i];
9728
- if (Math.abs(a[across] - b[across]) > EPS$3 || a[across] <= lo + EPS$3 || a[across] >= hi - EPS$3) continue;
9729
- if (Math.abs(a[along] - b[along]) <= EPS$3) continue;
10907
+ if (Math.abs(a[across] - b[across]) > EPS$4 || a[across] <= lo + EPS$4 || a[across] >= hi - EPS$4) continue;
10908
+ if (Math.abs(a[along] - b[along]) <= EPS$4) continue;
9730
10909
  const side = (q, r) => Math.sign(r[across] - q[across]);
9731
10910
  pieces.push({
9732
10911
  link: li,
@@ -9802,7 +10981,7 @@ function leftEdgeLanes(list, constrained) {
9802
10981
  const now = [];
9803
10982
  let end = -Infinity;
9804
10983
  for (const i of byStart) {
9805
- if (lane[i] >= 0 || wait[i] > 0 || list[i].lo < end - EPS$3) continue;
10984
+ if (lane[i] >= 0 || wait[i] > 0 || list[i].lo < end - EPS$4) continue;
9806
10985
  now.push(i);
9807
10986
  end = list[i].hi;
9808
10987
  }
@@ -9825,15 +11004,15 @@ function density(list) {
9825
11004
  }
9826
11005
  return most;
9827
11006
  }
9828
- const overlaps = (P, Q) => Math.min(P.hi, Q.hi) - Math.max(P.lo, Q.lo) > EPS$3;
11007
+ const overlaps = (P, Q) => Math.min(P.hi, Q.hi) - Math.max(P.lo, Q.lo) > EPS$4;
9829
11008
  /**
9830
11009
  * This function counts the crossings when the piece `P` is on the near side of the piece `Q`.
9831
11010
  * A turn of `P` to the far side crosses `Q` when it is inside the span of `Q`. A turn of `Q` to the near side crosses `P` in the same way.
9832
11011
  */
9833
11012
  function cost$1(P, Q) {
9834
11013
  let c = 0;
9835
- for (const [y, s] of P.turns) if (s > 0 && y > Q.lo + EPS$3 && y < Q.hi - EPS$3) c++;
9836
- for (const [y, s] of Q.turns) if (s < 0 && y > P.lo + EPS$3 && y < P.hi - EPS$3) c++;
11014
+ for (const [y, s] of P.turns) if (s > 0 && y > Q.lo + EPS$4 && y < Q.hi - EPS$4) c++;
11015
+ for (const [y, s] of Q.turns) if (s < 0 && y > P.lo + EPS$4 && y < P.hi - EPS$4) c++;
9837
11016
  return c;
9838
11017
  }
9839
11018
  /** This function gives the pieces of one gap in lane order, from the near side to the far side. */
@@ -9846,7 +11025,7 @@ function orderPieces(pieces, order) {
9846
11025
  const index = new Map(list.map((P, i) => [P, i]));
9847
11026
  const c = list.map((P) => list.map((Q) => P !== Q && overlaps(P, Q) ? cost$1(P, Q) : 0));
9848
11027
  const at = (P, Q) => c[index.get(P)][index.get(Q)];
9849
- for (let round = 0; round < ROUNDS; round++) {
11028
+ for (let round = 0; round < ROUNDS$1; round++) {
9850
11029
  let better = false;
9851
11030
  for (let i = 0; i < n; i++) {
9852
11031
  const x = list[i];
@@ -10037,17 +11216,10 @@ function facingEnds(input) {
10037
11216
  }
10038
11217
  return out;
10039
11218
  }
10040
- // src/plugs/spread/index.ts
11219
+ // src/plugs/walls.ts
10041
11220
  /**
10042
- * The plug strategy `spread` (PLUG-SPREAD): the free plugs of each wall share the room of that wall with a minimum gap.
10043
- * The order on the wall keeps the order of the far ends, so two routes do not cross at the wall. No plug drops.
11221
+ * The walls of the plug strategies `spread`, `leaders` and `assign`: one side of one node, its room, and the order of its far ends.
10044
11222
  */
10045
- /** The defaults of the plug strategy `spread`. */
10046
- const SPREAD_DEFAULTS = Object.freeze({
10047
- plugGap: 8,
10048
- inset: 6,
10049
- rounds: 2
10050
- });
10051
11223
  /** This function gives the axis along a side. */
10052
11224
  function alongOf(side) {
10053
11225
  return side === "east" || side === "west" ? "y" : "x";
@@ -10096,6 +11268,30 @@ function keyOf$1(box, side, far) {
10096
11268
  if (out >= 0) return p;
10097
11269
  return along >= 0 ? 2 - p : -2 - p;
10098
11270
  }
11271
+ // src/plugs/spread/index.ts
11272
+ /**
11273
+ * The plug strategy `spread` (PLUG-SPREAD): the free plugs of each wall share the room of that wall with a minimum gap.
11274
+ * The order on the wall keeps the order of the far ends, so two routes do not cross at the wall. No plug drops.
11275
+ */
11276
+ /** The defaults of the plug strategy `spread`. */
11277
+ const SPREAD_DEFAULTS = Object.freeze({
11278
+ plugGap: 8,
11279
+ inset: 6,
11280
+ rounds: 2
11281
+ });
11282
+ /**
11283
+ * This function gives the sort key of a far point for a po-leader (EDA-041): its place on the axis along the wall.
11284
+ * A far point behind the wall gets the key of `keyOf` with a large shift, so it goes to an end of the wall.
11285
+ */
11286
+ function poKeyOf(box, side, far) {
11287
+ const c = center(box);
11288
+ const n = SIDE_NORMAL[side];
11289
+ const out = (far.x - c.x) * n.x + (far.y - c.y) * n.y;
11290
+ const along = alongOf(side) === "y" ? far.y : far.x;
11291
+ if (out > 0) return along;
11292
+ const k = keyOf$1(box, side, far);
11293
+ return k >= 0 ? 0xe8d4a51000 + k : -0xe8d4a51000 + k;
11294
+ }
10099
11295
  /**
10100
11296
  * This function places `ideal.length` plugs in their given order on the segment from `lo` to `hi`. Two neighbours are at least `gap` apart.
10101
11297
  * The gap shrinks evenly when the segment is too short. The sum of the squares of the moves from the ideal places is the least.
@@ -10113,7 +11309,7 @@ function placeInOrder(ideal, lo, hi, gap) {
10113
11309
  * Each slot goes into the part of its ideal place, and the parts keep the order of the slots. In each part, two plugs and a plug and a fixed plug are at least `gap` apart.
10114
11310
  * The gap of a part shrinks evenly when the part is too short.
10115
11311
  */
10116
- function placeWall(slots, fixed, lo, hi, gap) {
11312
+ function placeWall(slots, fixed, lo, hi, gap, idealOf = (s) => s.ideal) {
10117
11313
  const cuts = [...new Set(fixed.filter((f) => f > lo - 1e-9 && f < hi + 1e-9))].sort((a, b) => a - b);
10118
11314
  const ends = [
10119
11315
  lo,
@@ -10124,10 +11320,11 @@ function placeWall(slots, fixed, lo, hi, gap) {
10124
11320
  let part = 0;
10125
11321
  for (const s of slots) {
10126
11322
  let p = 0;
10127
- while (p < cuts.length && s.ideal > cuts[p]) p++;
11323
+ while (p < cuts.length && idealOf(s) > cuts[p]) p++;
10128
11324
  part = Math.max(part, p);
10129
11325
  parts[part].push(s);
10130
11326
  }
11327
+ const ranged = slots.some((s) => s.range !== void 0);
10131
11328
  parts.forEach((list, p) => {
10132
11329
  if (list.length === 0) return;
10133
11330
  const a = ends[p];
@@ -10138,7 +11335,22 @@ function placeWall(slots, fixed, lo, hi, gap) {
10138
11335
  const s = steps > 0 ? Math.max(0, Math.min(gap, (b - a) / steps)) : gap;
10139
11336
  const lower = a + fa * s;
10140
11337
  const upper = Math.max(lower, b - fb * s);
10141
- const at = placeInOrder(list.map((x) => x.ideal), lower, upper, s);
11338
+ if (ranged) {
11339
+ const span = list.map((x) => {
11340
+ if (!x.range) return [lower, upper];
11341
+ const lo = Math.max(lower, x.range[0]);
11342
+ const hi = Math.min(upper, x.range[1]);
11343
+ if (lo <= hi) return [lo, hi];
11344
+ const v = x.range[1] < lower ? lower : upper;
11345
+ return [v, v];
11346
+ });
11347
+ const at = placeInIntervals(list.map((x) => idealOf(x)), span.map((r) => r[0]), span.map((r) => r[1]), s);
11348
+ list.forEach((x, i) => {
11349
+ x.at = at[i];
11350
+ });
11351
+ return;
11352
+ }
11353
+ const at = placeInOrder(list.map((x) => idealOf(x)), lower, upper, s);
10142
11354
  list.forEach((x, i) => {
10143
11355
  x.at = at[i];
10144
11356
  });
@@ -10152,6 +11364,13 @@ function placeWall(slots, fixed, lo, hi, gap) {
10152
11364
  * The output has one entry for each edge with two known nodes, in the order of the edges.
10153
11365
  */
10154
11366
  function spreadEnds(input, options = {}) {
11367
+ return spreadWith(input, options);
11368
+ }
11369
+ /**
11370
+ * This function does the work of `spreadEnds`, and of the strategy `leaders` with the argument `leader`.
11371
+ * Without `leader`, it gives the plugs of `spread`.
11372
+ */
11373
+ function spreadWith(input, options = {}, leader) {
10155
11374
  const { graph } = input;
10156
11375
  const gap = options.plugGap ?? SPREAD_DEFAULTS.plugGap;
10157
11376
  const inset = options.inset ?? SPREAD_DEFAULTS.inset;
@@ -10191,12 +11410,14 @@ function spreadEnds(input, options = {}) {
10191
11410
  if (slots.has(key)) return key;
10192
11411
  const side = port?.side ?? sideToward(node.box, toward);
10193
11412
  const ideal = placePlug(node, end, toward).point[alongOf(side)];
11413
+ const range = rangeInterval(node.box, side, port, inset);
10194
11414
  const slot = {
10195
11415
  node,
10196
11416
  side,
10197
11417
  ideal,
10198
11418
  rank: [port ? node.ports.indexOf(port) : -1, slots.size],
10199
- at: ideal
11419
+ at: ideal,
11420
+ ...range ? { range } : {}
10200
11421
  };
10201
11422
  slots.set(key, slot);
10202
11423
  wallOf(node, side).slots.push(slot);
@@ -10229,11 +11450,15 @@ function spreadEnds(input, options = {}) {
10229
11450
  const order = /* @__PURE__ */ new Map();
10230
11451
  for (let round = 0; round <= rounds; round++) for (const wall of walls.values()) {
10231
11452
  const [lo, hi] = roomOf(wall.node.box, wall.side, inset);
10232
- const keys = new Map(wall.slots.map((s) => [s, keyOf$1(wall.node.box, wall.side, resolve(s.far).point)]));
11453
+ const key = leader === "po" ? poKeyOf : keyOf$1;
11454
+ const keys = new Map(wall.slots.map((s) => [s, key(wall.node.box, wall.side, resolve(s.far).point)]));
10233
11455
  const tie = (a, b) => round === 0 ? a.rank[0] - b.rank[0] || a.rank[1] - b.rank[1] : order.get(a) - order.get(b);
10234
11456
  const sorted = [...wall.slots].sort((a, b) => keys.get(a) - keys.get(b) || tie(a, b));
10235
11457
  sorted.forEach((s, i) => order.set(s, i));
10236
- placeWall(sorted, wall.fixed, lo, hi, gap);
11458
+ if (leader === "opo") {
11459
+ const even = new Map(sorted.map((s, i) => [s, lo + (i + .5) * (hi - lo) / sorted.length]));
11460
+ placeWall(sorted, wall.fixed, lo, hi, gap, (s) => even.get(s));
11461
+ } else placeWall(sorted, wall.fixed, lo, hi, gap);
10237
11462
  }
10238
11463
  return ends.map((x) => ({
10239
11464
  edge: x.edge,
@@ -10241,6 +11466,497 @@ function spreadEnds(input, options = {}) {
10241
11466
  target: resolve(x.target)
10242
11467
  }));
10243
11468
  }
11469
+ // src/plugs/assign/index.ts
11470
+ /**
11471
+ * The plug strategy `assign` (PLUG-ASSIGN): the plugs of each wall go to the intervals of their ends, with a least gap.
11472
+ * An interval is the range of the port (PORT-RANGE), or the room of the wall. The assignment is exact for the count of the ends in their intervals.
11473
+ */
11474
+ /** The defaults of the plug strategy `assign`. */
11475
+ const ASSIGN_DEFAULTS = Object.freeze({
11476
+ plugGap: 8,
11477
+ inset: 6
11478
+ });
11479
+ /** The tolerance of the comparisons of places along a wall. */
11480
+ const EPS$3 = 1e-9;
11481
+ /** This function gives the index of the first value of the sorted list `list` that is `v` or more. */
11482
+ function lowerBound(list, v) {
11483
+ let a = 0;
11484
+ let b = list.length;
11485
+ while (a < b) {
11486
+ const mid = a + b >> 1;
11487
+ if (list[mid] < v) a = mid + 1;
11488
+ else b = mid;
11489
+ }
11490
+ return a;
11491
+ }
11492
+ /**
11493
+ * This function moves the place `p` out of the zones of the fixed plugs: the open intervals of the width `2·s` around each fixed plug.
11494
+ * The list `fixed` is sorted. The place only moves up.
11495
+ */
11496
+ function pushOut(p, fixed, s) {
11497
+ for (const f of fixed) if (p > f - s + EPS$3 && p < f + s - EPS$3) p = f + s;
11498
+ return p;
11499
+ }
11500
+ /**
11501
+ * This function gives the gap of a wall with `n` plugs: `gap`, or the largest gap that lets all the plugs go in the room.
11502
+ * The plugs keep the gap to the fixed plugs too.
11503
+ */
11504
+ function wallGap(n, fixed, lo, hi, gap) {
11505
+ if (n <= 1) return gap;
11506
+ if (fixed.length === 0) return Math.max(0, Math.min(gap, (hi - lo) / (n - 1)));
11507
+ const fits = (s) => {
11508
+ let p = lo;
11509
+ for (let c = 0; c < n; c++) {
11510
+ p = pushOut(p, fixed, s);
11511
+ if (p > hi + EPS$3) return false;
11512
+ p += s;
11513
+ }
11514
+ return true;
11515
+ };
11516
+ if (fits(gap)) return gap;
11517
+ let a = 0;
11518
+ let b = gap;
11519
+ for (let i = 0; i < 60; i++) {
11520
+ const mid = (a + b) / 2;
11521
+ if (fits(mid)) a = mid;
11522
+ else b = mid;
11523
+ }
11524
+ return Math.max(0, a - 1e-7);
11525
+ }
11526
+ /**
11527
+ * This function gives the plan of the ends of one wall in the order of their keys, with no crossing at the wall.
11528
+ * The plan has the most ends in their intervals that this order lets. The answer is exact for this order: a table of the least last place
11529
+ * for each count of ends in their intervals.
11530
+ */
11531
+ function keyOrderPlan(order, win, fixed, lo, hi, s) {
11532
+ const n = order.length;
11533
+ const nf = fixed.length;
11534
+ const at = (i, j, c) => (i * (nf + 1) + j) * (n + 1) + c;
11535
+ const best = new Float64Array((n + 1) * (nf + 1) * (n + 1)).fill(Infinity);
11536
+ const step = new Int8Array(best.length);
11537
+ best[at(0, 0, 0)] = -Infinity;
11538
+ const relax = (k, v, how) => {
11539
+ if (v < best[k]) {
11540
+ best[k] = v;
11541
+ step[k] = how;
11542
+ }
11543
+ };
11544
+ for (let i = 0; i <= n; i++) for (let j = 0; j <= nf; j++) for (let c = 0; c <= i; c++) {
11545
+ const v = best[at(i, j, c)];
11546
+ if (v === Infinity) continue;
11547
+ const next = v === -Infinity ? -Infinity : v + s;
11548
+ if (j < nf && (v === -Infinity || v + s <= fixed[j] + EPS$3)) relax(at(i, j + 1, c), fixed[j], 0);
11549
+ if (i < n) {
11550
+ const w = win[order[i]];
11551
+ if (w) {
11552
+ const p = Math.max(w[0], next);
11553
+ if (p <= w[1] + EPS$3) relax(at(i + 1, j, c + 1), p, 1);
11554
+ }
11555
+ const q = Math.max(lo, next);
11556
+ if (q <= hi + EPS$3) relax(at(i + 1, j, c), q, 2);
11557
+ }
11558
+ }
11559
+ let c = n;
11560
+ while (c >= 0 && best[at(n, nf, c)] === Infinity) c--;
11561
+ if (c < 0) return void 0;
11562
+ const items = [];
11563
+ let i = n;
11564
+ let j = nf;
11565
+ while (i > 0 || j > 0) {
11566
+ const k = at(i, j, c);
11567
+ const how = step[k];
11568
+ if (how === 0) items.push({
11569
+ end: -1,
11570
+ anchored: true,
11571
+ at: fixed[--j]
11572
+ });
11573
+ else {
11574
+ items.push({
11575
+ end: order[--i],
11576
+ anchored: how === 1,
11577
+ at: best[k]
11578
+ });
11579
+ if (how === 1) c--;
11580
+ }
11581
+ }
11582
+ return items.reverse();
11583
+ }
11584
+ /**
11585
+ * This function gives the most ends of a wall that can have their plug in their interval, in any order. It also gives one plan with that count.
11586
+ * Each end without its plug in its interval still gets a plug in the room of the wall: a free plug.
11587
+ *
11588
+ * The plan comes from two facts. First, a plan can move each plug down until it touches its interval, the room, a fixed zone or the plug before it.
11589
+ * Thus each plug is at a start place plus a whole number of gaps. Second, a swap of two plugs makes the last plug of the plan the end with the latest interval end.
11590
+ * Then the ends with an interval start up to its place are before it, and the others after it. This split gives a table over the ends and two places (the recursion `count`).
11591
+ * The free plugs have the room as interval and the latest end. Thus they split the wall into parts, and a second table chains the parts.
11592
+ */
11593
+ function bestPlan(order, win, ideals, fixed, lo, hi, s) {
11594
+ const n = order.length;
11595
+ const jobs = order.filter((e) => win[e] !== void 0).sort((a, b) => win[a][1] - win[b][1] || win[a][0] - win[b][0] || a - b);
11596
+ const r = jobs.length;
11597
+ const allowed = (t) => t >= lo - EPS$3 && t <= hi + EPS$3 && fixed.every((f) => t <= f - s + EPS$3 || t >= f + s - EPS$3);
11598
+ const starts = [
11599
+ lo,
11600
+ ...jobs.map((e) => win[e][0]),
11601
+ ...fixed.map((f) => f + s)
11602
+ ];
11603
+ const raw = [];
11604
+ for (const b of starts) for (let q = 0; q <= n; q++) {
11605
+ const t = b + q * s;
11606
+ if (t > hi + EPS$3) break;
11607
+ if (allowed(t)) raw.push(t);
11608
+ if (s === 0) break;
11609
+ }
11610
+ raw.sort((a, b) => a - b);
11611
+ const T = [];
11612
+ for (const t of raw) if (T.length === 0 || t - T[T.length - 1] > EPS$3) T.push(t);
11613
+ const m = T.length;
11614
+ const id = (k, ui, vi) => (k * (m + 2) + ui + 1) * (m + 2) + vi + 1;
11615
+ const memo = /* @__PURE__ */ new Map();
11616
+ const pick = /* @__PURE__ */ new Map();
11617
+ const count = (k, ui, vi) => {
11618
+ if (k === 0) return 0;
11619
+ const key = id(k, ui, vi);
11620
+ const got = memo.get(key);
11621
+ if (got !== void 0) return got;
11622
+ const [a, b] = win[jobs[k - 1]];
11623
+ const u = ui < 0 ? -Infinity : T[ui];
11624
+ const v = vi >= m ? Infinity : T[vi];
11625
+ let most = count(k - 1, ui, vi);
11626
+ let choice = -1;
11627
+ if (a > u + EPS$3 && a <= v + EPS$3) {
11628
+ const from = Math.max(a, ui < 0 ? lo : u + s);
11629
+ const to = Math.min(b, vi >= m ? hi : v - s);
11630
+ for (let ti = lowerBound(T, from - EPS$3); ti < m && T[ti] <= to + EPS$3; ti++) {
11631
+ const got2 = 1 + count(k - 1, ui, ti) + count(k - 1, ti, vi);
11632
+ if (got2 > most) {
11633
+ most = got2;
11634
+ choice = ti;
11635
+ }
11636
+ }
11637
+ }
11638
+ memo.set(key, most);
11639
+ if (choice >= 0) pick.set(key, choice);
11640
+ return most;
11641
+ };
11642
+ let total = r === n && count(r, -1, m) === n ? n : -1;
11643
+ let bestQ = 0;
11644
+ let bestI = -1;
11645
+ const chain = [];
11646
+ const back = [];
11647
+ for (let q = 1; q <= n; q++) {
11648
+ const cur = new Float64Array(m).fill(-Infinity);
11649
+ const from = new Int32Array(m).fill(-1);
11650
+ for (let i = 0; i < m; i++) {
11651
+ if (q === 1) {
11652
+ cur[i] = count(r, -1, i);
11653
+ continue;
11654
+ }
11655
+ const prev = chain[q - 2];
11656
+ for (let h = 0; h < i; h++) {
11657
+ if (prev[h] === -Infinity || T[i] - T[h] < s - EPS$3) continue;
11658
+ const v = prev[h] + count(r, h, i);
11659
+ if (v > cur[i]) {
11660
+ cur[i] = v;
11661
+ from[i] = h;
11662
+ }
11663
+ }
11664
+ }
11665
+ chain.push(cur);
11666
+ back.push(from);
11667
+ for (let i = 0; i < m; i++) {
11668
+ if (cur[i] === -Infinity) continue;
11669
+ const a = cur[i] + count(r, i, m);
11670
+ if (a + q >= n && a > total) {
11671
+ total = a;
11672
+ bestQ = q;
11673
+ bestI = i;
11674
+ }
11675
+ }
11676
+ }
11677
+ const placed = /* @__PURE__ */ new Map();
11678
+ const place = (k, ui, vi) => {
11679
+ if (k === 0) return;
11680
+ const t = pick.get(id(k, ui, vi));
11681
+ if (t === void 0) return place(k - 1, ui, vi);
11682
+ placed.set(jobs[k - 1], T[t]);
11683
+ place(k - 1, ui, t);
11684
+ place(k - 1, t, vi);
11685
+ };
11686
+ const free = [];
11687
+ let ui = bestI;
11688
+ for (let q = bestQ; q >= 1; q--) {
11689
+ free.push(T[ui]);
11690
+ const h = back[q - 1][ui];
11691
+ place(r, h, ui);
11692
+ ui = h;
11693
+ }
11694
+ if (bestQ === 0) place(r, -1, m);
11695
+ else place(r, bestI, m);
11696
+ free.reverse();
11697
+ const rest = order.filter((e) => !placed.has(e));
11698
+ const slots = assignInOrder(rest, free);
11699
+ const items = [];
11700
+ for (const [e, p] of placed) items.push({
11701
+ end: e,
11702
+ anchored: true,
11703
+ at: p
11704
+ });
11705
+ rest.forEach((e, i) => items.push({
11706
+ end: e,
11707
+ anchored: false,
11708
+ at: slots[i]
11709
+ }));
11710
+ for (const f of fixed) items.push({
11711
+ end: -1,
11712
+ anchored: true,
11713
+ at: f
11714
+ });
11715
+ return items.sort((a, b) => a.at - b.at || (a.end < 0 ? -1 : b.end < 0 ? 1 : order.indexOf(a.end) - order.indexOf(b.end)));
11716
+ /** This function gives each end of `list` one of the sorted places `free`, in order. The sum of the squares of the moves from the ideal places is the least. */
11717
+ function assignInOrder(list, places) {
11718
+ const a = list.length;
11719
+ const b = places.length;
11720
+ const cost = (i, j) => (places[j] - ideals[list[i]]) ** 2;
11721
+ const g = Array.from({ length: a + 1 }, () => new Float64Array(b + 1).fill(Infinity));
11722
+ for (let j = 0; j <= b; j++) g[0][j] = 0;
11723
+ for (let i = 1; i <= a; i++) for (let j = i; j <= b; j++) g[i][j] = Math.min(g[i][j - 1], g[i - 1][j - 1] + cost(i - 1, j - 1));
11724
+ const out = new Array(a);
11725
+ let j = b;
11726
+ for (let i = a; i >= 1; i--) {
11727
+ while (j > i && g[i][j - 1] === g[i][j]) j--;
11728
+ out[i - 1] = places[j - 1];
11729
+ j--;
11730
+ }
11731
+ return out;
11732
+ }
11733
+ }
11734
+ /**
11735
+ * This function gives the plan of one wall (PLUG-ASSIGN). Each end has an interval on the axis along the wall. The plugs keep at least `gap` between two of them,
11736
+ * and between a plug and a fixed plug at a place of `fixed`. When the room is too short for all the plugs, the gap shrinks evenly. The plan has three goals, in this order:
11737
+ * 1. the most ends with their plug in their interval (exact).
11738
+ * 2. no crossing at the wall: the order of the keys, when that order lets the count of goal 1.
11739
+ * 3. the least sum of the squares of the moves from the ideal places, for the order and the ends of goals 1 and 2.
11740
+ *
11741
+ * An end that cannot get a plug in its interval still gets a plug in the room. When the order of the keys loses ends, the plugs swap their places
11742
+ * to come nearer to that order, while each end keeps its interval.
11743
+ */
11744
+ function assignWall(ends, room, gap, fixed = []) {
11745
+ const n = ends.length;
11746
+ if (n === 0) return {
11747
+ at: [],
11748
+ anchored: [],
11749
+ gap
11750
+ };
11751
+ const [lo, hi] = room;
11752
+ const pins = [...fixed].sort((a, b) => a - b);
11753
+ let s = wallGap(n, pins, lo, hi, Math.max(0, gap));
11754
+ const win = ends.map((e) => {
11755
+ const a = Math.max(lo, e.lo);
11756
+ const b = Math.min(hi, e.hi);
11757
+ return a <= b + EPS$3 ? [a, Math.max(a, b)] : void 0;
11758
+ });
11759
+ const order = ends.map((_, i) => i).sort((a, b) => ends[a].key - ends[b].key || a - b);
11760
+ let items = keyOrderPlan(order, win, pins, lo, hi, s);
11761
+ if (!items) {
11762
+ s = 0;
11763
+ items = keyOrderPlan(order, win, pins, lo, hi, s);
11764
+ }
11765
+ const inKey = items.filter((x) => x.end >= 0 && x.anchored).length;
11766
+ if (inKey < win.filter((w) => w !== void 0).length && inKey < unionCap(win, pins, lo, hi, s, n)) {
11767
+ const best = bestPlan(order, win, ends.map((e) => e.ideal), pins, lo, hi, s);
11768
+ if (best.filter((x) => x.end >= 0 && x.anchored).length > inKey) items = untangle(best, ends, win);
11769
+ }
11770
+ const span = items.map((x) => x.end < 0 ? [x.at, x.at] : x.anchored ? win[x.end] : [lo, hi]);
11771
+ const placed = placeInIntervals(items.map((x) => x.end < 0 ? x.at : ends[x.end].ideal), span.map((v) => v[0]), span.map((v) => v[1]), s);
11772
+ const at = new Array(n);
11773
+ items.forEach((x, i) => {
11774
+ if (x.end >= 0) at[x.end] = placed[i];
11775
+ });
11776
+ return {
11777
+ at,
11778
+ anchored: ends.map((e, i) => at[i] >= e.lo - 1e-7 && at[i] <= e.hi + 1e-7 && win[i] !== void 0),
11779
+ gap: s
11780
+ };
11781
+ }
11782
+ /** This function gives the most plugs with the gap `s` in the union of the intervals, out of the fixed zones, and at most `n`. It is a bound of goal 1. */
11783
+ function unionCap(win, fixed, lo, hi, s, n) {
11784
+ const parts = win.filter((w) => w !== void 0).sort((a, b) => a[0] - b[0]);
11785
+ let count = 0;
11786
+ let p = -Infinity;
11787
+ for (const [a, b] of parts) {
11788
+ let v = Math.max(a, p, lo);
11789
+ for (;;) {
11790
+ v = pushOut(v, fixed, s);
11791
+ if (v > b + EPS$3 || v > hi + EPS$3 || count >= n) break;
11792
+ count++;
11793
+ p = v + s;
11794
+ v = p;
11795
+ if (s === 0) return n;
11796
+ }
11797
+ }
11798
+ return count;
11799
+ }
11800
+ /**
11801
+ * This function swaps the places of two neighbours of a plan when their keys are in the wrong order, and each end keeps its interval.
11802
+ * Thus the plan comes nearer to the order of the keys. Each swap removes one inversion, so the loop ends.
11803
+ */
11804
+ function untangle(items, ends, win) {
11805
+ const own = items.filter((x) => x.end >= 0);
11806
+ const ok = (x, p) => !x.anchored || p >= win[x.end][0] - EPS$3 && p <= win[x.end][1] + EPS$3;
11807
+ for (let changed = true, rounds = 0; changed && rounds <= own.length * own.length; rounds++) {
11808
+ changed = false;
11809
+ for (let i = 0; i + 1 < own.length; i++) {
11810
+ const x = own[i];
11811
+ const y = own[i + 1];
11812
+ if (ends[x.end].key <= ends[y.end].key) continue;
11813
+ if (!ok(x, y.at) || !ok(y, x.at)) continue;
11814
+ const t = x.at;
11815
+ x.at = y.at;
11816
+ y.at = t;
11817
+ own[i] = y;
11818
+ own[i + 1] = x;
11819
+ changed = true;
11820
+ }
11821
+ }
11822
+ return items.sort((a, b) => a.at - b.at || (a.end < 0 ? -1 : b.end < 0 ? 1 : ends[a.end].key - ends[b.end].key));
11823
+ }
11824
+ /**
11825
+ * This function gives the plugs of the plug strategy `assign` (PLUG-ASSIGN). A fixed plug stays at its point, and a plug with a side stays on that side.
11826
+ * Each free plug goes on the side that faces the other end (`sideToward`). Each end of a wall has an interval: the range of its port, or the room of the wall.
11827
+ * Then `assignWall` places the plugs of each wall, with at least `plugGap` between two of them. The ends of one port share one plug.
11828
+ *
11829
+ * The output has one entry for each edge with two known nodes, in the order of the edges.
11830
+ */
11831
+ function assignEnds(input, options = {}) {
11832
+ const { graph } = input;
11833
+ const gap = options.plugGap ?? ASSIGN_DEFAULTS.plugGap;
11834
+ const inset = options.inset ?? ASSIGN_DEFAULTS.inset;
11835
+ const byId = nodeIndex(graph);
11836
+ const slots = /* @__PURE__ */ new Map();
11837
+ const walls = /* @__PURE__ */ new Map();
11838
+ const wallOf = (node, side) => {
11839
+ const key = JSON.stringify([node.id, side]);
11840
+ let wall = walls.get(key);
11841
+ if (!wall) {
11842
+ const fixed = [];
11843
+ for (const p of node.ports ?? []) {
11844
+ const at = portOffset(node.box, p);
11845
+ if (at === void 0) continue;
11846
+ const point = {
11847
+ x: node.box.x + at.x,
11848
+ y: node.box.y + at.y
11849
+ };
11850
+ if ((p.side ?? nearestSide(node.box, point)) === side) fixed.push(point[alongOf(side)]);
11851
+ }
11852
+ walls.set(key, wall = {
11853
+ node,
11854
+ side,
11855
+ slots: [],
11856
+ fixed
11857
+ });
11858
+ }
11859
+ return wall;
11860
+ };
11861
+ const plugOf = (node, end, far) => {
11862
+ const toward = center(far.box);
11863
+ const port = end.port === void 0 ? void 0 : node.ports?.find((p) => p.id === end.port);
11864
+ if (port && portOffset(node.box, port) !== void 0) return placePlug(node, end, toward);
11865
+ const key = port ? JSON.stringify([node.id, port.id]) : JSON.stringify([node.id, slots.size]);
11866
+ if (slots.has(key)) return key;
11867
+ const side = port?.side ?? sideToward(node.box, toward);
11868
+ const ideal = placePlug(node, end, toward).point[alongOf(side)];
11869
+ const range = rangeInterval(node.box, side, port, inset);
11870
+ const slot = {
11871
+ node,
11872
+ side,
11873
+ ideal,
11874
+ at: ideal,
11875
+ ...range ? { range } : {}
11876
+ };
11877
+ slots.set(key, slot);
11878
+ wallOf(node, side).slots.push(slot);
11879
+ return key;
11880
+ };
11881
+ const ends = [];
11882
+ for (const e of graph.edges) {
11883
+ const s = byId.get(e.source.node);
11884
+ const t = byId.get(e.target.node);
11885
+ if (!s || !t) continue;
11886
+ const source = plugOf(s, e.source, t);
11887
+ const target = plugOf(t, e.target, s);
11888
+ for (const [own, far] of [[source, target], [target, source]]) {
11889
+ const slot = typeof own === "string" ? slots.get(own) : void 0;
11890
+ if (slot && slot.far === void 0) slot.far = far;
11891
+ }
11892
+ ends.push({
11893
+ edge: e.id,
11894
+ source,
11895
+ target
11896
+ });
11897
+ }
11898
+ const farPoint = (s) => {
11899
+ const far = s.far;
11900
+ if (typeof far !== "string") return far.point;
11901
+ const f = slots.get(far);
11902
+ return pointAt$1(f.node.box, f.side, f.ideal);
11903
+ };
11904
+ for (const wall of walls.values()) {
11905
+ const room = roomOf(wall.node.box, wall.side, inset);
11906
+ const plan = assignWall(wall.slots.map((s) => ({
11907
+ lo: s.range?.[0] ?? room[0],
11908
+ hi: s.range?.[1] ?? room[1],
11909
+ ideal: s.ideal,
11910
+ key: keyOf$1(wall.node.box, wall.side, farPoint(s))
11911
+ })), room, gap, wall.fixed);
11912
+ wall.slots.forEach((s, i) => {
11913
+ s.at = plan.at[i];
11914
+ });
11915
+ }
11916
+ const resolve = (p) => {
11917
+ if (typeof p !== "string") return p;
11918
+ const s = slots.get(p);
11919
+ return {
11920
+ point: pointAt$1(s.node.box, s.side, s.at),
11921
+ normal: SIDE_NORMAL[s.side]
11922
+ };
11923
+ };
11924
+ return ends.map((x) => ({
11925
+ edge: x.edge,
11926
+ source: resolve(x.source),
11927
+ target: resolve(x.target)
11928
+ }));
11929
+ }
11930
+ // src/plugs/leaders/index.ts
11931
+ /** The values of `LeaderType`. */
11932
+ const LEADER_TYPES = Object.freeze([
11933
+ "s",
11934
+ "po",
11935
+ "opo"
11936
+ ]);
11937
+ /** The defaults of the plug strategy `leaders`. */
11938
+ const LEADER_DEFAULTS = Object.freeze({
11939
+ leader: "po",
11940
+ slotSpacing: 8,
11941
+ inset: SPREAD_DEFAULTS.inset,
11942
+ rounds: SPREAD_DEFAULTS.rounds
11943
+ });
11944
+ /**
11945
+ * This function gives the plugs of the plug strategy `leaders` (EDA-041, after Bekos 2007). A fixed plug stays at its point, and a plug with a side stays on that side.
11946
+ * Each free plug goes on the side that faces the other end. The plugs of each wall keep at least `slotSpacing` between two of them.
11947
+ * The type of leader sets their order and their places. The output has one entry for each edge with two known nodes, in the order of the edges.
11948
+ */
11949
+ function leaderEnds(input, options = {}) {
11950
+ const o = {
11951
+ ...LEADER_DEFAULTS,
11952
+ ...options
11953
+ };
11954
+ return spreadWith(input, {
11955
+ plugGap: o.slotSpacing,
11956
+ inset: o.inset,
11957
+ rounds: o.rounds
11958
+ }, o.leader === "s" ? void 0 : o.leader);
11959
+ }
10244
11960
  // src/highways/model.ts
10245
11961
  /** This function gives the point at a place on a polyline. A place outside the polyline gives its nearest end. */
10246
11962
  function pointAt(line, at) {
@@ -12794,7 +14510,7 @@ function adjustedRandIndex(a, b) {
12794
14510
  return top === expected ? 1 : (index - expected) / (top - expected);
12795
14511
  }
12796
14512
  // src/bench/measures/routes.ts
12797
- const key$1 = (p) => `${Math.round(p.x * 8)},${Math.round(p.y * 8)}`;
14513
+ const key = (p) => `${Math.round(p.x * 8)},${Math.round(p.y * 8)}`;
12798
14514
  /**
12799
14515
  * This function gives the drawn segments of the routes: one segment for each distinct pair of ends.
12800
14516
  * A segment that several routes share is one drawn segment, as in `measureQuality`.
@@ -12805,8 +14521,8 @@ function drawnSegments(routes) {
12805
14521
  for (const r of routes) {
12806
14522
  const p = r.points;
12807
14523
  for (let i = 1; i < p.length; i++) {
12808
- const ka = key$1(p[i - 1]);
12809
- const kb = key$1(p[i]);
14524
+ const ka = key(p[i - 1]);
14525
+ const kb = key(p[i]);
12810
14526
  if (ka === kb) continue;
12811
14527
  const k = ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
12812
14528
  if (seen.has(k)) continue;
@@ -12818,8 +14534,8 @@ function drawnSegments(routes) {
12818
14534
  }
12819
14535
  /** This function gives the key of the drawn segment between two points. Two routes share a segment when they give the same key. */
12820
14536
  function segmentKey(a, b) {
12821
- const ka = key$1(a);
12822
- const kb = key$1(b);
14537
+ const ka = key(a);
14538
+ const kb = key(b);
12823
14539
  if (ka === kb) return void 0;
12824
14540
  return ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
12825
14541
  }
@@ -12832,8 +14548,8 @@ function boardUnit(graph) {
12832
14548
  const u = quantile(graph.nodes.map((n) => Math.min(n.box.w, n.box.h)), .5);
12833
14549
  return u > 0 ? u : 1;
12834
14550
  }
12835
- const EXACT_CROSSINGS$1 = 2e4;
12836
- const SAMPLE$1 = 2e3;
14551
+ const EXACT_CROSSINGS = 2e4;
14552
+ const SAMPLE = 2e3;
12837
14553
  /** This function gives the angle between two segments, in degrees from 0 to 90. */
12838
14554
  function crossingAngle(a, b, c, d) {
12839
14555
  const ux = b.x - a.x;
@@ -12859,9 +14575,9 @@ function crossingAngles(segs) {
12859
14575
  const len = segs.reduce((s, [a, b]) => s + dist(a, b), 0) / segs.length;
12860
14576
  const grid = new Grid(Math.max(20, len));
12861
14577
  segs.forEach(([a, b], i) => grid.addSegment(i, a, b));
12862
- const exact = segs.length <= EXACT_CROSSINGS$1;
14578
+ const exact = segs.length <= EXACT_CROSSINGS;
12863
14579
  const R = rng(segs.length);
12864
- const pick = exact ? segs.map((_, i) => i) : Array.from({ length: SAMPLE$1 }, () => Math.floor(R() * segs.length));
14580
+ const pick = exact ? segs.map((_, i) => i) : Array.from({ length: SAMPLE }, () => Math.floor(R() * segs.length));
12865
14581
  let count = 0;
12866
14582
  let sum = 0;
12867
14583
  let min = 90;
@@ -17988,6 +19704,271 @@ function kneePlan(input, o) {
17988
19704
  }
17989
19705
  return sharedStretches(ways, { minSize: Math.max(2, Math.floor(o.minSize)) });
17990
19706
  }
19707
+ // src/highways/balance/index.ts
19708
+ /**
19709
+ * The highway strategy `balance` (EDA-017): the stretch of each edge of a bundle stays near a target.
19710
+ * A Lagrange price for each edge grows while its stretch is above the target, and the price moves the exit of the edge forward.
19711
+ */
19712
+ /** The values of `BalanceEnd`. */
19713
+ const BALANCE_ENDS = Object.freeze([
19714
+ "exit",
19715
+ "entry",
19716
+ "both"
19717
+ ]);
19718
+ /** The default options of the strategy `balance`. */
19719
+ const BALANCE_DEFAULTS = Object.freeze({
19720
+ tau: 1.2,
19721
+ iterations: 5,
19722
+ end: "exit",
19723
+ from: "shared-ways"
19724
+ });
19725
+ /** The margin of the way finder of `follow`. A ride shorter than this length leaves the plan. */
19726
+ const MARGIN = 5;
19727
+ /** This function gives the length of the route of each edge of a plan after `follow`. */
19728
+ function routeLengths(graph, plan) {
19729
+ return new Map(follow(graph, plan).routes.map((r) => [r.edge, polylineLength(r.points)]));
19730
+ }
19731
+ /**
19732
+ * This function gives the plan of the highway strategy `balance` (EDA-017, after the Lagrange relaxation of the stretch).
19733
+ * The stretch of an edge is its route on the plan over its route alone, as the gate `stretch` measures it.
19734
+ * At each update, an edge with a stretch above `tau` adds `stretch - tau` to its price. Each of its rides keeps the share `1 - price` of its first length.
19735
+ * A ride shorter than the margin leaves the plan. The updates stop when no edge is above `tau`.
19736
+ */
19737
+ function balancePlan(input, options = {}) {
19738
+ const o = {
19739
+ ...BALANCE_DEFAULTS,
19740
+ ...options
19741
+ };
19742
+ const graph = outerGraph(input.graph);
19743
+ return balanceOf(graph, basePlan({
19744
+ ...input,
19745
+ graph
19746
+ }, o.from), o);
19747
+ }
19748
+ /** This function balances the plan `first` of the graph, as `balancePlan` does. It reads all the options except `from`. */
19749
+ function balanceOf(graph, first, options = {}) {
19750
+ const o = {
19751
+ ...BALANCE_DEFAULTS,
19752
+ ...options
19753
+ };
19754
+ const riders = new Set(first.highways.flatMap((h) => h.members.map((m) => m.edge)));
19755
+ if (riders.size === 0) return first;
19756
+ const sub = {
19757
+ nodes: graph.nodes,
19758
+ edges: graph.edges.filter((e) => riders.has(e.id))
19759
+ };
19760
+ const alone = routeLengths(sub, { highways: [] });
19761
+ const price = /* @__PURE__ */ new Map();
19762
+ let plan = first;
19763
+ for (let k = 0; k < Math.max(1, Math.floor(o.iterations)); k++) {
19764
+ const along = routeLengths(sub, plan);
19765
+ let over = false;
19766
+ for (const [edge, length] of along) {
19767
+ const base = alone.get(edge) ?? 0;
19768
+ if (base <= 1e-9) continue;
19769
+ const stretch = length / base;
19770
+ if (stretch <= o.tau + 1e-9) continue;
19771
+ price.set(edge, (price.get(edge) ?? 0) + stretch - o.tau);
19772
+ over = true;
19773
+ }
19774
+ if (!over) break;
19775
+ plan = tidyPlan({ highways: first.highways.map((h) => ({
19776
+ ...h,
19777
+ members: h.members.flatMap((m) => {
19778
+ const keep = 1 - Math.min(1, price.get(m.edge) ?? 0);
19779
+ if (keep >= 1) return [m];
19780
+ const span = m.off - m.on;
19781
+ const cut = (1 - keep) * span;
19782
+ const on = o.end === "entry" ? m.on + cut : o.end === "both" ? m.on + cut / 2 : m.on;
19783
+ const off = o.end === "exit" ? m.off - cut : o.end === "both" ? m.off - cut / 2 : m.off;
19784
+ if (lengthBetween(h.spine, Math.min(on, off), Math.max(on, off)) < MARGIN) return [];
19785
+ return [memberAt(h.spine, m.edge, on, off, m.seq)];
19786
+ })
19787
+ })) });
19788
+ if (plan.highways.length === 0) break;
19789
+ }
19790
+ return plan;
19791
+ }
19792
+ // src/highways/manifolds/index.ts
19793
+ /**
19794
+ * The highway strategy `manifolds` (WILD-036): a header for a fan from one node to a row of nodes.
19795
+ * The header lies in the gap before the row, the taps leave it at right angles, and the feed joins it from the node.
19796
+ */
19797
+ /** The values of `ManifoldSide`. */
19798
+ const MANIFOLD_SIDES = Object.freeze(["near", "far"]);
19799
+ /** The default options of the strategy `manifolds`. */
19800
+ const MANIFOLD_DEFAULTS = Object.freeze({
19801
+ kMin: 3,
19802
+ band: 1,
19803
+ side: "near",
19804
+ detour: 1.5,
19805
+ from: "shared-ways"
19806
+ });
19807
+ /** This function gives the median of the values, or 0 when there is none. */
19808
+ function median$1(values) {
19809
+ if (values.length === 0) return 0;
19810
+ const s = [...values].sort((a, b) => a - b);
19811
+ return s[Math.floor(s.length / 2)];
19812
+ }
19813
+ /** This function compares two texts in the order of their code units. */
19814
+ function byId(a, b) {
19815
+ return a < b ? -1 : a > b ? 1 : 0;
19816
+ }
19817
+ /**
19818
+ * This function gives the manifolds that the strategy can make from one feed node, for one axis of the header.
19819
+ * The far centers go in bands across the axis. A band with `kMin` taps or more, wholly on one side of the feed node, is a candidate.
19820
+ */
19821
+ function candidatesOf(feed, taps, axis, band, o, margin) {
19822
+ const across = axis === "x" ? "y" : "x";
19823
+ const size = axis === "x" ? "h" : "w";
19824
+ const sorted = [...taps].sort((a, b) => center(a.far.box)[across] - center(b.far.box)[across] || byId(a.edge, b.edge));
19825
+ const out = [];
19826
+ const lo = feed.box[across];
19827
+ const hi = lo + feed.box[size];
19828
+ const share = o.side === "near" ? 1 / 3 : 2 / 3;
19829
+ for (let i = 0; i < sorted.length;) {
19830
+ let j = i;
19831
+ const first = center(sorted[i].far.box)[across];
19832
+ while (j + 1 < sorted.length && center(sorted[j + 1].far.box)[across] - first <= band) j++;
19833
+ const row = sorted.slice(i, j + 1);
19834
+ i = j + 1;
19835
+ if (row.length < o.kMin) continue;
19836
+ const top = Math.min(...row.map((t) => t.far.box[across]));
19837
+ const bottom = Math.max(...row.map((t) => t.far.box[across] + t.far.box[size]));
19838
+ if (hi + 2 * margin <= top) out.push({
19839
+ feed,
19840
+ axis,
19841
+ at: top - share * (top - hi),
19842
+ taps: row
19843
+ });
19844
+ else if (bottom + 2 * margin <= lo) out.push({
19845
+ feed,
19846
+ axis,
19847
+ at: bottom + share * (lo - bottom),
19848
+ taps: row
19849
+ });
19850
+ }
19851
+ return out;
19852
+ }
19853
+ /**
19854
+ * This function gives the plan of the highway strategy `manifolds` (WILD-036, after the headers of pipes and of bus bars).
19855
+ * A fan of `kMin` edges or more from one node to a row of nodes gets a straight header in the gap before the row.
19856
+ * Each edge of the fan joins the header at the feed point, in line with the center of the node. It leaves the header at its tap, in line with the center of its far node.
19857
+ * A tap whose way is more than `detour` times the direct line leaves the manifold. Each edge is on one manifold at most. The other edges keep the plan `from`.
19858
+ */
19859
+ function manifoldPlan(input, options = {}) {
19860
+ const o = {
19861
+ ...MANIFOLD_DEFAULTS,
19862
+ ...options
19863
+ };
19864
+ const graph = outerGraph(input.graph);
19865
+ const nodes = nodeIndex(graph);
19866
+ const margin = 5;
19867
+ const bandOf = {
19868
+ x: o.band * median$1(graph.nodes.map((n) => n.box.h)),
19869
+ y: o.band * median$1(graph.nodes.map((n) => n.box.w))
19870
+ };
19871
+ const fans = /* @__PURE__ */ new Map();
19872
+ const add = (id, tap) => {
19873
+ const list = fans.get(id);
19874
+ if (list) list.push(tap);
19875
+ else fans.set(id, [tap]);
19876
+ };
19877
+ for (const e of graph.edges) {
19878
+ const s = nodes.get(e.source.node);
19879
+ const t = nodes.get(e.target.node);
19880
+ if (!s || !t || s === t) continue;
19881
+ add(s.id, {
19882
+ edge: e.id,
19883
+ far: t,
19884
+ out: true
19885
+ });
19886
+ add(t.id, {
19887
+ edge: e.id,
19888
+ far: s,
19889
+ out: false
19890
+ });
19891
+ }
19892
+ const all = [];
19893
+ for (const [id, taps] of fans) {
19894
+ if (taps.length < o.kMin) continue;
19895
+ const feed = nodes.get(id);
19896
+ for (const axis of ["x", "y"]) all.push(...candidatesOf(feed, taps, axis, bandOf[axis], o, margin));
19897
+ }
19898
+ all.sort((a, b) => b.taps.length - a.taps.length || byId(a.feed.id, b.feed.id) || byId(a.axis, b.axis) || a.at - b.at);
19899
+ const used = /* @__PURE__ */ new Set();
19900
+ const lines = [];
19901
+ for (const c of all) {
19902
+ const fc = center(c.feed.box);
19903
+ const feedPoint = c.axis === "x" ? {
19904
+ x: fc.x,
19905
+ y: c.at
19906
+ } : {
19907
+ x: c.at,
19908
+ y: fc.y
19909
+ };
19910
+ const tapOf = (t) => {
19911
+ const tc = center(t.far.box);
19912
+ return c.axis === "x" ? {
19913
+ x: tc.x,
19914
+ y: c.at
19915
+ } : {
19916
+ x: c.at,
19917
+ y: tc.y
19918
+ };
19919
+ };
19920
+ const taps = c.taps.filter((t) => {
19921
+ if (used.has(t.edge)) return false;
19922
+ const tc = center(t.far.box);
19923
+ return dist(fc, feedPoint) + dist(feedPoint, tapOf(t)) + dist(tapOf(t), tc) <= o.detour * dist(fc, tc);
19924
+ });
19925
+ if (taps.length < o.kMin) continue;
19926
+ const along = [feedPoint, ...taps.map(tapOf)].map((p) => p[c.axis]);
19927
+ const lo = Math.min(...along);
19928
+ const hi = Math.max(...along);
19929
+ const a = c.axis === "x" ? {
19930
+ x: lo,
19931
+ y: c.at
19932
+ } : {
19933
+ x: c.at,
19934
+ y: lo
19935
+ };
19936
+ const b = c.axis === "x" ? {
19937
+ x: hi,
19938
+ y: c.at
19939
+ } : {
19940
+ x: c.at,
19941
+ y: hi
19942
+ };
19943
+ for (const t of taps) used.add(t.edge);
19944
+ lines.push({
19945
+ key: `manifold:${c.feed.id}:${c.axis}`,
19946
+ a,
19947
+ b,
19948
+ members: taps.map((t) => t.out ? {
19949
+ edge: t.edge,
19950
+ from: feedPoint,
19951
+ to: tapOf(t),
19952
+ at: .5
19953
+ } : {
19954
+ edge: t.edge,
19955
+ from: tapOf(t),
19956
+ to: feedPoint,
19957
+ at: .5
19958
+ })
19959
+ });
19960
+ }
19961
+ const manifolds = snapPlan({ graph }, lines, {
19962
+ minSize: Math.max(2, o.kMin),
19963
+ direct: true
19964
+ });
19965
+ const riding = new Set(manifolds.highways.flatMap((h) => h.members.map((m) => m.edge)));
19966
+ const base = keepMembers(basePlan({
19967
+ ...input,
19968
+ graph
19969
+ }, o.from), (_, m) => !riding.has(m.edge));
19970
+ return tidyPlan({ highways: [...manifolds.highways, ...base.highways] });
19971
+ }
17991
19972
  // src/highways/bronchial/index.ts
17992
19973
  /** The values of `BronchialSplit`. */
17993
19974
  const BRONCHIAL_SPLITS = Object.freeze([
@@ -21226,6 +23207,151 @@ function venationLevels(input, options) {
21226
23207
  links: venationLinks(level.items, level.links, options)
21227
23208
  });
21228
23209
  }
23210
+ // src/levels/detail/index.ts
23211
+ /**
23212
+ * The level strategy `detail` (WILD-016 with EDA-038): the level of detail of the bundles of each zoom level.
23213
+ * Two legs of level links that are closer on the screen than `pxMin` pixels become one stroke, with a width that grows with the count.
23214
+ */
23215
+ /** The values of `WidthScale`. */
23216
+ const WIDTH_SCALES = Object.freeze([
23217
+ "linear",
23218
+ "sqrt",
23219
+ "log"
23220
+ ]);
23221
+ /** The default options of the level strategy `detail`. */
23222
+ const DETAIL_DEFAULTS = Object.freeze({
23223
+ pxMin: 3,
23224
+ minCount: 2,
23225
+ w0: 1.5,
23226
+ widthScale: "log"
23227
+ });
23228
+ /**
23229
+ * This function gives the width of a stroke of `count` edges, in pixels (WILD-016). With `log`, it is `w0 × (1 + log2(count))`.
23230
+ * With `sqrt`, it is `w0 × sqrt(count)`. With `linear`, it is `w0 × count`.
23231
+ */
23232
+ function detailWidth(count, o = DETAIL_DEFAULTS) {
23233
+ const c = Math.max(1, count);
23234
+ if (o.widthScale === "linear") return o.w0 * c;
23235
+ if (o.widthScale === "sqrt") return o.w0 * Math.sqrt(c);
23236
+ return o.w0 * (1 + Math.log2(c));
23237
+ }
23238
+ /** This function gives the inner legs of the links that are parallel to an axis. The first and the last leg of a link touch its items, so they stay. */
23239
+ function legsOf$1(links) {
23240
+ const out = [];
23241
+ links.forEach((l, link) => {
23242
+ const p = l.points;
23243
+ for (let at = 1; at + 2 < p.length; at++) {
23244
+ const a = p[at];
23245
+ const b = p[at + 1];
23246
+ if (Math.abs(a.y - b.y) <= 1e-9 && Math.abs(a.x - b.x) > 1e-9) out.push({
23247
+ link,
23248
+ at,
23249
+ axis: "x",
23250
+ across: a.y,
23251
+ lo: Math.min(a.x, b.x),
23252
+ hi: Math.max(a.x, b.x),
23253
+ count: l.count
23254
+ });
23255
+ else if (Math.abs(a.x - b.x) <= 1e-9 && Math.abs(a.y - b.y) > 1e-9) out.push({
23256
+ link,
23257
+ at,
23258
+ axis: "y",
23259
+ across: a.x,
23260
+ lo: Math.min(a.y, b.y),
23261
+ hi: Math.max(a.y, b.y),
23262
+ count: l.count
23263
+ });
23264
+ }
23265
+ });
23266
+ return out;
23267
+ }
23268
+ /** This function takes out the repeated points and the points in the middle of a straight run. The first and the last point stay. */
23269
+ function tidy(p) {
23270
+ const out = [];
23271
+ for (const q of p) {
23272
+ const last = out[out.length - 1];
23273
+ if (last && Math.abs(last.x - q.x) <= 1e-9 && Math.abs(last.y - q.y) <= 1e-9) continue;
23274
+ if (out.length >= 2) {
23275
+ const a = out[out.length - 2];
23276
+ const b = out[out.length - 1];
23277
+ const cross = (b.x - a.x) * (q.y - b.y) - (b.y - a.y) * (q.x - b.x);
23278
+ const dot = (b.x - a.x) * (q.x - b.x) + (b.y - a.y) * (q.y - b.y);
23279
+ if (Math.abs(cross) <= 1e-9 && dot > 0) out.pop();
23280
+ }
23281
+ out.push(q);
23282
+ }
23283
+ if (out.length < 2 && p.length >= 2) return [p[0], p[p.length - 1]];
23284
+ return out;
23285
+ }
23286
+ /**
23287
+ * This function collapses the legs of the links of one level that are closer than `tol` (WILD-016, EDA-038).
23288
+ * The legs of each axis go in clusters along the axis across it. The leg with the largest count of a cluster is its stroke, and each other leg moves onto it.
23289
+ * A cluster needs two legs whose projections overlap, and a count of `minCount` or more. A move that puts a leg through the box of an item does not happen.
23290
+ */
23291
+ function collapseLinks(items, links, tol, minCount) {
23292
+ if (!(tol > 0)) return [...links];
23293
+ const points = links.map((l) => [...l.points]);
23294
+ const boxes = items.map((i) => i.box);
23295
+ const clear = (a, b) => boxes.every((box) => !segmentHitsBox(a, b, box, .001));
23296
+ for (const axis of ["x", "y"]) {
23297
+ const legs = legsOf$1(links.map((l, i) => ({
23298
+ ...l,
23299
+ points: points[i]
23300
+ }))).filter((g) => g.axis === axis).sort((a, b) => a.across - b.across || a.lo - b.lo || a.link - b.link || a.at - b.at);
23301
+ const done = /* @__PURE__ */ new Set();
23302
+ for (const anchor of [...legs].sort((a, b) => b.count - a.count || b.hi - b.lo - (a.hi - a.lo) || a.link - b.link || a.at - b.at)) {
23303
+ if (done.has(anchor)) continue;
23304
+ const near = legs.filter((g) => !done.has(g) && g !== anchor && g.link !== anchor.link && Math.abs(g.across - anchor.across) < tol && Math.min(g.hi, anchor.hi) - Math.max(g.lo, anchor.lo) > 1e-9);
23305
+ if (near.length === 0 || near.reduce((s, g) => s + g.count, anchor.count) < minCount) continue;
23306
+ done.add(anchor);
23307
+ for (const g of near) {
23308
+ done.add(g);
23309
+ const p = points[g.link];
23310
+ const a = p[g.at];
23311
+ const b = p[g.at + 1];
23312
+ if (axis === "x" ? a.y !== g.across || b.y !== g.across : a.x !== g.across || b.x !== g.across) continue;
23313
+ const a2 = axis === "x" ? {
23314
+ x: a.x,
23315
+ y: anchor.across
23316
+ } : {
23317
+ x: anchor.across,
23318
+ y: a.y
23319
+ };
23320
+ const b2 = axis === "x" ? {
23321
+ x: b.x,
23322
+ y: anchor.across
23323
+ } : {
23324
+ x: anchor.across,
23325
+ y: b.y
23326
+ };
23327
+ if (!clear(p[g.at - 1], a2) || !clear(a2, b2) || !clear(b2, p[g.at + 2])) continue;
23328
+ p[g.at] = a2;
23329
+ p[g.at + 1] = b2;
23330
+ }
23331
+ }
23332
+ }
23333
+ return links.map((l, i) => points[i].every((q, k) => q === l.points[k]) ? l : {
23334
+ ...l,
23335
+ points: tidy(points[i])
23336
+ });
23337
+ }
23338
+ /**
23339
+ * This function gives the zoom levels of the level strategy `detail` (WILD-016 with EDA-038). The levels and their items are the levels of `depths`.
23340
+ * A level with groups first shows at the scale `itemAt / size`, with `size` its median item (`pickLevel`). At that scale, `pxMin` pixels are `pxMin × size / itemAt` units.
23341
+ * The links of the level collapse their legs that are closer than that length (`collapseLinks`). The last level stays as in `depths`.
23342
+ */
23343
+ function detailLevels(input, options) {
23344
+ const levels = depthLevels(input, options);
23345
+ const sizes = itemSizes(levels);
23346
+ return levels.map((level, i) => {
23347
+ if (i === levels.length - 1) return level;
23348
+ const tol = options.pxMin * sizes[i] / PICK.itemAt;
23349
+ return {
23350
+ ...level,
23351
+ links: collapseLinks(level.items, level.links, tol, options.minCount)
23352
+ };
23353
+ });
23354
+ }
21229
23355
  // src/strategies/options.ts
21230
23356
  /** The number of values of a sweep on a range when the call and the declaration give no number. */
21231
23357
  const SWEEP_COUNT = 5;
@@ -21491,190 +23617,6 @@ function runPipeline(graph, options) {
21491
23617
  result: asRouter(options.router, routerOptions).route(board)
21492
23618
  };
21493
23619
  }
21494
- // src/bench/quality.ts
21495
- /** The clearance stops at this distance. */
21496
- const CLEARANCE_CAP = 40;
21497
- const TIGHT = 4;
21498
- const EXACT_CROSSINGS = 2e4;
21499
- const SAMPLE = 2e3;
21500
- const key = (p) => `${Math.round(p.x * 8)},${Math.round(p.y * 8)}`;
21501
- /** This function measures the quality of a result. */
21502
- function measureQuality(graph, result) {
21503
- const byId = nodeIndex(graph);
21504
- const nodes = new Grid(180);
21505
- for (const n of graph.nodes) nodes.add(n, n.box);
21506
- const segs = [];
21507
- const seen = /* @__PURE__ */ new Set();
21508
- let bends = 0;
21509
- let infl = 0;
21510
- let clear = 0;
21511
- let tight = 0;
21512
- let detour = 0;
21513
- let detourN = 0;
21514
- let angle = 0;
21515
- let angleN = 0;
21516
- for (const r of result.routes) {
21517
- const p = r.points;
21518
- for (let i = 1; i < p.length; i++) {
21519
- const ka = key(p[i - 1]);
21520
- const kb = key(p[i]);
21521
- if (ka === kb) continue;
21522
- const k = ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
21523
- if (seen.has(k)) continue;
21524
- seen.add(k);
21525
- segs.push([p[i - 1], p[i]]);
21526
- }
21527
- bends += bendCount(p);
21528
- infl += inflectionCount(p);
21529
- const ends = endNodes(graph, byId, r.edge);
21530
- let c = 40;
21531
- for (let i = 1; i < p.length; i++) {
21532
- const a = p[i - 1];
21533
- const b = p[i];
21534
- nodes.querySegment(a, b, c, (n) => {
21535
- if (n !== ends.source && n !== ends.target) c = Math.min(c, segmentBoxDistance(a, b, n.box));
21536
- });
21537
- }
21538
- clear += c;
21539
- if (c < TIGHT) tight++;
21540
- if (p.length >= 2) {
21541
- const d = dist(p[0], p[p.length - 1]);
21542
- if (d > 1) {
21543
- detour += polylineLength(p) / d;
21544
- detourN++;
21545
- }
21546
- for (const [at, next, node] of [[
21547
- p[0],
21548
- p[1],
21549
- ends.source
21550
- ], [
21551
- p[p.length - 1],
21552
- p[p.length - 2],
21553
- ends.target
21554
- ]]) {
21555
- if (!node) continue;
21556
- const a = sideAngle(node.box, at, next);
21557
- if (a !== void 0) {
21558
- angle += a;
21559
- angleN++;
21560
- }
21561
- }
21562
- }
21563
- }
21564
- const n = Math.max(1, result.routes.length);
21565
- let ink = 0;
21566
- for (const [a, b] of segs) ink += dist(a, b);
21567
- const [crossings, crossingsEstimated] = countCrossings(segs);
21568
- return {
21569
- ink,
21570
- crossings,
21571
- crossingsEstimated,
21572
- bends: bends / n,
21573
- inflections: infl / n,
21574
- clearance: clear / n,
21575
- tight: tight / n,
21576
- detour: detourN ? detour / detourN : 1,
21577
- plugAngle: angleN ? angle / angleN : 0
21578
- };
21579
- }
21580
- function endNodes(graph, byId, edgeId) {
21581
- const e = edgeIndex(graph).get(edgeId);
21582
- return {
21583
- source: e && byId.get(e.source.node),
21584
- target: e && byId.get(e.target.node)
21585
- };
21586
- }
21587
- const edgeIndexes = /* @__PURE__ */ new WeakMap();
21588
- function edgeIndex(graph) {
21589
- let m = edgeIndexes.get(graph);
21590
- if (!m) {
21591
- m = /* @__PURE__ */ new Map();
21592
- for (const e of graph.edges) m.set(e.id, e);
21593
- edgeIndexes.set(graph, m);
21594
- }
21595
- return m;
21596
- }
21597
- /**
21598
- * This function gives an angle in degrees: the angle between the segment from `at` to `next` and the outward normal.
21599
- * The normal is the normal of the side of the box nearest to `at`. A route that starts inside the box (an interior route) gives no angle.
21600
- */
21601
- function sideAngle(box, at, next) {
21602
- const d = [
21603
- [
21604
- Math.abs(at.y - box.y),
21605
- 0,
21606
- -1
21607
- ],
21608
- [
21609
- Math.abs(at.x - (box.x + box.w)),
21610
- 1,
21611
- 0
21612
- ],
21613
- [
21614
- Math.abs(at.y - (box.y + box.h)),
21615
- 0,
21616
- 1
21617
- ],
21618
- [
21619
- Math.abs(at.x - box.x),
21620
- -1,
21621
- 0
21622
- ]
21623
- ].sort((a, b) => a[0] - b[0])[0];
21624
- if (d[0] > 1) return void 0;
21625
- const vx = next.x - at.x;
21626
- const vy = next.y - at.y;
21627
- const l = Math.hypot(vx, vy);
21628
- if (l === 0) return void 0;
21629
- const cos = Math.max(-1, Math.min(1, (vx * d[1] + vy * d[2]) / l));
21630
- return Math.acos(cos) * 180 / Math.PI;
21631
- }
21632
- /**
21633
- * This function counts the crossings of a set of segments with a grid of buckets.
21634
- * Above 20 000 segments, it counts the crossings of a seeded sample of 2 000 segments and scales the count.
21635
- */
21636
- function countCrossings(segs) {
21637
- if (segs.length < 2) return [0, false];
21638
- const len = segs.reduce((s, [a, b]) => s + dist(a, b), 0) / segs.length;
21639
- const grid = new Grid(Math.max(20, len));
21640
- segs.forEach(([a, b], i) => grid.addSegment(i, a, b));
21641
- const exact = segs.length <= EXACT_CROSSINGS;
21642
- const R = rng(segs.length);
21643
- const pick = exact ? segs.map((_, i) => i) : Array.from({ length: SAMPLE }, () => Math.floor(R() * segs.length));
21644
- let count = 0;
21645
- for (const i of pick) {
21646
- const [a, b] = segs[i];
21647
- grid.querySegment(a, b, 0, (j) => {
21648
- if (exact && j <= i) return;
21649
- if (j === i) return;
21650
- const [c, d] = segs[j];
21651
- if (segmentsCross(a, b, c, d)) count++;
21652
- });
21653
- }
21654
- return exact ? [count, false] : [Math.round(count * segs.length / (2 * pick.length)), true];
21655
- }
21656
- /** This function measures how much the routes changed between two results. */
21657
- function measureStability(before, after) {
21658
- const prev = new Map(before.routes.map((r) => [r.edge, r.points]));
21659
- let sum = 0;
21660
- let max = 0;
21661
- let moved = 0;
21662
- let n = 0;
21663
- for (const r of after.routes) {
21664
- const p = prev.get(r.edge);
21665
- if (!p) continue;
21666
- const f = frechet(p, r.points);
21667
- sum += f;
21668
- max = Math.max(max, f);
21669
- if (f > 1) moved++;
21670
- n++;
21671
- }
21672
- return {
21673
- mean: n ? sum / n : 0,
21674
- max,
21675
- moved: n ? moved / n : 0
21676
- };
21677
- }
21678
23620
  // src/whole/portfolio/index.ts
21679
23621
  /** The clock in milliseconds. */
21680
23622
  const clock = () => globalThis.performance?.now() ?? Date.now();
@@ -21761,6 +23703,916 @@ function measuresOf(graph, result) {
21761
23703
  ink: inkSaved(result.routes)
21762
23704
  };
21763
23705
  }
23706
+ // src/whole/curriculum/index.ts
23707
+ /** The values of `CurriculumOrder`. */
23708
+ const CURRICULUM_ORDERS = Object.freeze([
23709
+ "R1-R12-R2",
23710
+ "R12-R1-R2",
23711
+ "R1-R2-R12"
23712
+ ]);
23713
+ /** The default options of the strategy `curriculum`. */
23714
+ const CURRICULUM_DEFAULTS = Object.freeze({
23715
+ order: "R1-R12-R2",
23716
+ passes: 1,
23717
+ settings: 3,
23718
+ detour: 2
23719
+ });
23720
+ /** Two legs closer than this distance count for the lane separation (R12), as in the screen. */
23721
+ const NEAR = 4;
23722
+ /** This function gives the shape of a route. */
23723
+ function shapeOf(route) {
23724
+ let x0 = Infinity;
23725
+ let y0 = Infinity;
23726
+ let x1 = -Infinity;
23727
+ let y1 = -Infinity;
23728
+ for (const p of route.points) {
23729
+ x0 = Math.min(x0, p.x);
23730
+ y0 = Math.min(y0, p.y);
23731
+ x1 = Math.max(x1, p.x);
23732
+ y1 = Math.max(y1, p.y);
23733
+ }
23734
+ return {
23735
+ route,
23736
+ x0,
23737
+ y0,
23738
+ x1,
23739
+ y1
23740
+ };
23741
+ }
23742
+ /** The sine of 5 degrees, as a literal. A literal gives the same bits on each platform, so a decision does not depend on `Math.sin`. */
23743
+ const SIN_5 = .08715574274765817;
23744
+ /** The constants of the arc sine of fdlibm (`e_asin.c`). */
23745
+ const PIO2_HI = 1.5707963267948966;
23746
+ const PIO2_LO = 6123233995736766e-32;
23747
+ const PIO4_HI = .7853981633974483;
23748
+ const PS = [
23749
+ .16666666666666666,
23750
+ -.3255658186224009,
23751
+ .20121253213486293,
23752
+ -.04005553450067941,
23753
+ .0007915349942898145,
23754
+ 3479331075960212e-20
23755
+ ];
23756
+ const QS = [
23757
+ -2.403394911734414,
23758
+ 2.0209457602335057,
23759
+ -.6882839716054533,
23760
+ .07703815055590194
23761
+ ];
23762
+ const WORDS = /* @__PURE__ */ new DataView(/* @__PURE__ */ new ArrayBuffer(8));
23763
+ /** This function gives the rational part `p / q` of the arc sine of fdlibm at `t`. */
23764
+ function asinRatio(t) {
23765
+ return [t * (PS[0] + t * (PS[1] + t * (PS[2] + t * (PS[3] + t * (PS[4] + t * PS[5]))))), 1 + t * (QS[0] + t * (QS[1] + t * (QS[2] + t * QS[3])))];
23766
+ }
23767
+ /**
23768
+ * This function gives the arc sine of `x` in radians, from -1 to 1. It is the algorithm of fdlibm, with only `+`, `*`, `/` and `Math.sqrt`.
23769
+ * These operations are exact to the last bit on each platform, so the result does not depend on the engine.
23770
+ */
23771
+ function arcsin(x) {
23772
+ const ax = Math.abs(x);
23773
+ if (Number.isNaN(x) || ax > 1) return NaN;
23774
+ if (ax === 1) return x * PIO2_HI + x * PIO2_LO;
23775
+ if (ax < .5) {
23776
+ if (ax < 2 ** -27) return x;
23777
+ const [p, q] = asinRatio(x * x);
23778
+ return x + x * (p / q);
23779
+ }
23780
+ const t = (1 - ax) * .5;
23781
+ const [p, q] = asinRatio(t);
23782
+ const s = Math.sqrt(t);
23783
+ let r;
23784
+ WORDS.setFloat64(0, ax);
23785
+ if (WORDS.getUint32(0) >= 1072640819) r = PIO2_HI - (2 * (s + s * (p / q)) - PIO2_LO);
23786
+ else {
23787
+ WORDS.setFloat64(0, s);
23788
+ WORDS.setUint32(4, 0);
23789
+ const w = WORDS.getFloat64(0);
23790
+ const c = (t - w * w) / (s + w);
23791
+ const pp = 2 * s * (p / q) - (PIO2_LO - 2 * c);
23792
+ const qq = PIO4_HI - 2 * w;
23793
+ r = PIO4_HI - (pp - qq);
23794
+ }
23795
+ return x > 0 ? r : -r;
23796
+ }
23797
+ /** This function gives the distance between the segments `ab` and `cd`, when they do not cross. */
23798
+ function segmentGap(a, b, c, d) {
23799
+ const at = (p, u, v) => {
23800
+ const dx = v.x - u.x;
23801
+ const dy = v.y - u.y;
23802
+ const l2 = dx * dx + dy * dy;
23803
+ const t = l2 > 0 ? Math.max(0, Math.min(1, ((p.x - u.x) * dx + (p.y - u.y) * dy) / l2)) : 0;
23804
+ const ex = p.x - u.x - t * dx;
23805
+ const ey = p.y - u.y - t * dy;
23806
+ return Math.sqrt(ex * ex + ey * ey);
23807
+ };
23808
+ return Math.min(at(a, c, d), at(b, c, d), at(c, a, b), at(d, a, b));
23809
+ }
23810
+ /**
23811
+ * This function gives the costs of the rules between two routes. R1 is the number of crossings.
23812
+ * R2 is the sum of `1 - θ / 90` over the crossings. θ is the crossing angle in degrees.
23813
+ * R12 is the overlap of the legs of `a` and `b` that are within 5 degrees of parallel. The two legs are nearer than 4 units.
23814
+ */
23815
+ function pairCosts(a, b) {
23816
+ let r1 = 0;
23817
+ let r2 = 0;
23818
+ let r12 = 0;
23819
+ for (let i = 1; i < a.length; i++) {
23820
+ const p = a[i - 1];
23821
+ const q = a[i];
23822
+ const ux = q.x - p.x;
23823
+ const uy = q.y - p.y;
23824
+ const lu = Math.sqrt(ux * ux + uy * uy);
23825
+ if (lu <= 1e-9) continue;
23826
+ for (let j = 1; j < b.length; j++) {
23827
+ const r = b[j - 1];
23828
+ const s = b[j];
23829
+ const vx = s.x - r.x;
23830
+ const vy = s.y - r.y;
23831
+ const lv = Math.sqrt(vx * vx + vy * vy);
23832
+ if (lv <= 1e-9) continue;
23833
+ if (segmentsCross(p, q, r, s)) {
23834
+ r1++;
23835
+ const angle = arcsin(Math.min(1, Math.abs(ux * vy - uy * vx) / (lu * lv))) * 180 / Math.PI;
23836
+ r2 += 1 - angle / 90;
23837
+ continue;
23838
+ }
23839
+ if (Math.abs(ux * vy - uy * vx) > SIN_5 * lu * lv) continue;
23840
+ const t0 = ((r.x - p.x) * ux + (r.y - p.y) * uy) / lu;
23841
+ const t1 = ((s.x - p.x) * ux + (s.y - p.y) * uy) / lu;
23842
+ const overlap = Math.min(lu, Math.max(t0, t1)) - Math.max(0, Math.min(t0, t1));
23843
+ if (overlap > 1e-9 && segmentGap(p, q, r, s) < NEAR) r12 += overlap;
23844
+ }
23845
+ }
23846
+ return {
23847
+ R1: r1,
23848
+ R2: r2,
23849
+ R12: r12
23850
+ };
23851
+ }
23852
+ /** This function gives the detour of a route: its length over the distance between its ends. */
23853
+ function detourOf$1(points) {
23854
+ const span = dist(points[0], points[points.length - 1]);
23855
+ return span > 1e-9 ? polylineLength(points) / span : 1;
23856
+ }
23857
+ /**
23858
+ * This function runs the whole strategy `curriculum` (RULE-048, after curriculum learning, Bengio 2009). The record has no energy solver yet (RULE-019).
23859
+ * Thus a pool of routes of fixed settings takes its place. Each edge can take its route from any setting of the pool.
23860
+ * The first answer is the first setting. Each stage adds one rule, in the order `order`. A pass of a stage visits the edges, the worst first.
23861
+ * An edge takes the route of the pool with the least cost of the new rule. That cost is lower, and the earlier rules do not get worse.
23862
+ * A route with a break of a hard rule is not in the pool. The output has the routes alone, with no runs, bundles or levels.
23863
+ */
23864
+ function curriculum(graph, list, options = {}) {
23865
+ const o = {
23866
+ ...CURRICULUM_DEFAULTS,
23867
+ ...options
23868
+ };
23869
+ const count = Math.max(1, Math.min(list.length, Math.floor(o.settings)));
23870
+ const pool = /* @__PURE__ */ new Map();
23871
+ let first;
23872
+ for (const s of list.slice(0, count)) {
23873
+ const limit = (s.pipeline.highway ?? s.pipeline.router).maxNodes;
23874
+ if (limit !== void 0 && graph.nodes.length > limit) continue;
23875
+ const output = runPipeline(graph, s.pipeline);
23876
+ first ??= output;
23877
+ const faulty = new Set(checkProperties(output.graph, output.result, 1e6).map((v) => v.edge));
23878
+ for (const r of output.result.routes) {
23879
+ if (faulty.has(r.edge) || r.points.length < 2) continue;
23880
+ const list2 = pool.get(r.edge);
23881
+ if (list2) list2.push(r);
23882
+ else pool.set(r.edge, [r]);
23883
+ }
23884
+ }
23885
+ if (!first) return runPipeline(graph, list[0].pipeline);
23886
+ const rules = o.order.split("-");
23887
+ const current = new Map(first.result.routes.map((r) => [r.edge, shapeOf(r)]));
23888
+ for (const [edge, shape] of current) {
23889
+ const own = pool.get(edge);
23890
+ if (own && !own.includes(shape.route)) current.set(edge, shapeOf(own[0]));
23891
+ }
23892
+ const costOf = (edge, s) => {
23893
+ const sum = {
23894
+ R1: 0,
23895
+ R2: 0,
23896
+ R12: 0
23897
+ };
23898
+ for (const [other, t] of current) {
23899
+ if (other === edge || t.x0 > s.x1 + NEAR || t.x1 < s.x0 - NEAR || t.y0 > s.y1 + NEAR || t.y1 < s.y0 - NEAR) continue;
23900
+ const c = pairCosts(s.route.points, t.route.points);
23901
+ sum.R1 += c.R1;
23902
+ sum.R2 += c.R2;
23903
+ sum.R12 += c.R12;
23904
+ }
23905
+ return sum;
23906
+ };
23907
+ const edges = [...current.keys()];
23908
+ for (let stage = 0; stage < rules.length; stage++) {
23909
+ const rule = rules[stage];
23910
+ const earlier = rules.slice(0, stage);
23911
+ for (let pass = 0; pass < Math.max(1, Math.floor(o.passes)); pass++) {
23912
+ const costs = new Map(edges.map((e) => [e, costOf(e, current.get(e))]));
23913
+ const order = [...edges].sort((a, b) => costs.get(b)[rule] - costs.get(a)[rule] || (a < b ? -1 : a > b ? 1 : 0));
23914
+ let moved = 0;
23915
+ for (const edge of order) {
23916
+ const now = current.get(edge);
23917
+ const base = costOf(edge, now);
23918
+ if (base[rule] <= 1e-9) continue;
23919
+ const cap = Math.max(o.detour, detourOf$1(now.route.points));
23920
+ let best;
23921
+ for (const r of pool.get(edge) ?? []) {
23922
+ if (r === now.route || detourOf$1(r.points) > cap + 1e-9) continue;
23923
+ const shape = shapeOf(r);
23924
+ const c = costOf(edge, shape);
23925
+ if (c[rule] >= base[rule] - 1e-9 || earlier.some((k) => c[k] > base[k] + 1e-9)) continue;
23926
+ if (!best || c[rule] < best.cost - 1e-9) best = {
23927
+ shape,
23928
+ cost: c[rule]
23929
+ };
23930
+ }
23931
+ if (best) {
23932
+ current.set(edge, best.shape);
23933
+ moved++;
23934
+ }
23935
+ }
23936
+ if (moved === 0) break;
23937
+ }
23938
+ }
23939
+ const routes = first.result.routes.map((r) => current.get(r.edge)?.route ?? r);
23940
+ return {
23941
+ graph: first.graph,
23942
+ result: { routes }
23943
+ };
23944
+ }
23945
+ // src/highways/slots/index.ts
23946
+ /**
23947
+ * The highway strategy `slots` (RULE-047): a pull of each route toward the lane slots of the highway skeleton.
23948
+ * An edge whose first route runs near a trunk of the plan joins that trunk when the pull is larger than its detour.
23949
+ */
23950
+ /** The values of `SlotKeys`. */
23951
+ const SLOT_KEYS = Object.freeze(["same", "any"]);
23952
+ /** The default options of the strategy `slots`. */
23953
+ const SLOT_DEFAULTS = Object.freeze({
23954
+ pull: 2,
23955
+ reach: 24,
23956
+ laneSpacing: 4,
23957
+ maxStretch: 1.25,
23958
+ keys: "same",
23959
+ from: "shared-ways"
23960
+ });
23961
+ /** This function gives points along a polyline, one at each `step` of length, and the last point. */
23962
+ function samplesOf$1(points, step) {
23963
+ const out = [];
23964
+ if (points.length === 0) return out;
23965
+ out.push({
23966
+ p: points[0],
23967
+ arc: 0
23968
+ });
23969
+ let walked = 0;
23970
+ let next = step;
23971
+ for (let i = 1; i < points.length; i++) {
23972
+ const a = points[i - 1];
23973
+ const b = points[i];
23974
+ const len = dist(a, b);
23975
+ while (len > 0 && next <= walked + len) {
23976
+ const u = (next - walked) / len;
23977
+ out.push({
23978
+ p: {
23979
+ x: a.x + (b.x - a.x) * u,
23980
+ y: a.y + (b.y - a.y) * u
23981
+ },
23982
+ arc: next
23983
+ });
23984
+ next += step;
23985
+ }
23986
+ walked += len;
23987
+ }
23988
+ if (walked > out[out.length - 1].arc + 1e-9) out.push({
23989
+ p: points[points.length - 1],
23990
+ arc: walked
23991
+ });
23992
+ return out;
23993
+ }
23994
+ /**
23995
+ * This function gives the best join of one route to the trunk `h`, or `undefined`.
23996
+ * The longest row of samples nearer than `reach` to the spine is the part of the route that the trunk pulls.
23997
+ * The slot of the new lane is at `laneSpacing × (lanes + 1) / 2` from the spine. The weight is `w(d) = max(0, 1 - d / r)`.
23998
+ */
23999
+ function slotJoin(route, h, index, o) {
24000
+ const step = Math.max(2, o.reach / 4);
24001
+ const samples = samplesOf$1(route, step);
24002
+ const offset = o.laneSpacing * (h.lanes.length + 1) / 2;
24003
+ let best;
24004
+ let start = -1;
24005
+ const near = samples.map((s) => {
24006
+ const at = nearestPlace(h.spine, s.p);
24007
+ return {
24008
+ at,
24009
+ d: dist(s.p, pointAt(h.spine, at))
24010
+ };
24011
+ });
24012
+ for (let i = 0; i <= samples.length; i++) {
24013
+ const inside = i < samples.length && near[i].d < o.reach;
24014
+ if (inside && start < 0) start = i;
24015
+ if (!inside && start >= 0) {
24016
+ if (!best || samples[i - 1].arc - samples[start].arc > samples[best.to].arc - samples[best.from].arc) best = {
24017
+ from: start,
24018
+ to: i - 1
24019
+ };
24020
+ start = -1;
24021
+ }
24022
+ }
24023
+ if (!best || best.to <= best.from) return void 0;
24024
+ const on = near[best.from].at;
24025
+ const off = near[best.to].at;
24026
+ const ride = lengthBetween(h.spine, Math.min(on, off), Math.max(on, off));
24027
+ if (ride < 2 * o.reach || Math.abs(on - off) < 1e-9) return void 0;
24028
+ let pull = 0;
24029
+ for (let i = best.from; i <= best.to; i++) {
24030
+ const d = near[i].d;
24031
+ const w = Math.max(0, 1 - d / o.reach);
24032
+ const gap = d - offset;
24033
+ pull += w * gap * gap;
24034
+ }
24035
+ pull = o.pull * pull * step / (o.reach * o.reach);
24036
+ const arc = samples[best.to].arc - samples[best.from].arc;
24037
+ const detour = near[best.from].d + near[best.to].d + ride - arc;
24038
+ return {
24039
+ highway: index,
24040
+ on,
24041
+ off,
24042
+ pull,
24043
+ detour
24044
+ };
24045
+ }
24046
+ /**
24047
+ * This function gives the plan of the highway strategy `slots` (RULE-047, after the pull of force-directed edge bundling, Holten and van Wijk 2009).
24048
+ * The trunks are the highways of the plan `from`. Each edge outside the plan gets a pull toward the slot of a new lane of a trunk near its first route.
24049
+ * The energy of the pull is `E = k Σ w(d_i) ‖x_i - slot_i‖²` over the samples of the route, with `w(d) = max(0, 1 - d / r)`.
24050
+ * The edge joins the trunk when the pull is larger than the length that the join adds, and the stretch stays under `maxStretch`.
24051
+ * The edge takes the trunk with the largest gain, on the outer lane. An edge that does not fit stays off the trunks.
24052
+ */
24053
+ function slotPlan(input, options = {}) {
24054
+ const o = {
24055
+ ...SLOT_DEFAULTS,
24056
+ ...options
24057
+ };
24058
+ const graph = outerGraph(input.graph);
24059
+ return slotsOf(graph, basePlan({
24060
+ ...input,
24061
+ graph
24062
+ }, o.from), firstRoutes({
24063
+ ...input,
24064
+ graph
24065
+ }), o);
24066
+ }
24067
+ /** This function adds the joins of `slotPlan` to the plan `first` of the graph. It reads all the options except `from`. A join that breaks a hard rule after `follow` leaves the plan. */
24068
+ function slotsOf(graph, first, routes, options = {}) {
24069
+ const o = {
24070
+ ...SLOT_DEFAULTS,
24071
+ ...options
24072
+ };
24073
+ if (first.highways.length === 0) return first;
24074
+ const riders = new Set(first.highways.flatMap((h) => h.members.map((m) => m.edge)));
24075
+ const added = first.highways.map(() => []);
24076
+ const edges = [...graph.edges].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
24077
+ for (const e of edges) {
24078
+ if (riders.has(e.id)) continue;
24079
+ const route = routes.get(e.id);
24080
+ if (!route || route.length < 2) continue;
24081
+ const length = polylineLength(route);
24082
+ if (length <= 1e-9) continue;
24083
+ const key = byCollection(e);
24084
+ let best;
24085
+ first.highways.forEach((h, i) => {
24086
+ if (o.keys === "same" && (key === void 0 || h.key !== key)) return;
24087
+ const j = slotJoin(route, h, i, o);
24088
+ if (!j || j.pull <= j.detour || (length + Math.max(0, j.detour)) / length > o.maxStretch + 1e-9) return;
24089
+ if (!best || j.pull - j.detour > best.pull - best.detour + 1e-9) best = j;
24090
+ });
24091
+ if (best) added[best.highway].push({
24092
+ edge: e.id,
24093
+ on: best.on,
24094
+ off: best.off
24095
+ });
24096
+ }
24097
+ const build = () => tidyPlan({ highways: first.highways.map((h, i) => {
24098
+ const more = added[i];
24099
+ if (more.length === 0) return h;
24100
+ return {
24101
+ ...h,
24102
+ lanes: [...h.lanes, ...more.map((m) => m.edge)],
24103
+ members: [...h.members, ...more.map((m) => memberAt(h.spine, m.edge, m.on, m.off, 0))]
24104
+ };
24105
+ }) });
24106
+ const before = new Set(checkProperties(graph, follow(graph, first), 1e6).flatMap((v) => v.edge === void 0 ? [] : [v.edge]));
24107
+ for (let round = 0; round < 4; round++) {
24108
+ if (added.every((list) => list.length === 0)) return first;
24109
+ const plan = build();
24110
+ const faulty = new Set(checkProperties(graph, follow(graph, plan), 1e6).flatMap((v) => v.edge === void 0 ? [] : [v.edge]));
24111
+ const joined = new Set(added.flatMap((list) => list.map((m) => m.edge)));
24112
+ const others = [...faulty].filter((e) => !joined.has(e) && !before.has(e));
24113
+ if (![...faulty].some((e) => joined.has(e)) && others.length === 0) return plan;
24114
+ first.highways.forEach((h, i) => {
24115
+ const hit = others.some((e) => h.members.some((m) => m.edge === e));
24116
+ added[i] = hit ? [] : added[i].filter((m) => !faulty.has(m.edge));
24117
+ });
24118
+ }
24119
+ return first;
24120
+ }
24121
+ // src/whole/preferences/index.ts
24122
+ /** The features of one route, in order: crossings (R1), angle cost (R2), lane overlap (R12) over 20, detour minus 1 (R5), and bends over 2. */
24123
+ const PREFERENCE_FEATURES = Object.freeze([
24124
+ "R1",
24125
+ "R2",
24126
+ "R12",
24127
+ "R5",
24128
+ "bends"
24129
+ ]);
24130
+ /** The scale of the lane overlap in the features, in units of length. */
24131
+ const OVERLAP_SCALE = 20;
24132
+ /**
24133
+ * The weights of the made-up panel that labels the made-up pairs, in the order of `PREFERENCE_FEATURES`.
24134
+ * The order comes from the studies of the readers of graphs: the crossings count most, then the bends and the length, then the angle (Purchase 1997, Ware, Purchase, Colpoys and McGill 2002).
24135
+ * The numbers are made up. They do not come from the screen or from a board.
24136
+ */
24137
+ const PREFERENCE_PANEL = Object.freeze([
24138
+ 3,
24139
+ .75,
24140
+ .5,
24141
+ 1.5,
24142
+ 1
24143
+ ]);
24144
+ /** The default options of the strategy `preferences`. */
24145
+ const PREFERENCE_DEFAULTS = Object.freeze({
24146
+ l2: .01,
24147
+ passes: 1,
24148
+ settings: 3,
24149
+ detour: 2
24150
+ });
24151
+ /**
24152
+ * This function gives a smooth step from 0 to 1, a stand-in for the logistic function with only `+`, `*`, `/` and `Math.abs`.
24153
+ * It is `0.5 + 0.5 z / (1 + |z|)`. These operations are exact to the last bit on each platform.
24154
+ */
24155
+ function softStep(z) {
24156
+ return .5 + .5 * z / (1 + Math.abs(z));
24157
+ }
24158
+ /** This function gives a made-up feature vector of one route. */
24159
+ function madeUpFeatures(random) {
24160
+ return [
24161
+ Math.floor(random() * 5),
24162
+ random() * 2,
24163
+ random() * 2,
24164
+ random(),
24165
+ Math.floor(random() * 7) / 2
24166
+ ];
24167
+ }
24168
+ /** This function gives a noise with a mean of 0 and a spread of approximately 0.6: the sum of three uniform values. */
24169
+ function noise(random) {
24170
+ return (random() + random() + random() - 1.5) * 1.2;
24171
+ }
24172
+ /** This function gives the utility of a feature vector for the weights `w`. A larger utility is better. */
24173
+ function utility(w, f) {
24174
+ let u = 0;
24175
+ for (let i = 0; i < f.length; i++) u -= w[i] * f[i];
24176
+ return u;
24177
+ }
24178
+ /**
24179
+ * This function gives `count` made-up pairs, with the labels of the made-up panel `PREFERENCE_PANEL` and a noise.
24180
+ * The pairs come from the seed alone. They do not come from a board.
24181
+ */
24182
+ function madeUpPairs(count, seed) {
24183
+ const random = rng(seed);
24184
+ const out = [];
24185
+ for (let i = 0; i < count; i++) {
24186
+ const a = madeUpFeatures(random);
24187
+ const b = madeUpFeatures(random);
24188
+ out.push({
24189
+ a,
24190
+ b,
24191
+ first: utility(PREFERENCE_PANEL, a) - utility(PREFERENCE_PANEL, b) + noise(random) > 0 ? 1 : 0
24192
+ });
24193
+ }
24194
+ return out;
24195
+ }
24196
+ /** This function gives the share of the pairs where the weights `w` prefer the same route as the panel. */
24197
+ function agreementOf(w, pairs) {
24198
+ if (pairs.length === 0) return 0;
24199
+ let n = 0;
24200
+ for (const p of pairs) if ((utility(w, p.a) > utility(w, p.b) ? 1 : 0) === p.first) n++;
24201
+ return n / pairs.length;
24202
+ }
24203
+ /** The fits of earlier calls, by their options. */
24204
+ const FITS = /* @__PURE__ */ new Map();
24205
+ /**
24206
+ * This function fits the weights of the rules to made-up pairs (RULE-038, a Bradley-Terry model, after Bradley and Terry 1952).
24207
+ * The probability that a reader prefers `a` to `b` is `softStep(u(a) - u(b))`, with `u(x) = -w · f(x)`. The fit takes 400 steps of the gradient of the score.
24208
+ * An L2 term `l2 × |w|²` keeps the weights small. A weight below 0 becomes 0. The weights are scaled so that their sum is the number of features.
24209
+ * The held-out pairs come from the seed plus 7919. The pairs do not come from the screen or from a board.
24210
+ */
24211
+ function fitPreferences(options = {}) {
24212
+ const o = {
24213
+ l2: .01,
24214
+ seed: 1,
24215
+ pairs: 200,
24216
+ ...options
24217
+ };
24218
+ const key = JSON.stringify(o);
24219
+ const known = FITS.get(key);
24220
+ if (known) return known;
24221
+ const pairs = madeUpPairs(Math.max(1, Math.floor(o.pairs)), o.seed);
24222
+ const n = PREFERENCE_FEATURES.length;
24223
+ const w = new Array(n).fill(1);
24224
+ const rate = .5;
24225
+ for (let step = 0; step < 400; step++) {
24226
+ const grad = new Array(n).fill(0);
24227
+ for (const p of pairs) {
24228
+ const d = p.b.map((v, i) => v - p.a[i]);
24229
+ let z = 0;
24230
+ for (let i = 0; i < n; i++) z += w[i] * d[i];
24231
+ const e = softStep(z) - p.first;
24232
+ for (let i = 0; i < n; i++) grad[i] += e * d[i] / pairs.length;
24233
+ }
24234
+ for (let i = 0; i < n; i++) w[i] = Math.max(0, w[i] - rate * (grad[i] + 2 * o.l2 * w[i]));
24235
+ }
24236
+ const sum = w.reduce((s, v) => s + v, 0);
24237
+ const weights = sum > 1e-12 ? w.map((v) => v * n / sum) : w.map(() => 1);
24238
+ const model = {
24239
+ weights,
24240
+ agreement: agreementOf(weights, madeUpPairs(50, o.seed + 7919)),
24241
+ source: `${Math.floor(o.pairs)} made-up pairs of seed ${o.seed}, labelled by the made-up panel PREFERENCE_PANEL with a noise`
24242
+ };
24243
+ FITS.set(key, model);
24244
+ return model;
24245
+ }
24246
+ /** This function gives the detour of a route: its length over the distance between its ends. */
24247
+ function detourOf(points) {
24248
+ const span = dist(points[0], points[points.length - 1]);
24249
+ return span > 1e-9 ? polylineLength(points) / span : 1;
24250
+ }
24251
+ /**
24252
+ * This function runs the whole strategy `preferences` (RULE-038). It fits the weights of the rules to made-up pairs (`fitPreferences`).
24253
+ * Each edge can take its route from any setting of the pool. The first answer is the first setting.
24254
+ * A pass visits the edges in order. An edge takes the route of the pool with the least weighted cost, when that cost is lower than the cost of its route.
24255
+ * The cost is the weighted sum of the features of the route against the routes of the other edges. A route with a break of a hard rule is not in the pool.
24256
+ * The output has the routes alone, with no runs, bundles or levels.
24257
+ */
24258
+ function preferences(graph, list, options = {}) {
24259
+ const o = {
24260
+ ...PREFERENCE_DEFAULTS,
24261
+ ...options
24262
+ };
24263
+ const w = fitPreferences({ l2: o.l2 }).weights;
24264
+ const count = Math.max(1, Math.min(list.length, Math.floor(o.settings)));
24265
+ const pool = /* @__PURE__ */ new Map();
24266
+ let first;
24267
+ for (const s of list.slice(0, count)) {
24268
+ const limit = (s.pipeline.highway ?? s.pipeline.router).maxNodes;
24269
+ if (limit !== void 0 && graph.nodes.length > limit) continue;
24270
+ const output = runPipeline(graph, s.pipeline);
24271
+ first ??= output;
24272
+ const faulty = new Set(checkProperties(output.graph, output.result, 1e6).map((v) => v.edge));
24273
+ for (const r of output.result.routes) {
24274
+ if (faulty.has(r.edge) || r.points.length < 2) continue;
24275
+ const own = pool.get(r.edge);
24276
+ if (own) own.push(r);
24277
+ else pool.set(r.edge, [r]);
24278
+ }
24279
+ }
24280
+ if (!first) return runPipeline(graph, list[0].pipeline);
24281
+ const shapeOf = (route) => {
24282
+ let x0 = Infinity;
24283
+ let y0 = Infinity;
24284
+ let x1 = -Infinity;
24285
+ let y1 = -Infinity;
24286
+ for (const p of route.points) {
24287
+ x0 = Math.min(x0, p.x);
24288
+ y0 = Math.min(y0, p.y);
24289
+ x1 = Math.max(x1, p.x);
24290
+ y1 = Math.max(y1, p.y);
24291
+ }
24292
+ return {
24293
+ route,
24294
+ x0,
24295
+ y0,
24296
+ x1,
24297
+ y1,
24298
+ own: w[3] * (detourOf(route.points) - 1) + w[4] * turnsOf(route.points) / 2
24299
+ };
24300
+ };
24301
+ const current = new Map(first.result.routes.map((r) => [r.edge, shapeOf(r)]));
24302
+ for (const [edge, shape] of current) {
24303
+ const own = pool.get(edge);
24304
+ if (own && !own.includes(shape.route)) current.set(edge, shapeOf(own[0]));
24305
+ }
24306
+ const near = 4;
24307
+ const costOf = (edge, s) => {
24308
+ let sum = s.own;
24309
+ for (const [other, t] of current) {
24310
+ if (other === edge || t.x0 > s.x1 + near || t.x1 < s.x0 - near || t.y0 > s.y1 + near || t.y1 < s.y0 - near) continue;
24311
+ const c = pairCosts(s.route.points, t.route.points);
24312
+ sum += w[0] * c.R1 + w[1] * c.R2 + w[2] * c.R12 / OVERLAP_SCALE;
24313
+ }
24314
+ return sum;
24315
+ };
24316
+ const edges = [...current.keys()];
24317
+ for (let pass = 0; pass < Math.max(1, Math.floor(o.passes)); pass++) {
24318
+ let moved = 0;
24319
+ for (const edge of edges) {
24320
+ const now = current.get(edge);
24321
+ const base = costOf(edge, now);
24322
+ const cap = Math.max(o.detour, detourOf(now.route.points));
24323
+ let best;
24324
+ for (const r of pool.get(edge) ?? []) {
24325
+ if (r === now.route || detourOf(r.points) > cap + 1e-9) continue;
24326
+ const shape = shapeOf(r);
24327
+ const c = costOf(edge, shape);
24328
+ if (c < (best?.cost ?? base) - 1e-9) best = {
24329
+ shape,
24330
+ cost: c
24331
+ };
24332
+ }
24333
+ if (best) {
24334
+ current.set(edge, best.shape);
24335
+ moved++;
24336
+ }
24337
+ }
24338
+ if (moved === 0) break;
24339
+ }
24340
+ const routes = first.result.routes.map((r) => current.get(r.edge)?.route ?? r);
24341
+ return {
24342
+ graph: first.graph,
24343
+ result: { routes }
24344
+ };
24345
+ }
24346
+ // src/whole/fireflies/index.ts
24347
+ /** The values of `SwarmSearch`. */
24348
+ const SWARM_SEARCHES = Object.freeze(["firefly", "bees"]);
24349
+ /** The default options of the strategy `fireflies`. */
24350
+ const FIREFLY_DEFAULTS = Object.freeze({
24351
+ search: "firefly",
24352
+ budget: 12,
24353
+ colony: 6,
24354
+ gamma: 1,
24355
+ alpha: .2,
24356
+ limit: 10,
24357
+ seed: 1
24358
+ });
24359
+ /** The attraction of a firefly at the distance 0, as the record gives it. */
24360
+ const BETA0 = 1;
24361
+ /** The largest number of rounds of a search. A round with no new setting still counts. */
24362
+ const ROUNDS = 60;
24363
+ /**
24364
+ * This function gives the attraction of a firefly at the distance `r`: `BETA0 / (1 + u + u² / 2)` with `u = gamma × r²`.
24365
+ * It is a rational stand-in for `BETA0 × exp(-u)`, with only `+`, `*` and `/`, so it is exact to the last bit on each platform.
24366
+ */
24367
+ function attraction(gamma, r2) {
24368
+ const u = gamma * r2;
24369
+ return BETA0 / (1 + u + u * u / 2);
24370
+ }
24371
+ /** This function gives the setting of a point of the unit square, on the grid of the search. */
24372
+ function settingAt(x, y) {
24373
+ const i = Math.max(0, Math.min(19, Math.round(x * 19)));
24374
+ const j = Math.max(0, Math.min(7, Math.round(y * 7)));
24375
+ return {
24376
+ discount: (i + 1) / 20,
24377
+ margin: 3 + j
24378
+ };
24379
+ }
24380
+ /** The point of the unit square of the defaults of `shared`: the discount 0.5 and the margin 5. */
24381
+ const HOME = [9 / 19, 2 / 7];
24382
+ /**
24383
+ * This function gives the score of an output on its board. A lower score is better.
24384
+ * The score is `crossings / edges + (median detour - 1) - ink saved`. A break of a hard rule gives `Infinity`.
24385
+ */
24386
+ function swarmScore(output) {
24387
+ if (checkProperties(output.graph, output.result, 1).length > 0) return Infinity;
24388
+ const edges = Math.max(1, output.result.routes.length);
24389
+ return measureQuality(output.graph, output.result).crossings / edges + (detourSpread(output.result.routes).median - 1) - inkSaved(output.result.routes);
24390
+ }
24391
+ /**
24392
+ * This function runs the whole strategy `fireflies` (NAT-047, after the firefly algorithm of Yang 2008 and the bee colony of Karaboga 2005).
24393
+ * It searches the options `discount` and `margin` of the router `router` on the board, on a grid of 20 × 8 settings.
24394
+ * The first firefly or food source is the default setting, so the score of the output is not worse than the score of the default.
24395
+ * A firefly moves toward each brighter one by `attraction(gamma, r²)` of the distance, plus a seeded step of size `alpha`.
24396
+ * A bee tries `v = x + phi (x - x_k)` in one dimension, and a scout takes a new seeded place after `limit` tries with no gain.
24397
+ * The search stops after `budget` different settings, or after 60 rounds. It reads only the board and the outputs of its own settings.
24398
+ */
24399
+ function fireflies(graph, router, options = {}) {
24400
+ const o = {
24401
+ ...FIREFLY_DEFAULTS,
24402
+ ...options
24403
+ };
24404
+ const budget = Math.max(1, Math.floor(o.budget));
24405
+ const random = rng(Math.max(0, Math.floor(o.seed)));
24406
+ const known = /* @__PURE__ */ new Map();
24407
+ const entries = [];
24408
+ const score = (x, y) => {
24409
+ const setting = settingAt(x, y);
24410
+ const key = `${setting.discount}:${setting.margin}`;
24411
+ const seen = known.get(key);
24412
+ if (seen) return seen.score;
24413
+ if (entries.length >= budget) return Infinity;
24414
+ const output = runPipeline(graph, {
24415
+ router,
24416
+ routerOptions: { ...setting }
24417
+ });
24418
+ const entry = {
24419
+ setting,
24420
+ score: swarmScore(output),
24421
+ output
24422
+ };
24423
+ known.set(key, entry);
24424
+ entries.push(entry);
24425
+ return entry.score;
24426
+ };
24427
+ const clamp = (v) => Math.max(0, Math.min(1, v));
24428
+ const colony = Math.max(2, Math.min(20, Math.floor(o.colony)));
24429
+ const xs = [[HOME[0], HOME[1]]];
24430
+ while (xs.length < colony) xs.push([random(), random()]);
24431
+ const fs = xs.map(([x, y]) => score(x, y));
24432
+ if (o.search === "firefly") for (let round = 0; round < ROUNDS && entries.length < budget; round++) for (let i = 0; i < colony; i++) for (let j = 0; j < colony; j++) {
24433
+ if (!(fs[j] < fs[i])) continue;
24434
+ const dx = xs[j][0] - xs[i][0];
24435
+ const dy = xs[j][1] - xs[i][1];
24436
+ const b = attraction(o.gamma, dx * dx + dy * dy);
24437
+ xs[i] = [clamp(xs[i][0] + b * dx + o.alpha * (random() - .5)), clamp(xs[i][1] + b * dy + o.alpha * (random() - .5))];
24438
+ fs[i] = score(xs[i][0], xs[i][1]);
24439
+ }
24440
+ else {
24441
+ const trials = new Array(colony).fill(0);
24442
+ const tryNear = (i) => {
24443
+ let k = Math.floor(random() * (colony - 1));
24444
+ if (k >= i) k++;
24445
+ const d = random() < .5 ? 0 : 1;
24446
+ const phi = 2 * random() - 1;
24447
+ const v = [xs[i][0], xs[i][1]];
24448
+ v[d] = clamp(v[d] + phi * (v[d] - xs[k][d]));
24449
+ const f = score(v[0], v[1]);
24450
+ if (f < fs[i]) {
24451
+ xs[i] = v;
24452
+ fs[i] = f;
24453
+ trials[i] = 0;
24454
+ } else trials[i]++;
24455
+ };
24456
+ for (let round = 0; round < ROUNDS && entries.length < budget; round++) {
24457
+ for (let i = 0; i < colony; i++) tryNear(i);
24458
+ const order = xs.map((_, i) => i).sort((a, b) => fs[a] - fs[b] || a - b);
24459
+ for (let r = 0; r < colony; r++) {
24460
+ const pick = order[Math.min(colony - 1, Math.floor(random() * random() * colony))];
24461
+ tryNear(pick);
24462
+ }
24463
+ const worn = trials.findIndex((t, i) => t > o.limit && fs[i] !== Math.min(...fs));
24464
+ if (worn >= 0) {
24465
+ xs[worn] = [random(), random()];
24466
+ fs[worn] = score(xs[worn][0], xs[worn][1]);
24467
+ trials[worn] = 0;
24468
+ }
24469
+ }
24470
+ }
24471
+ let best = entries[0];
24472
+ for (const e of entries) if (e.score < best.score - 1e-12) best = e;
24473
+ return {
24474
+ output: best.output,
24475
+ best: best.setting,
24476
+ entries
24477
+ };
24478
+ }
24479
+ // src/whole/tracing/index.ts
24480
+ /** The values of `TracingQuantile`. */
24481
+ const TRACING_QUANTILES = Object.freeze([
24482
+ "q50",
24483
+ "q95",
24484
+ "mean"
24485
+ ]);
24486
+ /** The default weights of the tracing load, as the record gives them. */
24487
+ const TRACING_WEIGHTS = Object.freeze({
24488
+ crossing: 1,
24489
+ merge: 2,
24490
+ bend: .5
24491
+ });
24492
+ /** The default options of the strategy `tracing`. */
24493
+ const TRACING_DEFAULTS = Object.freeze({
24494
+ ...TRACING_WEIGHTS,
24495
+ settings: 4,
24496
+ quantile: "q95"
24497
+ });
24498
+ /** The bounds of a polyline: the least x and y, and the largest x and y. */
24499
+ function boundsOf(p) {
24500
+ let x0 = Infinity;
24501
+ let y0 = Infinity;
24502
+ let x1 = -Infinity;
24503
+ let y1 = -Infinity;
24504
+ for (const q of p) {
24505
+ x0 = Math.min(x0, q.x);
24506
+ y0 = Math.min(y0, q.y);
24507
+ x1 = Math.max(x1, q.x);
24508
+ y1 = Math.max(y1, q.y);
24509
+ }
24510
+ return [
24511
+ x0,
24512
+ y0,
24513
+ x1,
24514
+ y1
24515
+ ];
24516
+ }
24517
+ /**
24518
+ * This function gives the tracing load of each edge of a result (WILD-057, after the tracing tasks of Ware, Purchase, Colpoys and McGill 2002).
24519
+ * The load is `T(e) = crossing × crossings + merge × (merges and splits where e is not an outer lane) + bend × bends`.
24520
+ * A shared run where the edge is not an outer lane gives one merge and one split. Such a bundle gives one split.
24521
+ * A bend is a turn of more than 1 degree. The measure is W4 of MEAS3.
24522
+ */
24523
+ function tracingLoads(result, weights = {}) {
24524
+ const w = {
24525
+ ...TRACING_WEIGHTS,
24526
+ ...weights
24527
+ };
24528
+ const routes = result.routes;
24529
+ const bounds = routes.map((r) => boundsOf(r.points));
24530
+ const crossings = new Array(routes.length).fill(0);
24531
+ for (let i = 0; i < routes.length; i++) {
24532
+ const a = routes[i].points;
24533
+ const [ax0, ay0, ax1, ay1] = bounds[i];
24534
+ for (let j = i + 1; j < routes.length; j++) {
24535
+ const [bx0, by0, bx1, by1] = bounds[j];
24536
+ if (bx0 > ax1 || bx1 < ax0 || by0 > ay1 || by1 < ay0) continue;
24537
+ const b = routes[j].points;
24538
+ let n = 0;
24539
+ for (let p = 1; p < a.length; p++) for (let q = 1; q < b.length; q++) if (segmentsCross(a[p - 1], a[p], b[q - 1], b[q])) n++;
24540
+ crossings[i] += n;
24541
+ crossings[j] += n;
24542
+ }
24543
+ }
24544
+ const choices = /* @__PURE__ */ new Map();
24545
+ const inner = (edges, add) => {
24546
+ if (edges.length < 3) return;
24547
+ for (const e of edges.slice(1, -1)) choices.set(e, (choices.get(e) ?? 0) + add);
24548
+ };
24549
+ for (const run of result.runs ?? []) inner(run.edges, 2);
24550
+ for (const b of result.bundles ?? []) inner(b.edges, 1);
24551
+ const out = /* @__PURE__ */ new Map();
24552
+ routes.forEach((r, i) => out.set(r.edge, w.crossing * crossings[i] + w.merge * (choices.get(r.edge) ?? 0) + w.bend * turnsOf(r.points)));
24553
+ return out;
24554
+ }
24555
+ /** This function gives a value of a list of loads: the median, the quantile 0.95 by the nearest rank, or the mean. An empty list gives 0. */
24556
+ function tracingValue(loads, quantile) {
24557
+ const list = [...loads].sort((a, b) => a - b);
24558
+ if (list.length === 0) return 0;
24559
+ if (quantile === "mean") return list.reduce((s, v) => s + v, 0) / list.length;
24560
+ const q = quantile === "q50" ? .5 : .95;
24561
+ return list[Math.min(list.length - 1, Math.max(0, Math.ceil(q * list.length) - 1))];
24562
+ }
24563
+ /**
24564
+ * This function runs the whole strategy `tracing` (WILD-057). It runs the first `settings` settings of `list` on the board.
24565
+ * A setting with a break of a hard rule, or above its node limit, does not count. The setting with the least tracing load wins.
24566
+ * A tie goes to the least median load, then to the earlier setting. It reads only the board and the outputs of its own settings.
24567
+ */
24568
+ function tracing(graph, list, options = {}) {
24569
+ const o = {
24570
+ ...TRACING_DEFAULTS,
24571
+ ...options
24572
+ };
24573
+ const count = Math.max(1, Math.min(list.length, Math.floor(o.settings)));
24574
+ const entries = [];
24575
+ const outputs = [];
24576
+ let fallback;
24577
+ for (const s of list.slice(0, count)) {
24578
+ const limit = (s.pipeline.highway ?? s.pipeline.router).maxNodes;
24579
+ if (limit !== void 0 && graph.nodes.length > limit) continue;
24580
+ const output = runPipeline(graph, s.pipeline);
24581
+ fallback ??= {
24582
+ name: s.name,
24583
+ output
24584
+ };
24585
+ if (checkProperties(output.graph, output.result, 1).length > 0) continue;
24586
+ const loads = [...tracingLoads(output.result, o).values()];
24587
+ entries.push({
24588
+ name: s.name,
24589
+ value: tracingValue(loads, o.quantile),
24590
+ median: tracingValue(loads, "q50")
24591
+ });
24592
+ outputs.push(output);
24593
+ }
24594
+ if (entries.length === 0) {
24595
+ const first = fallback ?? {
24596
+ name: list[0].name,
24597
+ output: runPipeline(graph, list[0].pipeline)
24598
+ };
24599
+ return {
24600
+ output: first.output,
24601
+ pick: first.name,
24602
+ entries
24603
+ };
24604
+ }
24605
+ let best = 0;
24606
+ entries.forEach((e, i) => {
24607
+ const b = entries[best];
24608
+ if (e.value < b.value - 1e-9 || Math.abs(e.value - b.value) <= 1e-9 && e.median < b.median - 1e-9) best = i;
24609
+ });
24610
+ return {
24611
+ output: outputs[best],
24612
+ pick: entries[best].name,
24613
+ entries
24614
+ };
24615
+ }
21764
24616
  // src/strategies/schemas.ts
21765
24617
  /** The options of the way finder (`WayOptions`), for each router that goes around the nodes. */
21766
24618
  const WAY_SCHEMA = {
@@ -24722,6 +27574,25 @@ const SPREAD_SCHEMA = {
24722
27574
  describe: "The passes that sort each wall again by the new places of the far ends."
24723
27575
  }
24724
27576
  };
27577
+ /** The declarations of the options of the plug strategy `assign` (PLUG-ASSIGN). */
27578
+ const ASSIGN_SCHEMA = {
27579
+ plugGap: {
27580
+ type: "number",
27581
+ min: 0,
27582
+ max: 24,
27583
+ step: 1,
27584
+ default: 8,
27585
+ describe: "The least gap between two plugs of one wall. On a short wall, the gap shrinks evenly."
27586
+ },
27587
+ inset: {
27588
+ type: "number",
27589
+ min: 0,
27590
+ max: 12,
27591
+ step: 1,
27592
+ default: 6,
27593
+ describe: "A plug stays this far from the corners of its wall."
27594
+ }
27595
+ };
24725
27596
  /** The declarations of the options of the whole strategy `pipeline`. */
24726
27597
  const PIPELINE_SCHEMA = {
24727
27598
  router: {
@@ -25333,6 +28204,501 @@ const BOARD_PORTFOLIO_SCHEMA = {
25333
28204
  describe: "The time budget in seconds. The value 0 sets no budget."
25334
28205
  }
25335
28206
  };
28207
+ /** The declarations of the options of the highway strategy `balance` (EDA-017). */
28208
+ const BALANCE_SCHEMA = {
28209
+ tau: {
28210
+ type: "number",
28211
+ min: 1.1,
28212
+ max: 2,
28213
+ step: .05,
28214
+ marks: [
28215
+ 1.1,
28216
+ 1.2,
28217
+ 1.5,
28218
+ 2
28219
+ ],
28220
+ default: 1.2,
28221
+ describe: "The target of the stretch of an edge: its route on the plan over its route alone."
28222
+ },
28223
+ iterations: {
28224
+ type: "integer",
28225
+ min: 5,
28226
+ max: 30,
28227
+ step: 1,
28228
+ marks: [
28229
+ 5,
28230
+ 10,
28231
+ 30
28232
+ ],
28233
+ default: 5,
28234
+ describe: "The number of updates of the prices."
28235
+ },
28236
+ end: {
28237
+ type: "enum",
28238
+ values: [
28239
+ "exit",
28240
+ "entry",
28241
+ "both"
28242
+ ],
28243
+ default: "exit",
28244
+ describe: "The end of a ride that the price moves: the exit, the entry, or both by half."
28245
+ },
28246
+ from: {
28247
+ ...HIGHWAY_BASE,
28248
+ default: "shared-ways"
28249
+ }
28250
+ };
28251
+ /** The declarations of the options of the highway strategy `manifolds` (WILD-036). */
28252
+ const MANIFOLD_SCHEMA = {
28253
+ kMin: {
28254
+ type: "integer",
28255
+ min: 3,
28256
+ max: 8,
28257
+ step: 1,
28258
+ marks: [
28259
+ 3,
28260
+ 4,
28261
+ 6,
28262
+ 8
28263
+ ],
28264
+ default: 3,
28265
+ describe: "A manifold has this number of taps or more."
28266
+ },
28267
+ band: {
28268
+ type: "number",
28269
+ min: .5,
28270
+ max: 2,
28271
+ step: .25,
28272
+ marks: [
28273
+ .5,
28274
+ 1,
28275
+ 2
28276
+ ],
28277
+ default: 1,
28278
+ describe: "The nodes of one row have centers in a band of this many median box heights."
28279
+ },
28280
+ side: {
28281
+ type: "enum",
28282
+ values: ["near", "far"],
28283
+ default: "near",
28284
+ describe: "The place of the header in the gap before the row: near the row, or near the feed node."
28285
+ },
28286
+ detour: {
28287
+ type: "number",
28288
+ min: 1,
28289
+ max: 3,
28290
+ step: .05,
28291
+ marks: [
28292
+ 1.25,
28293
+ 1.5,
28294
+ 2
28295
+ ],
28296
+ default: 1.5,
28297
+ describe: "A tap stays on the manifold only when its way is this many times the direct line or less."
28298
+ },
28299
+ from: {
28300
+ ...HIGHWAY_BASE,
28301
+ default: "shared-ways"
28302
+ }
28303
+ };
28304
+ /** The declarations of the options of the highway strategy `slots` (RULE-047). */
28305
+ const SLOT_SCHEMA = {
28306
+ pull: {
28307
+ type: "number",
28308
+ min: .25,
28309
+ max: 8,
28310
+ step: .25,
28311
+ marks: [
28312
+ .5,
28313
+ 1,
28314
+ 2,
28315
+ 4,
28316
+ 8
28317
+ ],
28318
+ default: 2,
28319
+ describe: "The weight of the pull toward the slot of a new lane."
28320
+ },
28321
+ reach: {
28322
+ type: "number",
28323
+ min: 8,
28324
+ max: 64,
28325
+ step: 4,
28326
+ marks: [
28327
+ 8,
28328
+ 16,
28329
+ 24,
28330
+ 48,
28331
+ 64
28332
+ ],
28333
+ default: 24,
28334
+ describe: "A route point farther than this distance from a trunk gets no pull."
28335
+ },
28336
+ laneSpacing: {
28337
+ type: "number",
28338
+ min: 2,
28339
+ max: 8,
28340
+ step: 1,
28341
+ marks: [
28342
+ 2,
28343
+ 4,
28344
+ 8
28345
+ ],
28346
+ default: 4,
28347
+ describe: "The distance between two lanes of a trunk."
28348
+ },
28349
+ maxStretch: {
28350
+ type: "number",
28351
+ min: 1.05,
28352
+ max: 2,
28353
+ step: .05,
28354
+ marks: [
28355
+ 1.1,
28356
+ 1.25,
28357
+ 1.5,
28358
+ 2
28359
+ ],
28360
+ default: 1.25,
28361
+ describe: "The largest new length of a route over its first length after it joins a trunk."
28362
+ },
28363
+ keys: {
28364
+ type: "enum",
28365
+ values: ["same", "any"],
28366
+ default: "same",
28367
+ describe: "The edges that can join a trunk: only the edges with the key of the trunk, or each edge."
28368
+ },
28369
+ from: {
28370
+ ...HIGHWAY_BASE,
28371
+ default: "shared-ways"
28372
+ }
28373
+ };
28374
+ /** The declarations of the options of the plug strategy `leaders` (EDA-041). */
28375
+ const LEADER_SCHEMA = {
28376
+ leader: {
28377
+ type: "enum",
28378
+ values: [
28379
+ "s",
28380
+ "po",
28381
+ "opo"
28382
+ ],
28383
+ default: "po",
28384
+ describe: "The type of leader: the order of the angles, the order along the wall, or even places."
28385
+ },
28386
+ slotSpacing: {
28387
+ type: "number",
28388
+ min: 2,
28389
+ max: 8,
28390
+ step: 1,
28391
+ marks: [
28392
+ 2,
28393
+ 4,
28394
+ 8
28395
+ ],
28396
+ default: 8,
28397
+ describe: "The least gap between two plugs of one wall."
28398
+ },
28399
+ inset: SPREAD_SCHEMA.inset,
28400
+ rounds: {
28401
+ type: "integer",
28402
+ min: 0,
28403
+ max: 4,
28404
+ step: 1,
28405
+ default: 2,
28406
+ describe: "The passes that sort each wall again by the new places of the far ends."
28407
+ }
28408
+ };
28409
+ /** The declarations of the options of the level strategy `detail` (WILD-016 with EDA-038): the options of `depths`, and the limits of the collapse. */
28410
+ const DETAIL_SCHEMA = {
28411
+ ...DEPTHS_SCHEMA,
28412
+ pxMin: {
28413
+ type: "number",
28414
+ min: .5,
28415
+ max: 4,
28416
+ step: .5,
28417
+ marks: [
28418
+ 1,
28419
+ 2,
28420
+ 3,
28421
+ 4
28422
+ ],
28423
+ default: 3,
28424
+ describe: "Two parallel legs closer than this number of pixels on the screen become one stroke."
28425
+ },
28426
+ minCount: {
28427
+ type: "integer",
28428
+ min: 2,
28429
+ max: 5,
28430
+ step: 1,
28431
+ default: 2,
28432
+ describe: "A stroke holds this number of edges or more."
28433
+ },
28434
+ w0: {
28435
+ type: "number",
28436
+ min: 1,
28437
+ max: 4,
28438
+ step: .5,
28439
+ default: 1.5,
28440
+ describe: "The width of a stroke of one edge, in pixels."
28441
+ },
28442
+ widthScale: {
28443
+ type: "enum",
28444
+ values: [
28445
+ "linear",
28446
+ "sqrt",
28447
+ "log"
28448
+ ],
28449
+ default: "log",
28450
+ describe: "The growth of the width of a stroke with its count."
28451
+ }
28452
+ };
28453
+ /** The declarations of the options of the whole strategy `curriculum` (RULE-048). */
28454
+ const CURRICULUM_SCHEMA = {
28455
+ order: {
28456
+ type: "enum",
28457
+ values: [
28458
+ "R1-R12-R2",
28459
+ "R12-R1-R2",
28460
+ "R1-R2-R12"
28461
+ ],
28462
+ default: "R1-R12-R2",
28463
+ describe: "The order in which the stages add the rules."
28464
+ },
28465
+ passes: {
28466
+ type: "integer",
28467
+ min: 1,
28468
+ max: 3,
28469
+ step: 1,
28470
+ default: 1,
28471
+ describe: "The number of passes over the edges in each stage."
28472
+ },
28473
+ settings: {
28474
+ type: "integer",
28475
+ min: 2,
28476
+ max: 3,
28477
+ step: 1,
28478
+ default: 3,
28479
+ describe: "The number of settings in the pool of routes, from the start of its list."
28480
+ },
28481
+ detour: {
28482
+ type: "number",
28483
+ min: 1.5,
28484
+ max: 4,
28485
+ step: .25,
28486
+ marks: [
28487
+ 1.5,
28488
+ 2,
28489
+ 3
28490
+ ],
28491
+ default: 2,
28492
+ describe: "A new route has a detour of at most this value, or of its old route."
28493
+ }
28494
+ };
28495
+ /** The declarations of the options of the whole strategy `preferences` (RULE-038). */
28496
+ const PREFERENCE_SCHEMA = {
28497
+ l2: {
28498
+ type: "number",
28499
+ min: 0,
28500
+ max: 1,
28501
+ step: .01,
28502
+ marks: [
28503
+ 0,
28504
+ .01,
28505
+ .1,
28506
+ 1
28507
+ ],
28508
+ default: .01,
28509
+ describe: "The weight of the L2 term of the fit of the weights to the made-up pairs."
28510
+ },
28511
+ passes: {
28512
+ type: "integer",
28513
+ min: 1,
28514
+ max: 3,
28515
+ step: 1,
28516
+ default: 1,
28517
+ describe: "The number of passes over the edges."
28518
+ },
28519
+ settings: {
28520
+ type: "integer",
28521
+ min: 2,
28522
+ max: 3,
28523
+ step: 1,
28524
+ default: 3,
28525
+ describe: "The number of settings in the pool of routes, from the start of its list."
28526
+ },
28527
+ detour: {
28528
+ type: "number",
28529
+ min: 1.5,
28530
+ max: 4,
28531
+ step: .25,
28532
+ marks: [
28533
+ 1.5,
28534
+ 2,
28535
+ 3
28536
+ ],
28537
+ default: 2,
28538
+ describe: "A new route has a detour of at most this value, or of its old route."
28539
+ }
28540
+ };
28541
+ /** The declarations of the options of the whole strategy `fireflies` (NAT-047). */
28542
+ const FIREFLY_SCHEMA = {
28543
+ search: {
28544
+ type: "enum",
28545
+ values: ["firefly", "bees"],
28546
+ default: "firefly",
28547
+ describe: "The search: the firefly algorithm, or the artificial bee colony."
28548
+ },
28549
+ budget: {
28550
+ type: "integer",
28551
+ min: 4,
28552
+ max: 40,
28553
+ step: 1,
28554
+ marks: [
28555
+ 4,
28556
+ 12,
28557
+ 20,
28558
+ 40
28559
+ ],
28560
+ default: 12,
28561
+ describe: "The largest number of different settings that the search routes."
28562
+ },
28563
+ colony: {
28564
+ type: "integer",
28565
+ min: 4,
28566
+ max: 20,
28567
+ step: 1,
28568
+ marks: [
28569
+ 4,
28570
+ 6,
28571
+ 10,
28572
+ 20
28573
+ ],
28574
+ default: 6,
28575
+ describe: "The number of fireflies or food sources."
28576
+ },
28577
+ gamma: {
28578
+ type: "number",
28579
+ min: .01,
28580
+ max: 10,
28581
+ step: .01,
28582
+ scale: "log",
28583
+ marks: [
28584
+ .01,
28585
+ .1,
28586
+ 1,
28587
+ 10
28588
+ ],
28589
+ default: 1,
28590
+ describe: "The absorption of the light of a firefly."
28591
+ },
28592
+ alpha: {
28593
+ type: "number",
28594
+ min: .01,
28595
+ max: .5,
28596
+ step: .01,
28597
+ marks: [
28598
+ .01,
28599
+ .1,
28600
+ .2,
28601
+ .5
28602
+ ],
28603
+ default: .2,
28604
+ describe: "The size of the random step of a firefly, as a share of the range."
28605
+ },
28606
+ limit: {
28607
+ type: "integer",
28608
+ min: 10,
28609
+ max: 100,
28610
+ step: 10,
28611
+ marks: [
28612
+ 10,
28613
+ 50,
28614
+ 100
28615
+ ],
28616
+ default: 10,
28617
+ describe: "A scout bee leaves a food source after this number of tries with no gain."
28618
+ },
28619
+ seed: {
28620
+ type: "integer",
28621
+ min: 0,
28622
+ max: 1e6,
28623
+ step: 1,
28624
+ marks: [
28625
+ 1,
28626
+ 2,
28627
+ 3
28628
+ ],
28629
+ default: 1,
28630
+ describe: "The seed of the search. The same seed gives the same routes."
28631
+ }
28632
+ };
28633
+ /** The declarations of the options of the whole strategy `tracing` (WILD-057). */
28634
+ const TRACING_SCHEMA = {
28635
+ crossing: {
28636
+ type: "number",
28637
+ min: 0,
28638
+ max: 4,
28639
+ step: .25,
28640
+ marks: [
28641
+ 0,
28642
+ 1,
28643
+ 2,
28644
+ 4
28645
+ ],
28646
+ default: 1,
28647
+ describe: "The weight of a crossing in the tracing load."
28648
+ },
28649
+ merge: {
28650
+ type: "number",
28651
+ min: 0,
28652
+ max: 4,
28653
+ step: .25,
28654
+ marks: [
28655
+ 0,
28656
+ 1,
28657
+ 2,
28658
+ 4
28659
+ ],
28660
+ default: 2,
28661
+ describe: "The weight of a merge or a split where the edge is not an outer lane."
28662
+ },
28663
+ bend: {
28664
+ type: "number",
28665
+ min: 0,
28666
+ max: 4,
28667
+ step: .25,
28668
+ marks: [
28669
+ 0,
28670
+ .5,
28671
+ 1,
28672
+ 4
28673
+ ],
28674
+ default: .5,
28675
+ describe: "The weight of a bend in the tracing load."
28676
+ },
28677
+ settings: {
28678
+ type: "integer",
28679
+ min: 1,
28680
+ max: 12,
28681
+ step: 1,
28682
+ marks: [
28683
+ 1,
28684
+ 4,
28685
+ 8,
28686
+ 12
28687
+ ],
28688
+ default: 4,
28689
+ describe: "The number of settings that the strategy tries, from the start of its list."
28690
+ },
28691
+ quantile: {
28692
+ type: "enum",
28693
+ values: [
28694
+ "q50",
28695
+ "q95",
28696
+ "mean"
28697
+ ],
28698
+ default: "q95",
28699
+ describe: "The value of the loads of the edges that the pick reads."
28700
+ }
28701
+ };
25336
28702
  // src/strategies/builtins.ts
25337
28703
  /** This function makes a router strategy from a router factory and a preset of its options. Each call makes the router of its options. */
25338
28704
  function routerStrategy(name, describe, maxNodes, defaults, make, schema) {
@@ -26084,6 +29450,33 @@ const highwayStrategies = Object.freeze({
26084
29450
  maxNodes: 1e4,
26085
29451
  run: (input, o) => bronchialPlan(input, o)
26086
29452
  }),
29453
+ balance: defineStrategy({
29454
+ step: "highway",
29455
+ name: "balance",
29456
+ describe: "Stretch balance in a bundle (EDA-017). A price for each edge grows while its stretch is above the target, and the price moves its exit forward.",
29457
+ defaults: { ...BALANCE_DEFAULTS },
29458
+ schema: BALANCE_SCHEMA,
29459
+ maxNodes: 1e4,
29460
+ run: (input, o) => balancePlan(input, o)
29461
+ }),
29462
+ manifolds: defineStrategy({
29463
+ step: "highway",
29464
+ name: "manifolds",
29465
+ describe: "Manifolds for a fan to a row (WILD-036). A straight header in the gap before the row, with taps at right angles and one feed from the node.",
29466
+ defaults: { ...MANIFOLD_DEFAULTS },
29467
+ schema: MANIFOLD_SCHEMA,
29468
+ maxNodes: 1e4,
29469
+ run: (input, o) => manifoldPlan(input, o)
29470
+ }),
29471
+ slots: defineStrategy({
29472
+ step: "highway",
29473
+ name: "slots",
29474
+ describe: "A pull toward the lane slots of the trunks (RULE-047). An edge near a trunk joins it on a new lane when the pull is larger than its detour.",
29475
+ defaults: { ...SLOT_DEFAULTS },
29476
+ schema: SLOT_SCHEMA,
29477
+ maxNodes: 1e4,
29478
+ run: (input, o) => slotPlan(input, o)
29479
+ }),
26087
29480
  "corridors+breakeven": gateStrategy(corridors, breakEvenGate),
26088
29481
  "corridors+stretch": gateStrategy(corridors, stretchGate),
26089
29482
  "corridors+ambiguity": gateStrategy(corridors, ambiguityGate),
@@ -26127,6 +29520,22 @@ const plugStrategies = Object.freeze({
26127
29520
  defaults: { ...SPREAD_DEFAULTS },
26128
29521
  schema: SPREAD_SCHEMA,
26129
29522
  run: (input, o) => spreadEnds(input, o)
29523
+ }),
29524
+ leaders: defineStrategy({
29525
+ step: "plugs",
29526
+ name: "leaders",
29527
+ describe: "Boundary leaders as plug stubs (EDA-041). The plugs of each wall take the order of po-leaders with no crossing, and keep a least gap.",
29528
+ defaults: { ...LEADER_DEFAULTS },
29529
+ schema: LEADER_SCHEMA,
29530
+ run: (input, o) => leaderEnds(input, o)
29531
+ }),
29532
+ assign: defineStrategy({
29533
+ step: "plugs",
29534
+ name: "assign",
29535
+ describe: "PLUG-ASSIGN. The plugs of each wall go into the ranges of their ports with a minimum gap. The most ends are in their range, then the order of the far ends.",
29536
+ defaults: { ...ASSIGN_DEFAULTS },
29537
+ schema: ASSIGN_SCHEMA,
29538
+ run: (input, o) => assignEnds(input, o)
26130
29539
  })
26131
29540
  });
26132
29541
  /** The built-in level rules: the strategies of the step `levels`. The router `sections` makes its zoom levels with its option `level`. */
@@ -26146,6 +29555,14 @@ const levelStrategies = Object.freeze({
26146
29555
  defaults: { ...VENATION_DEFAULTS },
26147
29556
  schema: VENATION_SCHEMA,
26148
29557
  run: (input, o) => venationLevels(input, o)
29558
+ }),
29559
+ detail: defineStrategy({
29560
+ step: "levels",
29561
+ name: "detail",
29562
+ describe: "The level of detail of the bundles (WILD-016 with EDA-038). Legs of level links closer than a few pixels at the scale of their level become one stroke.",
29563
+ defaults: { ...DETAIL_DEFAULTS },
29564
+ schema: DETAIL_SCHEMA,
29565
+ run: (input, o) => detailLevels(input, o)
26149
29566
  })
26150
29567
  });
26151
29568
  /**
@@ -26177,6 +29594,18 @@ const BOARD_PORTFOLIO_SETTINGS = Object.freeze([...[
26177
29594
  }
26178
29595
  }))].map((s) => Object.freeze(s)));
26179
29596
  /**
29597
+ * The pool of routes of the whole strategy `curriculum` (RULE-048), in order: the routers of the base of the screen. The first one gives the first answer.
29598
+ * The option `settings` takes the first ones.
29599
+ */
29600
+ const CURRICULUM_POOL = Object.freeze([
29601
+ "corners",
29602
+ "shared",
29603
+ "trunks"
29604
+ ].map((name) => Object.freeze({
29605
+ name,
29606
+ pipeline: { router: routerStrategies[name] }
29607
+ })));
29608
+ /**
26180
29609
  * The built-in whole strategies: the strategies of the step `whole`. The strategy `pipeline` runs one strategy of each step, as the demo of the site does.
26181
29610
  * Without options, it routes the graph with the router `corners`.
26182
29611
  */
@@ -26196,6 +29625,38 @@ const wholeStrategies = Object.freeze({
26196
29625
  defaults: { ...BOARD_PORTFOLIO_DEFAULTS },
26197
29626
  schema: BOARD_PORTFOLIO_SCHEMA,
26198
29627
  run: (graph, o) => boardPortfolio(graph, BOARD_PORTFOLIO_SETTINGS, o).output
29628
+ }),
29629
+ curriculum: defineStrategy({
29630
+ step: "whole",
29631
+ name: "curriculum",
29632
+ describe: "A curriculum of rules (RULE-048). Each stage adds one rule, and each edge takes the route of a pool that improves it with no loss on the earlier rules.",
29633
+ defaults: { ...CURRICULUM_DEFAULTS },
29634
+ schema: CURRICULUM_SCHEMA,
29635
+ run: (graph, o) => curriculum(graph, CURRICULUM_POOL, o)
29636
+ }),
29637
+ preferences: defineStrategy({
29638
+ step: "whole",
29639
+ name: "preferences",
29640
+ describe: "Rule weights from preferences (RULE-038). A fit to made-up pairs gives the weights, and each edge takes the route of a pool with the least weighted cost.",
29641
+ defaults: { ...PREFERENCE_DEFAULTS },
29642
+ schema: PREFERENCE_SCHEMA,
29643
+ run: (graph, o) => preferences(graph, CURRICULUM_POOL, o)
29644
+ }),
29645
+ fireflies: defineStrategy({
29646
+ step: "whole",
29647
+ name: "fireflies",
29648
+ describe: "Firefly and bee searches (NAT-047). A seeded swarm searches the discount and the margin of `shared` on the board, within a budget of settings.",
29649
+ defaults: { ...FIREFLY_DEFAULTS },
29650
+ schema: FIREFLY_SCHEMA,
29651
+ run: (graph, o) => fireflies(graph, routerStrategies.shared, o).output
29652
+ }),
29653
+ tracing: defineStrategy({
29654
+ step: "whole",
29655
+ name: "tracing",
29656
+ describe: "The least tracing load (WILD-057). The pipeline tries a fixed list of settings, and the least load of crossings, merges and bends on one edge wins.",
29657
+ defaults: { ...TRACING_DEFAULTS },
29658
+ schema: TRACING_SCHEMA,
29659
+ run: (graph, o) => tracing(graph, BOARD_PORTFOLIO_SETTINGS, o).output
26199
29660
  })
26200
29661
  });
26201
29662
  /** The built-in strategies of each step, in order. A consumer adds its own: `createRegistry([...BUILT_IN_STRATEGIES, mine])`. */
@@ -26221,6 +29682,42 @@ function sideOf(plug, node) {
26221
29682
  for (const [side, n] of Object.entries(SIDE_NORMAL)) if (n.x === plug.normal.x && n.y === plug.normal.y) return side;
26222
29683
  return nearestSide(node.box, plug.point);
26223
29684
  }
29685
+ /**
29686
+ * This function moves the new fixed ports of a side of a node into their ranges (PORT-RANGE). It does this when one port is out of its range.
29687
+ * The ports of the side keep their order, and two of them keep at least `gap` (`placeInIntervals`). A side with each port in its range does not change.
29688
+ */
29689
+ function keepInRanges(node, ports, gap, inset) {
29690
+ const bySide = /* @__PURE__ */ new Map();
29691
+ for (const p of ports.values()) {
29692
+ const list = bySide.get(p.side);
29693
+ if (list) list.push(p);
29694
+ else bySide.set(p.side, [p]);
29695
+ }
29696
+ const box = node.box;
29697
+ for (const [side, list] of bySide) {
29698
+ const along = alongAxis(side);
29699
+ const v = (p) => p.at[along] + (along === "y" ? box.y : box.x);
29700
+ if (!list.some((p) => {
29701
+ const r = rangeInterval(box, side, p, inset);
29702
+ return r !== void 0 && (v(p) < r[0] - 1e-6 || v(p) > r[1] + 1e-6);
29703
+ })) continue;
29704
+ const start = along === "y" ? box.y : box.x;
29705
+ const len = along === "y" ? box.h : box.w;
29706
+ const sorted = [...list].sort((a, b) => v(a) - v(b));
29707
+ const span = sorted.map((p) => rangeInterval(box, side, p, inset) ?? [start, start + len]);
29708
+ const at = placeInIntervals(sorted.map(v), span.map((r) => r[0]), span.map((r) => r[1]), gap);
29709
+ sorted.forEach((p, i) => {
29710
+ const point = {
29711
+ ...p.at,
29712
+ [along]: at[i] - start
29713
+ };
29714
+ ports.set(p.id, {
29715
+ ...p,
29716
+ at: point
29717
+ });
29718
+ });
29719
+ }
29720
+ }
26224
29721
  /** This function gives an id that no port of the node has, from `base`. */
26225
29722
  function freeId(taken, base) {
26226
29723
  let id = base;
@@ -26235,6 +29732,7 @@ function freeId(taken, base) {
26235
29732
  *
26236
29733
  * The rule sees the graph of `corners`: the plugs of `resolvePorts`, without the inner edges of the frames.
26237
29734
  * The rule is a strategy of the step `plugs`, or the name of one in the registry.
29735
+ * A new fixed port with a `range` is in its range (PORT-RANGE). When the rule gives a plug out of the range, the new plugs of that side move into their ranges.
26238
29736
  * The output is a new graph. The function does not change the input.
26239
29737
  */
26240
29738
  function fixPorts(graph, rule, options = {}) {
@@ -26303,6 +29801,16 @@ function fixPorts(graph, rule, options = {}) {
26303
29801
  target
26304
29802
  };
26305
29803
  });
29804
+ const own = strategy.defaults;
29805
+ const given = options.plugOptions ?? {};
29806
+ const num = (a, b, c) => typeof a === "number" ? a : typeof b === "number" ? b : c;
29807
+ const gap = num(given.plugGap, own.plugGap, 0);
29808
+ const inset = num(given.inset, own.inset, 6);
29809
+ const ranges = options.ranges ?? (strategy.name === "assign" ? "rule" : "keep");
29810
+ for (const [id, m] of fixed) {
29811
+ const node = byId.get(id);
29812
+ if (node && ranges === "keep") keepInRanges(node, m, gap, inset);
29813
+ }
26306
29814
  const nodes = graph.nodes.map((n) => {
26307
29815
  const m = fixed.get(n.id);
26308
29816
  const more = added.get(n.id);
@@ -33584,18 +37092,21 @@ const MOVE_NAMES = Object.freeze({
33584
37092
  search: "searched",
33585
37093
  interchanges: "networkRoad",
33586
37094
  knee: "tree",
33587
- bronchial: "tree"
37095
+ bronchial: "tree",
37096
+ manifolds: "manifold"
33588
37097
  });
33589
37098
  /**
33590
37099
  * The highway strategies that change the plan of another strategy (`from`). Their own move is a change of that plan, so they count no move.
33591
37100
  * The output against the plan `from` tells if they ran.
33592
37101
  */
33593
37102
  const PLAN_CHANGERS = Object.freeze([
37103
+ "balance",
33594
37104
  "crossbars",
33595
37105
  "divided",
33596
37106
  "lanes",
33597
37107
  "level-links",
33598
37108
  "meet",
37109
+ "slots",
33599
37110
  "stubs",
33600
37111
  "weiszfeld",
33601
37112
  "zipper"
@@ -35799,6 +39310,6 @@ const sharedStretchStrategy = defineStrategy({
35799
39310
  name: "shared-stretch"
35800
39311
  }).route(graph)
35801
39312
  });
35802
- export { AMBIGUITY_GUARD_DEFAULTS, ANNEALING_DEFAULTS, ANNEALING_SCHEMA, ARC_SOURCES, AREA_DEFAULTS, AREA_WIDTHS, ATTRACTION_DEFAULTS, ATTRACTION_SCHEMA, BACKTRACK_MIN, BACKTRACK_SHARE, BIPARTITE, BIPARTITE_LAYOUTS, BOARD_PORTFOLIO_DEFAULTS, BOARD_PORTFOLIO_SETTINGS, BREAK, BREAK_EVEN, BREAK_EVEN_DEFAULTS, BRONCHIAL_DEFAULTS, BRONCHIAL_SPLITS, BUILT_IN_STRATEGIES, BoundaryWalk, CANDIDATE_SOURCES, CAP_SWEEP, CASES, CHANNEL_REACH, CLEARANCE_CAP, COMPARE_ALL_STEPS, COMPARE_STEPS, CONFLUENT_DEFAULTS, CONSENSUS_DEFAULTS, CONSENSUS_LINKAGES, CORRIDOR_DEFAULTS, COVERAGE_FAMILIES, CROSSBAR_DEFAULTS, CUBU_DEFAULTS, CURVE_TYPES, CUT_DEFAULTS, CornerGraph, DEFAULT_RNG, DENSE_GROUPS, DENSITY_KERNELS, DIFFUSION_DEFAULTS, DISTORTION_NEIGHBOURS, DIVIDED_DEFAULTS, EDGE_DISTANCES, EDGE_PATH_DEFAULTS, EXACT_CLASSES, EXACT_EDGES_PER_NODE, EXACT_V1, EXACT_V1_BOUNDS, EXACT_V1_BOUNDS_SIZES, EXACT_V1_SEEDS, EXACT_V1_SIZES, FAN_OUT, FDEB_DEFAULTS, FIXTURES, FIXTURE_LAYOUTS, FIXTURE_LOCALITY, FLOW_CLUSTER_DEFAULTS, FLOW_FIELD_DEFAULTS, FRAME_ROWS, FRECHET_DEFAULTS, FRONT_OBJECTIVES, GAP_BASELINES, GAP_BOOTSTRAP, GAP_CAPS, GAP_DEFAULTS, GAP_RULES, GAP_VIEWS, GROUP_SCOPES, GROUP_SPINES, GROWTH_DEFAULTS, GROWTH_SWEEP, GUTTER_BEND_COST, Grid, HDBSCAN_DEFAULTS, HIERARCHY_DEFAULTS, HIERARCHY_GROUPS, HIGHWAYS_ENGINE, HIGHWAYS_FAMILIES, HIGHWAY_BASES, HIGHWAY_BASE_ROUTERS, HOUGH_DEFAULTS, HUB_ALLOCATIONS, HUB_ARC_DEFAULTS, HUB_CANDIDATES, HUB_CHAIN, HYSTERESIS_DEFAULTS, INTERCHANGE_BASES, INTERCHANGE_DEFAULTS, KDEEB_DEFAULTS, KD_DEFAULTS, KENDALL_EXACT, KNEE_DEFAULTS, KNEE_RULES, LANES_DEFAULTS, LANE_ORDERS, LANE_STRETCH_DEFAULTS, LANE_STRETCH_ORDERS, LANE_TRACKS, LEVEL_HIGHWAY_DEFAULTS, LINKAGES, LINK_COMMUNITY_DEFAULTS, LOCAL_MESH, MANY_GROUPS, MEASURES, MEASURE_LANE_WIDTH, MEET_DEFAULTS, MEMBER_FORMS, MESH_DEFAULTS, MINGLE_DEFAULTS, MIN_EFFECTS, MLSEB_DEFAULTS, MLS_FITS, MODULE_RULES, MOVE_NAMES, NESTED_DEFAULTS, OD_CELL_DEFAULTS, OUTSIDE_SLACK, Obstacles, PACKED_GRID, PICK, PIPELINE, PLAN_CHANGERS, PORTFOLIO_B1, PORTFOLIO_BANDS, PORTFOLIO_BASE_MEMBER, PORTFOLIO_CAPS, PORTFOLIO_FIT, PORTFOLIO_HASHES, PORTFOLIO_KS_API, PORTFOLIO_RULES, PORTFOLIO_SETS, POWER_DEFAULTS, PROPOSED_RULES, P_HUB_DEFAULTS, QUICKBUNDLES_DEFAULTS, RECORD_EPS, RESISTANCE_DEFAULTS, RIDGE_DEFAULTS, ROAD_CLASS_DEFAULTS, ROOM_FAMILIES, ROUTED_STEPS, ROUTERS, SAME_HIGHWAY, SBEB_DEFAULTS, SCALE_SPACE_DEFAULTS, SCREEN_FAMILY_BY, SCREEN_RULES, SCREEN_SETS, SCREEN_STEPS, SCREEN_V1, SCREEN_V2, SCREEN_V2_HELD_OUT, SCREEN_V3, SCREEN_V3_HELD_OUT, SCREEN_WRAPS, SEARCH_DEFAULTS, SEARCH_MOVES, SECTION_DEFAULTS, SELECT_DEFAULTS, SIDE_NORMAL, SKELETON_GROUPS, SKELETON_TARGETS, SLACK_SWEEP, SMALL_CLUSTERED, SOFT_KEY_DEFAULTS, SPACING_LIMIT, SPARSE_LONG, SPECTRAL_DEFAULTS, SPIRAL_DEFAULTS, SPREAD_DEFAULTS, STABLE_ID_DEFAULTS, STEINER_DEFAULTS, STEINER_GROUPS, STEINER_METHODS, STEINER_METRICS, STEPS, STRATEGIES, STREET_DEFAULTS, STREET_SOURCES, STRETCH_DEFAULTS, STRETCH_SWEEP, STUB_DEFAULTS, SUITE_COLUMNS, SoftGraph, SplitTree, TERRITORY_DEFAULTS, TERRITORY_POINTS, THIN_BARS, TIGHT_DEFAULTS, TIGHT_FIRSTS, TIGHT_SCHEMA, TIGHT_TIES, TINY_UNIFORM, TOLERANCE, TRACLUS_DEFAULTS, TRAFFIC_RULES, TWO_HUBS, UNEVEN_SIZES, VENATION_BACKBONES, VENATION_DEFAULTS, WEISZFELD_DEFAULTS, WSPD_DEFAULTS, WayFinder, ZIPPER_DEFAULTS, activationBase, activationReport, adjustedRandIndex, allSections, ambiguity, ambiguityGate, ambiguityPlan, annealWays, annealingRouter, annealingStrategy, arcCurve, areaPlan, aroundBox, asRouter, attractionRouter, attractionStrategy, backboneLinks, backtrackOf, balance, basePlan, benchBendRoom, benchCurveStrategies, benchCurves, benchHighways, benchLevelRoom, benchOne, benchRoom, benchRun, benchTracks, bendCount, bendingEnergy, bicliqueGroups, bigGroups, boardGaps, boardPortfolio, boardUnit, boardWins, bootstrapMedian, boundsFamilyName, boxDisplacement, boxExit, boxPush, boxUnit, boxesOverlap, breakEvenGate, breakEvenPlan, bronchialPlan, buildSections, bundleStretch, bundledShare, byCollection, byDataGroup, candidateArcs, candidatePlan, candidateScore, canonicalJson, capSettings, caseSeed, cellSteps, center, chainClusters, changeGraph, changedDimensions, channelOverflows, channelWidth, characteristicPoints, checkFamilies, checkLevels, checkOptions, checkParents, checkPlan, checkPort, checkProperties, checkSchema, checkSections, clampOptions, clothoidCurve, clusters1d, coAssociation, commonEdges, compareAnyStep, compareMeasure, compareSpec, compareStep, compatibility, compatibilityMatrix, confluentPlan, consensusGroups, consensusLabels, consensusPlan, controlChain, cornersRouter, corpusManifest, corridorPlan, countCrossings, countOverlaps, createFamilyRegistry, createRegistry, crossbarPlan, crossesItself, crossingAngle, crossingAngles, cubicAt, cubuPlan, curvatureVariation, curveDeviation, curveFaults, curveJoints, curveOf, curveRuns, curveSpace, curveStrategies, curvesHash, cutGroups, cutShare, declaresFamily, defineStrategy, denseGroupsBoard, densityPlan, densityRidges, depthLevels, detourSpread, diamondAngle, diffusionPlan, dimensionAt, dimensionUnit, dist, distanceDistortion, distinctPoints, divideHighway, dividedPlan, documentHash, dominates, drawnSegments, edgeBlocks, edgeEnds, edgeLengths, edgePathPlan, edgePaths, edgePlugs, edgeResistance, enumDimension, exactBoard, exactFamily, exactGenerator, exactSeed, exactStretch, exactStretches, exercised, facingEnds, familyCell, familyKind, familyOf, familySeed, farLevel, farLinks, fdebLines, fdebPlan, fft, findStrategy, finderOf, firstLines, fixPorts, fixedPortPoint, fixtureGroupOptions, fixtureParams, fixtureRngSeed, fixtureSpecOf, flowClusterPlan, flowClusters, flowDistance, flowFieldPlan, fnv1a32, follow, formatActivation, formatBendRoom, formatCapped, formatCurves, formatFaults, formatFronts, formatGap, formatHighways, formatInput, formatLevelRoom, formatLevels, formatMeasureTime, formatPaired, formatRoom, formatRules, formatScreen, formatSections, formatSummaries, formatTable, formatTracks, frameRoutes, frechet, frechetBundles, frechetPlan, freeLines, g1Defect, g2Defect, gapBaselines, gapCap, gapClosed, gapLaneCounts, gapLanes, gapNames, gapPlan, gapValues, gateStrategy, getStrategy, goesOutAndBack, graphInput, greedyBicliques, greedyCrossbars, groupEdges, groupFill, groupRun, groupSettings, groupStability, groupStrategies, groupedPlan, growthPlan, growthSettings, guardBreaks, gutterRoutes, hasCusp, hdbscanClusters, hdbscanLabels, hdbscanPlan, hierarchyPlan, highwayChurn, highwayCurve, highwayCurves, highwayEdits, highwayIdKeep, highwayMoves, highwayRouter, highwayRouterStrategy, highwayRouters, highwayStrategies, highwaysRegistry, houghLines, houghPeaks, houghPlan, hubArcPlan, hubCandidates, hubChainBoard, hysteresisGate, hysteresisPlan, inflate, inflectionCount, inflectionCurve, inflectionSector, inkSaved, innerPoints, inputGraph, inputId, interchangeCandidates, interchangePlan, isRoutedStep, itemOverlaps, itemSizes, jaccard, jaccardIndex, jogPeaks, joinReports, jsonSafe, kMeans, kdGroups, kdeebLines, kdeebPlan, keepMembers, kendallTau, keyGroups, keyedSections, keysPlan, kneeOf, kneePlan, labelGroups, laneArea, laneCrossings, laneFidelity, laneOrder, laneRoutes, laneSeparation, laneSettings, laneStretches, lanesPlan, lanesThrough, leafGroups, lengthBetween, levelCut, levelGrouping, levelHighwayPlan, levelMeasures, levelPairs, levelRatio, levelRoom, levelStrategies, levelsOf, lineDensity, linePoint, linkAgreement, linkCommunities, linkCommunityGroups, linkCommunityPlan, linkDensity, listOptions, listStrategies, makeFixture, makeRoom, manifestHash, manyGroupsBoard, matchHighways, maxStretch, mdf, meanLine, measureBoard, measureByKey, measureByName, measureHighways, measureJogs, measureLevel, measureLevelFaults, measureLinks, measureName, measureQuality, measureRouteFaults, measureStability, meetMember, meetPlan, meetingPoints, memberAt, memberStrategy, mergeAngle, meshPlan, meshWaypoints, minEffectOf, minHash, mingleBundles, minglePlan, mixedSegments, mixesCollections, mlsPoint, mlsebLines, mlsebPlan, modularity, modulesOf, mulberry32, namedObjectives, nearestCentre, nearestOnBoundary, nearestOnLine, nearestSide, nearestSkeleton, nestedGridBoard, nestedGroups, nestedPlan, nestingFaults, nestingViolations, nextRow, nodeAmbiguity, nodeIndex, nodeLinks, numberDimension, numberOf, occupiedArea, odCellPlan, odGroups, offsetLine, offsetPolyline, oneSteiner, openGaps, openLanes, optimaFile, optimumKey, optionDefaults, orderChanges, orderFragility, orderLanes, orderRows, orderStability, orderSuiteRows, orthogonality, outOfBoxes, outerGraph, outputGroups, outputHash, pHubPlan, pMedian, packedGridBoard, paramProblem, paretoFront, parseLines, partWithin, passSettings, pathPoints, peakCurvature, pickLevel, pickMoves, placePlug, planMoves, planOfResult, plugSpacing, plugStrategies, plugsRouter, pointAt, pointBoxDistance, pointDensity, pointGroups, pointOnSide, pointSegmentDistance, polylineLength, portOffset, portfolio, portfolioB1Spec, portfolioBand, portfolioFacts, portfolioKey, portfolioMember, portfolioMembers, portfolioName, portfolioRouter, portfolioStrategy, positionStability, powerPlan, pullApart, quadtreeGroups, quantile, quickBundles, quickBundlesPlan, randomInt, rankShares, rayToBox, recordLines, recordMoves, rectScattered, regionBounds, register, repairResult, repairStrategy, representative, resample, resistanceClusters, resistancePlan, resolvePorts, resultForm, resultHash, ridgeLines, ridgePlan, rng, roadClassPlan, roomDemand, roomLinks, roomOrderFaults, roomStrategies, roomValues, round3, roundCorners, roundedCurve, routeCrossings, routeInStableOrder, routeMeasures, routePortfolio, routeSeparation, routeStability, routeSymmetry, routerStrategies, routersOf, ruleCounts, runBenchmark, runKey, runPipeline, runProfile, runSuite, sameData, sampleCurve, samplesOf, sbebLines, sbebPlan, scaleClusters, scaleSpaceGroups, scaleSpacePlan, screen, screenInputs, screenSplit, searchPlan, searchScore, sectionsRouter, segmentBounds, segmentBoxDistance, segmentClusters, segmentDistance, segmentHitsBox, segmentHitsRect, segmentKey, segmentsCross, segmentsOf, selectBest, selectPlan, setManifest, sha256Hex, shapeSkeleton, sharedRouter, sharedStretchStrategy, sharedStretches, sharedWaysAll, sharedWaysPlan, shuffleEdges, sideAngle, sideToward, signTestPower, silhouette, skeletonGroups, skeletonPath, slackSettings, smoothCurve, snapPlan, softDistance, softKeyClusters, softKeyPlan, spanningTree, spectralClusters, spectralLabels, spectralPlan, spinePart, spiralJoin, spiralPlan, spiralTree, splineCurve, splitFrames, splitPath, splitPoint, splitTargets, spreadEnds, stableIdGate, stableIdPlan, stableOrder, stableOrderStrategy, steinerPlan, stepRouterStrategy, stepRouters, stepSeed, straightCurve, straightRouter, strategyKey, streetEdges, streetPlan, streetPoint, streetRide, stretchGate, stretchLaneOrders, stretchPlan, stretchSettings, stubPlan, suiteFronts, sumFaults, summarize, summarizeGap, summarizeRows, sweepGrid, sweepOf, sweepRoom, sweepSections, sweepValues, symmetricEigen, territoryPlan, tightRouter, tightStrategy, toSvgPath, totalTurn, tracePlan, traclusPlan, treeCost, treeDepth, trunksRouter, turns, unwrappedName, utf8, venationLevels, venationLinks, waySlacks, waypointWay, weberPoint, weiszfeldPlan, wholeBoard, wholeStrategies, withBends, withRepair, withStableOrder, wspdGroups, wspdPlan, zipLegs, zipperPlan };
39313
+ export { ADAM_DEFAULTS, ADAM_SCHEMA, AMBIGUITY_GUARD_DEFAULTS, ANNEALING_DEFAULTS, ANNEALING_SCHEMA, ARC_SOURCES, AREA_DEFAULTS, AREA_WIDTHS, ASSIGN_DEFAULTS, ATTRACTION_DEFAULTS, ATTRACTION_SCHEMA, BACKTRACK_MIN, BACKTRACK_SHARE, BALANCE_DEFAULTS, BALANCE_ENDS, BIPARTITE, BIPARTITE_LAYOUTS, BOARD_PORTFOLIO_DEFAULTS, BOARD_PORTFOLIO_SETTINGS, BREAK, BREAK_EVEN, BREAK_EVEN_DEFAULTS, BRONCHIAL_DEFAULTS, BRONCHIAL_SPLITS, BUILT_IN_STRATEGIES, BoundaryWalk, CANAL_DEFAULTS, CANAL_SCHEMA, CANDIDATE_SOURCES, CAP_SWEEP, CASES, CHANNEL_REACH, CLEARANCE_CAP, COMPARE_ALL_STEPS, COMPARE_STEPS, CONFLUENT_DEFAULTS, CONSENSUS_DEFAULTS, CONSENSUS_LINKAGES, CORRIDOR_DEFAULTS, COVERAGE_FAMILIES, CROSSBAR_DEFAULTS, CUBU_DEFAULTS, CURRICULUM_DEFAULTS, CURRICULUM_ORDERS, CURRICULUM_POOL, CURVE_TYPES, CUT_DEFAULTS, CornerGraph, DEFAULT_RNG, DENSE_GROUPS, DENSITY_KERNELS, DETAIL_DEFAULTS, DIFFUSION_DEFAULTS, DISTORTION_NEIGHBOURS, DIVIDED_DEFAULTS, EDGE_DISTANCES, EDGE_PATH_DEFAULTS, EXACT_CLASSES, EXACT_EDGES_PER_NODE, EXACT_V1, EXACT_V1_BOUNDS, EXACT_V1_BOUNDS_SIZES, EXACT_V1_SEEDS, EXACT_V1_SIZES, FAN_OUT, FDEB_DEFAULTS, FIREFLY_DEFAULTS, FIXTURES, FIXTURE_LAYOUTS, FIXTURE_LOCALITY, FLOW_CLUSTER_DEFAULTS, FLOW_FIELD_DEFAULTS, FRAME_ROWS, FRECHET_DEFAULTS, FRONT_OBJECTIVES, GAP_BASELINES, GAP_BOOTSTRAP, GAP_CAPS, GAP_DEFAULTS, GAP_RULES, GAP_VIEWS, GROUP_SCOPES, GROUP_SPINES, GROWTH_DEFAULTS, GROWTH_SWEEP, GUTTER_BEND_COST, Grid, HDBSCAN_DEFAULTS, HIERARCHY_DEFAULTS, HIERARCHY_GROUPS, HIGHWAYS_ENGINE, HIGHWAYS_FAMILIES, HIGHWAY_BASES, HIGHWAY_BASE_ROUTERS, HOUGH_DEFAULTS, HUB_ALLOCATIONS, HUB_ARC_DEFAULTS, HUB_CANDIDATES, HUB_CHAIN, HYSTERESIS_DEFAULTS, INTERCHANGE_BASES, INTERCHANGE_DEFAULTS, KDEEB_DEFAULTS, KD_DEFAULTS, KENDALL_EXACT, KNEE_DEFAULTS, KNEE_RULES, LANES_DEFAULTS, LANE_ORDERS, LANE_STRETCH_DEFAULTS, LANE_STRETCH_ORDERS, LANE_TRACKS, LEADER_DEFAULTS, LEADER_TYPES, LEVEL_HIGHWAY_DEFAULTS, LINKAGES, LINK_COMMUNITY_DEFAULTS, LOCAL_MESH, MANIFOLD_DEFAULTS, MANIFOLD_SIDES, MANY_GROUPS, MEASURES, MEASURE_LANE_WIDTH, MEET_DEFAULTS, MEMBER_FORMS, MESH_DEFAULTS, MINGLE_DEFAULTS, MIN_EFFECTS, MLSEB_DEFAULTS, MLS_FITS, MODULE_RULES, MOVE_NAMES, NESTED_DEFAULTS, OD_CELL_DEFAULTS, OUTSIDE_SLACK, Obstacles, PACKED_GRID, PICK, PIPELINE, PLAN_CHANGERS, PORTFOLIO_B1, PORTFOLIO_BANDS, PORTFOLIO_BASE_MEMBER, PORTFOLIO_CAPS, PORTFOLIO_FIT, PORTFOLIO_HASHES, PORTFOLIO_KS_API, PORTFOLIO_RULES, PORTFOLIO_SETS, POWER_DEFAULTS, PREFERENCE_DEFAULTS, PREFERENCE_FEATURES, PREFERENCE_PANEL, PROPOSED_RULES, P_HUB_DEFAULTS, QUICKBUNDLES_DEFAULTS, RECORD_EPS, REROUTE_DEFAULTS, REROUTE_SCHEMA, RESISTANCE_DEFAULTS, RIDGE_DEFAULTS, ROAD_CLASS_DEFAULTS, ROOM_FAMILIES, ROUTED_STEPS, ROUTERS, SAME_HIGHWAY, SBEB_DEFAULTS, SCALE_SPACE_DEFAULTS, SCREEN_FAMILY_BY, SCREEN_RULES, SCREEN_SETS, SCREEN_STEPS, SCREEN_V1, SCREEN_V2, SCREEN_V2_HELD_OUT, SCREEN_V3, SCREEN_V3_HELD_OUT, SCREEN_WRAPS, SEARCH_DEFAULTS, SEARCH_MOVES, SECTION_DEFAULTS, SELECT_DEFAULTS, SIDE_NORMAL, SKELETON_GROUPS, SKELETON_TARGETS, SLACK_SWEEP, SLOT_DEFAULTS, SLOT_KEYS, SMALL_CLUSTERED, SOFT_KEY_DEFAULTS, SPACING_LIMIT, SPARSE_LONG, SPECTRAL_DEFAULTS, SPIRAL_DEFAULTS, SPREAD_DEFAULTS, STABLE_ID_DEFAULTS, STEINER_DEFAULTS, STEINER_GROUPS, STEINER_METHODS, STEINER_METRICS, STEPS, STRATEGIES, STREET_DEFAULTS, STREET_SOURCES, STRETCH_DEFAULTS, STRETCH_SWEEP, STUB_DEFAULTS, SUITE_COLUMNS, SWARM_SEARCHES, SoftGraph, SplitTree, TERRITORY_DEFAULTS, TERRITORY_POINTS, THIN_BARS, TIGHT_DEFAULTS, TIGHT_FIRSTS, TIGHT_SCHEMA, TIGHT_TIES, TINY_UNIFORM, TOLERANCE, TRACING_DEFAULTS, TRACING_QUANTILES, TRACING_WEIGHTS, TRACLUS_DEFAULTS, TRAFFIC_RULES, TWO_HUBS, UNEVEN_SIZES, VENATION_BACKBONES, VENATION_DEFAULTS, WEISZFELD_DEFAULTS, WIDTH_SCALES, WSPD_DEFAULTS, WayFinder, ZIPPER_DEFAULTS, activationBase, activationReport, adamRouter, adamRoutes, adamStrategy, adjustedRandIndex, agreementOf, allSections, ambiguity, ambiguityGate, ambiguityPlan, annealWays, annealingRouter, annealingStrategy, arcCurve, arcsin, areaPlan, aroundBox, asRouter, assignEnds, assignWall, attraction, attractionRouter, attractionStrategy, backboneLinks, backtrackOf, balance, balanceOf, balancePlan, basePlan, benchBendRoom, benchCurveStrategies, benchCurves, benchHighways, benchLevelRoom, benchOne, benchRoom, benchRun, benchTracks, bendCount, bendingEnergy, bicliqueGroups, bigGroups, boardGaps, boardPortfolio, boardUnit, boardWins, bootstrapMedian, boundsFamilyName, boxDisplacement, boxExit, boxPush, boxUnit, boxesOverlap, breakEvenGate, breakEvenPlan, bronchialPlan, buildSections, bundleStretch, bundledShare, byCollection, byDataGroup, canalHubs, canalRouter, canalStrategy, canalWays, candidateArcs, candidatePlan, candidateScore, canonicalJson, capSettings, caseSeed, cellSteps, center, chainClusters, changeGraph, changedDimensions, channelOverflows, channelWidth, characteristicPoints, checkFamilies, checkLevels, checkOptions, checkParents, checkPlan, checkPort, checkProperties, checkSchema, checkSections, clampOptions, clothoidCurve, clusters1d, coAssociation, collapseLinks, commonEdges, compareAnyStep, compareMeasure, compareSpec, compareStep, compatibility, compatibilityMatrix, confluentPlan, consensusGroups, consensusLabels, consensusPlan, controlChain, cornersRouter, corpusManifest, corridorPlan, countCrossings, countOverlaps, createFamilyRegistry, createRegistry, crossbarPlan, crossesItself, crossingAngle, crossingAngles, cubicAt, cubuPlan, curriculum, curvatureVariation, curveDeviation, curveFaults, curveJoints, curveOf, curveRuns, curveSpace, curveStrategies, curvesHash, cutGroups, cutShare, declaresFamily, defineStrategy, denseGroupsBoard, densityPlan, densityRidges, depthLevels, detailLevels, detailWidth, detourSpread, diamondAngle, diffusionPlan, dimensionAt, dimensionUnit, dist, distanceDistortion, distinctPoints, divideHighway, dividedPlan, documentHash, dominates, drawnSegments, edgeBlocks, edgeEnds, edgeLengths, edgePathPlan, edgePaths, edgePlugs, edgeResistance, enumDimension, exactBoard, exactFamily, exactGenerator, exactSeed, exactStretch, exactStretches, exercised, facingEnds, familyCell, familyKind, familyOf, familySeed, farLevel, farLinks, fdebLines, fdebPlan, fft, findStrategy, finderOf, fireflies, firstLines, fitPreferences, fixPorts, fixedPortPoint, fixtureGroupOptions, fixtureParams, fixtureRngSeed, fixtureSpecOf, flowClusterPlan, flowClusters, flowDistance, flowFieldPlan, fnv1a32, follow, formatActivation, formatBendRoom, formatCapped, formatCurves, formatFaults, formatFronts, formatGap, formatHighways, formatInput, formatLevelRoom, formatLevels, formatMeasureTime, formatPaired, formatRoom, formatRules, formatScreen, formatSections, formatSummaries, formatTable, formatTracks, frameRoutes, frechet, frechetBundles, frechetPlan, freeLines, g1Defect, g2Defect, gapBaselines, gapCap, gapClosed, gapLaneCounts, gapLanes, gapNames, gapPlan, gapValues, gateStrategy, getStrategy, goesOutAndBack, graphInput, greedyBicliques, greedyCrossbars, groupEdges, groupFill, groupRun, groupSettings, groupStability, groupStrategies, groupedPlan, growthPlan, growthSettings, guardBreaks, gutterRoutes, halfPower, hasCusp, hdbscanClusters, hdbscanLabels, hdbscanPlan, hierarchyPlan, highwayChurn, highwayCurve, highwayCurves, highwayEdits, highwayIdKeep, highwayMoves, highwayRouter, highwayRouterStrategy, highwayRouters, highwayStrategies, highwaysRegistry, houghLines, houghPeaks, houghPlan, hubArcPlan, hubCandidates, hubChainBoard, hysteresisGate, hysteresisPlan, inflate, inflectionCount, inflectionCurve, inflectionSector, inkSaved, innerPoints, inputGraph, inputId, interchangeCandidates, interchangePlan, isRoutedStep, itemOverlaps, itemSizes, jaccard, jaccardIndex, jogPeaks, joinReports, jsonSafe, kMeans, kdGroups, kdeebLines, kdeebPlan, keepMembers, kendallTau, keyGroups, keyedSections, keysPlan, kneeOf, kneePlan, labelGroups, laneArea, laneCrossings, laneFidelity, laneOrder, laneRoutes, laneSeparation, laneSettings, laneStretches, lanesPlan, lanesThrough, leaderEnds, leafGroups, lengthBetween, levelCut, levelGrouping, levelHighwayPlan, levelMeasures, levelPairs, levelRatio, levelRoom, levelStrategies, levelsOf, lineDensity, linePoint, linkAgreement, linkCommunities, linkCommunityGroups, linkCommunityPlan, linkDensity, listOptions, listStrategies, madeUpPairs, makeFixture, makeRoom, manifestHash, manifoldPlan, manyGroupsBoard, matchHighways, maxStretch, mdf, meanLine, measureBoard, measureByKey, measureByName, measureHighways, measureJogs, measureLevel, measureLevelFaults, measureLinks, measureName, measureQuality, measureRouteFaults, measureStability, meetMember, meetPlan, meetingPoints, memberAt, memberStrategy, mergeAngle, meshPlan, meshWaypoints, minEffectOf, minHash, mingleBundles, minglePlan, mixedSegments, mixesCollections, mlsPoint, mlsebLines, mlsebPlan, modularity, modulesOf, mulberry32, namedObjectives, nearestCentre, nearestOnBoundary, nearestOnLine, nearestSide, nearestSkeleton, nestedGridBoard, nestedGroups, nestedPlan, nestingFaults, nestingViolations, nextRow, nodeAmbiguity, nodeIndex, nodeLinks, numberDimension, numberOf, occupiedArea, odCellPlan, odGroups, offsetLine, offsetPolyline, oneSteiner, openGaps, openLanes, optimaFile, optimumKey, optionDefaults, orderChanges, orderFragility, orderLanes, orderRows, orderStability, orderSuiteRows, orthogonality, outOfBoxes, outerGraph, outputGroups, outputHash, pHubPlan, pMedian, packedGridBoard, pairCosts, paramProblem, paretoFront, parseLines, partWithin, passSettings, pathPoints, peakCurvature, pickLevel, pickMoves, placePlug, planMoves, planOfResult, plugSpacing, plugStrategies, plugsRouter, pointAt, pointBoxDistance, pointDensity, pointGroups, pointOnSide, pointSegmentDistance, polylineLength, portOffset, portfolio, portfolioB1Spec, portfolioBand, portfolioFacts, portfolioKey, portfolioMember, portfolioMembers, portfolioName, portfolioRouter, portfolioStrategy, positionStability, powerPlan, preferences, pullApart, quadtreeGroups, quantile, quickBundles, quickBundlesPlan, randomInt, rankShares, rayToBox, recordLines, recordMoves, rectScattered, regionBounds, register, repairResult, repairStrategy, representative, rerouteRouter, rerouteStrategy, rerouteWays, resample, resistanceClusters, resistancePlan, resolvePorts, resultForm, resultHash, ridgeLines, ridgePlan, rng, roadClassPlan, roomDemand, roomLinks, roomOrderFaults, roomStrategies, roomValues, round3, roundCorners, roundedCurve, routeCrossings, routeInStableOrder, routeMeasures, routePortfolio, routeSeparation, routeStability, routeSymmetry, routerStrategies, routersOf, ruleCounts, runBenchmark, runKey, runPipeline, runProfile, runSuite, sameData, sampleCurve, samplesOf, sbebLines, sbebPlan, scaleClusters, scaleSpaceGroups, scaleSpacePlan, screen, screenInputs, screenSplit, searchPlan, searchScore, sectionsRouter, segmentBounds, segmentBoxDistance, segmentClusters, segmentDistance, segmentHitsBox, segmentHitsRect, segmentKey, segmentsCross, segmentsOf, selectBest, selectPlan, setManifest, settingAt, sha256Hex, shapeSkeleton, sharedRouter, sharedStretchStrategy, sharedStretches, sharedWaysAll, sharedWaysPlan, shuffleEdges, sideAngle, sideToward, signTestPower, silhouette, skeletonGroups, skeletonPath, slackSettings, slotJoin, slotPlan, slotsOf, smoothCurve, snapPlan, softDistance, softKeyClusters, softKeyPlan, softStep, spanningTree, spectralClusters, spectralLabels, spectralPlan, spinePart, spiralJoin, spiralPlan, spiralTree, splineCurve, splitFrames, splitPath, splitPoint, splitTargets, spreadEnds, spreadWith, stableIdGate, stableIdPlan, stableOrder, stableOrderStrategy, steinerPlan, stepRouterStrategy, stepRouters, stepSeed, straightCurve, straightRouter, strategyKey, streetEdges, streetPlan, streetPoint, streetRide, stretchGate, stretchLaneOrders, stretchPlan, stretchSettings, stubPlan, suiteFronts, sumFaults, summarize, summarizeGap, summarizeRows, swarmScore, sweepGrid, sweepOf, sweepRoom, sweepSections, sweepValues, symmetricEigen, territoryPlan, tightRouter, tightStrategy, toSvgPath, totalTurn, tracePlan, tracing, tracingLoads, tracingValue, traclusPlan, treeCost, treeDepth, trunksRouter, turns, turnsOf, unwrappedName, utf8, venationLevels, venationLinks, waySlacks, waypointWay, weberPoint, weiszfeldPlan, wholeBoard, wholeStrategies, withBends, withRepair, withStableOrder, wspdGroups, wspdPlan, zipLegs, zipperPlan };
35803
39314
 
35804
39315
  //# sourceMappingURL=index.js.map