UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

547 lines (416 loc) • 13.1 kB
/** * * @param {Array.<AABB2>} boxes */ import AABB2 from "../../../geom/2d/aabb/AABB2.js"; import Vector2 from "../../../geom/Vector2.js"; import { computeDisconnectedSubGraphs } from "../computeDisconnectedSubGraphs.js"; import { Connection } from "../Connection.js"; import { applyCentralGravityAABB2 } from "./applyCentralGravityAABB2.js"; import { resolveAABB2Overlap } from "./resolveAABB2Overlap.js"; /** * * @param {Connection} edge * @returns {number} */ function evaluateEdgeCost(edge) { const first = edge.source; const second = edge.target; //compute center points const x0 = first.midX(); const y0 = first.midY(); const x1 = second.midX(); const y1 = second.midY(); return Vector2._distance(x0, y0, x1, y1); } /** * Computes a bounding box for a group of {@link AABB2}s * @param {AABB2[]} boxes * @returns {AABB2} */ function computeBoundingBox(boxes) { const footprint = new AABB2(); footprint.setNegativelyInfiniteBounds(); const n = boxes.length; for (let i = 0; i < n; i++){ const b = boxes[i]; footprint.expandToFit(b); } return footprint; } /** * * @param {Array.<AABB2>} boxes * @param {Array.<Connection>} edges * @returns {number} */ function evaluateLayout(boxes, edges) { const footprint = computeBoundingBox(boxes); //overall size of the layout plays a role const totalArea = footprint.getHeight() * footprint.getWidth(); const totalConnectionLength = edges.reduce(function (sum, edge) { return sum + evaluateEdgeCost(edge); }, 0); return totalConnectionLength + totalArea * 0.1; } /** * * @param {AABB2} box * @returns {number} */ function evaluateBoxPositionCost(box) { let result = 0; //compute current edge cost const connections = box.connections; const n = connections.length; for (let i = 0; i < n; i++) { const connection = connections[i]; result += evaluateEdgeCost(connection); } return result; } /** * * @param {AABB2} box0 * @param {AABB2} box1 * @param {function(AABB2):number} costFunction * @returns {boolean} */ function trySwapBoxes(box0, box1, costFunction) { if (box0.locked === true || box1.locked === true) { return false; } const costBefore = costFunction(box0) + costFunction(box1); const temp = new AABB2(); temp.copy(box0); box0.copy(box1); box1.copy(temp); const costAfter = costFunction(box0) + costFunction(box1); if (costAfter > costBefore) { //revert swap box1.copy(box0); box0.copy(temp); return false; } return true; } /** * * @param {Array.<AABB2>} boxes * @returns {number} */ function applyNodeSwapPass(boxes) { let swaps = 0; const numBoxes = boxes.length; for (let i = 0; i < numBoxes; i++) { const b0 = boxes[i]; for (let j = i + 1; j < numBoxes; j++) { const b1 = boxes[j]; if (trySwapBoxes(b0, b1, evaluateBoxPositionCost)) { swaps++; } } } return swaps; } /** * * @param {Array.<AABB2>} boxes * @param {number} allowedSteps * @returns {number} */ function applyNodeSwaps(boxes, allowedSteps) { let swaps; do { swaps = applyNodeSwapPass(boxes); } while (swaps > 0 && --allowedSteps > 0); } /** * * @param {Array.<Connection>} edges * @param {number} strength */ function applyPull(edges, strength) { const d = new Vector2(); for (let i = 0, l = edges.length; i < l; i++) { const edge = edges[i]; const first = edge.source; const second = edge.target; d.set(second.midX(), second.midY()); d._sub(first.midX(), first.midY()); d.multiplyScalar(strength); //apply pull first.move(d.x, d.y); second.move(-d.x, -d.y); } } function resolveBoxConnectionOverlaps(boxes, connections, maxSteps) { for (let i = 0; i < maxSteps; i++) { const shiftsMade = resolveBoxConnectionOverlapsStep(boxes, connections); if (shiftsMade === 0) { //we're done, no changes were made break; } } } function resolveBoxConnectionOverlapsStep(boxes, connections) { const forces = new Map(); //initialize forces boxes.forEach(b => forces.set(b, new Vector2())); function addForce(box, x, y) { const force = forces.get(box); force._add(x, y); } /** * * @param {AABB2} box * @param {Connection} connection */ function resolveOverlap(box, connection) { const source = connection.source; const target = connection.target; if (box === source || box === target) { //connection is attached to the box, no overlap resolution required return false; } const p0 = new Vector2(); const p1 = new Vector2(); if (!AABB2.computeLineBetweenTwoBoxes(source, target, p0, p1)) { return false; } const intersectionPoint = new Vector2(); const intersectionExists = box.lineIntersectionPoint(p0, p1, intersectionPoint); if (!intersectionExists) { return false; } //localize intersection point const left = intersectionPoint.x - box.x0; const right = box.x1 - intersectionPoint.x; const top = intersectionPoint.y - box.y0; const bottom = box.y1 - intersectionPoint.y; //figure out smallest move necessary to resolve overlap const move = new Vector2(0, 0); if (left !== 0 && right !== 0) { if (left < right) { //move right move.x = left; } else { move.x = -right; } } else { if (top < bottom) { move.y = top; } else { move.y = -bottom; } } const dy = move.y / 2; const dx = move.x / 2; if (box.locked !== true) { addForce(box, dx, dy); } //nudge connected pair other way if (source.locked !== true) { addForce(source, -dx, -dy); } if (target.locked !== true) { addForce(target, -dx, -dy); } return true; } let overlapsResolved = 0; for (let i = 0, il = boxes.length; i < il; i++) { const box = boxes[i]; for (let j = 0, jl = connections.length; j < jl; j++) { const connection = connections[j]; if (resolveOverlap(box, connection)) { overlapsResolved++; } } } forces.forEach(function (force, box) { box.move(force.x, force.y); }); return overlapsResolved; } function applyClusteredLayoutStep(boxes) { const subGraphs = computeDisconnectedSubGraphs(boxes); const boundingBoxes = []; const boundingBoxesPositions = []; subGraphs.forEach(function (boxes, clusterIndex) { //collect edges const connections = []; boxes.forEach(function (box) { const edges = box.connections; edges.forEach(function (e) { if (connections.indexOf(e) === -1) { connections.push(e); } }); }); applyPull(connections, 0.2); applyNodeSwaps(boxes); resolveAABB2Overlap(boxes, 3); resolveBoxConnectionOverlaps(boxes, connections, 3); const bbox = computeBoundingBox(boxes); boundingBoxes[clusterIndex] = bbox; boundingBoxesPositions[clusterIndex] = new Vector2(bbox.x0, bbox.y0); }); //apply gravity to clusters as a whole applyCentralGravityAABB2(boundingBoxes, 0.3); resolveAABB2Overlap(boundingBoxes, 20); //translate original boxes boundingBoxes.forEach(function (bbox, clusterIndex) { const bboxOrigin = boundingBoxesPositions[clusterIndex]; const dX = bbox.x0 - bboxOrigin.x; const dY = bbox.y0 - bboxOrigin.y; if (dX !== 0 || dY !== 0) { //box moved const clusterBoxes = subGraphs[clusterIndex]; clusterBoxes.forEach(function (box) { box.move(dX, dY); }); } }); } function doLayerLayout(boxes) { const remaining = boxes.filter(function (b) { return b.locked !== true; }); function buildLayer() { const layer = []; main_loop:for (let i = remaining.length - 1; i >= 0; i--) { const box = remaining[i]; const connections = box.connections; for (let j = 0; j < connections.length; j++) { const connection = connections[j]; const source = connection.source; if (source !== box && remaining.indexOf(source) !== -1) { //not closed yet continue main_loop; } } remaining.splice(i, 1); //all sources are already laid out layer.push(box); } return layer; } let offsetY = 0; while (remaining.length > 0) { const layer = buildLayer(); if (layer.length === 0) { console.error("Layer is empty", remaining, boxes, offsetY); break; } let offsetX = 0; let layerHeight = 0; for (let i = 0, l = layer.length; i < l; i++) { const box = layer[i]; const y0 = box.y0; const y1 = box.y1; const x0 = box.x0; const x1 = box.x1; const h = y1 - y0; const w = x1 - x0; box.y0 = offsetY; box.y1 = offsetY + h; box.x0 = offsetX; box.x1 = offsetX + w; offsetX += w; layerHeight = Math.max(h, layerHeight); } offsetY += layerHeight; } } /** * * @param {AABB2[]} boxes * @param {Vector2} center */ export function centerAABB2CollectionOn(boxes, center) { const bBox = computeBoundingBox(boxes); const dX = bBox.centerX - center.x; const dY = bBox.centerY - center.y; boxes.forEach(function (b) { b.move(-dX, -dY); }); } function alignOnOrigin(boxes) { const bBox = computeBoundingBox(boxes); boxes.forEach(function (b) { b.move(-bBox.x0, -bBox.y0); }); } /** * * @param {Array.<AABB2>} input * @param {Graph} graph * @param {Vector2} [center] */ function forceLayout(input, graph, center = null) { if (input.length === 0) { //nothing to layout, we're done return; } const connections = []; const boxes = input.map(function (b) { const node = b.model; const box = new AABB2(b.x0, b.y0, b.x1, b.y1); box.node = node; box.source = b; box.connections = []; box.locked = b.locked === true; return box; }); graph.traverseEdges(function (edge) { const first = edge.first; const second = edge.second; const firstBox = boxes.find(function (box) { return box.node === first; }); const secondBox = boxes.find(function (box) { return box.node === second; }); const connection = new Connection(firstBox, secondBox); connection.edge = edge; firstBox.connections.push(connection); secondBox.connections.push(connection); connections.push(connection); }); let constBefore = evaluateLayout(boxes, connections); function step() { applyClusteredLayoutStep(boxes); // applyGravityClustered(boxes, 0.05); // applyPull(connections, 0.2); // applyNodeSwaps(boxes); // resolveOverlap(boxes); } console.time("layout"); doLayerLayout(boxes); for (let i = 0; i < 20; i++) { step(); } for (let i = 0; i < 10; i++) { resolveBoxConnectionOverlaps(boxes, connections, 20); resolveAABB2Overlap(boxes, 20); } console.timeEnd("layout"); const costAfter = evaluateLayout(boxes, connections); console.log("Fitness change:", constBefore - costAfter); if (center !== null) { //align on center centerAABB2CollectionOn(boxes, center); } else { //re-center boxes alignOnOrigin(boxes); } //apply layout boxes.forEach(function (b) { b.source.copy(b); }); } export { forceLayout, alignOnOrigin };