From 13a883afddc912be6c3c55bd195e5ee6b4fa6478 Mon Sep 17 00:00:00 2001 From: Amit Patel Date: Sat, 29 Apr 2023 17:47:30 -0700 Subject: [PATCH] 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. --- dual-mesh/create.ts | 229 +++++++++++++++---------------------- dual-mesh/dist/create.d.ts | 83 +++++++------- dual-mesh/dist/create.js | 199 +++++++++++++------------------- dual-mesh/dist/index.d.ts | 23 ++-- dual-mesh/dist/index.js | 15 +-- dual-mesh/index.ts | 27 +++-- dual-mesh/tests.js | 60 +++++++--- generate-points.ts | 37 ++---- mesh.ts | 2 +- 9 files changed, 304 insertions(+), 371 deletions(-) diff --git a/dual-mesh/create.ts b/dual-mesh/create.ts index c76879b..b693af7 100644 --- a/dual-mesh/create.ts +++ b/dual-mesh/create.ts @@ -3,26 +3,46 @@ * Copyright 2017, 2023 Red Blob Games * License: Apache v2.0 * - * 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; } diff --git a/dual-mesh/dist/create.d.ts b/dual-mesh/dist/create.d.ts index a0b56a9..9d1065c 100644 --- a/dual-mesh/dist/create.d.ts +++ b/dual-mesh/dist/create.d.ts @@ -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[]; diff --git a/dual-mesh/dist/create.js b/dual-mesh/dist/create.js index 50e70a9..9b66407 100644 --- a/dual-mesh/dist/create.js +++ b/dual-mesh/dist/create.js @@ -3,21 +3,42 @@ * Copyright 2017, 2023 Red Blob Games * License: Apache v2.0 * - * 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 { TriangleMesh } from "./index.js"; -function checkPointInequality(_init) { - // 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({ 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; @@ -41,138 +62,78 @@ function checkTriangleInequality({ points, delaunator: { triangles, halfedges } console.log(' bad angles:', summary.join(" ")); } } -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. * - * I use a *slight* curve so that the Delaunay triangulation - * doesn't make long thin triangles along the boundary. + * 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. */ -function addBoundaryPoints({ left, top, width, height }, boundarySpacing) { +export function generateInteriorBoundaryPoints({ left, top, width, height }, boundarySpacing) { + // 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 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; + 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; + 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; - numBoundaryRegions; - options; - constructor(options = {}) { - 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) { - this.points = adder(this.points); - return this; - } - /** pass in an array of new points to append to the points array */ - appendPoint(newPoints) { - this.points = this.points.concat(newPoints); - 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; +export function generateExteriorBoundaryPoints({ left, top, width, height }, boundarySpacing) { + // 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 = false) { - let init = { - 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; } diff --git a/dual-mesh/dist/index.d.ts b/dual-mesh/dist/index.d.ts index a3a91e7..4dfcd53 100644 --- a/dual-mesh/dist/index.d.ts +++ b/dual-mesh/dist/index.d.ts @@ -3,10 +3,20 @@ export type Delaunator = { triangles: Int32Array; halfedges: Int32Array; }; +/** + * Each initial point generates one region, where + * points.slice(0, numBoundaryPoints) are considered to be + * boundary points/regions. + * + * As created from Delaunator, the mesh has some sides without pairs. + * Optionally use TriangleMesh.addGhostStructure() to add "ghost" + * sides, triangles, and region to complete the mesh. Elements that + * aren't "ghost" are called "solid". + */ export type MeshInitializer = { points: Point[]; delaunator: Delaunator; - numBoundaryRegions?: number; + numBoundaryPoints?: number; numSolidSides?: number; }; /** @@ -31,14 +41,9 @@ export type MeshInitializer = { * r0, r1 are adjacent, there will be two sides representing the boundary, * r_begin_s and r_end_s. * - * Each side will have a pair, accessed with s_opposite_s. - * - * If created using the functions in create.js, the mesh has no - * boundaries; it wraps around the "back" using a "ghost" region. Some - * regions are marked as the boundary; these are connected to the - * ghost region. Ghost triangles and ghost sides connect these - * boundary regions to the ghost region. Elements that aren't "ghost" - * are called "solid". + * A side from p-->q will have a pair q-->p, at index + * s_opposite_s. It will be -1 if the side doesn't have a pair. + * Use addGhostStructure() to add ghost pairs to all sides. */ export declare class TriangleMesh { static t_from_s(s: number): number; diff --git a/dual-mesh/dist/index.js b/dual-mesh/dist/index.js index 30e5384..72ec1b0 100644 --- a/dual-mesh/dist/index.js +++ b/dual-mesh/dist/index.js @@ -25,14 +25,9 @@ * r0, r1 are adjacent, there will be two sides representing the boundary, * r_begin_s and r_end_s. * - * Each side will have a pair, accessed with s_opposite_s. - * - * If created using the functions in create.js, the mesh has no - * boundaries; it wraps around the "back" using a "ghost" region. Some - * regions are marked as the boundary; these are connected to the - * ghost region. Ghost triangles and ghost sides connect these - * boundary regions to the ghost region. Elements that aren't "ghost" - * are called "solid". + * A side from p-->q will have a pair q-->p, at index + * s_opposite_s. It will be -1 if the side doesn't have a pair. + * Use addGhostStructure() to add ghost pairs to all sides. */ export class TriangleMesh { static t_from_s(s) { return (s / 3) | 0; } @@ -60,7 +55,7 @@ export class TriangleMesh { constructor(init) { if ('points' in init) { // Construct a new TriangleMesh from points + delaunator data - this.numBoundaryRegions = init.numBoundaryRegions ?? 0; + this.numBoundaryRegions = init.numBoundaryPoints ?? 0; this.numSolidSides = init.numSolidSides ?? 0; this._vertex_t = []; this.update(init); @@ -164,7 +159,7 @@ export class TriangleMesh { } return { numSolidSides, - numBoundaryRegions: init.numBoundaryRegions, + numBoundaryPoints: init.numBoundaryPoints, points: newpoints, delaunator: { triangles: r_newstart_s, diff --git a/dual-mesh/index.ts b/dual-mesh/index.ts index 489d8f3..cff4a7f 100644 --- a/dual-mesh/index.ts +++ b/dual-mesh/index.ts @@ -11,10 +11,20 @@ export type Delaunator = { halfedges: Int32Array; } +/** + * Each initial point generates one region, where + * points.slice(0, numBoundaryPoints) are considered to be + * boundary points/regions. + * + * As created from Delaunator, the mesh has some sides without pairs. + * Optionally use TriangleMesh.addGhostStructure() to add "ghost" + * sides, triangles, and region to complete the mesh. Elements that + * aren't "ghost" are called "solid". + */ export type MeshInitializer = { points: Point[]; delaunator: Delaunator; - numBoundaryRegions?: number; + numBoundaryPoints?: number; numSolidSides?: number; } @@ -40,14 +50,9 @@ export type MeshInitializer = { * r0, r1 are adjacent, there will be two sides representing the boundary, * r_begin_s and r_end_s. * - * Each side will have a pair, accessed with s_opposite_s. - * - * If created using the functions in create.js, the mesh has no - * boundaries; it wraps around the "back" using a "ghost" region. Some - * regions are marked as the boundary; these are connected to the - * ghost region. Ghost triangles and ghost sides connect these - * boundary regions to the ghost region. Elements that aren't "ghost" - * are called "solid". + * A side from p-->q will have a pair q-->p, at index + * s_opposite_s. It will be -1 if the side doesn't have a pair. + * Use addGhostStructure() to add ghost pairs to all sides. */ export class TriangleMesh { static t_from_s(s: number): number { return (s/3) | 0; } @@ -80,7 +85,7 @@ export class TriangleMesh { constructor (init: MeshInitializer | TriangleMesh) { if ('points' in init) { // Construct a new TriangleMesh from points + delaunator data - this.numBoundaryRegions = init.numBoundaryRegions ?? 0; + this.numBoundaryRegions = init.numBoundaryPoints ?? 0; this.numSolidSides = init.numSolidSides ?? 0; this._vertex_t = []; this.update(init); @@ -203,7 +208,7 @@ export class TriangleMesh { return { numSolidSides, - numBoundaryRegions: init.numBoundaryRegions, + numBoundaryPoints: init.numBoundaryPoints, points: newpoints, delaunator: { triangles: r_newstart_s, diff --git a/dual-mesh/tests.js b/dual-mesh/tests.js index 89fc7a2..b7519b9 100644 --- a/dual-mesh/tests.js +++ b/dual-mesh/tests.js @@ -8,7 +8,11 @@ import Delaunator from 'delaunator'; import Poisson from 'poisson-disk-sampling'; -import MeshBuilder from "./dist/create.js"; +import {TriangleMesh} from "./dist/index.js"; +import { + generateInteriorBoundaryPoints, + checkTriangleInequality, +} from "./dist/create.js"; const Test = { count: 0, @@ -26,19 +30,49 @@ const Test = { }; +/** Check mesh connectivity for a complete mesh (with ghost elements added) */ +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); + } +} + + function testStructuralInvariants() { - let mesh = new MeshBuilder({ - bounds: {left: 0, top: 0, width: 1000, height: 1000}, - boundarySpacing: 450}) - .replacePointsFn((points) => { - let generator = new Poisson({ - shape: [1000, 1000], - minDistance: 450, - }); - for (let p of points) generator.addPoint(p); - return generator.fill(); - }) - .create(true); + const bounds = {left: 0, top: 0, width: 1000, height: 1000}; + const spacing = 450; + + 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}; + checkTriangleInequality(init); + + init = TriangleMesh.addGhostStructure(init); + checkMeshConnectivity(init); + + let mesh = new TriangleMesh(init); let s_out = []; for (let s1 = 0; s1 < mesh.numSides; s1++) { diff --git a/generate-points.ts b/generate-points.ts index c66ee14..fcb88cb 100644 --- a/generate-points.ts +++ b/generate-points.ts @@ -11,6 +11,7 @@ import Poisson from 'fast-2d-poisson-disk-sampling'; import {makeRandFloat} from "./prng.ts"; +import {generateInteriorBoundaryPoints, generateExteriorBoundaryPoints} from "./dual-mesh/create.ts"; type Point = [number, number]; type PointsData = { @@ -36,40 +37,16 @@ type PointsData = { */ export function choosePoints(seed: number, spacing: number, mountainSpacing: number): PointsData { - // First, generate both interior and exterior boundary points, using - // a double layer like I show on + // Generate both interior and exterior boundary points; see // https://www.redblobgames.com/x/2314-poisson-with-boundary/ - const epsilon = 1e-4 const boundarySpacing = spacing * Math.sqrt(2); - const left = 0, top = 0, width = 1000, height = 1000; - const curvature = 1.0; - let interiorBoundaryPoints = [], exteriorBoundaryPoints = []; - let W = Math.ceil((width - 2 * curvature) / boundarySpacing); - let H = Math.ceil((height - 2 * curvature) / boundarySpacing); - for (let q = 0; q < W; q++) { - let t = q / W; - let dx = (width - 2 * curvature) * t; - let dy = epsilon + curvature * 4 * (t - 0.5) ** 2; - interiorBoundaryPoints.push([left + curvature + dx, top + dy], [left + width - curvature - dx, top + height - dy]); - exteriorBoundaryPoints.push([left + dx + boundarySpacing/2, top - boundarySpacing/Math.sqrt(2)], - [left + width - dx - boundarySpacing/2, top + height + boundarySpacing/Math.sqrt(2)]); - } - for (let r = 0; r < H; r++) { - let t = r / H; - let dy = (height - 2 * curvature) * t; - let dx = epsilon + curvature * 4 * (t - 0.5) ** 2; - interiorBoundaryPoints.push([left + dx, top + height - curvature - dy], [left + width - dx, top + curvature + dy]); - exteriorBoundaryPoints.push([left - boundarySpacing/Math.sqrt(2), top + height - dy - boundarySpacing/2], - [left + width + boundarySpacing/Math.sqrt(2), top + dy + boundarySpacing/2]); - } - exteriorBoundaryPoints.push([left - boundarySpacing/Math.sqrt(2), top - boundarySpacing/Math.sqrt(2)], - [left + width + boundarySpacing/Math.sqrt(2), top - boundarySpacing/Math.sqrt(2)], - [left - boundarySpacing/Math.sqrt(2), top + height + boundarySpacing/Math.sqrt(2)], - [left + width + boundarySpacing/Math.sqrt(2), top + height + boundarySpacing/Math.sqrt(2)]); + const bounds = {left: 0, top: 0, width: 1000, height: 1000}; // left,top must be 0 for poisson + let interiorBoundaryPoints = generateInteriorBoundaryPoints(bounds, boundarySpacing); + let exteriorBoundaryPoints = generateExteriorBoundaryPoints(bounds, boundarySpacing); // Second, generate the mountain points, with the interior boundary points pushing mountains away let mountainPointsGenerator = new Poisson({ - shape: [width, height], + shape: [bounds.width, bounds.height], radius: mountainSpacing, tries: 30, }, makeRandFloat(seed)); @@ -79,7 +56,7 @@ export function choosePoints(seed: number, spacing: number, mountainSpacing: num // Generate the rest of the mesh points with the interior boundary points and mountain points as constraints let generator = new Poisson({ - shape: [1000, 1000], + shape: [bounds.width, bounds.height], radius: spacing, tries: 6, // NOTE: below 5 is unstable, and 5 is borderline; defaults to 30, but lower is faster }, makeRandFloat(seed)); diff --git a/mesh.ts b/mesh.ts index 23e84ea..af6f2f0 100644 --- a/mesh.ts +++ b/mesh.ts @@ -24,7 +24,7 @@ export async function makeMesh() { let meshInit: MeshInitializer = TriangleMesh.addGhostStructure({ points, delaunator: Delaunator.from(points), - numBoundaryRegions: numExteriorBoundaryPoints, + numBoundaryPoints: numExteriorBoundaryPoints, }); let mesh = new TriangleMesh(meshInit) as Mesh; console.log(`triangles = ${mesh.numTriangles} regions = ${mesh.numRegions}`);