@xeokit/xeokit-sdk
Version:
3D BIM IFC Viewer SDK for AEC engineering applications. Open Source JavaScript Toolkit based on pure WebGL for top performance, real-world coordinates and full double precision
257 lines (242 loc) • 12.4 kB
JavaScript
import { math } from "../math/math.js";
import { Mesh } from "../mesh/Mesh.js";
import { ReadableGeometry } from "../geometry/ReadableGeometry.js";
import { buildLineGeometry } from "../geometry/index.js";
import { PhongMaterial } from "../materials/PhongMaterial.js";
const tempVec3a = math.vec3();
/**
* @desc Implements hatching for Solid objects on a {@link Scene}.
*
* [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/slicing/SectionPlanesPlugin_Duplex_SectionCaps.html)]
*
* ##Overview
*
* The WebGL implementation for capping sliced 3D objects works by first calculating intersection segments where a cutting
* plane meets the object's edges. These segments form a contour that is triangulated using the Earcut algorithm, which
* handles any internal holes efficiently. The resulting triangulated cap is then integrated into the original mesh with
* appropriate normals and UVs.
*
* ##Usage
*
* In the example, we'll start by enabling readable geometry on the viewer.
*
* Then we'll position the camera, and configure the near and far perspective and orthographic
* clipping planes. Finally, we'll use {@link XKTLoaderPlugin} to load the Duplex model.
*
* ````javascript
* const viewer = new Viewer({
* canvasId: "myCanvas",
* transparent: true,
* readableGeometryEnabled: true
* });
*
* viewer.camera.eye = [-2.341298674548419, 22.43987089731119, 7.236688436028655];
* viewer.camera.look = [4.399999999999963, 3.7240000000000606, 8.899000000000006];
* viewer.camera.up = [0.9102954845584759, 0.34781746407929504, 0.22446635042673466];
*
* const cameraControl = viewer.cameraControl;
* cameraControl.navMode = "orbit";
* cameraControl.followPointer = true;
*
* const xktLoader = new XKTLoaderPlugin(viewer);
*
* var t0 = performance.now();
*
* document.getElementById("time").innerHTML = "Loading model...";
*
* const sceneModel = xktLoader.load({
* id: "myModel",
* src: "../../assets/models/xkt/v10/glTF-Embedded/Duplex_A_20110505.glTFEmbedded.xkt",
* edges: true
* });
*
* sceneModel.on("loaded", () => {
*
* var t1 = performance.now();
* document.getElementById("time").innerHTML = "Model loaded in " + Math.floor(t1 - t0) / 1000.0 + " seconds<br>Objects: " + sceneModel.numEntities;
*
* //------------------------------------------------------------------------------------------------------------------
* // Add caps materials to all objects inside the loaded model that have an opacity equal to or above 0.7
* //------------------------------------------------------------------------------------------------------------------
* const opacityThreshold = 0.7;
* const material = new PhongMaterial(viewer.scene,{
* diffuse: [1.0, 0.0, 0.0],
* backfaces: true
* });
* addCapsMaterialsToAllObjects(sceneModel, opacityThreshold, material);
*
* //------------------------------------------------------------------------------------------------------------------
* // Create a moving SectionPlane, that moves through the table models
* //------------------------------------------------------------------------------------------------------------------
*
* const sectionPlanes = new SectionPlanesPlugin(viewer, {
* overviewCanvasId: "mySectionPlanesOverviewCanvas",
* overviewVisible: true,
* });
*
* const sectionPlane = sectionPlanes.createSectionPlane({
* id: "mySectionPlane",
* pos: [0.5, 2.5, 5.0],
* dir: math.normalizeVec3([1.0, 0.01, 1])
* });
*
* sectionPlanes.showControl(sectionPlane.id);
*
* window.viewer = viewer;
*
* });
*
* function addCapsMaterialsToAllObjects(sceneModel, opacityThreshold, material) {
* const allObjects = sceneModel.objects;
* for(const key in allObjects){
* const object = allObjects[key];
* if(object.opacity >= opacityThreshold)
* object.capMaterial = material;
* }
* }
* ````
*/
class SectionCaps {
/**
* @constructor
*/
constructor(scene) {
let destroy = null;
const modelCaches = { };
const sectionPlanes = [ ];
this.destroy = () => destroy && destroy();
let updateTimeout = null;
const update = () => {
clearTimeout(updateTimeout);
updateTimeout = setTimeout(() => {
const visibleSceneModels = Object.values(scene.models).filter(sceneModel => (sceneModel.id in modelCaches) && sceneModel.visible);
sectionPlanes.forEach((plane) => {
if (plane.active) {
const sliceMesh = math.makeSectionPlaneSlicer(plane);
visibleSceneModels.forEach(sceneModel => {
const modelAABB = sceneModel.aabb;
if (math.planeIntersectsAABB3(plane, modelAABB)) {
// modelCenter is critical to use when handling models with large coordinates.
// See XCD-306 and examples/slicing/SectionCaps_at_distance.html for more details.
const modelCenter = math.getAABB3Center(modelAABB, math.vec3());
modelCaches[sceneModel.id].entityCaches.forEach((entityCache, entityId) => {
const entity = sceneModel.objects[entityId];
if (entityCache.generateCaps && entity.capMaterial && math.planeIntersectsAABB3(plane, entity.aabb)) {
entityCache.meshCaches ||= entity.meshes.filter(mesh => mesh.isSolid()).map(mesh => {
const meshIndices = [ ];
const meshVertices = [ ];
mesh.getEachIndex(i => meshIndices.push(i));
mesh.getEachVertex(v => meshVertices.push(...math.subVec3(v, modelCenter, tempVec3a)));
return { mesh: mesh, meshIndices: meshIndices, meshVertices: meshVertices };
});
entityCache.meshCaches.filter(meshCache => math.planeIntersectsAABB3(plane, meshCache.mesh.aabb)).forEach((meshCache, meshIdx) => {
const geo = sliceMesh({ origin: modelCenter, indices: meshCache.meshIndices, positions: meshCache.meshVertices }, { onlyPosSliceWithUV: true }).pos;
if (geo) {
entityCache.capMeshes.push(new Mesh(scene, {
isObject: true,
id: `${plane.id}-${entityId}-${meshIdx}`,
material: entity.capMaterial,
origin: math.addVec3(modelCenter, math.mulVec3Scalar(plane.dir, 0.001, tempVec3a), tempVec3a),
geometry: new ReadableGeometry(scene, {
primitive: "triangles",
indices: geo.indices,
positions: geo.positions,
normals: geo.normals,
uv: geo.uv
})
}));
}
});
}
});
}
});
}
});
visibleSceneModels.forEach(sceneModel => modelCaches[sceneModel.id].entityCaches.forEach(entityCache => entityCache.generateCaps = false));
}, 100);
};
this._onCapMaterialUpdated = (entity) => {
if (! destroy) {
updateTimeout = null;
const handleSectionPlane = (sectionPlane) => {
const onSectionPlaneUpdated = () => {
Object.values(modelCaches).forEach(modelCache => modelCache.entityCaches.forEach(entityCache => {
entityCache.destroyCaps();
entityCache.generateCaps = true;
}));
update();
};
sectionPlanes.push(sectionPlane);
sectionPlane.on('pos', onSectionPlaneUpdated);
sectionPlane.on('dir', onSectionPlaneUpdated);
sectionPlane.on('active', onSectionPlaneUpdated);
sectionPlane.once('destroyed', () => {
const idx = sectionPlanes.indexOf(sectionPlane);
if (idx >= 0) {
sectionPlanes.splice(idx, 1);
onSectionPlaneUpdated();
}
});
};
for (const key in scene.sectionPlanes){
handleSectionPlane(scene.sectionPlanes[key]);
}
const onSectionPlaneCreated = scene.on('sectionPlaneCreated', handleSectionPlane);
const onTick = scene.on("tick", () => {
//on ticks we only check if there is a model that we have saved vertices for,
//but it's no more available on the scene, or if its visibility changed
let doUpdate = false;
for (const sceneModelId in modelCaches) {
const sceneModel = scene.models[sceneModelId];
if (! sceneModel) {
modelCaches[sceneModelId].entityCaches.forEach(entityCache => entityCache.destroyCaps());
delete modelCaches[sceneModelId];
doUpdate = true;
} else if (modelCaches[sceneModelId].modelVisible != sceneModel.visible) {
const modelCache = modelCaches[sceneModelId];
modelCache.modelVisible = !!sceneModel.visible;
modelCache.entityCaches.forEach(entityCache => {
entityCache.destroyCaps();
entityCache.generateCaps = true;
});
doUpdate = true;
}
}
if (doUpdate) {
update();
}
});
destroy = () => {
for (const sceneModelId in modelCaches) {
modelCaches[sceneModelId].entityCaches.forEach(entityCache => entityCache.destroyCaps());
}
scene.off(onSectionPlaneCreated);
scene.off(onTick);
};
}
const model = entity.model;
const modelId = model.id;
modelCaches[modelId] ||= { modelVisible: model.visible, entityCaches: new Map() };
const entityCaches = modelCaches[modelId].entityCaches;
const entityId = entity.id;
if (! entityCaches.has(entityId)) {
const entityCache = {
capMeshes: [ ],
meshCaches: null,
generateCaps: false,
destroyCaps: () => {
entityCache.capMeshes.forEach(capMesh => capMesh.destroy());
entityCache.capMeshes.length = 0;
}
};
entityCaches.set(entityId, entityCache);
}
const entityCache = entityCaches.get(entityId);
entityCache.destroyCaps();
entityCache.generateCaps = true;
update();
};
}
}
export { SectionCaps };