From dd582c4d20f39afc42dcd5bcf6fe0a20058802f5 Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Mon, 3 Jul 2023 19:30:04 -0500 Subject: [PATCH] Refinements to basic graph editing --- .../src/classes/display/flowchart.js | 6 +- forum-network/src/classes/display/mermaid.js | 4 ++ forum-network/src/classes/display/scene.js | 3 +- forum-network/src/classes/supporting/edge.js | 18 +++-- .../src/classes/supporting/vertex.js | 4 ++ forum-network/src/classes/supporting/wdg.js | 70 ++++++++++++++++++- 6 files changed, 92 insertions(+), 13 deletions(-) diff --git a/forum-network/src/classes/display/flowchart.js b/forum-network/src/classes/display/flowchart.js index 120b5c3..058e928 100644 --- a/forum-network/src/classes/display/flowchart.js +++ b/forum-network/src/classes/display/flowchart.js @@ -3,7 +3,11 @@ import { MermaidDiagram } from './mermaid.js'; export class Flowchart extends MermaidDiagram { constructor(box, logBox, direction = 'BT') { super(box, logBox); + this.direction = direction; + this.init(); + } - this.log(`graph ${direction}`, false); + init() { + this.log(`graph ${this.direction}`, false); } } diff --git a/forum-network/src/classes/display/mermaid.js b/forum-network/src/classes/display/mermaid.js index 201bba1..a11a588 100644 --- a/forum-network/src/classes/display/mermaid.js +++ b/forum-network/src/classes/display/mermaid.js @@ -69,4 +69,8 @@ export class MermaidDiagram { } }, 100); } + + reset() { + this.logBoxPre.textContent = ''; + } } diff --git a/forum-network/src/classes/display/scene.js b/forum-network/src/classes/display/scene.js index e6a0130..cc40e96 100644 --- a/forum-network/src/classes/display/scene.js +++ b/forum-network/src/classes/display/scene.js @@ -61,8 +61,7 @@ export class Scene { const container = this.middleSection.addBox(name).flex(); const box = container.addBox('Flowchart').addClass('padded'); const logBox = container.addBox('Flowchart text').addClass('dim'); - const flowchart = new MermaidDiagram(box, logBox); - flowchart.log(`graph ${direction}`, false); + const flowchart = new Flowchart(box, logBox, direction); this.flowcharts.set(id, flowchart); return this; } diff --git a/forum-network/src/classes/supporting/edge.js b/forum-network/src/classes/supporting/edge.js index ba82aa2..bc15b81 100644 --- a/forum-network/src/classes/supporting/edge.js +++ b/forum-network/src/classes/supporting/edge.js @@ -7,6 +7,11 @@ export class Edge { this.weight = weight; this.data = data; this.options = options; + this.installedClickCallback = false; + } + + reset() { + this.installedClickCallback = false; } static getKey({ @@ -49,8 +54,9 @@ export class Edge { } this.graph.flowchart?.log(`${this.from.id} --- ${this.getFlowchartNode()} --> ${this.to.id}`); this.graph.flowchart?.log(`class ${Edge.getCombinedKey(this)} edge`); - if (this.graph.editable) { + if (this.graph.editable && !this.installedClickCallback) { this.graph.flowchart?.log(`click ${Edge.getCombinedKey(this)} WDGHandler${this.graph.index} "Edit Edge"`); + this.installedClickCallback = true; } } @@ -100,19 +106,17 @@ export class Edge { name: 'Save', cb: ({ form: { value } }, { initializing }) => { if (initializing) return; - console.log('graph before save', { ...graph }); // Handle additions and updates for (const { type, weight } of value.edges) { graph.setEdgeWeight(type, from, to, weight); } // Handle removals - for (const edgeType of graph.edgeTypes.values()) { - if (!value.edges.find((edge) => edge.type === edgeType)) { - graph.deleteEdge(edgeType, from, to); + for (const edge of graph.getEdges(null, from, to)) { + if (!value.edges.find(({ type }) => type === edge.type)) { + graph.deleteEdge(edge.type, from, to); } } - console.log('graph after save', { ...graph }); - // TODO: Redraw graph + graph.redraw(); }, }); } diff --git a/forum-network/src/classes/supporting/vertex.js b/forum-network/src/classes/supporting/vertex.js index 0b86e9b..160126c 100644 --- a/forum-network/src/classes/supporting/vertex.js +++ b/forum-network/src/classes/supporting/vertex.js @@ -12,6 +12,10 @@ export class Vertex { this.installedClickCallback = false; } + reset() { + this.installedClickCallback = false; + } + set id(newId) { this._id = newId; } diff --git a/forum-network/src/classes/supporting/wdg.js b/forum-network/src/classes/supporting/wdg.js index fe0984b..41f2ad6 100644 --- a/forum-network/src/classes/supporting/wdg.js +++ b/forum-network/src/classes/supporting/wdg.js @@ -3,7 +3,8 @@ import { Edge } from './edge.js'; const graphs = []; -const makeWDGHandler = (graph) => (vertexId) => { +const makeWDGHandler = (graphIndex) => (vertexId) => { + const graph = graphs[graphIndex]; // We want a document for editing this node, which may be a vertex or an edge const editorDoc = graph.scene.getDocument('editorDocument') ?? graph.scene.withDocument('editorDocument').lastDocument; @@ -28,7 +29,62 @@ export class WeightedDirectedGraph { // Mermaid supports a click callback, but we can't customize arguments; we just get the vertex ID. // In order to provide the appropriate graph context for each callback, we create a separate callback // function for each graph. - window[`WDGHandler${this.index}`] = makeWDGHandler(this); + window[`WDGHandler${this.index}`] = makeWDGHandler(this.index); + this.history = {}; + } + + getHistory() { + // record operations that modify the graph + return this.history; + } + + toJSON() { + return { + vertices: Array.from(this.vertices.values()), + edgeTypes: Array.from(this.edgeTypes.keys()), + edges: Array.from(this.edgeTypes.values()).flatMap((edges) => Array.from(edges.values())), + history: this.getHistory(), + }; + } + + redraw() { + // Call .reset() on all vertices and edges + for (const vertex of this.vertices.values()) { + vertex.reset(); + } + for (const edges of this.edgeTypes.values()) { + for (const edge of edges.values()) { + edge.reset(); + } + } + + // Clear the target div + this.flowchart?.reset(); + this.flowchart?.init(); + + // Draw all vertices and edges + for (const vertex of this.vertices.values()) { + vertex.displayVertex(); + } + // Let's flatmap and dedupe by [from, to] since each edge + // renders all comorphic edges as well. + const edgesFrom = new Map(); // edgeMap[from][to] = edge + for (const edges of this.edgeTypes.values()) { + for (const edge of edges.values()) { + const edgesTo = edgesFrom.get(edge.from) || new Map(); + edgesTo.set(edge.to, edge); + edgesFrom.set(edge.from, edgesTo); + } + } + + for (const edgesTo of edgesFrom.values()) { + for (const edge of edgesTo.values()) { + edge.displayEdge(); + } + } + + // Rerender + this.flowchart?.render(); } withFlowchart() { @@ -88,6 +144,9 @@ export class WeightedDirectedGraph { to.edges.from.forEach((x, i) => (x === edge) && to.edges.from.splice(i, 1)); from.edges.to.forEach((x, i) => (x === edge) && from.edges.to.splice(i, 1)); edges.delete(edgeKey); + if (edges.size === 0) { + this.edgeTypes.delete(type); + } } getEdgeWeight(type, from, to) { @@ -112,13 +171,18 @@ export class WeightedDirectedGraph { addEdge(type, from, to, weight, data, options) { from = from instanceof Vertex ? from : this.getVertex(from); to = to instanceof Vertex ? to : this.getVertex(to); + const existingEdges = this.getEdges(type, from, to); if (this.getEdge(type, from, to)) { throw new Error(`Edge ${type} from ${from.id} to ${to.id} already exists`); } const edge = this.setEdgeWeight(type, from, to, weight, data, options); from.edges.from.push(edge); to.edges.to.push(edge); - edge.displayEdge(); + if (existingEdges.length) { + edge.displayEdgeNode(); + } else { + edge.displayEdge(); + } return edge; }