Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion common/api/core-frontend.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion core/frontend/src/internal/tile/B3dmReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
1 change: 1 addition & 0 deletions core/frontend/src/internal/tile/RealityModelTileTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
14 changes: 12 additions & 2 deletions core/frontend/src/internal/tile/RealityTileLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -98,7 +106,7 @@ export abstract class RealityTileLoader {
}
}

private async loadGraphicsFromStream(tile: RealityTile, streamBuffer: ByteStream, system: RenderSystem, isCanceled?: () => boolean): Promise<TileContent> {
public async loadGraphicsFromStream(tile: RealityTile, streamBuffer: ByteStream, system: RenderSystem, isCanceled?: () => boolean): Promise<TileContent> {
const format = this._getFormat(streamBuffer);
if (undefined === isCanceled)
isCanceled = () => !tile.isLoading;
Expand Down Expand Up @@ -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.
Expand Down
124 changes: 84 additions & 40 deletions core/frontend/src/test/tile/RealityTile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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 {
Expand Down Expand Up @@ -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: {
Expand All @@ -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,
Expand All @@ -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; }
Expand Down Expand Up @@ -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);
Expand All @@ -158,58 +181,49 @@ 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);
const view = new DataView(buffer.buffer);
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 () => {
await imodel.close();
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);

Expand All @@ -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);

Expand Down Expand Up @@ -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
Expand All @@ -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);
});
});
4 changes: 3 additions & 1 deletion core/frontend/src/tile/RealityTile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions core/frontend/src/tile/RealityTileTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading