@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
727 lines (554 loc) • 18.1 kB
JavaScript
import { vec3 } from "gl-matrix";
import { assert } from "../../../../assert.js";
import { array_push_if_unique } from "../../../../collection/array/array_push_if_unique.js";
import { noop } from "../../../../function/noop.js";
import { number_format_by_thousands } from "../../../../primitives/numbers/number_format_by_thousands.js";
import { query_edge_other_vertex } from "../query/query_edge_other_vertex.js";
import { tm_kill_only_edge } from "../tm_kill_only_edge.js";
import { tm_kill_only_face } from "../tm_kill_only_face.js";
import { tm_kill_only_vert } from "../tm_kill_only_vert.js";
import { TopoEdge } from "./TopoEdge.js";
import { TopoTriangle } from "./TopoTriangle.js";
import { TopoVertex } from "./TopoVertex.js";
export class TopoMesh {
/**
*
* @type {TopoVertex[]}
*/
vertices = [];
/**
*
* @type {Set<TopoEdge>}
* @private
*/
__edges = new Set();
/**
*
* @type {Set<TopoTriangle>}
* @private
*/
__faces = new Set();
/**
* Approximation of memory footprint of this object
* NOTE: this is highly speculative and will differ from reality depending on VM running the code as well as many other factors
* @return {number}
*/
get byteSize() {
let r = 0;
for (let i = 0; i < this.vertices.length; i++) {
const v = this.vertices[i];
r += v.byteSize;
}
for (const edge of this.__edges) {
r += edge.byteSize;
}
for (const face of this.__faces) {
r += face.byteSize;
}
return r;
}
/**
*
* @returns {Set<TopoEdge>}
*/
getEdges() {
return this.__edges;
}
/**
*
* @returns {Set<TopoTriangle>}
*/
getFaces() {
return this.__faces;
}
/**
*
* @param {number} index
* @return {undefined|TopoTriangle}
*/
getFaceByIndex(index) {
assert.isNonNegativeInteger(index, 'index');
for (const face of this.__faces) {
if (face.index === index) {
return face;
}
}
return undefined;
}
/**
*
* @param {function(reason:string)} [error_consumer]
* @returns {boolean}
*/
validate(error_consumer = noop) {
let valid = true;
let error_context = "";
let error_index = 0;
function consume_sub_error(reason) {
error_consumer(`${error_context}[${error_index}]: ${reason}`);
valid = false;
}
error_context = "Edge";
error_index = 0;
const edges = this.getEdges();
for (let edge of edges) {
edge.validate(consume_sub_error);
if (!this.containsVertex(edge.v0)) {
consume_sub_error(`Link to off-mesh vertex v0`);
}
if (!this.containsVertex(edge.v1)) {
consume_sub_error(`Link to off-mesh vertex v1`);
}
const edge_faces = edge.faces;
const edge_face_count = edge_faces.length;
for (let j = 0; j < edge_face_count; j++) {
const edge_face = edge_faces[j];
if (!this.containsFace(edge_face)) {
consume_sub_error(`Link to off-mesh face[${j}]`);
}
}
error_index++;
}
const faces = this.getFaces();
error_context = "Face";
error_index = 0;
for (let face of faces) {
face.validate(consume_sub_error);
const face_edges = face.edges;
const face_edge_count = face_edges.length;
for (let j = 0; j < face_edge_count; j++) {
const face_edge = face_edges[j];
if (!this.containsEdge(face_edge)) {
consume_sub_error(`Link to off-mesh edge[${j}]`);
}
}
const face_vertices = face.vertices;
const face_vertex_count = face_vertices.length;
for (let j = 0; j < face_vertex_count; j++) {
const face_vertex = face_vertices[j];
if (!this.containsVertex(face_vertex)) {
consume_sub_error(`Link to off-mesh vertex[${j}]`);
}
}
error_index++;
}
const vertices = this.vertices;
const vertex_count = vertices.length;
error_context = "Vertex";
for (let i = 0; i < vertex_count; i++) {
error_index = i;
const vertex = vertices[i];
vertex.validate(consume_sub_error);
const vertex_edges = vertex.edges;
const vertex_edge_count = vertex_edges.length;
for (let j = 0; j < vertex_edge_count; j++) {
const vertex_edge = vertex_edges[j];
if (!this.containsEdge(vertex_edge)) {
consume_sub_error(`Link to off-mesh edge[${j}]`);
}
}
const vertex_faces = vertex.faces;
const vertex_face_count = vertex_faces.length;
for (let j = 0; j < vertex_face_count; j++) {
const vertex_face = vertex_faces[j];
if (!this.containsFace(vertex_face)) {
consume_sub_error(`Link to off-mesh face[${j}]`);
}
}
}
return valid;
}
clone() {
const r = new TopoMesh();
r.add(this);
return r;
}
/**
*
* @returns {Map<number, TopoVertex>}
*/
buildVertexMapping() {
/**
* map vertices by index
* @type {Map<number, TopoVertex>}
*/
const vertex_map = new Map();
const this_vertices = this.vertices;
const this_vertex_count = this_vertices.length;
for (let i = 0; i < this_vertex_count; i++) {
const vertex = this_vertices[i];
vertex_map.set(vertex.index, vertex);
}
return vertex_map;
}
/**
*
* @param {TopoMesh} other
* @param {Map<number, TopoVertex>} vertex_map
*/
_addWithVertexMap(other, vertex_map) {
assert.notEqual(this, other, "can't add self");
// Ingest faces
const other_faces = other.getFaces();
for (let face of other_faces) {
this.addFaceCopy(face, vertex_map);
}
}
/**
* Will create a topologically matching copy of the given face in this Mesh
* @param {TopoTriangle} face
* @param {Map<number,TopoVertex>} vertex_map
*/
addFaceCopy(face, vertex_map) {
const f = new TopoTriangle();
// copy face normal
vec3.copy(f.normal, face.normal);
const face_vertices = face.vertices;
for (let j = 0; j < 3; j++) {
const other_vertex = face_vertices[j];
let v = vertex_map.get(other_vertex.index);
if (v === undefined) {
v = new TopoVertex();
v.index = other_vertex.index;
v.x = other_vertex.x;
v.y = other_vertex.y;
v.z = other_vertex.z;
vertex_map.set(other_vertex.index, v);
this.addVertex(v);
}
f.setVertexAt(j, v);
v.addUniqueFace(f);
}
this.addFace(f);
this.patchFaceEdges(f);
}
/**
* Ensures that face has correct edges.
* NOTE: this method is additive, it will not *NOT* remove any references
* @param {TopoTriangle} face
*/
patchFaceEdges(face) {
const f_vertices = face.vertices;
const vA = f_vertices[0];
const vB = f_vertices[1];
const vC = f_vertices[2];
// patch edges
const edge0 = this.ensureEdge(vA, vB);
const edge1 = this.ensureEdge(vB, vC);
const edge2 = this.ensureEdge(vC, vA);
edge0.addUniqueFace(face);
edge1.addUniqueFace(face);
edge2.addUniqueFace(face);
face.addUniqueEdge(edge0);
face.addUniqueEdge(edge1);
face.addUniqueEdge(edge2);
}
/**
*
* @param {TopoMesh} other
*/
add(other) {
assert.notEqual(this, other, "can't add self");
/**
* map vertices by index
* @type {Map<number, TopoVertex>}
*/
const vertex_map = this.buildVertexMapping();
this._addWithVertexMap(other, vertex_map);
}
/**
*
* @param {TopoVertex} v
*/
addVertex(v) {
assert.equal(v.isTopoVertex, true, "v.isTopoVertex !== true");
this.vertices.push(v);
}
/**
*
* @param {TopoVertex} v
*/
addUniqueVertex(v) {
assert.equal(v.isTopoVertex, true, "v.isTopoVertex !== true");
array_push_if_unique(this.vertices, v);
}
/**
* @deprecated use {@link tm_kill_only_vert}
* @param {TopoVertex} v
*/
removeVertex(v) {
throw new Error(`deprecated, use tm_kill_only_vert instead`)
}
/**
*
* @param {TopoVertex} v
* @returns {boolean}
*/
containsVertex(v) {
assert.equal(v.isTopoVertex, true, "v.isTopoVertex !== true");
return this.vertices.indexOf(v) !== -1;
}
/**
*
* @param {TopoEdge} e
*/
addEdge(e) {
assert.equal(e.isTopoEdge, true, "e.isTopoEdge !== true");
this.__edges.add(e);
}
/**
*
* @param {TopoEdge} e
*/
addUniqueEdge(e) {
assert.equal(e.isTopoEdge, true, "e.isTopoEdge !== true");
this.__edges.add(e);
}
/**
* @deprecated use {@link tm_kill_only_edge}
* @param {TopoEdge} e
*/
removeEdge(e) {
throw new Error(`deprecated, use tm_kill_only_edge instead`)
}
/**
*
* @param {TopoEdge} e
* @returns {boolean}
*/
containsEdge(e) {
assert.equal(e.isTopoEdge, true, "e.isTopoEdge !== true");
return this.__edges.has(e);
}
/**
*
* @param {TopoTriangle} f
*/
addFace(f) {
assert.equal(f.isTopoFace, true, "f.isTopoFace !== true");
// assert.arrayHasNo(this.faces, f, 'already contains this face');
// this.faces.push(f);
this.__faces.add(f);
}
/**
*
* @param {Iterable<TopoTriangle>} faces
*/
injectManyFaces(faces) {
for (let face of faces) {
this.injectFace(face);
}
}
/**
* Add face as well as it's edges and vertices
* @param {TopoTriangle} f
*/
injectFace(f) {
assert.equal(f.isTopoFace, true, "f.isTopoFace !== true");
this.addFace(f);
const edges = f.edges;
const edge_count = edges.length;
for (let i = 0; i < edge_count; i++) {
const edge = edges[i];
this.addUniqueEdge(edge);
}
const vertices = f.vertices;
const vertex_count = vertices.length;
for (let i = 0; i < vertex_count; i++) {
const vertex = vertices[i];
this.addUniqueVertex(vertex);
}
}
/**
* @deprecated use {@link tm_kill_only_face} instead
* @param {TopoTriangle} f
*/
removeFace(f) {
throw new Error(`deprecated, use tm_kill_only_face instead`)
}
/**
*
* @param {TopoTriangle} f
* @returns {boolean}
*/
containsFace(f) {
assert.equal(f.isTopoFace, true, "f.isTopoFace !== true");
return this.__faces.has(f);
}
/**
* Creates an edge between two vertices, or if one already exists - returns that
* @param {TopoVertex} a
* @param {TopoVertex} b
*
* @returns {TopoEdge} Edge connecting vertices A and B
*/
ensureEdge(a, b) {
assert.defined(a, 'a');
assert.defined(b, 'b');
assert.equal(a.isTopoVertex, true, 'a.isTopoVertex !== true');
assert.equal(b.isTopoVertex, true, 'b.isTopoVertex !== true');
// DEBUG degenerate edge check
// assert.notEqual(a, b, 'a === b, attempting to create an edge from vertex to itself');
const aEdges = a.edges;
const n = aEdges.length;
for (let i = 0; i < n; i++) {
const edge = aEdges[i];
if (query_edge_other_vertex(edge, a) === b) {
// found existing edge between A and B
return edge;
}
}
//edge doesn't exist, lets create one
const topoEdge = new TopoEdge();
topoEdge.v0 = a;
topoEdge.v1 = b;
a.edges.push(topoEdge);
b.edges.push(topoEdge);
this.__edges.add(topoEdge);
return topoEdge;
}
mergeEdges() {
const edges = this.getEdges();
for (let e0 of edges) {
const e0v0 = e0.v0;
const e0v1 = e0.v1;
for (let e1 of edges) {
if (e0 === e1) {
continue;
}
const e1v0 = e1.v0;
const e1v1 = e1.v1;
if (e0v0 === e1v0) {
if (e0v1 !== e1v1) {
//not a match
continue;
}
} else if (e0v0 === e1v1) {
if (e0v1 !== e1v0) {
//not a match
continue;
}
} else {
//not a match
continue;
}
//cut the second edge
tm_kill_only_edge(this, e1);
//absorb edge
e0.merge(e1);
}
}
}
computeEdgeSquaredLengths() {
const edges = this.getEdges();
for (let edge of edges) {
edge.computeSquaredLength();
}
}
/**
*
* @param {Float32Array} vertices
*/
setVerticesFromArray(vertices) {
assert.defined(vertices, 'vertices');
const vertex_array_size = vertices.length;
assert.equal(vertex_array_size % 3, 0, `Vertex array size must be multiple of 3, instead was ${vertex_array_size}`)
const vertex_count = vertex_array_size / 3;
//populate vertices
const topo_vertices = this.vertices;
for (let i = 0; i < vertex_count; i++) {
const i3 = i * 3;
const v = new TopoVertex();
v.index = i;
v.x = vertices[i3];
v.y = vertices[i3 + 1];
v.z = vertices[i3 + 2];
topo_vertices[i] = v;
}
}
/**
*
* @param {Uint16Array|Uint32Array|number[]} faces
*/
setFacesFromIndexArray(faces) {
const face_array_size = faces.length;
assert.equal(face_array_size % 3, 0, `Face array size must be multiple of 3, instead was ${face_array_size}`)
//populate vertices
const topo_vertices = this.vertices;
const face_count = face_array_size / 3;
// populate faces
for (let i = 0; i < face_count; i++) {
const i3 = i * 3;
const a = faces[i3];
const b = faces[i3 + 1];
const c = faces[i3 + 2];
this.createTriangle(
i,
topo_vertices[a],
topo_vertices[b],
topo_vertices[c]
);
}
}
setFacedUnindexed() {
const topo_vertices = this.vertices;
const face_count = topo_vertices.length / 3;
// populate faces
for (let i = 0; i < face_count; i++) {
const offset = i * 3;
this.createTriangle(
i,
topo_vertices[offset],
topo_vertices[offset + 1],
topo_vertices[offset + 2]
);
}
}
/**
*
* @param {Float64Array|Float32Array|number[]} vertices
* @param {Uint32Array|Uint16Array|Uint8Array|number[]} [faces]
*/
build(vertices, faces) {
this.setVerticesFromArray(vertices);
if (faces === undefined || faces === null) {
this.setFacedUnindexed();
} else {
this.setFacesFromIndexArray(faces);
}
}
/**
*
* @param {number} index
* @param {TopoVertex} v0
* @param {TopoVertex} v1
* @param {TopoVertex} v2
* @returns {TopoTriangle}
*/
createTriangle(index, v0, v1, v2) {
const f = new TopoTriangle();
f.index = index;
const e01 = this.ensureEdge(v0, v1);
const e12 = this.ensureEdge(v1, v2);
const e20 = this.ensureEdge(v2, v0);
//link primitives
e01.faces.push(f);
e12.faces.push(f);
e20.faces.push(f);
f.vertices.push(v0, v1, v2);
f.edges.push(e01, e12, e20);
v0.faces.push(f);
v1.faces.push(f);
v2.faces.push(f);
this.__faces.add(f);
return f;
}
toString() {
return `TopoMesh{ vertices: ${number_format_by_thousands(this.vertices.length)}, edges: ${number_format_by_thousands(this.getEdges().size)}, faces: ${number_format_by_thousands(this.getFaces().size)} }`;
}
}
/**
* @readonly
* @type {boolean}
*/
TopoMesh.prototype.isTopoMesh = true;