UNPKG

@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
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 };