Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/viewer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<input type="file" id="finput">
<input type="button" id="cmem" value="Clear Memory"/>
<input type="button" id="rcode" value="Reset Editor"/>
<input type="button" id="repairfaces" value="Repair Faces"/>
Log Level: <select id="logLevel">
<option value="6">Off</option>
<option value="4" selected>Error</option>
Expand Down
80 changes: 63 additions & 17 deletions examples/viewer/web-ifc-three.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -40,14 +40,22 @@ 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;

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)
{
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -96,25 +113,54 @@ 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);
mesh.matrixAutoUpdate = false;
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 = {};
Expand All @@ -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;

Expand Down
36 changes: 35 additions & 1 deletion examples/viewer/web-ifc-viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 = <HTMLInputElement>document.getElementById("finput");
if (fileInput.files.length == 0) return console.log("No files selected!");
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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()).
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
127 changes: 127 additions & 0 deletions src/cpp/wasm/web-ifc-wasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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<uint32_t> &expressIds, emscripten::val callback)
{
if (!manager.IsModelOpen(modelID))
Expand Down Expand Up @@ -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<float> fvertexData;
std::vector<uint32_t> 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<double>& vertexData,
std::vector<uint32_t>& 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<float>(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<double> verts = geom.vertexData;
std::vector<uint32_t> 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<uint32_t>();
const uint32_t ilen = indexDataVal["length"].as<uint32_t>();

std::vector<double> verts(vlen);
std::vector<uint32_t> idx(ilen);

for (uint32_t i = 0; i < vlen; ++i)
verts[i] = vertexDataVal[i].as<double>();
for (uint32_t i = 0; i < ilen; ++i)
idx[i] = indexDataVal[i].as<uint32_t>();

return BuildRepairedMeshFromBuffers(verts, idx);
}

EMSCRIPTEN_BINDINGS(my_module)
{

Expand Down Expand Up @@ -1007,6 +1114,13 @@ EMSCRIPTEN_BINDINGS(my_module)
.field("expressID", &webifc::geometry::IfcFlatMesh::expressID);

emscripten::register_vector<webifc::geometry::IfcFlatMesh>("IfcFlatMeshVector");

emscripten::value_object<webifc::geometry::IfcGeometryProcessor::IfcBooleanOperands>("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<uint32_t>("UintVector");

emscripten::value_object<webifc::geometry::SweptDiskSolid>("SweptDiskSolid")
Expand Down Expand Up @@ -1070,6 +1184,16 @@ EMSCRIPTEN_BINDINGS(my_module)

emscripten::register_vector<float>("vector<float>");

emscripten::value_object<RepairedMesh>("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_<bimGeometry::AABB>("AABB")
.constructor<>()
.function("GetBuffers", &bimGeometry::AABB::GetBuffers)
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading