-
Notifications
You must be signed in to change notification settings - Fork 95
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor: add boundary generators to dual-mesh; remove MeshBuilder
There's a tradeoff between dual-mesh's ease of use and flexibility. The MeshBuilder was trying to be easy to use, but over time the various use cases didn't fit into it, so it became more convoluted. I decided to remove MeshBuilder, as it's not that useful for mapgen4 or mapgen2. Instead, dual-mesh exposes the boundary point generators and then the map generator can call those functions.
- Loading branch information
1 parent
7a6b513
commit 13a883a
Showing
9 changed files
with
304 additions
and
371 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,26 +3,46 @@ | |
* Copyright 2017, 2023 Red Blob Games <[email protected]> | ||
* License: Apache v2.0 <http://www.apache.org/licenses/LICENSE-2.0.html> | ||
* | ||
* Generate a random triangle mesh for a rectangular area, optionally | ||
* with boundary points, optionally with ghost elements. | ||
* Helper functions for building a TriangleMesh. | ||
* | ||
* The TriangleMesh constructor takes points, delaunator output, | ||
* and a count of the number of boundary points. The boundary points | ||
* must be the prefix of the points array. | ||
* | ||
* To have equally spaced points added around a rectangular boundary, | ||
* pass in a boundary with the rectangle size and the boundary | ||
* spacing. If using Poisson disc points, I recommend √2 times the | ||
* spacing used for Poisson disc. | ||
* | ||
* Recommended code structure: | ||
import {generateInteriorBoundaryPoints} from "dual-mesh/create.js"; | ||
import {TriangleMesh} from "dual-mesh/index.js"; | ||
const bounds = {left: 0, top: 0, width: 1000, height: 1000}; | ||
const spacing = 50; | ||
let points = generateInteriorBoundaryPoints(bounds, spacing); | ||
let numBoundaryPoints = points.length; | ||
let generator = new Poisson({ | ||
shape: [bounds.width, bounds.height] | ||
minDistance: spacing / Math.sqrt(2), | ||
}); | ||
for (let p of points) { generator.addPoint(p); } | ||
points = generator.fill(); | ||
let init = {points, delaunator: Delaunator.from(points), numBoundaryPoints}; | ||
init = TriangleMesh.addGhostStructure(init); | ||
let mesh = new TriangleMesh(init); | ||
*/ | ||
|
||
'use strict'; | ||
|
||
import Delaunator from 'delaunator'; | ||
import {type Point, TriangleMesh, MeshInitializer} from "./index.js"; | ||
|
||
function checkPointInequality(_init: MeshInitializer) { | ||
// 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. | ||
} | ||
import {type Point, TriangleMesh} from "./index.js"; | ||
|
||
|
||
function checkTriangleInequality({points, delaunator: {triangles, halfedges}}) { | ||
// check for skinny triangles | ||
/** Check for skinny triangles, indicating bad point selection */ | ||
export function checkTriangleInequality({points, delaunator: {triangles, halfedges}}) { | ||
const badAngleLimit = 30; | ||
let summary = new Array(badAngleLimit).fill(0); | ||
let count = 0; | ||
|
@@ -53,152 +73,87 @@ function checkTriangleInequality({points, delaunator: {triangles, halfedges}}) { | |
} | ||
|
||
|
||
function checkMeshConnectivity({points, delaunator: {triangles, halfedges}}) { | ||
// 1. make sure each side's opposite is back to itself | ||
// 2. make sure region-circulating starting from each side works | ||
let r_ghost = points.length - 1, s_out = []; | ||
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; | ||
s_out.length = 0; | ||
do { | ||
count++; s_out.push(s); | ||
s = TriangleMesh.s_next_s(halfedges[s]); | ||
if (count > 100 && triangles[s0] !== r_ghost) { | ||
console.log(`FAIL to circulate around region with start side=${s0} from region ${triangles[s0]} to ${triangles[TriangleMesh.s_next_s(s0)]}, out_s=${s_out}`); | ||
break; | ||
} | ||
} while (s !== s0); | ||
} | ||
} | ||
|
||
|
||
/* | ||
* Add vertices evenly along the boundary of the mesh | ||
* just barely inside the given boundary rectangle. | ||
/** | ||
* Add vertices evenly along the boundary of the mesh just barely | ||
* inside the given boundary rectangle. | ||
* | ||
* The boundarySpacing parameter should be roughly √2 times the | ||
* poisson disk minDistance spacing or √½ the maxDistance spacing. | ||
* | ||
* The boundarySpacing parameter should be roughly √2 | ||
* times the poisson disk minDistance spacing or √½ the | ||
* maxDistance spacing. | ||
* They need to be inside and not outside so that these points can be | ||
* used with the poisson disk libraries I commonly use. The libraries | ||
* require that all points be inside the range. | ||
* | ||
* They need to be inside and not outside so that these | ||
* points can be used with the poisson disk libraries I | ||
* commonly use. The libraries require that all points | ||
* be inside the range. | ||
* Since these points are slightly inside the boundary, the triangle | ||
* mesh will not fill the boundary. Generate exterior boundary points | ||
* if you need to fill the boundary. | ||
* | ||
* I use a *slight* curve so that the Delaunay triangulation | ||
* doesn't make long thin triangles along the boundary. | ||
* I use a *slight* curve so that the Delaunay triangulation doesn't | ||
* make long thin triangles along the boundary. | ||
*/ | ||
function addBoundaryPoints({left, top, width, height}, boundarySpacing: number): Point[] { | ||
export function generateInteriorBoundaryPoints({left, top, width, height}, boundarySpacing: number): Point[] { | ||
// https://www.redblobgames.com/x/2314-poisson-with-boundary/ | ||
const epsilon = 1e-4; | ||
const curvature = 1.0; | ||
let W = Math.ceil((width - 2 * curvature)/boundarySpacing); | ||
let H = Math.ceil((height - 2 * curvature)/boundarySpacing); | ||
let W = Math.ceil((width - 2 * curvature) / boundarySpacing); | ||
let H = Math.ceil((height - 2 * curvature) / boundarySpacing); | ||
let points = []; | ||
// Top and bottom | ||
for (let q = 0; q < W; q++) { | ||
let t = q / W; | ||
let dx = (width - 2 * curvature) * t; | ||
let dy = curvature * 4 * (t - 0.5) ** 2; | ||
points.push([left+curvature+dx, top+dy], | ||
[left+width-curvature-dx, top+height-dy]); | ||
let dy = epsilon + curvature * 4 * (t - 0.5) ** 2; | ||
points.push([left + curvature + dx, top + dy], | ||
[left + width - curvature - dx, top + height - dy]); | ||
} | ||
// Left and right | ||
for (let r = 0; r < H; r++) { | ||
let t = r / H; | ||
let dy = (height - 2 * curvature) * t; | ||
let dx = curvature * 4 * (t - 0.5) ** 2; | ||
points.push([left+dx, top+height-curvature-dy], | ||
[left+width-dx, top+curvature+dy]); | ||
let dx = epsilon + curvature * 4 * (t - 0.5) ** 2; | ||
points.push([left + dx, top + height - curvature - dy], | ||
[left + width - dx, top + curvature + dy]); | ||
} | ||
return points; | ||
} | ||
|
||
|
||
/** | ||
* Build a dual mesh from points, with ghost triangles around the exterior. | ||
* | ||
* Options: | ||
* - To have equally spaced points added around a rectangular boundary, | ||
* pass in a boundary with the rectangle size and the boundary spacing. | ||
* If using Poisson disc points, I recommend √2 times the spacing used | ||
* for Poisson disc. | ||
* | ||
* Phases: | ||
* - Add boundary points | ||
* - Add your own set of points | ||
* - Add Poisson disc points | ||
* | ||
* The mesh generator runs some sanity checks but does not correct the | ||
* generated points. | ||
* | ||
* Examples: | ||
* Add vertices evenly along the boundary of the mesh | ||
* outside the given boundary rectangle. | ||
* | ||
* Build a mesh with poisson disc points and a boundary: | ||
* The boundarySpacing parameter should be roughly √2 times the | ||
* poisson disk minDistance spacing or √½ the maxDistance spacing. | ||
* | ||
* TODO: | ||
* new MeshBuilder(options) | ||
* .appendPoints(pointsArray) | ||
* .create() | ||
* If using poisson disc selection, the interior boundary points will | ||
* be to keep the points separated and the exterior boundary points | ||
* will be to make sure the entire map area is filled. | ||
*/ | ||
export default class MeshBuilder { | ||
points: Point[]; | ||
numBoundaryRegions: number; | ||
options: {left: number, top: number, width: number, height: number}; | ||
|
||
constructor (options:any={}) { | ||
let boundaryPoints = options.boundarySpacing ? addBoundaryPoints(options.bounds, options.boundarySpacing) : []; | ||
this.points = boundaryPoints; | ||
this.numBoundaryRegions = boundaryPoints.length; | ||
this.options = options; | ||
} | ||
|
||
/** pass in a function to return a new points array; note that | ||
* if there are existing boundary points, they should be preserved */ | ||
replacePointsFn(adder: (points: Point[]) => Point[]): this { | ||
this.points = adder(this.points); | ||
return this; | ||
} | ||
|
||
/** pass in an array of new points to append to the points array */ | ||
appendPoint(newPoints: Point[]): this { | ||
this.points = this.points.concat(newPoints); | ||
return this; | ||
} | ||
|
||
/** Points will be [x, y] */ | ||
getNonBoundaryPoints(): Point[] { | ||
return this.points.slice(this.numBoundaryRegions); | ||
} | ||
|
||
/** (used for more advanced mixing of different mesh types) */ | ||
clearNonBoundaryPoints(): this { | ||
this.points.splice(this.numBoundaryRegions, this.points.length); | ||
return this; | ||
export function generateExteriorBoundaryPoints({left, top, width, height}, boundarySpacing: number): Point[] { | ||
// https://www.redblobgames.com/x/2314-poisson-with-boundary/ | ||
const curvature = 1.0; | ||
const diagonal = boundarySpacing / Math.sqrt(2); | ||
let points = []; | ||
let W = Math.ceil((width - 2 * curvature) / boundarySpacing); | ||
let H = Math.ceil((height - 2 * curvature) / boundarySpacing); | ||
// Top and bottom | ||
for (let q = 0; q < W; q++) { | ||
let t = q / W; | ||
let dx = (width - 2 * curvature) * t + boundarySpacing/2; | ||
points.push([left + dx, top - diagonal], | ||
[left + width - dx, top + height + diagonal]); | ||
} | ||
|
||
/** Build and return a TriangleMesh */ | ||
create(runChecks:boolean=false) { | ||
let init: MeshInitializer = { | ||
points: this.points, | ||
delaunator: Delaunator.from(this.points), | ||
}; | ||
|
||
// TODO: check that all bounding points are inside the bounding rectangle | ||
// TODO: check that the boundary points at the corners connect in a way that stays outside | ||
// the bounding rectangle, so that the convex hull is entirely outside the rectangle | ||
if (runChecks) { | ||
checkPointInequality(init); | ||
checkTriangleInequality(init); | ||
} | ||
|
||
let withGhost = TriangleMesh.addGhostStructure(init); | ||
withGhost.numBoundaryRegions = this.numBoundaryRegions; | ||
if (runChecks) { | ||
checkMeshConnectivity(withGhost); | ||
} | ||
|
||
let mesh = new TriangleMesh(withGhost); | ||
mesh._options = this.options; | ||
return mesh; | ||
// Left and right | ||
for (let r = 0; r < H; r++) { | ||
let t = r / H; | ||
let dy = (height - 2 * curvature) * t + boundarySpacing/2; | ||
points.push([left - diagonal, top + height - dy], | ||
[left + width + diagonal, top + dy]); | ||
} | ||
// Corners | ||
points.push([left - diagonal, top - diagonal], | ||
[left + width + diagonal, top - diagonal], | ||
[left - diagonal, top + height + diagonal], | ||
[left + width + diagonal, top + height + diagonal]); | ||
return points; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,49 +1,50 @@ | ||
import { type Point, TriangleMesh } from "./index.js"; | ||
import { type Point } from "./index.js"; | ||
/** Check for skinny triangles, indicating bad point selection */ | ||
export declare function checkTriangleInequality({ points, delaunator: { triangles, halfedges } }: { | ||
points: any; | ||
delaunator: { | ||
triangles: any; | ||
halfedges: any; | ||
}; | ||
}): void; | ||
/** | ||
* Build a dual mesh from points, with ghost triangles around the exterior. | ||
* Add vertices evenly along the boundary of the mesh just barely | ||
* inside the given boundary rectangle. | ||
* | ||
* Options: | ||
* - To have equally spaced points added around a rectangular boundary, | ||
* pass in a boundary with the rectangle size and the boundary spacing. | ||
* If using Poisson disc points, I recommend √2 times the spacing used | ||
* for Poisson disc. | ||
* The boundarySpacing parameter should be roughly √2 times the | ||
* poisson disk minDistance spacing or √½ the maxDistance spacing. | ||
* | ||
* Phases: | ||
* - Add boundary points | ||
* - Add your own set of points | ||
* - Add Poisson disc points | ||
* They need to be inside and not outside so that these points can be | ||
* used with the poisson disk libraries I commonly use. The libraries | ||
* require that all points be inside the range. | ||
* | ||
* The mesh generator runs some sanity checks but does not correct the | ||
* generated points. | ||
* Since these points are slightly inside the boundary, the triangle | ||
* mesh will not fill the boundary. Generate exterior boundary points | ||
* if you need to fill the boundary. | ||
* | ||
* Examples: | ||
* I use a *slight* curve so that the Delaunay triangulation doesn't | ||
* make long thin triangles along the boundary. | ||
*/ | ||
export declare function generateInteriorBoundaryPoints({ left, top, width, height }: { | ||
left: any; | ||
top: any; | ||
width: any; | ||
height: any; | ||
}, boundarySpacing: number): Point[]; | ||
/** | ||
* Add vertices evenly along the boundary of the mesh | ||
* outside the given boundary rectangle. | ||
* | ||
* Build a mesh with poisson disc points and a boundary: | ||
* The boundarySpacing parameter should be roughly √2 times the | ||
* poisson disk minDistance spacing or √½ the maxDistance spacing. | ||
* | ||
* TODO: | ||
* new MeshBuilder(options) | ||
* .appendPoints(pointsArray) | ||
* .create() | ||
* If using poisson disc selection, the interior boundary points will | ||
* be to keep the points separated and the exterior boundary points | ||
* will be to make sure the entire map area is filled. | ||
*/ | ||
export default class MeshBuilder { | ||
points: Point[]; | ||
numBoundaryRegions: number; | ||
options: { | ||
left: number; | ||
top: number; | ||
width: number; | ||
height: number; | ||
}; | ||
constructor(options?: any); | ||
/** pass in a function to return a new points array; note that | ||
* if there are existing boundary points, they should be preserved */ | ||
replacePointsFn(adder: (points: Point[]) => Point[]): this; | ||
/** pass in an array of new points to append to the points array */ | ||
appendPoint(newPoints: Point[]): this; | ||
/** Points will be [x, y] */ | ||
getNonBoundaryPoints(): Point[]; | ||
/** (used for more advanced mixing of different mesh types) */ | ||
clearNonBoundaryPoints(): this; | ||
/** Build and return a TriangleMesh */ | ||
create(runChecks?: boolean): TriangleMesh; | ||
} | ||
export declare function generateExteriorBoundaryPoints({ left, top, width, height }: { | ||
left: any; | ||
top: any; | ||
width: any; | ||
height: any; | ||
}, boundarySpacing: number): Point[]; |
Oops, something went wrong.