@mark1russell7/highways 0.9.1 → 0.9.2

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.
@@ -1,17 +1,20 @@
1
- import type { Graph, Result, Route } from "../../model.ts";
1
+ import type { Graph, Level, Result, Route } from "../../model.ts";
2
2
  import { stableOrderStrategy } from "../../order/index.ts";
3
3
  import { repairStrategy } from "../../repair/index.ts";
4
4
  import { STRATEGIES } from "../../strategies/builtins.ts";
5
5
  import { asRouter, createRegistry, getStrategy, listStrategies, register, STEPS, type AnyStrategy, type Registry, type Strategy } from "../../strategies/model.ts";
6
- import { compareStep, highwayRouters, isRoutedStep, stepRouters, type CompareRow, type CompareStep, type RoutedStep, type StepComparison } from "../compare.ts";
6
+ import { compareStep, highwayRouters, isRoutedStep, stepRouters, wholeBoard, type CompareRow, type CompareStep, type RoutedStep, type StepComparison } from "../compare.ts";
7
7
  import { bundledShare, bundleStretch, mergeAngle } from "../measures/bundles.ts";
8
8
  import { highwayEdits } from "../measures/highways.ts";
9
9
  import { laneArea, laneCrossings, laneFidelity, lanesThrough, occupiedArea, openLanes, orthogonality, routeSeparation, runProfile } from "../measures/lanes.ts";
10
- import { linkAgreement } from "../measures/levels.ts";
10
+ import { farLevel, groupFill, itemOverlaps, silhouette } from "../measures/groups.ts";
11
+ import { levelRatio, linkAgreement } from "../measures/levels.ts";
11
12
  import { orderFragility } from "../measures/order.ts";
12
13
  import { paretoFront, type Objective } from "../measures/pareto.ts";
13
14
  import { boardUnit } from "../measures/routes.ts";
15
+ import { boxDisplacement, distanceDistortion, roomOrderFaults } from "../measures/room.ts";
14
16
  import { routeStability } from "../measures/stability.ts";
17
+ import { routeSymmetry } from "../measures/symmetry.ts";
15
18
  import { quantile } from "../measures/stats.ts";
16
19
  import { checkLevels, checkProperties, TOLERANCE } from "../properties.ts";
17
20
  import { measureQuality } from "../quality.ts";
