mirror of
https://github.com/dbgate/dbgate
synced 2024-11-07 20:26:23 +00:00
solve overlaps alg layout
This commit is contained in:
parent
5faddc0dc8
commit
e99a6a189f
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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 => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
28
packages/web/src/utility/statusBarStore.ts
Normal file
28
packages/web/src/utility/statusBarStore.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
}
|
@ -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 '';
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user