Home Reference Source

scripts/util/graphpack.js

"use strict";

import {Vector2} from "./vectormath.js";
import * as math from './math.js';
import * as util from './util.js';

import {Constraint, Solver} from './solver.js';

let idgen = 0;

export class PackNodeVertex extends Vector2 {
  constructor(node, co) {
    super(co);

    this.node = node;
    this._id = idgen++;
    this.edges = [];
    this._absPos = new Vector2();
  }

  get absPos() {
    this._absPos.load(this).add(this.node.pos);
    return this._absPos;
  }

  [Symbol.keystr]() {
    return this._id;
  }
}

export class PackNode {
  constructor() {
    this.pos = new Vector2();
    this.vel = new Vector2();
    this.oldpos = new Vector2();
    this._id = idgen++;
    this.size = new Vector2();
    this.verts = [];
  }

  [Symbol.keystr]() {
    return this._id;
  }
}

function copyGraph(nodes) {
  let ret = [];
  let idmap = {};

  for (let n of nodes) {
    let n2 = new PackNode();
    n2._id = n._id;
    n2.pos.load(n.pos);
    n2.vel.load(n.vel);
    n2.size.load(n.size);

    n2.verts = [];
    idmap[n2._id] = n2;

    for (let v of n.verts) {
      let v2 = new PackNodeVertex(n2, v);
      v2._id = v._id;
      idmap[v2._id] = v2;

      n2.verts.push(v2);
    }

    ret.push(n2);
  }

  for (let n of nodes) {
    for (let v of n.verts) {
      let v2 = idmap[v._id];

      for (let v3 of v.edges) {
        v2.edges.push(idmap[v3._id]);
      }
    }
  }

  return ret;
}

function getCenter(nodes) {
  let cent = new Vector2();

  for (let n of nodes) {
    cent.add(n.pos);
  }

  if (nodes.length === 0)
    return cent;

  cent.mulScalar(1.0 / nodes.length);

  return cent;
}

function loadGraph(nodes, copy) {
  let idmap = {};

  for (let i=0; i<nodes.length; i++) {
    nodes[i].pos.load(copy[i].pos);
    nodes[i].oldpos.load(copy[i].oldpos);
    nodes[i].vel.load(copy[i].vel);
  }
}

export function graphGetIslands(nodes) {
  let islands = [];
  let visit1 = new util.set();

  let rec = (n, island) => {
    island.push(n);
    visit1.add(n);

    for (let v of n.verts) {
      for (let e of v.edges) {
        let n2 = e.node;
        if (n2 !== n && !visit1.has(n2)) {
          rec(n2, island);
        }
      }
    }
  };

  for (let n of nodes) {
    if (visit1.has(n)) {
      continue;
    }

    let island = [];
    islands.push(island);
    rec(n, island);
  }

  return islands;
}

