web-ifc-viewer
Version:
354 lines • 15.8 kB
JavaScript
import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry';
import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2';
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial';
import { BufferAttribute, BufferGeometry, DynamicDrawUsage, Line3, LineSegments, Matrix4, MeshBasicMaterial, Plane, Vector3 } from 'three';
import { IFCBEAM, IFCBUILDINGELEMENTPROXY, IFCCOLUMN, IFCDOOR, IFCFOOTING, IFCFURNISHINGELEMENT, IFCMEMBER, IFCPLATE, IFCPROXY, IFCROOF, IFCSLAB, IFCSTAIRFLIGHT, IFCWALL, IFCWALLSTANDARDCASE, IFCWINDOW } from 'web-ifc';
export class ClippingEdges {
constructor(clippingPlane) {
this.edges = {};
this.isVisible = true;
this.inverseMatrix = new Matrix4();
this.localPlane = new Plane();
this.tempLine = new Line3();
this.tempVector = new Vector3();
this.stylesInitialized = false;
this.clippingPlane = clippingPlane;
}
get visible() {
return this.isVisible;
}
set visible(visible) {
this.isVisible = visible;
const allEdges = Object.values(this.edges);
allEdges.forEach((edges) => {
edges.mesh.visible = visible;
if (visible)
ClippingEdges.context.getScene().add(edges.mesh);
else
edges.mesh.removeFromParent();
});
if (visible)
this.updateEdges();
}
// Initializes the helper geometry used to compute the vertices
static newGeneratorGeometry() {
// create line geometry with enough data to hold 100000 segments
const generatorGeometry = new BufferGeometry();
const linePosAttr = new BufferAttribute(new Float32Array(300000), 3, false);
linePosAttr.setUsage(DynamicDrawUsage);
generatorGeometry.setAttribute('position', linePosAttr);
return generatorGeometry;
}
dispose() {
Object.values(this.edges).forEach((edge) => {
if (edge.generatorGeometry.boundsTree)
edge.generatorGeometry.disposeBoundsTree();
edge.generatorGeometry.dispose();
if (edge.mesh.geometry.boundsTree)
edge.mesh.geometry.disposeBoundsTree();
edge.mesh.geometry.dispose();
edge.mesh.removeFromParent();
edge.mesh = null;
});
this.edges = null;
this.clippingPlane = null;
}
disposeStylesAndHelpers() {
if (ClippingEdges.basicEdges) {
ClippingEdges.basicEdges.removeFromParent();
ClippingEdges.basicEdges.geometry.dispose();
ClippingEdges.basicEdges = null;
ClippingEdges.basicEdges = new LineSegments();
}
ClippingEdges.context = null;
ClippingEdges.ifc = null;
ClippingEdges.edgesParent = undefined;
if (!ClippingEdges.styles)
return;
const styles = Object.values(ClippingEdges.styles);
styles.forEach((style) => {
style.ids.length = 0;
style.meshes.forEach((mesh) => {
mesh.removeFromParent();
mesh.geometry.dispose();
if (mesh.geometry.boundsTree)
mesh.geometry.disposeBoundsTree();
if (Array.isArray(mesh.material))
mesh.material.forEach((mat) => mat.dispose());
else
mesh.material.dispose();
});
style.meshes.length = 0;
style.categories.length = 0;
style.material.dispose();
});
ClippingEdges.styles = null;
ClippingEdges.styles = {};
}
async updateEdges() {
if (ClippingEdges.createDefaultIfcStyles) {
await this.updateIfcStyles();
}
if (ClippingEdges.forceStyleUpdate) {
this.updateSubsetsTranformation();
}
Object.keys(ClippingEdges.styles).forEach((styleName) => {
try {
// this can trow error if there is an empty mesh, we still want to update other edges so we catch ere
this.drawEdges(styleName);
}
catch (e) {
console.error('error in drawing edges', e);
}
});
}
// Creates a new style that applies to all clipping edges for IFC models
static async newStyle(styleName, categories, material = ClippingEdges.defaultMaterial) {
const subsets = [];
const models = ClippingEdges.context.items.ifcModels;
for (let i = 0; i < models.length; i++) {
// eslint-disable-next-line no-await-in-loop
const subset = await ClippingEdges.newSubset(styleName, models[i], categories);
if (subset) {
subsets.push(subset);
}
}
material.clippingPlanes = ClippingEdges.context.getClippingPlanes();
ClippingEdges.styles[styleName] = {
ids: models.map((model) => model.modelID),
categories,
material,
meshes: subsets
};
}
// Creates a new style that applies to all clipping edges for generic models
static async newStyleFromMesh(styleName, meshes, material = ClippingEdges.defaultMaterial) {
const ids = meshes.map((mesh) => mesh.modelID);
meshes.forEach((mesh) => {
if (!mesh.geometry.boundsTree)
mesh.geometry.computeBoundsTree();
});
material.clippingPlanes = ClippingEdges.context.getClippingPlanes();
ClippingEdges.styles[styleName] = {
ids,
categories: [],
material,
meshes
};
}
async updateStylesIfcGeometry() {
const styleNames = Object.keys(ClippingEdges.styles);
for (let i = 0; i < styleNames.length; i++) {
const name = styleNames[i];
const style = ClippingEdges.styles[name];
const models = ClippingEdges.context.items.ifcModels;
style.meshes.length = 0;
for (let i = 0; i < models.length; i++) {
// eslint-disable-next-line no-await-in-loop
const subset = await ClippingEdges.newSubset(name, models[i], style.categories);
if (subset) {
style.meshes.push(subset);
}
}
}
}
updateSubsetsTranformation() {
const styleNames = Object.keys(ClippingEdges.styles);
for (let i = 0; i < styleNames.length; i++) {
const styleName = styleNames[i];
const style = ClippingEdges.styles[styleName];
style.meshes.forEach((mesh) => {
const model = ClippingEdges.context.items.ifcModels.find((model) => model.modelID === mesh.modelID);
if (model) {
mesh.position.copy(model.position);
mesh.rotation.copy(model.rotation);
mesh.scale.copy(model.scale);
}
});
}
ClippingEdges.forceStyleUpdate = false;
}
async updateIfcStyles() {
if (!this.stylesInitialized) {
await this.createDefaultIfcStyles();
}
if (ClippingEdges.forceStyleUpdate) {
await this.updateStylesIfcGeometry();
ClippingEdges.forceStyleUpdate = false;
}
}
// Creates some basic styles so that users don't have to create it each time
async createDefaultIfcStyles() {
if (Object.keys(ClippingEdges.styles).length === 0) {
await ClippingEdges.newStyle('thick', [
IFCWALLSTANDARDCASE,
IFCWALL,
IFCSLAB,
IFCSTAIRFLIGHT,
IFCCOLUMN,
IFCBEAM,
IFCROOF,
IFCBUILDINGELEMENTPROXY,
IFCPROXY
], new LineMaterial({ color: 0x000000, linewidth: 0.0015 }));
await ClippingEdges.newStyle('thin', [
IFCWINDOW,
IFCPLATE,
IFCMEMBER,
IFCDOOR,
IFCFURNISHINGELEMENT,
IFCPROXY,
IFCBUILDINGELEMENTPROXY,
IFCFOOTING
], new LineMaterial({ color: 0x333333, linewidth: 0.001 }));
this.stylesInitialized = true;
}
}
// Creates a new subset. This allows to apply a style just to a specific set of items
static async newSubset(styleName, model, categories) {
const modelID = model.modelID;
const ids = await this.getItemIDs(modelID, categories);
// If no items were found, no geometry is created for this style
if (!ids.length)
return null;
const manager = this.ifc.loader.ifcManager;
let subset;
if (ids.length > 0) {
subset = manager.createSubset({
modelID,
ids,
customID: styleName,
material: ClippingEdges.invisibleMaterial,
removePrevious: true,
scene: ClippingEdges.context.getScene(),
applyBVH: true
});
}
else {
subset = manager.getSubset(modelID, ClippingEdges.invisibleMaterial, styleName);
}
subset.position.copy(model.position);
subset.rotation.copy(model.rotation);
subset.scale.copy(model.scale);
return subset;
}
static async getItemIDs(modelID, categories) {
const ids = [];
for (let j = 0; j < categories.length; j++) {
// eslint-disable-next-line no-await-in-loop
const found = await this.ifc.getAllItemsOfType(modelID, categories[j], false);
ids.push(...found);
}
const visibleItems = this.getVisibileItems(modelID);
return ids.filter((id) => visibleItems.has(id));
}
static getVisibileItems(modelID) {
const visibleItems = new Set();
const model = this.context.items.ifcModels.find((model) => model.modelID === modelID);
if (!model)
throw new Error('IFC model was not found for computing clipping edges.');
if (!model.geometry.index)
throw new Error('Indices were not found for clipping edges.');
const indices = new Set(model.geometry.index.array);
indices.forEach((index) => {
visibleItems.add(model.geometry.attributes.expressID.getX(index));
});
return visibleItems;
}
// Creates the geometry of the clipping edges
newThickEdges(styleName) {
const material = ClippingEdges.styles[styleName].material;
const thickLineGeometry = new LineSegmentsGeometry();
const thickEdges = new LineSegments2(thickLineGeometry, material);
thickEdges.material.polygonOffset = true;
thickEdges.material.polygonOffsetFactor = -2;
thickEdges.material.polygonOffsetUnits = 1;
thickEdges.renderOrder = 3;
return thickEdges;
}
// Source: https://gkjohnson.github.io/three-mesh-bvh/example/bundle/clippedEdges.html
drawEdges(styleName) {
const style = ClippingEdges.styles[styleName];
// if (!style.subsets.geometry.boundsTree) return;
if (!this.edges[styleName]) {
this.edges[styleName] = {
generatorGeometry: ClippingEdges.newGeneratorGeometry(),
mesh: this.newThickEdges(styleName)
};
}
const edges = this.edges[styleName];
let index = 0;
const posAttr = edges.generatorGeometry.attributes.position;
// @ts-ignore
posAttr.array.fill(0);
const notEmptyMeshes = style.meshes.filter((subset) => subset.geometry);
notEmptyMeshes.forEach((mesh) => {
if (!mesh.geometry.boundsTree) {
throw new Error('Boundstree not found for clipping edges subset.');
}
this.inverseMatrix.copy(mesh.matrixWorld).invert();
this.localPlane.copy(this.clippingPlane).applyMatrix4(this.inverseMatrix);
mesh.geometry.boundsTree.shapecast({
intersectsBounds: (box) => {
return this.localPlane.intersectsBox(box);
},
// @ts-ignore
intersectsTriangle: (tri) => {
// check each triangle edge to see if it intersects with the plane. If so then
// add it to the list of segments.
let count = 0;
this.tempLine.start.copy(tri.a);
this.tempLine.end.copy(tri.b);
if (this.localPlane.intersectLine(this.tempLine, this.tempVector)) {
const result = this.tempVector.applyMatrix4(mesh.matrixWorld);
posAttr.setXYZ(index, result.x, result.y, result.z);
count++;
index++;
}
this.tempLine.start.copy(tri.b);
this.tempLine.end.copy(tri.c);
if (this.localPlane.intersectLine(this.tempLine, this.tempVector)) {
const result = this.tempVector.applyMatrix4(mesh.matrixWorld);
posAttr.setXYZ(index, result.x, result.y, result.z);
count++;
index++;
}
this.tempLine.start.copy(tri.c);
this.tempLine.end.copy(tri.a);
if (this.localPlane.intersectLine(this.tempLine, this.tempVector)) {
const result = this.tempVector.applyMatrix4(mesh.matrixWorld);
posAttr.setXYZ(index, result.x, result.y, result.z);
count++;
index++;
}
// If we only intersected with one or three sides then just remove it. This could be handled
// more gracefully.
if (count !== 2) {
index -= count;
}
}
});
});
// set the draw range to only the new segments and offset the lines so they don't intersect with the geometry
edges.mesh.geometry.setDrawRange(0, index);
edges.mesh.position.copy(this.clippingPlane.normal).multiplyScalar(0.0001);
posAttr.needsUpdate = true;
// Update the edges geometry only if there is no NaN in the output (which means there's been an error)
if (!Number.isNaN(edges.generatorGeometry.attributes.position.array[0])) {
ClippingEdges.basicEdges.geometry = edges.generatorGeometry;
edges.mesh.geometry.fromLineSegments(ClippingEdges.basicEdges);
const parent = ClippingEdges.edgesParent || ClippingEdges.context.getScene();
parent.add(edges.mesh);
ClippingEdges.context.renderer.postProduction.excludedItems.add(edges.mesh);
}
}
}
ClippingEdges.styles = {};
ClippingEdges.forceStyleUpdate = false;
ClippingEdges.createDefaultIfcStyles = true;
ClippingEdges.edgesParent = null;
ClippingEdges.invisibleMaterial = new MeshBasicMaterial({ visible: false });
ClippingEdges.defaultMaterial = new LineMaterial({ color: 0x000000, linewidth: 0.001 });
// Helpers
ClippingEdges.basicEdges = new LineSegments();
//# sourceMappingURL=clipping-edges.js.map