diff --git a/packages/web/src/designer/Designer.svelte b/packages/web/src/designer/Designer.svelte index 47c2fb7c0..8df08f463 100644 --- a/packages/web/src/designer/Designer.svelte +++ b/packages/web/src/designer/Designer.svelte @@ -481,7 +481,7 @@ graph.initialize(); - const layout = GraphLayout.createCircle(graph).springyAlg().fixViewBox(); + const layout = GraphLayout.createCircle(graph).springyAlg().doMoveSteps().fixViewBox(); callChange(current => { return { diff --git a/packages/web/src/designer/GraphLayout.ts b/packages/web/src/designer/GraphLayout.ts index 96b9dc26b..5ab1e667f 100644 --- a/packages/web/src/designer/GraphLayout.ts +++ b/packages/web/src/designer/GraphLayout.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { rectangleDistance, Vector2D } from './designerMath'; +import { IBoxBounds, rectangleDistance, rectangleIntersectArea, Vector2D } from './designerMath'; const MIN_NODE_DISTANCE = 50; const SPRING_LENGTH = 100; @@ -7,6 +7,11 @@ const SPRINGY_STEPS = 50; const GRAVITY = 0.01; const REPULSION = 500_000; const MAX_FORCE_SIZE = 100; +const NODE_MARGIN = 20; +const MOVE_STEP = 20; +const MOVE_BIG_STEP = 70; +const MOVE_STEP_COUNT = 1000; +const MINIMAL_SCORE_BENEFIT = 1; class GraphNode { neightboors: GraphNode[] = []; @@ -69,6 +74,7 @@ class LayoutNode { right: number; top: number; bottom: number; + paddedRect: IBoxBounds; constructor(public node: GraphNode, public x: number, public y: number) { this.left = x - node.width / 2; @@ -76,6 +82,13 @@ class LayoutNode { this.right = x + node.width / 2; 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, + }; } translate(dx: number, dy: number) { @@ -83,16 +96,11 @@ class LayoutNode { } distanceTo(node: LayoutNode) { - return rectangleDistance( - this.left, - this.top, - this.right, - this.bottom, - node.left, - node.top, - node.right, - node.bottom - ); + return rectangleDistance(this, node); + } + + intersectArea(node: LayoutNode) { + return rectangleIntersectArea(this.paddedRect, node.paddedRect); } } @@ -233,8 +241,8 @@ export class GraphLayout { } fixViewBox() { - const minX = _.min(_.values(this.nodes).map(n => n.x - n.node.width / 2)); - const minY = _.min(_.values(this.nodes).map(n => n.y - n.node.height / 2)); + const minX = _.min(_.values(this.nodes).map(n => n.left)); + const minY = _.min(_.values(this.nodes).map(n => n.top)); return this.changePositions(n => n.translate(-minX + 50, -minY + 50)); } @@ -254,4 +262,72 @@ export class GraphLayout { } return res; } + + 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; + } + + 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; + + return res; + } + + tryMoveNode(node: LayoutNode): GraphLayout[] { + return [ + this.changePositions(x => (x == node ? node.translate(MOVE_STEP, 0) : x)), + this.changePositions(x => (x == node ? node.translate(-MOVE_STEP, 0) : x)), + this.changePositions(x => (x == node ? node.translate(0, MOVE_STEP) : x)), + this.changePositions(x => (x == node ? node.translate(0, -MOVE_STEP) : x)), + + this.changePositions(x => (x == node ? node.translate(MOVE_BIG_STEP, MOVE_BIG_STEP) : x)), + this.changePositions(x => (x == node ? node.translate(MOVE_BIG_STEP, -MOVE_BIG_STEP) : x)), + this.changePositions(x => (x == node ? node.translate(-MOVE_BIG_STEP, MOVE_BIG_STEP) : x)), + this.changePositions(x => (x == node ? node.translate(-MOVE_BIG_STEP, -MOVE_BIG_STEP) : x)), + ]; + } + + 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(); + for (let step = 0; step < MOVE_STEP_COUNT; step++) { + const lastRes = res; + res = res.tryMoveElement(); + const newScore = res.score(); + // console.log('SCORE, NEW SCORE', score, newScore); + if (score - newScore < MINIMAL_SCORE_BENEFIT) return lastRes; + score = newScore; + } + return res; + } } diff --git a/packages/web/src/designer/designerMath.ts b/packages/web/src/designer/designerMath.ts index b5f2b4452..ce7cf584b 100644 --- a/packages/web/src/designer/designerMath.ts +++ b/packages/web/src/designer/designerMath.ts @@ -3,7 +3,7 @@ interface IPoint { y: number; } -interface IBoxBounds { +export interface IBoxBounds { left: number; top: number; right: number; @@ -56,26 +56,26 @@ export function intersectLineBox(p1: IPoint, p2: IPoint, box: IBoxBounds): IPoin return res; } -export function rectangleDistance( - x1: number, - y1: number, - x1b: number, - y1b: number, - x2: number, - y2: number, - x2b: number, - y2b: number -) { +export function rectangleDistance(r1: IBoxBounds, r2: IBoxBounds) { function dist(x1: number, y1: number, x2: number, y2: number) { let dx = x1 - x2; let dy = y1 - y2; return Math.sqrt(dx * dx + dy * dy); } - let left = x2b < x1; - let right = x1b < x2; - let bottom = y2b < y1; - let top = y1b < y2; + const x1 = r1.left; + const y1 = r1.top; + const x1b = r1.right; + const y1b = r1.bottom; + const x2 = r2.left; + const y2 = r2.top; + const x2b = r2.right; + const y2b = r2.bottom; + + const left = x2b < x1; + const right = x1b < x2; + const bottom = y2b < y1; + const top = y1b < y2; if (top && left) return dist(x1, y1b, x2b, y2); else if (left && bottom) return dist(x1, y1, x2b, y2b); @@ -89,6 +89,12 @@ export function rectangleDistance( else return 0; } +export function rectangleIntersectArea(rect1: IBoxBounds, rect2: IBoxBounds) { + const x_overlap = Math.max(0, Math.min(rect1.right, rect2.right) - Math.max(rect1.left, rect2.left)); + const y_overlap = Math.max(0, Math.min(rect1.bottom, rect2.bottom) - Math.max(rect1.top, rect2.top)); + return x_overlap * y_overlap; +} + export class Vector2D { constructor(public x: number, public y: number) {}