1 | import StyleLayer from '../style_layer';
|
2 |
|
3 | import FillExtrusionBucket from '../../data/bucket/fill_extrusion_bucket';
|
4 | import {polygonIntersectsPolygon, polygonIntersectsMultiPolygon} from '../../util/intersection_tests';
|
5 | import {translateDistance, translate} from '../query_utils';
|
6 | import properties, {FillExtrusionPaintPropsPossiblyEvaluated} from './fill_extrusion_style_layer_properties.g';
|
7 | import {Transitionable, Transitioning, PossiblyEvaluated} from '../properties';
|
8 | import {mat4, vec4} from 'gl-matrix';
|
9 | import Point from '@mapbox/point-geometry';
|
10 | import type {FeatureState} from '../../style-spec/expression';
|
11 | import type {BucketParameters} from '../../data/bucket';
|
12 | import type {FillExtrusionPaintProps} from './fill_extrusion_style_layer_properties.g';
|
13 | import type Transform from '../../geo/transform';
|
14 | import type {LayerSpecification} from '../../style-spec/types.g';
|
15 | import type {VectorTileFeature} from '@mapbox/vector-tile';
|
16 |
|
17 | export class Point3D extends Point {
|
18 | z: number;
|
19 | }
|
20 |
|
21 | class FillExtrusionStyleLayer extends StyleLayer {
|
22 | _transitionablePaint: Transitionable<FillExtrusionPaintProps>;
|
23 | _transitioningPaint: Transitioning<FillExtrusionPaintProps>;
|
24 | paint: PossiblyEvaluated<FillExtrusionPaintProps, FillExtrusionPaintPropsPossiblyEvaluated>;
|
25 |
|
26 | constructor(layer: LayerSpecification) {
|
27 | super(layer, properties);
|
28 | }
|
29 |
|
30 | createBucket(parameters: BucketParameters<FillExtrusionStyleLayer>) {
|
31 | return new FillExtrusionBucket(parameters);
|
32 | }
|
33 |
|
34 | queryRadius(): number {
|
35 | return translateDistance(this.paint.get('fill-extrusion-translate'));
|
36 | }
|
37 |
|
38 | is3D(): boolean {
|
39 | return true;
|
40 | }
|
41 |
|
42 | queryIntersectsFeature(
|
43 | queryGeometry: Array<Point>,
|
44 | feature: VectorTileFeature,
|
45 | featureState: FeatureState,
|
46 | geometry: Array<Array<Point>>,
|
47 | zoom: number,
|
48 | transform: Transform,
|
49 | pixelsToTileUnits: number,
|
50 | pixelPosMatrix: mat4
|
51 | ): boolean | number {
|
52 |
|
53 | const translatedPolygon = translate(queryGeometry,
|
54 | this.paint.get('fill-extrusion-translate'),
|
55 | this.paint.get('fill-extrusion-translate-anchor'),
|
56 | transform.angle, pixelsToTileUnits);
|
57 |
|
58 | const height = this.paint.get('fill-extrusion-height').evaluate(feature, featureState);
|
59 | const base = this.paint.get('fill-extrusion-base').evaluate(feature, featureState);
|
60 |
|
61 | const projectedQueryGeometry = projectQueryGeometry(translatedPolygon, pixelPosMatrix, transform, 0);
|
62 |
|
63 | const projected = projectExtrusion(geometry, base, height, pixelPosMatrix);
|
64 | const projectedBase = projected[0];
|
65 | const projectedTop = projected[1];
|
66 | return checkIntersection(projectedBase, projectedTop, projectedQueryGeometry);
|
67 | }
|
68 | }
|
69 |
|
70 | function dot(a, b) {
|
71 | return a.x * b.x + a.y * b.y;
|
72 | }
|
73 |
|
74 | export function getIntersectionDistance(projectedQueryGeometry: Array<Point3D>, projectedFace: Array<Point3D>) {
|
75 |
|
76 | if (projectedQueryGeometry.length === 1) {
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 | let i = 0;
|
87 | const a = projectedFace[i++];
|
88 | let b;
|
89 | while (!b || a.equals(b)) {
|
90 | b = projectedFace[i++];
|
91 | if (!b) return Infinity;
|
92 | }
|
93 |
|
94 |
|
95 | for (; i < projectedFace.length; i++) {
|
96 | const c = projectedFace[i];
|
97 |
|
98 | const p = projectedQueryGeometry[0];
|
99 |
|
100 | const ab = b.sub(a);
|
101 | const ac = c.sub(a);
|
102 | const ap = p.sub(a);
|
103 |
|
104 | const dotABAB = dot(ab, ab);
|
105 | const dotABAC = dot(ab, ac);
|
106 | const dotACAC = dot(ac, ac);
|
107 | const dotAPAB = dot(ap, ab);
|
108 | const dotAPAC = dot(ap, ac);
|
109 | const denom = dotABAB * dotACAC - dotABAC * dotABAC;
|
110 |
|
111 | const v = (dotACAC * dotAPAB - dotABAC * dotAPAC) / denom;
|
112 | const w = (dotABAB * dotAPAC - dotABAC * dotAPAB) / denom;
|
113 | const u = 1 - v - w;
|
114 |
|
115 |
|
116 | const distance = a.z * u + b.z * v + c.z * w;
|
117 |
|
118 | if (isFinite(distance)) return distance;
|
119 | }
|
120 |
|
121 | return Infinity;
|
122 |
|
123 | } else {
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 | let closestDistance = Infinity;
|
130 | for (const p of projectedFace) {
|
131 | closestDistance = Math.min(closestDistance, p.z);
|
132 | }
|
133 | return closestDistance;
|
134 | }
|
135 | }
|
136 |
|
137 | function checkIntersection(projectedBase: Array<Array<Point3D>>, projectedTop: Array<Array<Point3D>>, projectedQueryGeometry: Array<Point3D>) {
|
138 | let closestDistance = Infinity;
|
139 |
|
140 | if (polygonIntersectsMultiPolygon(projectedQueryGeometry, projectedTop)) {
|
141 | closestDistance = getIntersectionDistance(projectedQueryGeometry, projectedTop[0]);
|
142 | }
|
143 |
|
144 | for (let r = 0; r < projectedTop.length; r++) {
|
145 | const ringTop = projectedTop[r];
|
146 | const ringBase = projectedBase[r];
|
147 | for (let p = 0; p < ringTop.length - 1; p++) {
|
148 | const topA = ringTop[p];
|
149 | const topB = ringTop[p + 1];
|
150 | const baseA = ringBase[p];
|
151 | const baseB = ringBase[p + 1];
|
152 | const face = [topA, topB, baseB, baseA, topA];
|
153 | if (polygonIntersectsPolygon(projectedQueryGeometry, face)) {
|
154 | closestDistance = Math.min(closestDistance, getIntersectionDistance(projectedQueryGeometry, face));
|
155 | }
|
156 | }
|
157 | }
|
158 |
|
159 | return closestDistance === Infinity ? false : closestDistance;
|
160 | }
|
161 |
|
162 |
|
163 |
|
164 |
|
165 |
|
166 |
|
167 |
|
168 |
|
169 | function projectExtrusion(geometry: Array<Array<Point>>, zBase: number, zTop: number, m: mat4): [Array<Array<Point3D>>, Array<Array<Point3D>>] {
|
170 | const projectedBase = [] as Array<Array<Point3D>>;
|
171 | const projectedTop = [] as Array<Array<Point3D>>;
|
172 | const baseXZ = m[8] * zBase;
|
173 | const baseYZ = m[9] * zBase;
|
174 | const baseZZ = m[10] * zBase;
|
175 | const baseWZ = m[11] * zBase;
|
176 | const topXZ = m[8] * zTop;
|
177 | const topYZ = m[9] * zTop;
|
178 | const topZZ = m[10] * zTop;
|
179 | const topWZ = m[11] * zTop;
|
180 |
|
181 | for (const r of geometry) {
|
182 | const ringBase = [] as Array<Point3D>;
|
183 | const ringTop = [] as Array<Point3D>;
|
184 | for (const p of r) {
|
185 | const x = p.x;
|
186 | const y = p.y;
|
187 |
|
188 | const sX = m[0] * x + m[4] * y + m[12];
|
189 | const sY = m[1] * x + m[5] * y + m[13];
|
190 | const sZ = m[2] * x + m[6] * y + m[14];
|
191 | const sW = m[3] * x + m[7] * y + m[15];
|
192 |
|
193 | const baseX = sX + baseXZ;
|
194 | const baseY = sY + baseYZ;
|
195 | const baseZ = sZ + baseZZ;
|
196 | const baseW = sW + baseWZ;
|
197 |
|
198 | const topX = sX + topXZ;
|
199 | const topY = sY + topYZ;
|
200 | const topZ = sZ + topZZ;
|
201 | const topW = sW + topWZ;
|
202 |
|
203 | const b = new Point(baseX / baseW, baseY / baseW) as Point3D;
|
204 | b.z = baseZ / baseW;
|
205 | ringBase.push(b);
|
206 |
|
207 | const t = new Point(topX / topW, topY / topW) as Point3D;
|
208 | t.z = topZ / topW;
|
209 | ringTop.push(t);
|
210 | }
|
211 | projectedBase.push(ringBase);
|
212 | projectedTop.push(ringTop);
|
213 | }
|
214 | return [projectedBase, projectedTop];
|
215 | }
|
216 |
|
217 | function projectQueryGeometry(queryGeometry: Array<Point>, pixelPosMatrix: mat4, transform: Transform, z: number) {
|
218 | const projectedQueryGeometry = [];
|
219 | for (const p of queryGeometry) {
|
220 | const v = [p.x, p.y, z, 1] as vec4;
|
221 | vec4.transformMat4(v, v, pixelPosMatrix);
|
222 | projectedQueryGeometry.push(new Point(v[0] / v[3], v[1] / v[3]));
|
223 | }
|
224 | return projectedQueryGeometry;
|
225 | }
|
226 |
|
227 | export default FillExtrusionStyleLayer;
|