From 974e2546052a7f7b43568253c053bdf72bb87675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Gonz=C3=A1lez=20Viegas?= Date: Thu, 1 Aug 2024 18:20:24 +0200 Subject: [PATCH] feat(front): allow static elements when streaming --- packages/front/package.json | 2 +- .../front/src/fragments/Highlighter/index.ts | 31 ++++- .../front/src/fragments/IfcStreamer/index.ts | 79 ++++++++++- .../src/geometry-culler-renderer.ts | 128 ++++++++++++++++-- 4 files changed, 221 insertions(+), 19 deletions(-) diff --git a/packages/front/package.json b/packages/front/package.json index bdf8333de..41cbc8863 100644 --- a/packages/front/package.json +++ b/packages/front/package.json @@ -1,7 +1,7 @@ { "name": "@thatopen/components-front", "description": "Collection of frontend tools to author BIM apps.", - "version": "2.1.13", + "version": "2.1.14", "author": "That Open Company", "contributors": [ "Antonio Gonzalez Viegas (https://github.com/agviegas)", diff --git a/packages/front/src/fragments/Highlighter/index.ts b/packages/front/src/fragments/Highlighter/index.ts index 8d3413d1f..65b1c7a59 100644 --- a/packages/front/src/fragments/Highlighter/index.ts +++ b/packages/front/src/fragments/Highlighter/index.ts @@ -343,16 +343,21 @@ export class Highlighter } for (const fragID in filtered) { - const fragment = fragments.list.get(fragID); - if (!fragment) { - continue; - } if (!this.selection[name][fragID]) { this.selection[name][fragID] = new Set(); } const itemIDs = fragmentIdMap[fragID]; + for (const itemID of itemIDs) { this.selection[name][fragID].add(itemID); + } + + const fragment = fragments.list.get(fragID); + if (!fragment) { + continue; + } + + for (const itemID of itemIDs) { fragment.setColor(color, [itemID]); } @@ -436,6 +441,24 @@ export class Highlighter this.onSetup.trigger(this); } + /** + * Applies all the existing styles to the given fragments. Useful when combining the highlighter with streaming. + * + * @param fragments - The list of fragment to update. + */ + updateFragments(fragments: Iterable) { + for (const frag of fragments) { + for (const name in this.selection) { + const map = this.selection[name]; + const ids = map[frag.id]; + const color = this.colors.get(name); + if (ids && color) { + frag.setColor(color, ids); + } + } + } + } + private async zoomSelection(name: string) { if (!this.config.world) { throw new Error("No world found in config!"); diff --git a/packages/front/src/fragments/IfcStreamer/index.ts b/packages/front/src/fragments/IfcStreamer/index.ts index 2ce116585..bf3528aef 100644 --- a/packages/front/src/fragments/IfcStreamer/index.ts +++ b/packages/front/src/fragments/IfcStreamer/index.ts @@ -396,9 +396,54 @@ export class IfcStreamer extends OBC.Component implements OBC.Disposable { await this._fileCache.delete(); } - private async loadFoundGeometries(seen: { - [modelID: string]: Map>; - }) { + /** + * Sets or unsets the specified fragments as static. Static fragments are streamed once and then kept in memory. + * + * @param ids - The list of fragment IDs to make static. + * @param active - Whether these items should be static or not. + * @param culled - Whether these items should be culled or not. If undefined: active=true will set items as culled, while active=false will remove items from both the culled and unculled list. + */ + async setStatic(ids: Iterable, active: boolean, culled?: boolean) { + const staticGeometries: { [modelID: string]: Set } = {}; + + for (const id of ids) { + const found = this.fragIDData.get(id); + if (!found) { + console.log(`Item not found: ${id}.`); + continue; + } + + const [group, geometryID] = found; + const modelID = group.uuid; + + if (!staticGeometries[modelID]) { + staticGeometries[modelID] = new Set(); + } + + staticGeometries[modelID].add(geometryID); + } + + if (active) { + const seen: { [modelID: string]: Map> } = {}; + for (const modelID in staticGeometries) { + const map = new Map>(); + map.set(1, staticGeometries[modelID]); + seen[modelID] = map; + } + // Load the static geometries, but don't show them + await this.loadFoundGeometries(seen, false); + await this.culler.addStaticGeometries(staticGeometries, culled); + } else { + this.culler.removeStaticGeometries(staticGeometries, culled); + } + } + + private async loadFoundGeometries( + seen: { + [modelID: string]: Map>; + }, + visible = true, + ) { for (const modelID in seen) { if (this._isDisposing) return; @@ -518,8 +563,25 @@ export class IfcStreamer extends OBC.Component implements OBC.Disposable { } } - this.newFragment(group, geometryID, geom, transp, true, loaded); - this.newFragment(group, geometryID, geom, opaque, false, loaded); + this.newFragment( + group, + geometryID, + geom, + transp, + true, + loaded, + visible, + ); + + this.newFragment( + group, + geometryID, + geom, + opaque, + false, + loaded, + visible, + ); } } @@ -555,7 +617,10 @@ export class IfcStreamer extends OBC.Component implements OBC.Disposable { throw new Error("Fragment group not found!"); } - if (!this._loadedFragments[modelID]) continue; + if (!this._loadedFragments[modelID]) { + continue; + } + const loadedFrags = this._loadedFragments[modelID]; const geometries = unseen[modelID]; @@ -608,6 +673,7 @@ export class IfcStreamer extends OBC.Component implements OBC.Disposable { instances: StreamedInstance[], transparent: boolean, result: FRAG.Fragment[], + visible: boolean, ) { if (instances.length === 0) return; if (this._isDisposing) return; @@ -635,6 +701,7 @@ export class IfcStreamer extends OBC.Component implements OBC.Disposable { const material = transparent ? this._baseMaterialT : this._baseMaterial; const fragment = new FRAG.Fragment(geometry, material, instances.length); + fragment.mesh.visible = visible; fragment.id = fragID; fragment.mesh.uuid = fragID; diff --git a/packages/front/src/fragments/IfcStreamer/src/geometry-culler-renderer.ts b/packages/front/src/fragments/IfcStreamer/src/geometry-culler-renderer.ts index 4fd6c55d0..0a1d53a3d 100644 --- a/packages/front/src/fragments/IfcStreamer/src/geometry-culler-renderer.ts +++ b/packages/front/src/fragments/IfcStreamer/src/geometry-culler-renderer.ts @@ -26,6 +26,11 @@ export class GeometryCullerRenderer extends OBC.CullerRenderer { boxes = new Map(); + private _staticGeometries: { + culled: { [modelID: string]: Set }; + unculled: { [modelID: string]: Set }; + } = { culled: {}, unculled: {} }; + private readonly _geometry: THREE.BufferGeometry; private _material = new THREE.MeshBasicMaterial({ @@ -47,7 +52,7 @@ export class GeometryCullerRenderer extends OBC.CullerRenderer { private _geometries = new Map(); private _geometriesGroups = new Map(); - private _foundGeometries = new Set(); + private _geometriesInMemory = new Set(); private _intervalID: number | null = null; private codes = new Map>(); @@ -110,6 +115,8 @@ export class GeometryCullerRenderer extends OBC.CullerRenderer { } this._geometries.clear(); + this._staticGeometries = { culled: {}, unculled: {} }; + this._geometry.dispose(); this._material.dispose(); this._modelIDIndex.clear(); @@ -274,7 +281,7 @@ export class GeometryCullerRenderer extends OBC.CullerRenderer { this._modelIDIndex.delete(modelID); this._indexModelID.delete(index); - this._foundGeometries.clear(); + this._geometriesInMemory.clear(); } addFragment(modelID: string, geometryID: number, frag: FRAGS.Fragment) { @@ -421,6 +428,94 @@ export class GeometryCullerRenderer extends OBC.CullerRenderer { } } + async addStaticGeometries( + geometries: { [modelID: string]: Set }, + culled = true, + ) { + const event = { + data: { + colors: new Map(), + }, + }; + const dummyPixelValue = this.threshold + 1000; + + for (const modelID in geometries) { + const modelKey = this._modelIDIndex.get(modelID); + if (modelKey === undefined) { + continue; + } + const map = this.codes.get(modelKey); + if (!map) { + continue; + } + + const geometryIDs = geometries[modelID]; + + for (const geometryID of geometryIDs) { + const colorCode = map.get(geometryID); + if (!colorCode) { + continue; + } + + const geometry = this._geometries.get(colorCode); + if (!geometry) { + continue; + } + + geometry.exists = true; + if (!culled) { + // Static unculled geometries are always visible + geometry.hidden = false; + geometry.time = performance.now(); + event.data.colors.set(colorCode, dummyPixelValue); + } + + this._geometriesInMemory.add(colorCode); + + const statics = culled + ? this._staticGeometries.culled + : this._staticGeometries.unculled; + + if (!statics[modelID]) { + statics[modelID] = new Set(); + } + + statics[modelID].add(geometryID); + } + } + + if (!culled) { + // If unculled, we'll make these geometries visible by forcing its discovery + await this.handleWorkerMessage(event as any); + } + } + + removeStaticGeometries( + geometries: { [modelID: string]: Set }, + culled?: boolean, + ) { + const options: ("culled" | "unculled")[] = []; + if (culled === undefined) { + options.push("culled", "unculled"); + } else if (culled === true) { + options.push("culled"); + } else { + options.push("unculled"); + } + + for (const modelID in geometries) { + const geometryIDs = geometries[modelID]; + for (const option of options) { + const set = this._staticGeometries[option][modelID]; + if (set) { + for (const geometryID of geometryIDs) { + set.delete(geometryID); + } + } + } + } + } + private setGeometryVisibility( geometry: CullerBoundingBox, visible: boolean, @@ -459,7 +554,7 @@ export class GeometryCullerRenderer extends OBC.CullerRenderer { let viewWasUpdated = false; // We can only lose geometries that were previously found - const lostGeometries = new Set(this._foundGeometries); + const lostGeometries = new Set(this._geometriesInMemory); for (const [color, number] of colors) { const geometry = this._geometries.get(color); @@ -480,16 +575,16 @@ export class GeometryCullerRenderer extends OBC.CullerRenderer { const modelID = this._indexModelID.get(geometry.modelIndex) as string; if (exists) { - // Geometry was visible, and still is + // Geometry was present in memory, and still is, so show it geometry.time = now; if (!toShow[modelID]) { toShow[modelID] = new Set(); } toShow[modelID].add(geometry.geometryID); - this._foundGeometries.add(color); + this._geometriesInMemory.add(color); viewWasUpdated = true; } else { - // New geometry found + // New geometry found that is not in memory if (!toLoad[modelID]) { toLoad[modelID] = new Map(); } @@ -501,7 +596,7 @@ export class GeometryCullerRenderer extends OBC.CullerRenderer { } const set = toLoad[modelID].get(number) as Set; set.add(geometry.geometryID); - this._foundGeometries.add(color); + this._geometriesInMemory.add(color); viewWasUpdated = true; } } @@ -533,15 +628,32 @@ export class GeometryCullerRenderer extends OBC.CullerRenderer { ) { const modelID = this._indexModelID.get(geometry.modelIndex) as string; const lostTime = now - geometry.time; + + const { culled, unculled } = this._staticGeometries; + if (lostTime > this.maxLostTime) { // This geometry was lost too long - delete it + + // If it's any kind of static geometry, skip it + if ( + culled[modelID]?.has(geometry.geometryID) || + unculled[modelID]?.has(geometry.geometryID) + ) { + return; + } + if (!toRemove[modelID]) { toRemove[modelID] = new Set(); } geometry.exists = false; toRemove[modelID].add(geometry.geometryID); - this._foundGeometries.delete(color); + this._geometriesInMemory.delete(color); } else if (lostTime > this.maxHiddenTime) { + // If it's an unculled static geometry, skip it + if (unculled[modelID]?.has(geometry.geometryID)) { + return; + } + // This geometry was lost for a while - hide it if (!toHide[modelID]) { toHide[modelID] = new Set();