Files
architype/LayoutLink.js

170 lines
4.2 KiB
JavaScript
Raw Normal View History

class LayoutLink {
constructor(from, to, nodesByPos, linksByPos) {
this.from_ = from;
this.to_ = to;
this.nodesByPos_ = nodesByPos;
this.linksByPos_ = linksByPos;
this.bfs();
}
bfs() {
let bestByPos = new StringMap();
2019-07-07 22:30:49 +00:00
// shortcut to save the lookup
let cheapestCostToGoal = null;
// BFS work queue
2019-07-07 22:28:38 +00:00
let queue = new MinHeap((a) => a.cost);
queue.push(...[
{
path: [Array.from(this.from_.pos)],
cost: 0,
source: 1,
},
{
path: [Array.from(this.to_.pos)],
cost: 0,
source: 2,
},
]);
2019-07-07 22:28:38 +00:00
for (let next = queue.pop(); next; next = queue.pop()) {
let pos = next.path[next.path.length - 1];
let best = bestByPos.get(pos);
if (best) {
if (best.source != next.source) {
// Goal reached; encountered a path from the other source
best.path.reverse();
next.path.splice(next.path.length - 1, 1);
this.path = next.path.concat(best.path);
break;
}
if (best.cost <= next.cost) {
// Reached a previous pos via a higher- or equal-cost path
continue;
}
}
bestByPos.set(pos, next);
for (let xOff of [-1, 0, 1]) {
for (let yOff of [-1, 0, 1]) {
if (xOff == 0 && yOff == 0) {
continue;
}
let newPos = [pos[0] + xOff, pos[1] + yOff];
let path = Array.from(next.path);
path.push(newPos);
queue.push({
path: path,
cost: next.cost + this.getCost(pos, newPos),
source: next.source,
});
}
}
}
for (let hop of this.path) {
getOrSet(this.linksByPos_, hop, new Set()).add(this);
}
}
// Mapping to lines.svg clock-style numbering
outPointLookup = new StringMap([
[[ 0,-1], 0],
[[ 1,-1], 1],
[[ 1, 0], 2],
[[ 1, 1], 3],
[[ 0, 1], 4],
[[-1, 1], 5],
[[-1, 0], 6],
[[-1,-1], 7],
]);
getCost(from, to) {
// Only handles adjacent positions
let cost = 1;
// Because we're doing bidirectional search, this function must always be
// symmetric, i.e. returning the same value regardless of order of
// arguments. That means that any costs applied to nodes must be applied
// whether the node is from or to. Traversal is double-charged.
for (let pos of [from, to]) {
if (this.nodesByPos_.has(pos)) {
// Traversing nodes has higher cost
cost += 5;
} else if (this.linksByPos_.has(pos)) {
// Overlapping links have cost
cost += 2 * this.linksByPos_.get(pos).size;
}
}
let offset = [
to[0] - from[0],
to[1] - from[1],
];
if (offset[0] != 0 && offset[1] != 0) {
// Diagonal; approximate sqrt(2) from distance formula
cost += 0.5;
// The two other positions in a quad covering this diagonal
let others = [
[from[0] + Math.sign(offset[0]), from[1]],
[from[0], from[1] + Math.sign(offset[1])],
];
if (intersects(
this.linksByPos_.get(others[0]) || new Set(),
this.linksByPos_.get(others[1]) || new Set())) {
// We cross another link at a right angle
cost += 2;
}
}
return cost;
}
2019-07-08 02:24:23 +00:00
// TODO: split layout and rendering
getOutPoint(from, to) {
let offset = [
to[0] - from[0],
to[1] - from[1],
];
return this.outPointLookup.get(offset);
}
getInPoint(from, to) {
return (this.getOutPoint(from, to) + 4) % 8;
}
getSteps() {
let steps = [];
steps.push({
type: 'line',
pos: Array.from(this.path[0]),
cls: 's' + this.getOutPoint(this.path[0], this.path[1]),
});
for (let i = 1; i < this.path.length - 1; ++i) {
let inPoint = this.getInPoint(this.path[i - 1], this.path[i]);
let outPoint = this.getOutPoint(this.path[i], this.path[i + 1]);
steps.push({
type: 'line',
pos: Array.from(this.path[i]),
cls: `i${inPoint}o${outPoint}`,
});
}
steps.push({
type: 'line',
pos: Array.from(this.path[this.path.length - 1]),
cls: 's' + this.getInPoint(
this.path[this.path.length - 2], this.path[this.path.length - 1]),
});
return steps;
}
}