import { expect } from "chai" ; import { Id64String , SortedArray } from "@itwin/core-bentley" ; import { ColorDef , Feature , GeometryClass } from "@itwin/core-common" ; import { IModelApp , IModelConnection , OffScreenViewport , Pixel , ScreenViewport , Tile , TileTreeLoadStatus , Viewport , ViewRect , } from "@itwin/core-frontend" ; function compareFeatures ( lhs ? , rhs ? ) { if ( undefined === lhs && undefined === rhs ) return 0 ; else if ( undefined === lhs ) return - 1 ; else if ( undefined === rhs ) return 1 ; else return lhs . compare ( rhs ) ; } export function comparePixelData ( lhs , rhs ) { let diff = lhs . distanceFraction - rhs . distanceFraction ; if ( 0 === diff ) { diff = lhs . type - rhs . type ; if ( 0 === diff ) { diff = lhs . planarity - rhs . planarity ; if ( 0 === diff ) { diff = compareFeatures ( lhs . feature , rhs . feature ) ; } } } return diff ; } export class PixelDataSet extends SortedArray < Pixel . Data > { public constructor ( ) { super ( ( lhs , rhs ) => comparePixelData ( lhs , rhs ) ) ; } public get array ( ) : Pixel . Data [ ] { return this . _array ; } public containsFeature ( elemId ? , subcatId ? , geomClass ? ) { return this . containsWhere ( ( pxl ) => ( undefined === elemId || pxl . elementId === elemId ) && ( undefined === subcatId || pxl . subCategoryId === subcatId ) && ( undefined === geomClass || pxl . geometryClass === geomClass ) ) ; } public containsElement ( id ) { return this . containsWhere ( ( pxl ) => pxl . elementId === id ) ; } public containsPlanarity ( planarity ) { return this . containsWhere ( ( pxl ) => pxl . planarity === planarity ) ; } public containsGeometryType ( type ) { return this . containsWhere ( ( pxl ) => pxl . type === type ) ; } public containsGeometry ( type , planarity ) { return this . containsWhere ( ( pxl ) => pxl . type === type && pxl . planarity === planarity ) ; } public containsWhere ( criterion ) { for ( const pixel of this . array ) if ( criterion ( pixel ) ) return true ; return false ; } } export class Color { public readonly v : number ; public readonly r : number ; public readonly g : number ; public readonly b : number ; public readonly a : number ; public constructor ( val ) { this . v = val ; this . r = ( ( val & 0x000000ff ) >>> 0x00 ) >>> 0 ; this . g = ( ( val & 0x0000ff00 ) >>> 0x08 ) >>> 0 ; this . b = ( ( val & 0x00ff0000 ) >>> 0x10 ) >>> 0 ; this . a = ( ( val & 0xff000000 ) >>> 0x18 ) >>> 0 ; } public static from ( val ) { return new Color ( val ) ; } public static fromRgba ( r , g , b , a ) { const v = ( r | ( g << 0x08 ) | ( b << 0x10 ) | ( a << 0x18 ) ) >>> 0 ; return Color . from ( v ) ; } public static fromColorDef ( def ) { const colors = def . colors ; return Color . fromRgba ( colors . r , colors . g , colors . b , 0xff - colors . t ) ; } public compare ( rhs ) { return this . v - rhs . v ; } public equalsColorDef ( def ) { const colors = def . colors ; return colors . r === this . r && colors . g === this . g && colors . b === this . b && colors . t === 0xff - this . a ; } } export class ColorSet extends SortedArray < Color > { public constructor ( ) { super ( ( lhs , rhs ) => lhs . compare ( rhs ) ) ; } public get array ( ) : Color [ ] { return this . _array ; } } function readUniquePixelData ( vp , readRect ? , excludeNonLocatable = false ) { const rect = undefined !== readRect ? readRect : vp . viewRect ; const set = new PixelDataSet ( ) ; vp . readPixels ( rect , Pixel . Selector . All , ( pixels ) => { if ( undefined === pixels ) return ; const sRect = rect . clone ( ) ; sRect . left = vp . cssPixelsToDevicePixels ( sRect . left ) ; sRect . right = vp . cssPixelsToDevicePixels ( sRect . right ) ; sRect . bottom = vp . cssPixelsToDevicePixels ( sRect . bottom ) ; sRect . top = vp . cssPixelsToDevicePixels ( sRect . top ) ; for ( let x = sRect . left ; x < sRect . right ; x ++ ) for ( let y = sRect . top ; y < sRect . bottom ; y ++ ) set . insert ( pixels . getPixel ( x , y ) ) ; } , excludeNonLocatable ) ; return set ; } function readPixel ( vp , x , y , excludeNonLocatable ? ) { const pixels = readUniquePixelData ( vp , new ViewRect ( x , y , x + 1 , y + 1 ) , excludeNonLocatable ) ; expect ( pixels . length ) . to . equal ( 1 ) ; return pixels . array [ 0 ] ; } function readUniqueColors ( vp , readRect ? ) { const rect = undefined !== readRect ? readRect : vp . viewRect ; const buffer = vp . readImageBuffer ( { rect } ) ! ; expect ( buffer ) . not . to . be . undefined ; const u32 = new Uint32Array ( buffer . data . buffer ) ; const colors = new ColorSet ( ) ; for ( const rgba of u32 ) colors . insert ( Color . from ( rgba ) ) ; return colors ; } function readColor ( vp , x , y ) { const colors = readUniqueColors ( vp , new ViewRect ( x , y , x + 1 , y + 1 ) ) ; expect ( colors . length ) . to . equal ( 1 ) ; return colors . array [ 0 ] ; } function areAllChildTilesLoaded ( parent ? ) { if ( ! parent ) return true ; else if ( TileTreeLoadStatus . Loading === ( parent as any ) . _childrenLoadStatus ) return false ; const kids = parent . children ; if ( ! kids ) return true ; for ( const kid of kids ) if ( ! areAllChildTilesLoaded ( kid ) ) return false ; return true ; } function areAllTilesLoaded ( vp ) { if ( vp . numRequestedTiles > 0 || ! vp . areAllTileTreesLoaded ) return false ; let allLoaded = true ; vp . forEachTileTreeRef ( ( ref ) => { allLoaded = allLoaded && ref . isLoadingComplete && areAllChildTilesLoaded ( ref . treeOwner . tileTree ?. rootTile ) ; } ) ; return allLoaded ; } export interface TestableViewport { waitForAllTilesToRender ( ) : Promise < void > ; drawFrame ( ) : Promise < void > ; readUniquePixelData ( readRect ? , excludeNonLocatable ? ) : PixelDataSet ; readUniqueColors ( readRect ? ) : ColorSet ; readColor ( x , y ) : Color ; readPixel ( x , y , excludeNonLocatable ? ) : Pixel . Data ; areAllTilesLoaded : boolean ; } class OffScreenTestViewport extends OffScreenViewport implements TestableViewport { public readUniquePixelData ( readRect ? , excludeNonLocatable = false ) { return readUniquePixelData ( this , readRect , excludeNonLocatable ) ; } public readUniqueColors ( readRect ? ) { return readUniqueColors ( this , readRect ) ; } public readColor ( x , y ) { return readColor ( this , x , y ) ; } public readPixel ( x , y , excludeNonLocatable ? ) { return readPixel ( this , x , y , excludeNonLocatable ) ; } public get areAllTilesLoaded ( ) : boolean { return areAllTilesLoaded ( this ) ; } public async waitForAllTilesToRender ( ) { this . renderFrame ( ) ; IModelApp . tileAdmin . process ( ) ; if ( this . areAllTilesLoaded ) return ; await new Promise < void > ( ( resolve ) => setTimeout ( resolve , 100 ) ) ; this . invalidateScene ( ) ; return this . waitForAllTilesToRender ( ) ; } public static async createTestViewport ( viewId , imodel , width , height ) { const view = await imodel . views . load ( viewId ) ; const rect = new ViewRect ( 0 , 0 , width , height ) ; const vp = this . create ( { view , viewRect : rect } ) as OffScreenTestViewport ; expect ( vp ) . instanceof ( OffScreenTestViewport ) ; return vp ; } public async drawFrame ( ) { this . renderFrame ( ) ; IModelApp . tileAdmin . process ( ) ; } } export class ScreenTestViewport extends ScreenViewport implements TestableViewport { private _frameRendered : boolean = false ; public readUniquePixelData ( readRect ? , excludeNonLocatable = false ) { return readUniquePixelData ( this , readRect , excludeNonLocatable ) ; } public readUniqueColors ( readRect ? ) { return readUniqueColors ( this , readRect ) ; } public readColor ( x , y ) { return readColor ( this , x , y ) ; } public readPixel ( x , y , excludeNonLocatable ? ) { return readPixel ( this , x , y , excludeNonLocatable ) ; } public get areAllTilesLoaded ( ) : boolean { return areAllTilesLoaded ( this ) ; } private async waitForRenderFrame ( ) { if ( this . _frameRendered ) { this . _frameRendered = false ; return ; } this . onRender . addOnce ( ( _ ) => { this . _frameRendered = true ; } ) ; await new Promise < void > ( ( resolve ) => requestAnimationFrame ( resolve ) ) ; return this . waitForRenderFrame ( ) ; } public async waitForAllTilesToRender ( ) { await this . drawFrame ( ) ; if ( this . areAllTilesLoaded ) return ; await this . waitForRenderFrame ( ) ; return this . waitForAllTilesToRender ( ) ; } public async drawFrame ( ) { return this . waitForRenderFrame ( ) ; } public override dispose ( ) { if ( ! this . isDisposed ) { IModelApp . viewManager . dropViewport ( this , false ) ; super . dispose ( ) ; document . body . removeChild ( this . parentDiv ) ; } } public static async createTestViewport ( viewId , imodel , width , height ) { const div = document . createElement ( "div" ) ; div . style . width = `${ width }px` ; div . style . height = `${ height }px` ; div . style . position = "absolute" ; div . style . top = div . style . left = "0px" ; document . body . appendChild ( div ) ; const view = await imodel . views . load ( viewId ) ; view . viewFlags = view . viewFlags . copy ( { acsTriad : false , grid : false } ) ; const vp = this . create ( div , view ) as ScreenTestViewport ; expect ( vp ) . instanceof ( ScreenTestViewport ) ; IModelApp . viewManager . addViewport ( vp ) ; return vp ; } } export type TestViewport = Viewport & TestableViewport ; export async function createOffScreenTestViewport ( viewId , imodel , width , height ) { return OffScreenTestViewport . createTestViewport ( viewId , imodel , width , height ) ; } export async function createOnScreenTestViewport ( viewId , imodel , width , height , devicePixelRatio ? ) { const vp = await ScreenTestViewport . createTestViewport ( viewId , imodel , width , height ) ; if ( undefined !== devicePixelRatio ) { const debugControl = vp . target . debugControl ; if ( undefined !== debugControl ) debugControl . devicePixelRatioOverride = devicePixelRatio ; } return vp ; } export async function testOnScreenViewport ( viewId , imodel , width , height , test , devicePixelRatio ? ) { if ( ! IModelApp . initialized ) return ; const onscreen = await createOnScreenTestViewport ( viewId , imodel , width , height , devicePixelRatio ) ; onscreen . continuousRendering = true ; try { await test ( onscreen ) ; } finally { onscreen . continuousRendering = false ; onscreen . dispose ( ) ; } } export async function testOffScreenViewport ( viewId , imodel , width , height , test ) { if ( ! IModelApp . initialized ) return ; const offscreen = await createOffScreenTestViewport ( viewId , imodel , width , height ) ; try { await test ( offscreen ) ; } finally { offscreen . dispose ( ) ; } } export async function testViewports ( viewId , imodel , width , height , test , devicePixelRatio ? ) { if ( ! IModelApp . initialized ) return ; await testOnScreenViewport ( viewId , imodel , width , height , test , devicePixelRatio ) ; await testOffScreenViewport ( viewId , imodel , width , height , test ) ; } /** Execute a test against both an off-screen and on-screen viewport at varying device pixel ratios. */ export async function testViewportsWithDpr ( imodel , rect , test ) { const devicePixelRatios = [ 1.0 , 1.25 , 1.5 , 2.0 ] ; for ( const dpr of devicePixelRatios ) await testViewports ( "0x24" , imodel , rect . width , rect . height , test , dpr ) ; }