diff --git a/packages/web/package.json b/packages/web/package.json
index 781b921ae..a1ba7b058 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -55,6 +55,7 @@
"uuid": "^3.4.0"
},
"dependencies": {
- "chartjs-plugin-zoom": "^1.2.0"
+ "chartjs-plugin-zoom": "^1.2.0",
+ "interval-operations": "^1.0.7"
}
}
diff --git a/packages/web/src/designer/Designer.svelte b/packages/web/src/designer/Designer.svelte
index aea2af21b..fe5059791 100644
--- a/packages/web/src/designer/Designer.svelte
+++ b/packages/web/src/designer/Designer.svelte
@@ -523,7 +523,16 @@
graph.initialize();
- const layout = GraphLayout.createCircle(graph, circleMiddle).springyAlg().doMoveSteps().fixViewBox();
+ const layout = GraphLayout
+ // initial circle layout
+ .createCircle(graph, circleMiddle)
+ // simulation with Hook's, Coulomb's and gravity law
+ .springyAlg()
+ // move nodes to avoid overlaps
+ .solveOverlaps()
+ // view box starts with [0,0]
+ .fixViewBox();
+
// layout.print();
callChange(current => {
diff --git a/packages/web/src/designer/GraphLayout.ts b/packages/web/src/designer/GraphLayout.ts
index ba78776ac..79913cc24 100644
--- a/packages/web/src/designer/GraphLayout.ts
+++ b/packages/web/src/designer/GraphLayout.ts
@@ -1,19 +1,29 @@
import _ from 'lodash';
-import { IBoxBounds, IPoint, rectangleDistance, rectangleIntersectArea, Vector2D } from './designerMath';
+import {
+ IBoxBounds,
+ IPoint,
+ rectangleDistance,
+ rectangleIntersectArea,
+ solveOverlapsInIntervalArray,
+ Vector2D,
+} from './designerMath';
+import { union, intersection } from 'interval-operations';
const MIN_NODE_DISTANCE = 50;
const SPRING_LENGTH = 100;
const SPRINGY_STEPS = 50;
const GRAVITY_X = 0.005;
const GRAVITY_Y = 0.01;
-const REPULSION = 500_000;
+// const REPULSION = 500_000;
+const REPULSION = 1000;
const MAX_FORCE_SIZE = 100;
-const NODE_MARGIN = 20;
-const MOVE_STEP = 20;
-const MOVE_BIG_STEP = 50;
-const MOVE_STEP_COUNT = 100;
-const MINIMAL_SCORE_BENEFIT = 1;
-const SCORE_ASPECT_RATIO = 1.6;
+const NODE_MARGIN = 30;
+
+// const MOVE_STEP = 20;
+// const MOVE_BIG_STEP = 50;
+// const MOVE_STEP_COUNT = 100;
+// const MINIMAL_SCORE_BENEFIT = 1;
+// const SCORE_ASPECT_RATIO = 1.6;
class GraphNode {
neightboors: GraphNode[] = [];
@@ -106,7 +116,10 @@ class LayoutNode {
right: number;
top: number;
bottom: number;
- paddedRect: IBoxBounds;
+ // paddedRect: IBoxBounds;
+
+ rangeXPadded: [number, number];
+ rangeYPadded: [number, number];
constructor(public node: GraphNode, public x: number, public y: number) {
this.left = x - node.width / 2;
@@ -115,12 +128,14 @@ class LayoutNode {
this.bottom = y + node.height / 2;
this.position = new Vector2D(x, y);
- this.paddedRect = {
- left: this.left - NODE_MARGIN,
- top: this.top - NODE_MARGIN,
- right: this.right + NODE_MARGIN,
- bottom: this.bottom + NODE_MARGIN,
- };
+ this.rangeXPadded = [this.left - NODE_MARGIN, this.right + NODE_MARGIN];
+ this.rangeYPadded = [this.top - NODE_MARGIN, this.bottom + NODE_MARGIN];
+ // this.paddedRect = {
+ // left: this.left - NODE_MARGIN,
+ // top: this.top - NODE_MARGIN,
+ // right: this.right + NODE_MARGIN,
+ // bottom: this.bottom + NODE_MARGIN,
+ // };
}
translate(dx: number, dy: number, forceMoveFixed = false) {
@@ -132,8 +147,14 @@ class LayoutNode {
return rectangleDistance(this, node);
}
- intersectArea(node: LayoutNode) {
- return rectangleIntersectArea(this.paddedRect, node.paddedRect);
+ // intersectArea(node: LayoutNode) {
+ // return rectangleIntersectArea(this.paddedRect, node.paddedRect);
+ // }
+ hasPaddedIntersect(node: LayoutNode) {
+ return !!(
+ intersection(this.rangeXPadded, node.rangeXPadded) &&
+ intersection(this.rangeYPadded, node.rangeYPadded)
+ );
}
}
@@ -309,81 +330,113 @@ export class GraphLayout {
return res;
}
- score() {
- let res = 0;
+ // score() {
+ // let res = 0;
- for (const n1 of _.values(this.nodes)) {
- for (const n2 of _.values(this.nodes)) {
- if (n1.node.designerId == n2.node.designerId) {
- continue;
- }
+ // for (const n1 of _.values(this.nodes)) {
+ // for (const n2 of _.values(this.nodes)) {
+ // if (n1.node.designerId == n2.node.designerId) {
+ // continue;
+ // }
- res += n1.intersectArea(n2);
+ // res += n1.intersectArea(n2);
+ // }
+ // }
+
+ // const minX = _.min(_.values(this.nodes).map(n => n.left));
+ // const minY = _.min(_.values(this.nodes).map(n => n.top));
+ // const maxX = _.max(_.values(this.nodes).map(n => n.right));
+ // const maxY = _.max(_.values(this.nodes).map(n => n.bottom));
+
+ // res += maxX - minX;
+ // res += (maxY - minY) * SCORE_ASPECT_RATIO;
+
+ // return res;
+ // }
+
+ // tryMoveNode(node: LayoutNode): GraphLayout[] {
+ // if (node.node.fixedPosition) return [];
+ // return [
+ // this.changePositions(x => (x == node ? node.translate(MOVE_STEP, 0) : x), false),
+ // this.changePositions(x => (x == node ? node.translate(-MOVE_STEP, 0) : x), false),
+ // this.changePositions(x => (x == node ? node.translate(0, MOVE_STEP) : x), false),
+ // this.changePositions(x => (x == node ? node.translate(0, -MOVE_STEP) : x), false),
+
+ // this.changePositions(x => (x == node ? node.translate(MOVE_BIG_STEP, MOVE_BIG_STEP) : x), false),
+ // this.changePositions(x => (x == node ? node.translate(MOVE_BIG_STEP, -MOVE_BIG_STEP) : x), false),
+ // this.changePositions(x => (x == node ? node.translate(-MOVE_BIG_STEP, MOVE_BIG_STEP) : x), false),
+ // this.changePositions(x => (x == node ? node.translate(-MOVE_BIG_STEP, -MOVE_BIG_STEP) : x), false),
+ // ];
+ // }
+
+ // tryMoveElement() {
+ // let res = null;
+ // let resScore = null;
+
+ // for (const node of _.values(this.nodes)) {
+ // for (const item of this.tryMoveNode(node)) {
+ // const score = item.score();
+ // if (resScore == null || score < resScore) {
+ // res = item;
+ // resScore = score;
+ // }
+ // }
+ // }
+
+ // return res;
+ // }
+
+ // doMoveSteps() {
+ // let res: GraphLayout = this;
+ // let score = res.score();
+ // const start = new Date().getTime();
+ // for (let step = 0; step < MOVE_STEP_COUNT; step++) {
+ // const lastRes = res;
+ // res = res.tryMoveElement();
+ // if (!res) {
+ // lastRes.fillEdges();
+ // return lastRes;
+ // }
+ // const newScore = res.score();
+ // // console.log('STEP, SCORE, NEW SCORE', step, score, newScore);
+ // if (score - newScore < MINIMAL_SCORE_BENEFIT || new Date().getTime() - start > 1000) {
+ // lastRes.fillEdges();
+ // return lastRes;
+ // }
+ // score = newScore;
+ // }
+ // res.fillEdges();
+ // return res;
+ // }
+
+ solveOverlaps(): GraphLayout {
+ const nodes = _.sortBy(_.values(this.nodes), x => x.position.magnitude());
+ const res = new GraphLayout(this.graph);
+ for (const node of nodes) {
+ const placedNodes = _.values(res.nodes);
+ if (placedNodes.find(x => x.hasPaddedIntersect(node))) {
+ // intersection found, must perform moving algorithm
+ const xIntervalArray = union(
+ ...placedNodes
+ .filter(x => intersection(x.rangeYPadded, node.rangeYPadded))
+ .map(x => x.rangeXPadded)
+ );
+
+ const yIntervalArray = union(
+ ...placedNodes
+ .filter(x => intersection(x.rangeXPadded, node.rangeXPadded))
+ .map(x => x.rangeYPadded)
+ );
+
+ const newX = solveOverlapsInIntervalArray(node.x, node.node.width, xIntervalArray as any);
+ const newY = solveOverlapsInIntervalArray(node.y, node.node.height, yIntervalArray as any);
+
+ if (newX < newY) res.nodes[node.node.designerId] = new LayoutNode(node.node, newX, node.y);
+ else res.nodes[node.node.designerId] = new LayoutNode(node.node, node.x, newY);
+ } else {
+ res.nodes[node.node.designerId] = node;
}
}
-
- const minX = _.min(_.values(this.nodes).map(n => n.left));
- const minY = _.min(_.values(this.nodes).map(n => n.top));
- const maxX = _.max(_.values(this.nodes).map(n => n.right));
- const maxY = _.max(_.values(this.nodes).map(n => n.bottom));
-
- res += maxX - minX;
- res += (maxY - minY) * SCORE_ASPECT_RATIO;
-
- return res;
- }
-
- tryMoveNode(node: LayoutNode): GraphLayout[] {
- if (node.node.fixedPosition) return [];
- return [
- this.changePositions(x => (x == node ? node.translate(MOVE_STEP, 0) : x), false),
- this.changePositions(x => (x == node ? node.translate(-MOVE_STEP, 0) : x), false),
- this.changePositions(x => (x == node ? node.translate(0, MOVE_STEP) : x), false),
- this.changePositions(x => (x == node ? node.translate(0, -MOVE_STEP) : x), false),
-
- this.changePositions(x => (x == node ? node.translate(MOVE_BIG_STEP, MOVE_BIG_STEP) : x), false),
- this.changePositions(x => (x == node ? node.translate(MOVE_BIG_STEP, -MOVE_BIG_STEP) : x), false),
- this.changePositions(x => (x == node ? node.translate(-MOVE_BIG_STEP, MOVE_BIG_STEP) : x), false),
- this.changePositions(x => (x == node ? node.translate(-MOVE_BIG_STEP, -MOVE_BIG_STEP) : x), false),
- ];
- }
-
- tryMoveElement() {
- let res = null;
- let resScore = null;
-
- for (const node of _.values(this.nodes)) {
- for (const item of this.tryMoveNode(node)) {
- const score = item.score();
- if (resScore == null || score < resScore) {
- res = item;
- resScore = score;
- }
- }
- }
-
- return res;
- }
-
- doMoveSteps() {
- let res: GraphLayout = this;
- let score = res.score();
- const start = new Date().getTime();
- for (let step = 0; step < MOVE_STEP_COUNT; step++) {
- const lastRes = res;
- res = res.tryMoveElement();
- if (!res) {
- lastRes.fillEdges();
- return lastRes;
- }
- const newScore = res.score();
- // console.log('STEP, SCORE, NEW SCORE', step, score, newScore);
- if (score - newScore < MINIMAL_SCORE_BENEFIT || new Date().getTime() - start > 1000) {
- lastRes.fillEdges();
- return lastRes;
- }
- score = newScore;
- }
res.fillEdges();
return res;
}
diff --git a/packages/web/src/designer/designerMath.ts b/packages/web/src/designer/designerMath.ts
index bcc513182..6fd2f3d18 100644
--- a/packages/web/src/designer/designerMath.ts
+++ b/packages/web/src/designer/designerMath.ts
@@ -1,3 +1,5 @@
+import { arrayDifference } from 'interval-operations';
+import _ from 'lodash';
export interface IPoint {
x: number;
y: number;
@@ -134,3 +136,27 @@ export class Vector2D {
return this.divide(this.magnitude());
}
}
+
+export function solveOverlapsInIntervalArray(position: number, size: number, usedIntervals: [number, number][]) {
+ const freeIntervals = arrayDifference([[-Infinity, Infinity]], usedIntervals) as [number, number][];
+
+ const candidates = [];
+
+ for (const interval of freeIntervals) {
+ const intervalSize = interval[1] - interval[0];
+ if (intervalSize < size) continue;
+ if (interval[1] < position) {
+ candidates.push(interval[1] - size / 2);
+ } else if (interval[0] > position) {
+ candidates.push(interval[0] - size / 2);
+ } else {
+ // position is in interval
+ let candidate = position;
+ if (candidate - size / 2 < interval[0]) candidate = interval[0] + size / 2;
+ if (candidate + size / 2 > interval[1]) candidate = interval[1] - size / 2;
+ candidates.push(candidate);
+ }
+ }
+
+ return _.minBy(candidates, x => Math.abs(x - position));
+}
diff --git a/packages/web/src/utility/statusBarStore.ts b/packages/web/src/utility/statusBarStore.ts
new file mode 100644
index 000000000..59bba75b3
--- /dev/null
+++ b/packages/web/src/utility/statusBarStore.ts
@@ -0,0 +1,28 @@
+import { writable } from "svelte/store";
+
+export const statusBarTabInfo = writable({});
+
+// export function updateStatuBarInfo(tabid, info) {
+// statusBarTabInfo.update(x => ({
+// ...x,
+// [tabid]: info,
+// }));
+// }
+
+export function updateStatuBarInfoItem(tabid, key, item) {
+ statusBarTabInfo.update(tabs => {
+ const items = tabs[tabid] || [];
+ let newItems;
+ if (item == null) {
+ newItems = items.filter(x => x.key != key);
+ } else if (items.find(x => x.key == key)) {
+ newItems = items.map(x => (x.key == key ? { ...item, key } : x));
+ } else {
+ newItems = [...items, { ...item, key }];
+ }
+ return {
+ ...tabs,
+ [tabid]: newItems,
+ };
+ });
+}
diff --git a/packages/web/src/utility/useTimerLabel.ts b/packages/web/src/utility/useTimerLabel.ts
index 9eb2d2221..b0cf98329 100644
--- a/packages/web/src/utility/useTimerLabel.ts
+++ b/packages/web/src/utility/useTimerLabel.ts
@@ -1,6 +1,6 @@
import _ from 'lodash';
import { getContext, onDestroy } from 'svelte';
-import { updateStatuBarInfoItem } from '../widgets/StatusBar.svelte';
+import { updateStatuBarInfoItem } from './statusBarStore';
function formatSeconds(duration) {
if (duration == null) return '';
diff --git a/packages/web/src/widgets/StatusBar.svelte b/packages/web/src/widgets/StatusBar.svelte
index 70e35042a..5e640d7ca 100644
--- a/packages/web/src/widgets/StatusBar.svelte
+++ b/packages/web/src/widgets/StatusBar.svelte
@@ -1,35 +1,5 @@
-
-