diff --git a/common/api/core-common.api.md b/common/api/core-common.api.md index 37306466aeec..f9df3f00af1c 100644 --- a/common/api/core-common.api.md +++ b/common/api/core-common.api.md @@ -1864,6 +1864,8 @@ export class ContextRealityModel { // @beta readonly rdSourceKey?: RealityDataSourceKey; readonly realityDataId?: string; + // @alpha + get skipGcsConversion(): boolean; toJSON(): ContextRealityModelProps; readonly url: string; } @@ -1884,6 +1886,8 @@ export interface ContextRealityModelProps { // @beta rdSourceKey?: RealityDataSourceKey; realityDataId?: string; + // @alpha + skipGcsConversion?: boolean; tilesetUrl: string; } @@ -7694,6 +7698,7 @@ export interface RealityDataSourceKey { id: string; iTwinId?: string; provider: string; + skipGcsConversion?: boolean; } // @beta diff --git a/common/api/core-frontend.api.md b/common/api/core-frontend.api.md index 4ae54e616654..8fcc58c71bb3 100644 --- a/common/api/core-frontend.api.md +++ b/common/api/core-frontend.api.md @@ -8420,6 +8420,8 @@ export class RealityTileTree extends TileTree { // @internal (undocumented) protected _selectTiles(args: TileDrawArgs): Tile[]; // @internal (undocumented) + readonly skipGcsConversion: boolean; + // @internal (undocumented) traversalChildrenByDepth: TraversalChildrenDetails[]; // @internal (undocumented) get viewFlagOverrides(): ViewFlagOverrides; diff --git a/common/api/frontend-devtools.api.md b/common/api/frontend-devtools.api.md index a3fd464b805e..79b90a6098a2 100644 --- a/common/api/frontend-devtools.api.md +++ b/common/api/frontend-devtools.api.md @@ -2620,7 +2620,7 @@ export class ViewportAddRealityModel extends Tool { // (undocumented) static get minArgs(): number; parseAndRun(...args: string[]): Promise; - run(url: string): Promise; + run(url: string, skipGcsConversion: boolean): Promise; // (undocumented) static toolId: string; } diff --git a/common/changes/@itwin/core-common/markschlosser-reality-use-backend-gcs-option_2025-06-03-12-35.json b/common/changes/@itwin/core-common/markschlosser-reality-use-backend-gcs-option_2025-06-03-12-35.json new file mode 100644 index 000000000000..331c354a7276 --- /dev/null +++ b/common/changes/@itwin/core-common/markschlosser-reality-use-backend-gcs-option_2025-06-03-12-35.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-common", + "comment": "Add skip GCS conversion option to reality tiles.", + "type": "none" + } + ], + "packageName": "@itwin/core-common" +} \ No newline at end of file diff --git a/common/changes/@itwin/core-frontend/markschlosser-reality-use-backend-gcs-option_2025-06-03-12-35.json b/common/changes/@itwin/core-frontend/markschlosser-reality-use-backend-gcs-option_2025-06-03-12-35.json new file mode 100644 index 000000000000..44556860eee3 --- /dev/null +++ b/common/changes/@itwin/core-frontend/markschlosser-reality-use-backend-gcs-option_2025-06-03-12-35.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-frontend", + "comment": "Add skip GCS conversion option to reality tiles.", + "type": "none" + } + ], + "packageName": "@itwin/core-frontend" +} \ No newline at end of file diff --git a/common/changes/@itwin/frontend-devtools/markschlosser-reality-use-backend-gcs-option_2025-06-03-12-35.json b/common/changes/@itwin/frontend-devtools/markschlosser-reality-use-backend-gcs-option_2025-06-03-12-35.json new file mode 100644 index 000000000000..1d213442266d --- /dev/null +++ b/common/changes/@itwin/frontend-devtools/markschlosser-reality-use-backend-gcs-option_2025-06-03-12-35.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/frontend-devtools", + "comment": "Add skip GCS conversion option to reality tiles.", + "type": "none" + } + ], + "packageName": "@itwin/frontend-devtools" +} \ No newline at end of file diff --git a/core/common/src/ContextRealityModel.ts b/core/common/src/ContextRealityModel.ts index d44e009e01d1..2ef6fc79f596 100644 --- a/core/common/src/ContextRealityModel.ts +++ b/core/common/src/ContextRealityModel.ts @@ -104,7 +104,10 @@ export interface RealityDataSourceKey { id: string; /** The context id that was used when reality data was attached - if none provided, current session iTwinId will be used */ iTwinId?: string; + /** If true, do not reproject tiles for this model using the GCS from the backend. Instead, just use the basic ECEF transformation. */ + skipGcsConversion?: boolean; } + /** * RealityDataSourceKey utility functions * @beta */ @@ -168,6 +171,11 @@ export interface ContextRealityModelProps { * @beta */ invisible?: boolean; + /** + * See [[ContextRealityModel.skipGcsConversion]] + * @alpha + */ + skipGcsConversion?: boolean; } /** @public */ @@ -214,6 +222,9 @@ export namespace ContextRealityModelProps { if (input.invisible) output.invisible = input.invisible; + if (input.skipGcsConversion) + output.skipGcsConversion = input.skipGcsConversion; + return output; } } @@ -242,6 +253,7 @@ export class ContextRealityModel { /** An optional identifier that, if present, can be used to elide a request to the reality data service. */ public readonly realityDataId?: string; + private _skipGcsConversion: boolean; private _invisible: boolean; private readonly _classifiers: SpatialClassifiers; /** @alpha */ @@ -276,6 +288,7 @@ export class ContextRealityModel { this.realityDataId = props.realityDataId; this.description = props.description ?? ""; this._invisible = props.invisible ?? false; + this._skipGcsConversion = props.skipGcsConversion ?? false; this._appearanceOverrides = props.appearanceOverrides ? FeatureAppearance.fromJSON(props.appearanceOverrides) : undefined; this._displaySettings = RealityModelDisplaySettings.fromJSON(props.displaySettings); @@ -334,6 +347,14 @@ export class ContextRealityModel { this._displaySettings = settings; } + /** + * If true, do not reproject tiles for this model using the GCS from the backend. Instead, just use the basic ECEF transformation. + * @alpha + */ + public get skipGcsConversion(): boolean { + return this._skipGcsConversion; + } + /** If true, reality model is not drawn. * @beta */ diff --git a/core/frontend-devtools/README.md b/core/frontend-devtools/README.md index 72815ef67792..2e4d055eac6b 100644 --- a/core/frontend-devtools/README.md +++ b/core/frontend-devtools/README.md @@ -165,8 +165,9 @@ These keyins take the form `fdt attach maplayer ` where `for Reality models can be attached to a display style to provide context when that display style is used. These are sometimes referred to as "contextual" reality models to differentiate them from reality models that are attached to the project as spatial models. The keyins below provide methods to attach, detach and control their display. -* `fdt attach reality model` - Attach a "context" reality model to the currently selected viewport. +* `fdt attach reality model` - Attach a "context" reality model to the currently selected viewport. Arguments, in order, include: * the URL for the reality model root JSON file. + * optionally, a boolean, which when "true" will skip the backend GCS conversion when displaying this reality model. * `fdt attach cesium asset` - Attach a "context" reality model from Cesium ion. * the asset ID. * the authorization token. diff --git a/core/frontend-devtools/src/tools/ViewportTools.ts b/core/frontend-devtools/src/tools/ViewportTools.ts index 611b821aba3d..5cb6f12d0e15 100644 --- a/core/frontend-devtools/src/tools/ViewportTools.ts +++ b/core/frontend-devtools/src/tools/ViewportTools.ts @@ -447,24 +447,25 @@ export class ViewportTileSizeModifierTool extends Tool { export class ViewportAddRealityModel extends Tool { public static override toolId = "ViewportAddRealityModel"; public static override get minArgs() { return 1; } - public static override get maxArgs() { return 1; } + public static override get maxArgs() { return 2; } /** This method runs the tool, adding a reality model to the viewport * @param url the URL which points to the reality model tileset */ - public override async run(url: string): Promise { + public override async run(url: string, skipGcsConversion: boolean): Promise { const vp = IModelApp.viewManager.selectedView; if (undefined !== vp) - vp.displayStyle.attachRealityModel({ tilesetUrl: url }); + vp.displayStyle.attachRealityModel({ tilesetUrl: url, skipGcsConversion }); return true; } /** Executes this tool's run method with args[0] containing the `url` argument. + * args[1] can contain an optional "skipGcsConversion" argument, which if set to "true" will skip the GCS conversion. * @see [[run]] */ public override async parseAndRun(...args: string[]): Promise { - return this.run(args[0]); + return this.run(args[0], "true" === args[1] || "1" === args[1] || "yes" === args[1]); } } diff --git a/core/frontend/src/ContextRealityModelState.ts b/core/frontend/src/ContextRealityModelState.ts index 1f1fc1388c57..024f9ac2b033 100644 --- a/core/frontend/src/ContextRealityModelState.ts +++ b/core/frontend/src/ContextRealityModelState.ts @@ -50,6 +50,7 @@ export class ContextRealityModelState extends ContextRealityModel { name: props.name, classifiers: this.classifiers, planarClipMask: this.planarClipMaskSettings, + skipGcsConversion: props.skipGcsConversion, getDisplaySettings: () => this.displaySettings, getBackgroundBase: () => displayStyle.settings.mapImagery.backgroundBase, getBackgroundLayers: () => displayStyle.settings.mapImagery.backgroundLayers, diff --git a/core/frontend/src/internal/tile/RealityModelTileTree.ts b/core/frontend/src/internal/tile/RealityModelTileTree.ts index 98cefc00983a..fb95a5b5d760 100644 --- a/core/frontend/src/internal/tile/RealityModelTileTree.ts +++ b/core/frontend/src/internal/tile/RealityModelTileTree.ts @@ -45,6 +45,7 @@ interface RealityTreeId { maskModelIds?: string; deduplicateVertices: boolean; produceGeometry?: boolean; + skipGcsConversion?: boolean; displaySettings: RealityModelDisplaySettings; backgroundBase?: BaseLayerSettings; backgroundLayers?: MapLayerSettings[]; @@ -78,6 +79,7 @@ namespace RealityTreeId { compareRealityDataSourceKeys(lhs.rdSourceKey, rhs.rdSourceKey) || compareBooleans(lhs.deduplicateVertices, rhs.deduplicateVertices) || compareBooleansOrUndefined(lhs.produceGeometry, rhs.produceGeometry) || + compareBooleansOrUndefined(lhs.skipGcsConversion, rhs.skipGcsConversion) || compareStringsOrUndefined(lhs.maskModelIds, rhs.maskModelIds) || comparePossiblyUndefined((ltf, rtf) => compareTransforms(ltf, rtf), lhs.transform, rhs.transform) ); @@ -99,7 +101,7 @@ class RealityTreeSupplier implements TileTreeSupplier { if (treeId.maskModelIds) await iModel.models.load(CompressedId64Set.decompressSet(treeId.maskModelIds)); - const opts = { deduplicateVertices: treeId.deduplicateVertices, produceGeometry: treeId.produceGeometry }; + const opts = { deduplicateVertices: treeId.deduplicateVertices, produceGeometry: treeId.produceGeometry, skipGcsConversion: treeId.skipGcsConversion }; return RealityModelTileTree.createRealityModelTileTree(treeId.rdSourceKey, iModel, treeId.modelId, treeId.transform, opts); } @@ -273,16 +275,18 @@ class RealityModelTileTreeParams implements RealityTileTreeParams { public loader: RealityModelTileLoader; public rootTile: RealityTileParams; public baseUrl?: string; + public readonly skipGcsConversion?: boolean; public get location() { return this.loader.tree.location; } public get yAxisUp() { return this.loader.tree.yAxisUp; } public get priority() { return this.loader.priority; } - public constructor(tileTreeId: string, iModel: IModelConnection, modelId: Id64String, loader: RealityModelTileLoader, public readonly gcsConverterAvailable: boolean, public readonly rootToEcef: Transform | undefined, baseUrl?: string) { + public constructor(tileTreeId: string, iModel: IModelConnection, modelId: Id64String, loader: RealityModelTileLoader, public readonly gcsConverterAvailable: boolean, public readonly rootToEcef: Transform | undefined, baseUrl?: string, skipGcsConversion?: boolean) { this.loader = loader; this.id = tileTreeId; this.modelId = modelId; this.iModel = iModel; + this.skipGcsConversion = skipGcsConversion; const refine = loader.tree.tilesetJson.refine; this.rootTile = new RealityModelTileProps({ json: loader.tree.tilesetJson, @@ -561,6 +565,7 @@ export namespace RealityModelTileTree { name?: string; classifiers?: SpatialClassifiersState; planarClipMask?: PlanarClipMaskSettings; + skipGcsConversion?: boolean; getDisplaySettings(): RealityModelDisplaySettings; getBackgroundBase?(): BaseLayerSettings; getBackgroundLayers?(): MapLayerSettings[]; @@ -723,7 +728,7 @@ export namespace RealityModelTileTree { iModel: IModelConnection, modelId: Id64String, tilesetToDb: Transform | undefined, - opts?: { deduplicateVertices?: boolean, produceGeometry?: boolean }, + opts?: { deduplicateVertices?: boolean, produceGeometry?: boolean, skipGcsConversion?: boolean }, ): Promise { const rdSource = await RealityDataSource.fromKey(rdSourceKey, iModel.iTwinId); // If we can get a valid connection from sourceKey, returns the tile tree @@ -737,7 +742,7 @@ export namespace RealityModelTileTree { const gcsConverterAvailable = await getGcsConverterAvailable(iModel); //The full tileset url is needed so that it includes the url's search parameters if any are present const baseUrl = rdSource instanceof RealityDataSourceTilesetUrlImpl ? rdSource.getTilesetUrl() : undefined; - const params = new RealityModelTileTreeParams(tileTreeId, iModel, modelId, loader, gcsConverterAvailable, props.tilesetToEcef, baseUrl); + const params = new RealityModelTileTreeParams(tileTreeId, iModel, modelId, loader, gcsConverterAvailable, props.tilesetToEcef, baseUrl, opts?.skipGcsConversion); return new RealityModelTileTree(params); } return undefined; @@ -801,11 +806,13 @@ export namespace RealityModelTileTree { export class RealityTreeReference extends RealityModelTileTree.Reference { protected _rdSourceKey: RealityDataSourceKey; private readonly _produceGeometry?: boolean; + private readonly _skipGcsConversion?: boolean; private readonly _modelId: Id64String; public constructor(props: RealityModelTileTree.ReferenceProps) { super(props); this._produceGeometry = props.produceGeometry; + this._skipGcsConversion = props.skipGcsConversion; // Maybe we should throw if both props.rdSourceKey && props.url are undefined if (props.rdSourceKey) @@ -835,6 +842,7 @@ export class RealityTreeReference extends RealityModelTileTree.Reference { maskModelIds: this.maskModelIds, deduplicateVertices: this._wantWiremesh, produceGeometry: this._produceGeometry, + skipGcsConversion: this._skipGcsConversion, displaySettings: this._getDisplaySettings(), }; } @@ -851,6 +859,7 @@ export class RealityTreeReference extends RealityModelTileTree.Reference { rdSourceKey: this._rdSourceKey, name: this._name, produceGeometry: true, + skipGcsConversion: this._skipGcsConversion, getDisplaySettings: () => RealityModelDisplaySettings.defaults, }); diff --git a/core/frontend/src/tile/RealityTileTree.ts b/core/frontend/src/tile/RealityTileTree.ts index 7d8690bd82c8..5c1466f8dc4c 100644 --- a/core/frontend/src/tile/RealityTileTree.ts +++ b/core/frontend/src/tile/RealityTileTree.ts @@ -164,6 +164,7 @@ export interface RealityTileTreeParams extends TileTreeParams { readonly rootTile: RealityTileParams; readonly rootToEcef?: Transform; readonly gcsConverterAvailable: boolean; + readonly skipGcsConversion?: boolean; readonly baseUrl?: string; } @@ -192,6 +193,8 @@ export class RealityTileTree extends TileTree { protected _ecefToDb?: Transform; /** @internal */ public readonly baseUrl?: string; + /** @internal */ + public readonly skipGcsConversion: boolean; /** @internal */ public constructor(params: RealityTileTreeParams) { @@ -201,6 +204,7 @@ export class RealityTileTree extends TileTree { this._rootTile = this.createTile(params.rootTile); this.cartesianRange = BackgroundMapGeometry.getCartesianRange(this.iModel); this.cartesianTransitionDistance = this.cartesianRange.diagonal().magnitudeXY() * .25; // Transition distance from elliptical to cartesian. + this.skipGcsConversion = true === params.skipGcsConversion; this._gcsConverter = params.gcsConverterAvailable ? params.iModel.geoServices.getConverter("WGS84") : undefined; if (params.rootToEcef) { this._rootToEcef = params.rootToEcef; @@ -393,13 +397,16 @@ export class RealityTileTree extends TileTree { else { const requestProps = new Array(); - for (const reprojection of reprojectChildren) { - for (const dbPoint of reprojection.dbPoints) { - const ecefPoint = dbToEcef.multiplyPoint3d(dbPoint); - const carto = Cartographic.fromEcef(ecefPoint, scratchCarto); - if (carto) - requestProps.push({ x: carto.longitudeDegrees, y: carto.latitudeDegrees, z: carto.height }); - + // ###TODO - remove this console.log. + console.log(`reprojectAndResolveChildren, skipGcsConversion=${ this.skipGcsConversion}`); + if (!this.skipGcsConversion) { + for (const reprojection of reprojectChildren) { + for (const dbPoint of reprojection.dbPoints) { + const ecefPoint = dbToEcef.multiplyPoint3d(dbPoint); + const carto = Cartographic.fromEcef(ecefPoint, scratchCarto); + if (carto) + requestProps.push({ x: carto.longitudeDegrees, y: carto.latitudeDegrees, z: carto.height }); + } } }