diff --git a/examples/viewer/index.html b/examples/viewer/index.html index 3af3f1253..92e31e970 100644 --- a/examples/viewer/index.html +++ b/examples/viewer/index.html @@ -24,6 +24,7 @@ + Log Level: Off Error diff --git a/examples/viewer/web-ifc-three.ts b/examples/viewer/web-ifc-three.ts index 4168903d9..13039a281 100644 --- a/examples/viewer/web-ifc-three.ts +++ b/examples/viewer/web-ifc-three.ts @@ -23,7 +23,7 @@ export class IfcThree * @scene Threejs Scene object * @modelID Model handle retrieved by OpenModel, model must not be closed */ - public LoadAllGeometry(scene: THREE.Scene, modelID: number) { + public LoadAllGeometry(scene: THREE.Scene, modelID: number, repair: boolean = false) { const startUploadingTime = ms(); @@ -40,6 +40,14 @@ export class IfcThree let geometries = []; let transparentGeometries = []; + // Repair stats accumulator (only used if repair=true). + let repairTotals = { + processed: 0, + flipped: 0, + componentsLowConfidence: 0, + worstMargin: 0, + }; + this.ifcAPI.StreamAllMeshes(modelID, (mesh: FlatMesh) => { // only during the lifetime of this function call, the geometry is available in memory const placedGeometries = mesh.geometries; @@ -47,7 +55,7 @@ export class IfcThree for (let i = 0; i < placedGeometries.size(); i++) { const placedGeometry = placedGeometries.get(i); - let mesh = this.getPlacedGeometry(modelID, placedGeometry); + let mesh = this.getPlacedGeometry(modelID, placedGeometry, repair, repairTotals); let geom = mesh.geometry.applyMatrix4(mesh.matrix); if (placedGeometry.color.w !== 1) { @@ -62,11 +70,20 @@ export class IfcThree //console.log(this.ifcAPI.wasmModule.HEAPU8.length); }); + if (repair) { + console.log( + `[RepairFaces] processed ${repairTotals.processed} geometries, ` + + `flipped ${repairTotals.flipped} triangles, ` + + `${repairTotals.componentsLowConfidence} low-confidence components, ` + + `worst margin ${repairTotals.worstMargin.toFixed(4)}` + ); + } + console.log("Loading "+geometries.length+" geometries and "+transparentGeometries.length+" transparent geometries"); if (geometries.length > 0) { const combinedGeometry = BufferGeometryUtils.mergeGeometries(geometries); - let mat = new THREE.MeshPhongMaterial({side:THREE.DoubleSide}); + let mat = new THREE.MeshPhongMaterial({side:THREE.FrontSide}); mat.vertexColors = true; const mergedMesh = new THREE.Mesh(combinedGeometry, mat); scene.add(mergedMesh); @@ -75,7 +92,7 @@ export class IfcThree if (transparentGeometries.length > 0) { const combinedGeometryTransp = BufferGeometryUtils.mergeGeometries(transparentGeometries); - let matTransp = new THREE.MeshPhongMaterial({side:THREE.DoubleSide}); + let matTransp = new THREE.MeshPhongMaterial({side:THREE.FrontSide}); matTransp.vertexColors = true; matTransp.transparent = true; matTransp.opacity = 0.5; @@ -96,8 +113,13 @@ export class IfcThree return flatMeshes; } - private getPlacedGeometry(modelID: number, placedGeometry: PlacedGeometry) { - const geometry = this.getBufferGeometry(modelID, placedGeometry); + private getPlacedGeometry( + modelID: number, + placedGeometry: PlacedGeometry, + repair: boolean = false, + repairTotals?: { processed: number; flipped: number; componentsLowConfidence: number; worstMargin: number; } + ) { + const geometry = this.getBufferGeometry(modelID, placedGeometry, repair, repairTotals); const material = this.getMeshMaterial(placedGeometry.color); const mesh = new THREE.Mesh(geometry, material); mesh.matrix = this.getMeshMatrix(placedGeometry.flatTransformation); @@ -105,16 +127,40 @@ export class IfcThree return mesh; } - private getBufferGeometry(modelID: number, placedGeometry: PlacedGeometry) { - // WARNING: geometry must be deleted when requested from WASM - const geometry = this.ifcAPI.GetGeometry(modelID, placedGeometry.geometryExpressID); - const verts = this.ifcAPI.GetVertexArray(geometry.GetVertexData(), geometry.GetVertexDataSize()); - const indices = this.ifcAPI.GetIndexArray(geometry.GetIndexData(), geometry.GetIndexDataSize()); - const bufferGeometry = this.ifcGeometryToBuffer(placedGeometry.color, verts, indices); - - //@ts-ignore - geometry.delete(); - return bufferGeometry; + private getBufferGeometry( + modelID: number, + placedGeometry: PlacedGeometry, + repair: boolean = false, + repairTotals?: { processed: number; flipped: number; componentsLowConfidence: number; worstMargin: number; } + ) { + let verts: Float32Array; + let indices: Uint32Array; + + if (repair) { + // Repaired path: copy of the geometry with winding fixed and + // normals regenerated. Original cached geometry is untouched. + const repaired = this.ifcAPI.GetRepairedMesh(modelID, placedGeometry.geometryExpressID); + verts = repaired.vertexData; + indices = repaired.indexData; + + if (repairTotals) { + repairTotals.processed += 1; + repairTotals.flipped += repaired.stats.trianglesFlipped; + if (!repaired.stats.confident) repairTotals.componentsLowConfidence += 1; + if (repaired.stats.maxVoteMargin > repairTotals.worstMargin) { + repairTotals.worstMargin = repaired.stats.maxVoteMargin; + } + } + } else { + // WARNING: geometry must be deleted when requested from WASM + const geometry = this.ifcAPI.GetGeometry(modelID, placedGeometry.geometryExpressID); + verts = this.ifcAPI.GetVertexArray(geometry.GetVertexData(), geometry.GetVertexDataSize()); + indices = this.ifcAPI.GetIndexArray(geometry.GetIndexData(), geometry.GetIndexDataSize()); + //@ts-ignore + geometry.delete(); + } + + return this.ifcGeometryToBuffer(placedGeometry.color, verts, indices); } private materials = {}; @@ -127,7 +173,7 @@ export class IfcThree } const col = new THREE.Color(color.x, color.y, color.z); - const material = new THREE.MeshPhongMaterial({ color: col, side: THREE.DoubleSide }); + const material = new THREE.MeshPhongMaterial({ color: col, side: THREE.FrontSide }); material.transparent = color.w !== 1; if (material.transparent) material.opacity = color.w; diff --git a/examples/viewer/web-ifc-viewer.ts b/examples/viewer/web-ifc-viewer.ts index 789796398..d2616d4a3 100644 --- a/examples/viewer/web-ifc-viewer.ts +++ b/examples/viewer/web-ifc-viewer.ts @@ -30,6 +30,10 @@ let ifcAPI = new IfcAPI(); ifcAPI.SetWasmPath("./"); let ifcThree = new IfcThree(ifcAPI); +// Keeps the most recently loaded model open so the "Repair Faces" button +// can re-stream its geometry with orientation repair enabled. +let lastLoadedModelID: number | undefined = undefined; + let timeout = undefined; function Edited(monacoEditor: Monaco.editor.IStandaloneCodeEditor) { @@ -103,6 +107,8 @@ if (typeof window != "undefined") { coderun.addEventListener("click", runCode); const clearmem = document.getElementById("cmem"); clearmem.addEventListener("click", clearMem); + const repairFacesBtn = document.getElementById("repairfaces"); + repairFacesBtn.addEventListener("click", repairFaces); const changeLogLevelSelect = document.getElementById("logLevel"); changeLogLevelSelect.addEventListener("change", changeLogLevel); Init3DView(); @@ -169,9 +175,27 @@ async function resetCode() { async function clearMem() { ClearScene(); ifcAPI.Dispose(); + lastLoadedModelID = undefined; await ifcAPI.Init(); } +async function repairFaces() { + if (lastLoadedModelID === undefined) { + console.warn("[RepairFaces] no model loaded yet."); + return; + } + if (!ifcAPI.IsModelOpen(lastLoadedModelID)) { + console.warn("[RepairFaces] model is no longer open."); + return; + } + + const t0 = ms(); + ClearScene(); + InitBasicScene(); + ifcThree.LoadAllGeometry(scene, lastLoadedModelID, true); + console.log(`[RepairFaces] rebuilt scene in ${ms() - t0} ms.`); +} + async function fileInputChanged() { let fileInput = document.getElementById("finput"); if (fileInput.files.length == 0) return console.log("No files selected!"); @@ -225,6 +249,13 @@ async function LoadModel(data: Uint8Array) { // Ferroflex, samMateo -> CIRCLE_SEGMENTS: 6 const time = ms() - start; console.log(`Opening model took ${time} ms`); + + // Close any previously-loaded model so only one stays open at a time. + if (lastLoadedModelID !== undefined && lastLoadedModelID !== modelID) { + try { ifcAPI.CloseModel(lastLoadedModelID); } catch (_) {} + } + lastLoadedModelID = modelID; + ifcThree.LoadAllGeometry(scene, modelID); if ( @@ -265,5 +296,8 @@ async function LoadModel(data: Uint8Array) { //console.log(ifcAPI.GetLine(modelID, unitList.Units[u].value)); } } - ifcAPI.CloseModel(modelID); + // NOTE: the model is intentionally kept open so the "Repair Faces" button + // can re-stream geometry with orientation repair applied. It will be + // closed automatically when another model is loaded, or by the + // "Clear Memory" button (ifcAPI.Dispose()). } diff --git a/package.json b/package.json index 307a8ea9a..701edb82a 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "build-release": "npm run build-wasm-release && npm run build-api && npm run build-cleanup", "build-cleanup": "rimraf dist/helpers/log.ts && rimraf dist/helpers/properties.ts && rimraf dist/web-ifc-api.ts && rimraf dist/ifc-schema.ts", "build-debug": "npm run build-wasm-debug && npm run build-api && npm run build-cleanup", - "copy-to-dist": "make-dir dist && cpy \"src/cpp/build_wasm/*.js\" dist && cpy \"src/cpp/build_wasm/*.wasm\" dist && cp ./LICENSE.md ./dist", + "copy-to-dist": "make-dir dist && cpy \"src/cpp/build_wasm/*.js\" dist && cpy \"src/cpp/build_wasm/*.wasm\" dist && cpy ./LICENSE.md ./dist", "copy-debug-to-dist": "make-dir dist && cpy \"src/cpp/build_wasm_debug/*.js\" dist && cpy \"src/cpp/build_wasm_debug/*.wasm\" dist ", "build-wasm-debug": "make-dir src/cpp/build_wasm_debug && cd src/cpp/build_wasm_debug && emcmake cmake .. -DEMSCRIPTEN=true -DCMAKE_BUILD_TYPE=Debug && emmake make && npm run copy-debug-to-dist", "build-wasm-release": "make-dir src/cpp/build_wasm && cd src/cpp/build_wasm && emcmake cmake .. -DEMSCRIPTEN=true -DCMAKE_BUILD_TYPE=Release && emmake make && npm run copy-to-dist", diff --git a/src/cpp/wasm/web-ifc-wasm.cpp b/src/cpp/wasm/web-ifc-wasm.cpp index 93e72ce30..1af16727c 100644 --- a/src/cpp/wasm/web-ifc-wasm.cpp +++ b/src/cpp/wasm/web-ifc-wasm.cpp @@ -24,6 +24,7 @@ #include "../web-ifc/geometry/operations/bim-geometry/utils.h" #include "../web-ifc/geometry/operations/bim-geometry/boolean.h" #include "../web-ifc/geometry/operations/bim-geometry/profile.h" +#include "../web-ifc/geometry/operations/mesh-orientation-repair.h" namespace webifc::parsing { @@ -91,6 +92,23 @@ webifc::geometry::IfcFlatMesh GetFlatMesh(uint32_t modelID, uint32_t expressID) return mesh; } +webifc::geometry::IfcGeometryProcessor::IfcBooleanOperands GetBooleanOperands(uint32_t modelID, uint32_t expressID) +{ + if (!manager.IsModelOpen(modelID)) + return {}; + auto geomProc = manager.GetGeometryProcessor(modelID); + auto result = geomProc->GetBooleanOperands(expressID); + + for (auto &geom : result.bodyMesh.geometries) + geomProc->GetGeometry(geom.geometryExpressID).GetVertexData(); + + for (auto &voidMesh : result.voidMeshes) + for (auto &geom : voidMesh.geometries) + geomProc->GetGeometry(geom.geometryExpressID).GetVertexData(); + + return result; +} + void StreamMeshes(uint32_t modelID, const std::vector &expressIds, emscripten::val callback) { if (!manager.IsModelOpen(modelID)) @@ -944,6 +962,95 @@ bimGeometry::Profile CreateProfile() return bimGeometry::Profile(); } +// --------------------------------------------------------------------------- +// Mesh orientation repair (Layer 1 voting + Layer 2 global orientation) +// --------------------------------------------------------------------------- +// +// These helpers are intentionally decoupled from the geometry pipeline. They +// operate on the FINAL mesh (after booleans) and are called explicitly by +// the user. See mesh-orientation-repair.h for the full algorithm contract. + +// Struct carried back to JS. Mirrors webifc::geometry::OrientationRepairResult +// but with plain fields that Embind can serialize as a value_object. +struct RepairedMesh +{ + // Geometry buffers after repair (vertex positions unchanged; normals + // regenerated; index winding flipped where needed). + std::vector fvertexData; + std::vector indexData; + + // Quantitative confidence signals. + uint32_t trianglesFlipped = 0; + uint32_t componentsProcessed = 0; + double maxVoteMargin = 0.0; + uint32_t unsatisfiedEdges = 0; + bool layer2Decisive = true; + bool confident = true; +}; + +static RepairedMesh BuildRepairedMeshFromBuffers( + std::vector& vertexData, + std::vector& indexData) +{ + webifc::geometry::OrientationRepairOptions options; // defaults + webifc::geometry::OrientationRepairResult stats = + webifc::geometry::RepairMeshOrientation(vertexData, indexData, options); + + RepairedMesh out; + // Convert the double vertex buffer to float (same convention as + // IfcGeometry::GetVertexData()). + out.fvertexData.resize(vertexData.size()); + for (size_t i = 0; i < vertexData.size(); ++i) + out.fvertexData[i] = static_cast(vertexData[i]); + + out.indexData = std::move(indexData); + out.trianglesFlipped = stats.trianglesFlipped; + out.componentsProcessed = stats.componentsProcessed; + out.maxVoteMargin = stats.maxVoteMargin; + out.unsatisfiedEdges = stats.unsatisfiedEdges; + out.layer2Decisive = stats.layer2Decisive; + out.confident = stats.confident; + return out; +} + +// Option B: repair from a geometryExpressID held by the model. +// The model's own IfcGeometry is NOT modified -- we copy its buffers first. +RepairedMesh GetRepairedMesh(uint32_t modelID, uint32_t geometryExpressID) +{ + if (!manager.IsModelOpen(modelID)) + return RepairedMesh{}; + + webifc::geometry::IfcGeometry& geom = + manager.GetGeometryProcessor(modelID)->GetGeometry(geometryExpressID); + + // Copy buffers so the in-place repair does not mutate the cached geometry. + std::vector verts = geom.vertexData; + std::vector idx = geom.indexData; + + return BuildRepairedMeshFromBuffers(verts, idx); +} + +// Option C: repair from raw arrays provided by the caller (no IFC model +// involved). Input vertices are float (3 positions + 3 normals, interleaved) +// because that's what the TS side already has as Float32Array / Uint32Array. +RepairedMesh RepairMeshOrientationRaw( + emscripten::val vertexDataVal, + emscripten::val indexDataVal) +{ + const uint32_t vlen = vertexDataVal["length"].as(); + const uint32_t ilen = indexDataVal["length"].as(); + + std::vector verts(vlen); + std::vector idx(ilen); + + for (uint32_t i = 0; i < vlen; ++i) + verts[i] = vertexDataVal[i].as(); + for (uint32_t i = 0; i < ilen; ++i) + idx[i] = indexDataVal[i].as(); + + return BuildRepairedMeshFromBuffers(verts, idx); +} + EMSCRIPTEN_BINDINGS(my_module) { @@ -1007,6 +1114,13 @@ EMSCRIPTEN_BINDINGS(my_module) .field("expressID", &webifc::geometry::IfcFlatMesh::expressID); emscripten::register_vector("IfcFlatMeshVector"); + + emscripten::value_object("IfcBooleanOperands") + .field("expressID", &webifc::geometry::IfcGeometryProcessor::IfcBooleanOperands::expressID) + .field("bodyMesh", &webifc::geometry::IfcGeometryProcessor::IfcBooleanOperands::bodyMesh) + .field("voidMeshes", &webifc::geometry::IfcGeometryProcessor::IfcBooleanOperands::voidMeshes) + .field("hasBooleanOp", &webifc::geometry::IfcGeometryProcessor::IfcBooleanOperands::hasBooleans); + emscripten::register_vector("UintVector"); emscripten::value_object("SweptDiskSolid") @@ -1070,6 +1184,16 @@ EMSCRIPTEN_BINDINGS(my_module) emscripten::register_vector("vector"); + emscripten::value_object("RepairedMesh") + .field("fvertexData", &RepairedMesh::fvertexData) + .field("indexData", &RepairedMesh::indexData) + .field("trianglesFlipped", &RepairedMesh::trianglesFlipped) + .field("componentsProcessed", &RepairedMesh::componentsProcessed) + .field("maxVoteMargin", &RepairedMesh::maxVoteMargin) + .field("unsatisfiedEdges", &RepairedMesh::unsatisfiedEdges) + .field("layer2Decisive", &RepairedMesh::layer2Decisive) + .field("confident", &RepairedMesh::confident); + emscripten::class_("AABB") .constructor<>() .function("GetBuffers", &bimGeometry::AABB::GetBuffers) @@ -1156,7 +1280,10 @@ EMSCRIPTEN_BINDINGS(my_module) emscripten::function("GetModelSize", &GetModelSize); emscripten::function("IsModelOpen", &IsModelOpen); emscripten::function("GetGeometry", &GetGeometry); + emscripten::function("GetRepairedMesh", &GetRepairedMesh); + emscripten::function("RepairMeshOrientationRaw", &RepairMeshOrientationRaw); emscripten::function("GetFlatMesh", &GetFlatMesh); + emscripten::function("GetBooleanOperands", &GetBooleanOperands); emscripten::function("GetCoordinationMatrix", &GetCoordinationMatrix); emscripten::function("GetWorldTransformMatrix", &GetWorldTransformMatrix); emscripten::function("StreamMeshes", &StreamMeshesWithExpressID); diff --git a/src/cpp/web-ifc/geometry/IfcGeometryProcessor.cpp b/src/cpp/web-ifc/geometry/IfcGeometryProcessor.cpp index 123d72ab5..c2f304269 100644 --- a/src/cpp/web-ifc/geometry/IfcGeometryProcessor.cpp +++ b/src/cpp/web-ifc/geometry/IfcGeometryProcessor.cpp @@ -1692,6 +1692,83 @@ namespace webifc::geometry return flatMesh; } + IfcGeometryProcessor::IfcBooleanOperands IfcGeometryProcessor::GetBooleanOperands(uint32_t expressID) + { + spdlog::debug("[GetBooleanOperands({})]", expressID); + IfcBooleanOperands result; + result.expressID = expressID; + + auto lineType = _loader.GetLineType(expressID); + if (!_schemaManager.IsIfcElement(lineType)) + return result; + + _loader.MoveToArgumentOffset(expressID, 5); + uint32_t localPlacement = 0; + if (_loader.GetTokenType() == parsing::IfcTokenType::REF) + { + _loader.StepBack(); + localPlacement = _loader.GetRefArgument(); + } + uint32_t ifcPresentation = 0; + if (_loader.GetTokenType() == parsing::IfcTokenType::REF) + { + _loader.StepBack(); + ifcPresentation = _loader.GetRefArgument(); + } + + IfcComposedMesh bodyMesh; + bodyMesh.expressID = expressID; + bodyMesh.transformation = glm::dmat4(1); + + std::optional generatedColor = GetStyleItemFromExpressId(expressID); + if (generatedColor) + { + bodyMesh.color = generatedColor.value(); + bodyMesh.hasColor = true; + } + else + { + bodyMesh.color = glm::dvec4(1.0); + bodyMesh.hasColor = false; + } + + if (localPlacement != 0 && _loader.IsValidExpressID(localPlacement)) + bodyMesh.transformation = _geometryLoader.GetLocalPlacement(localPlacement); + + if (ifcPresentation != 0 && _loader.IsValidExpressID(ifcPresentation)) + bodyMesh.children.push_back(GetMesh(ifcPresentation)); + + glm::dmat4 mat = glm::scale(glm::dvec3(_cache.GetLinearScalingFactor())); + + result.bodyMesh.expressID = expressID; + glm::dvec4 bodyColor = glm::dvec4(1, 1, 1, 1); + bool bodyHasColor = false; + AddComposedMeshToFlatMesh(result.bodyMesh, bodyMesh, _transformation * NormalizeIFC * mat, bodyColor, bodyHasColor); + + auto &relVoids = _cache.GetRelVoids(); + auto relVoidsIt = relVoids.find(expressID); + + if (relVoidsIt != relVoids.end() && !relVoidsIt->second.empty()) + { + result.hasBooleans = true; + + for (auto relVoidExpressID : relVoidsIt->second) + { + IfcComposedMesh voidComposed = GetMesh(relVoidExpressID); + + IfcFlatMesh voidFlat; + voidFlat.expressID = relVoidExpressID; + glm::dvec4 voidColor = glm::dvec4(1, 1, 1, 1); + bool voidHasColor = false; + AddComposedMeshToFlatMesh(voidFlat, voidComposed, _transformation * NormalizeIFC * mat, voidColor, voidHasColor); + + result.voidMeshes.push_back(voidFlat); + } + } + + return result; + } + /** * Extracts all the geometries and their associated colors and transformations from a composed mesh * and adds them to the flat mesh geometries field. diff --git a/src/cpp/web-ifc/geometry/IfcGeometryProcessor.h b/src/cpp/web-ifc/geometry/IfcGeometryProcessor.h index 37c9e80be..ad5056a9c 100644 --- a/src/cpp/web-ifc/geometry/IfcGeometryProcessor.h +++ b/src/cpp/web-ifc/geometry/IfcGeometryProcessor.h @@ -56,6 +56,13 @@ namespace webifc::geometry IfcGeometry &GetGeometry(uint32_t expressID); IfcGeometryLoader& GetLoader(); IfcFlatMesh GetFlatMesh(uint32_t expressID, bool applyLinearScalingFactor = true); + struct IfcBooleanOperands { + uint32_t expressID = 0; + IfcFlatMesh bodyMesh; + std::vector voidMeshes; + bool hasBooleans = false; + }; + IfcBooleanOperands GetBooleanOperands(uint32_t expressID); IfcComposedMesh GetMesh(uint32_t expressID); void SetTransformation(const std::array &val); std::array GetFlatCoordinationMatrix() const; diff --git a/src/cpp/web-ifc/geometry/operations/mesh-orientation-repair.cpp b/src/cpp/web-ifc/geometry/operations/mesh-orientation-repair.cpp new file mode 100644 index 000000000..0f9f4bcb4 --- /dev/null +++ b/src/cpp/web-ifc/geometry/operations/mesh-orientation-repair.cpp @@ -0,0 +1,548 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// Implementation of the two-layer voting-based mesh orientation repair. +// See mesh-orientation-repair.h for the public contract. + +#include "mesh-orientation-repair.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace webifc::geometry { + +namespace { + + // ------------------------------------------------------------------ + // Constants + // ------------------------------------------------------------------ + + // Interleaved layout: 6 doubles per vertex (x,y,z, nx,ny,nz). + constexpr uint32_t STRIDE = 6; + + // Safety threshold: triangles with area below this are considered + // degenerate and skipped for normal accumulation. + constexpr double DEGENERATE_AREA_EPS = 1e-18; + + // Near-zero detection thresholds for Layer 2 decisiveness. + constexpr double NEAR_ZERO_VOLUME_RATIO = 1e-6; + constexpr double NEAR_ZERO_AABB_DOT_RATIO = 1e-6; + + // ------------------------------------------------------------------ + // Tiny 3D vector helpers (avoid pulling in glm for this TU) + // ------------------------------------------------------------------ + + struct V3 { double x, y, z; }; + + inline V3 vsub(const V3& a, const V3& b) { return {a.x-b.x, a.y-b.y, a.z-b.z}; } + inline V3 vmul(const V3& a, double s) { return {a.x*s, a.y*s, a.z*s}; } + inline double vdot(const V3& a, const V3& b) { + return a.x*b.x + a.y*b.y + a.z*b.z; + } + inline V3 vcross(const V3& a, const V3& b) { + return { a.y*b.z - a.z*b.y, + a.z*b.x - a.x*b.z, + a.x*b.y - a.y*b.x }; + } + inline double vlen(const V3& a) { + return std::sqrt(a.x*a.x + a.y*a.y + a.z*a.z); + } + + inline V3 getPos(const std::vector& v, uint32_t i) { + return { v[i*STRIDE+0], v[i*STRIDE+1], v[i*STRIDE+2] }; + } + + // ------------------------------------------------------------------ + // Union-Find (for connected components) + // ------------------------------------------------------------------ + + struct UnionFind { + std::vector parent; + std::vector rank_; + + explicit UnionFind(uint32_t n) : parent(n), rank_(n, 0) { + for (uint32_t i = 0; i < n; ++i) parent[i] = i; + } + + uint32_t find(uint32_t x) { + while (parent[x] != x) { + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + } + + void unite(uint32_t a, uint32_t b) { + a = find(a); + b = find(b); + if (a == b) return; + if (rank_[a] < rank_[b]) std::swap(a, b); + parent[b] = a; + if (rank_[a] == rank_[b]) ++rank_[a]; + } + }; + + // ------------------------------------------------------------------ + // Weld: map near-coincident vertices to a canonical index + // ------------------------------------------------------------------ + + struct I64Triple { + int64_t x, y, z; + bool operator==(const I64Triple& o) const { + return x == o.x && y == o.y && z == o.z; + } + }; + + struct I64TripleHash { + size_t operator()(const I64Triple& k) const noexcept { + size_t h = std::hash{}(k.x); + h ^= std::hash{}(k.y) + 0x9e3779b97f4a7c15ULL + (h<<6) + (h>>2); + h ^= std::hash{}(k.z) + 0x9e3779b97f4a7c15ULL + (h<<6) + (h>>2); + return h; + } + }; + + std::vector buildWeldMap( + const std::vector& vertexData, + double epsilon) + { + const uint32_t vcount = static_cast(vertexData.size() / STRIDE); + std::vector welded(vcount, 0); + if (vcount == 0) return welded; + + const double eps = (epsilon > 0.0) ? epsilon : 1e-6; + const double invE = 1.0 / eps; + + std::unordered_map table; + table.reserve(vcount); + + uint32_t nextCanon = 0; + for (uint32_t i = 0; i < vcount; ++i) { + const V3 p = getPos(vertexData, i); + I64Triple key{ + static_cast(std::llround(p.x * invE)), + static_cast(std::llround(p.y * invE)), + static_cast(std::llround(p.z * invE)) + }; + auto it = table.find(key); + if (it == table.end()) { + welded[i] = nextCanon; + table.emplace(key, nextCanon); + ++nextCanon; + } else { + welded[i] = it->second; + } + } + return welded; + } + + // ------------------------------------------------------------------ + // Edge map built on welded vertex ids + // ------------------------------------------------------------------ + + struct HalfEdge { + uint32_t tri; + bool forward; + }; + + inline uint64_t edgeKey(uint32_t a, uint32_t b) { + if (a < b) return (static_cast(a) << 32) | b; + else return (static_cast(b) << 32) | a; + } + + // ------------------------------------------------------------------ + // Flip a triangle's winding in the index buffer. + // Matches IfcGeometry::ReverseFace: swap indices 0 and 2 of the tri. + // ------------------------------------------------------------------ + inline void flipTri(std::vector& indexData, uint32_t ti) { + std::swap(indexData[ti*3 + 0], indexData[ti*3 + 2]); + } + + // ------------------------------------------------------------------ + // Regenerate per-vertex normals by area-weighted averaging. + // Must run AFTER all flips have been applied. + // ------------------------------------------------------------------ + void regenerateNormals( + std::vector& vertexData, + const std::vector& indexData) + { + const uint32_t vcount = static_cast(vertexData.size() / STRIDE); + const uint32_t tcount = static_cast(indexData.size() / 3); + + for (uint32_t i = 0; i < vcount; ++i) { + vertexData[i*STRIDE + 3] = 0.0; + vertexData[i*STRIDE + 4] = 0.0; + vertexData[i*STRIDE + 5] = 0.0; + } + + for (uint32_t t = 0; t < tcount; ++t) { + const uint32_t i0 = indexData[t*3 + 0]; + const uint32_t i1 = indexData[t*3 + 1]; + const uint32_t i2 = indexData[t*3 + 2]; + if (i0 >= vcount || i1 >= vcount || i2 >= vcount) continue; + + const V3 a = getPos(vertexData, i0); + const V3 b = getPos(vertexData, i1); + const V3 c = getPos(vertexData, i2); + const V3 n = vcross(vsub(b, a), vsub(c, a)); + const double len = vlen(n); + if (len <= DEGENERATE_AREA_EPS) continue; + + // |n| already encodes 2*area -> area weighting for free. + vertexData[i0*STRIDE + 3] += n.x; + vertexData[i0*STRIDE + 4] += n.y; + vertexData[i0*STRIDE + 5] += n.z; + vertexData[i1*STRIDE + 3] += n.x; + vertexData[i1*STRIDE + 4] += n.y; + vertexData[i1*STRIDE + 5] += n.z; + vertexData[i2*STRIDE + 3] += n.x; + vertexData[i2*STRIDE + 4] += n.y; + vertexData[i2*STRIDE + 5] += n.z; + } + + for (uint32_t i = 0; i < vcount; ++i) { + const V3 n{ vertexData[i*STRIDE + 3], + vertexData[i*STRIDE + 4], + vertexData[i*STRIDE + 5] }; + const double len = vlen(n); + if (len > 0.0) { + const double inv = 1.0 / len; + vertexData[i*STRIDE + 3] = n.x * inv; + vertexData[i*STRIDE + 4] = n.y * inv; + vertexData[i*STRIDE + 5] = n.z * inv; + } + } + } + +} // anonymous namespace + + +// ---------------------------------------------------------------------- +// Public entry point +// ---------------------------------------------------------------------- +OrientationRepairResult RepairMeshOrientation( + std::vector& vertexData, + std::vector& indexData, + const OrientationRepairOptions& options) +{ + OrientationRepairResult result; + + const uint32_t tcount = static_cast(indexData.size() / 3); + const uint32_t vcount = static_cast(vertexData.size() / STRIDE); + if (tcount == 0 || vcount == 0) { + return result; + } + + // --- Step 1: weld spatial duplicates ------------------------------ + const std::vector welded = buildWeldMap(vertexData, options.weldEpsilon); + + // --- Step 2: build the edge map on welded ids --------------------- + std::unordered_map> edgeMap; + edgeMap.reserve(tcount * 3); + + for (uint32_t t = 0; t < tcount; ++t) { + const uint32_t raw[3] = { + indexData[t*3 + 0], + indexData[t*3 + 1], + indexData[t*3 + 2] + }; + if (raw[0] >= vcount || raw[1] >= vcount || raw[2] >= vcount) continue; + + const uint32_t w[3] = { welded[raw[0]], welded[raw[1]], welded[raw[2]] }; + + // Skip triangles that collapse under welding. + if (w[0] == w[1] || w[1] == w[2] || w[0] == w[2]) continue; + + for (int e = 0; e < 3; ++e) { + const uint32_t u = w[e]; + const uint32_t v = w[(e + 1) % 3]; + const uint64_t key = edgeKey(u, v); + const bool forward = (u < v); + edgeMap[key].push_back(HalfEdge{ t, forward }); + } + } + + // --- Step 3: adjacency (manifold edges only) + Union-Find --------- + struct Adj { uint32_t nb; bool agree; }; + std::vector> adjacency(tcount); + UnionFind uf(tcount); + + for (const auto& kv : edgeMap) { + const auto& halves = kv.second; + if (halves.size() != 2) continue; // boundary (1) or non-manifold (>=3) + const uint32_t t0 = halves[0].tri; + const uint32_t t1 = halves[1].tri; + const bool f0 = halves[0].forward; + const bool f1 = halves[1].forward; + const bool agree = (f0 != f1); // opposite directions -> coherent + uf.unite(t0, t1); + adjacency[t0].push_back(Adj{ t1, agree }); + adjacency[t1].push_back(Adj{ t0, agree }); + } + + // --- Step 4: Layer 1 -- 2-color each component via BFS, flip minority. + constexpr int8_t UNSET = -1; + std::vector team(tcount, UNSET); + + std::unordered_map> components; + components.reserve(tcount / 8 + 1); + for (uint32_t t = 0; t < tcount; ++t) { + components[uf.find(t)].push_back(t); + } + result.componentsProcessed = static_cast(components.size()); + + uint32_t totalFlipped = 0; + uint32_t totalUnsatisfied = 0; + double worstMargin = 0.0; + + for (auto& kv : components) { + const std::vector& compTris = kv.second; + if (compTris.empty()) continue; + + // BFS 2-coloring seeded from the first triangle of the component. + std::deque queue; + const uint32_t seed = compTris.front(); + team[seed] = 0; + queue.push_back(seed); + + while (!queue.empty()) { + const uint32_t cur = queue.front(); + queue.pop_front(); + const int8_t curTeam = team[cur]; + for (const Adj& a : adjacency[cur]) { + const int8_t expected = a.agree ? curTeam : (1 - curTeam); + if (team[a.nb] == UNSET) { + team[a.nb] = expected; + queue.push_back(a.nb); + } else if (team[a.nb] != expected) { + ++totalUnsatisfied; // Moebius-like contradiction + } + } + } + + // Tally votes. Any triangle still UNSET (lone island in the UF + // component without manifold adjacency) is assigned to team 0. + uint32_t count0 = 0; + uint32_t count1 = 0; + for (uint32_t ti : compTris) { + if (team[ti] == UNSET) team[ti] = 0; + if (team[ti] == 0) ++count0; else ++count1; + } + const uint32_t total = count0 + count1; + const uint32_t minority = (count0 <= count1) ? 0 : 1; + const uint32_t minCount = std::min(count0, count1); + const double margin = (total > 0) + ? (static_cast(minCount) / total) + : 0.0; + if (margin > worstMargin) worstMargin = margin; + + if (minCount > 0) { + for (uint32_t ti : compTris) { + if (team[ti] == static_cast(minority)) { + flipTri(indexData, ti); + ++totalFlipped; + } + } + } + } + + result.maxVoteMargin = worstMargin; + result.unsatisfiedEdges = totalUnsatisfied; + + // --- Step 5: Layer 2 -- decide outward/inward per component. ------ + bool everyLayer2Decisive = true; + + for (auto& kv : components) { + const std::vector& compTris = kv.second; + if (compTris.empty()) continue; + + // Count boundary edges for THIS component. + uint32_t boundaryCount = 0; + uint32_t totalHalf = 0; + for (uint32_t ti : compTris) { + const uint32_t raw[3] = { + indexData[ti*3 + 0], + indexData[ti*3 + 1], + indexData[ti*3 + 2] + }; + if (raw[0] >= vcount || raw[1] >= vcount || raw[2] >= vcount) continue; + const uint32_t w[3] = { welded[raw[0]], welded[raw[1]], welded[raw[2]] }; + if (w[0] == w[1] || w[1] == w[2] || w[0] == w[2]) continue; + + for (int e = 0; e < 3; ++e) { + const uint32_t u = w[e]; + const uint32_t v = w[(e + 1) % 3]; + ++totalHalf; + auto it = edgeMap.find(edgeKey(u, v)); + if (it == edgeMap.end() || it->second.size() != 2) { + ++boundaryCount; + } + } + } + + const bool nearlyClosed = + (totalHalf > 0) && + (static_cast(boundaryCount) <= + options.nearlyClosedBoundaryRatio * static_cast(totalHalf)); + + // Component AABB. + V3 bbMin{ std::numeric_limits::infinity(), + std::numeric_limits::infinity(), + std::numeric_limits::infinity() }; + V3 bbMax{ -std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + -std::numeric_limits::infinity() }; + for (uint32_t ti : compTris) { + for (int e = 0; e < 3; ++e) { + const uint32_t vi = indexData[ti*3 + e]; + if (vi >= vcount) continue; + const V3 p = getPos(vertexData, vi); + if (p.x < bbMin.x) bbMin.x = p.x; + if (p.y < bbMin.y) bbMin.y = p.y; + if (p.z < bbMin.z) bbMin.z = p.z; + if (p.x > bbMax.x) bbMax.x = p.x; + if (p.y > bbMax.y) bbMax.y = p.y; + if (p.z > bbMax.z) bbMax.z = p.z; + } + } + const V3 bbCenter{ 0.5 * (bbMin.x + bbMax.x), + 0.5 * (bbMin.y + bbMax.y), + 0.5 * (bbMin.z + bbMax.z) }; + const V3 bbExtent{ bbMax.x - bbMin.x, + bbMax.y - bbMin.y, + bbMax.z - bbMin.z }; + + bool shouldFlipAll = false; + + if (nearlyClosed) { + // --- Strategy A: signed volume around the AABB center. + double signedVol = 0.0; + for (uint32_t ti : compTris) { + const uint32_t i0 = indexData[ti*3 + 0]; + const uint32_t i1 = indexData[ti*3 + 1]; + const uint32_t i2 = indexData[ti*3 + 2]; + if (i0 >= vcount || i1 >= vcount || i2 >= vcount) continue; + const V3 a = vsub(getPos(vertexData, i0), bbCenter); + const V3 b = vsub(getPos(vertexData, i1), bbCenter); + const V3 c = vsub(getPos(vertexData, i2), bbCenter); + signedVol += vdot(a, vcross(b, c)); + } + + const double bboxVol = bbExtent.x * bbExtent.y * bbExtent.z; + const double nearZero = NEAR_ZERO_VOLUME_RATIO * std::max(bboxVol, 1.0); + + if (std::fabs(signedVol) <= nearZero) { + everyLayer2Decisive = false; // flat/degenerate shell + } else if (signedVol < 0.0) { + shouldFlipAll = true; + } + } else { + // --- Strategy B: AABB-center heuristic for open surfaces. + uint32_t bestTri = compTris.front(); + double bestDist = -1.0; + for (uint32_t ti : compTris) { + const uint32_t i0 = indexData[ti*3 + 0]; + const uint32_t i1 = indexData[ti*3 + 1]; + const uint32_t i2 = indexData[ti*3 + 2]; + if (i0 >= vcount || i1 >= vcount || i2 >= vcount) continue; + const V3 a = getPos(vertexData, i0); + const V3 b = getPos(vertexData, i1); + const V3 c = getPos(vertexData, i2); + const V3 cn{ (a.x + b.x + c.x) / 3.0, + (a.y + b.y + c.y) / 3.0, + (a.z + b.z + c.z) / 3.0 }; + const V3 d = vsub(cn, bbCenter); + const double d2 = d.x*d.x + d.y*d.y + d.z*d.z; + if (d2 > bestDist) { + bestDist = d2; + bestTri = ti; + } + } + + const uint32_t i0 = indexData[bestTri*3 + 0]; + const uint32_t i1 = indexData[bestTri*3 + 1]; + const uint32_t i2 = indexData[bestTri*3 + 2]; + const V3 a = getPos(vertexData, i0); + const V3 b = getPos(vertexData, i1); + const V3 c = getPos(vertexData, i2); + const V3 n = vcross(vsub(b, a), vsub(c, a)); + const V3 cn{ (a.x + b.x + c.x) / 3.0, + (a.y + b.y + c.y) / 3.0, + (a.z + b.z + c.z) / 3.0 }; + const V3 outward = vsub(cn, bbCenter); + const double dotVal = vdot(n, outward); + + const double nLen = vlen(n); + const double outwardLen = vlen(outward); + const double scale = std::max(nLen * outwardLen, 1.0); + const double nearZeroDot = NEAR_ZERO_AABB_DOT_RATIO * scale; + + if (std::fabs(dotVal) <= nearZeroDot) { + // Ambiguous -> Y-up fallback. + double bestY = -std::numeric_limits::infinity(); + uint32_t topTri = compTris.front(); + for (uint32_t ti : compTris) { + for (int e = 0; e < 3; ++e) { + const uint32_t vi = indexData[ti*3 + e]; + if (vi >= vcount) continue; + const double y = vertexData[vi*STRIDE + 1]; + if (y > bestY) { + bestY = y; + topTri = ti; + } + } + } + const uint32_t j0 = indexData[topTri*3 + 0]; + const uint32_t j1 = indexData[topTri*3 + 1]; + const uint32_t j2 = indexData[topTri*3 + 2]; + const V3 ta = getPos(vertexData, j0); + const V3 tb = getPos(vertexData, j1); + const V3 tc = getPos(vertexData, j2); + const V3 tn = vcross(vsub(tb, ta), vsub(tc, ta)); + + const double tnLen = vlen(tn); + const double yEps = 1e-9 * std::max(tnLen, 1.0); + if (tn.y < -yEps) { + shouldFlipAll = true; + } else if (std::fabs(tn.y) <= yEps) { + if (tn.x < -yEps) { + shouldFlipAll = true; + } else if (std::fabs(tn.x) <= yEps) { + everyLayer2Decisive = false; // truly ambiguous + } + } + } else if (dotVal < 0.0) { + shouldFlipAll = true; + } + } + + if (shouldFlipAll) { + for (uint32_t ti : compTris) { + flipTri(indexData, ti); + ++totalFlipped; + } + } + } + + result.trianglesFlipped = totalFlipped; + result.layer2Decisive = everyLayer2Decisive; + result.confident = + (worstMargin < options.confidenceMarginThreshold) && + (totalUnsatisfied == 0) && + everyLayer2Decisive; + + // --- Step 6: regenerate vertex normals from the final winding. ---- + if (options.regenerateNormals) { + regenerateNormals(vertexData, indexData); + } + + return result; +} + +} // namespace webifc::geometry diff --git a/src/cpp/web-ifc/geometry/operations/mesh-orientation-repair.h b/src/cpp/web-ifc/geometry/operations/mesh-orientation-repair.h new file mode 100644 index 000000000..43b230dac --- /dev/null +++ b/src/cpp/web-ifc/geometry/operations/mesh-orientation-repair.h @@ -0,0 +1,112 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// Mesh orientation repair: fixes inside-out triangles on a finished mesh. +// +// The algorithm is intentionally decoupled from IfcGeometry so it can operate +// on raw vertex/index arrays as well. It is meant to be called AFTER the +// geometry pipeline has finished (including booleans), never inside it. +// +// Two layers (see geometry-orientation-repair.md): +// Layer 1 - Vote-based local coherence (per connected component) +// * Internal "weld" by coordinate quantization so that triangles +// which happen to share a spatial vertex are treated as edge +// neighbours even if their index-level vertices are distinct +// (which is the common case in web-ifc: every AddFace() pushes +// 3 brand new vertices). +// * 2-colouring of the signed adjacency graph via BFS. +// * Minority team (one triangle, one vote) is flipped. +// +// Layer 2 - Global orientation decision per component. +// * Signed volume (around the component centroid) when the +// component is closed or nearly-closed (<= 5% boundary edges). +// * AABB-center heuristic with Y-up fallback for open components. +// +// After the repair, vertex normals are regenerated from the (possibly new) +// triangle winding by area-weighted averaging over incident faces. + +#pragma once + +#include +#include + +namespace webifc::geometry { + + /** + * Quantitative signals returned by the orientation repair pass. + * + * - trianglesFlipped Total triangles whose winding was reversed + * (Layer 1 minority flips + Layer 2 global flips). + * - componentsProcessed Number of connected components detected by the + * edge-based Union-Find (after internal weld). + * - maxVoteMargin Worst Layer-1 margin across all components. + * 0.0 = clean verdict (one team only) + * 0.5 = tied (ambiguous) + * Values >= 0.05 are typically suspicious. + * - unsatisfiedEdges Count of manifold edges whose BFS propagation + * contradicted an already-assigned team. Non-zero + * means non-orientable topology (Moebius-like). + * - layer2Decisive True if every component's Layer-2 probe was + * clearly above the near-zero threshold. + * - confident Convenience: maxVoteMargin < 0.05 + * && unsatisfiedEdges == 0 + * && layer2Decisive. + * Intended for the caller to decide single-sided + * vs double-sided rendering. + */ + struct OrientationRepairResult + { + uint32_t trianglesFlipped = 0; + uint32_t componentsProcessed = 0; + double maxVoteMargin = 0.0; + uint32_t unsatisfiedEdges = 0; + bool layer2Decisive = true; + bool confident = true; + }; + + /** + * Tunable parameters. Defaults are sensible for IFC BIM geometry. + */ + struct OrientationRepairOptions + { + // Welding epsilon in model units. Vertices closer than this (per axis) + // are considered the same topological vertex for adjacency purposes. + // Only affects the edge map; original vertex data is preserved. + double weldEpsilon = 1e-6; + + // Fraction of boundary edges under which the component is treated as + // "closed" and evaluated via signed volume. 0.05 = up to 5% boundary. + double nearlyClosedBoundaryRatio = 0.05; + + // Margin threshold above which a component is no longer "confident". + double confidenceMarginThreshold = 0.05; + + // If true, regenerate per-vertex normals from the final winding. + // Uses area-weighted averaging over incident faces. + bool regenerateNormals = true; + }; + + // --- Public API -------------------------------------------------------- + + /** + * Repair orientation of a mesh stored in web-ifc's interleaved format. + * + * Input layout: + * vertexData packed doubles, 6 per vertex: [x,y,z,nx,ny,nz, ...] + * indexData flat triples, 3 per triangle: [i0,i1,i2, i0,i1,i2, ...] + * + * Both buffers are modified in place: + * - indexData: winding reversals (swap index 0 and 2 of flipped tris). + * - vertexData: normals regenerated if options.regenerateNormals. + * + * Vertex positions are NEVER modified. + * + * Returns quantitative confidence signals (see OrientationRepairResult). + */ + OrientationRepairResult RepairMeshOrientation( + std::vector& vertexData, + std::vector& indexData, + const OrientationRepairOptions& options = OrientationRepairOptions{}); + +} // namespace webifc::geometry diff --git a/src/ts/web-ifc-api.ts b/src/ts/web-ifc-api.ts index 491d2b82d..a45634f3e 100644 --- a/src/ts/web-ifc-api.ts +++ b/src/ts/web-ifc-api.ts @@ -115,6 +115,13 @@ export interface FlatMesh { delete(): void; } +export interface BooleanOperands { + expressID: number; + bodyMesh: FlatMesh; + voidMeshes: Vector; + hasBooleanOp: boolean; +} + export interface Point { x: number; y: number; @@ -166,6 +173,36 @@ export interface IfcGeometry { delete(): void; } +/** + * Confidence signals returned by the mesh orientation repair pass. + * All fields are populated regardless of whether any flips occurred. + */ +export interface OrientationRepairStats { + /** Total triangles whose winding was reversed. */ + trianglesFlipped: number; + /** Number of connected components detected after internal welding. */ + componentsProcessed: number; + /** Worst Layer-1 vote margin across all components (0 = clean, 0.5 = tied). */ + maxVoteMargin: number; + /** Count of Moebius-like contradictory edges (non-orientable topology). */ + unsatisfiedEdges: number; + /** True if every component's Layer-2 probe was clearly decisive. */ + layer2Decisive: boolean; + /** True if the whole result is trustworthy (margin < 5%, 0 unsatisfied, layer-2 decisive). */ + confident: boolean; +} + +/** + * Result of a mesh orientation repair. Buffers follow the web-ifc convention: + * interleaved vertices (x,y,z,nx,ny,nz) as Float32Array, and triangle indices + * as Uint32Array in groups of 3. + */ +export interface RepairedMesh { + vertexData: Float32Array; + indexData: Uint32Array; + stats: OrientationRepairStats; +} + export interface Buffers { fvertexData: Array; indexData: Array; @@ -677,6 +714,79 @@ export class IfcAPI { return this.wasmModule.GetGeometry(modelID, geometryExpressID); } + /** + * Returns a copy of the geometry with inside-out triangles repaired. + * + * The original geometry held by the model is NOT modified. The repair is + * applied to a buffer copy, so callers can compare pre- and post-repair + * data or keep both versions. + * + * Uses a two-layer voting algorithm: + * Layer 1 - per-component majority vote fixes local incoherence. + * Layer 2 - signed volume (closed shells) or AABB-center heuristic + * (open surfaces) fixes global outward/inward orientation. + * + * Vertex normals are regenerated by area-weighted averaging from the + * final winding. Vertex POSITIONS are never modified. + * + * @param modelID Model handle retrieved by OpenModel. + * @param geometryExpressID Express ID of the geometry to repair. + * @returns A RepairedMesh with the new buffers and confidence stats. + */ + GetRepairedMesh(modelID: number, geometryExpressID: number): RepairedMesh { + const raw = this.wasmModule.GetRepairedMesh(modelID, geometryExpressID); + return this.wrapRepairedMesh(raw); + } + + /** + * Repairs the orientation of a mesh supplied as raw arrays. Useful for + * meshes not originating from an IFC model (synthetic geometry, imported + * from another format, etc.). + * + * Input format matches GetVertexArray/GetIndexArray: + * - vertexData: interleaved [x,y,z,nx,ny,nz, ...] as Float32Array. + * - indexData: flat triangle indices [i0,i1,i2, ...] as Uint32Array. + * + * Returns freshly allocated Float32Array/Uint32Array; the input arrays + * are NOT modified. + * + * @param vertexData Interleaved vertex buffer (6 floats per vertex). + * @param indexData Triangle index buffer (3 uint32 per triangle). + * @returns A RepairedMesh with repaired buffers and confidence stats. + */ + RepairMeshOrientation( + vertexData: Float32Array, + indexData: Uint32Array + ): RepairedMesh { + // Embind accepts JS arrays / typed arrays for val parameters. + const raw = this.wasmModule.RepairMeshOrientationRaw(vertexData, indexData); + return this.wrapRepairedMesh(raw); + } + + /** @ignore */ + private wrapRepairedMesh(raw: any): RepairedMesh { + // raw.fvertexData and raw.indexData come back as Embind vectors. + // Convert to typed arrays for a TS-friendly shape. + const vLen = raw.fvertexData.size(); + const iLen = raw.indexData.size(); + const vertexData = new Float32Array(vLen); + const indexData = new Uint32Array(iLen); + for (let i = 0; i < vLen; i++) vertexData[i] = raw.fvertexData.get(i); + for (let i = 0; i < iLen; i++) indexData[i] = raw.indexData.get(i); + const stats: OrientationRepairStats = { + trianglesFlipped: raw.trianglesFlipped, + componentsProcessed: raw.componentsProcessed, + maxVoteMargin: raw.maxVoteMargin, + unsatisfiedEdges: raw.unsatisfiedEdges, + layer2Decisive: raw.layer2Decisive, + confident: raw.confident, + }; + // Free the underlying Embind vectors to avoid leaks. + if (typeof raw.fvertexData.delete === "function") raw.fvertexData.delete(); + if (typeof raw.indexData.delete === "function") raw.indexData.delete(); + return { vertexData, indexData, stats }; + } + CreateAABB() { return this.wasmModule.CreateAABB(); } @@ -730,7 +840,11 @@ export class IfcAPI { * @param modelID Model handle retrieved by OpenModel * @param headerType Type of header data you want to retrieve * ifc.FILE_NAME, ifc.FILE_DESCRIPTION or ifc.FILE_SCHEMA - * @returns An object with parameters ID, type and arguments + *despres tenim aquests dos arxius. + + + + Bé la cosa es que necessito d'alguna manera pensar un métode per extreure, d'un objecte del IFC els elements que fa servir en una operació booleana pero sense fer la booleana @returns An object with parameters ID, type and arguments */ GetHeaderLine(modelID: number, headerType: number) { return this.wasmModule.GetHeaderLine(modelID, headerType); @@ -1439,6 +1553,19 @@ export class IfcAPI { return this.wasmModule.GetFlatMesh(modelID, expressID); } + /** + * Returns the boolean operands for an element without performing the final boolean operation. + * The body mesh includes all internal CSG operations (clippings, halfspaces) fully resolved. + * The void meshes are the resolved geometries of each IfcOpeningElement linked via IfcRelVoidsElement. + * If the element has no voids, hasBooleanOp is false and voidMeshes is empty. + * @param modelID Model handle retrieved by OpenModel + * @param expressID ExpressID of the IfcElement (wall, slab, etc.) + * @returns BooleanOperands with separated body and void geometries + */ + GetBooleanOperands(modelID: number, expressID: number): BooleanOperands { + return this.wasmModule.GetBooleanOperands(modelID, expressID); + } + /** * Returns the maximum ExpressID value in the IFC file, ex.- #9999999 * @param modelID Model handle retrieved by OpenModel diff --git a/tsconfig.json b/tsconfig.json index 89942bd33..3bd72c5d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,7 @@ "strictPropertyInitialization": true, "alwaysStrict": true, "rootDir":"./src/ts", - "ignoreDeprecations": "6.0", + "ignoreDeprecations": "5.0", "types": ["node","jest"], "module":"node16", "isolatedModules": true