import { Expr , getFeatureId , getPropertyValue , IndexedTechnique , MapEnv , Pickability } from "@here/harp-datasource-protocol" ; import { BackgroundDataSource } from "./BackgroundDataSource" ; import { SolidLineMesh } from "./geometry/SolidLineMesh" ; import { MapObjectAdapter } from "./MapObjectAdapter" ; import { Tile , TileFeatureData , TileObject } from "./Tile" ; const DEFAULT_STENCIL_VALUE = 1 ; interface RenderItem { groupOrder : number ; renderOrder : number ; program : { id : number ; } ; material : { id : number ; } ; z : number ; id : number ; object : THREE . Object3D ; } export class TileObjectRenderer { private readonly m_renderOrderStencilValues = new Map < number , number > ( ) ; private m_stencilValue : number = DEFAULT_STENCIL_VALUE ; constructor ( private readonly m_env , private readonly m_renderer ) { } render ( tile , storageLevel , zoomLevel , cameraPosition , rootNode ) { const worldOffsetX = tile . computeWorldOffsetX ( ) ; if ( tile . willRender ( storageLevel ) ) { for ( const object of tile . objects ) { const mapObjectAdapter = MapObjectAdapter . get ( object ) ; if ( ! this . processTileObject ( tile , storageLevel , zoomLevel , object , mapObjectAdapter ) ) { continue ; } this . updateStencilRef ( object ) ; object . position . copy ( tile . center ) ; if ( object . displacement !== undefined ) { object . position . add ( object . displacement ) ; } object . position . x += worldOffsetX ; object . position . sub ( cameraPosition ) ; if ( tile . localTangentSpace ) { object . setRotationFromMatrix ( tile . boundingBox . getRotationMatrix ( ) ) ; } object . frustumCulled = false ; rootNode . add ( object ) ; } tile . didRender ( ) ; } } prepareRender ( ) { this . m_stencilValue = DEFAULT_STENCIL_VALUE ; this . m_renderOrderStencilValues . clear ( ) ; } /**
     * Prepares the sorting of tile objects.
     */ setupRenderer ( ) { /**
         * Custom sorting function to avoid non-deterministic IBCT testcases. It's basically a copy
         * of the three.js internal sorting, see:
         * https://github.com/mrdoob/three.js/blob/r118/src/renderers/webgl/WebGLRenderLists.js#L5
         * but additionally checking tile-id before checking object.id, material.id and program.id
         * b/c these ids are generated by incrementing a counter. This means if for two test
         * executions the tiles are processed in a different order the ids hence draw call order
         * will also be different.
         */ const stableSort = ( a , b ) : number => { if ( a . groupOrder !== b . groupOrder ) { return a . groupOrder - b . groupOrder ; } else if ( a . renderOrder !== b . renderOrder ) { return a . renderOrder - b . renderOrder ; } else if ( a . object . userData . tileKey && b . object . userData . tileKey && a . object . userData . tileKey . mortonCode ( ) !== b . object . userData . tileKey . mortonCode ( ) ) { return ( a . object . userData . tileKey . mortonCode ( ) - b . object . userData . tileKey . mortonCode ( ) ) ; } else if ( a . program !== b . program ) { return a . program . id - b . program . id ; } else if ( a . material . id !== b . material . id ) { return a . material . id - b . material . id ; } else if ( a . z !== b . z ) { return a . z - b . z ; } else { return a . id - b . id ; } } ; const painterSortStable = ( a , b ) : number => { const mapObjectAdapterA = MapObjectAdapter . get ( a . object ) ; const mapObjectAdapterB = MapObjectAdapter . get ( b . object ) ; const dataSourceOrder = mapObjectAdapterA ?. dataSource ?. dataSourceOrder ; const otherDataSourceOrder = mapObjectAdapterB ?. dataSource ?. dataSourceOrder ; if ( dataSourceOrder !== undefined && otherDataSourceOrder !== undefined && dataSourceOrder !== otherDataSourceOrder ) { return dataSourceOrder - otherDataSourceOrder ; } if ( a . renderOrder === BackgroundDataSource . GROUND_RENDER_ORDER || b . renderOrder === BackgroundDataSource . GROUND_RENDER_ORDER ) { return stableSort ( a , b ) ; } if ( mapObjectAdapterA ?. level !== undefined && mapObjectAdapterB ?. level !== undefined ) { const eitherIsBuilding = mapObjectAdapterA . kind ?. find ( s => s === "building" ) !== undefined || mapObjectAdapterB . kind ?. find ( s => s === "building" ) !== undefined ; const sameLevel = mapObjectAdapterA . level === mapObjectAdapterB . level ; if ( sameLevel || eitherIsBuilding ) { return stableSort ( a , b ) ; } return mapObjectAdapterA . level - mapObjectAdapterB . level ; } return stableSort ( a , b ) ; } ; this . m_renderer . setOpaqueSort ( painterSortStable ) ; } private updateStencilRef ( object ) { if ( object . renderOrder !== undefined && object instanceof SolidLineMesh ) { const material = object . material ; if ( Array . isArray ( material ) ) { material . forEach ( mat => ( mat . stencilRef = this . getStencilValue ( object . renderOrder ) ) ) ; } else { material . stencilRef = this . getStencilValue ( object . renderOrder ) ; } } } private allocateStencilValue ( renderOrder ) { const stencilValue = this . m_stencilValue ++ ; this . m_renderOrderStencilValues . set ( renderOrder , stencilValue ) ; return stencilValue ; } private getStencilValue ( renderOrder ) { return ( this . m_renderOrderStencilValues . get ( renderOrder ) ?? this . allocateStencilValue ( renderOrder ) ) ; } returns private processTileObject ( tile , storageLevel , zoomLevel , object , mapObjectAdapter ? ) { if ( ! object . visible ) { return false ; } if ( ! this . processTileObjectFeatures ( tile , storageLevel , zoomLevel , object ) ) { return false ; } if ( mapObjectAdapter ) { mapObjectAdapter . ensureUpdated ( tile . mapView ) ; if ( ! mapObjectAdapter . isVisible ( ) && ! ( mapObjectAdapter . pickability === Pickability . all ) ) { return false ; } } return true ; } param tile - The  Tile owning the `TileObject`'s features.
     *  param storageLevel param zoomLevel param object returns private processTileObjectFeatures ( tile , storageLevel , zoomLevel , object ) { const technique = object . userData . technique ; const minZoomLevel = getPropertyValue ( technique ?. minZoomLevel , this . m_env ) ; const maxZoomLevel = getPropertyValue ( technique ?. maxZoomLevel , this . m_env ) ; if ( typeof minZoomLevel === "number" && zoomLevel < minZoomLevel ) { return false ; } if ( typeof maxZoomLevel === "number" && zoomLevel >= maxZoomLevel ) { return false ; } if ( technique ?. enabled === undefined ) { return true ; } const feature = object . userData . feature ; if ( ! feature || ! Expr . isExpr ( technique . enabled ) ) { return Boolean ( getPropertyValue ( technique . enabled , this . m_env ) ) ; } const { starts , objInfos } = feature ; if ( ! Array . isArray ( objInfos ) || ! Array . isArray ( starts ) ) { return true ; } const geometry = ( object as any ) . geometry ; if ( ! geometry || ! geometry . isBufferGeometry ) { return true ; } const finalIndex = geometry . getIndex ( ) ?. count ?? geometry . attributes . position . count ; geometry . clearGroups ( ) ; let endOfLastGroup ; objInfos . forEach ( ( properties , featureIndex ) => { const featureId = getFeatureId ( properties ) ; let enabled = true ; if ( Expr . isExpr ( technique . enabled ) ) { const featureState = tile . dataSource . getFeatureState ( featureId ) ; const $state = featureState ? new MapEnv ( featureState ) : null ; const parentEnv = typeof properties === "object" ? new MapEnv ( properties , this . m_env ) : this . m_env ; const env = new MapEnv ( { $state } , parentEnv ) ; enabled = Boolean ( getPropertyValue ( technique . enabled , env ) ) ; } if ( ! enabled ) { return ; } const start = starts [ featureIndex ] ?? 0 ; const end = starts [ featureIndex + 1 ] ?? finalIndex ; const count = end - start ; if ( start === endOfLastGroup ) { geometry . groups [ geometry . groups . length - 1 ] . count += count ; } else { geometry . addGroup ( start , count ) ; } endOfLastGroup = start + count ; } ) ; return geometry . groups . length > 0 ; } }