solve overlaps alg layout

This commit is contained in:
Jan Prochazka 2022-01-20 09:13:23 +01:00
parent 5faddc0dc8
commit e99a6a189f
9 changed files with 215 additions and 122 deletions

View File

@ -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"
}
}

View File

@ -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 => {

View File

@ -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;
}

View File

@ -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));
}

View File

@ -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,
};
});
}

View File

@ -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 '';

View File

@ -1,35 +1,5 @@
<script lang="ts" context="module">
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,
};
});
}
</script>
<script lang="ts">
import _ from 'lodash';
import { writable } from 'svelte/store';
import moment from 'moment';
import { showModal } from '../modals/modalTools';
import ChooseConnectionColorModal from '../modals/ChooseConnectionColorModal.svelte';
@ -42,6 +12,7 @@
import { findCommand } from '../commands/runCommand';
import { useConnectionColor } from '../utility/useConnectionColor';
import { apiCall } from '../utility/api';
import { statusBarTabInfo } from '../utility/statusBarStore';
$: databaseName = $currentDatabase && $currentDatabase.name;
$: connection = $currentDatabase && $currentDatabase.connection;

View File

@ -2,7 +2,7 @@
import { getContext, onDestroy, onMount } from 'svelte';
import uuidv1 from 'uuid/v1';
import { updateStatuBarInfoItem } from './StatusBar.svelte';
import { updateStatuBarInfoItem } from '../utility/statusBarStore';
export let text;
export let clickable = false;

View File

@ -5177,6 +5177,11 @@ interpret@1.2.0:
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==
interval-operations@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/interval-operations/-/interval-operations-1.0.7.tgz#c935dfed6bb040064d488f542b9a2c0004d7a333"
integrity sha512-VBxXaK+DxTt7Hwr8Rhg03bgdyv5OZ0OfH3hotg9fT2jqufF3VOtvkX33n3t6jNjS8tx1jTKBt7fs++SqNd1OQg==
invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"