From d53e3260a87a57cdc7652274a9a1739245138534 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Sat, 6 Jul 2019 20:59:27 +0000 Subject: [PATCH] Add LayoutLink, start with BFS for line drawing --- Layout.js | 92 +++++++++--------------------- LayoutLink.js | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++ LayoutNode.js | 7 +++ 3 files changed, 185 insertions(+), 67 deletions(-) create mode 100644 LayoutLink.js diff --git a/Layout.js b/Layout.js index 61e27f3..b1e42ce 100644 --- a/Layout.js +++ b/Layout.js @@ -5,13 +5,14 @@ class Layout { this.nodes_ = []; this.nodesByPos_ = new StringMap(); this.nodesByGraphNode_ = new Map(); - this.lineSteps_ = []; + this.links_ = []; this.setInitialPositions(); + this.resolveLinks(); this.resolveAffinity(); this.resolveGroups(); while (this.iterate()); - this.drawLines(); + this.drawLinks(); this.fixOrigin(); } @@ -61,8 +62,14 @@ class Layout { } } + resolveLinks() { + for (let node of this.nodes_) { + node.resolveLinks(this.nodesByGraphNode_); + } + } + resolveAffinity() { - for (let node of this.nodesByGraphNode_.values()) { + for (let node of this.nodes_) { node.resolveAffinity(this.nodesByGraphNode_); } } @@ -148,9 +155,11 @@ class Layout { node.pos[i] -= min[i]; } } - for (let lineStep of this.lineSteps_) { - for (let i of [0, 1]) { - lineStep.pos[i] -= min[i]; + for (let link of this.links_) { + for (let hop of link.path) { + for (let i of [0, 1]) { + hop[i] -= min[i]; + } } } this.size = [ @@ -159,66 +168,12 @@ class Layout { ]; } - drawLines() { - for (let link of this.graph_.links) { - for (let from of link.from) { - for (let to of link.to) { - this.drawLine( - this.nodesByGraphNode_.get(from), - this.nodesByGraphNode_.get(to)); - } - } - } - } - - // 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], - ]); - - drawLine(from, to) { - let pos = Array.from(from.pos); - let inPoint = null; - while (true) { - let offset = []; - for (let i of [0, 1]) { - offset[i] = Math.sign(to.pos[i] - pos[i]); - } - let outPoint = this.outPointLookup.get(offset); - if (inPoint === null) { - this.lineSteps_.push({ - type: 'line', - pos: Array.from(pos), - cls: 's' + outPoint, - }); - } else { - if (offset[0] == 0 && offset[1] == 0) { - // Reached destination - this.lineSteps_.push({ - type: 'line', - pos: Array.from(pos), - cls: 's' + inPoint, - }); - break; - } else { - this.lineSteps_.push({ - type: 'line', - pos: Array.from(pos), - cls: 'i' + inPoint + 'o' + outPoint, - }); - } - } - // Set values for the next loop - inPoint = (outPoint + 4) % 8; - for (let i of [0, 1]) { - pos[i] += offset[i]; + drawLinks() { + let nodes = Array.from(this.nodes_); + nodes.sort((a, b) => (b.links.length - a.links.length)); + for (let node of nodes) { + for (let to of node.links) { + this.links_.push(new LayoutLink(node, to, this.nodesByPos_)); } } } @@ -244,11 +199,14 @@ class Layout { steps.push(step); } } - steps.push(...this.lineSteps_); + for (let link of this.links_) { + steps.push(...link.getSteps()); + } return steps; } } + diff --git a/LayoutLink.js b/LayoutLink.js new file mode 100644 index 0000000..7dd1cb0 --- /dev/null +++ b/LayoutLink.js @@ -0,0 +1,153 @@ +class LayoutLink { + constructor(from, to, nodesByPos) { + this.from_ = from; + this.to_ = to; + this.nodesByPos_ = nodesByPos; + this.bfs(); + } + + getDirect() { + let cost = 0; + let pos = Array.from(this.from_.pos); + let path = [Array.from(this.from_.pos)]; + + while (pos[0] != this.to_.pos[0] || pos[1] != this.to_.pos[1]) { + cost += 1; + if (this.nodesByPos_.has(pos)) { + cost += 5; + } + for (let i of [0, 1]) { + pos[i] += Math.sign(this.to_.pos[i] - pos[i]); + } + path.push(Array.from(pos)); + } + + return [cost, path]; + } + + bfs() { + // TODO: give more thought to birdirectional search + // TODO: give more thought to minheap instead of queue + // TODO: first hop is free + // TODO: diagonals cost more + // TODO: don't intersect other lines at the same angle + + let cheapestCostByPos = new StringMap(); + + // shortcuts to save the lookup + let direct = this.getDirect(); + let cheapestCostToGoal = direct[0]; + this.path = direct[1]; + + // BFS work queue + let queue = [ + { + path: [Array.from(this.from_.pos)], + cost: 0, + }, + ]; + + while (queue.length) { + let next = queue.shift(); + let pos = next.path[next.path.length - 1]; + + let prev = cheapestCostByPos.get(pos); + if (prev && prev <= next.cost) { + // Reached a previous pos via a higher- or equal-cost path + continue; + } + cheapestCostByPos.set(pos, next); + + if (pos[0] == this.to_.pos[0] && pos[1] == this.to_.pos[1]) { + // Reached the goal + cheapestCostToGoal = next.cost; + this.path = next.path; + continue; + } + + //// Calculate cost for next hop + let newCost = next.cost; + + // Any hop has cost + newCost += 1; + + if (this.nodesByPos_.has(pos)) { + // Traversing nodes has higher cost + newCost += 5; + } + + if (cheapestCostToGoal && newCost >= cheapestCostToGoal) { + // Can't possibly find a cheaper route via this path + continue; + } + + 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 newPath = Array.from(next.path); + newPath.push(newPos); + queue.push({ + cost: newCost, + path: newPath, + }); + } + } + } + } + + // 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], + ]); + + 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; + } +} diff --git a/LayoutNode.js b/LayoutNode.js index 596fa28..4b16dc9 100644 --- a/LayoutNode.js +++ b/LayoutNode.js @@ -7,6 +7,13 @@ class LayoutNode { this.nodesByPos_.set(this.pos, this); } + resolveLinks(nodesByGraphNode) { + this.links = []; + for (let to of this.graphNode_.links) { + this.links.push(nodesByGraphNode.get(to)); + } + } + resolveAffinity(nodesByGraphNode) { this.affinity_ = []; for (let aff of this.graphNode_.affinity) {