230 lines
7.6 KiB
Java
230 lines
7.6 KiB
Java
package org.atriasoft.arkon;
|
|
|
|
public class Create {
|
|
let Delaunator = require('delaunator'); // ISC licensed
|
|
let TriangleMesh = require('./');
|
|
|
|
function s_next_s(s) { return (s % 3 == 2) ? s-2 : s+1; }
|
|
|
|
|
|
function checkPointInequality({_r_vertex, _triangles, _halfedges}) {
|
|
// TODO: check for collinear vertices. Around each red point P if
|
|
// there's a point Q and R both connected to it, and the angle P→Q and
|
|
// the angle P→R are 180° apart, then there's collinearity. This would
|
|
// indicate an issue with point selection.
|
|
}
|
|
|
|
|
|
function checkTriangleInequality({_r_vertex, _triangles, _halfedges}) {
|
|
// check for skinny triangles
|
|
const badAngleLimit = 30;
|
|
let summary = new Array(badAngleLimit).fill(0);
|
|
let count = 0;
|
|
for (let s = 0; s < _triangles.length; s++) {
|
|
let r0 = _triangles[s],
|
|
r1 = _triangles[s_next_s(s)],
|
|
r2 = _triangles[s_next_s(s_next_s(s))];
|
|
let p0 = _r_vertex[r0],
|
|
p1 = _r_vertex[r1],
|
|
p2 = _r_vertex[r2];
|
|
let d0 = [p0[0]-p1[0], p0[1]-p1[1]];
|
|
let d2 = [p2[0]-p1[0], p2[1]-p1[1]];
|
|
let dotProduct = d0[0] * d2[0] + d0[1] + d2[1];
|
|
let angleDegrees = 180 / Math.PI * Math.acos(dotProduct);
|
|
if (angleDegrees < badAngleLimit) {
|
|
summary[angleDegrees|0]++;
|
|
count++;
|
|
}
|
|
}
|
|
// NOTE: a much faster test would be the ratio of the inradius to
|
|
// the circumradius, but as I'm generating these offline, I'm not
|
|
// worried about speed right now
|
|
|
|
// TODO: consider adding circumcenters of skinny triangles to the point set
|
|
if (count > 0) {
|
|
console.log(' bad angles:', summary.join(" "));
|
|
}
|
|
}
|
|
|
|
|
|
function checkMeshConnectivity({_r_vertex, _triangles, _halfedges}) {
|
|
// 1. make sure each side's opposite is back to itself
|
|
// 2. make sure region-circulating starting from each side works
|
|
let ghost_r = _r_vertex.length - 1, out_s = [];
|
|
for (let s0 = 0; s0 < _triangles.length; s0++) {
|
|
if (_halfedges[_halfedges[s0]] !== s0) {
|
|
console.log(`FAIL _halfedges[_halfedges[${s0}]] !== ${s0}`);
|
|
}
|
|
let s = s0, count = 0;
|
|
out_s.length = 0;
|
|
do {
|
|
count++; out_s.push(s);
|
|
s = s_next_s(_halfedges[s]);
|
|
if (count > 100 && _triangles[s0] !== ghost_r) {
|
|
console.log(`FAIL to circulate around region with start side=${s0} from region ${_triangles[s0]} to ${_triangles[s_next_s(s0)]}, out_s=${out_s}`);
|
|
break;
|
|
}
|
|
} while (s !== s0);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Add vertices evenly along the boundary of the mesh;
|
|
* use a slight curve so that the Delaunay triangulation
|
|
* doesn't make long thing triangles along the boundary.
|
|
* These points also prevent the Poisson disc generator
|
|
* from making uneven points near the boundary.
|
|
*/
|
|
function addBoundaryPoints(spacing, size) {
|
|
let N = Math.ceil(size/spacing);
|
|
let points = [];
|
|
for (let i = 0; i <= N; i++) {
|
|
let t = (i + 0.5) / (N + 1);
|
|
let w = size * t;
|
|
let offset = Math.pow(t - 0.5, 2);
|
|
points.push([offset, w], [size-offset, w]);
|
|
points.push([w, offset], [w, size-offset]);
|
|
}
|
|
return points;
|
|
}
|
|
|
|
|
|
function addGhostStructure({_r_vertex, _triangles, _halfedges}) {
|
|
const numSolidSides = _triangles.length;
|
|
const ghost_r = _r_vertex.length;
|
|
|
|
let numUnpairedSides = 0, firstUnpairedEdge = -1;
|
|
let r_unpaired_s = []; // seed to side
|
|
for (let s = 0; s < numSolidSides; s++) {
|
|
if (_halfedges[s] === -1) {
|
|
numUnpairedSides++;
|
|
r_unpaired_s[_triangles[s]] = s;
|
|
firstUnpairedEdge = s;
|
|
}
|
|
}
|
|
|
|
let r_newvertex = _r_vertex.concat([[500, 500]]);
|
|
let s_newstart_r = new Int32Array(numSolidSides + 3 * numUnpairedSides);
|
|
s_newstart_r.set(_triangles);
|
|
let s_newopposite_s = new Int32Array(numSolidSides + 3 * numUnpairedSides);
|
|
s_newopposite_s.set(_halfedges);
|
|
|
|
for (let i = 0, s = firstUnpairedEdge;
|
|
i < numUnpairedSides;
|
|
i++, s = r_unpaired_s[s_newstart_r[s_next_s(s)]]) {
|
|
|
|
// Construct a ghost side for s
|
|
let ghost_s = numSolidSides + 3 * i;
|
|
s_newopposite_s[s] = ghost_s;
|
|
s_newopposite_s[ghost_s] = s;
|
|
s_newstart_r[ghost_s] = s_newstart_r[s_next_s(s)];
|
|
|
|
// Construct the rest of the ghost triangle
|
|
s_newstart_r[ghost_s + 1] = s_newstart_r[s];
|
|
s_newstart_r[ghost_s + 2] = ghost_r;
|
|
let k = numSolidSides + (3 * i + 4) % (3 * numUnpairedSides);
|
|
s_newopposite_s[ghost_s + 2] = k;
|
|
s_newopposite_s[k] = ghost_s + 2;
|
|
}
|
|
|
|
return {
|
|
numSolidSides,
|
|
_r_vertex: r_newvertex,
|
|
_triangles: s_newstart_r,
|
|
_halfedges: s_newopposite_s
|
|
};
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Build a dual mesh from points, with ghost triangles around the exterior.
|
|
*
|
|
* The builder assumes 0 ≤ x < 1000, 0 ≤ y < 1000
|
|
*
|
|
* Options:
|
|
* - To have equally spaced points added around the 1000x1000 boundary,
|
|
* pass in boundarySpacing > 0 with the spacing value. If using Poisson
|
|
* disc points, I recommend 1.5 times the spacing used for Poisson disc.
|
|
*
|
|
* Phases:
|
|
* - Your own set of points
|
|
* - Poisson disc points
|
|
*
|
|
* The mesh generator runs some sanity checks but does not correct the
|
|
* generated points.
|
|
*
|
|
* Examples:
|
|
*
|
|
* Build a mesh with poisson disc points and a boundary:
|
|
*
|
|
* new MeshBuilder({boundarySpacing: 150})
|
|
* .addPoisson(Poisson, 100)
|
|
* .create()
|
|
*/
|
|
class MeshBuilder {
|
|
/** If boundarySpacing > 0 there will be a boundary added around the 1000x1000 area */
|
|
public MeshBuilder (float boundarySpacing) {
|
|
let boundaryPoints = boundarySpacing > 0 ? addBoundaryPoints(boundarySpacing, 1000) : [];
|
|
this.points = boundaryPoints;
|
|
this.numBoundaryRegions = boundaryPoints.length;
|
|
}
|
|
|
|
/** Points should be [x, y] */
|
|
addPoints(newPoints) {
|
|
for (let p of newPoints) {
|
|
this.points.push(p);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/** Points will be [x, y] */
|
|
getNonBoundaryPoints() {
|
|
return this.points.slice(this.numBoundaryRegions);
|
|
}
|
|
|
|
/** (used for more advanced mixing of different mesh types) */
|
|
clearNonBoundaryPoints() {
|
|
this.points.splice(this.numBoundaryRegions, this.points.length);
|
|
return this;
|
|
}
|
|
|
|
/** Pass in the constructor from the poisson-disk-sampling module */
|
|
addPoisson(Poisson, spacing, random=Math.random) {
|
|
let generator = new Poisson({
|
|
shape: [1000, 1000],
|
|
minDistance: spacing,
|
|
}, random);
|
|
this.points.forEach(p => generator.addPoint(p));
|
|
this.points = generator.fill();
|
|
return this;
|
|
}
|
|
|
|
/** Build and return a TriangleMesh */
|
|
create(runChecks=false) {
|
|
// TODO: use Float32Array instead of this, so that we can
|
|
// construct directly from points read in from a file
|
|
let delaunator = Delaunator.from(this.points);
|
|
let graph = {
|
|
_r_vertex: this.points,
|
|
_triangles: delaunator.triangles,
|
|
_halfedges: delaunator.halfedges
|
|
};
|
|
|
|
if (runChecks) {
|
|
checkPointInequality(graph);
|
|
checkTriangleInequality(graph);
|
|
}
|
|
|
|
graph = addGhostStructure(graph);
|
|
graph.numBoundaryRegions = this.numBoundaryRegions;
|
|
if (runChecks) {
|
|
checkMeshConnectivity(graph);
|
|
}
|
|
|
|
return new TriangleMesh(graph);
|
|
}
|
|
}
|
|
}
|