From b91b92eef35e7faf7ad97b4c4d93bc1c353ec006 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Wed, 3 Jul 2019 18:13:11 +0000 Subject: [PATCH] Broken, but checkpoint editor/graph/layout split --- Architype.js | 362 +------------------------------- EditorEntryBase.js | 3 - EditorGroup.js | 8 - EditorLink.js | 4 - EditorNode.js | 59 ------ Layout.js | 123 ++++++++++- Collection.js => LayoutGroup.js | 21 +- LayoutNode.js | 67 ++++++ 8 files changed, 197 insertions(+), 450 deletions(-) rename Collection.js => LayoutGroup.js (70%) create mode 100644 LayoutNode.js diff --git a/Architype.js b/Architype.js index a7dc867..5087b68 100644 --- a/Architype.js +++ b/Architype.js @@ -70,10 +70,8 @@ class Architype { onmessage(serialized); localStorage.setItem('currentState', JSON.stringify(serialized)); - this.graph_ = this.buildGraph(); - this.buildGrid(this.graph_); - this.updateTargets(this.graph_); - this.fixSizes(this.graph_.nodes); + //this.updateTargets(this.graph_); + //this.fixSizes(this.graph_.nodes); } onKeyDown(e) { @@ -137,300 +135,6 @@ class Architype { } } - buildGraph() { - let graph = { - nodesByLabel: new Map(), - nodesByPageRank: new Map(), - nodesByPos: new Map(), - nodesBySubgraph: new Map(), - groups: [], - links: [], - nodes: [], - }; - // Order here is important, as each step carefully builds on data - // constructed by the previous - this.buildGraphInt(graph, this.editor_.getEntries()); - this.trimSoftNodes(graph); - this.processLinks(graph); - this.processGroups(graph); - this.manifestNodes(graph); - this.setPageRank(graph); - this.bucketByPageRank(graph); - this.bucketBySubgraph(graph); - this.setInitialPositions(graph); - this.setAffinity(graph); - - while (this.iterate(graph)); - this.fixOrigin(graph); - - return graph; - } - - buildGraphInt(graph, entries) { - for (let entry of entries) { - if (entry instanceof EditorNode) { - this.buildGraphNode(graph, entry); - } else if (entry instanceof EditorGroup) { - this.buildGraphGroup(graph, entry); - } else if (entry instanceof EditorLink) { - this.buildGraphLink(graph, entry); - } - } - } - - buildGraphNode(graph, node) { - node.clear(); - if (node.getLabel() == '') { - return; - } - let targets = graph.nodesByLabel.get(node.getLabel()); - if (!targets) { - targets = []; - graph.nodesByLabel.set(node.getLabel(), targets); - } - targets.push(node); - } - - buildGraphGroup(graph, group) { - group.clear(); - graph.groups.push(group); - this.buildGraphInt(graph, group.getNodes()); - } - - buildGraphLink(graph, link) { - link.clear(); - graph.links.push(link); - this.buildGraphInt(graph, [link.getFrom(), link.getTo()]); - } - - trimSoftNodes(graph) { - for (let entries of graph.nodesByLabel.values()) { - for (let i = entries.length - 1; i >= 0 && entries.length > 1; --i) { - if (entries[i] instanceof EditorNode && entries[i].isSoft()) { - entries.splice(i, 1); - } - } - } - } - - processLinks(graph) { - for (let link of graph.links) { - // Re-resolve each from/to reference by label, so we skip soft nodes and - // handle multiple objects with the same label - link.from = graph.nodesByLabel.get(link.getFrom().getLabel()) || []; - link.to = graph.nodesByLabel.get(link.getTo().getLabel()) || []; - for (let from of link.from) { - for (let to of link.to) { - from.links.push(to); - } - } - } - } - - processGroups(graph) { - for (let group of graph.groups) { - group.nodes = []; - for (let member of group.getNodes()) { - let nodes = graph.nodesByLabel.get(member.getLabel()) || []; - for (let node of nodes) { - group.nodes.push(node); - node.groups.push(group); - } - } - } - } - - manifestNodes(graph) { - for (let entries of graph.nodesByLabel.values()) { - for (let entry of entries) { - if (entry instanceof EditorNode) { - graph.nodes.push(entry); - } - } - } - } - - setPageRank(graph) { - for (let link of graph.links) { - for (let to of link.to) { - this.setPageRankInt(to, new Set()); - } - } - for (let group of graph.groups) { - // All members of a group get the rank of the maximum member, so the - // initial positions will put them all near each other - let maxRank = 0; - for (let member of group.nodes) { - maxRank = Math.max(maxRank, member.pageRank); - } - for (let member of group.nodes) { - member.pageRank = maxRank; - } - } - } - - setPageRankInt(node, visited) { - if (visited.has(node)) { - // Loop detection - return; - } - ++node.pageRank; - visited.add(node); - for (let out of node.links) { - this.setPageRankInt(out, visited); - } - visited.delete(node); - } - - bucketByPageRank(graph) { - for (let node of graph.nodes) { - let bucket = graph.nodesByPageRank.get(node.pageRank); - if (!bucket) { - bucket = []; - graph.nodesByPageRank.set(node.pageRank, bucket); - } - bucket.push(node); - } - let cmp = (a, b) => { - if (a < b) { - return -1; - } else if (a > b) { - return 1; - } else { - return 0; - } - }; - for (let bucket of graph.nodesByPageRank.values()) { - bucket.sort(cmp); - } - } - - bucketBySubgraph(graph) { - let nodes = new Set(); - let ranks = Array.from(graph.nodesByPageRank.keys()); - ranks.sort((a, b) => a - b); - for (let rank of ranks) { - for (let node of graph.nodesByPageRank.get(rank)) { - nodes.add(node); - } - } - for (let subgraph = 0; nodes.size; ++subgraph) { - let node = nodes.values().next().value; - let subgraphArr = []; - graph.nodesBySubgraph.set(subgraph, subgraphArr); - this.recurseSubgraph(subgraph, subgraphArr, node, nodes); - } - } - - recurseSubgraph(subgraph, subgraphArr, node, nodes) { - if (node.subgraph !== null) { - return; - } - node.subgraph = subgraph; - subgraphArr.push(node); - nodes.delete(node); - for (let to of node.links) { - this.recurseSubgraph(subgraph, subgraphArr, to, nodes); - } - for (let group of node.groups) { - for (let member of group.nodes) { - this.recurseSubgraph(subgraph, subgraphArr, member, nodes); - } - } - } - - setInitialPositions(graph) { - const SPACING = 4; - - let maxRankNodes = 0; - for (let nodes of graph.nodesByPageRank.values()) { - maxRankNodes = Math.max(maxRankNodes, nodes.length); - } - - let ranks = Array.from(graph.nodesByPageRank.keys()); - ranks.sort((a, b) => a - b); - for (let r = 0; r < ranks.length; ++r) { - let nodes = graph.nodesByPageRank.get(ranks[r]); - for (let n = 0; n < nodes.length; ++n) { - let node = nodes[n]; - let pos = [ - r * SPACING, - Math.floor((nodes.length / 2) * SPACING) + (n * SPACING) + - (node.subgraph * SPACING * maxRankNodes), - ]; - node.moveTo(graph, pos); - } - } - } - - fixOrigin(graph) { - let min = [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]; - let max = [Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER]; - for (let node of graph.nodes) { - for (let i of [0, 1]) { - min[i] = Math.min(min[i], node.pos[i]); - max[i] = Math.max(max[i], node.pos[i]); - } - } - // Offset is negative minimum, e.g min -1 means +1 to all values - for (let node of graph.nodes) { - for (let i of [0, 1]) { - node.pos[i] -= min[i]; - } - } - graph.size = [ - max[0] - min[0] + 1, - max[1] - min[1] + 1, - ]; - } - - setAffinity(graph) { - const INF = 999999; - for (let node of graph.nodes) { - for (let other of graph.nodes) { - // Weak affinity full mesh - // Keep unassociated subgroups together - this.addAffinity(node, other, d => d); - - if (node.subgraph != other.subgraph) { - this.addAffinity(node, other, d => d < 1.5 ? -INF : 0); - } - } - for (let to of node.links) { - // Stronger affinity for links - // Prefer to move toward the target instance - this.addAffinity(node, to, d => d < 1.5 ? -INF : d * 11); - this.addAffinity(to, node, d => d < 1.5 ? -INF : d * 9); - } - for (let group of node.groups) { - for (let member of group.nodes) { - // Even stronger affinity for groups - // Other nodes will reference this one and take care of the full - // group mesh - this.addAffinity(node, member, d => d * 100); - } - let members = new Set(group.nodes); - for (let other of graph.nodes) { - if (members.has(other)) { - continue; - } - // Nodes not in this group run away - this.addAffinity(other, node, d => d < 1.5 ? -INF : 0); - } - } - } - } - - addAffinity(node, other, func) { - if (node == other) { - return; - } - node.affinity.push({ - node: other, - distanceToWeight: func, - }); - } - buildGrid(graph) { this.grid_.innerHTML = ''; @@ -453,67 +157,6 @@ class Architype { return lines; } - iterate(graph) { - let nodes = Array.from(graph.nodes); - this.sortByMostTension(nodes); - for (let group of graph.groups) { - nodes.push(group.getCollection()); - } - for (let subgraph of graph.nodesBySubgraph.values()) { - nodes.push(new Collection(subgraph)); - } - - let newOffset = null; - let newTension = this.getTotalTension(nodes); - for (let node of nodes) { - let origPos = node.pos; - let offsets = new Map(); - let addOffset = (x, y) => { - if (!offsets.has([x, y].toString())) { - offsets.set([x, y].toString(), [x, y]); - } - }; - for (let dir of [-1, 0, 1]) { - addOffset(Math.sign(node.vec[0]), dir); - addOffset(dir, Math.sign(node.vec[1])); - } - for (let offset of offsets.values()) { - if (node.offsetCollides(graph, offset)) { - continue; - } - node.savePos(); - node.moveBy(graph, offset); - let testTension = this.getTotalTension(nodes); - node.restorePos(graph); - if (testTension < newTension) { - newOffset = offset; - newTension = testTension; - } - } - if (newOffset) { - node.moveBy(graph, newOffset); - return true; - } - } - return false; - } - - getTotalTension(nodes) { - let total = 0; - for (let node of nodes) { - node.setTension(); - total += node.tension; - } - return total; - } - - sortByMostTension(nodes) { - for (let node of nodes) { - node.setTension(); - } - nodes.sort((a, b) => b.tension - a.tension); - } - drawGridNodes(graph) { for (let node of graph.nodes) { node.gridElem = document.createElement('div'); @@ -539,7 +182,6 @@ class Architype { } } - diff --git a/EditorEntryBase.js b/EditorEntryBase.js index 7cab616..69f805e 100644 --- a/EditorEntryBase.js +++ b/EditorEntryBase.js @@ -31,9 +31,6 @@ class EditorEntryBase extends ListenUtils { this.elem_.xArchObj = null; } - clear() { - } - wantFocus() { return false; } diff --git a/EditorGroup.js b/EditorGroup.js index f77ef5e..43eac49 100644 --- a/EditorGroup.js +++ b/EditorGroup.js @@ -51,10 +51,6 @@ class EditorGroup extends EditorEntryBase { return lines; } - clear() { - super.clear(); - } - getNodes() { return this.nodes_.getEntries(); } @@ -72,10 +68,6 @@ class EditorGroup extends EditorEntryBase { return this.elem_; } - getCollection() { - return new Collection(this.nodes); - } - onInputKeyDown(e) { switch (e.key) { case 'Enter': diff --git a/EditorLink.js b/EditorLink.js index 3cc09ec..09c4aa1 100644 --- a/EditorLink.js +++ b/EditorLink.js @@ -55,10 +55,6 @@ class EditorLink extends EditorEntryBase { return ret; }; - clear() { - super.clear(); - } - getFrom() { return this.nodes_.getEntries()[0]; } diff --git a/EditorNode.js b/EditorNode.js index caf37c5..7913fa0 100644 --- a/EditorNode.js +++ b/EditorNode.js @@ -33,15 +33,6 @@ class EditorNode extends EditorEntryBase { return ['"' + this.id + '" [label="' + this.getLabel() + '"];']; } - clear() { - super.clear(); - this.links = []; - this.groups = []; - this.affinity = []; - this.pageRank = 0; - this.subgraph = null; - } - getLabel() { return this.input_.value; } @@ -70,56 +61,6 @@ class EditorNode extends EditorEntryBase { return false; } - setTension() { - this.vec = [0, 0]; - this.tension = 0; - for (let aff of this.affinity) { - let vec = [], vecsum = 0; - for (let i of [0, 1]) { - vec[i] = aff.node.pos[i] - this.pos[i]; - vecsum += Math.abs(vec[i]); - }; - let distance = Math.sqrt(Math.pow(vec[0], 2) + Math.pow(vec[1], 2)); - let weight = aff.distanceToWeight(distance); - for (let i of [0, 1]) { - this.vec[i] += (weight * vec[i]) / vecsum; - } - this.tension += Math.abs(weight); - } - } - - offsetToPos(offset) { - return [ - this.pos[0] + offset[0], - this.pos[1] + offset[1], - ]; - } - - offsetCollides(graph, offset) { - let newPos = this.offsetToPos(offset); - return graph.nodesByPos.get(newPos.toString()); - } - - moveBy(graph, offset) { - this.moveTo(graph, this.offsetToPos(offset)); - } - - moveTo(graph, pos) { - if (this.pos) { - graph.nodesByPos.delete(this.pos.toString()); - } - this.pos = pos; - graph.nodesByPos.set(this.pos.toString(), this); - } - - savePos() { - this.savedPos = this.pos; - } - - restorePos(graph) { - this.moveTo(graph, this.savedPos); - } - onInput() { this.input_.setAttribute('data-arch-value', this.input_.value); } diff --git a/Layout.js b/Layout.js index f8b17ae..c13507e 100644 --- a/Layout.js +++ b/Layout.js @@ -1,10 +1,15 @@ class Layout { constructor(graph) { - this.nodesByPos = new Map(); - this.graph_ = graph; + this.nodesByPos_ = new Map(); + this.nodesByGraphNode_ = new Map(); + this.setInitialPositions(); + this.resolveAffinity(); + this.resolveGroups(); + while (this.iterate()); + this.fixOrigin(); } setInitialPositions() { @@ -26,13 +31,119 @@ class Layout { Math.floor((nodes.length / 2) * SPACING) + (n * SPACING) + (node.subgraph * SPACING * maxRankNodes), ]; - node.pos = pos; - this.setNodePos(node, pos); + this.nodesByGraphNode_.set( + node, + new LayoutNode(node, this.nodesByPos_, pos)); } } } - setNodePos(node, pos) { - this.nodesByPos.set(pos.toString(), node); + nodesFromGraphNodes(graphNodes) { + let nodes = []; + for (let graphNode of graphNodes) { + nodes.push(this.nodesByGraphNode_.get(graphNode)); + } + return nodes; + } + + resolveGroups() { + this.groups_ = []; + for (let group of this.graph_.groups) { + let nodes = this.nodesFromGraphNodes(group.nodes); + this.groups_.push(new LayoutGroup(nodes)); + } + for (let subgraph of this.graph_.nodesBySubgraph.values()) { + let nodes = this.nodesFromGraphNodes(subgraph); + this.groups_.push(new LayoutGroup(nodes)); + } + } + + resolveAffinity() { + for (let node of this.nodesByGraphNode_.values()) { + node.resolveAffinity(this.nodesByGraphNode_); + } + } + + iterate() { + let objects = Array.from(this.nodesByPos_.values()); + objects.push(...this.groups_); + this.setTension(objects); + this.sortByMostTension(objects); + + let newOffset = null; + let newTension = this.getTotalTension(objects); + for (let obj of objects) { + let offsets = new Map(); + let addOffset = (x, y) => { + if (x == 0 && y == 0) { + return; + } + offsets.set([x, y].toString(), [x, y]); + }; + for (let dir of [-1, 0, 1]) { + addOffset(Math.sign(obj.vec[0]), dir); + addOffset(dir, Math.sign(obj.vec[1])); + } + for (let offset of offsets.values()) { + if (obj.offsetCollides(offset)) { + continue; + } + obj.savePos(); + obj.moveBy(offset); + let testTension = this.getTotalTension(objects); + obj.restorePos(); + if (testTension < newTension) { + newOffset = offset; + newTension = testTension; + } + } + if (newOffset) { + obj.moveBy(newOffset); + return true; + } + } + return false; + } + + setTension(objects) { + for (let obj of objects) { + obj.setTension(); + } + } + + sortByMostTension(objects) { + objects.sort((a, b) => b.tension - a.tension); + } + + getTotalTension(objects) { + let total = 0; + for (let obj of objects) { + total += obj.tension; + } + return total; + } + + fixOrigin() { + let min = [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]; + let max = [Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER]; + for (let node of this.nodesByPos_.values()) { + for (let i of [0, 1]) { + min[i] = Math.min(min[i], node.pos[i]); + max[i] = Math.max(max[i], node.pos[i]); + } + } + // Offset is negative minimum, e.g min -1 means +1 to all values + for (let node of this.nodesByPos_.values()) { + for (let i of [0, 1]) { + node.pos[i] -= min[i]; + } + } + this.size = [ + max[0] - min[0] + 1, + max[1] - min[1] + 1, + ]; } } + + + diff --git a/Collection.js b/LayoutGroup.js similarity index 70% rename from Collection.js rename to LayoutGroup.js index cf8856e..e953413 100644 --- a/Collection.js +++ b/LayoutGroup.js @@ -1,23 +1,24 @@ -class Collection { +class LayoutGroup { constructor(nodes) { this.nodes = new Set(nodes); + this.tension = 0; } setTension() { + // Groups don't track tension, since we always want to sort last for total + // tension this.vec = [0, 0]; - this.tension = 0; for (let node of this.nodes.values()) { node.setTension(); for (let i of [0, 1]) { this.vec[i] += node.vec[i]; }; - this.tension += node.tension; } } - offsetCollides(graph, offset) { + offsetCollides(offset) { for (let node of this.nodes.values()) { - let other = node.offsetCollides(graph, offset); + let other = node.offsetCollides(offset); if (other && !this.nodes.has(other)) { return other; } @@ -31,20 +32,20 @@ class Collection { } } - restorePos(graph) { + restorePos() { for (let node of this.nodes.values()) { - node.restorePos(graph); + node.restorePos(); } } - moveBy(graph, offset) { + moveBy(offset) { let nodes = new Set(this.nodes.values()); while (nodes.size) { for (let node of nodes) { - if (node.offsetCollides(graph, offset)) { + if (node.offsetCollides(offset)) { continue; } - node.moveBy(graph, offset); + node.moveBy(offset); nodes.delete(node); } } diff --git a/LayoutNode.js b/LayoutNode.js new file mode 100644 index 0000000..fb90d09 --- /dev/null +++ b/LayoutNode.js @@ -0,0 +1,67 @@ +class LayoutNode { + constructor(graphNode, nodesByPos, pos) { + this.graphNode_ = graphNode; + this.nodesByPos_ = nodesByPos; + this.pos = pos; + + this.nodesByPos_.set(this.pos.toString(), this); + } + + resolveAffinity(nodesByGraphNode) { + this.affinity_ = []; + for (let aff of this.graphNode_.affinity) { + this.affinity_.push({ + node: nodesByGraphNode.get(aff.node), + distanceToWeight: aff.distanceToWeight, + }); + } + } + + setTension() { + this.vec = [0, 0]; + this.tension = 0; + for (let aff of this.affinity_) { + let vec = [], vecsum = 0; + for (let i of [0, 1]) { + vec[i] = aff.node.pos[i] - this.pos[i]; + vecsum += Math.abs(vec[i]); + }; + let distance = Math.sqrt(Math.pow(vec[0], 2) + Math.pow(vec[1], 2)); + let weight = aff.distanceToWeight(distance); + for (let i of [0, 1]) { + this.vec[i] += (weight * vec[i]) / vecsum; + } + this.tension += Math.abs(weight); + } + } + + offsetToPos(offset) { + return [ + this.pos[0] + offset[0], + this.pos[1] + offset[1], + ]; + } + + offsetCollides(offset) { + let newPos = this.offsetToPos(offset); + return this.nodesByPos_.get(newPos.toString()); + } + + moveTo(pos) { + this.nodesByPos_.delete(this.pos.toString()); + this.pos = pos; + this.nodesByPos_.set(this.pos.toString(), this); + } + + moveBy(offset) { + this.moveTo(this.offsetToPos(offset)); + } + + savePos() { + this.savedPos_ = this.pos; + } + + restorePos() { + this.moveTo(this.savedPos_); + } +}