diff --git a/core/common/src/MapLayerSettings.ts b/core/common/src/MapLayerSettings.ts index 19c6123a552c..dfbb47f2b5c0 100644 --- a/core/common/src/MapLayerSettings.ts +++ b/core/common/src/MapLayerSettings.ts @@ -141,6 +141,10 @@ export interface CommonMapLayerProps { * Default: true. */ transparentBackground?: boolean; + /** True to drape this layer onto all attached reality data, not the background map. Otherwise, the layer will be draped onto the background map. + * Default: false. + */ + toRealityData?: boolean; } /** JSON representation of an [[ImageMapLayerSettings]]. @@ -216,16 +220,18 @@ export abstract class MapLayerSettings { public readonly name: string; public readonly transparency: number; public readonly transparentBackground: boolean; + public readonly toRealityData: boolean; public abstract get allSubLayersInvisible(): boolean; public abstract clone(changedProps: Partial): MapLayerSettings; public abstract toJSON(): MapLayerProps; /** @internal */ - protected constructor(name: string, visible = true, transparency: number = 0, transparentBackground = true) { + protected constructor(name: string, visible = true, transparency: number = 0, transparentBackground = true, toRealityData = false) { this.name = name; this.visible = visible; this.transparentBackground = transparentBackground; this.transparency = transparency; + this.toRealityData = toRealityData; } /** Create a map layer settings from its JSON representation. */ @@ -246,6 +252,9 @@ export abstract class MapLayerSettings { if (this.transparentBackground === false) props.transparentBackground = this.transparentBackground; + if (this.toRealityData === true) + props.toRealityData = this.toRealityData; + return props; } @@ -255,6 +264,7 @@ export abstract class MapLayerSettings { name: undefined !== changedProps.name ? changedProps.name : this.name, visible: undefined !== changedProps.visible ? changedProps.visible : this.visible, transparency: undefined !== changedProps.transparency ? changedProps.transparency : this.transparency, + toRealityData: undefined !== changedProps.toRealityData ? changedProps.toRealityData : this.toRealityData, transparentBackground: undefined !== changedProps.transparentBackground ? changedProps.transparentBackground : this.transparentBackground, }; } @@ -303,7 +313,7 @@ export class ImageMapLayerSettings extends MapLayerSettings { /** @internal */ protected constructor(props: ImageMapLayerProps) { const transparentBackground = props.transparentBackground ?? true; - super(props.name, props.visible, props.transparency, transparentBackground); + super(props.name, props.visible, props.transparency, transparentBackground, props.toRealityData); this.formatId = props.formatId; this.url = props.url; @@ -488,15 +498,15 @@ export class ModelMapLayerSettings extends MapLayerSettings { /** @internal */ protected constructor(modelId: Id64String, name: string, visible = true, - transparency: number = 0, transparentBackground = true) { - super(name, visible, transparency, transparentBackground); + transparency: number = 0, transparentBackground = true, toRealityData = false) { + super(name, visible, transparency, transparentBackground, toRealityData); this.modelId = modelId; } /** Construct from JSON, performing validation and applying default values for undefined fields. */ public static override fromJSON(json: ModelMapLayerProps): ModelMapLayerSettings { const transparentBackground = (json.transparentBackground === undefined) ? true : json.transparentBackground; - return new this(json.modelId, json.name, json.visible, json.transparency, transparentBackground); + return new this(json.modelId, json.name, json.visible, json.transparency, transparentBackground, json.toRealityData); } /** return JSON representation of this MapLayerSettings object */ diff --git a/core/frontend-devtools/README.md b/core/frontend-devtools/README.md index 5ba5514bb981..aa5aa19d12ac 100644 --- a/core/frontend-devtools/README.md +++ b/core/frontend-devtools/README.md @@ -130,7 +130,7 @@ This package provides several keyins to control the display of background maps, * `fdt attach maplayer ` - Attach a background map layer from name within the map layer source list. Partial names may be used. * `fdt attach mapoverlay ` - Attach an overlay map layer from name within the map layer source list. Partial names may be used. -* `fdt attach model maplayer ` - Attach a model map layer for each unique model of the currently selected elements. +* `fdt attach model maplayer [name]` - Attach a model map layer for each unique model of the currently selected elements to either the background map or attached reality models. If name is specified, use that. * `fdt set map base ` - Set the background base map from name within the map layer source list. Partial names may be used. * `fdt set map base color ` - Set map base color by red, green and blue values [0..255]. diff --git a/core/frontend-devtools/src/tools/MapLayerTool.ts b/core/frontend-devtools/src/tools/MapLayerTool.ts index 9117685fb720..818d8c6ccb3c 100644 --- a/core/frontend-devtools/src/tools/MapLayerTool.ts +++ b/core/frontend-devtools/src/tools/MapLayerTool.ts @@ -65,12 +65,12 @@ class AttachMapLayerBaseTool extends Tool { * @beta */ export class AttachModelMapLayerTool extends Tool { - public static override get minArgs() { return 0; } - public static override get maxArgs() { return 1; } + public static override get minArgs() { return 1; } + public static override get maxArgs() { return 2; } public static override toolId = "AttachModelMapLayerTool"; constructor(protected _formatId: string) { super(); } - public override async run(nameIn?: string): Promise { + public override async run(toRealityData: boolean, nameIn?: string): Promise { const vp = IModelApp.viewManager.selectedView; if (!vp) return false; @@ -86,14 +86,14 @@ export class AttachModelMapLayerTool extends Tool { const modelProps = await iModel.models.getProps(modelId); const modelName = modelProps[0].name ? modelProps[0].name : modelId; const name = nameIn ? (modelIds.size > 1 ? `${nameIn}: ${modelName}` : nameIn) : modelName; - const settings = ModelMapLayerSettings.fromJSON({ name, modelId }); + const settings = ModelMapLayerSettings.fromJSON({ name, modelId, toRealityData }); vp.displayStyle.attachMapLayer({ settings, mapLayerIndex: { isOverlay: false, index: -1 } }); } return true; } public override async parseAndRun(...args: string[]): Promise { - return this.run(args[0]); + return this.run("reality" === args[0], args[1]); } public async onRestartTool() { } diff --git a/core/frontend/src/ContextRealityModelState.ts b/core/frontend/src/ContextRealityModelState.ts index 5cc2b43cad1b..eee22fc65377 100644 --- a/core/frontend/src/ContextRealityModelState.ts +++ b/core/frontend/src/ContextRealityModelState.ts @@ -41,6 +41,8 @@ export class ContextRealityModelState extends ContextRealityModel { this.rdSourceKey = props.rdSourceKey ? props.rdSourceKey : RealityDataSource.createKeyFromOrbitGtBlobProps(props.orbitGtBlob); } const useOrbitGtTileTreeReference = this.rdSourceKey.format === RealityDataFormat.OPC; + const mapSettings = displayStyle.backgroundMapSettings; + const mapImagery = displayStyle.settings.mapImagery; this._treeRef = (!useOrbitGtTileTreeReference) ? createRealityTileTreeReference({ iModel, @@ -51,6 +53,9 @@ export class ContextRealityModelState extends ContextRealityModel { classifiers: this.classifiers, planarClipMask: this.planarClipMaskSettings, getDisplaySettings: () => this.displaySettings, + mapSettings, + backgroundBase: mapImagery.backgroundBase, + backgroundLayers: mapImagery.backgroundLayers }) : createOrbitGtTileTreeReference({ iModel, diff --git a/core/frontend/src/ModelState.ts b/core/frontend/src/ModelState.ts index 6ca3cb332078..0d1855f8846d 100644 --- a/core/frontend/src/ModelState.ts +++ b/core/frontend/src/ModelState.ts @@ -17,7 +17,7 @@ import { IModelConnection } from "./IModelConnection"; import { RealityDataSource } from "./RealityDataSource"; import { createOrbitGtTileTreeReference, createPrimaryTileTreeReference, createRealityTileTreeReference, TileTreeReference } from "./tile/internal"; import { ViewState } from "./ViewState"; -import { SpatialClassifiersState } from "./SpatialClassifiersState"; +import { SpatialClassifiersState } from "./SpatialClassifiersState"; /** Represents the front-end state of a [Model]($backend). * @public @@ -120,8 +120,12 @@ export abstract class GeometricModelState extends ModelState implements Geometri const rdSourceKey = this.jsonProperties.rdSourceKey; const getDisplaySettings = () => view.displayStyle.settings.getRealityModelDisplaySettings(this.id) ?? RealityModelDisplaySettings.defaults; + const mapSettings = view.displayStyle.backgroundMapSettings; + const mapImagery = view.displayStyle.settings.mapImagery; + if (rdSourceKey) { const useOrbitGtTileTreeReference = rdSourceKey.format === RealityDataFormat.OPC; + const treeRef = (!useOrbitGtTileTreeReference) ? createRealityTileTreeReference({ rdSourceKey, @@ -131,6 +135,9 @@ export abstract class GeometricModelState extends ModelState implements Geometri // url: tilesetUrl, // If rdSourceKey is defined, url is not used classifiers: undefined !== spatialModel ? spatialModel.classifiers : undefined, getDisplaySettings, + mapSettings, + backgroundBase: mapImagery.backgroundBase, + backgroundLayers: mapImagery.backgroundLayers }) : createOrbitGtTileTreeReference({ rdSourceKey, @@ -184,7 +191,10 @@ export abstract class GeometricModelState extends ModelState implements Geometri tilesetToDbTransform: this.jsonProperties.tilesetToDbTransform, classifiers: undefined !== spatialModel ? spatialModel.classifiers : undefined, getDisplaySettings, - }); + mapSettings, + backgroundBase: mapImagery.backgroundBase, + backgroundLayers: mapImagery.backgroundLayers + }); } return createPrimaryTileTreeReference(view, this); diff --git a/core/frontend/src/Viewport.ts b/core/frontend/src/Viewport.ts index 11a981b53df8..83457bc85dbc 100644 --- a/core/frontend/src/Viewport.ts +++ b/core/frontend/src/Viewport.ts @@ -49,7 +49,7 @@ import { StandardView, StandardViewId } from "./StandardView"; import { SubCategoriesCache } from "./SubCategoriesCache"; import { DisclosedTileTreeSet, MapCartoRectangle, MapFeatureInfo, MapFeatureInfoOptions, MapLayerFeatureInfo, MapLayerImageryProvider, MapLayerIndex, MapLayerInfoFromTileTree, MapTiledGraphicsProvider, - MapTileTreeReference, MapTileTreeScaleRangeVisibility, TileBoundingBoxes, TiledGraphicsProvider, TileTreeLoadStatus, TileTreeReference, TileUser, + MapTileTreeReference, MapTileTreeScaleRangeVisibility, RealityModelTileTree, RealityTileTree, TileBoundingBoxes, TiledGraphicsProvider, TileTreeLoadStatus, TileTreeReference, TileUser, } from "./tile/internal"; import { EventController } from "./tools/EventController"; import { ToolSettings } from "./tools/ToolSettings"; @@ -425,6 +425,7 @@ export abstract class Viewport implements Disposable, TileUser { /** Mark the viewport's [[ViewState]] as having changed, so that the next call to [[renderFrame]] will invoke [[setupFromView]] to synchronize with the view. * This method is not typically invoked directly - the controller is automatically invalidated in response to events such as a call to [[changeView]]. + * Additionally, refresh the Reality Tile Tree to reflect changes in the map layer. */ public invalidateController(): void { this._controllerValid = this._analysisFractionValid = false; @@ -917,6 +918,61 @@ export abstract class Viewport implements Disposable, TileUser { return true; } + /** Refresh the Reality Tile Tree to reflect changes in the map layer. */ + private refreshRealityTile(): void { + for (const { supplier, id, owner } of this.iModel.tiles) { + if (owner.tileTree instanceof RealityTileTree) { + this.iModel.tiles.resetTileTreeOwner(id, supplier); + } + } + } + + /** + * Compares the map layers of two view states, ensuring both the number of layers + * and their order remain unchanged. + * Returns true if the map layers differ in count, order, or model IDs; otherwise, returns false. + * + * @param prevView The previous view state. + * @param newView The new view state. + * @returns {boolean} True if there is any difference in the model layer configuration; false otherwise. + * @internal + */ + private compareMapLayer(prevView: ViewState, newView: ViewState): boolean { + + const prevLayers = prevView.displayStyle.getMapLayers(false); + const newLayers = newView.displayStyle.getMapLayers(false); + + const prevModelIds: string[] = []; + const newModelIds: string[] = []; + + // Extract model IDs from the previous layers using a for loop + for (const layer of prevLayers) { + if (layer instanceof ModelMapLayerSettings) { + prevModelIds.push(layer.modelId); + } + } + + // Extract model IDs from the new layers using a for loop + for (const layer of newLayers) { + if (layer instanceof ModelMapLayerSettings) { + newModelIds.push(layer.modelId); + } + } + + if (prevModelIds.length !== newModelIds.length) { + return true; + } + + // Check if all model IDs in newModelIds exist in prevModelIds + for (let i = 0; i < prevModelIds.length; i++) { + if (prevModelIds[i] !== newModelIds[i]) { + return true; + } + } + + return false; + } + /** Fully reset a map-layer tile tree; by calling this, the map-layer will to go through initialize process again, and all previously fetched tile will be lost. * @beta */ @@ -1303,6 +1359,7 @@ export abstract class Viewport implements Disposable, TileUser { const mapChanged = () => { this.invalidateController(); this._changeFlags.setDisplayStyle(); + this.refreshRealityTile(); }; removals.push(settings.onBackgroundMapChanged.addListener(mapChanged)); @@ -1783,6 +1840,10 @@ export abstract class Viewport implements Disposable, TileUser { if (undefined !== prevView && prevView !== view) { this.onChangeView.raiseEvent(this, prevView); this._changeFlags.setViewState(); + + if (this.compareMapLayer(prevView, view)) { + this.refreshRealityTile(); + } } } @@ -3187,7 +3248,7 @@ export class ScreenViewport extends Viewport { if (undefined !== IModelApp.applicationLogoCard) { logos.appendChild(IModelApp.applicationLogoCard()); } - + logos.appendChild(IModelApp.makeIModelJsLogoCard()); for (const ref of this.getTileTreeRefs()) { ref.addLogoCards(logos, this); diff --git a/core/frontend/src/render/RealityMeshGraphicParams.ts b/core/frontend/src/render/RealityMeshGraphicParams.ts index 7152c60f5e86..8f51c4cb728f 100644 --- a/core/frontend/src/render/RealityMeshGraphicParams.ts +++ b/core/frontend/src/render/RealityMeshGraphicParams.ts @@ -12,10 +12,10 @@ import { MapLayerClassifiers, RenderTerrainGeometry, TerrainTexture } from "./ /** @internal */ export interface RealityMeshGraphicParams { - readonly realityMesh: RenderTerrainGeometry; + readonly realityMesh?: RenderTerrainGeometry; readonly projection: MapTileProjection; readonly tileRectangle: MapCartoRectangle; - readonly featureTable: PackedFeatureTable; + readonly featureTable?: PackedFeatureTable; readonly tileId: string | undefined; readonly baseColor: ColorDef | undefined; readonly baseTransparent: boolean; diff --git a/core/frontend/src/render/RealityMeshParams.ts b/core/frontend/src/render/RealityMeshParams.ts index 7f35f3ffa89e..d5afbfd02046 100644 --- a/core/frontend/src/render/RealityMeshParams.ts +++ b/core/frontend/src/render/RealityMeshParams.ts @@ -13,7 +13,7 @@ import { import { OctEncodedNormal, QPoint2d, QPoint2dBuffer, QPoint2dBufferBuilder, QPoint3d, QPoint3dBuffer, QPoint3dBufferBuilder, RenderTexture, } from "@itwin/core-common"; -import { GltfMeshData } from "../tile/internal"; +import { GltfMeshData, RealityTile } from "../tile/internal"; import { MeshPrimitiveType } from "../common/internal/render/MeshPrimitive"; function precondition(condition: boolean, message: string | (() => string)): asserts condition { @@ -46,6 +46,7 @@ export interface RealityMeshParams { featureID?: number; // default 0 /** @alpha unused by terrain meshes */ texture?: RenderTexture; + tile?: RealityTile; } /** @public */ diff --git a/core/frontend/src/render/webgl/RealityMesh.ts b/core/frontend/src/render/webgl/RealityMesh.ts index 3aaa6ddbbe12..2231b7acb39a 100644 --- a/core/frontend/src/render/webgl/RealityMesh.ts +++ b/core/frontend/src/render/webgl/RealityMesh.ts @@ -8,8 +8,8 @@ */ import { assert, dispose, disposeArray, UintArray } from "@itwin/core-bentley"; -import { ColorDef, Quantization, RenderTexture } from "@itwin/core-common"; -import { Matrix4d, Range2d, Range3d, Transform, Vector2d } from "@itwin/core-geometry"; +import { CartographicRange, ColorDef, Quantization, RenderTexture } from "@itwin/core-common"; +import { Matrix4d, Range2d, Range3d, Transform, Vector2d, Vector3d } from "@itwin/core-geometry"; import { GraphicBranch } from "../GraphicBranch"; import { RealityMeshGraphicParams } from "../RealityMeshGraphicParams"; import { RealityMeshParams } from "../RealityMeshParams"; @@ -29,6 +29,7 @@ import { System } from "./System"; import { Target } from "./Target"; import { TechniqueId } from "./TechniqueId"; import { RenderGeometry } from "../../internal/render/RenderGeometry"; +import { MapCartoRectangle, PlanarProjection, PlanarTilePatch, RealityModelTileTree, RealityTile } from "../../tile/internal"; const scratchOverlapRange = Range2d.createNull(); const scratchBytes = new Uint8Array(4); @@ -286,6 +287,7 @@ export class RealityMeshGeometry extends IndexedGeometry implements RenderGeomet public static createForTerrain(mesh: RealityMeshParams, transform: Transform | undefined, disableTextureDisposal = false) { const params = RealityMeshGeometryParams.fromRealityMesh(mesh); + if (!params) return undefined; @@ -300,11 +302,53 @@ export class RealityMeshGeometry extends IndexedGeometry implements RenderGeomet public static createFromRealityMesh(realityMesh: RealityMeshParams, disableTextureDisposal = false): RealityMeshGeometry | undefined { const params = RealityMeshGeometryParams.fromRealityMesh(realityMesh); - if (!params) - return undefined; - const texture = realityMesh.texture ? new TerrainTexture(realityMesh.texture, realityMesh.featureID ?? 0, Vector2d.create(1.0, -1.0), Vector2d.create(0.0, 1.0), Range2d.createXYXY(0, 0, 1, 1), 0, 0) : undefined; + if (!params) return undefined; + + const { texture: meshTexture, featureID } = realityMesh; + const tile = realityMesh.tile as RealityTile; + const layerClassifiers = (tile?.tree as RealityModelTileTree)?.layerClassifiers; + const texture = meshTexture ? new TerrainTexture(meshTexture, featureID ?? 0, Vector2d.create(1.0, -1.0), Vector2d.create(0.0, 1.0), Range2d.createXYXY(0, 0, 1, 1), 0, 0) : undefined; + + if (!layerClassifiers?.size || !tile) return new RealityMeshGeometry({ realityMeshParams: params, textureParams: texture ? RealityTextureParams.create([texture]) : undefined, baseIsTransparent: false, isTerrain: false, disableTextureDisposal }); + + const transformECEF = tile.tree.iModel.getEcefTransform(); + if (!transformECEF || !tile.range) return new RealityMeshGeometry({ realityMeshParams: params, textureParams: texture ? RealityTextureParams.create([texture]) : undefined, baseIsTransparent: false, isTerrain: false, disableTextureDisposal }); + + const tileEcefRange = transformECEF.multiplyRange(tile.range); + const cartographicRange = new CartographicRange(tileEcefRange, transformECEF); + const boundingBox = cartographicRange.getLongitudeLatitudeBoundingBox(); + const mapCartoRectangle = MapCartoRectangle.fromRadians( + boundingBox.low.x, + boundingBox.low.y, + boundingBox.high.x, + boundingBox.high.y + ); + + const corners = tile.range.corners(); + + const normal = Vector3d.createCrossProductToPoints(corners[0], corners[1], corners[2])?.normalize(); + if (!normal) { + return new RealityMeshGeometry({ realityMeshParams: params, textureParams: texture ? RealityTextureParams.create([texture]) : undefined, baseIsTransparent: false, isTerrain: false, disableTextureDisposal }); + } + const chordHeight = corners[0].distance(corners[3]) / 2; + + const realityPlanarTilePatch = new PlanarTilePatch(corners, normal, chordHeight); + const realityProjection = new PlanarProjection(realityPlanarTilePatch); + + const realityMeshParams: RealityMeshGraphicParams = { + projection: realityProjection, + tileRectangle: mapCartoRectangle, + tileId: undefined, + baseColor: undefined, + baseTransparent: false, + layerClassifiers + }; + + const layerTextures: TerrainOrProjectedTexture[] = texture ? [texture] : []; + + layerClassifiers?.forEach((layerClassifier, layerIndex) => layerTextures[layerIndex] = new ProjectedTexture(layerClassifier, realityMeshParams, realityMeshParams.tileRectangle)); - return new RealityMeshGeometry({ realityMeshParams: params, textureParams: texture ? RealityTextureParams.create([texture]) : undefined, baseIsTransparent: false, isTerrain: false, disableTextureDisposal }); + return new RealityMeshGeometry({ realityMeshParams: params, textureParams: layerTextures.length > 0 ? RealityTextureParams.create(layerTextures) : undefined, baseIsTransparent: false, isTerrain: false, disableTextureDisposal }); } public getRange(): Range3d { @@ -381,7 +425,7 @@ export class RealityMeshGeometry extends IndexedGeometry implements RenderGeomet const branch = new GraphicBranch(true); for (const mesh of meshes) { const primitive = Primitive.create(mesh); - branch.add(system.createBatch(primitive!, featureTable, mesh.getRange(), { tileId })); + branch.add(system.createBatch(primitive!, featureTable!, mesh.getRange(), { tileId })); } return system.createBranch(branch, realityMesh._transform ? realityMesh._transform : Transform.createIdentity(), { disableClipStyle: params.disableClipStyle }); diff --git a/core/frontend/src/tile/B3dmReader.ts b/core/frontend/src/tile/B3dmReader.ts index 00c2ade33eb7..d77ad83065f0 100644 --- a/core/frontend/src/tile/B3dmReader.ts +++ b/core/frontend/src/tile/B3dmReader.ts @@ -14,9 +14,10 @@ import { Mesh } from "../common/internal/render/MeshPrimitives"; import { RenderSystem } from "../render/RenderSystem"; import { GltfDataType, GltfMeshPrimitive } from "../common/gltf/GltfSchema"; import { - BatchedTileIdMap, GltfBufferData, GltfReader, GltfReaderProps, GltfReaderResult, ShouldAbortReadGltf, + BatchedTileIdMap, GltfBufferData, GltfReader, GltfReaderProps, GltfReaderResult, RealityTile,ShouldAbortReadGltf } from "./internal"; + /** * Deserializes a tile in [b3dm](https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification/TileFormats/Batched3DModel) format. * @internal @@ -28,7 +29,7 @@ export class B3dmReader extends GltfReader { public static create(stream: ByteStream, iModel: IModelConnection, modelId: Id64String, is3d: boolean, range: ElementAlignedBox3d, system: RenderSystem, yAxisUp: boolean, isLeaf: boolean, tileCenter: Point3d, transformToRoot?: Transform, - isCanceled?: ShouldAbortReadGltf, idMap?: BatchedTileIdMap, deduplicateVertices=false): B3dmReader | undefined { + isCanceled?: ShouldAbortReadGltf, idMap?: BatchedTileIdMap, deduplicateVertices=false, tile?: RealityTile): B3dmReader | undefined { const header = new B3dmHeader(stream); if (!header.isValid) return undefined; @@ -55,15 +56,15 @@ export class B3dmReader extends GltfReader { const batchTableLength = header.featureTableJson ? JsonUtils.asInt(header.featureTableJson.BATCH_LENGTH, 0) : 0; return undefined !== props ? new B3dmReader(props, iModel, modelId, is3d, system, range, isLeaf, batchTableLength, - transformToRoot, header.batchTableJson, isCanceled, idMap, pseudoRtcBias, deduplicateVertices) : undefined; + transformToRoot, header.batchTableJson, isCanceled, idMap, pseudoRtcBias, deduplicateVertices, tile) : undefined; } private constructor(props: GltfReaderProps, iModel: IModelConnection, modelId: Id64String, is3d: boolean, system: RenderSystem, private _range: ElementAlignedBox3d, private _isLeaf: boolean, private _batchTableLength: number, private _transformToRoot?: Transform, private _batchTableJson?: any - , shouldAbort?: ShouldAbortReadGltf, _idMap?: BatchedTileIdMap, private _pseudoRtcBias?: Vector3d, deduplicateVertices=false) { + , shouldAbort?: ShouldAbortReadGltf, _idMap?: BatchedTileIdMap, private _pseudoRtcBias?: Vector3d, deduplicateVertices=false, tile?: RealityTile) { super({ props, iModel, system, shouldAbort, deduplicateVertices, - is2d: !is3d, idMap: _idMap, + is2d: !is3d, idMap: _idMap, tile }); this._modelId = modelId; } @@ -146,7 +147,7 @@ export class B3dmReader extends GltfReader { if (this._isCanceled) return { readStatus: TileReadStatus.Canceled, isLeaf: this._isLeaf }; - return this.readGltfAndCreateGraphics(this._isLeaf, featureTable, this._range, this._transformToRoot, this._pseudoRtcBias); + return this.readGltfAndCreateGraphics(this._isLeaf, featureTable, this._range, this._transformToRoot, this._pseudoRtcBias, undefined); } protected override readBatchTable(mesh: Mesh, json: GltfMeshPrimitive) { diff --git a/core/frontend/src/tile/GltfReader.ts b/core/frontend/src/tile/GltfReader.ts index 946c65491994..0c29cd20640f 100644 --- a/core/frontend/src/tile/GltfReader.ts +++ b/core/frontend/src/tile/GltfReader.ts @@ -25,7 +25,7 @@ import { Mesh } from "../common/internal/render/MeshPrimitives"; import { Triangle } from "../common/internal/render/Primitives"; import { RenderGraphic } from "../render/RenderGraphic"; import { RenderSystem } from "../render/RenderSystem"; -import { BatchedTileIdMap, decodeMeshoptBuffer, RealityTileGeometry, TileContent } from "./internal"; +import { BatchedTileIdMap, decodeMeshoptBuffer, RealityTile, RealityTileGeometry,TileContent } from "./internal"; import type { DracoLoader, DracoMesh } from "@loaders.gl/draco"; import { CreateRenderMaterialArgs } from "../render/CreateRenderMaterialArgs"; import { DisplayParams } from "../common/internal/render/DisplayParams"; @@ -454,6 +454,7 @@ export abstract class GltfReader { protected _meshElementIdToFeatureIndex: Map = new Map(); protected _structuralMetadata?: StructuralMetadata; protected readonly _idMap?: BatchedTileIdMap; + private _tile: RealityTile | undefined; protected get _nodes(): GltfDictionary { return this._glTF.nodes ?? emptyDict; } protected get _meshes(): GltfDictionary { return this._glTF.meshes ?? emptyDict; } @@ -619,7 +620,13 @@ export abstract class GltfReader { if (!gltfMesh.points || !gltfMesh.pointRange) return this._system.createGeometryFromMesh(gltfMesh.primitive, undefined); - const realityMeshPrimitive = (this._vertexTableRequired || isInstanced) ? undefined : RealityMeshParams.fromGltfMesh(gltfMesh); + let realityMeshPrimitive = (this._vertexTableRequired || isInstanced) ? undefined : RealityMeshParams.fromGltfMesh(gltfMesh); + if (realityMeshPrimitive) { + realityMeshPrimitive = { + ...realityMeshPrimitive, + tile: this._tile as RealityTile, + }; + } if (realityMeshPrimitive) { const realityMesh = this._system.createRealityMeshGeometry(realityMeshPrimitive); if (realityMesh) @@ -793,7 +800,7 @@ export abstract class GltfReader { featureTable: FeatureTable | undefined, transformStack: TransformStack, batchInstances?: InstancedGraphicParams, - pseudoRtcBias?: Vector3d, + pseudoRtcBias?: Vector3d ): TileReadStatus { if (undefined === node) return TileReadStatus.InvalidTileData; @@ -988,7 +995,8 @@ export abstract class GltfReader { public readBufferData8(json: { [k: string]: any }, accessorName: string): GltfBufferData | undefined { return this.readBufferData(json, accessorName, GltfDataType.UnsignedByte); } public readBufferDataFloat(json: { [k: string]: any }, accessorName: string): GltfBufferData | undefined { return this.readBufferData(json, accessorName, GltfDataType.Float); } - protected constructor(args: GltfReaderArgs) { + protected constructor(args: GltfReaderArgs & { tile?: RealityTile }) { + this._tile = args.tile; this._glTF = args.props.glTF; this._version = args.props.version; this._yAxisUp = args.props.yAxisUp; @@ -2229,12 +2237,13 @@ export class GltfGraphicsReader extends GltfReader { public readonly binaryData?: Uint8Array; // strictly for tests public meshes?: GltfMeshData; // strictly for tests - public constructor(props: GltfReaderProps, args: ReadGltfGraphicsArgs) { + public constructor(props: GltfReaderProps, args: ReadGltfGraphicsArgs & { tile?: RealityTile }) { super({ props, iModel: args.iModel, vertexTableRequired: true, idMap: args.idMap, + tile: args.tile, }); this._contentRange = args.contentRange; diff --git a/core/frontend/src/tile/I3dmReader.ts b/core/frontend/src/tile/I3dmReader.ts index 7a4409ee6ac3..6ed1457164ad 100644 --- a/core/frontend/src/tile/I3dmReader.ts +++ b/core/frontend/src/tile/I3dmReader.ts @@ -12,7 +12,7 @@ import { IModelConnection } from "../IModelConnection"; import { InstancedGraphicParams } from "../common/render/InstancedGraphicParams"; import { Mesh } from "../common/internal/render/MeshPrimitives"; import { RenderSystem } from "../render/RenderSystem"; -import { BatchedTileIdMap, GltfReader, GltfReaderProps, GltfReaderResult, ShouldAbortReadGltf } from "./internal"; +import { BatchedTileIdMap, GltfReader, GltfReaderProps, GltfReaderResult, RealityTile, ShouldAbortReadGltf } from "./internal"; function setTransform(transforms: Float32Array, index: number, rotation: Matrix3d, origin: Point3d): void { const i = index * 12; @@ -52,7 +52,7 @@ export class I3dmReader extends GltfReader { private readonly _modelId: Id64String; public static create(stream: ByteStream, iModel: IModelConnection, modelId: Id64String, is3d: boolean, range: ElementAlignedBox3d, - system: RenderSystem, yAxisUp: boolean, isLeaf: boolean, isCanceled?: ShouldAbortReadGltf, idMap?: BatchedTileIdMap, deduplicateVertices=false): I3dmReader | undefined { + system: RenderSystem, yAxisUp: boolean, isLeaf: boolean, isCanceled?: ShouldAbortReadGltf, idMap?: BatchedTileIdMap, deduplicateVertices=false, tile?: RealityTile): I3dmReader | undefined { const header = new I3dmHeader(stream); if (!header.isValid) return undefined; @@ -68,15 +68,15 @@ export class I3dmReader extends GltfReader { const featureBinary = new Uint8Array(stream.arrayBuffer, header.featureTableJsonPosition + header.featureTableJsonLength, header.featureTableBinaryLength); return new I3dmReader(featureBinary, JSON.parse(featureStr), header.batchTableJson, props, iModel, modelId, is3d, system, - range, isLeaf, isCanceled, idMap, deduplicateVertices); + range, isLeaf, isCanceled, idMap, deduplicateVertices, tile); } private constructor(private _featureBinary: Uint8Array, private _featureJson: any, private _batchTableJson: any, props: GltfReaderProps, iModel: IModelConnection, modelId: Id64String, is3d: boolean, system: RenderSystem, private _range: ElementAlignedBox3d, - private _isLeaf: boolean, shouldAbort?: ShouldAbortReadGltf, _idMap?: BatchedTileIdMap, deduplicateVertices=false) { + private _isLeaf: boolean, shouldAbort?: ShouldAbortReadGltf, _idMap?: BatchedTileIdMap, deduplicateVertices=false, tile?: RealityTile) { super({ props, iModel, system, shouldAbort, deduplicateVertices, - is2d: !is3d, idMap: _idMap, + is2d: !is3d, idMap: _idMap, tile }); this._modelId = modelId; } diff --git a/core/frontend/src/tile/RealityModelTileTree.ts b/core/frontend/src/tile/RealityModelTileTree.ts index a8f148c6cfcf..2c7815f77dd7 100644 --- a/core/frontend/src/tile/RealityModelTileTree.ts +++ b/core/frontend/src/tile/RealityModelTileTree.ts @@ -11,7 +11,9 @@ import { compareStringsOrUndefined, CompressedId64Set, Id64, Id64String, } from "@itwin/core-bentley"; import { - Cartographic, DefaultSupportedTypes, GeoCoordStatus, PlanarClipMaskPriority, PlanarClipMaskSettings, + BackgroundMapSettings, + BaseLayerSettings, + Cartographic, ColorDef, DefaultSupportedTypes, GeoCoordStatus, MapImagerySettings, MapLayerSettings, PlanarClipMaskPriority, PlanarClipMaskSettings, RealityDataProvider, RealityDataSourceKey, RealityModelDisplaySettings, ViewFlagOverrides, } from "@itwin/core-common"; import { Angle, Constant, Ellipsoid, Matrix3d, Point3d, Range3d, Ray3d, Transform, TransformProps, Vector3d, XYZ } from "@itwin/core-geometry"; @@ -26,11 +28,12 @@ import { RenderMemory } from "../render/RenderMemory"; import { SceneContext } from "../ViewContext"; import { ViewState } from "../ViewState"; import { - BatchedTileIdMap, CesiumIonAssetProvider, createClassifierTileTreeReference, createDefaultViewFlagOverrides, DisclosedTileTreeSet, GeometryTileTreeReference, - getGcsConverterAvailable, RealityTile, RealityTileLoader, RealityTileParams, RealityTileTree, RealityTileTreeParams, SpatialClassifierTileTreeReference, Tile, - TileDrawArgs, TileLoadPriority, TileRequest, TileTree, TileTreeOwner, TileTreeReference, TileTreeSupplier, + BatchedTileIdMap, CesiumIonAssetProvider, createClassifierTileTreeReference, createDefaultViewFlagOverrides, createMapLayerTreeReference, DisclosedTileTreeSet, GeometryTileTreeReference, + getGcsConverterAvailable, ImageryMapLayerTreeReference, ImageryMapTileTree, ImageryTileTreeState, MapLayerTileTreeReference, ModelMapLayerTileTreeReference, RealityTile, RealityTileLoader, RealityTileParams, RealityTileTree, RealityTileTreeParams, SpatialClassifierTileTreeReference, Tile, + TileDrawArgs, TileLoadPriority, TileRequest, TileTree, TileTreeLoadStatus, TileTreeOwner, TileTreeReference, TileTreeSupplier, } from "./internal"; import { SpatialClassifiersState } from "../SpatialClassifiersState"; +import { RenderPlanarClassifier } from "../render/RenderPlanarClassifier"; function getUrl(content: any) { return content ? (content.url ? content.url : content.uri) : undefined; @@ -530,15 +533,66 @@ class RealityModelTileLoader extends RealityTileLoader { /** @internal */ export type RealityModelSource = ViewState | DisplayStyleState; +// ###TODO this is a duplicate from MapTileTree.ts! +/** Utility interface that ties an imagery tile tree to its corresponding map-layer settings object. + * @internal + */ +interface MapLayerTreeSetting { + tree: ImageryMapTileTree; + settings: MapLayerSettings; + baseImageryLayer: boolean; +} + /** @internal */ export class RealityModelTileTree extends RealityTileTree { private readonly _isContentUnbounded: boolean; + public layerImageryTrees: MapLayerTreeSetting[] = []; + private _layerSettings = new Map(); + private _imageryTreeState = new Map(); + private _modelIdToIndex = new Map(); + /** @internal */ + public layerClassifiers = new Map(); + public constructor(params: RealityTileTreeParams) { super(params); this._isContentUnbounded = this.rootTile.contentRange.diagonal().magnitude() > 2 * Constant.earthRadiusWGS84.equator; } public override get isContentUnbounded() { return this._isContentUnbounded; } + + /** Add a new imagery tile tree / map-layer settings pair and initialize the imagery tile tree state. + * @internal + */ + public addImageryLayer(tree: ImageryMapTileTree, settings: MapLayerSettings, index: number, baseImageryLayer: boolean) { + this.layerImageryTrees.push({ tree, settings, baseImageryLayer }); + this._layerSettings.set(tree.modelId, settings); + if (!this._imageryTreeState.has(tree.modelId)) + this._imageryTreeState.set(tree.modelId, new ImageryTileTreeState()); + this._modelIdToIndex.set(tree.modelId, index); + } + + /** @internal */ + public addModelLayer(layerTreeRef: ModelMapLayerTileTreeReference, context: SceneContext) { + const classifier = context.addPlanarClassifier(`MapLayer ${this.modelId}-${layerTreeRef.layerIndex}`, layerTreeRef); + if (classifier) + this.layerClassifiers.set(layerTreeRef.layerIndex, classifier); + } + + /** @internal */ + public clearLayers() { + this._rootTile.clearLayers(); + } + + /** @internal */ + protected override collectClassifierGraphics(args: TileDrawArgs, selectedTiles: RealityTile[]) { + super.collectClassifierGraphics(args, selectedTiles); + + this.layerClassifiers.forEach((layerClassifier: RenderPlanarClassifier) => { + // if (!(args instanceof GraphicsCollectorDrawArgs)) + layerClassifier.collectGraphics(args.context, { modelId: this.modelId, tiles: selectedTiles, location: args.location, isPointCloud: this.isPointCloud }); + + }); + } } /** @internal */ @@ -553,6 +607,9 @@ export namespace RealityModelTileTree { classifiers?: SpatialClassifiersState; planarClipMask?: PlanarClipMaskSettings; getDisplaySettings(): RealityModelDisplaySettings; + mapSettings?: BackgroundMapSettings; + backgroundBase?: BaseLayerSettings; + backgroundLayers?: MapLayerSettings[]; } export interface ReferenceProps extends ReferenceBaseProps { @@ -566,11 +623,19 @@ export namespace RealityModelTileTree { protected _transform?: Transform; protected _iModel: IModelConnection; private _isGlobal?: boolean; + private readonly _layerTrees = new Array(); + private _baseImageryLayerIncluded = false; protected readonly _source: RealityModelSource; protected _planarClipMask?: PlanarClipMaskState; protected _classifier?: SpatialClassifierTileTreeReference; protected _mapDrapeTree?: TileTreeReference; protected _getDisplaySettings: () => RealityModelDisplaySettings; + private _baseLayerSettings: BaseLayerSettings | undefined; + private _baseTransparent = false; + private _baseColor?: ColorDef; + private _layerSettings: MapLayerSettings[] | undefined; + private _bgMapSettings?: BackgroundMapSettings; + private readonly _detachFromDisplayStyle: VoidFunction[] = []; public abstract get modelId(): Id64String; // public get classifiers(): SpatialClassifiers | undefined { return undefined !== this._classifier ? this._classifier.classifiers : undefined; } @@ -610,6 +675,37 @@ export namespace RealityModelTileTree { if (undefined !== props.classifiers) this._classifier = createClassifierTileTreeReference(props.classifiers, this, props.iModel, props.source); + + this._baseLayerSettings = props.backgroundBase; + const isOverlay = false; // ###TODO + + let tree; + if (!isOverlay && this._baseLayerSettings !== undefined) { + if (this._baseLayerSettings instanceof MapLayerSettings) { + tree = createMapLayerTreeReference(this._baseLayerSettings, 0, this._iModel); + this._baseTransparent = this._baseLayerSettings.transparency > 0; + } else { + this._baseColor = this._baseLayerSettings; + this._baseTransparent = this._baseColor?.getTransparency() > 0; + } + } + + if (this._baseImageryLayerIncluded = (undefined !== tree)) + this._layerTrees.push(tree); + + this._layerSettings = props.backgroundLayers; + + if (undefined !== this._layerSettings) { // ###TODO should this always be true? + for (let i = 0; i < this._layerSettings.length; i++) + if (undefined !== (tree = createMapLayerTreeReference(this._layerSettings[i], i + 1, this._iModel))) + this._layerTrees.push(tree); + } + + this._bgMapSettings = props.mapSettings; + if (this._bgMapSettings) { // ###TODO should this always be true? + if (this._bgMapSettings.planarClipMask && this._bgMapSettings.planarClipMask.isValid) + this._planarClipMask = PlanarClipMaskState.create(this._bgMapSettings.planarClipMask); + } } public get planarClassifierTreeRef() { return this._classifier && this._classifier.activeClassifier && this._classifier.isPlanar ? this._classifier : undefined; } @@ -629,7 +725,125 @@ export namespace RealityModelTileTree { return this._isGlobal === undefined ? false : this._isGlobal; } + // ###TODO lifted from MapTileTree.ts - should be refactored + public setBaseLayerSettings(baseLayerSettings: BaseLayerSettings) { + let tree; + this._baseLayerSettings = baseLayerSettings; + + if (baseLayerSettings instanceof MapLayerSettings) { + tree = createMapLayerTreeReference(baseLayerSettings, 0, this._iModel); + this._baseColor = undefined; + this._baseTransparent = baseLayerSettings.transparency > 0; + } else { + this._baseColor = baseLayerSettings; + this._baseTransparent = this._baseColor.getTransparency() > 0; + } + + if (tree) { + if (this._baseImageryLayerIncluded) + this._layerTrees[0] = tree; + else + this._layerTrees.splice(0, 0, tree); + } else { + if (this._baseImageryLayerIncluded) + this._layerTrees.shift(); + } + this._baseImageryLayerIncluded = tree !== undefined; + this.clearLayers(); + } + + public setLayerSettings(layerSettings: MapLayerSettings[]) { + this._layerSettings = layerSettings; + const baseLayerIndex = this._baseImageryLayerIncluded ? 1 : 0; + + this._layerTrees.length = Math.min(layerSettings.length + baseLayerIndex, this._layerTrees.length); // Truncate if number of layers reduced. + for (let i = 0; i < layerSettings.length; i++) { + const treeIndex = i + baseLayerIndex; + if (treeIndex >= this._layerTrees.length || !this._layerTrees[treeIndex]?.layerSettings.displayMatches(layerSettings[i])) + this._layerTrees[treeIndex] = createMapLayerTreeReference(layerSettings[i], treeIndex, this._iModel)!; + } + this.clearLayers(); + } + + public clearLayers() { + const tree = this.treeOwner.tileTree as RealityModelTileTree; + if (undefined !== tree) + tree.clearLayers(); + } + + public initializeLayers(context: SceneContext): boolean { + const removals = this._detachFromDisplayStyle; + if (0 === removals.length) { + removals.push(context.viewport.displayStyle.settings.onMapImageryChanged.addListener((imagery: Readonly) => { + this.setBaseLayerSettings(imagery.backgroundBase); + this.setLayerSettings(imagery.backgroundLayers); + this.clearLayers(); + })); + } + + let hasLoadedTileTree = false; + const tree = this.treeOwner.load() as RealityModelTileTree; + if (undefined === tree) { + return hasLoadedTileTree; // Not loaded yet. + } + + tree.layerImageryTrees.length = 0; + if (0 === this._layerTrees.length) { + // return !this.isOverlay; + // ###TODO + return true; + } + + let treeIndex = this._layerTrees.length - 1; + // Start displaying at the highest completely opaque layer... + for (; treeIndex >= 1; treeIndex--) { + const layerTreeRef = this._layerTrees[treeIndex]; + if (layerTreeRef?.isOpaque) + break; // This layer is completely opaque and will obscure all others so ignore lower ones. + } + + for (; treeIndex < this._layerTrees.length; treeIndex++) { + const layerTreeRef = this._layerTrees[treeIndex]; + const hasValidTileTree = layerTreeRef && TileTreeLoadStatus.NotFound !== layerTreeRef.treeOwner.loadStatus; + const isImageryMapLayer = layerTreeRef instanceof ImageryMapLayerTreeReference; + const isLayerVisible = (isImageryMapLayer || (!isImageryMapLayer && layerTreeRef?.layerSettings.visible)); + + if (true !== layerTreeRef?.layerSettings.toRealityData) { + // If the layer is not to be displayed on top of reality data, then we should skip adding it to the tile tree. + hasLoadedTileTree = true; // ###TODO had to set this to true so addToScene actually works. alternative? + continue; + } + + // Load tile tree for each configured layer. + // Note: Non-visible imagery layer are always added to allow proper tile tree scale range visibility reporting. + if (hasValidTileTree + && isLayerVisible + && !layerTreeRef.layerSettings.allSubLayersInvisible) { + const layerTree = layerTreeRef.treeOwner.load(); + if (layerTree !== undefined) { + hasLoadedTileTree = true; + } else { + // Let's continue, there might be loaded tile tree in the list + continue; + } + + // Add loaded TileTree + const baseImageryLayer = this._baseImageryLayerIncluded && (treeIndex === 0); + if (layerTree instanceof ImageryMapTileTree) { + tree.addImageryLayer(layerTree, layerTreeRef.layerSettings, treeIndex, baseImageryLayer); + } else if (layerTreeRef instanceof ModelMapLayerTileTreeReference) + tree.addModelLayer(layerTreeRef, context); + } + } + + return hasLoadedTileTree; + } + public override addToScene(context: SceneContext): void { + const tree = this.treeOwner.load() as RealityModelTileTree; + if (undefined === tree || !this.initializeLayers(context)) + return; // Not loaded yet. + // NB: The classifier must be added first, so we can find it when adding our own tiles. if (this._classifier && this._classifier.activeClassifier) this._classifier.addToScene(context); diff --git a/core/frontend/src/tile/RealityTile.ts b/core/frontend/src/tile/RealityTile.ts index b46b302e81b2..556ff3fe15b4 100644 --- a/core/frontend/src/tile/RealityTile.ts +++ b/core/frontend/src/tile/RealityTile.ts @@ -579,6 +579,14 @@ export class RealityTile extends Tile { resolve(stepChildren); } + /** @internal */ + public clearLayers() { + this._graphic = undefined; + if (this.children) + for (const child of this.children) + (child as RealityTile).clearLayers(); + } + /** @internal */ public override produceGraphics(): RenderGraphic | undefined { if (undefined === this._reprojectionTransform) diff --git a/core/frontend/src/tile/RealityTileLoader.ts b/core/frontend/src/tile/RealityTileLoader.ts index 890fecb85a59..ab0865748cf9 100644 --- a/core/frontend/src/tile/RealityTileLoader.ts +++ b/core/frontend/src/tile/RealityTileLoader.ts @@ -130,7 +130,7 @@ export abstract class RealityTileLoader { return { graphic }; case TileFormat.B3dm: - reader = B3dmReader.create(streamBuffer, iModel, modelId, is3d, tile.contentRange, system, yAxisUp, tile.isLeaf, tile.center, tile.transformToRoot, isCanceled, this.getBatchIdMap(), this.wantDeduplicatedVertices); + reader = B3dmReader.create(streamBuffer, iModel, modelId, is3d, tile.contentRange, system, yAxisUp, tile.isLeaf, tile.center, tile.transformToRoot, isCanceled, this.getBatchIdMap(), this.wantDeduplicatedVertices, tile); if (reader) { // glTF spec defaults wrap mode to "repeat" but many reality tiles omit the wrap mode and should not repeat. // The render system also currently only produces mip-maps for repeating textures, and we don't want mip-maps for reality tile textures. @@ -140,7 +140,7 @@ export abstract class RealityTileLoader { break; case TileFormat.I3dm: - reader = I3dmReader.create(streamBuffer, iModel, modelId, is3d, tile.contentRange, system, yAxisUp, tile.isLeaf, isCanceled, undefined, this.wantDeduplicatedVertices); + reader = I3dmReader.create(streamBuffer, iModel, modelId, is3d, tile.contentRange, system, yAxisUp, tile.isLeaf, isCanceled, undefined, this.wantDeduplicatedVertices, tile); break; case TileFormat.Gltf: const props = GltfReaderProps.create(streamBuffer.nextBytes(streamBuffer.arrayBuffer.byteLength), yAxisUp); @@ -153,6 +153,7 @@ export abstract class RealityTileLoader { hasChildren: !tile.isLeaf, pickableOptions: { id: modelId }, idMap: this.getBatchIdMap(), + tile }); } diff --git a/core/frontend/src/tile/map/MapTile.ts b/core/frontend/src/tile/map/MapTile.ts index 42055ae8451a..967f5b0c7372 100644 --- a/core/frontend/src/tile/map/MapTile.ts +++ b/core/frontend/src/tile/map/MapTile.ts @@ -90,7 +90,7 @@ class EllipsoidProjection extends MapTileProjection { } /** @alpha */ -class PlanarProjection extends MapTileProjection { +export class PlanarProjection extends MapTileProjection { private _bilinearPatch: BilinearPatch; public transformFromLocal: Transform; public localRange: Range3d; @@ -498,15 +498,6 @@ export class MapTile extends RealityTile { return FrustumPlanes.Containment.Outside === args.frustumPlanes.computeContainment(this.getRangeCorners(scratchCorners)); } - /** @internal */ - public clearLayers() { - this.clearImageryTiles(); - this._graphic = undefined; - if (this.children) - for (const child of this.children) - (child as MapTile).clearLayers(); - } - private clearImageryTiles() { if (this._imageryTiles) { this._imageryTiles.forEach((tile) => tile.releaseMapTileUsage()); diff --git a/core/frontend/src/tile/map/MapTileTree.ts b/core/frontend/src/tile/map/MapTileTree.ts index a278b8c2b60b..373486d3766f 100644 --- a/core/frontend/src/tile/map/MapTileTree.ts +++ b/core/frontend/src/tile/map/MapTileTree.ts @@ -1004,6 +1004,13 @@ export class MapTileTreeReference extends TileTreeReference { const hasValidTileTree = layerTreeRef && TileTreeLoadStatus.NotFound !== layerTreeRef.treeOwner.loadStatus; const isImageryMapLayer = layerTreeRef instanceof ImageryMapLayerTreeReference; const isLayerVisible = (isImageryMapLayer || (!isImageryMapLayer && layerTreeRef?.layerSettings.visible)); + + if (true === layerTreeRef?.layerSettings.toRealityData) { + // If the layer is not to be displayed on top of background maps, then we should skip adding it to the tile tree. + hasLoadedTileTree = true; // ###TODO had to set this to true so addToScene actually works. alternative? + continue; + } + // Load tile tree for each configured layer. // Note: Non-visible imagery layer are always added to allow proper tile tree scale range visibility reporting. if (hasValidTileTree