@mark1russell7/highways 0.9.1 → 0.9.3
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/README.md +66 -2
- package/build/index.d.ts +195 -4
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3530 -2298
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bench/compare.ts +13 -1
- package/src/bench/measures/curves.ts +55 -1
- package/src/bench/measures/groups.ts +71 -0
- package/src/bench/measures/index.ts +1 -0
- package/src/bench/measures/levels.ts +24 -0
- package/src/bench/measures/room.ts +106 -1
- package/src/bench/measures/symmetry.ts +114 -0
- package/src/bench/screen/forms.ts +9 -2
- package/src/bench/screen/measures.ts +40 -3
- package/src/bench/screen/screen.ts +40 -5
- package/src/bench/suite.ts +10 -2
- package/src/highways/follow.ts +2 -2
- package/src/index.ts +2 -0
- package/src/model.ts +67 -0
- package/src/plugs/fix.ts +112 -0
- package/src/plugs/justify.ts +66 -0
- package/src/plugs/row-cards.ts +46 -0
- package/src/plugs/side.ts +23 -2
- package/src/plugs/spread/index.ts +217 -0
- package/src/routers/corners.ts +15 -44
- package/src/routers/frames.ts +136 -0
- package/src/routers/index.ts +1 -0
- package/src/routers/plugs.ts +2 -2
- package/src/routers/sections.ts +2 -2
- package/src/routers/shared.ts +2 -2
- package/src/routers/straight.ts +2 -2
- package/src/routers/trunks.ts +2 -2
- package/src/strategies/builtins.ts +31 -1
- package/src/strategies/schemas.ts +35 -0
|
@@ -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 {
|
|
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)
|
|
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
|
|
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]);
|
package/src/bench/suite.ts
CHANGED
|
@@ -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
|
};
|
package/src/highways/follow.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { center, dist, pointBoxDistance, type Point } from "../geom.ts";
|
|
2
|
-
import { fixedPortPoint, nodeIndex,
|
|
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 =
|
|
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/index.ts
CHANGED
|
@@ -18,6 +18,8 @@ export * from "./room/index.ts";
|
|
|
18
18
|
export * from "./room/lanes.ts";
|
|
19
19
|
export * from "./room/levels/index.ts";
|
|
20
20
|
export * from "./plugs/facing/index.ts";
|
|
21
|
+
export * from "./plugs/spread/index.ts";
|
|
22
|
+
export * from "./plugs/fix.ts";
|
|
21
23
|
export * from "./levels/depths/index.ts";
|
|
22
24
|
export * from "./whole/pipeline/index.ts";
|
|
23
25
|
export * from "./bench/properties.ts";
|
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
|
+
}
|
package/src/plugs/fix.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixed ports from a plug rule (FIX-PORTS): the plugs of the rule become the fixed `at` of the ports.
|
|
3
|
+
* Then each router takes each plug rule, because each router keeps a fixed plug at its point.
|
|
4
|
+
*/
|
|
5
|
+
import { nearestSide, SIDE_NORMAL, type Plug } from "../plugs.ts";
|
|
6
|
+
import { nodeIndex, portOffset, resolvePorts, splitFrames, type Edge, type End, type Graph, type Node, type Port, type Side } from "../model.ts";
|
|
7
|
+
import { STRATEGIES } from "../strategies/builtins.ts";
|
|
8
|
+
import { getStrategy, type Registry, type Strategy } from "../strategies/model.ts";
|
|
9
|
+
import { WayFinder, type WayOptions } from "../ways.ts";
|
|
10
|
+
|
|
11
|
+
/** The options of `fixPorts`. */
|
|
12
|
+
export interface FixPortsOptions {
|
|
13
|
+
/** The options of the plug rule. The rule gets its `defaults` for each option that this object does not give. */
|
|
14
|
+
readonly plugOptions?: object;
|
|
15
|
+
/** The registry that gives the rule by its name. The default is `STRATEGIES`. */
|
|
16
|
+
readonly registry?: Registry;
|
|
17
|
+
/** The options of the way finder that a rule reads, for example `side`. The defaults are the defaults of the router `corners`. */
|
|
18
|
+
readonly ways?: WayOptions;
|
|
19
|
+
/**
|
|
20
|
+
* With `true`, an end without a port gets a new fixed port, with the id `<edge id>:source` or `<edge id>:target`.
|
|
21
|
+
* The default is `false`: such an end stays free.
|
|
22
|
+
*/
|
|
23
|
+
readonly newPorts?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** This function gives the side of a plug from its normal. */
|
|
27
|
+
function sideOf(plug: Plug, node: Node): Side {
|
|
28
|
+
for (const [side, n] of Object.entries(SIDE_NORMAL) as [Side, { x: number; y: number }][]) if (n.x === plug.normal.x && n.y === plug.normal.y) return side;
|
|
29
|
+
return nearestSide(node.box, plug.point);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** This function gives an id that no port of the node has, from `base`. */
|
|
33
|
+
function freeId(taken: Set<string>, base: string): string {
|
|
34
|
+
let id = base;
|
|
35
|
+
for (let i = 2; taken.has(id); i++) id = `${base}#${i}`;
|
|
36
|
+
taken.add(id);
|
|
37
|
+
return id;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* This function runs the plug rule. It gives each free port a fixed `at` and a `side` from the plugs of the rule.
|
|
42
|
+
* A free port has no `at` and no `along`. A port of several ends takes the plug of its first end, in the order of the edges.
|
|
43
|
+
* A fixed port keeps its values, and a port of no edge stays free.
|
|
44
|
+
*
|
|
45
|
+
* The rule sees the graph of `corners`: the plugs of `resolvePorts`, without the inner edges of the frames.
|
|
46
|
+
* The rule is a strategy of the step `plugs`, or the name of one in the registry.
|
|
47
|
+
* The output is a new graph. The function does not change the input.
|
|
48
|
+
*/
|
|
49
|
+
export function fixPorts(graph: Graph, rule: Strategy<"plugs"> | string, options: FixPortsOptions = {}): Graph {
|
|
50
|
+
const strategy = typeof rule === "string" ? getStrategy(options.registry ?? STRATEGIES, "plugs", rule) : rule;
|
|
51
|
+
const outer = splitFrames(resolvePorts(graph)).graph;
|
|
52
|
+
const pairs = strategy.run({ graph: outer, ways: new WayFinder(outer.nodes, options.ways) }, options.plugOptions);
|
|
53
|
+
const byEdge = new Map<string, (typeof pairs)[number][]>();
|
|
54
|
+
for (const p of pairs) {
|
|
55
|
+
const list = byEdge.get(p.edge);
|
|
56
|
+
if (list) list.push(p);
|
|
57
|
+
else byEdge.set(p.edge, [p]);
|
|
58
|
+
}
|
|
59
|
+
const byId = nodeIndex(graph);
|
|
60
|
+
// The new fixed ports of each node, by the port id, and the new ports for the ends without a port.
|
|
61
|
+
const fixed = new Map<string, Map<string, Port>>();
|
|
62
|
+
const added = new Map<string, Port[]>();
|
|
63
|
+
const taken = new Map<string, Set<string>>();
|
|
64
|
+
const portsOf = (node: Node): Map<string, Port> => {
|
|
65
|
+
let m = fixed.get(node.id);
|
|
66
|
+
if (!m) fixed.set(node.id, (m = new Map()));
|
|
67
|
+
return m;
|
|
68
|
+
};
|
|
69
|
+
const fix = (node: Node, plug: Plug, base: Port): Port => ({
|
|
70
|
+
...base,
|
|
71
|
+
side: sideOf(plug, node),
|
|
72
|
+
at: { x: plug.point.x - node.box.x, y: plug.point.y - node.box.y },
|
|
73
|
+
});
|
|
74
|
+
// The outer graph has the edge objects of the input. An inner edge keeps its ends, because the rule did not see it.
|
|
75
|
+
const outerEdges = new Set(outer.edges);
|
|
76
|
+
const edges = graph.edges.map((e): Edge => {
|
|
77
|
+
if (!outerEdges.has(e)) return e;
|
|
78
|
+
const pair = byEdge.get(e.id)?.shift();
|
|
79
|
+
if (!pair) return e;
|
|
80
|
+
const end = (which: "source" | "target", plug: Plug): End => {
|
|
81
|
+
const own = e[which];
|
|
82
|
+
const node = byId.get(own.node);
|
|
83
|
+
if (!node) return own;
|
|
84
|
+
if (own.port === undefined) {
|
|
85
|
+
if (!options.newPorts) return own;
|
|
86
|
+
let ids = taken.get(node.id);
|
|
87
|
+
if (!ids) taken.set(node.id, (ids = new Set((node.ports ?? []).map((p) => p.id))));
|
|
88
|
+
const id = freeId(ids, `${e.id}:${which}`);
|
|
89
|
+
const list = added.get(node.id) ?? [];
|
|
90
|
+
list.push(fix(node, plug, { id }));
|
|
91
|
+
added.set(node.id, list);
|
|
92
|
+
return { ...own, port: id };
|
|
93
|
+
}
|
|
94
|
+
const port = node.ports?.find((p) => p.id === own.port);
|
|
95
|
+
if (!port || portOffset(node.box, port) !== undefined) return own;
|
|
96
|
+
const m = portsOf(node);
|
|
97
|
+
if (!m.has(port.id)) m.set(port.id, fix(node, plug, port));
|
|
98
|
+
return own;
|
|
99
|
+
};
|
|
100
|
+
const source = end("source", pair.source);
|
|
101
|
+
const target = end("target", pair.target);
|
|
102
|
+
return source === e.source && target === e.target ? e : { ...e, source, target };
|
|
103
|
+
});
|
|
104
|
+
const nodes = graph.nodes.map((n): Node => {
|
|
105
|
+
const m = fixed.get(n.id);
|
|
106
|
+
const more = added.get(n.id);
|
|
107
|
+
if (!m && !more) return n;
|
|
108
|
+
const ports = (n.ports ?? []).map((p) => m?.get(p.id) ?? p);
|
|
109
|
+
return { ...n, ports: more ? [...ports, ...more] : ports };
|
|
110
|
+
});
|
|
111
|
+
return { ...graph, nodes, edges };
|
|
112
|
+
}
|
package/src/plugs/justify.ts
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
* They move the least distance from their ideal places.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { center, type Point } from "../geom.ts";
|
|
7
|
+
import { fixedPortPoint, nodeIndex, type Edge, type Node } from "../model.ts";
|
|
8
|
+
import { placePlug, type Plug } from "../plugs.ts";
|
|
9
|
+
import type { PlugInput, PlugPair } from "../strategies/model.ts";
|
|
10
|
+
|
|
6
11
|
/**
|
|
7
12
|
* This function places `ideal.length` points on the segment from `lo` to `hi` of one side.
|
|
8
13
|
* The points keep the order of their ideal places. Two neighbours are at least `gap` apart, or the side divided evenly when it is too short.
|
|
@@ -46,3 +51,64 @@ export function isotonic(v: readonly number[]): number[] {
|
|
|
46
51
|
});
|
|
47
52
|
return out;
|
|
48
53
|
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* This function moves the free plugs of each side of each node, so that two of them are at least `gap` apart (`justifyPlugs`).
|
|
57
|
+
* 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.
|
|
58
|
+
* A plug stays `inset` from the corners of its side, or a quarter of the side when the side is short.
|
|
59
|
+
*/
|
|
60
|
+
export function spacePlugs(ends: [Edge, Node, Node, Plug, Plug][], gap: number, inset = 6): void {
|
|
61
|
+
// The free plugs of each side: the index of the edge, the end (3 for the source, 4 for the target) and the point of the other end.
|
|
62
|
+
const sides = new Map<string, { node: Node; normal: Point; items: [number, 3 | 4, Point][] }>();
|
|
63
|
+
ends.forEach(([e, s, t, ps, pt], i) => {
|
|
64
|
+
for (const [end, node, plug, other, k] of [
|
|
65
|
+
[e.source, s, ps, pt, 3],
|
|
66
|
+
[e.target, t, pt, ps, 4],
|
|
67
|
+
] as const) {
|
|
68
|
+
if (fixedPortPoint(node, end) !== undefined) continue;
|
|
69
|
+
const key = JSON.stringify([node.id, plug.normal.x, plug.normal.y]);
|
|
70
|
+
let side = sides.get(key);
|
|
71
|
+
if (!side) sides.set(key, (side = { node, normal: plug.normal, items: [] }));
|
|
72
|
+
side.items.push([i, k, other.point]);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
for (const { node, normal, items } of sides.values()) {
|
|
76
|
+
if (items.length < 2) continue;
|
|
77
|
+
const along = normal.x !== 0 ? "y" : "x";
|
|
78
|
+
const b = node.box;
|
|
79
|
+
const start = along === "y" ? b.y : b.x;
|
|
80
|
+
const len = along === "y" ? b.h : b.w;
|
|
81
|
+
const m = Math.min(inset, len / 4);
|
|
82
|
+
const ideal = items.map(([i, k]) => ends[i]![k].point[along]);
|
|
83
|
+
const placed = justifyPlugs(ideal, start + m, start + len - m, gap, (j) => items[j]![2][along]);
|
|
84
|
+
items.forEach(([i, k], j) => {
|
|
85
|
+
const old = ends[i]![k];
|
|
86
|
+
ends[i]![k] = { point: along === "y" ? { x: old.point.x, y: placed[j]! } : { x: placed[j]!, y: old.point.y }, normal: old.normal };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The options of the plug strategy `justify`. */
|
|
92
|
+
export interface JustifyOptions {
|
|
93
|
+
/** The least gap between two free plugs of one side. The default is 8. 0 keeps each plug where it faces. */
|
|
94
|
+
readonly plugGap?: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* This function gives the plugs of the plug strategy `justify` (WILD-002): the plugs of `facing`, then the gaps of `spacePlugs`.
|
|
99
|
+
* The router `corners` with the option `plugGap` gives the same plugs.
|
|
100
|
+
*/
|
|
101
|
+
export function justifyEnds(input: PlugInput, options: JustifyOptions = {}): PlugPair[] {
|
|
102
|
+
const { graph } = input;
|
|
103
|
+
const byId = nodeIndex(graph);
|
|
104
|
+
const ends: [Edge, Node, Node, Plug, Plug][] = [];
|
|
105
|
+
for (const e of graph.edges) {
|
|
106
|
+
const s = byId.get(e.source.node);
|
|
107
|
+
const t = byId.get(e.target.node);
|
|
108
|
+
if (!s || !t) continue;
|
|
109
|
+
ends.push([e, s, t, placePlug(s, e.source, center(t.box)), placePlug(t, e.target, center(s.box))]);
|
|
110
|
+
}
|
|
111
|
+
const gap = options.plugGap ?? 8;
|
|
112
|
+
if (gap > 0) spacePlugs(ends, gap);
|
|
113
|
+
return ends.map(([e, , , source, target]) => ({ edge: e.id, source, target }));
|
|
114
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A made-up board of cards with rows (PLUG-SPREAD, FIX-PORTS): each row of a card links to one row of another card.
|
|
3
|
+
* The tests of the plug rules read it.
|
|
4
|
+
*/
|
|
5
|
+
import type { Edge, Graph, Node, Port } from "../model.ts";
|
|
6
|
+
|
|
7
|
+
/** The options of `rowCardsBoard`. */
|
|
8
|
+
export interface RowCardsOptions {
|
|
9
|
+
/** The number of columns of cards. The default is 3. */
|
|
10
|
+
readonly columns?: number;
|
|
11
|
+
/** The number of cards. The default is 6. */
|
|
12
|
+
readonly cards?: number;
|
|
13
|
+
/** The number of rows of each card. The default is 20. */
|
|
14
|
+
readonly rows?: number;
|
|
15
|
+
/** The height of one row. The default is 12. */
|
|
16
|
+
readonly rowHeight?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* This function gives a board of cards with rows. Each row has a free port `o<row>` for its link out and a free port `i<row>` for a link in.
|
|
21
|
+
* The row `r` of the card `c` links to the card `(c + 1 + r mod 5) mod cards`. Each card takes its links in on its rows in order.
|
|
22
|
+
* With the defaults, the board has 6 cards and 120 edges, and each card is 160 by 240.
|
|
23
|
+
*/
|
|
24
|
+
export function rowCardsBoard(options: RowCardsOptions = {}): Graph {
|
|
25
|
+
const columns = options.columns ?? 3;
|
|
26
|
+
const count = options.cards ?? 6;
|
|
27
|
+
const rows = options.rows ?? 20;
|
|
28
|
+
const rowHeight = options.rowHeight ?? 12;
|
|
29
|
+
const w = 160;
|
|
30
|
+
const h = rows * rowHeight;
|
|
31
|
+
const nodes: Node[] = [];
|
|
32
|
+
for (let c = 0; c < count; c++) {
|
|
33
|
+
const ports: Port[] = [];
|
|
34
|
+
for (let r = 0; r < rows; r++) ports.push({ id: `o${r}` }, { id: `i${r}` });
|
|
35
|
+
nodes.push({ id: `card${c}`, box: { x: (c % columns) * (w + 160), y: Math.floor(c / columns) * (h + 160), w, h }, ports });
|
|
36
|
+
}
|
|
37
|
+
const taken = new Array<number>(count).fill(0);
|
|
38
|
+
const edges: Edge[] = [];
|
|
39
|
+
for (let c = 0; c < count; c++)
|
|
40
|
+
for (let r = 0; r < rows; r++) {
|
|
41
|
+
const t = (c + 1 + (r % Math.min(5, count - 1))) % count;
|
|
42
|
+
const row = taken[t]!++ % rows;
|
|
43
|
+
edges.push({ id: `card${c}.r${r}`, source: { node: `card${c}`, port: `o${r}` }, target: { node: `card${t}`, port: `i${row}` } });
|
|
44
|
+
}
|
|
45
|
+
return { nodes, edges };
|
|
46
|
+
}
|
package/src/plugs/side.ts
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
* Thus the first leg goes straight out to the first corner of the way, and the plug is the foot of that leg.
|
|
4
4
|
*/
|
|
5
5
|
import { center, dist, type Box, type Point } from "../geom.ts";
|
|
6
|
-
import { fixedPortPoint, type End, type Node, type Side } from "../model.ts";
|
|
6
|
+
import { fixedPortPoint, nodeIndex, type End, type Node, type Side } from "../model.ts";
|
|
7
7
|
import { placePlug, pointOnSide, SIDE_NORMAL, sideToward, type Plug } from "../plugs.ts";
|
|
8
|
+
import type { PlugInput, PlugPair } from "../strategies/model.ts";
|
|
8
9
|
import type { Terminal } from "../visibility.ts";
|
|
9
|
-
import
|
|
10
|
+
import { WayFinder } from "../ways.ts";
|
|
10
11
|
|
|
11
12
|
/** The options of `sidePlugs`. */
|
|
12
13
|
export interface SidePlugOptions {
|
|
@@ -136,3 +137,23 @@ export function sidePlugs(ways: WayFinder, s: Node, es: End, t: Node, et: End, o
|
|
|
136
137
|
if (!way) return [ps, pt];
|
|
137
138
|
return [from[way.from]!.plug, to[way.to]!.plug];
|
|
138
139
|
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* This function gives the plugs of the plug strategy `side` (WILD-001): the plugs of `sidePlugs` for each edge.
|
|
143
|
+
* It uses the way finder of the input, or a way finder with the default margin when the input has none.
|
|
144
|
+
* The router `corners` with the option `plugs: "side"` gives the same plugs.
|
|
145
|
+
*/
|
|
146
|
+
export function sideEnds(input: PlugInput, options: SidePlugOptions = {}): PlugPair[] {
|
|
147
|
+
const { graph } = input;
|
|
148
|
+
const byId = nodeIndex(graph);
|
|
149
|
+
const ways = input.ways ?? new WayFinder(graph.nodes);
|
|
150
|
+
const out: PlugPair[] = [];
|
|
151
|
+
for (const e of graph.edges) {
|
|
152
|
+
const s = byId.get(e.source.node);
|
|
153
|
+
const t = byId.get(e.target.node);
|
|
154
|
+
if (!s || !t) continue;
|
|
155
|
+
const [source, target] = sidePlugs(ways, s, e.source, t, e.target, options);
|
|
156
|
+
out.push({ edge: e.id, source, target });
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|