|
| 1 | +import {mat4} from 'gl-matrix'; |
| 2 | +import {calculateModelMatrix} from '../../data/model'; |
| 3 | +import LngLat from '../../../src/geo/lng_lat'; |
| 4 | +import {latFromMercatorY, lngFromMercatorX} from '../../../src/geo/mercator_coordinate'; |
| 5 | +import EXTENT from '../../../src/style-spec/data/extent'; |
| 6 | +import {convertModelMatrixForGlobe, queryGeometryIntersectsProjectedAabb} from '../../util/model_util'; |
| 7 | +import Feature from '../../../src/util/vectortile_to_geojson'; |
| 8 | +import ModelBucket from '../../data/bucket/model_bucket'; |
| 9 | + |
| 10 | +import type ModelSource from '../../source/model_source'; |
| 11 | +import type Tiled3dModelBucket from '../../data/bucket/tiled_3d_model_bucket'; |
| 12 | +import type ModelStyleLayer from './model_style_layer'; |
| 13 | +import type Transform from '../../../src/geo/transform'; |
| 14 | +import type SourceCache from '../../../src/source/source_cache'; |
| 15 | +import type {QueryGeometry, TilespaceQueryGeometry} from '../../../src/style/query_geometry'; |
| 16 | +import type {QueryResult} from '../../../src/source/query_features'; |
| 17 | +import type {Feature as ExpressionEvalFeature, FeatureState} from '../../../src/style-spec/expression/index'; |
| 18 | +import type {EvaluationFeature} from '../../../src/data/evaluation_feature'; |
| 19 | +import type {ModelNode} from '../../data/model'; |
| 20 | +import type {VectorTileFeature} from '@mapbox/vector-tile'; |
| 21 | +import type {CanonicalTileID} from '../../../src/source/tile_id'; |
| 22 | + |
| 23 | +export function tileToLngLat(id: CanonicalTileID, position: LngLat, pointX: number, pointY: number) { |
| 24 | + const tileCount = 1 << id.z; |
| 25 | + position.lat = latFromMercatorY((pointY / EXTENT + id.y) / tileCount); |
| 26 | + position.lng = lngFromMercatorX((pointX / EXTENT + id.x) / tileCount); |
| 27 | +} |
| 28 | + |
| 29 | +export function queryModelLayerRendered( |
| 30 | + layer: ModelStyleLayer, |
| 31 | + queryGeometry: QueryGeometry, |
| 32 | + sourceCache: SourceCache, |
| 33 | + transform: Transform |
| 34 | +): QueryResult { |
| 35 | + const source = sourceCache.getSource<ModelSource>(); |
| 36 | + if (!source || source.type !== 'model') return {}; |
| 37 | + const modelSource = source; |
| 38 | + |
| 39 | + const result: QueryResult = {}; |
| 40 | + result[layer.id] = []; |
| 41 | + const layerResult = result[layer.id]; |
| 42 | + |
| 43 | + let modelFeatureIndex = 0; |
| 44 | + for (const model of modelSource.models) { |
| 45 | + const modelFeatureState = sourceCache.getFeatureState(layer.sourceLayer, model.id); |
| 46 | + |
| 47 | + const modelFeatureForEval: ExpressionEvalFeature = { |
| 48 | + type: 'Unknown', |
| 49 | + id: model.id, |
| 50 | + properties: model.featureProperties |
| 51 | + }; |
| 52 | + const rotation = layer.paint.get('model-rotation').evaluate(modelFeatureForEval, modelFeatureState); |
| 53 | + const scale = layer.paint.get('model-scale').evaluate(modelFeatureForEval, modelFeatureState); |
| 54 | + const translation = layer.paint.get('model-translation').evaluate(modelFeatureForEval, modelFeatureState); |
| 55 | + const elevationReference = layer.paint.get('model-elevation-reference'); |
| 56 | + const shouldFollowTerrainSlope = elevationReference === 'ground'; |
| 57 | + const shouldApplyElevation = elevationReference === 'ground'; |
| 58 | + |
| 59 | + let matrix: mat4 = []; |
| 60 | + calculateModelMatrix(matrix, |
| 61 | + model, |
| 62 | + transform, |
| 63 | + model.position, |
| 64 | + rotation, |
| 65 | + scale, |
| 66 | + translation, |
| 67 | + shouldApplyElevation, |
| 68 | + shouldFollowTerrainSlope, |
| 69 | + false); |
| 70 | + |
| 71 | + if (transform.projection.name === 'globe') { |
| 72 | + matrix = convertModelMatrixForGlobe(matrix, transform); |
| 73 | + } |
| 74 | + const worldViewProjection = mat4.multiply([], transform.projMatrix, matrix); |
| 75 | + |
| 76 | + const projectedQueryGeometry = queryGeometry.isPointQuery() ? queryGeometry.screenBounds : queryGeometry.screenGeometry; |
| 77 | + |
| 78 | + const depth = queryGeometryIntersectsProjectedAabb(projectedQueryGeometry, transform, worldViewProjection, model.aabb); |
| 79 | + if (depth != null) { |
| 80 | + const modelFeature: Feature = new Feature(undefined, 0, 0, 0, model.id); |
| 81 | + modelFeature.layer = layer.layer; |
| 82 | + // Use unsafe assignment for now, due to restriction of GeoJSON/Feature properties to number, string and boolean. |
| 83 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any |
| 84 | + modelFeature.properties = structuredClone(model.featureProperties) as any; |
| 85 | + modelFeature.properties['layer'] = layer.id; |
| 86 | + modelFeature.properties['uri'] = model.uri; |
| 87 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any |
| 88 | + modelFeature.properties['orientation'] = model.orientation as any; |
| 89 | + modelFeature.sourceLayer = layer.sourceLayer; |
| 90 | + modelFeature.geometry = { |
| 91 | + type: 'Point', |
| 92 | + coordinates: [model.position.lng, model.position.lat] |
| 93 | + }; |
| 94 | + modelFeature.state = modelFeatureState; |
| 95 | + modelFeature.source = layer.source; |
| 96 | + layerResult.push({featureIndex: modelFeatureIndex, feature: modelFeature, intersectionZ: depth}); |
| 97 | + } |
| 98 | + |
| 99 | + ++modelFeatureIndex; |
| 100 | + } |
| 101 | + |
| 102 | + return result; |
| 103 | +} |
| 104 | + |
| 105 | +export function queryModelLayerIntersectsFeature( |
| 106 | + layer: ModelStyleLayer, |
| 107 | + queryGeometry: TilespaceQueryGeometry, |
| 108 | + feature: VectorTileFeature, |
| 109 | + featureState: FeatureState, |
| 110 | + transform: Transform, |
| 111 | + scope: string | undefined |
| 112 | +): number | boolean { |
| 113 | + if (!layer.modelManager) return false; |
| 114 | + const modelManager = layer.modelManager; |
| 115 | + const bucket = queryGeometry.tile.getBucket(layer); |
| 116 | + if (!bucket || !(bucket instanceof ModelBucket)) return false; |
| 117 | + |
| 118 | + for (const modelId in bucket.instancesPerModel) { |
| 119 | + const instances = bucket.instancesPerModel[modelId]; |
| 120 | + const featureId = feature.id !== undefined ? feature.id : |
| 121 | + (feature.properties && Object.hasOwn(feature.properties, "id")) ? (feature.properties["id"] as string | number) : undefined; |
| 122 | + if (Object.hasOwn(instances.idToFeaturesIndex, featureId)) { |
| 123 | + const modelFeature = instances.features[instances.idToFeaturesIndex[featureId]]; |
| 124 | + const model = modelManager.getModel(modelId, scope || layer.scope); |
| 125 | + if (!model) return false; |
| 126 | + |
| 127 | + let matrix: mat4 = []; |
| 128 | + const position = new LngLat(0, 0); |
| 129 | + const id = bucket.canonical; |
| 130 | + let minDepth = Number.MAX_VALUE; |
| 131 | + for (let i = 0; i < modelFeature.instancedDataCount; ++i) { |
| 132 | + const instanceOffset = modelFeature.instancedDataOffset + i; |
| 133 | + const offset = instanceOffset * 16; |
| 134 | + |
| 135 | + const va = instances.instancedDataArray.float32; |
| 136 | + const translation: [number, number, number] = [va[offset + 4], va[offset + 5], va[offset + 6]]; |
| 137 | + const pointX = Math.floor(va[offset]); // point.x stored in integer part |
| 138 | + const pointY = Math.floor(va[offset + 1]); // point.y stored in integer part |
| 139 | + |
| 140 | + tileToLngLat(id, position, pointX, pointY); |
| 141 | + |
| 142 | + calculateModelMatrix(matrix, |
| 143 | + model, |
| 144 | + transform, |
| 145 | + position, |
| 146 | + modelFeature.rotation, |
| 147 | + modelFeature.scale, |
| 148 | + translation, |
| 149 | + false, |
| 150 | + false, |
| 151 | + false); |
| 152 | + if (transform.projection.name === 'globe') { |
| 153 | + matrix = convertModelMatrixForGlobe(matrix, transform); |
| 154 | + } |
| 155 | + const worldViewProjection = mat4.multiply([], transform.projMatrix, matrix); |
| 156 | + // Collision checks are performed in screen space. Corners are in ndc space. |
| 157 | + const screenQuery = queryGeometry.queryGeometry; |
| 158 | + const projectedQueryGeometry = screenQuery.isPointQuery() ? screenQuery.screenBounds : screenQuery.screenGeometry; |
| 159 | + const depth = queryGeometryIntersectsProjectedAabb(projectedQueryGeometry, transform, worldViewProjection, model.aabb); |
| 160 | + if (depth != null) { |
| 161 | + minDepth = Math.min(depth, minDepth); |
| 162 | + } |
| 163 | + } |
| 164 | + if (minDepth !== Number.MAX_VALUE) { |
| 165 | + return minDepth; |
| 166 | + } |
| 167 | + return false; |
| 168 | + } |
| 169 | + } |
| 170 | + return false; |
| 171 | +} |
| 172 | + |
| 173 | +export function loadMatchingModelFeature(bucket: Tiled3dModelBucket, featureIndex: number, tilespaceGeometry: TilespaceQueryGeometry, transform: Transform): {feature: EvaluationFeature, intersectionZ: number, position: LngLat} | undefined { |
| 174 | + const nodeInfo = bucket.getNodesInfo()[featureIndex]; |
| 175 | + |
| 176 | + if (!nodeInfo || nodeInfo.hiddenByReplacement || !nodeInfo.node.meshes) return; |
| 177 | + |
| 178 | + let intersectionZ = Number.MAX_VALUE; |
| 179 | + |
| 180 | + // AABB check |
| 181 | + const node = nodeInfo.node; |
| 182 | + const tile = tilespaceGeometry.tile; |
| 183 | + const tileMatrix = transform.calculatePosMatrix(tile.tileID.toUnwrapped(), transform.worldSize); |
| 184 | + const modelMatrix = tileMatrix; |
| 185 | + const scale = nodeInfo.evaluatedScale; |
| 186 | + let elevation = 0; |
| 187 | + if (transform.elevation && node.elevation) { |
| 188 | + elevation = node.elevation * transform.elevation.exaggeration(); |
| 189 | + } |
| 190 | + const anchorX = node.anchor ? node.anchor[0] : 0; |
| 191 | + const anchorY = node.anchor ? node.anchor[1] : 0; |
| 192 | + |
| 193 | + mat4.translate(modelMatrix, modelMatrix, [anchorX * (scale[0] - 1), anchorY * (scale[1] - 1), elevation]); |
| 194 | + mat4.scale(modelMatrix, modelMatrix, scale); |
| 195 | + |
| 196 | + // Collision checks are performed in screen space. Corners are in ndc space. |
| 197 | + const screenQuery = tilespaceGeometry.queryGeometry; |
| 198 | + const projectedQueryGeometry = screenQuery.isPointQuery() ? screenQuery.screenBounds : screenQuery.screenGeometry; |
| 199 | + |
| 200 | + const checkNode = function (n: ModelNode) { |
| 201 | + const worldViewProjectionForNode = mat4.multiply([], modelMatrix, n.globalMatrix); |
| 202 | + mat4.multiply(worldViewProjectionForNode, transform.expandedFarZProjMatrix, worldViewProjectionForNode); |
| 203 | + for (let i = 0; i < n.meshes.length; ++i) { |
| 204 | + const mesh = n.meshes[i]; |
| 205 | + if (i === n.lightMeshIndex) { |
| 206 | + continue; |
| 207 | + } |
| 208 | + const depth = queryGeometryIntersectsProjectedAabb(projectedQueryGeometry, transform, worldViewProjectionForNode, mesh.aabb); |
| 209 | + if (depth != null) { |
| 210 | + intersectionZ = Math.min(depth, intersectionZ); |
| 211 | + } |
| 212 | + } |
| 213 | + if (n.children) { |
| 214 | + for (const child of n.children) { |
| 215 | + checkNode(child); |
| 216 | + } |
| 217 | + } |
| 218 | + }; |
| 219 | + |
| 220 | + checkNode(node); |
| 221 | + if (intersectionZ === Number.MAX_VALUE) return; |
| 222 | + |
| 223 | + const position = new LngLat(0, 0); |
| 224 | + tileToLngLat(tile.tileID.canonical, position, nodeInfo.node.anchor[0], nodeInfo.node.anchor[1]); |
| 225 | + |
| 226 | + return {intersectionZ, position, feature: nodeInfo.feature}; |
| 227 | +} |
0 commit comments