From 2195f5ea5667cb819f1f2106ec04973f0e4cccdb Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Tue, 8 Aug 2023 12:08:57 -0500 Subject: [PATCH] Force-directed graph is working --- src/classes/display/box.js | 4 +- src/classes/display/force-directed.js | 224 +++++++++++++------ src/classes/display/geometry.js | 100 --------- src/classes/display/pairwise-forces.js | 35 +++ src/classes/supporting/edge.js | 2 +- src/classes/supporting/geometry/index.js | 3 + src/classes/supporting/geometry/polygon.js | 18 ++ src/classes/supporting/geometry/rectangle.js | 46 ++++ src/classes/supporting/geometry/vector.js | 47 ++++ src/tests/scripts/force-directed.test.js | 15 +- src/util/constants.js | 4 +- 11 files changed, 320 insertions(+), 178 deletions(-) delete mode 100644 src/classes/display/geometry.js create mode 100644 src/classes/display/pairwise-forces.js create mode 100644 src/classes/supporting/geometry/index.js create mode 100644 src/classes/supporting/geometry/polygon.js create mode 100644 src/classes/supporting/geometry/rectangle.js create mode 100644 src/classes/supporting/geometry/vector.js diff --git a/src/classes/display/box.js b/src/classes/display/box.js index 6a658c8..843b0b2 100644 --- a/src/classes/display/box.js +++ b/src/classes/display/box.js @@ -1,6 +1,6 @@ import { DisplayValue } from './display-value.js'; import { randomID } from '../../util/helpers.js'; -import { Rectangle, Vector } from './geometry.js'; +import { Rectangle, Vector } from '../supporting/geometry/index.js'; export class Box { constructor(name, parentEl, options = {}) { @@ -61,7 +61,7 @@ export class Box { return this.el.id; } - getGeometry() { + get rect() { const { width, height, } = this.el.getBoundingClientRect(); diff --git a/src/classes/display/force-directed.js b/src/classes/display/force-directed.js index eda9254..a1334f9 100644 --- a/src/classes/display/force-directed.js +++ b/src/classes/display/force-directed.js @@ -1,18 +1,16 @@ import { - DEFAULT_OVERLAP_FORCE, - DEFAULT_TARGET_RADIUS, DEFAULT_TIME_STEP, - DISTANCE_FACTOR, - EPSILON, - MINIMUM_FORCE, + MINIMUM_VELOCITY, VISCOSITY_FACTOR, TIME_DILATION_FACTOR, - MINIMUM_VELOCITY, + MAX_STEPS_TO_EQUILIBRIUM, + TRANSLATION_VELOCITY_FACTOR, } from '../../util/constants.js'; import { Edge } from '../supporting/edge.js'; import { WeightedDirectedGraph } from '../supporting/wdg.js'; import { Box } from './box.js'; -import { Rectangle, Vector } from './geometry.js'; +import { Vector, Rectangle } from '../supporting/geometry/index.js'; +import { overlapRepulsionForce, targetRadiusForce } from './pairwise-forces.js'; // Render children with absolute css positioning. @@ -39,15 +37,17 @@ import { Rectangle, Vector } from './geometry.js'; // NOTE: When mouse is in our box, we could hijack the scroll actions to zoom in/out. export class ForceDirectedGraph extends WeightedDirectedGraph { - constructor(name, parentEl, options = {}) { + constructor(name, parentEl, { width = 800, height = 600, ...options } = {}) { super(name, options); 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.width = width; + this.height = height; + this.box.el.style.width = `${width}px`; + this.box.el.style.height = `${height}px`; this.box.el.appendChild(this.canvas); this.fitCanvasToGraph(); this.nodes = []; @@ -55,23 +55,65 @@ export class ForceDirectedGraph extends WeightedDirectedGraph { } fitCanvasToGraph() { - [this.canvas.width, this.canvas.height] = this.box.getGeometry().dimensions; + [this.canvas.width, this.canvas.height] = this.box.rect.dimensions; } + // addVertex(type, id, data, label, options) { addVertex(...args) { const vertex = super.addVertex(...args); const box = this.box.addBox(vertex.id); + + // Link from the graph vertex to the corresponding display box + vertex.box = box; + + // Link from the display box to the corresponding graph vertex + box.vertex = vertex; + box.addClass('absolute'); box.addClass('vertex'); box.el.style.left = '0px'; box.el.style.top = '0px'; box.velocity = Vector.from([0, 0]); box.setInnerHTML(vertex.getHTML()); - box.vertex = vertex; this.nodes.push(box); + + // When vertex properties are updated, re-render the node contents vertex.onUpdate = () => { box.setInnerHTML(vertex.getHTML()); + // Maybe resolve forces + this.runUntilEquilibrium(); }; + + this.runUntilEquilibrium(); + + // Allow moving vertices with the mouse + box.el.addEventListener('mousedown', (e) => { + if (!this.mouseMoving) { + e.preventDefault(); + // Record current mouse position + this.mousePosition = Vector.from([e.clientX, e.clientY]); + // Begin tracking mouse movement + this.mouseMoving = box; + } + }); + document.addEventListener('mousemove', (e) => { + if (this.mouseMoving === box) { + const mousePosition = Vector.from([e.clientX, e.clientY]); + // Apply translation + box.move(mousePosition.subtract(this.mousePosition)); + // Update current mouse position + this.mousePosition = mousePosition; + // Equilibrate + this.runUntilEquilibrium(); + } + }); + document.addEventListener('mouseup', () => { + // Stop tracking mouse movement + this.mouseMoving = null; + // Equilibrate + this.runUntilEquilibrium(); + }); + return vertex; } @@ -83,15 +125,28 @@ export class ForceDirectedGraph extends WeightedDirectedGraph { const edge = super.addEdge(type, from, to, ...rest); const box = this.box.addBox(Edge.getKey({ from, to, type })); + + edge.box = box; + box.edge = edge; + 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); + + // Center between nodes, by applying an attraction force from the edge node + // to its `from` and `to` nodes. + // We can also initially place it near the midpoint between the `from` and `to` nodes. + const midpoint = fromBox.rect.center.add(toBox.rect.center).scale(0.5); + const startPosition = midpoint.subtract(box.rect.dimensions.scale(0.5)); + console.log({ + fromBox, toBox, midpoint, startPosition, dimensions: box.rect.dimensions, + }); + box.move(startPosition); + box.velocity = Vector.from([0, 0]); + this.runUntilEquilibrium(); + return edge; } @@ -101,35 +156,22 @@ export class ForceDirectedGraph extends WeightedDirectedGraph { return edge; } - static pairwiseForce(boxA, boxB, targetRadius) { - const rectA = boxA instanceof Rectangle ? boxA : boxA.getGeometry(); - const centerA = rectA.center; - const rectB = boxB instanceof Rectangle ? boxB : boxB.getGeometry(); - const centerB = rectB.center; - const r = centerB.subtract(centerA); - - // Apply a stronger force when overlap occurs - if (rectA.doesOverlap(rectB)) { - // if their centers actually coincide we can just randomize the direction. - if (r.magnitudeSquared === 0) { - return Vector.randomUnitVector(rectA.dim).scale(DEFAULT_OVERLAP_FORCE); - } - return r.normalize().scale(DEFAULT_OVERLAP_FORCE); - } - // repel if closer than targetRadius - // attract if farther than targetRadius - const force = -DISTANCE_FACTOR * (r.magnitude - targetRadius); - return r.normalize().scale(force); - } - async runUntilEquilibrium(tDelta = DEFAULT_TIME_STEP) { + this.steps = 0; if (this.intervalTask) { return Promise.resolve(); } - return new Promise((resolve) => { + return new Promise((resolve, reject) => { this.intervalTask = setInterval(() => { + this.steps++; + if (this.steps > MAX_STEPS_TO_EQUILIBRIUM) { + clearInterval(this.intervalTask); + this.intervalTask = null; + reject(new Error('Exceeded map steps to reach equilibrium')); + } const { atEquilibrium } = this.computeEulerFrame(tDelta); if (atEquilibrium) { + console.log(`Reached equilibrium after ${this.steps} steps`); clearInterval(this.intervalTask); this.intervalTask = null; resolve(); @@ -139,53 +181,99 @@ export class ForceDirectedGraph extends WeightedDirectedGraph { } computeEulerFrame(tDelta = DEFAULT_TIME_STEP) { - // Compute all net forces - const netForces = Array.from(Array(this.nodes.length), () => Vector.from([0, 0])); - let atEquilibrium = true; + // Compute net forces on each box in the graph + const boxes = [...this.nodes, ...this.edges]; + + // Initialize net force vectors + for (const box of boxes) { + box.netForce = Vector.zeros(2); + } + + // Compute overlap repulsion forces among node boxes for (const boxA of this.nodes) { - const idxA = this.nodes.indexOf(boxA); + const idxA = boxes.indexOf(boxA); for (const boxB of this.nodes.slice(idxA + 1)) { - const idxB = this.nodes.indexOf(boxB); - const force = ForceDirectedGraph.pairwiseForce(boxA, boxB, DEFAULT_TARGET_RADIUS); - // Ignore forces below a certain threshold - if (force.magnitude >= MINIMUM_FORCE) { - netForces[idxA] = netForces[idxA].subtract(force); - netForces[idxB] = netForces[idxB].add(force); - } + const force = overlapRepulsionForce(boxA, boxB); + boxA.netForce = boxA.netForce.subtract(force); + boxB.netForce = boxB.netForce.add(force); } } - // Compute motions - for (const box of this.nodes) { - const idx = this.nodes.indexOf(box); - box.velocity = box.velocity.add(netForces[idx].scale(tDelta)); + // Compute pairwise forces among nodes + for (const boxA of this.nodes) { + const idxA = this.nodes.indexOf(boxA); + for (const boxB of this.nodes.slice(idxA + 1)) { + const force = targetRadiusForce(boxA, boxB); + boxA.netForce = boxA.netForce.subtract(force); + boxB.netForce = boxB.netForce.add(force); + } + } + + // Compute forces on edge boxes: + // Attraction to the `from` and `to` nodes + for (const edgeBox of this.edges) { + const { edge } = edgeBox; + const fromBox = edge.from.box; + const toBox = edge.to.box; + edgeBox.netForce = edgeBox.netForce + .subtract(targetRadiusForce(edgeBox, fromBox, 0)) + .subtract(targetRadiusForce(edgeBox, toBox, 0)); + } + + // Do not apply forces to a box if it is being moved by the mouse + for (const box of boxes) { + if (this.mouseMoving === box) { + box.netForce = Vector.zeros(2); + } + } + + // Compute velocities + for (const box of boxes) { + box.velocity = box.velocity.add(box.netForce.scale(tDelta)); // Apply some drag box.velocity = box.velocity.scale(1 - VISCOSITY_FACTOR); } - for (const box of this.nodes) { + // When all velocities are below MINIMUM_VELOCITY, we have reached equilibrium. + let atEquilibrium = true; + + // Apply velocities + for (const box of boxes) { if (box.velocity.magnitude >= MINIMUM_VELOCITY) { atEquilibrium = false; box.move(box.velocity); } } - // 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]); + // Center the items by computing the bounding box and centering that + if (!this.mouseMoving) { + const topLeft = Vector.from(boxes[0].position); + const bottomRight = Vector.from(boxes[0].position); + for (const box of boxes) { + for (const vertex of box.rect.vertices) { + topLeft[0] = Math.min(topLeft[0], vertex[0]); + topLeft[1] = Math.min(topLeft[1], vertex[1]); + + bottomRight[0] = Math.max(bottomRight[0], vertex[0]); + bottomRight[1] = Math.max(bottomRight[1], vertex[1]); + } + } + const boundingBox = new Rectangle(topLeft, bottomRight.subtract(topLeft)); + const graphCenter = Vector.from([this.width, this.height]).scale(0.5); + const offset = graphCenter.subtract(boundingBox.center); + const translate = offset.scale(TRANSLATION_VELOCITY_FACTOR); + + if (translate.magnitude >= MINIMUM_VELOCITY) { + atEquilibrium = false; + + // Apply translations + for (const box of boxes) { + box.move(translate); + } } } - for (const box of this.nodes) { - box.move(translate); - } - this.fitCanvasToGraph(); + // TODO: Scaling to fit return { atEquilibrium }; } diff --git a/src/classes/display/geometry.js b/src/classes/display/geometry.js deleted file mode 100644 index 73fcc13..0000000 --- a/src/classes/display/geometry.js +++ /dev/null @@ -1,100 +0,0 @@ -export class Vector extends Array { - get dim() { - return this.length ?? 0; - } - - add(vector) { - if (vector.dim !== this.dim) { - throw new Error('Can only add vectors of the same dimensions'); - } - return Vector.from(this.map((q, idx) => q + vector[idx])); - } - - subtract(vector) { - if (vector.dim !== this.dim) { - throw new Error('Can only subtract vectors of the same dimensions'); - } - return Vector.from(this.map((q, idx) => q - vector[idx])); - } - - static unitVector(dim, totalDim) { - return Vector.from(Array(totalDim), (_, idx) => (idx === dim ? 1 : 0)); - } - - get magnitudeSquared() { - return this.reduce((total, q) => total += q ** 2, 0); - } - - get magnitude() { - return Math.sqrt(this.magnitudeSquared); - } - - scale(factor) { - return Vector.from(this.map((q) => q * factor)); - } - - normalize() { - return this.scale(1 / this.magnitude); - } - - static randomUnitVector(totalDim) { - return Vector.from(Array(totalDim), () => Math.random()).normalize(); - } - - static zeros(totalDim) { - return Vector.from(Array(totalDim), () => 0); - } -} - -export class Polygon { - constructor() { - this.vertices = []; - this.dim = 0; - } - - addVertex(point) { - point = point instanceof Vector ? point : Vector.from(point); - if (!this.dim) { - this.dim = point.dim; - } else if (this.dim !== point.dim) { - throw new Error('All vertices of a polygon must have the same dimensionality'); - } - this.vertices.push(point); - } -} - -export class Rectangle extends Polygon { - constructor(startPoint, dimensions) { - super(); - this.startPoint = Vector.from(startPoint); - this.dimensions = Vector.from(dimensions); - // Next point is obtained by moving the specified length along each dimension - // one at a time, then reversing these movements in the same order. - let point = this.startPoint; - for (let dim = dimensions.length - 1; dim >= 0; dim--) { - this.addVertex(point); - const increment = Vector.unitVector(dim, dimensions.length); - point = point.add(increment); - } - for (let dim = dimensions.length - 1; dim >= 0; dim--) { - this.addVertex(point); - const increment = Vector.unitVector(dim, dimensions.length); - point = point.subtract(increment); - } - } - - get center() { - return Vector.from(this.dimensions.map((Q, idx) => this.startPoint[idx] + Q / 2)); - } - - doesOverlap(rect) { - return this.dimensions.every((_, idx) => { - const thisMin = this.startPoint[idx]; - const thisMax = this.startPoint[idx] + this.dimensions[idx]; - const thatMin = rect.startPoint[idx]; - const thatMax = rect.startPoint[idx] + rect.dimensions[idx]; - return (thisMin <= thatMin && thisMax >= thatMin) - || (thisMin >= thatMin && thisMin <= thatMax); - }); - } -} diff --git a/src/classes/display/pairwise-forces.js b/src/classes/display/pairwise-forces.js new file mode 100644 index 0000000..0ddefdd --- /dev/null +++ b/src/classes/display/pairwise-forces.js @@ -0,0 +1,35 @@ +import { + DEFAULT_OVERLAP_FORCE, + DEFAULT_TARGET_RADIUS, + DISTANCE_FACTOR, +} from '../../util/constants.js'; +import { Rectangle, Vector } from '../supporting/geometry/index.js'; + +const getRectangles = (boxes) => boxes.map((box) => (box instanceof Rectangle ? box : box.rect)); +const getCenters = (boxes) => getRectangles(boxes).map((rect) => rect.center); + +export const overlapRepulsionForce = (boxA, boxB, force = DEFAULT_OVERLAP_FORCE) => { + const [rectA, rectB] = getRectangles([boxA, boxB]); + const [centerA, centerB] = getCenters([rectA, rectB]); + const r = centerB.subtract(centerA); + + // Apply a stronger force when overlap occurs + if (!rectA.doesOverlap(rectB)) { + return Vector.zeros(rectA.dim); + } + // If their centers actually coincide we can just randomize the direction. + if (r.magnitudeSquared === 0) { + return Vector.randomUnitVector(rectA.dim).scale(force); + } + return r.normalize().scale(force); +}; + +export const targetRadiusForce = (boxA, boxB, targetRadius = DEFAULT_TARGET_RADIUS) => { + const [centerA, centerB] = getCenters([boxA, boxB]); + const r = centerB.subtract(centerA); + + // Repel if closer than targetRadius + // Attract if farther than targetRadius + const force = -DISTANCE_FACTOR * (r.magnitude - targetRadius); + return r.normalize().scale(force); +}; diff --git a/src/classes/supporting/edge.js b/src/classes/supporting/edge.js index b571401..37f7294 100644 --- a/src/classes/supporting/edge.js +++ b/src/classes/supporting/edge.js @@ -46,7 +46,7 @@ export class Edge { } html += ''; - return `${Edge.getCombinedKey(this)}("${html}")`; + return html; } displayEdgeNode() { diff --git a/src/classes/supporting/geometry/index.js b/src/classes/supporting/geometry/index.js new file mode 100644 index 0000000..9a37313 --- /dev/null +++ b/src/classes/supporting/geometry/index.js @@ -0,0 +1,3 @@ +export * from './vector.js'; +export * from './polygon.js'; +export * from './rectangle.js'; diff --git a/src/classes/supporting/geometry/polygon.js b/src/classes/supporting/geometry/polygon.js new file mode 100644 index 0000000..376f9d4 --- /dev/null +++ b/src/classes/supporting/geometry/polygon.js @@ -0,0 +1,18 @@ +import { Vector } from './vector.js'; + +export class Polygon { + constructor() { + this.vertices = []; + this.dim = 0; + } + + addVertex(point) { + point = point instanceof Vector ? point : Vector.from(point); + if (!this.dim) { + this.dim = point.dim; + } else if (this.dim !== point.dim) { + throw new Error('All vertices of a polygon must have the same dimensionality'); + } + this.vertices.push(point); + } +} diff --git a/src/classes/supporting/geometry/rectangle.js b/src/classes/supporting/geometry/rectangle.js new file mode 100644 index 0000000..1943b54 --- /dev/null +++ b/src/classes/supporting/geometry/rectangle.js @@ -0,0 +1,46 @@ +import { Polygon } from './polygon.js'; +import { Vector } from './vector.js'; + +export class Rectangle extends Polygon { + constructor(position, dimensions) { + super(); + if (this.vertices.length) { + throw new Error('Reinitializing geometry is not allowed'); + } + this.position = Vector.from(position); + this.dimensions = Vector.from(dimensions); + // Next point is obtained by moving the specified length along each dimension + // one at a time, then reversing these movements in the same order. + let point = this.position; + for (let dim = dimensions.length - 1; dim >= 0; dim--) { + this.addVertex(point); + const increment = Vector.unitVector(dim, dimensions.length); + point = point.add(increment); + } + for (let dim = dimensions.length - 1; dim >= 0; dim--) { + this.addVertex(point); + const increment = Vector.unitVector(dim, dimensions.length); + point = point.subtract(increment); + } + } + + get center() { + return Vector.from(this.dimensions.map((Q, idx) => this.position[idx] + Q / 2)); + } + + doesOverlap(rect) { + return this.dimensions.every((_, idx) => { + const thisMin = this.position[idx]; + const thisMax = this.position[idx] + this.dimensions[idx]; + const thatMin = rect.position[idx]; + const thatMax = rect.position[idx] + rect.dimensions[idx]; + return (thisMin <= thatMin && thisMax >= thatMin) + || (thisMin >= thatMin && thisMin <= thatMax); + }); + } + + get aspectRatio() { + const [width, height] = this.dimensions; + return height / width; + } +} diff --git a/src/classes/supporting/geometry/vector.js b/src/classes/supporting/geometry/vector.js new file mode 100644 index 0000000..09588be --- /dev/null +++ b/src/classes/supporting/geometry/vector.js @@ -0,0 +1,47 @@ +export class Vector extends Array { + get dim() { + return this.length ?? 0; + } + + add(vector) { + if (vector.dim !== this.dim) { + throw new Error('Can only add vectors of the same dimensions'); + } + return Vector.from(this.map((q, idx) => q + vector[idx])); + } + + subtract(vector) { + if (vector.dim !== this.dim) { + throw new Error('Can only subtract vectors of the same dimensions'); + } + return Vector.from(this.map((q, idx) => q - vector[idx])); + } + + static unitVector(dim, totalDim) { + return Vector.from(Array(totalDim), (_, idx) => (idx === dim ? 1 : 0)); + } + + get magnitudeSquared() { + return this.reduce((total, q) => total += q ** 2, 0); + } + + get magnitude() { + return Math.sqrt(this.magnitudeSquared); + } + + scale(factor) { + return Vector.from(this.map((q) => q * factor)); + } + + normalize() { + return this.scale(1 / this.magnitude); + } + + static randomUnitVector(totalDim) { + return Vector.from(Array(totalDim), () => Math.random()).normalize(); + } + + static zeros(totalDim) { + return Vector.from(Array(totalDim), () => 0); + } +} diff --git a/src/tests/scripts/force-directed.test.js b/src/tests/scripts/force-directed.test.js index 0ba009f..56b1600 100644 --- a/src/tests/scripts/force-directed.test.js +++ b/src/tests/scripts/force-directed.test.js @@ -1,6 +1,7 @@ import { Box } from '../../classes/display/box.js'; import { ForceDirectedGraph } from '../../classes/display/force-directed.js'; -import { Rectangle, Vector } from '../../classes/display/geometry.js'; +import { Rectangle, Vector } from '../../classes/supporting/geometry/index.js'; +import { overlapRepulsionForce, targetRadiusForce } from '../../classes/display/pairwise-forces.js'; import { delayOrWait } from '../../classes/display/scene-controls.js'; import { Scene } from '../../classes/display/scene.js'; import { EPSILON } from '../../util/constants.js'; @@ -31,13 +32,13 @@ describe('Force-Directed Graph', function tests() { rect.vertices[3].should.eql([1, 0]); }); - it('overlapping boxes should repel with default force', () => { + it('overlapping boxes should repel', () => { const rect1 = new Rectangle([0, 0], [1, 1]); const rect2 = new Rectangle([0, 0], [1, 2]); rect1.center.should.eql([0.5, 0.5]); rect2.center.should.eql([0.5, 1]); - const force1 = ForceDirectedGraph.pairwiseForce(rect1, rect2, 10); - force1.should.eql([0, 200]); + const force1 = overlapRepulsionForce(rect1, rect2, 10); + force1.should.eql([0, 10]); }); it('boxes at target radius should have no net force', () => { @@ -45,7 +46,7 @@ describe('Force-Directed Graph', function tests() { const rect2 = new Rectangle([10, 0], [1, 1]); rect1.center.should.eql([0.5, 0.5]); rect2.center.should.eql([10.5, 0.5]); - const force = ForceDirectedGraph.pairwiseForce(rect1, rect2, 10); + const force = targetRadiusForce(rect1, rect2, 10); force[0].should.be.within(-EPSILON, EPSILON); force[1].should.be.within(-EPSILON, EPSILON); }); @@ -70,12 +71,14 @@ describe('Force-Directed Graph', function tests() { await delayOrWait(1000); const v = graph.addVertex('v1', 'box2'); v.setProperty('prop', 'value'); - await graph.runUntilEquilibrium(); }); it('can add an edge to the graph', async () => { await delayOrWait(1000); graph.addEdge('e1', 'box1', 'box2', 1); + }); + + it('runs until reaching equilibrium', async () => { await graph.runUntilEquilibrium(); }); }); diff --git a/src/util/constants.js b/src/util/constants.js index d8b1aac..b366d3f 100644 --- a/src/util/constants.js +++ b/src/util/constants.js @@ -11,10 +11,12 @@ export const DEFAULT_OVERLAP_FORCE = 200; export const DEFAULT_REP_TOKEN_TYPE_ID = 0; export const DEFAULT_TARGET_RADIUS = 300; export const DEFAULT_TIME_STEP = 0.1; -export const DISTANCE_FACTOR = 0.25; +export const DISTANCE_FACTOR = 0.5; export const EPSILON = 2.23e-16; export const INCINERATOR_ADDRESS = '0'; +export const MAX_STEPS_TO_EQUILIBRIUM = 100; export const MINIMUM_FORCE = 1; export const MINIMUM_VELOCITY = 0.1; export const TIME_DILATION_FACTOR = 500; +export const TRANSLATION_VELOCITY_FACTOR = 0.2; export const VISCOSITY_FACTOR = 0.4;