UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

727 lines (554 loc) • 18.1 kB
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;