diff --git a/packages/web/src/designer/Designer.svelte b/packages/web/src/designer/Designer.svelte index e26325267..dd793e8bc 100644 --- a/packages/web/src/designer/Designer.svelte +++ b/packages/web/src/designer/Designer.svelte @@ -1,4 +1,16 @@
@@ -418,7 +482,8 @@
e.preventDefault()} on:drop={handleDrop}> {#each references || [] as ref (ref.designerId)} - diff --git a/packages/web/src/designer/QueryDesigner.svelte b/packages/web/src/designer/QueryDesigner.svelte index 96dcc6de1..6c2932a01 100644 --- a/packages/web/src/designer/QueryDesigner.svelte +++ b/packages/web/src/designer/QueryDesigner.svelte @@ -14,6 +14,7 @@ useDatabaseReferences: false, allowScrollColumns: true, allowAddAllReferences: false, + canArrange: false, }} referenceComponent={QueryDesignerReference} /> diff --git a/packages/web/src/designer/SpringyAlg.ts b/packages/web/src/designer/SpringyAlg.ts new file mode 100644 index 000000000..4afd2bdce --- /dev/null +++ b/packages/web/src/designer/SpringyAlg.ts @@ -0,0 +1,530 @@ +const STIFFNESS = 400.0; +const REPULSION = 400.0; +const DAMPING = 0.5; +const MIN_ENERGY = 0.001; +const MASS = 1.0; +const EDGE_LENGTH = 10.0; +const MIN_NODE_DISTANCE = 0.5; +const NODE_DISTANCE_OVERRIDE = 0.05; +const STEP_COUNT = 1; +const TIMESTEP = 0.2; + +export interface ISpringyNodePosition { + nodeData: any; + x: number; + y: number; + nodeWidth: number; + nodeHeight: number; +} + +interface IBoundingBox { + bottomleft: Vector; + topright: Vector; +} + +class Vector { + constructor(public x: number, public y: number) {} + + static random() { + return new Vector(10.0 * (Math.random() - 0.5), 10.0 * (Math.random() - 0.5)); + } + + add(v2: Vector) { + return new Vector(this.x + v2.x, this.y + v2.y); + } + + subtract(v2: Vector) { + return new Vector(this.x - v2.x, this.y - v2.y); + } + + multiply(n: number) { + return new Vector(this.x * n, this.y * n); + } + + divide(n: number) { + return new Vector(this.x / n || 0, this.y / n || 0); // Avoid divide by zero errors.. + } + + magnitude() { + return Math.sqrt(this.x * this.x + this.y * this.y); + } + + normal(n: number) { + return new Vector(-this.y, this.x); + } + + normalise() { + return this.divide(this.magnitude()); + } +} + +class Node { + // must be set by renderer + width: number = 0; + height: number = 0; + + constructor(public id: number, public data: {}) {} +} + +class Edge { + constructor(public id: number, public source: Node, public target: Node, public data = {}) { + this.data = data !== undefined ? data : {}; + } +} + +export class SpringyGraph { + nodeSet: { [id: number]: Node } = {}; + nodes: Node[] = []; + edges: Edge[] = []; + adjacency = {}; + + nextNodeId = 0; + nextEdgeId = 0; + + addNode(node: Node) { + if (!(node.id in this.nodeSet)) { + this.nodes.push(node); + } + + this.nodeSet[node.id] = node; + + return node; + } + + addEdge(edge: Edge) { + var exists = false; + this.edges.forEach(function (e) { + if (edge.id === e.id) { + exists = true; + } + }); + + if (!exists) { + this.edges.push(edge); + } + + if (!(edge.source.id in this.adjacency)) { + this.adjacency[edge.source.id] = {}; + } + if (!(edge.target.id in this.adjacency[edge.source.id])) { + this.adjacency[edge.source.id][edge.target.id] = []; + } + + exists = false; + this.adjacency[edge.source.id][edge.target.id].forEach(function (e) { + if (edge.id === e.id) { + exists = true; + } + }); + + if (!exists) { + this.adjacency[edge.source.id][edge.target.id].push(edge); + } + + return edge; + } + + newNode(data: {}) { + var node = new Node(this.nextNodeId++, data); + this.addNode(node); + return node; + } + + newEdge(source: Node, target: Node, data: {} = {}) { + var edge = new Edge(this.nextEdgeId++, source, target, data); + this.addEdge(edge); + return edge; + } + + // find the edges from node1 to node2 + getEdges(node1: Node, node2: Node): Edge[] { + if (node1.id in this.adjacency && node2.id in this.adjacency[node1.id]) { + return this.adjacency[node1.id][node2.id]; + } + + return []; + } +} + +class ForceDirectedPoint { + velocity = new Vector(0, 0); // velocity + acceleration = new Vector(0, 0); // acceleration + + constructor(public position: Vector, public mass: number, public node: Node) {} + + applyForce(force: Vector) { + this.acceleration = this.acceleration.add(force.divide(this.mass)); + console.log('this.acceleration', this.acceleration); + } +} + +class ForceDirectedSpring { + constructor( + public point1: ForceDirectedPoint, + public point2: ForceDirectedPoint, + public length: number, + public k: number + ) {} +} + +export class ForceDirectedLayout { + nodePoints: { [id: number]: ForceDirectedPoint } = {}; // keep track of points associated with nodes + edgeSprings: { [id: number]: ForceDirectedSpring } = {}; // keep track of springs associated with edges + + // _started = false; + // _stop = false; + + constructor( + public graph: SpringyGraph, + public stiffnes: number = STIFFNESS, + public repulsion: number = REPULSION, + public damping: number = DAMPING, + public minEnergyThreshold: number = MIN_ENERGY, + public maxSpeed: number = Infinity + ) { + this.nodePoints = {}; // keep track of points associated with nodes + this.edgeSprings = {}; // keep track of springs associated with edges + } + + point(node) { + if (!(node.id in this.nodePoints)) { + var mass = node.data.mass !== undefined ? node.data.mass : MASS; + this.nodePoints[node.id] = new ForceDirectedPoint(Vector.random(), mass, node); + } + + return this.nodePoints[node.id]; + } + + spring(edge) { + if (!(edge.id in this.edgeSprings)) { + var length = edge.data.length !== undefined ? edge.data.length : EDGE_LENGTH; + + var existingSpring: ForceDirectedSpring = null; + + var from = this.graph.getEdges(edge.source, edge.target); + from.forEach(e => { + if (!existingSpring && e.id in this.edgeSprings) { + existingSpring = this.edgeSprings[e.id]; + } + }, this); + + if (existingSpring) { + return new ForceDirectedSpring(existingSpring.point1, existingSpring.point2, 0.0, 0.0); + } + + var to = this.graph.getEdges(edge.target, edge.source); + from.forEach(e => { + if (!existingSpring && e.id in this.edgeSprings) { + existingSpring = this.edgeSprings[e.id]; + } + }, this); + + if (existingSpring) { + return new ForceDirectedSpring(existingSpring.point2, existingSpring.point1, 0.0, 0.0); + } + + this.edgeSprings[edge.id] = new ForceDirectedSpring( + this.point(edge.source), + this.point(edge.target), + length, + STIFFNESS + ); + } + + return this.edgeSprings[edge.id]; + } + + // callback should accept two arguments: Node, Point + eachNode(callback: (node: Node, point: ForceDirectedPoint) => void) { + this.graph.nodes.forEach(n => { + callback(n, this.point(n)); + }); + } + + // callback should accept two arguments: Edge, Spring + eachEdge(callback: (node: Edge, spring: ForceDirectedSpring) => void) { + this.graph.edges.forEach(function (e) { + callback(e, this.spring(e)); + }); + } + + // callback should accept one argument: Spring + eachSpring(callback: (spring: ForceDirectedSpring) => void) { + this.graph.edges.forEach(e => { + callback(this.spring(e)); + }); + } + + // Physics stuff + applyCoulombsLaw() { + this.eachNode((n1, point1) => { + this.eachNode((n2, point2) => { + if (point1 !== point2) { + var d = point1.position.subtract(point2.position); + var direction = d.normalise(); + + //var distance = d.magnitude() + 0.1; // avoid massive forces at small distances (and divide by zero) + var distance = rectangle_distance( + point1.position.x - n1.width / 2, + point1.position.y - n1.height / 2, + point1.position.x + n1.width / 2, + point1.position.y + n1.height / 2, + point2.position.x - n2.width / 2, + point2.position.y - n2.height / 2, + point2.position.x + n2.width / 2, + point2.position.y + n2.height / 2 + ); + + if (distance == null) distance = NODE_DISTANCE_OVERRIDE; + else distance += MIN_NODE_DISTANCE; + + // apply force to each end point + point1.applyForce(direction.multiply(this.repulsion).divide(distance * distance * 0.5)); + point2.applyForce(direction.multiply(this.repulsion).divide(distance * distance * -0.5)); + } + }); + }); + } + + applyHookesLaw() { + this.eachSpring(spring => { + var d = spring.point2.position.subtract(spring.point1.position); // the direction of the spring + + let point1 = spring.point1; + let point2 = spring.point2; + let n1 = point1.node; + let n2 = point2.node; + var distance = rectangle_distance( + point1.position.x - n1.width / 2, + point1.position.y - n1.height / 2, + point1.position.x + n1.width / 2, + point1.position.y + n1.height / 2, + point2.position.x - n2.width / 2, + point2.position.y - n2.height / 2, + point2.position.x + n2.width / 2, + point2.position.y + n2.height / 2 + ); + + //var displacement = spring.length - d.magnitude(); + //console.log('Length', spring.length, 'distance', distance); + var displacement = spring.length - distance; + var direction = d.normalise(); + + // apply force to each end point + spring.point1.applyForce(direction.multiply(spring.k * displacement * -0.5)); + spring.point2.applyForce(direction.multiply(spring.k * displacement * 0.5)); + }); + } + + attractToCentre() { + this.eachNode((node, point) => { + var direction = point.position.multiply(-1.0); + point.applyForce(direction.multiply(this.repulsion / 50.0)); + }); + } + + updateVelocity(timestep: number) { + this.eachNode((node, point) => { + // Is this, along with updatePosition below, the only places that your + // integration code exist? + point.velocity = point.velocity.add(point.acceleration.multiply(timestep)).multiply(this.damping); + if (point.velocity.magnitude() > this.maxSpeed) { + point.velocity = point.velocity.normalise().multiply(this.maxSpeed); + } + point.acceleration = new Vector(0, 0); + }); + } + + updatePosition(timestep: number) { + this.eachNode((node, point) => { + // Same question as above; along with updateVelocity, is this all of + // your integration code? + point.position = point.position.add(point.velocity.multiply(timestep)); + }); + } + + // Calculate the total kinetic energy of the system + totalEnergy() { + var energy = 0.0; + this.eachNode((node, point) => { + var speed = point.velocity.magnitude(); + energy += 0.5 * point.mass * speed * speed; + }); + + return energy; + } + + // start(render, onRenderStop, onRenderStart) { + // var t = this; + + // if (this._started) return; + // this._started = true; + // this._stop = false; + + // if (onRenderStart !== undefined) { + // onRenderStart(); + // } + + // window.requestAnimationFrame(function step() { + // t.tick(0.03); + + // if (render !== undefined) { + // render(); + // } + + // // stop simulation when energy of the system goes below a threshold + // if (t._stop || t.totalEnergy() < t.minEnergyThreshold) { + // t._started = false; + // if (onRenderStop !== undefined) { + // onRenderStop(); + // } + // } else { + // window.requestAnimationFrame(step); + // } + // }); + // } + + // stop() { + // this._stop = true; + // } + + tick(timestep: number) { + // for(let nodeid in this.nodePoints) { + // console.log(this.nodePoints[nodeid].position); + // } + this.applyCoulombsLaw(); + this.applyHookesLaw(); + this.attractToCentre(); + this.updateVelocity(timestep); + this.updatePosition(timestep); + } + + compute(): ISpringyNodePosition[] { + for (let i = 0; i < STEP_COUNT; i += 1) { + this.tick(TIMESTEP); + } + const positions = []; + this.eachNode((node, point) => { + positions.push({ + nodeData: node.data, + x: point.position.x, + y: point.position.y, + nodeWidth: node.width, + nodeHeight: node.height, + }); + }); + return positions; + } + + // Find the nearest point to a particular position + nearest(pos: Vector) { + var min = { node: null, point: null, distance: null }; + var t = this; + this.graph.nodes.forEach(function (n) { + var point = t.point(n); + var distance = point.position.subtract(pos).magnitude(); + + if (min.distance === null || distance < min.distance) { + min = { node: n, point: point, distance: distance }; + } + }); + + return min; + } + + // // returns [bottomleft, topright] + // getBoundingBox(): IBoundingBox { + // var bottomleft = new Vector(-2, -2); + // var topright = new Vector(2, 2); + + // this.eachNode((n, point) => { + // if (point.position.x - n.width / 2 < bottomleft.x) { + // bottomleft.x = point.position.x - n.width / 2; + // } + // if (point.position.y - n.height / 2 < bottomleft.y) { + // bottomleft.y = point.position.y - n.height / 2; + // } + // if (point.position.x + n.width / 2 > topright.x) { + // topright.x = point.position.x + n.width / 2; + // } + // if (point.position.y + n.height / 2 > topright.y) { + // topright.y = point.position.y + n.height / 2; + // } + // }); + + // var padding = topright.subtract(bottomleft).multiply(0.07); // ~5% padding + + // return { bottomleft: bottomleft.subtract(padding), topright: topright.add(padding) }; + // } +} + +// export abstract class RendererBase { +// layout: ForceDirectedLayout; + +// constructor(public graph: Graph) { +// this.layout = new ForceDirectedLayout(graph); +// } + +// start() { +// this.layout.start( +// () => { +// let positions: INodePosition[] = []; + +// this.layout.eachNode((node, point) => { +// positions.push({ +// nodeData: node.data, +// x: point.position.x, +// y: point.position.y, +// nodeWidth: node.width, +// nodeHeight: node.height, +// }); +// }); + +// this.updateNodePositions(positions); +// }, +// this.onRenderStop.bind(this), +// this.onRenderStart.bind(this) +// ); +// } + +// abstract updateNodePositions(positions: INodePosition[]); +// onRenderStop() {} +// onRenderStart() {} + +// stop() { +// this.layout.stop(); +// } +// } + +function rectangle_distance( + x1: number, + y1: number, + x1b: number, + y1b: number, + x2: number, + y2: number, + x2b: number, + y2b: number +) { + 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; + + if (top && left) return dist(x1, y1b, x2b, y2); + else if (left && bottom) return dist(x1, y1, x2b, y2b); + else if (bottom && right) return dist(x1b, y1, x2, y2b); + else if (right && top) return dist(x1b, y1b, x2, y2); + else if (left) return x1 - x2b; + else if (right) return x2 - x1b; + else if (bottom) return y1 - y2b; + else if (top) return y2 - y1b; + // rectangles intersect + else return null; +} diff --git a/packages/web/src/designer/designerMath.ts b/packages/web/src/designer/designerMath.ts index 9e9ebb2ec..94b3bac3d 100644 --- a/packages/web/src/designer/designerMath.ts +++ b/packages/web/src/designer/designerMath.ts @@ -10,42 +10,6 @@ interface IBoxBounds { bottom: number; } -// export class Vector { -// constructor(public x: number, public y: number) {} - -// static random() { -// return new Vector(10.0 * (Math.random() - 0.5), 10.0 * (Math.random() - 0.5)); -// } - -// add(v2: Vector) { -// return new Vector(this.x + v2.x, this.y + v2.y); -// } - -// subtract(v2: Vector) { -// return new Vector(this.x - v2.x, this.y - v2.y); -// } - -// multiply(n: number) { -// return new Vector(this.x * n, this.y * n); -// } - -// divide(n: number) { -// return new Vector(this.x / n || 0, this.y / n || 0); // Avoid divide by zero errors.. -// } - -// magnitude() { -// return Math.sqrt(this.x * this.x + this.y * this.y); -// } - -// normal(n: number) { -// return new Vector(-this.y, this.x); -// } - -// normalise() { -// return this.divide(this.magnitude()); -// } -// } - // helpers for figuring out where to draw arrows export function intersectLineLine(p1: IPoint, p2: IPoint, p3: IPoint, p4: IPoint): IPoint { var denom = (p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y); diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index f2a1effe0..2c79d71f9 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -25,6 +25,7 @@ 'icon settings': 'mdi mdi-cog', 'icon version': 'mdi mdi-ticket-confirmation', 'icon pin': 'mdi mdi-pin', + 'icon arrange': 'mdi mdi-arrange-send-to-back', 'icon columns': 'mdi mdi-view-column', 'icon columns-outline': 'mdi mdi-view-column-outline',