export function graphPack(nodes, margin=15, steps=10, updateCb=undefined) {
  let orignodes = nodes;
  nodes = copyGraph(nodes);

  for (let n of nodes) {
    n.pos[0] += (Math.random()-0.5)*5.0;
    n.pos[1] += (Math.random()-0.5)*5.0;
  }

  let nodemap = {};
  for (let n of nodes) {
    n.vel.zero();
    nodemap[n._id] = n;
    for (let v of n.verts) {
      nodemap[v._id] = v;
    }
  }

  let visit = new util.set();
  let verts = new util.set();
  let isect = [];

  let disableEdges = false;

  function edge_c(params) {
    let [v1, v2] = params;

    if (disableEdges) return 0;

    return v1.absPos.vectorDistance(v2.absPos);
  }

  let p1 = new Vector2();
  let p2 = new Vector2();
  let s1 = new Vector2();
  let s2 = new Vector2();

  function loadBoxes(n1, n2, margin1=margin) {
    p1.load(n1.pos);
    p2.load(n2.pos);
    s1.load(n1.size);
    s2.load(n2.size);

    p1.subScalar(margin1);
    p2.subScalar(margin1);
    s1.addScalar(margin1*2.0);
    s2.addScalar(margin1*2.0);
  }

  let disableArea = false;

  function area_c(params) {
    let [n1, n2] = params;

    if (disableArea)
      return 0.0;

    loadBoxes(n1, n2);

    let a1 = n1.size[0]*n1.size[1];
    let a2 = n2.size[0]*n2.size[1];

    return math.aabb_overlap_area(p1, s1, p2, s2);
    return (math.aabb_overlap_area(p1, s1, p2, s2) / (a1+a2));
  }

  let lasterr, besterr, best;
  let err;

  let islands = graphGetIslands(nodes);
  let fakeVerts = [];
  for (let island of islands) {
    let n = island[0];
    let fv = new PackNodeVertex(n);
    fakeVerts.push(fv);
  }

  let solveStep1 = (gk=1.0) => {
    let solver = new Solver();

    isect.length = 0;
    visit = new util.set();

    if (fakeVerts.length > 1) {
      for (let i=1; i<fakeVerts.length; i++) {
        let v1 = fakeVerts[0];
        let v2 = fakeVerts[i];

        let con = new Constraint("edge_c", edge_c, [v1.node.pos, v2.node.pos], [v1, v2]);
        con.k = 0.25;
        solver.add(con);
      }
    }

    for (let n1 of nodes) {
      for (let v of n1.verts) {
        verts.add(v);
        for (let v2 of v.edges) {
          //hueristic to avoid adding same constraint twice
          if (v2._id < v._id) continue;

          let con = new Constraint("edge_c", edge_c, [v.node.pos, v2.node.pos], [v, v2]);
          con.k = 1.0;
          solver.add(con);
        }
      }

      for (let n2 of nodes) {
        if (n1 === n2) continue;
        let key = Math.min(n1._id, n2._id) + ":" + Math.max(n1._id, n2._id);
        if (visit.has(key)) continue;

        loadBoxes(n1, n2);
        let area = math.aabb_overlap_area(p1, s1, p2, s2);

        if (area > 0.01) {
          isect.push([n1, n2]);
          visit.add(key);
        }
      }

      for (let [n1, n2] of isect) {
        let con = new Constraint("area_c", area_c, [n1.pos, n2.pos], [n1, n2]);
        solver.add(con);
        con.k = 1.0;
      }
    }

    return solver;
  };

  let i = 1;
  let solveStep = (gk=0.5) => {
    let solver = solveStep1();

    if (i % 40 === 0.0) {
      let c1 = getCenter(nodes);

      let rfac = 1000.0;

      if (best) loadGraph(nodes, best);

      for (let n of nodes) {
        n.pos[0] += (Math.random() - 0.5) * rfac;
        n.pos[1] += (Math.random() - 0.5) * rfac;
        n.vel.zero();
      }

      let c2 = getCenter(nodes);
      c1.sub(c2);

      for (let n of nodes) {
        n.pos.add(c1);
      }
    }

    let err = 1e17;

    for (let n of nodes) {
      n.oldpos.load(n.pos);
      n.pos.addFac(n.vel, 0.5);
    }

    disableEdges = false;
    disableArea = true;
    solver.solve(1, gk);

    //solve so boxes don't overlap
    disableEdges = true;
    disableArea = false;

    for (let j=0; j<10; j++) {
      solver = solveStep1();
      err = solver.solve(10, gk);
    }

    for (let n of nodes) {
      n.vel.load(n.pos).sub(n.oldpos);
    }

    //get error from edge constraints

    disableEdges = false;
    disableArea = true;

    err = 0.0;
    for (let con of solver.constraints) {
      err += con.evaluate(true);
    }

    disableEdges = false;
    disableArea = false;

    /*
    loadGraph(orignodes, nodes);
    if (updateCb) {
      updateCb();
    }//*/

    lasterr = err;

    let add = Math.random()*besterr*Math.exp(-i*0.1);

    if (besterr === undefined || err < besterr+add) {
      best = copyGraph(nodes);
      besterr = err;
    }

    i++;

    return err;
  };


  for (let j=0; j<steps; j++) {
    solveStep();
  }

  loadGraph(orignodes, best ? best : nodes);

  if (updateCb) {
    if (nodes._timer !== undefined) {
      window.clearInterval(nodes._timer);
    }

    nodes._timer = window.setInterval(() => {
      let time = util.time_ms();

      while (util.time_ms() - time < 50) {
        let err = solveStep();
      }

      if (cconst.DEBUG.boxPacker) {
        console.log("err", (besterr / nodes.length).toFixed(2), (lasterr / nodes.length).toFixed(2), "isects", isect.length);
      }

      if (best) loadGraph(orignodes, best);

      if (updateCb() === false) {
        clearInterval(nodes._timer);
        return;
      }
    }, 100);

    let timer = nodes._timer;

    return {
      stop : () => {
        if (best) loadGraph(nodes, best);

        window.clearInterval(timer);
        nodes._timer = undefined;
      }
    }
  }
}