brepjs-bim 0.23.1 → 0.23.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/brepjs-bim.cjs +332 -99
- package/dist/brepjs-bim.js +324 -91
- package/dist/import/fromIfc.d.ts +7 -1
- package/dist/import/geometryRead.d.ts +13 -1
- package/dist/import/importedModel.d.ts +17 -8
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
package/dist/brepjs-bim.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { addHoles, applyMatrix, autoHeal, box, castShape, clone, convexHull, cut, err, extrude, fuse, getKernel, getSolids, isClosedWire, isOk, isPlanarWire, isSolid, isValid, isValidSolid, measureVolume, mesh, ok, outerWire, polygon, revolve, rotate, translate, validSolid } from "brepjs";
|
|
1
|
+
import { addHoles, applyMatrix, autoHeal, box, castShape, clone, convexHull, cut, err, extrude, fuse, getBounds, getKernel, getSolids, isClosedWire, isOk, isPlanarWire, isSolid, isValid, isValidSolid, measureVolume, mesh, ok, outerWire, polygon, revolve, rotate, translate, validSolid } from "brepjs";
|
|
2
2
|
import * as WebIFC from "web-ifc";
|
|
3
3
|
import { Handle, IfcAPI } from "web-ifc";
|
|
4
4
|
//#region src/identity/ifcGuid.ts
|
|
@@ -14065,6 +14065,7 @@ function buildElementContainmentMap(reader) {
|
|
|
14065
14065
|
//#endregion
|
|
14066
14066
|
//#region src/import/geometryRead.ts
|
|
14067
14067
|
var NONE = { kind: "NONE" };
|
|
14068
|
+
var testHooks$1 = null;
|
|
14068
14069
|
/**
|
|
14069
14070
|
* Reconstructs the `Body` (SweptSolid) representation of a product into a brepjs
|
|
14070
14071
|
* solid, falling back to tessellated mesh import when no parametric path exists.
|
|
@@ -14075,27 +14076,66 @@ var NONE = { kind: "NONE" };
|
|
|
14075
14076
|
* single bad element never aborts a model import.
|
|
14076
14077
|
*/
|
|
14077
14078
|
function readBodyGeometry(reader, productExpressId, scale, diagnostics) {
|
|
14079
|
+
const body = readBodyItems(reader, productExpressId, scale, diagnostics);
|
|
14080
|
+
const selected = body.items.find((item) => item.kind !== "NONE") ?? NONE;
|
|
14081
|
+
for (const item of body.items) if (item !== selected && item.kind === "SOLID") item.solid[Symbol.dispose]();
|
|
14082
|
+
return selected;
|
|
14083
|
+
}
|
|
14084
|
+
/** Reconstructs each supported IFC Body item independently in representation order. */
|
|
14085
|
+
function readBodyItems(reader, productExpressId, scale, diagnostics) {
|
|
14078
14086
|
const product = reader.getLine(productExpressId);
|
|
14079
|
-
if (product === null) return
|
|
14087
|
+
if (product === null) return {
|
|
14088
|
+
hasBody: false,
|
|
14089
|
+
itemCount: 0,
|
|
14090
|
+
items: []
|
|
14091
|
+
};
|
|
14080
14092
|
const representationRef = asRef(product["Representation"]);
|
|
14081
|
-
if (representationRef === void 0) return
|
|
14093
|
+
if (representationRef === void 0) return {
|
|
14094
|
+
hasBody: false,
|
|
14095
|
+
itemCount: 0,
|
|
14096
|
+
items: []
|
|
14097
|
+
};
|
|
14082
14098
|
const productShape = reader.getLine(representationRef.value);
|
|
14083
14099
|
const representations = productShape === null ? void 0 : asRefArray(productShape["Representations"]);
|
|
14084
|
-
if (representations === void 0) return
|
|
14085
|
-
|
|
14086
|
-
|
|
14100
|
+
if (representations === void 0) return {
|
|
14101
|
+
hasBody: false,
|
|
14102
|
+
itemCount: 0,
|
|
14103
|
+
items: []
|
|
14104
|
+
};
|
|
14105
|
+
const bodyItemIds = findBodyItems(reader, representations);
|
|
14106
|
+
if (bodyItemIds === null) return {
|
|
14107
|
+
hasBody: false,
|
|
14108
|
+
itemCount: 0,
|
|
14109
|
+
items: []
|
|
14110
|
+
};
|
|
14087
14111
|
const worldTransform = readWorldTransform(reader, product, scale);
|
|
14088
|
-
const
|
|
14089
|
-
|
|
14090
|
-
|
|
14091
|
-
|
|
14092
|
-
|
|
14093
|
-
} catch (
|
|
14094
|
-
diagnostics.push(issue("warning", "GEOMETRY_RECONSTRUCTION_FAILED", `Body
|
|
14095
|
-
return NONE;
|
|
14112
|
+
const itemTypes = new Map(bodyItemIds.map((id) => [id, reader.getLineType(id)]));
|
|
14113
|
+
const streamedItemIds = new Set(bodyItemIds.filter((id) => isStreamedTessellatedType(itemTypes.get(id))));
|
|
14114
|
+
let streamedMeshes = /* @__PURE__ */ new Map();
|
|
14115
|
+
if (streamedItemIds.size > 0) try {
|
|
14116
|
+
streamedMeshes = collectItemMeshes(reader, productExpressId, streamedItemIds);
|
|
14117
|
+
} catch (cause) {
|
|
14118
|
+
diagnostics.push(issue("warning", "GEOMETRY_RECONSTRUCTION_FAILED", `Body tessellation stream failed: ${errMsg$1(cause)}`, productExpressId));
|
|
14096
14119
|
}
|
|
14097
|
-
|
|
14098
|
-
|
|
14120
|
+
const items = bodyItemIds.map((bodyItemId) => {
|
|
14121
|
+
const itemType = itemTypes.get(bodyItemId);
|
|
14122
|
+
try {
|
|
14123
|
+
if (itemType === WebIFC.IFCEXTRUDEDAREASOLID) return reconstructExtrusion(reader, bodyItemId, scale, worldTransform, diagnostics);
|
|
14124
|
+
if (itemType === WebIFC.IFCREVOLVEDAREASOLID) return reconstructRevolution(reader, bodyItemId, scale, worldTransform, diagnostics);
|
|
14125
|
+
if (itemType === WebIFC.IFCTRIANGULATEDFACESET) return reconstructTriangulatedItem(reader, bodyItemId, scale, worldTransform, diagnostics);
|
|
14126
|
+
if (isStreamedTessellatedType(itemType)) return reconstructStreamedTessellatedItem(streamedMeshes.get(bodyItemId), bodyItemId, diagnostics);
|
|
14127
|
+
} catch (cause) {
|
|
14128
|
+
diagnostics.push(issue("warning", "GEOMETRY_RECONSTRUCTION_FAILED", `Body item ${bodyItemId} reconstruction threw: ${errMsg$1(cause)}`, bodyItemId, { productExpressId }));
|
|
14129
|
+
return NONE;
|
|
14130
|
+
}
|
|
14131
|
+
diagnostics.push(issue("warning", "UNSUPPORTED_REPRESENTATION_ITEM", `Unsupported Body item ${bodyItemId} type ${itemType}; geometry skipped`, bodyItemId, { productExpressId }));
|
|
14132
|
+
return NONE;
|
|
14133
|
+
});
|
|
14134
|
+
return {
|
|
14135
|
+
hasBody: true,
|
|
14136
|
+
itemCount: bodyItemIds.length,
|
|
14137
|
+
items
|
|
14138
|
+
};
|
|
14099
14139
|
}
|
|
14100
14140
|
function reconstructExtrusion(reader, extrusionId, scale, worldTransform, diagnostics) {
|
|
14101
14141
|
const ext = reader.getLine(extrusionId);
|
|
@@ -14227,22 +14267,61 @@ function reconstructRevolution(reader, revolutionId, scale, worldTransform, diag
|
|
|
14227
14267
|
}
|
|
14228
14268
|
return finalizeSolid(placed.value, revolutionId, diagnostics);
|
|
14229
14269
|
}
|
|
14230
|
-
function
|
|
14231
|
-
const
|
|
14270
|
+
function reconstructTriangulatedItem(reader, itemExpressId, scale, worldTransform, diagnostics) {
|
|
14271
|
+
const faceSet = reader.getLine(itemExpressId);
|
|
14272
|
+
const coordinatesRef = faceSet === null ? void 0 : asRef(faceSet["Coordinates"]);
|
|
14273
|
+
const rawPoints = (coordinatesRef === void 0 ? null : reader.getLine(coordinatesRef.value))?.["CoordList"];
|
|
14274
|
+
const rawTriangles = faceSet?.["CoordIndex"];
|
|
14275
|
+
return reconstructTessellatedMesh(readTriangulatedMesh(rawPoints, rawTriangles, scale, worldTransform), 1, itemExpressId, diagnostics);
|
|
14276
|
+
}
|
|
14277
|
+
function reconstructStreamedTessellatedItem(mesh, itemExpressId, diagnostics) {
|
|
14278
|
+
return reconstructTessellatedMesh(mesh ?? null, 1e3, itemExpressId, diagnostics);
|
|
14279
|
+
}
|
|
14280
|
+
function reconstructTessellatedMesh(mesh, scaleToMm, itemExpressId, diagnostics) {
|
|
14232
14281
|
if (mesh === null || mesh.indices.length === 0) {
|
|
14233
|
-
diagnostics.push(issue("warning", "GEOMETRY_RECONSTRUCTION_FAILED", "Tessellated geometry yielded no triangles",
|
|
14282
|
+
diagnostics.push(issue("warning", "GEOMETRY_RECONSTRUCTION_FAILED", "Tessellated geometry yielded no triangles", itemExpressId));
|
|
14234
14283
|
return NONE;
|
|
14235
14284
|
}
|
|
14236
|
-
const
|
|
14285
|
+
const stl = packBinaryStl(mesh.vertices, mesh.indices, scaleToMm);
|
|
14286
|
+
let solid = sewMeshToSolid(mesh, scaleToMm, itemExpressId, diagnostics);
|
|
14287
|
+
try {
|
|
14288
|
+
if (solid === null) {
|
|
14289
|
+
const cast = castFreshResult(getKernel().importSTL(new Uint8Array(stl).buffer));
|
|
14290
|
+
if (isSolid(cast)) {
|
|
14291
|
+
const valid = validSolid(cast);
|
|
14292
|
+
if (valid.ok) solid = valid.value;
|
|
14293
|
+
else cast[Symbol.dispose]();
|
|
14294
|
+
} else {
|
|
14295
|
+
const solids = getSolids(cast);
|
|
14296
|
+
if (solids.length === 1 && solids[0] !== void 0) {
|
|
14297
|
+
const copied = clone(solids[0]);
|
|
14298
|
+
if (copied.ok) {
|
|
14299
|
+
const valid = validSolid(copied.value);
|
|
14300
|
+
if (valid.ok) solid = valid.value;
|
|
14301
|
+
else copied.value[Symbol.dispose]();
|
|
14302
|
+
}
|
|
14303
|
+
}
|
|
14304
|
+
cast[Symbol.dispose]();
|
|
14305
|
+
}
|
|
14306
|
+
}
|
|
14307
|
+
} catch (e) {
|
|
14308
|
+
diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", `STL round-trip failed: ${errMsg$1(e)}`, itemExpressId));
|
|
14309
|
+
}
|
|
14237
14310
|
if (solid !== null) {
|
|
14238
|
-
|
|
14311
|
+
try {
|
|
14312
|
+
testHooks$1?.afterItemSolid?.(itemExpressId, solid);
|
|
14313
|
+
} catch (cause) {
|
|
14314
|
+
solid[Symbol.dispose]();
|
|
14315
|
+
throw cause;
|
|
14316
|
+
}
|
|
14317
|
+
diagnostics.push(issue("info", "TESSELLATED_MANIFOLD", "Tessellated mesh recovered as a closed solid", itemExpressId));
|
|
14239
14318
|
return {
|
|
14240
14319
|
kind: "SOLID",
|
|
14241
14320
|
solid,
|
|
14242
14321
|
lossy: true
|
|
14243
14322
|
};
|
|
14244
14323
|
}
|
|
14245
|
-
diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", "Tessellated mesh is not closed/manifold; returning raw triangle data (lossy)",
|
|
14324
|
+
diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", "Tessellated mesh is not closed/manifold; returning raw triangle data (lossy)", itemExpressId));
|
|
14246
14325
|
return {
|
|
14247
14326
|
kind: "MESH",
|
|
14248
14327
|
vertices: mesh.vertices,
|
|
@@ -14250,12 +14329,11 @@ function reconstructTessellated(reader, productExpressId, diagnostics) {
|
|
|
14250
14329
|
diagnostic: "TESSELLATED_LOSSY"
|
|
14251
14330
|
};
|
|
14252
14331
|
}
|
|
14253
|
-
/** Loose enough to weld float32 vertices
|
|
14332
|
+
/** Loose enough to weld float32 vertices emitted per face, expressed in millimetres. */
|
|
14254
14333
|
var MESH_SEW_TOLERANCE_MM = .001;
|
|
14255
14334
|
/**
|
|
14256
14335
|
* Brands a fresh kernel result and releases its pre-downcast arena slot when
|
|
14257
|
-
* the cast moved to a new one
|
|
14258
|
-
* are left alone. Mirrors brepjs's internal castResultShape.
|
|
14336
|
+
* the cast moved to a new one. In-place kernels share the slot.
|
|
14259
14337
|
*/
|
|
14260
14338
|
function castFreshResult(raw) {
|
|
14261
14339
|
const cast = castShape(raw);
|
|
@@ -14264,12 +14342,7 @@ function castFreshResult(raw) {
|
|
|
14264
14342
|
if (!(rawId !== void 0 && castId !== void 0 ? rawId === castId : cast.wrapped === raw)) getKernel().dispose(raw);
|
|
14265
14343
|
return cast;
|
|
14266
14344
|
}
|
|
14267
|
-
|
|
14268
|
-
* Sews the streamed triangles into a closed solid through the kernel's mesh
|
|
14269
|
-
* builder, the same path brepjs's mesh importers use. Null when the mesh is
|
|
14270
|
-
* open or the kernel rejects it.
|
|
14271
|
-
*/
|
|
14272
|
-
function sewMeshToSolid(mesh, scaleToMm, productExpressId, diagnostics) {
|
|
14345
|
+
function sewMeshToSolid(mesh, scaleToMm, itemExpressId, diagnostics) {
|
|
14273
14346
|
const kernel = getKernel();
|
|
14274
14347
|
const { vertices, indices } = mesh;
|
|
14275
14348
|
const point = (index) => [
|
|
@@ -14278,8 +14351,8 @@ function sewMeshToSolid(mesh, scaleToMm, productExpressId, diagnostics) {
|
|
|
14278
14351
|
(vertices[index * 3 + 2] ?? 0) * scaleToMm
|
|
14279
14352
|
];
|
|
14280
14353
|
const triangles = [];
|
|
14281
|
-
for (let
|
|
14282
|
-
const face = kernel.buildTriFace(point(indices[
|
|
14354
|
+
for (let index = 0; index + 2 < indices.length; index += 3) {
|
|
14355
|
+
const face = kernel.buildTriFace(point(indices[index] ?? 0), point(indices[index + 1] ?? 0), point(indices[index + 2] ?? 0));
|
|
14283
14356
|
if (face !== null) triangles.push(face);
|
|
14284
14357
|
}
|
|
14285
14358
|
if (triangles.length === 0) return null;
|
|
@@ -14289,38 +14362,45 @@ function sewMeshToSolid(mesh, scaleToMm, productExpressId, diagnostics) {
|
|
|
14289
14362
|
if (valid !== null && valid.ok) return valid.value;
|
|
14290
14363
|
cast[Symbol.dispose]();
|
|
14291
14364
|
return null;
|
|
14292
|
-
} catch (
|
|
14293
|
-
diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", `Mesh sewing failed: ${errMsg$1(
|
|
14365
|
+
} catch (cause) {
|
|
14366
|
+
diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", `Mesh sewing failed: ${errMsg$1(cause)}`, itemExpressId));
|
|
14294
14367
|
return null;
|
|
14295
14368
|
} finally {
|
|
14296
14369
|
for (const face of triangles) kernel.dispose(face);
|
|
14297
14370
|
}
|
|
14298
14371
|
}
|
|
14299
|
-
|
|
14300
|
-
|
|
14301
|
-
const
|
|
14302
|
-
|
|
14372
|
+
/** Streams a product once, then separates web-ifc geometry by Body item express ID. */
|
|
14373
|
+
function collectItemMeshes(reader, productExpressId, itemExpressIds) {
|
|
14374
|
+
const byItem = /* @__PURE__ */ new Map();
|
|
14375
|
+
const soleItemExpressId = itemExpressIds.size === 1 ? [...itemExpressIds][0] : void 0;
|
|
14303
14376
|
reader.streamMeshes([productExpressId], (flatMesh) => {
|
|
14304
14377
|
try {
|
|
14305
14378
|
const geometries = flatMesh.geometries;
|
|
14306
|
-
for (let
|
|
14307
|
-
const placed = geometries.get(
|
|
14308
|
-
const
|
|
14379
|
+
for (let geometryIndex = 0; geometryIndex < geometries.size(); geometryIndex++) {
|
|
14380
|
+
const placed = geometries.get(geometryIndex);
|
|
14381
|
+
const itemExpressId = itemExpressIds.has(placed.geometryExpressID) ? placed.geometryExpressID : soleItemExpressId;
|
|
14382
|
+
if (itemExpressId === void 0) continue;
|
|
14383
|
+
const accumulator = byItem.get(itemExpressId) ?? {
|
|
14384
|
+
vertices: [],
|
|
14385
|
+
indices: []
|
|
14386
|
+
};
|
|
14387
|
+
byItem.set(itemExpressId, accumulator);
|
|
14388
|
+
const geometry = reader.getGeometry(placed.geometryExpressID);
|
|
14309
14389
|
try {
|
|
14310
|
-
const
|
|
14311
|
-
const
|
|
14312
|
-
const
|
|
14313
|
-
const
|
|
14314
|
-
|
|
14315
|
-
|
|
14316
|
-
const
|
|
14317
|
-
const
|
|
14318
|
-
|
|
14390
|
+
const vertices = reader.getVertexArray(geometry.GetVertexData(), geometry.GetVertexDataSize());
|
|
14391
|
+
const indices = reader.getIndexArray(geometry.GetIndexData(), geometry.GetIndexDataSize());
|
|
14392
|
+
const transform = placed.flatTransformation;
|
|
14393
|
+
const base = accumulator.vertices.length / 3;
|
|
14394
|
+
const vertexCount = vertices.length / 6;
|
|
14395
|
+
for (let vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) {
|
|
14396
|
+
const x = vertices[vertexIndex * 6] ?? 0;
|
|
14397
|
+
const y = vertices[vertexIndex * 6 + 1] ?? 0;
|
|
14398
|
+
const z = vertices[vertexIndex * 6 + 2] ?? 0;
|
|
14399
|
+
accumulator.vertices.push((transform[0] ?? 1) * x + (transform[4] ?? 0) * y + (transform[8] ?? 0) * z + (transform[12] ?? 0), (transform[1] ?? 0) * x + (transform[5] ?? 1) * y + (transform[9] ?? 0) * z + (transform[13] ?? 0), (transform[2] ?? 0) * x + (transform[6] ?? 0) * y + (transform[10] ?? 1) * z + (transform[14] ?? 0));
|
|
14319
14400
|
}
|
|
14320
|
-
for (let
|
|
14321
|
-
base += vertexCount;
|
|
14401
|
+
for (let index = 0; index < indices.length; index++) accumulator.indices.push(base + (indices[index] ?? 0));
|
|
14322
14402
|
} finally {
|
|
14323
|
-
|
|
14403
|
+
geometry.delete();
|
|
14324
14404
|
}
|
|
14325
14405
|
}
|
|
14326
14406
|
} finally {
|
|
@@ -14328,12 +14408,62 @@ function collectMesh(reader, productExpressId) {
|
|
|
14328
14408
|
releaseEmbind(flatMesh);
|
|
14329
14409
|
}
|
|
14330
14410
|
});
|
|
14331
|
-
|
|
14411
|
+
return new Map([...byItem].map(([itemExpressId, mesh]) => [itemExpressId, {
|
|
14412
|
+
vertices: new Float32Array(mesh.vertices),
|
|
14413
|
+
indices: new Uint32Array(mesh.indices)
|
|
14414
|
+
}]));
|
|
14415
|
+
}
|
|
14416
|
+
function readTriangulatedMesh(rawPoints, rawTriangles, scale, worldTransform) {
|
|
14417
|
+
if (!Array.isArray(rawPoints) || !Array.isArray(rawTriangles)) return null;
|
|
14418
|
+
const vertices = [];
|
|
14419
|
+
const indices = [];
|
|
14420
|
+
const lengthFactor = scale * 1e3;
|
|
14421
|
+
for (const rawPoint of rawPoints) {
|
|
14422
|
+
if (!Array.isArray(rawPoint) || rawPoint.length < 3) return null;
|
|
14423
|
+
const placed = transformPoint([
|
|
14424
|
+
(readMeasure(rawPoint[0]) ?? 0) * lengthFactor,
|
|
14425
|
+
(readMeasure(rawPoint[1]) ?? 0) * lengthFactor,
|
|
14426
|
+
(readMeasure(rawPoint[2]) ?? 0) * lengthFactor
|
|
14427
|
+
], worldTransform);
|
|
14428
|
+
vertices.push(placed[0], placed[1], placed[2]);
|
|
14429
|
+
}
|
|
14430
|
+
for (const rawTriangle of rawTriangles) {
|
|
14431
|
+
if (!Array.isArray(rawTriangle) || rawTriangle.length < 3) return null;
|
|
14432
|
+
indices.push((readMeasure(rawTriangle[0]) ?? 1) - 1, (readMeasure(rawTriangle[1]) ?? 1) - 1, (readMeasure(rawTriangle[2]) ?? 1) - 1);
|
|
14433
|
+
}
|
|
14332
14434
|
return {
|
|
14333
14435
|
vertices: new Float32Array(vertices),
|
|
14334
14436
|
indices: new Uint32Array(indices)
|
|
14335
14437
|
};
|
|
14336
14438
|
}
|
|
14439
|
+
function transformPoint(point, transform) {
|
|
14440
|
+
if (transform === null) return point;
|
|
14441
|
+
const m = transform.linear;
|
|
14442
|
+
return [
|
|
14443
|
+
(m[0] ?? 1) * point[0] + (m[1] ?? 0) * point[1] + (m[2] ?? 0) * point[2] + transform.translation[0],
|
|
14444
|
+
(m[3] ?? 0) * point[0] + (m[4] ?? 1) * point[1] + (m[5] ?? 0) * point[2] + transform.translation[1],
|
|
14445
|
+
(m[6] ?? 0) * point[0] + (m[7] ?? 0) * point[1] + (m[8] ?? 1) * point[2] + transform.translation[2]
|
|
14446
|
+
];
|
|
14447
|
+
}
|
|
14448
|
+
function packBinaryStl(vertices, indices, scaleToMm) {
|
|
14449
|
+
const triCount = Math.floor(indices.length / 3);
|
|
14450
|
+
const buffer = /* @__PURE__ */ new ArrayBuffer(84 + triCount * 50);
|
|
14451
|
+
const view = new DataView(buffer);
|
|
14452
|
+
view.setUint32(80, triCount, true);
|
|
14453
|
+
let offset = 84;
|
|
14454
|
+
for (let t = 0; t < triCount; t++) {
|
|
14455
|
+
offset += 12;
|
|
14456
|
+
for (let c = 0; c < 3; c++) {
|
|
14457
|
+
const vi = (indices[t * 3 + c] ?? 0) * 3;
|
|
14458
|
+
view.setFloat32(offset, (vertices[vi] ?? 0) * scaleToMm, true);
|
|
14459
|
+
view.setFloat32(offset + 4, (vertices[vi + 1] ?? 0) * scaleToMm, true);
|
|
14460
|
+
view.setFloat32(offset + 8, (vertices[vi + 2] ?? 0) * scaleToMm, true);
|
|
14461
|
+
offset += 12;
|
|
14462
|
+
}
|
|
14463
|
+
offset += 2;
|
|
14464
|
+
}
|
|
14465
|
+
return new Uint8Array(buffer);
|
|
14466
|
+
}
|
|
14337
14467
|
var PARAMETRIC_PROFILE_BUILDERS = /* @__PURE__ */ new Map([
|
|
14338
14468
|
[WebIFC.IFCRECTANGLEPROFILEDEF, (f) => ({
|
|
14339
14469
|
kind: "RECTANGULAR",
|
|
@@ -14686,15 +14816,16 @@ function isIdentity(t) {
|
|
|
14686
14816
|
for (let i = 0; i < 9; i++) if (Math.abs((t.linear[i] ?? 0) - (expected[i] ?? 0)) > 1e-9) return false;
|
|
14687
14817
|
return Math.abs(t.translation[0]) < 1e-9 && Math.abs(t.translation[1]) < 1e-9 && Math.abs(t.translation[2]) < 1e-9;
|
|
14688
14818
|
}
|
|
14689
|
-
function
|
|
14819
|
+
function findBodyItems(reader, representations) {
|
|
14690
14820
|
for (const repRef of representations) {
|
|
14691
14821
|
const rep = reader.getLine(repRef.value);
|
|
14692
14822
|
if (rep === null) continue;
|
|
14693
14823
|
if (readLabel(rep["RepresentationIdentifier"]) !== "Body") continue;
|
|
14694
14824
|
const items = asRefArray(rep["Items"]);
|
|
14695
|
-
if (items === void 0
|
|
14696
|
-
return items
|
|
14825
|
+
if (items === void 0) return [];
|
|
14826
|
+
return items.map((item) => item.value);
|
|
14697
14827
|
}
|
|
14828
|
+
return null;
|
|
14698
14829
|
}
|
|
14699
14830
|
function readPolylinePoints(reader, ref, scale) {
|
|
14700
14831
|
const curveRef = asRef(ref);
|
|
@@ -14799,8 +14930,8 @@ function readLabel(value) {
|
|
|
14799
14930
|
if (typeof v === "string") return v;
|
|
14800
14931
|
}
|
|
14801
14932
|
}
|
|
14802
|
-
function
|
|
14803
|
-
return type === WebIFC.
|
|
14933
|
+
function isStreamedTessellatedType(type) {
|
|
14934
|
+
return type === WebIFC.IFCPOLYGONALFACESET || type === WebIFC.IFCFACEBASEDSURFACEMODEL;
|
|
14804
14935
|
}
|
|
14805
14936
|
function normalize(v) {
|
|
14806
14937
|
const len = Math.hypot(v[0], v[1], v[2]);
|
|
@@ -15139,6 +15270,7 @@ function readOwnerHistory(reader, ownerHistoryExpressId) {
|
|
|
15139
15270
|
}
|
|
15140
15271
|
//#endregion
|
|
15141
15272
|
//#region src/import/fromIfc.ts
|
|
15273
|
+
var testHooks = null;
|
|
15142
15274
|
/**
|
|
15143
15275
|
* Each enumerated physical-element IFC type and the {@link ImportedElementCategory}
|
|
15144
15276
|
* it maps to. Curtain-wall sub-components (IfcPlate/IfcMember) and stair/ramp
|
|
@@ -15187,6 +15319,7 @@ async function fromIfc(bytes, options = {}) {
|
|
|
15187
15319
|
const readerResult = await SpfReader.create(bytes, settings);
|
|
15188
15320
|
if (!readerResult.ok) return err(readerResult.error);
|
|
15189
15321
|
const reader = readerResult.value;
|
|
15322
|
+
const elements = [];
|
|
15190
15323
|
try {
|
|
15191
15324
|
reader.buildGuidMap();
|
|
15192
15325
|
const diagnostics = [];
|
|
@@ -15195,13 +15328,13 @@ async function fromIfc(bytes, options = {}) {
|
|
|
15195
15328
|
const spatialRoot = buildSpatialTree(reader, scale);
|
|
15196
15329
|
const containment = buildElementContainmentMap(reader);
|
|
15197
15330
|
const typeEnums = buildTypePredefinedMap(reader);
|
|
15198
|
-
const elements = [];
|
|
15199
15331
|
const byExpressId = /* @__PURE__ */ new Map();
|
|
15200
15332
|
for (const [type, category] of ELEMENT_TYPES) for (const expressId of reader.getLinesOfType(type)) {
|
|
15201
15333
|
const element = readElement(reader, expressId, category, scale, containment, typeEnums, options.skipGeometry ?? false, diagnostics);
|
|
15202
15334
|
if (element === null) continue;
|
|
15203
15335
|
elements.push(element);
|
|
15204
15336
|
byExpressId.set(element.expressId, element);
|
|
15337
|
+
testHooks?.afterElement?.(element, elements.length);
|
|
15205
15338
|
}
|
|
15206
15339
|
const report = appendIssues(emptyReport(), diagnostics);
|
|
15207
15340
|
const applicationName = readApplicationName(reader);
|
|
@@ -15215,6 +15348,7 @@ async function fromIfc(bytes, options = {}) {
|
|
|
15215
15348
|
};
|
|
15216
15349
|
return ok(model);
|
|
15217
15350
|
} catch (e) {
|
|
15351
|
+
disposeElements(elements);
|
|
15218
15352
|
return err(importError("IMPORT_FAILED", "Unexpected failure during IFC import", e));
|
|
15219
15353
|
} finally {
|
|
15220
15354
|
reader.close();
|
|
@@ -15226,6 +15360,7 @@ async function fromIfc(bytes, options = {}) {
|
|
|
15226
15360
|
* sub-read failure is isolated by its own reader and never aborts the element.
|
|
15227
15361
|
*/
|
|
15228
15362
|
function readElement(reader, expressId, category, scale, containment, typeEnums, skipGeometry, diagnostics) {
|
|
15363
|
+
let geometry = null;
|
|
15229
15364
|
try {
|
|
15230
15365
|
const line = reader.getLine(expressId);
|
|
15231
15366
|
if (line === null) {
|
|
@@ -15237,10 +15372,15 @@ function readElement(reader, expressId, category, scale, containment, typeEnums,
|
|
|
15237
15372
|
const predefinedType = readPredefinedType(line) ?? typeEnums.get(expressId);
|
|
15238
15373
|
const voidedBy = readVoids(reader, expressId).map((v) => v.openingExpressId);
|
|
15239
15374
|
const fills = findFills(reader, expressId);
|
|
15240
|
-
|
|
15375
|
+
geometry = skipGeometry ? {
|
|
15241
15376
|
fidelity: "NONE",
|
|
15242
|
-
|
|
15377
|
+
completeness: "NONE",
|
|
15378
|
+
solids: [],
|
|
15379
|
+
solid: null,
|
|
15380
|
+
bounds: null,
|
|
15381
|
+
volumeMm3: null
|
|
15243
15382
|
} : reconstructGeometry(reader, expressId, scale, voidedBy, diagnostics);
|
|
15383
|
+
testHooks?.afterGeometry?.(expressId, geometry);
|
|
15244
15384
|
const psets = readPsets(reader, expressId).map(toImportedPset);
|
|
15245
15385
|
const material = readMaterial(reader, expressId, scale);
|
|
15246
15386
|
const classification = readClassification(reader, expressId);
|
|
@@ -15263,6 +15403,7 @@ function readElement(reader, expressId, category, scale, containment, typeEnums,
|
|
|
15263
15403
|
...fills !== void 0 ? { fills } : {}
|
|
15264
15404
|
};
|
|
15265
15405
|
} catch (e) {
|
|
15406
|
+
if (geometry !== null) disposeGeometry(geometry);
|
|
15266
15407
|
diagnostics.push(issue("error", "ELEMENT_READ_FAILED", `Element ${expressId} reconstruction threw: ${errMsg(e)}`, expressId));
|
|
15267
15408
|
return null;
|
|
15268
15409
|
}
|
|
@@ -15284,42 +15425,134 @@ function findFills(reader, elementExpressId) {
|
|
|
15284
15425
|
* model. Falls back to the uncut solid (with a diagnostic) if a cut fails.
|
|
15285
15426
|
*/
|
|
15286
15427
|
function reconstructGeometry(reader, expressId, scale, voidedBy, diagnostics) {
|
|
15287
|
-
const base = toImportedGeometry(
|
|
15288
|
-
if (base.fidelity !== "PARAMETRIC" || base.
|
|
15289
|
-
|
|
15428
|
+
const base = toImportedGeometry(reader, expressId, scale, diagnostics);
|
|
15429
|
+
if (base.fidelity !== "PARAMETRIC" || base.completeness !== "COMPLETE" || voidedBy.length === 0) return base;
|
|
15430
|
+
const firstHost = base.solids[0];
|
|
15431
|
+
if (firstHost === void 0) return base;
|
|
15432
|
+
const hosts = [firstHost, ...base.solids.slice(1)];
|
|
15290
15433
|
for (const openingId of voidedBy) {
|
|
15291
15434
|
const opening = readBodyGeometry(reader, openingId, scale, diagnostics);
|
|
15292
15435
|
if (opening.kind !== "SOLID") continue;
|
|
15293
|
-
|
|
15294
|
-
|
|
15295
|
-
|
|
15296
|
-
|
|
15297
|
-
|
|
15298
|
-
|
|
15299
|
-
|
|
15300
|
-
|
|
15301
|
-
|
|
15436
|
+
try {
|
|
15437
|
+
for (let hostIndex = 0; hostIndex < hosts.length; hostIndex++) {
|
|
15438
|
+
const host = hosts[hostIndex];
|
|
15439
|
+
if (host === void 0) continue;
|
|
15440
|
+
const cutResult = cut(host, opening.solid);
|
|
15441
|
+
if (!cutResult.ok) {
|
|
15442
|
+
diagnostics.push(issue("warning", "VOID_SUBTRACTION_FAILED", `Opening ${openingId} could not be subtracted from element ${expressId}: ${cutResult.error.message}`, expressId));
|
|
15443
|
+
continue;
|
|
15444
|
+
}
|
|
15445
|
+
host[Symbol.dispose]();
|
|
15446
|
+
hosts[hostIndex] = cutResult.value;
|
|
15447
|
+
}
|
|
15448
|
+
} finally {
|
|
15449
|
+
opening.solid[Symbol.dispose]();
|
|
15450
|
+
}
|
|
15451
|
+
}
|
|
15452
|
+
return completeImportedGeometry("PARAMETRIC", hosts, expressId, diagnostics);
|
|
15453
|
+
}
|
|
15454
|
+
function toImportedGeometry(reader, expressId, scale, diagnostics) {
|
|
15455
|
+
const body = readBodyItems(reader, expressId, scale, diagnostics);
|
|
15456
|
+
const solids = [];
|
|
15457
|
+
let hasTessellatedSolid = false;
|
|
15458
|
+
let lossyMesh = null;
|
|
15459
|
+
for (const item of body.items) if (item.kind === "SOLID") {
|
|
15460
|
+
solids.push(item.solid);
|
|
15461
|
+
hasTessellatedSolid ||= item.lossy;
|
|
15462
|
+
} else if (item.kind === "MESH") {
|
|
15463
|
+
const meshAccumulator = lossyMesh ?? {
|
|
15464
|
+
vertices: [],
|
|
15465
|
+
indices: []
|
|
15466
|
+
};
|
|
15467
|
+
lossyMesh = meshAccumulator;
|
|
15468
|
+
const vertexOffset = meshAccumulator.vertices.length / 3;
|
|
15469
|
+
for (const vertex of item.vertices) meshAccumulator.vertices.push(vertex);
|
|
15470
|
+
for (const index of item.indices) meshAccumulator.indices.push(vertexOffset + index);
|
|
15471
|
+
}
|
|
15472
|
+
const completeness = body.hasBody && body.itemCount > 0 && solids.length === body.itemCount ? "COMPLETE" : solids.length > 0 ? "PARTIAL" : "NONE";
|
|
15473
|
+
if (completeness === "PARTIAL") diagnostics.push(issue("warning", "PARTIAL_BODY_RECONSTRUCTION", `Reconstructed ${solids.length} of ${body.itemCount} Body items`, expressId, {
|
|
15474
|
+
reconstructedItems: solids.length,
|
|
15475
|
+
bodyItems: body.itemCount
|
|
15476
|
+
}));
|
|
15477
|
+
else if (completeness === "NONE" && body.hasBody) diagnostics.push(issue("warning", "BODY_RECONSTRUCTION_NONE", `The Product has ${body.itemCount} Body item(s), but none reconstructed as solids`, expressId, { bodyItems: body.itemCount }));
|
|
15478
|
+
const fidelity = lossyMesh !== null ? "TESSELLATED_LOSSY" : hasTessellatedSolid ? "TESSELLATED_MANIFOLD" : solids.length > 0 ? "PARAMETRIC" : "NONE";
|
|
15479
|
+
const aggregate = completeness === "COMPLETE" ? measureCompleteBody(solids, expressId, diagnostics) : {
|
|
15480
|
+
bounds: null,
|
|
15481
|
+
volumeMm3: null
|
|
15482
|
+
};
|
|
15302
15483
|
return {
|
|
15303
|
-
fidelity
|
|
15304
|
-
|
|
15484
|
+
fidelity,
|
|
15485
|
+
completeness,
|
|
15486
|
+
solids,
|
|
15487
|
+
solid: completeness === "COMPLETE" && solids.length === 1 ? solids[0] ?? null : null,
|
|
15488
|
+
...aggregate,
|
|
15489
|
+
...lossyMesh !== null ? {
|
|
15490
|
+
meshVertices: new Float32Array(lossyMesh.vertices),
|
|
15491
|
+
meshIndices: new Uint32Array(lossyMesh.indices)
|
|
15492
|
+
} : {}
|
|
15305
15493
|
};
|
|
15306
15494
|
}
|
|
15307
|
-
function
|
|
15308
|
-
if (result.kind === "SOLID") return {
|
|
15309
|
-
fidelity: result.lossy ? "TESSELLATED_MANIFOLD" : "PARAMETRIC",
|
|
15310
|
-
solid: result.solid
|
|
15311
|
-
};
|
|
15312
|
-
if (result.kind === "MESH") return {
|
|
15313
|
-
fidelity: "TESSELLATED_LOSSY",
|
|
15314
|
-
solid: null,
|
|
15315
|
-
meshVertices: result.vertices,
|
|
15316
|
-
meshIndices: result.indices
|
|
15317
|
-
};
|
|
15495
|
+
function completeImportedGeometry(fidelity, solids, expressId, diagnostics) {
|
|
15318
15496
|
return {
|
|
15319
|
-
fidelity
|
|
15320
|
-
|
|
15497
|
+
fidelity,
|
|
15498
|
+
completeness: "COMPLETE",
|
|
15499
|
+
solids,
|
|
15500
|
+
solid: solids.length === 1 ? solids[0] : null,
|
|
15501
|
+
...measureCompleteBody(solids, expressId, diagnostics)
|
|
15321
15502
|
};
|
|
15322
15503
|
}
|
|
15504
|
+
function measureCompleteBody(solids, expressId, diagnostics) {
|
|
15505
|
+
try {
|
|
15506
|
+
const first = solids[0];
|
|
15507
|
+
if (first === void 0) return {
|
|
15508
|
+
bounds: null,
|
|
15509
|
+
volumeMm3: null
|
|
15510
|
+
};
|
|
15511
|
+
const initial = getBounds(first);
|
|
15512
|
+
let xMin = initial.xMin;
|
|
15513
|
+
let xMax = initial.xMax;
|
|
15514
|
+
let yMin = initial.yMin;
|
|
15515
|
+
let yMax = initial.yMax;
|
|
15516
|
+
let zMin = initial.zMin;
|
|
15517
|
+
let zMax = initial.zMax;
|
|
15518
|
+
let volumeMm3 = 0;
|
|
15519
|
+
for (const solid of solids) {
|
|
15520
|
+
const measured = measureVolume(solid);
|
|
15521
|
+
if (!measured.ok) throw new Error(measured.error.message);
|
|
15522
|
+
volumeMm3 += measured.value;
|
|
15523
|
+
const itemBounds = getBounds(solid);
|
|
15524
|
+
xMin = Math.min(xMin, itemBounds.xMin);
|
|
15525
|
+
xMax = Math.max(xMax, itemBounds.xMax);
|
|
15526
|
+
yMin = Math.min(yMin, itemBounds.yMin);
|
|
15527
|
+
yMax = Math.max(yMax, itemBounds.yMax);
|
|
15528
|
+
zMin = Math.min(zMin, itemBounds.zMin);
|
|
15529
|
+
zMax = Math.max(zMax, itemBounds.zMax);
|
|
15530
|
+
}
|
|
15531
|
+
return {
|
|
15532
|
+
bounds: {
|
|
15533
|
+
xMin,
|
|
15534
|
+
xMax,
|
|
15535
|
+
yMin,
|
|
15536
|
+
yMax,
|
|
15537
|
+
zMin,
|
|
15538
|
+
zMax
|
|
15539
|
+
},
|
|
15540
|
+
volumeMm3
|
|
15541
|
+
};
|
|
15542
|
+
} catch (cause) {
|
|
15543
|
+
diagnostics.push(issue("warning", "BODY_AGGREGATE_MEASUREMENT_FAILED", `Complete Body aggregate measurement failed: ${errMsg(cause)}`, expressId));
|
|
15544
|
+
return {
|
|
15545
|
+
bounds: null,
|
|
15546
|
+
volumeMm3: null
|
|
15547
|
+
};
|
|
15548
|
+
}
|
|
15549
|
+
}
|
|
15550
|
+
function disposeGeometry(geometry) {
|
|
15551
|
+
for (const solid of geometry.solids) solid[Symbol.dispose]();
|
|
15552
|
+
}
|
|
15553
|
+
function disposeElements(elements) {
|
|
15554
|
+
for (const element of elements) disposeGeometry(element.geometry);
|
|
15555
|
+
}
|
|
15323
15556
|
function toImportedPset(pset) {
|
|
15324
15557
|
return {
|
|
15325
15558
|
name: pset.name,
|
|
@@ -15397,7 +15630,7 @@ function errMsg(e) {
|
|
|
15397
15630
|
* with the model (it is not reclaimed automatically when the model is GC'd).
|
|
15398
15631
|
*/
|
|
15399
15632
|
function disposeImportedModel(model) {
|
|
15400
|
-
for (const el of model.elements) el.geometry.solid
|
|
15633
|
+
for (const el of model.elements) for (const solid of el.geometry.solids) solid[Symbol.dispose]();
|
|
15401
15634
|
}
|
|
15402
15635
|
//#endregion
|
|
15403
15636
|
//#region src/specs/materialSpec.ts
|
package/dist/import/fromIfc.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Result } from 'brepjs';
|
|
2
2
|
import { BimError } from '../errors/bimError.js';
|
|
3
|
-
import { ImportedModel } from './importedModel.js';
|
|
3
|
+
import { ImportedModel, ImportedElement, ImportedGeometry } from './importedModel.js';
|
|
4
4
|
export type { ImportedModel } from './importedModel.js';
|
|
5
5
|
export interface FromIfcOptions {
|
|
6
6
|
/** Activate web-ifc's large-coordinate recentering on open. Default false. */
|
|
@@ -8,6 +8,12 @@ export interface FromIfcOptions {
|
|
|
8
8
|
/** Skip body-geometry reconstruction for fast metadata-only reads. Default false. */
|
|
9
9
|
readonly skipGeometry?: boolean | undefined;
|
|
10
10
|
}
|
|
11
|
+
export interface FromIfcTestHooks {
|
|
12
|
+
readonly afterGeometry?: ((expressId: number, geometry: ImportedGeometry) => void) | undefined;
|
|
13
|
+
readonly afterElement?: ((element: ImportedElement, accumulatedCount: number) => void) | undefined;
|
|
14
|
+
}
|
|
15
|
+
/** Package-internal deterministic failure seam for import ownership tests. */
|
|
16
|
+
export declare function setFromIfcTestHooksForTesting(hooks: FromIfcTestHooks | null): void;
|
|
11
17
|
/**
|
|
12
18
|
* Reads an IFC STEP-SPF byte buffer into an {@link ImportedModel}: schema, unit
|
|
13
19
|
* scale, spatial tree, and one {@link ImportedElement} per physical product
|
|
@@ -7,7 +7,7 @@ import { SpfReader } from './spfReader.js';
|
|
|
7
7
|
* Outcome of reconstructing a single product's body geometry.
|
|
8
8
|
*
|
|
9
9
|
* - `SOLID` — a brepjs ValidSolid was rebuilt (parametrically from a swept
|
|
10
|
-
* solid, or by a manifold
|
|
10
|
+
* solid, or by sewing a manifold tessellated mesh).
|
|
11
11
|
* - `MESH` — geometry exists but could not be turned into a closed solid;
|
|
12
12
|
* raw triangle data is returned and flagged lossy via `diagnostic`.
|
|
13
13
|
* - `NONE` — the product carries no recognised body representation.
|
|
@@ -24,6 +24,16 @@ export type GeometryResult = {
|
|
|
24
24
|
} | {
|
|
25
25
|
readonly kind: 'NONE';
|
|
26
26
|
};
|
|
27
|
+
export interface BodyGeometryItems {
|
|
28
|
+
readonly hasBody: boolean;
|
|
29
|
+
readonly itemCount: number;
|
|
30
|
+
readonly items: readonly GeometryResult[];
|
|
31
|
+
}
|
|
32
|
+
export interface GeometryReadTestHooks {
|
|
33
|
+
readonly afterItemSolid?: ((itemExpressId: number, solid: ValidSolid) => void) | undefined;
|
|
34
|
+
}
|
|
35
|
+
/** Package-internal deterministic failure seam for import ownership tests. */
|
|
36
|
+
export declare function setGeometryReadTestHooksForTesting(hooks: GeometryReadTestHooks | null): void;
|
|
27
37
|
/**
|
|
28
38
|
* Reconstructs the `Body` (SweptSolid) representation of a product into a brepjs
|
|
29
39
|
* solid, falling back to tessellated mesh import when no parametric path exists.
|
|
@@ -34,6 +44,8 @@ export type GeometryResult = {
|
|
|
34
44
|
* single bad element never aborts a model import.
|
|
35
45
|
*/
|
|
36
46
|
export declare function readBodyGeometry(reader: SpfReader, productExpressId: number, scale: number, diagnostics: ValidationIssue[]): GeometryResult;
|
|
47
|
+
/** Reconstructs each supported IFC Body item independently in representation order. */
|
|
48
|
+
export declare function readBodyItems(reader: SpfReader, productExpressId: number, scale: number, diagnostics: ValidationIssue[]): BodyGeometryItems;
|
|
37
49
|
/**
|
|
38
50
|
* Reads an IfcProfileDef into a brepjs Profile (mm). Mirrors the writer's
|
|
39
51
|
* profile-def emission (geometryWriter.writeProfile / profileDefWriter).
|