@@ -227,11 +230,30 @@ function screenRegistry(step: ScreenStep, strategies: readonly (Strategy<ScreenS
227
230
  /** This function tells if a step of the screen gives routes. The step `router` gives routes, and each step of `RoutedStep` gives routes as a router. */
228
231
  const givesRoutes = (step: ScreenStep): step is "router" | RoutedStep => step === "router" || isRoutedStep(step);
229
232
 
233
+ /** This function gives the measures of the shape of the groups and of the levels: G4, G5, G7 and Z5. A value without its data is not in the result. */
234
+ function groupShapeValues(graph: Graph, levels: readonly Level[]): Record<string, number> {
235
+ const values: Record<string, number> = {};
236
+ if (levels.length === 0) return values;
237
+ values["G5.overlaps"] = itemOverlaps(levels);
238
+ const far = farLevel(levels);
239
+ const fill = far && groupFill(graph, far);
240
+ if (fill !== undefined) values["G4.fill"] = fill;
241
+ const sil = far && silhouette(graph, far);
242
+ if (sil !== undefined) values["G7.silhouette"] = sil;
243
+ const ratio = levelRatio(levels);
244
+ if (ratio) {
245
+ values["Z5.max"] = ratio.max;
246
+ values["Z5.spread"] = ratio.spread;
247
+ }
248
+ return values;
249
+ }
250
+
230
251
  /**
231
252
  * This function gives the values of a row of `compareStep`, with the measures of `measureQuality` for a router.
232
253
  * A router also gets the bundled share (B2), the merge angle (B6) and the bundle stretch (B3) against the routes `base` of `corners`.
233
254
  * It also gets the measures of MEAS3A: R12, R14, B5, B7, B8, B9, M1 and M4. The lane measures open each shared segment at `MEASURE_LANE_WIDTH`.
234
- * A result with zoom levels also gets the link agreement (Z9), for a router and for a grouping rule.
255
+ * A result with zoom levels also gets the link agreement (Z9) and `groupShapeValues`, for a router and for a grouping rule.
256
+ * A router also gets the symmetry of its routes (H3). A whole strategy also gets M2, M3 and M5 of the board of its output.
235
257
  */
236
258
  function rowValues(step: ScreenStep, graph: Graph, row: CompareRow, base: () => readonly Route[]): Record<string, number> {
237
259
  const values: Record<string, number> = { ...row.values };
@@ -265,10 +287,21 @@ function rowValues(step: ScreenStep, graph: Graph, row: CompareRow, base: () =>
265
287
  }
266
288
  values["M1.area"] = laneArea(graph, open);
267
289
  values["M4.laneThrough"] = lanesThrough(graph, open);
290
+ const h3 = routeSymmetry(graph, routes);
291
+ if (h3 !== undefined) values["H3.share"] = h3;
268
292
  }
269
293
  if ((givesRoutes(step) || step === "group") && row.result?.levels) {
270
- const la = linkAgreement(graph, row.result.levels, row.result.routes, boardUnit(graph));
294
+ const levels = row.result.levels;
295
+ const la = linkAgreement(graph, levels, row.result.routes, boardUnit(graph));
271
296
  if (la !== undefined) values["Z9.mean"] = la;
297
+ Object.assign(values, groupShapeValues(graph, levels));
298
+ }
299
+ // The step `whole` can move the boxes with a room strategy. M2, M3 and M5 compare the board of the output with the input board.
300
+ if (step === "whole" && row.result) {
301
+ const board = wholeBoard(row.result) ?? graph;
302
+ values["M2.order"] = roomOrderFaults(graph.nodes, board.nodes);
303
+ values["M3.displacement"] = boxDisplacement(graph.nodes, board.nodes, boardUnit(graph));
304
+ values["M5.stress"] = distanceDistortion(graph.nodes, board.nodes);
272
305
  }
273
306
  return values;
274
307
  }
@@ -426,6 +459,8 @@ export function screen(step: ScreenStep, strategies: readonly (Strategy<ScreenSt
426
459
  const values = rowValues(step, graph, row, cornersBase);
427
460
  // Z2 is the count of the hard rule `link-ends`, as a measure. The steps `router`, `highway` and `group` check the rule.
428
461
  if (hard["link-ends"] !== undefined) values["Z2.linkEnds"] = hard["link-ends"];
462
+ // Z1 is the count of the hard rule `counts-sum`, as a measure, the same way.
463
+ if (hard["counts-sum"] !== undefined) values["Z1.countsSum"] = hard["counts-sum"];
429
464
  if (optima && withOptima.has(input.id) && row.result) Object.assign(values, gapValues(input, graph, row.result, cornersBase(), gapBase(), optima));
430
465
  if (shuffles > 0 && row.result) {
431
466
  const router = asRouter(strategy as Strategy<"router">, own[row.strategy]);
@@ -15,7 +15,7 @@ import { asRouter, listStrategies, STEPS, type LaneBoard, type Registry, type St
15
15
  import { measureLevelFaults, sumFaults } from "./links.ts";
16
16
  import { checkLevels } from "./properties.ts";
17
17
  import { ambiguity, inkSaved } from "./measures/bundles.ts";
18
- import { bendingEnergy, curveFaults, g1Defect, g2Defect } from "./measures/curves.ts";
18
+ import { bendingEnergy, curvatureVariation, curveDeviation, curveFaults, g1Defect, g2Defect } from "./measures/curves.ts";
19
19
  import { balance, cutShare, farLevel, groupStability, levelGrouping, modularity, treeCost } from "./measures/groups.ts";
20
20
  import { nestingFaults, orderStability, positionStability } from "./measures/levels.ts";
21
21
  import { paretoFront, type Objective } from "./measures/pareto.ts";
@@ -328,7 +328,7 @@ export interface CurveRun {
328
328
  readonly edges: readonly string[];
329
329
  }
330
330
 
331
- /** This function draws the routes of a result with each curve strategy and measures the curves (C1, C2, C3, C6 and the curve rules). */
331
+ /** This function draws the routes of a result with each curve strategy and measures the curves (C1, C2, C3, C4, C6, C8 and the curve rules). */
332
332
  export function curveRuns(graph: Graph, result: Result, curves: readonly Strategy<"curve">[], options: CurveRunOptions): CurveRun[] {
333
333
  const byId = nodeIndex(graph);
334
334
  const edges = new Map(graph.edges.map((e) => [e.id, e]));
@@ -363,6 +363,8 @@ export function curveRuns(graph: Graph, result: Result, curves: readonly Strateg
363
363
  const g1: number[] = [];
364
364
  const g2: number[] = [];
365
365
  const energy: number[] = [];
366
+ const variation: number[] = [];
367
+ const deviation: number[] = [];
366
368
  const rules = { "curve-clear": 0, "curve-ends": 0, "curve-g1": 0, "curve-simple": 0 };
367
369
  let length = 0;
368
370
  drawn.forEach((segments, i) => {
@@ -371,6 +373,8 @@ export function curveRuns(graph: Graph, result: Result, curves: readonly Strateg
371
373
  g1.push(g1Defect(segments));
372
374
  g2.push(g2Defect(segments, u));
373
375
  energy.push(bendingEnergy(s, 0.25));
376
+ variation.push(curvatureVariation(s, 0.25));
377
+ deviation.push(curveDeviation(s, r.points, u / 4) / u);
374
378
  length += polylineLength(s);
375
379
  const normals = options.normals.has(curve.name);
376
380
  const f = curveFaults({
@@ -394,6 +398,10 @@ export function curveRuns(graph: Graph, result: Result, curves: readonly Strateg
394
398
  "C2.max": g2.reduce((m, x) => Math.max(m, x), 0),
395
399
  "C3.median": quantile(energy, 0.5) || 0,
396
400
  "C3.q95": quantile(energy, 0.95) || 0,
401
+ "C4.median": quantile(variation, 0.5) || 0,
402
+ "C4.q95": quantile(variation, 0.95) || 0,
403
+ "C8.median": quantile(deviation, 0.5) || 0,
404
+ "C8.max": deviation.reduce((m, x) => Math.max(m, x), 0),
397
405
  "C6.length": ink > 0 ? length / ink : 1,
398
406
  ...rules,
399
407
  };
@@ -1,5 +1,5 @@
1
1
  import { center, dist, pointBoxDistance, type Point } from "../geom.ts";
2
- import { fixedPortPoint, nodeIndex, resolvePorts, type End, type Graph, type Node, type Result, type Route, type SharedRun } from "../model.ts";
2
+ import { fixedPortPoint, nodeIndex, outerGraph, type End, type Graph, type Node, type Result, type Route, type SharedRun } from "../model.ts";
3
3
  import { placePlug } from "../plugs.ts";
4
4
  import { orderLanes, type RunOrder } from "../segments.ts";
5
5
  import { WayFinder, type WayOptions } from "../ways.ts";
@@ -69,7 +69,7 @@ function slice(stops: Stops, on: number, off: number): Point[] {
69
69
  * The same graph, plan and options give the same result.
70
70
  */
71
71
  export function follow(input: Graph, plan: HighwayPlan, options: FollowOptions = {}): Result {
72
- const graph = resolvePorts(input);
72
+ const graph = outerGraph(input);
73
73
  const byId = nodeIndex(graph);
74
74
  const ways = new WayFinder(graph.nodes, options);
75
75
  const base = new Map((options.routes?.routes ?? []).map((r) => [r.edge, r.points]));
package/src/model.ts CHANGED
@@ -30,6 +30,12 @@ export interface Node {
30
30
  readonly ports?: readonly Port[];
31
31
  /** This data is free for the grouping rules of the nodes. The rule `keys` reads the string `data.group` by default. */
32
32
  readonly data?: Readonly<Record<string, unknown>>;
33
+ /**
34
+ * This id names the frame node that holds the node. A frame is a node that another node names as its parent.
35
+ * An edge from a plug of a frame to a node in the frame is an inner edge (`splitFrames`). The router `corners` routes it inside the frame.
36
+ * A parent that names no node, or a cycle of parents, makes each router throw a `RangeError`.
37
+ */
38
+ readonly parent?: string;
33
39
  }
34
40
 
35
41
  /** One end of an edge: a node, and optionally one plug of that node. */
@@ -56,6 +62,8 @@ export interface Graph {
56
62
  export interface Route {
57
63
  readonly edge: string;
58
64
  readonly points: readonly Point[];
65
+ /** This id names the frame of an inner route: the route stays inside the box of that node. An outer route has no frame. */
66
+ readonly frame?: string;
59
67
  }
60
68
 
61
69
  /**
@@ -194,3 +202,62 @@ export function fixedPortPoint(node: Node, end: End): Point | undefined {
194
202
  if (at === undefined) return undefined;
195
203
  return { x: node.box.x + at.x, y: node.box.y + at.y };
196
204
  }
205
+
206
+ /**
207
+ * An inner edge: an edge from a plug of a frame to a node in that frame, at any depth.
208
+ * `plug` tells which end of the edge is the plug of the frame.
209
+ */
210
+ export interface InnerEdge {
211
+ readonly edge: Edge;
212
+ readonly frame: string;
213
+ readonly plug: "source" | "target";
214
+ }
215
+
216
+ /** This function throws a `RangeError` when the parent of a node names no node, or when the parents of a node make a cycle. */
217
+ export function checkParents(graph: Graph, byId: ReadonlyMap<string, Node> = nodeIndex(graph)): void {
218
+ const done = new Set<string>();
219
+ for (const n of graph.nodes) {
220
+ const seen = new Set<string>();
221
+ for (let m: Node | undefined = n; m?.parent !== undefined && !done.has(m.id); ) {
222
+ seen.add(m.id);
223
+ const p = byId.get(m.parent);
224
+ if (!p) throw new RangeError(`The parent "${m.parent}" of the node "${m.id}" names no node of the graph.`);
225
+ if (seen.has(p.id)) throw new RangeError(`The parents of the node "${n.id}" make a cycle through the node "${p.id}".`);
226
+ m = p;
227
+ }
228
+ for (const id of seen) done.add(id);
229
+ }
230
+ }
231
+
232
+ /** This function tells if the node `frame` holds the node `id`, at any depth. A node does not hold itself. */
233
+ function holds(byId: ReadonlyMap<string, Node>, frame: string, id: string): boolean {
234
+ for (let p = byId.get(id)?.parent; p !== undefined; p = byId.get(p)?.parent) if (p === frame) return true;
235
+ return false;
236
+ }
237
+
238
+ /**
239
+ * This function checks the parents of a graph (`checkParents`), and takes its inner edges out.
240
+ * An inner edge of the frame F has a plug of F (`End.port`) at one end. F holds the node at the other end.
241
+ * The graph that it gives keeps each node and the other edges. A graph without `parent` comes back as the same object, with no inner edge.
242
+ */
243
+ export function splitFrames(graph: Graph): { readonly graph: Graph; readonly inner: readonly InnerEdge[] } {
244
+ if (!graph.nodes.some((n) => n.parent !== undefined)) return { graph, inner: [] };
245
+ const byId = nodeIndex(graph);
246
+ checkParents(graph, byId);
247
+ const inner: InnerEdge[] = [];
248
+ const edges = graph.edges.filter((e) => {
249
+ if (e.source.port !== undefined && holds(byId, e.source.node, e.target.node)) inner.push({ edge: e, frame: e.source.node, plug: "source" });
250
+ else if (e.target.port !== undefined && holds(byId, e.target.node, e.source.node)) inner.push({ edge: e, frame: e.target.node, plug: "target" });
251
+ else return true;
252
+ return false;
253
+ });
254
+ return { graph: inner.length === 0 ? graph : { ...graph, edges }, inner };
255
+ }
256
+
257
+ /**
258
+ * This function gives the graph of a router without frames. It has the plugs of `resolvePorts`, and no inner edges (`splitFrames`).
259
+ * Each router except `corners` uses it at the start of `route`. So an inner edge gets no route from such a router.
260
+ */
261
+ export function outerGraph(graph: Graph): Graph {
262
+ return splitFrames(resolvePorts(graph)).graph;
263
+ }
@@ -1,16 +1,17 @@
1
1
  import { center, type Point } from "../geom.ts";
2
- import { nodeIndex, fixedPortPoint, resolvePorts, type Edge, type Graph, type Node, type Result, type Route, type Router } from "../model.ts";
2
+ import { nodeIndex, fixedPortPoint, resolvePorts, splitFrames, type Edge, type Graph, type Node, type Result, type Route, type Router } from "../model.ts";
3
3
  import { placePlug, type Plug } from "../plugs.ts";
4
4
  import { justifyPlugs } from "../plugs/justify.ts";
5
5
  import { sidePlugs } from "../plugs/side.ts";
6
6
  import type { PlugPair, Strategy } from "../strategies/model.ts";
7
- import { WayFinder, type WayOptions } from "../ways.ts";
7
+ import { WayFinder } from "../ways.ts";
8
+ import { frameRoutes, type FrameOptions } from "./frames.ts";
8
9
 
9
10
  /** The ways to place a free plug of the corner router. */
10
11
  export type PlugPlace = "facing" | "side";
11
12
 
12
13
  /** The options of the corner router. */
13
- export interface CornersOptions extends WayOptions {
14
+ export interface CornersOptions extends FrameOptions {
14
15
  /** The name in the benchmark. */
15
16
  readonly name: string;
16
17
  /** The benchmark skips a fixture with more nodes. */
@@ -38,15 +39,21 @@ export interface CornersOptions extends WayOptions {
38
39
  * This function makes a router that goes around the nodes.
39
40
  * Each edge leaves its plug along the normal of the side. It goes a shortest way (A*) through a sparse visibility graph
40
41
  * of the box corners, without the needless corners. It enters the target plug along its normal.
42
+ * An inner edge (`splitFrames`) gets an inner route in the bounds of its frame (`frameRoutes`), after the outer routes.
41
43
  */
42
44
  export function cornersRouter(options: CornersOptions): Router {
43
45
  const router: Router = {
44
46
  name: options.name,
45
47
  route(input: Graph): Result {
46
- const graph = resolvePorts(input);
48
+ const { graph, inner } = splitFrames(resolvePorts(input));
47
49
  const byId = nodeIndex(graph);
48
50
  const ways = new WayFinder(graph.nodes, options);
49
51
  const routes: Route[] = [];
52
+ // The inner routes come after the outer routes.
53
+ const done = (): Result => {
54
+ if (inner.length > 0) routes.push(...frameRoutes(graph, inner, options));
55
+ return { routes };
56
+ };
50
57
  if (!options.plug && (options.plugs ?? "facing") === "facing" && !((options.plugGap ?? 0) > 0)) {
51
58
  for (const e of graph.edges) {
52
59
  const s = byId.get(e.source.node);
@@ -56,7 +63,7 @@ export function cornersRouter(options: CornersOptions): Router {
56
63
  const pt = placePlug(t, e.target, center(s.box));
57
64
  routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
58
65
  }
59
- return { routes };
66
+ return done();
60
67
  }
61
68
  // First the plugs of each edge, then the gaps of the plugs of each side, then the ways between the plugs.
62
69
  const ends: [Edge, Node, Node, Plug, Plug][] = [];
@@ -75,7 +82,7 @@ export function cornersRouter(options: CornersOptions): Router {
75
82
  }
76
83
  if ((options.plugGap ?? 0) > 0) spacePlugs(ends, options.plugGap!);
77
84
  for (const [e, s, t, ps, pt] of ends) routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
78
- return { routes };
85
+ return done();
79
86
  },
80
87
  };
81
88
  return options.maxNodes === undefined ? router : { ...router, maxNodes: options.maxNodes };
@@ -0,0 +1,136 @@
1
+ import { center, dist, type Box, type Point } from "../geom.ts";
2
+ import { nodeIndex, type Graph, type InnerEdge, type Node, type Route } from "../model.ts";
3
+ import { placePlug, type Plug } from "../plugs.ts";
4
+ import { WayFinder, type WayOptions } from "../ways.ts";
5
+
6
+ /**
7
+ * The rule for a node box that is not fully inside the box of its frame.
8
+ * With `throw`, the router throws a `RangeError`. With `skip`, the node is not an obstacle, and its inner edges get no route.
9
+ * With `clamp`, the router cuts the box to the bounds of the frame.
10
+ */
11
+ export type Outside = "throw" | "skip" | "clamp";
12
+
13
+ /** The options of the inner routes: the options of the way finder, the rule `outside` and the inset `frameInset`. */
14
+ export interface FrameOptions extends WayOptions {
15
+ /** The rule for a node box outside the box of its frame. The default is `throw`. */
16
+ readonly outside?: Outside;
17
+ /** The distance from the walls of a frame to the bounds of its inner routes. The default is the margin of the router. */
18
+ readonly frameInset?: number;
19
+ }
20
+
21
+ /** The inside of one frame for the inner edges to one depth: the bounds, the way finder and the obstacles by id. */
22
+ interface Room {
23
+ readonly ways: WayFinder;
24
+ readonly nodes: ReadonlyMap<string, Node>;
25
+ }
26
+
27
+ /** The tiny gap between the bounds and the band of a wall, so that a way along the bounds stays clear of the wall. */
28
+ const EPS = 1e-6;
29
+
30
+ /**
31
+ * This function gives the inner routes of the inner edges (`splitFrames`). Each route stays in the bounds of its frame:
32
+ * the box of the frame, smaller by `frameInset`. It goes around the other nodes of the frame.
33
+ * It leaves the plug of the frame into the frame, along the normal of the wall. Each route has the id of its frame in `frame`.
34
+ */
35
+ export function frameRoutes(graph: Graph, inner: readonly InnerEdge[], options: FrameOptions): Route[] {
36
+ const byId = nodeIndex(graph);
37
+ const children = new Map<string, Node[]>();
38
+ for (const n of graph.nodes) {
39
+ if (n.parent === undefined) continue;
40
+ const list = children.get(n.parent);
41
+ if (list) list.push(n);
42
+ else children.set(n.parent, [n]);
43
+ }
44
+ const margin = options.margin ?? 5;
45
+ const inset = options.frameInset ?? margin;
46
+ const rooms = new Map<string, Room>();
47
+ const routes: Route[] = [];
48
+ for (const { edge, frame, plug } of inner) {
49
+ const f = byId.get(frame)!;
50
+ const [own, far] = plug === "source" ? [edge.source, edge.target] : [edge.target, edge.source];
51
+ // The nodes between the frame and the far end: each of them holds the far end, so it is not an obstacle.
52
+ const chain: string[] = [];
53
+ for (let p = byId.get(far.node)!.parent!; p !== frame; p = byId.get(p)!.parent!) chain.push(p);
54
+ const key = JSON.stringify([frame, ...chain]);
55
+ let room = rooms.get(key);
56
+ if (!room) rooms.set(key, (room = makeRoom(f, chain, children, byId, inset, options)));
57
+ const end = room.nodes.get(far.node);
58
+ if (!end) continue;
59
+ const fp = placePlug(f, own, center(end.box));
60
+ const inward = { x: -fp.normal.x, y: -fp.normal.y };
61
+ // The way starts on the bounds, straight in from the plug of the wall.
62
+ const start: Plug = { point: { x: fp.point.x + inward.x * inset, y: fp.point.y + inward.y * inset }, normal: inward };
63
+ const ep = placePlug(end, far, fp.point);
64
+ const ends = new Set<Node>([end]);
65
+ const points =
66
+ plug === "source" ? stub(fp.point, room.ways.between(start, ep, ends)) : stub(fp.point, room.ways.between(ep, start, ends).reverse()).reverse();
67
+ routes.push({ edge: edge.id, points, frame });
68
+ }
69
+ return routes;
70
+ }
71
+
72
+ /** This function puts the plug of the wall before a way that starts on the bounds. A first point on the line of the stub goes. */
73
+ function stub(plug: Point, way: Point[]): Point[] {
74
+ const [a, b] = way;
75
+ if (!a || dist(a, plug) === 0) return way;
76
+ if (b && Math.abs((a.x - plug.x) * (b.y - plug.y) - (a.y - plug.y) * (b.x - plug.x)) < 1e-9 && (a.x - plug.x) * (b.x - a.x) + (a.y - plug.y) * (b.y - a.y) > 0)
77
+ return [plug, ...way.slice(1)];
78
+ return [plug, ...way];
79
+ }
80
+
81
+ /**
82
+ * This function makes the room of a frame for the far ends below the nodes of `chain` (the nearest first).
83
+ * The obstacles are the children of the frame and of each node of the chain, except the nodes of the chain.
84
+ * Four walls close the bounds, so that no way leaves them.
85
+ */
86
+ function makeRoom(f: Node, chain: readonly string[], children: ReadonlyMap<string, readonly Node[]>, byId: ReadonlyMap<string, Node>, inset: number, options: FrameOptions): Room {
87
+ const margin = options.margin ?? 5;
88
+ const bounds = shrink(f.box, inset);
89
+ const skip = new Set(chain);
90
+ const nodes = new Map<string, Node>();
91
+ for (const holder of [f.id, ...chain]) {
92
+ for (const n of children.get(holder) ?? []) {
93
+ if (skip.has(n.id)) continue;
94
+ const box = byId.get(holder)!.box;
95
+ if (inside(n.box, box)) {
96
+ nodes.set(n.id, n);
97
+ continue;
98
+ }
99
+ const rule = options.outside ?? "throw";
100
+ if (rule === "throw")
101
+ throw new RangeError(`The box of the node "${n.id}" is not inside the box of its frame "${holder}". The option outside can skip or clamp it.`);
102
+ if (rule === "skip") continue;
103
+ const cut = cutBox(n.box, bounds);
104
+ if (cut) nodes.set(n.id, { ...n, box: cut, ...(n.ports ? { ports: n.ports.map((p) => (p.at ? { ...p, at: { x: p.at.x + n.box.x - cut.x, y: p.at.y + n.box.y - cut.y } } : p)) } : {}) });
105
+ }
106
+ }
107
+ // The band of each wall starts half a margin outside the bounds, so that a way keeps in the bounds.
108
+ const w = shrink(bounds, -(margin / 2 + EPS));
109
+ const t = 2 * margin + 1;
110
+ const walls: Node[] = [
111
+ { x: w.x - t, y: w.y - t, w: w.w + 2 * t, h: t },
112
+ { x: w.x - t, y: w.y + w.h, w: w.w + 2 * t, h: t },
113
+ { x: w.x - t, y: w.y, w: t, h: w.h },
114
+ { x: w.x + w.w, y: w.y, w: t, h: w.h },
115
+ ].map((box, i) => ({ id: `\u0000wall${i}`, box }));
116
+ return { ways: new WayFinder([...nodes.values(), ...walls], options), nodes };
117
+ }
118
+
119
+ /** This function gives the box, smaller by `d` on each side. A negative `d` makes it larger. */
120
+ function shrink(b: Box, d: number): Box {
121
+ return { x: b.x + d, y: b.y + d, w: Math.max(0, b.w - 2 * d), h: Math.max(0, b.h - 2 * d) };
122
+ }
123
+
124
+ /** This function tells if the box `a` is inside the box `b`. */
125
+ function inside(a: Box, b: Box): boolean {
126
+ return a.x >= b.x && a.y >= b.y && a.x + a.w <= b.x + b.w && a.y + a.h <= b.y + b.h;
127
+ }
128
+
129
+ /** This function gives the part of the box `a` inside the box `b`, or `undefined` when they have no common area. */
130
+ function cutBox(a: Box, b: Box): Box | undefined {
131
+ const x = Math.max(a.x, b.x);
132
+ const y = Math.max(a.y, b.y);
133
+ const w = Math.min(a.x + a.w, b.x + b.w) - x;
134
+ const h = Math.min(a.y + a.h, b.y + b.h) - y;
135
+ return w > 0 && h > 0 ? { x, y, w, h } : undefined;
136
+ }
@@ -9,6 +9,7 @@ import { byCollection, trunksRouter } from "./trunks.ts";
9
9
  export { boxExit, straightRouter } from "./straight.ts";
10
10
  export { plugsRouter } from "./plugs.ts";
11
11
  export { cornersRouter, type CornersOptions } from "./corners.ts";
12
+ export { frameRoutes, type FrameOptions, type Outside } from "./frames.ts";
12
13
  export { sharedRouter, type ShareCost, type SharedNegotiation, type SharedOptions, type ShareMerge, type ShareOrder } from "./shared.ts";
13
14
  export { ATTRACTION_DEFAULTS, ATTRACTION_SCHEMA, attractionRouter, attractionStrategy, type AttractionOptions } from "./attraction/index.ts";
14
15
  export { sectionsRouter, type SectionsOptions } from "./sections.ts";
@@ -1,6 +1,6 @@
1
1
  import { inflectionCurve } from "../curves.ts";
2
2
  import { center } from "../geom.ts";
3
- import { nodeIndex, resolvePorts, type Graph, type Result, type Route, type Router } from "../model.ts";
3
+ import { nodeIndex, outerGraph, type Graph, type Result, type Route, type Router } from "../model.ts";
4
4
  import { placePlug } from "../plugs.ts";
5
5
 
6
6
  /**
@@ -10,7 +10,7 @@ import { placePlug } from "../plugs.ts";
10
10
  export const plugsRouter: Router = {
11
11
  name: "plugs",
12
12
  route(input: Graph): Result {
13
- const graph = resolvePorts(input);
13
+ const graph = outerGraph(input);
14
14
  const byId = nodeIndex(graph);
15
15
  const routes: Route[] = [];
16
16
  for (const e of graph.edges) {
@@ -2,7 +2,7 @@ import { BACKTRACK_MIN, goesOutAndBack } from "../bench/links.ts";
2
2
  import { center, dist, pointSegmentDistance, polylineLength, type Point } from "../geom.ts";
3
3
  import { LEVEL_LINK_DEFAULTS, type LevelLinkOptions } from "../levels.ts";
4
4
  import { depthLevels, type DepthLevelOptions } from "../levels/depths/index.ts";
5
- import { nodeIndex, resolvePorts, type Graph, type Level, type Node, type Result, type Route, type Router, type SharedRun } from "../model.ts";
5
+ import { nodeIndex, outerGraph, type Graph, type Level, type Node, type Result, type Route, type Router, type SharedRun } from "../model.ts";
6
6
  import { placePlug } from "../plugs.ts";
7
7
  import { allSections, buildSections, checkSections, SECTION_DEFAULTS, type Section, type SectionOptions } from "../sections.ts";
8
8
  import { orderLanes, type RunOrder } from "../segments.ts";
@@ -92,7 +92,7 @@ export function sectionsRouter(options: SectionsOptions): Router {
92
92
  const router: Router = {
93
93
  name: options.name,
94
94
  route(input: Graph): Result {
95
- const graph = resolvePorts(input);
95
+ const graph = outerGraph(input);
96
96
  const stretch = options.stretch ?? SECTIONS_DEFAULTS.stretch;
97
97
  const stretchPad = options.stretchPad ?? SECTIONS_DEFAULTS.stretchPad;
98
98
  const hubSlack = options.hubSlack ?? SECTIONS_DEFAULTS.hubSlack;
@@ -1,5 +1,5 @@
1
1
  import { center, dist, type Point } from "../geom.ts";
2
- import { nodeIndex, resolvePorts, type Edge, type Graph, type GroupRule, type Result, type Route, type Router, type SharedRun } from "../model.ts";
2
+ import { nodeIndex, outerGraph, type Edge, type Graph, type GroupRule, type Result, type Route, type Router, type SharedRun } from "../model.ts";
3
3
  import { channelWidth } from "../channels.ts";
4
4
  import { orderRun } from "../lane-order.ts";
5
5
  import { stretchLaneOrders, type LaneStretchOrder } from "../lane-orders/stretch/index.ts";
@@ -149,7 +149,7 @@ export function sharedRouter(options: SharedOptions, negotiation?: SharedNegotia
149
149
  const router: Router = {
150
150
  name: options.name,
151
151
  route(input: Graph): Result {
152
- const graph = resolvePorts(input);
152
+ const graph = outerGraph(input);
153
153
  const byId = nodeIndex(graph);
154
154
  const ways = new WayFinder(graph.nodes, options);
155
155
  const corners = ways.corners;
@@ -1,5 +1,5 @@
1
1
  import { center, type Box, type Point } from "../geom.ts";
2
- import { fixedPortPoint, nodeIndex, resolvePorts, type Graph, type Result, type Route, type Router } from "../model.ts";
2
+ import { fixedPortPoint, nodeIndex, outerGraph, type Graph, type Result, type Route, type Router } from "../model.ts";
3
3
 
4
4
  /** This function gives the point where the ray from the center of the box to `toward` leaves the box. */
5
5
  export function boxExit(box: Box, toward: Point): Point {
@@ -20,7 +20,7 @@ export function boxExit(box: Box, toward: Point): Point {
20
20
  export const straightRouter: Router = {
21
21
  name: "straight",
22
22
  route(input: Graph): Result {
23
- const graph = resolvePorts(input);
23
+ const graph = outerGraph(input);
24
24
  const byId = nodeIndex(graph);
25
25
  const routes: Route[] = [];
26
26
  for (const e of graph.edges) {
@@ -1,5 +1,5 @@
1
1
  import { center, dist, type Point } from "../geom.ts";
2
- import { nodeIndex, resolvePorts, type Bundle, type Edge, type Graph, type GroupRule, type Node, type Result, type Route, type Router, type SharedRun } from "../model.ts";
2
+ import { nodeIndex, outerGraph, type Bundle, type Edge, type Graph, type GroupRule, type Node, type Result, type Route, type Router, type SharedRun } from "../model.ts";
3
3
  import { placePlug, type Plug } from "../plugs.ts";
4
4
  import { orderLanes, type RunOrder } from "../segments.ts";
5
5
  import { WayFinder, type WayOptions } from "../ways.ts";
@@ -210,7 +210,7 @@ export function trunksRouter(options: TrunksOptions): Router {
210
210
  const router: Router = {
211
211
  name: options.name,
212
212
  route(input: Graph): Result {
213
- const graph = resolvePorts(input);
213
+ const graph = outerGraph(input);
214
214
  const byId = nodeIndex(graph);
215
215
  const ways = new WayFinder(graph.nodes, options);
216
216
  const points = new Map<string, readonly Point[]>();
@@ -125,6 +125,19 @@ export const CORNERS_SCHEMA: OptionSchema<RouterPreset<CornersOptions>> = {
125
125
  },
126
126
  plug: { type: "strategy", step: "plugs", describe: "The plug rule: a strategy of the step `plugs`. Without it, the option `plugs` places the plugs." },
127
127
  plugOptions: { type: "object", describe: "The options of the strategy `plug`." },
128
+ outside: {
129
+ type: "enum",
130
+ values: ["throw", "skip", "clamp"],
131
+ default: "throw",
132
+ describe: "The rule for a box outside the box of its frame: throw a `RangeError`, skip the box and its inner edges, or cut the box to the bounds.",
133
+ },
134
+ frameInset: {
135
+ type: "number",
136
+ min: 0,
137
+ max: 20,
138
+ step: 1,
139
+ describe: "The distance from the walls of a frame to the bounds of its inner routes. Without a value, the inset is the margin.",
140
+ },
128
141
  };
129
142
 
130
143
  /** The declarations of the options of the router `trunks`. */