Further WIP
This commit is contained in:
parent
435633a893
commit
ec3401845d
|
@ -1,4 +1,3 @@
|
||||||
import { WeightedDirectedGraph } from '../supporting/wdg.js';
|
|
||||||
import { Action } from '../display/action.js';
|
import { Action } from '../display/action.js';
|
||||||
import { Actor } from '../display/actor.js';
|
import { Actor } from '../display/actor.js';
|
||||||
import { ReputationHolder } from '../reputation/reputation-holder.js';
|
import { ReputationHolder } from '../reputation/reputation-holder.js';
|
||||||
|
@ -6,6 +5,7 @@ import { displayNumber } from '../../util/helpers.js';
|
||||||
import {
|
import {
|
||||||
EPSILON, INCINERATOR_ADDRESS, EdgeTypes, VertexTypes,
|
EPSILON, INCINERATOR_ADDRESS, EdgeTypes, VertexTypes,
|
||||||
} from '../../util/constants.js';
|
} from '../../util/constants.js';
|
||||||
|
import { WDGDiagram } from '../display/wdg-mermaid-ui.js';
|
||||||
|
|
||||||
class Post extends Actor {
|
class Post extends Actor {
|
||||||
constructor(forum, senderId, postContent) {
|
constructor(forum, senderId, postContent) {
|
||||||
|
@ -44,7 +44,7 @@ export class Forum extends ReputationHolder {
|
||||||
super(name, scene);
|
super(name, scene);
|
||||||
this.dao = dao;
|
this.dao = dao;
|
||||||
this.id = this.reputationPublicKey;
|
this.id = this.reputationPublicKey;
|
||||||
this.graph = new WeightedDirectedGraph('forum', scene);
|
this.graph = new WDGDiagram('forum', scene);
|
||||||
this.actions = {
|
this.actions = {
|
||||||
propagate: new Action('propagate', scene),
|
propagate: new Action('propagate', scene),
|
||||||
confirm: new Action('confirm', scene),
|
confirm: new Action('confirm', scene),
|
||||||
|
@ -88,10 +88,10 @@ export class Forum extends ReputationHolder {
|
||||||
console.log('onValidate', { pool, postId, tokenAddress });
|
console.log('onValidate', { pool, postId, tokenAddress });
|
||||||
|
|
||||||
// What we have here now is an ERC-1155 rep token, which can contain multiple reputation types.
|
// What we have here now is an ERC-1155 rep token, which can contain multiple reputation types.
|
||||||
// ERC-1155 supports a batch transfer operation, so it makes sense to leverage that.
|
// TODO: ERC-1155 supports a batch transfer operation, so it probably makes sense to leverage that.
|
||||||
|
|
||||||
const initialValues = pool.reputationTypeIds
|
const initialValues = new Map(pool.reputationTypeIds
|
||||||
.map((tokenTypeId) => this.dao.reputation.valueOf(tokenAddress, tokenTypeId));
|
.map((tokenTypeId) => [tokenTypeId, this.dao.reputation.valueOf(tokenAddress, tokenTypeId)]));
|
||||||
const postVertex = this.graph.getVertex(postId);
|
const postVertex = this.graph.getVertex(postId);
|
||||||
const post = postVertex.data;
|
const post = postVertex.data;
|
||||||
post.setStatus('Validated');
|
post.setStatus('Validated');
|
||||||
|
@ -193,7 +193,8 @@ export class Forum extends ReputationHolder {
|
||||||
}) {
|
}) {
|
||||||
const postVertex = edge.to;
|
const postVertex = edge.to;
|
||||||
const post = postVertex.data;
|
const post = postVertex.data;
|
||||||
const incrementsStr = `(${increments.join(')(')})`;
|
const incrementsStrArray = Array.from(increments.entries()).map(([k, v]) => `${k} ${v}`);
|
||||||
|
const incrementsStr = `(${incrementsStrArray.join(')(')})`;
|
||||||
this.actions.propagate.log(edge.from.data, post, incrementsStr);
|
this.actions.propagate.log(edge.from.data, post, incrementsStr);
|
||||||
|
|
||||||
if (!!referenceChainLimit && depth > referenceChainLimit) {
|
if (!!referenceChainLimit && depth > referenceChainLimit) {
|
||||||
|
@ -211,7 +212,7 @@ export class Forum extends ReputationHolder {
|
||||||
from: edge.from.id ?? edge.from,
|
from: edge.from.id ?? edge.from,
|
||||||
to: edge.to.id,
|
to: edge.to.id,
|
||||||
depth,
|
depth,
|
||||||
value: post.value,
|
values: post.values,
|
||||||
increments,
|
increments,
|
||||||
initialNegative,
|
initialNegative,
|
||||||
});
|
});
|
||||||
|
@ -303,15 +304,15 @@ export class Forum extends ReputationHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now propagate value via positive citations
|
// Now propagate value via positive citations
|
||||||
const totalDonationAmount = await propagate(true);
|
const totalDonationAmounts = await propagate(true);
|
||||||
for (let idx = 0; idx < totalDonationAmounts.length; idx++) {
|
for (let idx = 0; idx < totalDonationAmounts.length; idx++) {
|
||||||
increments[idx] -= totalDonationAmounts[idx] * leachingValue;
|
increments[idx] -= totalDonationAmounts[idx] * leachingValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the remaining increment to the present post
|
// Apply the remaining increment to the present post
|
||||||
const rawNewValues = post.value + increment;
|
const rawNewValues = post.values.map((value, idx) => value + increments[idx]);
|
||||||
const newValue = Math.max(0, rawNewValue);
|
const newValues = rawNewValues.map((rawNewValue) => Math.max(0, rawNewValue));
|
||||||
const appliedIncrement = newValue - post.value;
|
const appliedIncrements = newValue - post.value;
|
||||||
const refundToInbound = increment - appliedIncrement;
|
const refundToInbound = increment - appliedIncrement;
|
||||||
|
|
||||||
// Apply reputation effects to post authors, not to the post directly
|
// Apply reputation effects to post authors, not to the post directly
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { DisplayValue } from './display-value.js';
|
import { DisplayValue } from './display-value.js';
|
||||||
import { randomID } from '../../util/helpers.js';
|
import { randomID } from '../../util/helpers.js';
|
||||||
import { Rectangle } from './geometry.js';
|
import { Rectangle, Vector } from './geometry.js';
|
||||||
|
|
||||||
export class Box {
|
export class Box {
|
||||||
constructor(name, parentEl, options = {}) {
|
constructor(name, parentEl, options = {}) {
|
||||||
|
@ -21,7 +21,7 @@ export class Box {
|
||||||
parentEl.appendChild(this.el);
|
parentEl.appendChild(this.el);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.boxes = [];
|
this.position = options.position ?? Vector.zeros(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
flex({ center = false } = {}) {
|
flex({ center = false } = {}) {
|
||||||
|
@ -44,7 +44,6 @@ export class Box {
|
||||||
|
|
||||||
addBox(name) {
|
addBox(name) {
|
||||||
const box = new Box(name, this.el);
|
const box = new Box(name, this.el);
|
||||||
this.boxes.push(box);
|
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,13 +63,15 @@ export class Box {
|
||||||
|
|
||||||
getGeometry() {
|
getGeometry() {
|
||||||
const {
|
const {
|
||||||
x, y, width, height,
|
width, height,
|
||||||
} = this.el.getBoundingClientRect();
|
} = this.el.getBoundingClientRect();
|
||||||
return new Rectangle([x, y], [width, height]);
|
return new Rectangle(this.position, [width, height]);
|
||||||
}
|
}
|
||||||
|
|
||||||
move(vector) {
|
move(vector) {
|
||||||
this.el.style.left = `${parseInt(this.el.style.left, 10) + vector[0]}px`;
|
this.position = this.position.add(vector);
|
||||||
this.el.style.top = `${parseInt(this.el.style.top, 10) + vector[1]}px`;
|
this.el.style.left = `${Math.floor(this.position[0])}px`;
|
||||||
|
this.el.style.top = `${Math.floor(this.position[1])}px`;
|
||||||
|
// this.el.dispatchEvent(new CustomEvent('moved'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
import {
|
import {
|
||||||
DEFAULT_TARGET_RADIUS, DISTANCE_FACTOR, MINIMUM_FORCE, OVERLAP_FORCE, VISCOSITY_FACTOR,
|
DEFAULT_OVERLAP_FORCE,
|
||||||
|
DEFAULT_TARGET_RADIUS,
|
||||||
|
DEFAULT_TIME_STEP,
|
||||||
|
DISTANCE_FACTOR,
|
||||||
|
EPSILON,
|
||||||
|
MINIMUM_FORCE,
|
||||||
|
VISCOSITY_FACTOR,
|
||||||
|
TIME_DILATION_FACTOR,
|
||||||
|
MINIMUM_VELOCITY,
|
||||||
} from '../../util/constants.js';
|
} from '../../util/constants.js';
|
||||||
|
import { Edge } from '../supporting/edge.js';
|
||||||
|
import { WeightedDirectedGraph } from '../supporting/wdg.js';
|
||||||
import { Box } from './box.js';
|
import { Box } from './box.js';
|
||||||
import { Rectangle, Vector } from './geometry.js';
|
import { Rectangle, Vector } from './geometry.js';
|
||||||
|
|
||||||
|
@ -28,19 +38,67 @@ import { Rectangle, Vector } from './geometry.js';
|
||||||
|
|
||||||
// NOTE: When mouse is in our box, we could hijack the scroll actions to zoom in/out.
|
// NOTE: When mouse is in our box, we could hijack the scroll actions to zoom in/out.
|
||||||
|
|
||||||
export class ForceDirectedGraph extends Box {
|
export class ForceDirectedGraph extends WeightedDirectedGraph {
|
||||||
constructor(name, parentEl, options = {}) {
|
constructor(name, parentEl, options = {}) {
|
||||||
super(name, parentEl, options);
|
super(name, options);
|
||||||
this.addClass('fixed');
|
this.box = new Box(name, parentEl, options);
|
||||||
|
this.box.addClass('fixed');
|
||||||
|
this.box.addClass('force-directed-graph');
|
||||||
|
this.intervalTask = null;
|
||||||
|
this.canvas = window.document.createElement('canvas');
|
||||||
|
this.box.el.style.width = `${options.width ?? 800}px`;
|
||||||
|
this.box.el.style.height = `${options.height ?? 600}px`;
|
||||||
|
this.box.el.appendChild(this.canvas);
|
||||||
|
this.fitCanvasToGraph();
|
||||||
|
this.nodes = [];
|
||||||
|
this.edges = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
addBox(name) {
|
fitCanvasToGraph() {
|
||||||
const box = super.addBox(name);
|
[this.canvas.width, this.canvas.height] = this.box.getGeometry().dimensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
addVertex(...args) {
|
||||||
|
const vertex = super.addVertex(...args);
|
||||||
|
const box = this.box.addBox(vertex.id);
|
||||||
box.addClass('absolute');
|
box.addClass('absolute');
|
||||||
|
box.addClass('vertex');
|
||||||
box.el.style.left = '0px';
|
box.el.style.left = '0px';
|
||||||
box.el.style.top = '0px';
|
box.el.style.top = '0px';
|
||||||
box.velocity = Vector.from([0, 0]);
|
box.velocity = Vector.from([0, 0]);
|
||||||
return box;
|
box.setInnerHTML(vertex.getHTML());
|
||||||
|
box.vertex = vertex;
|
||||||
|
this.nodes.push(box);
|
||||||
|
vertex.onUpdate = () => {
|
||||||
|
box.setInnerHTML(vertex.getHTML());
|
||||||
|
};
|
||||||
|
return vertex;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEdge(type, from, to, ...rest) {
|
||||||
|
const fromBox = this.nodes.find(({ name }) => name === from);
|
||||||
|
const toBox = this.nodes.find(({ name }) => name === to);
|
||||||
|
if (!fromBox) throw new Error(`from ${from}: Node not found`);
|
||||||
|
if (!toBox) throw new Error(`to ${to}: Node not found`);
|
||||||
|
|
||||||
|
const edge = super.addEdge(type, from, to, ...rest);
|
||||||
|
const box = this.box.addBox(Edge.getKey({ from, to, type }));
|
||||||
|
box.addClass('absolute');
|
||||||
|
box.addClass('edge');
|
||||||
|
// TODO: Center between nodes
|
||||||
|
box.el.style.left = '0px';
|
||||||
|
box.el.style.top = '0px';
|
||||||
|
box.velocity = Vector.from([0, 0]);
|
||||||
|
box.setInnerHTML(edge.getHTML());
|
||||||
|
box.edge = edge;
|
||||||
|
this.edges.push(box);
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEdgeWeight(...args) {
|
||||||
|
const edge = super.setEdgeWeight(...args);
|
||||||
|
edge.displayEdgeNode();
|
||||||
|
return edge;
|
||||||
}
|
}
|
||||||
|
|
||||||
static pairwiseForce(boxA, boxB, targetRadius) {
|
static pairwiseForce(boxA, boxB, targetRadius) {
|
||||||
|
@ -54,9 +112,9 @@ export class ForceDirectedGraph extends Box {
|
||||||
if (rectA.doesOverlap(rectB)) {
|
if (rectA.doesOverlap(rectB)) {
|
||||||
// if their centers actually coincide we can just randomize the direction.
|
// if their centers actually coincide we can just randomize the direction.
|
||||||
if (r.magnitudeSquared === 0) {
|
if (r.magnitudeSquared === 0) {
|
||||||
return Vector.randomUnitVector(rectA.dim).scale(OVERLAP_FORCE);
|
return Vector.randomUnitVector(rectA.dim).scale(DEFAULT_OVERLAP_FORCE);
|
||||||
}
|
}
|
||||||
return r.normalize().scale(OVERLAP_FORCE);
|
return r.normalize().scale(DEFAULT_OVERLAP_FORCE);
|
||||||
}
|
}
|
||||||
// repel if closer than targetRadius
|
// repel if closer than targetRadius
|
||||||
// attract if farther than targetRadius
|
// attract if farther than targetRadius
|
||||||
|
@ -64,13 +122,30 @@ export class ForceDirectedGraph extends Box {
|
||||||
return r.normalize().scale(force);
|
return r.normalize().scale(force);
|
||||||
}
|
}
|
||||||
|
|
||||||
computeEulerFrame(tDelta) {
|
async runUntilEquilibrium(tDelta = DEFAULT_TIME_STEP) {
|
||||||
|
if (this.intervalTask) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.intervalTask = setInterval(() => {
|
||||||
|
const { atEquilibrium } = this.computeEulerFrame(tDelta);
|
||||||
|
if (atEquilibrium) {
|
||||||
|
clearInterval(this.intervalTask);
|
||||||
|
this.intervalTask = null;
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, tDelta * TIME_DILATION_FACTOR);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
computeEulerFrame(tDelta = DEFAULT_TIME_STEP) {
|
||||||
// Compute all net forces
|
// Compute all net forces
|
||||||
const netForces = Array.from(Array(this.boxes.length), () => Vector.from([0, 0]));
|
const netForces = Array.from(Array(this.nodes.length), () => Vector.from([0, 0]));
|
||||||
for (const boxA of this.boxes) {
|
let atEquilibrium = true;
|
||||||
const idxA = this.boxes.indexOf(boxA);
|
for (const boxA of this.nodes) {
|
||||||
for (const boxB of this.boxes.slice(idxA + 1)) {
|
const idxA = this.nodes.indexOf(boxA);
|
||||||
const idxB = this.boxes.indexOf(boxB);
|
for (const boxB of this.nodes.slice(idxA + 1)) {
|
||||||
|
const idxB = this.nodes.indexOf(boxB);
|
||||||
const force = ForceDirectedGraph.pairwiseForce(boxA, boxB, DEFAULT_TARGET_RADIUS);
|
const force = ForceDirectedGraph.pairwiseForce(boxA, boxB, DEFAULT_TARGET_RADIUS);
|
||||||
// Ignore forces below a certain threshold
|
// Ignore forces below a certain threshold
|
||||||
if (force.magnitude >= MINIMUM_FORCE) {
|
if (force.magnitude >= MINIMUM_FORCE) {
|
||||||
|
@ -81,31 +156,37 @@ export class ForceDirectedGraph extends Box {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute motions
|
// Compute motions
|
||||||
for (const box of this.boxes) {
|
for (const box of this.nodes) {
|
||||||
const idx = this.boxes.indexOf(box);
|
const idx = this.nodes.indexOf(box);
|
||||||
box.velocity = box.velocity.add(netForces[idx].scale(tDelta));
|
box.velocity = box.velocity.add(netForces[idx].scale(tDelta));
|
||||||
// Apply some drag
|
// Apply some drag
|
||||||
box.velocity = box.velocity.scale(1 - VISCOSITY_FACTOR);
|
box.velocity = box.velocity.scale(1 - VISCOSITY_FACTOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const box of this.boxes) {
|
for (const box of this.nodes) {
|
||||||
box.move(box.velocity);
|
if (box.velocity.magnitude >= MINIMUM_VELOCITY) {
|
||||||
}
|
atEquilibrium = false;
|
||||||
|
box.move(box.velocity);
|
||||||
// TODO: translate everything to keep coordinates positive
|
|
||||||
const translate = Vector.zeros(2);
|
|
||||||
for (const box of this.boxes) {
|
|
||||||
const rect = box.getGeometry();
|
|
||||||
console.log({ box, rect });
|
|
||||||
for (const vertex of rect.vertices) {
|
|
||||||
for (let dim = 0; dim < vertex.dim; dim++) {
|
|
||||||
translate[dim] = Math.max(translate[dim], -vertex[dim]);
|
|
||||||
console.log(`vertex[${dim}] = ${vertex[dim]}, translate[${dim}] = ${translate[dim]}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const box of this.boxes) {
|
|
||||||
|
// Translate everything to keep coordinates positive
|
||||||
|
// TODO: Consider centering and scaling to viewport size
|
||||||
|
const topLeft = this.box.getGeometry().startPoint;
|
||||||
|
const translate = Vector.zeros(2);
|
||||||
|
for (const box of this.nodes) {
|
||||||
|
const rect = box.getGeometry();
|
||||||
|
for (const vertex of rect.vertices) {
|
||||||
|
translate[0] = Math.max(translate[0], topLeft[0] - vertex[0]);
|
||||||
|
translate[1] = Math.max(translate[1], topLeft[1] - vertex[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const box of this.nodes) {
|
||||||
box.move(translate);
|
box.move(translate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.fitCanvasToGraph();
|
||||||
|
|
||||||
|
return { atEquilibrium };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ export class Vector extends Array {
|
||||||
}
|
}
|
||||||
|
|
||||||
static unitVector(dim, totalDim) {
|
static unitVector(dim, totalDim) {
|
||||||
return Vector.from(Array(totalDim).map((_, idx) => (idx === dim ? 1 : 0)));
|
return Vector.from(Array(totalDim), (_, idx) => (idx === dim ? 1 : 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
get magnitudeSquared() {
|
get magnitudeSquared() {
|
||||||
|
@ -71,12 +71,12 @@ export class Rectangle extends Polygon {
|
||||||
// Next point is obtained by moving the specified length along each dimension
|
// Next point is obtained by moving the specified length along each dimension
|
||||||
// one at a time, then reversing these movements in the same order.
|
// one at a time, then reversing these movements in the same order.
|
||||||
let point = this.startPoint;
|
let point = this.startPoint;
|
||||||
for (let dim = 0; dim < dimensions.length; dim++) {
|
for (let dim = dimensions.length - 1; dim >= 0; dim--) {
|
||||||
this.addVertex(point);
|
this.addVertex(point);
|
||||||
const increment = Vector.unitVector(dim, dimensions.length);
|
const increment = Vector.unitVector(dim, dimensions.length);
|
||||||
point = point.add(increment);
|
point = point.add(increment);
|
||||||
}
|
}
|
||||||
for (let dim = 0; dim < dimensions.length; dim++) {
|
for (let dim = dimensions.length - 1; dim >= 0; dim--) {
|
||||||
this.addVertex(point);
|
this.addVertex(point);
|
||||||
const increment = Vector.unitVector(dim, dimensions.length);
|
const increment = Vector.unitVector(dim, dimensions.length);
|
||||||
point = point.subtract(increment);
|
point = point.subtract(increment);
|
||||||
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
import { Vertex } from '../supporting/vertex.js';
|
||||||
|
import { Edge } from '../supporting/edge.js';
|
||||||
|
import { Document } from './document.js';
|
||||||
|
import { WeightedDirectedGraph } from '../supporting/wdg.js';
|
||||||
|
|
||||||
|
const allGraphs = [];
|
||||||
|
|
||||||
|
const makeWDGHandler = (graphIndex) => (vertexId) => {
|
||||||
|
const graph = allGraphs[graphIndex];
|
||||||
|
// We want a document for editing this node, which may be a vertex or an edge
|
||||||
|
const { editorDoc } = graph;
|
||||||
|
editorDoc.clear();
|
||||||
|
if (vertexId.startsWith('edge:')) {
|
||||||
|
const [, from, to] = vertexId.split(':');
|
||||||
|
Edge.prepareEditorDocument(graph, editorDoc, from, to);
|
||||||
|
} else {
|
||||||
|
Vertex.prepareEditorDocument(graph, editorDoc, vertexId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export class WDGDiagram extends WeightedDirectedGraph {
|
||||||
|
constructor(name, scene, options = {}) {
|
||||||
|
super(name, options);
|
||||||
|
this.scene = scene;
|
||||||
|
this.flowchart = scene?.flowchart;
|
||||||
|
this.editable = options.editable;
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
this.index = allGraphs.length;
|
||||||
|
allGraphs.push(this);
|
||||||
|
window[`WDGHandler${this.index}`] = makeWDGHandler(this.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
fromJSON(...args) {
|
||||||
|
super.fromJSON(...args);
|
||||||
|
this.redraw();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addVertex(...args) {
|
||||||
|
const vertex = super.addVertex(...args);
|
||||||
|
vertex.displayVertex();
|
||||||
|
return vertex;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEdge(type, from, to, ...rest) {
|
||||||
|
const existingEdges = this.getEdges(null, from, to);
|
||||||
|
const edge = super.addEdge(type, from, to, ...rest);
|
||||||
|
if (existingEdges.length) {
|
||||||
|
edge.displayEdgeNode();
|
||||||
|
} else {
|
||||||
|
edge.displayEdge();
|
||||||
|
}
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEdgeWeight(...args) {
|
||||||
|
const edge = super.setEdgeWeight(...args);
|
||||||
|
edge.displayEdgeNode();
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure rerender
|
||||||
|
this.flowchart?.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
withFlowchart() {
|
||||||
|
this.scene?.withSectionFlowchart();
|
||||||
|
this.flowchart = this.scene?.lastFlowchart;
|
||||||
|
if (this.editable) {
|
||||||
|
this.controlDoc = new Document('WDGControl', this.flowchart.box.el, { prepend: true });
|
||||||
|
this.editorDoc = new Document('WDGEditor', this.flowchart.box.el);
|
||||||
|
this.errorDoc = new Document('WDGErrors', this.flowchart.box.el);
|
||||||
|
this.resetEditor();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareControlDoc() {
|
||||||
|
const form = this.controlDoc.form({ name: 'controlForm' }).lastElement;
|
||||||
|
const { subForm: graphPropertiesForm } = form.subForm({ name: 'graphPropsForm' }).lastItem;
|
||||||
|
graphPropertiesForm.flex()
|
||||||
|
.textField({
|
||||||
|
name: 'name',
|
||||||
|
label: 'Graph name',
|
||||||
|
defaultValue: this.name,
|
||||||
|
cb: (({ form: { value: { name } } }) => {
|
||||||
|
this.name = name;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const { subForm: exportImportForm } = form.subForm({ name: 'exportImportForm' }).lastItem;
|
||||||
|
exportImportForm.flex()
|
||||||
|
.button({
|
||||||
|
name: 'Export',
|
||||||
|
cb: () => {
|
||||||
|
const a = window.document.createElement('a');
|
||||||
|
const json = JSON.stringify(this.toJSON(), null, 2);
|
||||||
|
const currentTime = Math.floor(new Date().getTime() / 1000);
|
||||||
|
a.href = `data:attachment/text,${encodeURI(json)}`;
|
||||||
|
a.target = '_blank';
|
||||||
|
a.download = `wdg_${this.name}_${currentTime}.json`;
|
||||||
|
a.click();
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.fileInput({
|
||||||
|
name: 'Import',
|
||||||
|
cb: ({ input: { files: [file] } }) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = ({ target: { result: text } }) => {
|
||||||
|
console.log('imported file', { file });
|
||||||
|
// this.flowchart?.log(`%% Imported file ${file}`)
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
this.fromJSON(data);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetEditor() {
|
||||||
|
this.editorDoc.clear();
|
||||||
|
this.controlDoc.clear();
|
||||||
|
this.prepareControlDoc();
|
||||||
|
Vertex.prepareEditorDocument(this, this.editorDoc);
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,7 +37,7 @@ export class Edge {
|
||||||
return this.graph.getEdges(null, this.from, this.to);
|
return this.graph.getEdges(null, this.from, this.to);
|
||||||
}
|
}
|
||||||
|
|
||||||
getHtml() {
|
getHTML() {
|
||||||
const edges = this.getComorphicEdges();
|
const edges = this.getComorphicEdges();
|
||||||
let html = '';
|
let html = '';
|
||||||
html += '<table>';
|
html += '<table>';
|
||||||
|
@ -46,25 +46,21 @@ export class Edge {
|
||||||
}
|
}
|
||||||
html += '</table>';
|
html += '</table>';
|
||||||
|
|
||||||
return html;
|
return `${Edge.getCombinedKey(this)}("${html}")`;
|
||||||
}
|
|
||||||
|
|
||||||
getFlowchartNode() {
|
|
||||||
return `${Edge.getCombinedKey(this)}("${this.getHtml()}")`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
displayEdgeNode() {
|
displayEdgeNode() {
|
||||||
if (this.options.hide) {
|
if (this.options.hide) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.graph.flowchart?.log(this.getFlowchartNode());
|
this.graph.flowchart?.log(this.getHTML());
|
||||||
}
|
}
|
||||||
|
|
||||||
displayEdge() {
|
displayEdge() {
|
||||||
if (this.options.hide) {
|
if (this.options.hide) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.graph.flowchart?.log(`${this.from.id} --- ${this.getFlowchartNode()} --> ${this.to.id}`);
|
this.graph.flowchart?.log(`${this.from.id} --- ${this.getHTML()} --> ${this.to.id}`);
|
||||||
this.graph.flowchart?.log(`class ${Edge.getCombinedKey(this)} edge`);
|
this.graph.flowchart?.log(`class ${Edge.getCombinedKey(this)} edge`);
|
||||||
if (this.graph.editable && !this.installedClickCallback) {
|
if (this.graph.editable && !this.installedClickCallback) {
|
||||||
this.graph.flowchart?.log(`click ${Edge.getCombinedKey(this)} WDGHandler${this.graph.index} \
|
this.graph.flowchart?.log(`click ${Edge.getCombinedKey(this)} WDGHandler${this.graph.index} \
|
||||||
|
|
|
@ -40,23 +40,19 @@ export class Vertex {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setProperty(key, value) {
|
async setProperty(key, value) {
|
||||||
this.properties.set(key, value);
|
this.properties.set(key, value);
|
||||||
|
await this.onUpdate?.();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
displayVertex() {
|
getHTML() {
|
||||||
if (this.options.hide) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
if (this.type) {
|
if (this.type) {
|
||||||
html += `<span class='small'>${this.type}</span>`;
|
html += `<span class='small'>${this.type}</span><br>`;
|
||||||
}
|
}
|
||||||
html += `${this.label || this.id}`;
|
html += `${this.label || this.id}`;
|
||||||
html += '<table>';
|
html += '<table>';
|
||||||
console.log('displayVertex', { properties: this.properties });
|
|
||||||
for (const [key, value] of this.properties.entries()) {
|
for (const [key, value] of this.properties.entries()) {
|
||||||
const displayValue = typeof value === 'number' ? displayNumber(value) : value;
|
const displayValue = typeof value === 'number' ? displayNumber(value) : value;
|
||||||
html += `<tr><td>${key}</td><td>${displayValue}</td></tr>`;
|
html += `<tr><td>${key}</td><td>${displayValue}</td></tr>`;
|
||||||
|
@ -66,7 +62,15 @@ export class Vertex {
|
||||||
html += `<span class=small>${this.id}</span><br>`;
|
html += `<span class=small>${this.id}</span><br>`;
|
||||||
}
|
}
|
||||||
html = html.replaceAll(/\n\s*/g, '');
|
html = html.replaceAll(/\n\s*/g, '');
|
||||||
this.graph.flowchart?.log(`${this.id}["${html}"]`);
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayVertex() {
|
||||||
|
if (this.options.hide) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.graph.flowchart?.log(`${this.id}["${this.getHTML()}"]`);
|
||||||
|
|
||||||
if (this.graph.editable && !this.installedClickCallback) {
|
if (this.graph.editable && !this.installedClickCallback) {
|
||||||
this.graph.flowchart?.log(`click ${this.id} WDGHandler${this.graph.index} "Edit Vertex ${this.id}"`);
|
this.graph.flowchart?.log(`click ${this.id} WDGHandler${this.graph.index} "Edit Vertex ${this.id}"`);
|
||||||
|
|
|
@ -1,38 +1,12 @@
|
||||||
import { Vertex } from './vertex.js';
|
import { Vertex } from './vertex.js';
|
||||||
import { Edge } from './edge.js';
|
import { Edge } from './edge.js';
|
||||||
import { Document } from '../display/document.js';
|
|
||||||
|
|
||||||
const allGraphs = [];
|
|
||||||
|
|
||||||
const makeWDGHandler = (graphIndex) => (vertexId) => {
|
|
||||||
const graph = allGraphs[graphIndex];
|
|
||||||
// We want a document for editing this node, which may be a vertex or an edge
|
|
||||||
const { editorDoc } = graph;
|
|
||||||
editorDoc.clear();
|
|
||||||
if (vertexId.startsWith('edge:')) {
|
|
||||||
const [, from, to] = vertexId.split(':');
|
|
||||||
Edge.prepareEditorDocument(graph, editorDoc, from, to);
|
|
||||||
} else {
|
|
||||||
Vertex.prepareEditorDocument(graph, editorDoc, vertexId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export class WeightedDirectedGraph {
|
export class WeightedDirectedGraph {
|
||||||
constructor(name, scene, options = {}) {
|
constructor(name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.scene = scene;
|
|
||||||
this.vertices = new Map();
|
this.vertices = new Map();
|
||||||
this.edgeTypes = new Map();
|
this.edgeTypes = new Map();
|
||||||
this.nextVertexId = 0;
|
this.nextVertexId = 0;
|
||||||
this.flowchart = scene?.flowchart;
|
|
||||||
this.editable = options.editable;
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
this.index = allGraphs.length;
|
|
||||||
allGraphs.push(this);
|
|
||||||
window[`WDGHandler${this.index}`] = makeWDGHandler(this.index);
|
|
||||||
|
|
||||||
// TODO: Populate history
|
// TODO: Populate history
|
||||||
this.history = [];
|
this.history = [];
|
||||||
|
@ -72,109 +46,9 @@ export class WeightedDirectedGraph {
|
||||||
} of edges) {
|
} of edges) {
|
||||||
this.addEdge(type, from, to, weight);
|
this.addEdge(type, from, to, weight);
|
||||||
}
|
}
|
||||||
this.redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure rerender
|
|
||||||
this.flowchart?.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
withFlowchart() {
|
|
||||||
this.scene?.withSectionFlowchart();
|
|
||||||
this.flowchart = this.scene?.lastFlowchart;
|
|
||||||
if (this.editable) {
|
|
||||||
this.controlDoc = new Document('WDGControl', this.flowchart.box.el, { prepend: true });
|
|
||||||
this.editorDoc = new Document('WDGEditor', this.flowchart.box.el);
|
|
||||||
this.errorDoc = new Document('WDGErrors', this.flowchart.box.el);
|
|
||||||
this.resetEditor();
|
|
||||||
}
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareControlDoc() {
|
|
||||||
const form = this.controlDoc.form({ name: 'controlForm' }).lastElement;
|
|
||||||
const { subForm: graphPropertiesForm } = form.subForm({ name: 'graphPropsForm' }).lastItem;
|
|
||||||
graphPropertiesForm.flex()
|
|
||||||
.textField({
|
|
||||||
name: 'name',
|
|
||||||
label: 'Graph name',
|
|
||||||
defaultValue: this.name,
|
|
||||||
cb: (({ form: { value: { name } } }) => {
|
|
||||||
this.name = name;
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const { subForm: exportImportForm } = form.subForm({ name: 'exportImportForm' }).lastItem;
|
|
||||||
exportImportForm.flex()
|
|
||||||
.button({
|
|
||||||
name: 'Export',
|
|
||||||
cb: () => {
|
|
||||||
const a = window.document.createElement('a');
|
|
||||||
const json = JSON.stringify(this.toJSON(), null, 2);
|
|
||||||
const currentTime = Math.floor(new Date().getTime() / 1000);
|
|
||||||
a.href = `data:attachment/text,${encodeURI(json)}`;
|
|
||||||
a.target = '_blank';
|
|
||||||
a.download = `wdg_${this.name}_${currentTime}.json`;
|
|
||||||
a.click();
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.fileInput({
|
|
||||||
name: 'Import',
|
|
||||||
cb: ({ input: { files: [file] } }) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = ({ target: { result: text } }) => {
|
|
||||||
console.log('imported file', { file });
|
|
||||||
// this.flowchart?.log(`%% Imported file ${file}`)
|
|
||||||
const data = JSON.parse(text);
|
|
||||||
this.fromJSON(data);
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
resetEditor() {
|
|
||||||
this.editorDoc.clear();
|
|
||||||
this.controlDoc.clear();
|
|
||||||
this.prepareControlDoc();
|
|
||||||
Vertex.prepareEditorDocument(this, this.editorDoc);
|
|
||||||
}
|
|
||||||
|
|
||||||
addVertex(type, id, data, label, options) {
|
addVertex(type, id, data, label, options) {
|
||||||
// Supports simple case of auto-incremented numeric ids
|
// Supports simple case of auto-incremented numeric ids
|
||||||
if (typeof id === 'object') {
|
if (typeof id === 'object') {
|
||||||
|
@ -187,7 +61,6 @@ export class WeightedDirectedGraph {
|
||||||
}
|
}
|
||||||
const vertex = new Vertex(this, type, id, data, { ...options, label });
|
const vertex = new Vertex(this, type, id, data, { ...options, label });
|
||||||
this.vertices.set(id, vertex);
|
this.vertices.set(id, vertex);
|
||||||
vertex.displayVertex();
|
|
||||||
return vertex;
|
return vertex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,7 +103,6 @@ export class WeightedDirectedGraph {
|
||||||
}
|
}
|
||||||
const edgeKey = Edge.getKey(edge);
|
const edgeKey = Edge.getKey(edge);
|
||||||
edges.set(edgeKey, edge);
|
edges.set(edgeKey, edge);
|
||||||
edge.displayEdgeNode();
|
|
||||||
return edge;
|
return edge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,15 +112,9 @@ export class WeightedDirectedGraph {
|
||||||
if (this.getEdge(type, from, to)) {
|
if (this.getEdge(type, from, to)) {
|
||||||
throw new Error(`Edge ${type} from ${from.id} to ${to.id} already exists`);
|
throw new Error(`Edge ${type} from ${from.id} to ${to.id} already exists`);
|
||||||
}
|
}
|
||||||
const existingEdges = this.getEdges(null, from, to);
|
|
||||||
const edge = this.setEdgeWeight(type, from, to, weight, data, options);
|
const edge = this.setEdgeWeight(type, from, to, weight, data, options);
|
||||||
from.edges.from.push(edge);
|
from.edges.from.push(edge);
|
||||||
to.edges.to.push(edge);
|
to.edges.to.push(edge);
|
||||||
if (existingEdges.length) {
|
|
||||||
edge.displayEdgeNode();
|
|
||||||
} else {
|
|
||||||
edge.displayEdge();
|
|
||||||
}
|
|
||||||
return edge;
|
return edge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -112,4 +112,20 @@ span.small {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
|
}
|
||||||
|
.force-directed-graph {
|
||||||
|
border: 1px white dotted;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
.force-directed-graph > canvas {
|
||||||
|
position: absolute;
|
||||||
|
border: 1px red dashed;
|
||||||
|
}
|
||||||
|
.force-directed-graph > .vertex {
|
||||||
|
border: 1px #46b4b4 solid;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.force-directed-graph > .edge {
|
||||||
|
border: 1px #51b769 solid;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
|
@ -10,15 +10,17 @@ const rootElement = document.getElementById('scene');
|
||||||
const rootBox = new Box('rootBox', rootElement).flex();
|
const rootBox = new Box('rootBox', rootElement).flex();
|
||||||
window.scene = new Scene('WDG test', rootBox);
|
window.scene = new Scene('WDG test', rootBox);
|
||||||
|
|
||||||
describe('Weighted Directed Graph', function tests() {
|
describe('Force-Directed Graph', function tests() {
|
||||||
this.timeout(0);
|
this.timeout(0);
|
||||||
|
|
||||||
let graph;
|
let graph;
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
graph = (window.graph = new ForceDirectedGraph('test1', window.scene.middleSection.el));
|
graph = (window.graph = new ForceDirectedGraph('test1', window.scene.middleSection.el, {
|
||||||
|
width: 800, height: 600,
|
||||||
|
}));
|
||||||
|
|
||||||
graph.addBox('box1').setInnerHTML('Box 1');
|
graph.addVertex('v1', 'box1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rectangle should be a polygon with 4 vertices', () => {
|
it('rectangle should be a polygon with 4 vertices', () => {
|
||||||
|
@ -35,7 +37,7 @@ describe('Weighted Directed Graph', function tests() {
|
||||||
rect1.center.should.eql([0.5, 0.5]);
|
rect1.center.should.eql([0.5, 0.5]);
|
||||||
rect2.center.should.eql([0.5, 1]);
|
rect2.center.should.eql([0.5, 1]);
|
||||||
const force1 = ForceDirectedGraph.pairwiseForce(rect1, rect2, 10);
|
const force1 = ForceDirectedGraph.pairwiseForce(rect1, rect2, 10);
|
||||||
force1.should.eql([0, 100]);
|
force1.should.eql([0, 200]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('boxes at target radius should have no net force', () => {
|
it('boxes at target radius should have no net force', () => {
|
||||||
|
@ -48,6 +50,11 @@ describe('Weighted Directed Graph', function tests() {
|
||||||
force[1].should.be.within(-EPSILON, EPSILON);
|
force[1].should.be.within(-EPSILON, EPSILON);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can construct a unit vector', () => {
|
||||||
|
Vector.unitVector(0, 2).should.eql([1, 0]);
|
||||||
|
Vector.unitVector(1, 2).should.eql([0, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
it('normalized vector should have length = 1', () => {
|
it('normalized vector should have length = 1', () => {
|
||||||
const v = Vector.from([2, 0]);
|
const v = Vector.from([2, 0]);
|
||||||
const u = v.normalize();
|
const u = v.normalize();
|
||||||
|
@ -56,17 +63,20 @@ describe('Weighted Directed Graph', function tests() {
|
||||||
|
|
||||||
it('random unit vector should have length = 1', () => {
|
it('random unit vector should have length = 1', () => {
|
||||||
const u = Vector.randomUnitVector(2);
|
const u = Vector.randomUnitVector(2);
|
||||||
console.log('unit vector:', u);
|
|
||||||
u.magnitude.should.be.within(1 - EPSILON, 1 + EPSILON);
|
u.magnitude.should.be.within(1 - EPSILON, 1 + EPSILON);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can add a second box to the graph', async () => {
|
it('can add a second box to the graph', async () => {
|
||||||
await delayOrWait(1000);
|
await delayOrWait(1000);
|
||||||
graph.addBox('box2').setInnerHTML('Box 2');
|
const v = graph.addVertex('v1', 'box2');
|
||||||
for (let i = 1; i < 50; i++) {
|
v.setProperty('prop', 'value');
|
||||||
await delayOrWait(100);
|
await graph.runUntilEquilibrium();
|
||||||
graph.computeEulerFrame(0.1);
|
});
|
||||||
}
|
|
||||||
|
it('can add an edge to the graph', async () => {
|
||||||
|
await delayOrWait(1000);
|
||||||
|
graph.addEdge('e1', 'box1', 'box2', 1);
|
||||||
|
await graph.runUntilEquilibrium();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Box } from '../../classes/display/box.js';
|
import { Box } from '../../classes/display/box.js';
|
||||||
import { Scene } from '../../classes/display/scene.js';
|
import { Scene } from '../../classes/display/scene.js';
|
||||||
|
import { WDGDiagram } from '../../classes/display/wdg-mermaid-ui.js';
|
||||||
import { WeightedDirectedGraph } from '../../classes/supporting/wdg.js';
|
import { WeightedDirectedGraph } from '../../classes/supporting/wdg.js';
|
||||||
import { mochaRun } from '../../util/helpers.js';
|
import { mochaRun } from '../../util/helpers.js';
|
||||||
|
|
||||||
|
@ -13,7 +14,7 @@ describe('Weighted Directed Graph', function tests() {
|
||||||
let graph;
|
let graph;
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
graph = (window.graph = new WeightedDirectedGraph('test1', window.scene)).withFlowchart();
|
graph = (window.graph = new WDGDiagram('test1', window.scene)).withFlowchart();
|
||||||
|
|
||||||
graph.addVertex('v1', {});
|
graph.addVertex('v1', {});
|
||||||
graph.addVertex('v1', {});
|
graph.addVertex('v1', {});
|
||||||
|
@ -150,20 +151,4 @@ describe('Weighted Directed Graph', function tests() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Editable', () => {
|
|
||||||
let graph;
|
|
||||||
|
|
||||||
it('should be editable', () => {
|
|
||||||
graph = (window.graph2 = new WeightedDirectedGraph('test2', window.scene, { editable: true })).withFlowchart();
|
|
||||||
|
|
||||||
graph.addVertex('v1', {});
|
|
||||||
graph.addVertex('v2', {});
|
|
||||||
graph.addVertex('v3', {});
|
|
||||||
|
|
||||||
graph.addEdge('e1', 2, 1, 1);
|
|
||||||
graph.addEdge('e2', 1, 0, 0.5);
|
|
||||||
graph.addEdge('e3', 2, 0, 0.25);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
mochaRun();
|
mochaRun();
|
||||||
|
|
|
@ -7,11 +7,14 @@ export const VertexTypes = {
|
||||||
POST: 'post',
|
POST: 'post',
|
||||||
AUTHOR: 'author',
|
AUTHOR: 'author',
|
||||||
};
|
};
|
||||||
|
export const DEFAULT_OVERLAP_FORCE = 200;
|
||||||
export const DEFAULT_REP_TOKEN_TYPE_ID = 0;
|
export const DEFAULT_REP_TOKEN_TYPE_ID = 0;
|
||||||
export const DEFAULT_TARGET_RADIUS = 100;
|
export const DEFAULT_TARGET_RADIUS = 300;
|
||||||
|
export const DEFAULT_TIME_STEP = 0.1;
|
||||||
|
export const DISTANCE_FACTOR = 0.25;
|
||||||
export const EPSILON = 2.23e-16;
|
export const EPSILON = 2.23e-16;
|
||||||
export const INCINERATOR_ADDRESS = '0';
|
export const INCINERATOR_ADDRESS = '0';
|
||||||
export const OVERLAP_FORCE = 100;
|
export const MINIMUM_FORCE = 1;
|
||||||
export const DISTANCE_FACTOR = 0.25;
|
export const MINIMUM_VELOCITY = 0.1;
|
||||||
export const MINIMUM_FORCE = 2;
|
export const TIME_DILATION_FACTOR = 500;
|
||||||
export const VISCOSITY_FACTOR = 0.4;
|
export const VISCOSITY_FACTOR = 0.4;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Box } from '../classes/display/box.js';
|
import { Box } from '../classes/display/box.js';
|
||||||
import { Scene } from '../classes/display/scene.js';
|
import { Scene } from '../classes/display/scene.js';
|
||||||
import { WeightedDirectedGraph } from '../classes/supporting/wdg.js';
|
import { WDGDiagram } from '../classes/display/wdg-mermaid-ui.js';
|
||||||
|
|
||||||
const rootElement = document.getElementById('scene');
|
const rootElement = document.getElementById('scene');
|
||||||
const rootBox = new Box('rootBox', rootElement).flex();
|
const rootBox = new Box('rootBox', rootElement).flex();
|
||||||
window.disableSceneControls = true;
|
window.disableSceneControls = true;
|
||||||
window.scene = new Scene('WDG Editor', rootBox);
|
window.scene = new Scene('WDG Editor', rootBox);
|
||||||
window.graph = new WeightedDirectedGraph('new', window.scene, { editable: true }).withFlowchart();
|
window.graph = new WDGDiagram('new', window.scene, { editable: true }).withFlowchart();
|
||||||
|
|
Loading…
Reference in New Issue