diff --git a/common/api/core-frontend.api.md b/common/api/core-frontend.api.md index b40ba285ce5f..efc0eb042164 100644 --- a/common/api/core-frontend.api.md +++ b/common/api/core-frontend.api.md @@ -8430,7 +8430,7 @@ export class RealityTile extends Tile { selectSecondaryTiles(_args: TileDrawArgs, _context: TraversalSelectionContext): void; // @internal (undocumented) setContent(content: RealityTileContent): void; - // @internal (undocumented) + // @internal readonly transformToRoot?: Transform; // @internal (undocumented) readonly tree: RealityTileTree; diff --git a/core/frontend/src/internal/tile/B3dmReader.ts b/core/frontend/src/internal/tile/B3dmReader.ts index e81161eb3bda..873de1ba666f 100644 --- a/core/frontend/src/internal/tile/B3dmReader.ts +++ b/core/frontend/src/internal/tile/B3dmReader.ts @@ -59,7 +59,7 @@ export class B3dmReader extends GltfReader { transformToRoot, header.batchTableJson, isCanceled, idMap, pseudoRtcBias, deduplicateVertices, tileData) : undefined; } - private constructor(props: GltfReaderProps, iModel: IModelConnection, modelId: Id64String, is3d: boolean, system: RenderSystem, + public 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, tileData?: LayerTileData) { super({ diff --git a/core/frontend/src/internal/tile/RealityModelTileTree.ts b/core/frontend/src/internal/tile/RealityModelTileTree.ts index 3b75afeac28e..6ef9ca94e056 100644 --- a/core/frontend/src/internal/tile/RealityModelTileTree.ts +++ b/core/frontend/src/internal/tile/RealityModelTileTree.ts @@ -534,6 +534,7 @@ class RealityModelTileLoader extends RealityTileLoader { const thisParentId = parentId.length ? (`${parentId}_${childId}`) : childId; if (foundChild.transform) { const thisTransform = RealityModelTileUtils.transformFromJson(foundChild.transform); + // Accumulate tile's transform to apply it to this tile's children transformToRoot = transformToRoot ? transformToRoot.multiplyTransformTransform(thisTransform) : thisTransform; } diff --git a/core/frontend/src/internal/tile/RealityTileLoader.ts b/core/frontend/src/internal/tile/RealityTileLoader.ts index 88b6b0b1bb12..f1a0c6ec58fd 100644 --- a/core/frontend/src/internal/tile/RealityTileLoader.ts +++ b/core/frontend/src/internal/tile/RealityTileLoader.ts @@ -88,7 +88,15 @@ export abstract class RealityTileLoader { if (reader) reader.defaultWrapMode = GltfWrapMode.ClampToEdge; - const geom = reader?.readGltfAndCreateGeometry(tile.tree.iModelTransform); + let transform = tile.tree.iModelTransform; + if (tile.transformToRoot) { + transform = transform.multiplyTransformTransform(tile.transformToRoot); + } + console.log("RealityTileLoader.loadGeometryFromStream - transformToRoot argument:", transform); + + const geom = reader?.readGltfAndCreateGeometry(transform); + + // See RealityTileTree.reprojectAndResolveChildren for how reprojectionTransform is calculated const xForm = tile.reprojectionTransform; if (tile.tree.reprojectGeometry && geom?.polyfaces && xForm) { const polyfaces = geom.polyfaces.map((pf) => pf.cloneTransformed(xForm)); @@ -98,7 +106,7 @@ export abstract class RealityTileLoader { } } - private async loadGraphicsFromStream(tile: RealityTile, streamBuffer: ByteStream, system: RenderSystem, isCanceled?: () => boolean): Promise { + public async loadGraphicsFromStream(tile: RealityTile, streamBuffer: ByteStream, system: RenderSystem, isCanceled?: () => boolean): Promise { const format = this._getFormat(streamBuffer); if (undefined === isCanceled) isCanceled = () => !tile.isLoading; @@ -149,6 +157,8 @@ export abstract class RealityTileLoader { return { graphic }; case TileFormat.B3dm: + console.log("RealityTileLoader.loadGraphicsFromStream - B3dmReader.create - transformToRoot argument:", tile.transformToRoot); + reader = B3dmReader.create(streamBuffer, iModel, modelId, is3d, tile.contentRange, system, yAxisUp, tile.isLeaf, tile.center, tile.transformToRoot, isCanceled, this.getBatchIdMap(), this.wantDeduplicatedVertices, tileData); if (reader) { // glTF spec defaults wrap mode to "repeat" but many reality tiles omit the wrap mode and should not repeat. diff --git a/core/frontend/src/test/tile/RealityTile.test.ts b/core/frontend/src/test/tile/RealityTile.test.ts index 76ed96901240..d7ca70d99fa4 100644 --- a/core/frontend/src/test/tile/RealityTile.test.ts +++ b/core/frontend/src/test/tile/RealityTile.test.ts @@ -3,28 +3,27 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ -import sinon from "sinon"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { ByteStream, Id64String } from "@itwin/core-bentley"; -import { ElementAlignedBox3d, TileFormat } from "@itwin/core-common"; +import { afterEach, beforeEach, describe, expect, it, MockInstance, vi } from "vitest"; +import { ByteStream } from "@itwin/core-bentley"; +import { TileFormat } from "@itwin/core-common"; import { Point3d, PolyfaceBuilder, Range3d, StrokeOptions, Transform } from "@itwin/core-geometry"; import { IModelConnection } from "../../IModelConnection"; import { IModelApp } from "../../IModelApp"; import { MockRender } from "../../internal/render/MockRender"; import { RenderMemory } from "../../render/RenderMemory"; import { - B3dmReader, BatchedTileIdMap, LayerTileData, RealityTile, RealityTileLoader, RealityTileTree, - ShouldAbortReadGltf, Tile, TileDrawArgs, TileLoadPriority, TileRequest, TileRequestChannel + B3dmReader, GltfReaderProps, GltfReaderResult, RealityTile, RealityTileGeometry, RealityTileLoader, RealityTileTree, + Tile, TileDrawArgs, TileLoadPriority, TileRequest, TileRequestChannel } from "../../tile/internal"; import { createBlankConnection } from "../createBlankConnection"; -import { RenderSystem } from "../../render/RenderSystem"; describe("RealityTile", () => { class TestRealityTile extends RealityTile { private readonly _contentSize: number; public visible = true; + public override transformToRoot?: Transform | undefined; - public constructor(tileTree: RealityTileTree, contentSize: number, reprojectionTransform?: Transform) { + public constructor(tileTree: RealityTileTree, contentSize: number, reprojectTransform?: Transform, transformToRoot?: Transform) { super({ contentId: contentSize.toString(), range: new Range3d(0, 0, 0, 1, 1, 1), @@ -36,7 +35,8 @@ describe("RealityTile", () => { if (contentSize === 0) this.setIsReady(); - this._reprojectionTransform = reprojectionTransform; + this._reprojectionTransform = reprojectTransform; + this.transformToRoot = transformToRoot; } protected override _loadChildren(resolve: (children: Tile[] | undefined) => void): void { @@ -99,7 +99,7 @@ describe("RealityTile", () => { public readonly contentSize: number; protected override readonly _rootTile: TestRealityTile; - public constructor(contentSize: number, iModel: IModelConnection, loader: TestRealityTileLoader, reprojectGeometry: boolean, reprojectionTransform?: Transform) { + public constructor(contentSize: number, iModel: IModelConnection, loader: TestRealityTileLoader, reprojectGeometry: boolean, reprojectTransform?: Transform) { super({ loader, rootTile: { @@ -109,7 +109,8 @@ describe("RealityTile", () => { }, id: (++TestRealityTree._nextId).toString(), modelId: "0", - location: Transform.createIdentity(), + location: Transform.createTranslationXYZ(2, 2, 2), + // location: Transform.createIdentity(), priority: TileLoadPriority.Primary, iModel, gcsConverterAvailable: false, @@ -118,7 +119,9 @@ describe("RealityTile", () => { this.treeId = TestRealityTree._nextId; this.contentSize = contentSize; - this._rootTile = new TestRealityTile(this, contentSize, reprojectionTransform); + + const transformToRoot = Transform.createTranslationXYZ(10, 10, 10); + this._rootTile = new TestRealityTile(this, contentSize, reprojectTransform, transformToRoot); } public override get rootTile(): TestRealityTile { return this._rootTile; } @@ -150,6 +153,26 @@ describe("RealityTile", () => { public override prune() { } } + class TestB3dmReader extends B3dmReader { + public override readGltfAndCreateGeometry(transformToRoot?: Transform, needNormals?: boolean, needParams?: boolean): RealityTileGeometry { + // Create mock geometry data with a simple polyface + const options = StrokeOptions.createForFacets(); + const polyBuilder = PolyfaceBuilder.create(options); + polyBuilder.addPolygon([ + Point3d.create(0, 0, 0), + Point3d.create(1, 0, 0), + Point3d.create(1, 1, 0) + ]); + const originalPolyface = polyBuilder.claimPolyface(); + const mockGeometry = { polyfaces: [originalPolyface] }; + return mockGeometry; + } + + public override readGltfAndCreateGraphics(isLeaf: boolean, featureTable: FeatureTable | undefined, contentRange: Range3d | undefined, transformToRoot?: Transform | undefined, pseudoRtcBias?: Vector3d | undefined, instances?: InstancedGraphicParams | undefined): GltfReaderResult { + return {} as any as GltfReaderResult; + } + } + function expectPointToEqual(point: Point3d, x: number, y: number, z: number) { expect(point.x).to.equal(x); expect(point.y).to.equal(y); @@ -158,16 +181,19 @@ describe("RealityTile", () => { let imodel: IModelConnection; let reader: TestRealityTileLoader; - let transform: Transform; + let reprojectionTransform: Transform; let streamBuffer: ByteStream; - const sandbox = sinon.createSandbox(); + let createGeometrySpy: MockInstance; + let createGraphicsSpy: MockInstance; + let createReaderSpy: MockInstance; + let createGltfReaderPropsSpy: MockInstance; beforeEach(async () => { await MockRender.App.startup(); IModelApp.stopEventLoop(); imodel = createBlankConnection("imodel"); reader = new TestRealityTileLoader(); - transform = Transform.createTranslationXYZ(5, 5, 5); + reprojectionTransform = Transform.createTranslationXYZ(5, 5, 5); // Create a ByteStream with B3dm format header const buffer = new Uint8Array(16); @@ -175,28 +201,16 @@ describe("RealityTile", () => { view.setUint32(0, TileFormat.B3dm, true); streamBuffer = ByteStream.fromUint8Array(buffer); - // Create mock geometry data with a simple polyface - const options = StrokeOptions.createForFacets(); - const polyBuilder = PolyfaceBuilder.create(options); - polyBuilder.addPolygon([ - Point3d.create(0, 0, 0), - Point3d.create(1, 0, 0), - Point3d.create(1, 1, 0) - ]); - const originalPolyface = polyBuilder.claimPolyface(); - const mockGeometry = { polyfaces: [originalPolyface] }; - // Mock B3dmReader.create to return a reader with test geometry - const mockReader = { - defaultWrapMode: undefined, - readGltfAndCreateGeometry(this: void) { return mockGeometry; } - } as any as B3dmReader; - - sandbox.stub(B3dmReader, "create").callsFake((_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, _tileData?: LayerTileData) => { - return mockReader; - }); + createGltfReaderPropsSpy = vi.spyOn(GltfReaderProps, "create").mockReturnValue({ version: 1, glTF: {}, yAxisUp: true}); + const props = GltfReaderProps.create({}, true, new URL("http://www.sometestsite.com/tileset.json")); + + const transformToRoot = Transform.createTranslationXYZ(10, 10, 10); + const testReader = new TestB3dmReader(props!, imodel, "0", true, IModelApp.renderSystem, Range3d.createNull(), true, 0, transformToRoot); + + createReaderSpy = vi.spyOn(B3dmReader, "create").mockReturnValue(testReader); + createGeometrySpy = vi.spyOn(testReader, "readGltfAndCreateGeometry"); + createGraphicsSpy = vi.spyOn(testReader, "readGltfAndCreateGraphics"); }); afterEach(async () => { @@ -204,12 +218,12 @@ describe("RealityTile", () => { if (IModelApp.initialized) await MockRender.App.shutdown(); - sandbox.restore(); + vi.restoreAllMocks(); }); it("should apply reprojection transform to geometry in loadGeometryFromStream", async () => { // Create a test tree with reprojectGeometry = true - const tree = new TestRealityTree(0, imodel, reader, true, transform); + const tree = new TestRealityTree(0, imodel, reader, true, reprojectionTransform); const tile = tree.rootTile; const result = await reader.loadGeometryFromStream(tile, streamBuffer, IModelApp.renderSystem); @@ -229,7 +243,7 @@ describe("RealityTile", () => { it("should not apply reprojection transform when reprojectGeometry is false", async () => { // Create a test tree with reprojectGeometry = false - const tree = new TestRealityTree(0, imodel, reader, false, transform); + const tree = new TestRealityTree(0, imodel, reader, false, reprojectionTransform); const tile = tree.rootTile; const result = await reader.loadGeometryFromStream(tile, streamBuffer, IModelApp.renderSystem); @@ -269,7 +283,7 @@ describe("RealityTile", () => { it("should not apply reprojection transform twice", async () => { // Create a test tree with reprojectGeometry = true - const tree = new TestRealityTree(0, imodel, reader, true, transform); + const tree = new TestRealityTree(0, imodel, reader, true, reprojectionTransform); const tile = tree.rootTile; // Loop to call loadGeometryFromStream twice @@ -290,4 +304,34 @@ describe("RealityTile", () => { } } }); + + /* + - Want to test that result of GltfReader.readGltfAndCreateGraphics() and GltfReader.readGltfAndCreateGeometry() + are the same (functions are called w same transform?) + - When readGltfAndCreateGeometry() is called, the transform param is the same as the transform param when readGltfAndCreateGraphics() is called + - createGeometry() called with loadGeometryFromStream() - already ready to be called in a test, mocked (switch from sinon to vitest) + */ + it.only("creating tile graphics and tile geometry apply the same transform", async () => { + const tree = new TestRealityTree(0, imodel, reader, true, reprojectionTransform); + const tile = tree.rootTile; + const resultGeometry = await reader.loadGeometryFromStream(tile, streamBuffer, IModelApp.renderSystem); + const resultGraphics = await reader.loadGraphicsFromStream(tile, streamBuffer, IModelApp.renderSystem); + + expect(createGltfReaderPropsSpy).toHaveBeenCalledOnce(); + expect(createReaderSpy).toHaveBeenCalled(); + expect(createGeometrySpy).toHaveBeenCalledOnce(); + expect(createGraphicsSpy).toHaveBeenCalledOnce(); + + const testTransformResult = Transform.createTranslationXYZ(2, 2, 2).multiplyTransformTransform(Transform.createTranslationXYZ(10, 10, 10)); + // console.log("test transform result:", testTransformResult); + + // TODO this is going to be too annoying to test unless the root tile has a real child tile whose transformToRoot can be tested + + const expectedTransform = Transform.createIdentity(); + const geometryTransform = createGeometrySpy.mock.calls[0][0]; + expect(geometryTransform).toEqual(testTransformResult); + + const graphicsTransform = createGraphicsSpy.mock.calls[0][3]; + expect(graphicsTransform).toEqual(testTransformResult); + }); }); \ No newline at end of file diff --git a/core/frontend/src/tile/RealityTile.ts b/core/frontend/src/tile/RealityTile.ts index 7fb851901009..63c240735903 100644 --- a/core/frontend/src/tile/RealityTile.ts +++ b/core/frontend/src/tile/RealityTile.ts @@ -56,7 +56,9 @@ const scratchFrustum = new Frustum(); * @public */ export class RealityTile extends Tile { - /** @internal */ + /** Transform to go from tile's local coordinate system to the root tile's corodinate system. + * @see [[RealityModelTileLoader.findTileInJson]] to see how the transformToRoot is calculated. + * @internal */ public readonly transformToRoot?: Transform; /** @internal */ public readonly additiveRefinement?: boolean; diff --git a/core/frontend/src/tile/RealityTileTree.ts b/core/frontend/src/tile/RealityTileTree.ts index 1548216b836f..fc8f698d296d 100644 --- a/core/frontend/src/tile/RealityTileTree.ts +++ b/core/frontend/src/tile/RealityTileTree.ts @@ -415,6 +415,7 @@ export class RealityTileTree extends TileTree { const reprojectedCoords = response.iModelCoords; const dbToRoot = expectDefined(rootToDb.inverse()); + // Interpolate between the original and reprojected points const getReprojectedPoint = (original: Point3d, reprojectedXYZ: XYZProps) => { scratchPoint.setFromJSON(reprojectedXYZ); const cartesianDistance = this.cartesianRange.distanceToPoint(scratchPoint);