bruce-cesium 7.1.2 → 7.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39639,6 +39639,8 @@
39639
39639
  this.pendingIndex = -1;
39640
39640
  this.displayedPixels = null;
39641
39641
  this.fadeFromPixels = null;
39642
+ this.displayedValuePixels = null;
39643
+ this.fadeFromValuePixels = null;
39642
39644
  this.fadeToIndex = -1;
39643
39645
  this.fadeToDims = null;
39644
39646
  this.fadeStartMs = null;
@@ -39647,6 +39649,8 @@
39647
39649
  // forcing Cesium to re-upload the texture only when something changed.
39648
39650
  this.pool = [document.createElement("canvas"), document.createElement("canvas")];
39649
39651
  this.poolIdx = 0;
39652
+ this.valueDims = null;
39653
+ this.presentVersion = 0;
39650
39654
  if (!options.entity.polygon) {
39651
39655
  throw new Error("TextureFrameSeriesAnimator requires an entity with polygon graphics.");
39652
39656
  }
@@ -39664,9 +39668,15 @@
39664
39668
  this.frameDates = this.frames.map((f) => Cesium.JulianDate.fromIso8601(f.Timestamp));
39665
39669
  this.frameCache = new Array(this.frames.length).fill(null);
39666
39670
  this.frameDims = new Array(this.frames.length).fill(null);
39671
+ this.valueCache = new Array(this.frames.length).fill(null);
39672
+ this.produceValueCanvas = Boolean(options.produceValueCanvas);
39673
+ this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
39674
+ this.driveMaterial = options.driveMaterial !== false;
39667
39675
  this.originalMaterial = options.entity.polygon.material;
39668
39676
  this.imageProperty = new Cesium.CallbackProperty(() => this.pool[this.poolIdx], false);
39669
- options.entity.polygon.material = new Cesium.ImageMaterialProperty({ image: this.imageProperty, transparent: true });
39677
+ if (this.driveMaterial) {
39678
+ options.entity.polygon.material = new Cesium.ImageMaterialProperty({ image: this.imageProperty, transparent: true });
39679
+ }
39670
39680
  this.removeCrossfadeTick = this.viewer.scene.postUpdate.addEventListener(() => this.tickCrossfade());
39671
39681
  this.removeOnTick = this.viewer.clock.onTick.addEventListener((clock) => this.onClockTick(clock.currentTime));
39672
39682
  this.onClockTick(this.viewer.clock.currentTime);
@@ -39688,7 +39698,7 @@
39688
39698
  this.removeCrossfadeTick();
39689
39699
  this.removeCrossfadeTick = null;
39690
39700
  }
39691
- if (this.entity && this.entity.polygon) {
39701
+ if (this.driveMaterial && this.entity && this.entity.polygon) {
39692
39702
  this.entity.polygon.material = this.originalMaterial;
39693
39703
  }
39694
39704
  }
@@ -39709,6 +39719,20 @@
39709
39719
  GetImageProperty() {
39710
39720
  return this.imageProperty;
39711
39721
  }
39722
+ /**
39723
+ * The untinted canvas carrying the presented frame's value in RGB and its coverage in alpha,
39724
+ * or null unless the instance was constructed with produceValueCanvas.
39725
+ */
39726
+ GetValueCanvas() {
39727
+ return this.valueDims ? this.valueCanvas : null;
39728
+ }
39729
+ /**
39730
+ * Increments every time a frame is presented, so a consumer holding the value canvas can tell
39731
+ * whether it needs re-uploading without diffing pixels or subscribing to anything.
39732
+ */
39733
+ GetPresentVersion() {
39734
+ return this.presentVersion;
39735
+ }
39712
39736
  onClockTick(currentTime) {
39713
39737
  if (this.disposed) {
39714
39738
  return;
@@ -39821,6 +39845,7 @@
39821
39845
  }
39822
39846
  this.frameCache[idx] = decoded.pixels;
39823
39847
  this.frameDims[idx] = { width: decoded.width, height: decoded.height };
39848
+ this.valueCache[idx] = decoded.valuePixels || null;
39824
39849
  if (idx === this.currentFrameIndex) {
39825
39850
  this.beginCrossfadeTo(idx);
39826
39851
  }
@@ -39844,11 +39869,17 @@
39844
39869
  const ctx = canvas.getContext("2d");
39845
39870
  ctx.drawImage(image, 0, 0);
39846
39871
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
39872
+ // Copied before tinting: the ramp is a lerp between two colours, so the value cannot be
39873
+ // recovered from the tinted pixels afterwards.
39874
+ const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
39847
39875
  ApplyGrayscaleColorMask(imageData, this.lowColor, this.highColor);
39848
- return { pixels: imageData.data, width: canvas.width, height: canvas.height };
39876
+ return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
39849
39877
  }
39850
39878
  beginCrossfadeTo(idx) {
39851
39879
  const to = this.frameCache[idx];
39880
+ this.fadeFromValuePixels = this.displayedValuePixels === null && this.valueCache[idx]
39881
+ ? new Uint8ClampedArray(this.valueCache[idx].length)
39882
+ : this.displayedValuePixels;
39852
39883
  this.fadeFromPixels = this.displayedPixels === null ? new Uint8ClampedArray(to.length) : this.displayedPixels;
39853
39884
  this.fadeToIndex = idx;
39854
39885
  this.fadeToDims = this.frameDims[idx];
@@ -39867,13 +39898,28 @@
39867
39898
  for (let i = 0; i < n; i++) {
39868
39899
  blended[i] = from[i] + (to[i] - from[i]) * eased;
39869
39900
  }
39870
- this.presentPixels(blended, this.fadeToDims);
39901
+ let blendedValue = null;
39902
+ const toValue = this.valueCache[this.fadeToIndex];
39903
+ if (toValue) {
39904
+ const fromValue = this.fadeFromValuePixels;
39905
+ blendedValue = new Uint8ClampedArray(toValue.length);
39906
+ if (fromValue && fromValue.length === toValue.length) {
39907
+ for (let i = 0; i < toValue.length; i++) {
39908
+ blendedValue[i] = fromValue[i] + (toValue[i] - fromValue[i]) * eased;
39909
+ }
39910
+ }
39911
+ else {
39912
+ blendedValue.set(toValue);
39913
+ }
39914
+ }
39915
+ this.presentPixels(blended, this.fadeToDims, blendedValue);
39871
39916
  if (t >= 1) {
39872
39917
  this.fadeStartMs = null;
39873
39918
  this.fadeFromPixels = null;
39919
+ this.fadeFromValuePixels = null;
39874
39920
  }
39875
39921
  }
39876
- presentPixels(pixels, dims) {
39922
+ presentPixels(pixels, dims, valuePixels) {
39877
39923
  this.poolIdx = 1 - this.poolIdx;
39878
39924
  const target = this.pool[this.poolIdx];
39879
39925
  target.width = dims.width;
@@ -39883,6 +39929,17 @@
39883
39929
  imgData.data.set(pixels);
39884
39930
  ctx.putImageData(imgData, 0, 0);
39885
39931
  this.displayedPixels = pixels;
39932
+ if (this.valueCanvas && valuePixels) {
39933
+ this.valueCanvas.width = dims.width;
39934
+ this.valueCanvas.height = dims.height;
39935
+ const valueCtx = this.valueCanvas.getContext("2d");
39936
+ const valueImgData = valueCtx.createImageData(dims.width, dims.height);
39937
+ valueImgData.data.set(valuePixels);
39938
+ valueCtx.putImageData(valueImgData, 0, 0);
39939
+ this.displayedValuePixels = valuePixels;
39940
+ this.valueDims = dims;
39941
+ }
39942
+ this.presentVersion++;
39886
39943
  }
39887
39944
  }
39888
39945
  TextureFrameSeriesAnimator.Animator = Animator;
@@ -39894,10 +39951,726 @@
39894
39951
  }
39895
39952
  })(TextureFrameSeriesAnimator || (TextureFrameSeriesAnimator = {}));
39896
39953
 
39954
+ (function (DisplacedSurfacePrimitive) {
39955
+ // Grid densities a tile can draw at, coarsest first.
39956
+ const TIERS = [32, 64, 128, 192];
39957
+ // Tiles per side the extent is cut into.
39958
+ const TILES_PER_SIDE = 6;
39959
+ // Screen pixels one grid cell should cover. Lower means finer meshes sooner.
39960
+ const TARGET_CELL_PIXELS = 12;
39961
+ // Fraction of its distance to the surface the camera must move before densities are reconsidered.
39962
+ const MOVEMENT_THRESHOLD = 0.12;
39963
+ const RESELECT_INTERVAL_MS = 400;
39964
+ // Skirt depth as a fraction of the displaced range. The sheet is translucent, so an oversized
39965
+ // skirt blends through it and paints a seam down every tile boundary.
39966
+ const SKIRT_FRACTION = 0.015;
39967
+ // Alpha below which a texel counts as no-data and is not drawn at all.
39968
+ const COVERAGE_CUTOFF = 0.35;
39969
+ // With no exaggeration stated, the full value range is drawn as this fraction of the extent's shorter side.
39970
+ const AUTO_RELIEF_FRACTION = 0.02;
39971
+ // Terrain is sampled on this grid and interpolated between samples.
39972
+ // Per-texel sampling would be hundreds of thousands of lookups for a surface whose own data is far coarser than that.
39973
+ const GROUND_SAMPLES_PER_SIDE = 64;
39974
+ // Terrain detail level the samples are taken at.
39975
+ // Most-detailed would fetch every tile under a parcel that can be a hundred kilometres across.
39976
+ const GROUND_SAMPLE_LEVEL = 11;
39977
+ // Height window the packed ground texture covers, wide enough for any terrain on Earth.
39978
+ const GROUND_MIN_METRES = -1000;
39979
+ const GROUND_MAX_METRES = 9000;
39980
+ const DEFAULT_LOW_COLOR = { red: 255, green: 255, blue: 255, alpha: 0.12 };
39981
+ const DEFAULT_HIGH_COLOR = { red: 21, green: 96, blue: 196, alpha: 0.92 };
39982
+ const VERTEX_SHADER_GLSL300 = `
39983
+ in vec3 position3DHigh;
39984
+ in vec3 position3DLow;
39985
+ in vec3 normal;
39986
+ in vec2 st;
39987
+ in float skirt;
39988
+
39989
+ uniform sampler2D u_valueTexture;
39990
+ uniform sampler2D u_groundTexture;
39991
+ uniform float u_valueMin;
39992
+ uniform float u_valueRange;
39993
+ uniform float u_exaggeration;
39994
+ uniform float u_baseHeight;
39995
+ uniform float u_skirtDepth;
39996
+ uniform float u_groundWeight;
39997
+ uniform vec2 u_groundRange;
39998
+ uniform vec2 u_groundSize;
39999
+
40000
+ out vec2 v_st;
40001
+ out float v_value;
40002
+ out float v_coverage;
40003
+
40004
+ float decodeGround(vec2 uv) {
40005
+ // Channels arrive normalised to 0..1, so they go back to bytes before being reassembled.
40006
+ vec3 packed = texture(u_groundTexture, uv).rgb * 255.0;
40007
+ float normalised = dot(packed, vec3(65536.0, 256.0, 1.0)) / 16777215.0;
40008
+ return u_groundRange.x + normalised * (u_groundRange.y - u_groundRange.x);
40009
+ }
40010
+
40011
+ /*
40012
+ * Bilinear by hand, because the height is packed across three channels and hardware filtering would
40013
+ * blend the packing rather than the heights, turning a smooth slope into noise at every texel edge.
40014
+ */
40015
+ float sampleGround(vec2 uv) {
40016
+ vec2 texel = 1.0 / u_groundSize;
40017
+ vec2 coord = uv * u_groundSize - 0.5;
40018
+ vec2 base = floor(coord);
40019
+ vec2 f = coord - base;
40020
+ vec2 origin = (base + 0.5) * texel;
40021
+ float h00 = decodeGround(origin);
40022
+ float h10 = decodeGround(origin + vec2(texel.x, 0.0));
40023
+ float h01 = decodeGround(origin + vec2(0.0, texel.y));
40024
+ float h11 = decodeGround(origin + texel);
40025
+ return mix(mix(h00, h10, f.x), mix(h01, h11, f.x), f.y);
40026
+ }
40027
+
40028
+ void main() {
40029
+ vec4 texel = texture(u_valueTexture, st);
40030
+ v_st = st;
40031
+ v_value = texel.r;
40032
+ v_coverage = texel.a;
40033
+
40034
+ float metres = u_valueMin + texel.r * u_valueRange;
40035
+
40036
+ // Value zero sits at the polygon's own altitude, plus the terrain underneath when the polygon
40037
+ // is following the ground rather than absolutely positioned.
40038
+ float ground = sampleGround(st) * u_groundWeight;
40039
+ float displacement = u_baseHeight + ground + metres * u_exaggeration * texel.a - skirt * u_skirtDepth;
40040
+
40041
+ // Added to the LOW half of the encoded position: a metre-scale offset added to the high half
40042
+ // would be lost to float32 rounding at an earth radius.
40043
+ vec4 p = czm_translateRelativeToEye(position3DHigh, position3DLow + normal * displacement);
40044
+ gl_Position = czm_modelViewProjectionRelativeToEye * p;
40045
+ }
40046
+ `;
40047
+ const FRAGMENT_SHADER_GLSL300 = `
40048
+ uniform sampler2D u_valueTexture;
40049
+ uniform vec4 u_lowColor;
40050
+ uniform vec4 u_highColor;
40051
+ uniform float u_coverageCutoff;
40052
+ uniform vec2 u_texelSize;
40053
+ uniform float u_valueRange;
40054
+ uniform float u_exaggeration;
40055
+ uniform float u_metresPerTexel;
40056
+
40057
+ in vec2 v_st;
40058
+ in float v_value;
40059
+ in float v_coverage;
40060
+
40061
+ void main() {
40062
+ if (v_coverage < u_coverageCutoff) {
40063
+ discard;
40064
+ }
40065
+ vec4 color = mix(u_lowColor, u_highColor, v_value);
40066
+
40067
+ // Relief shading from the value gradient, so the displacement reads at a distance without
40068
+ // recomputing vertex normals every time the texture changes.
40069
+ float left = texture(u_valueTexture, v_st - vec2(u_texelSize.x, 0.0)).r;
40070
+ float right = texture(u_valueTexture, v_st + vec2(u_texelSize.x, 0.0)).r;
40071
+ float down = texture(u_valueTexture, v_st - vec2(0.0, u_texelSize.y)).r;
40072
+ float up = texture(u_valueTexture, v_st + vec2(0.0, u_texelSize.y)).r;
40073
+ float scale = u_valueRange * u_exaggeration;
40074
+ vec3 n = normalize(vec3((left - right) * scale, (down - up) * scale, 2.0 * u_metresPerTexel));
40075
+ float lambert = clamp(dot(n, normalize(vec3(-0.5, -0.6, 0.62))), 0.0, 1.0);
40076
+ color.rgb *= 0.45 + 0.9 * lambert;
40077
+
40078
+ out_FragColor = vec4(color.rgb, color.a * v_coverage);
40079
+ }
40080
+ `;
40081
+ /*
40082
+ * Rewrites a GLSL 300 es source into the GLSL 100 dialect older Cesium builds author in.
40083
+ */
40084
+ function toGlsl100(source, isFragment) {
40085
+ let result = source
40086
+ .replace(/\btexture\s*\(/g, "texture2D(")
40087
+ .replace(/^in\s+/gm, isFragment ? "varying " : "attribute ")
40088
+ .replace(/^out\s+/gm, "varying ");
40089
+ if (isFragment) {
40090
+ result = result.replace(/\bout_FragColor\b/g, "gl_FragColor");
40091
+ }
40092
+ return result;
40093
+ }
40094
+ let _glsl300Support = null;
40095
+ /*
40096
+ * Works out which GLSL dialect this host's Cesium authors shaders in.
40097
+ */
40098
+ function usesGlsl300(context) {
40099
+ if (_glsl300Support !== null) {
40100
+ return _glsl300Support;
40101
+ }
40102
+ let modernizes = false;
40103
+ try {
40104
+ const probe = new Cesium.ShaderSource({
40105
+ sources: ["varying float v_probe;\nvoid main() { gl_Position = vec4(0.0); }"],
40106
+ includeBuiltIns: false
40107
+ });
40108
+ const combined = probe.createCombinedVertexShader(context);
40109
+ modernizes = combined.indexOf("varying") < 0;
40110
+ }
40111
+ catch (e) {
40112
+ // No probe means no evidence the host rewrites anything, so trust the context alone.
40113
+ modernizes = false;
40114
+ }
40115
+ _glsl300Support = Boolean(context.webgl2) && !modernizes;
40116
+ return _glsl300Support;
40117
+ }
40118
+ /*
40119
+ * Places grid node (fx, fy) on the ellipsoid, walking equal steps of east/north metres.
40120
+ * Cesium maps a polygon's material across a tangent-plane rectangle and the generated raster is
40121
+ * linear in a projected CRS, so stepping in degrees instead would skew the surface against the flat-textured polygon it replaces.
40122
+ */
40123
+ function makePlacer(extent) {
40124
+ const ellipsoid = Cesium.Ellipsoid.WGS84;
40125
+ const centre = Cesium.Cartesian3.fromDegrees((extent.West + extent.East) / 2, (extent.South + extent.North) / 2, 0);
40126
+ const frame = Cesium.Transforms.eastNorthUpToFixedFrame(centre);
40127
+ const toLocal = Cesium.Matrix4.inverse(frame, new Cesium.Matrix4());
40128
+ const southWest = Cesium.Matrix4.multiplyByPoint(toLocal, Cesium.Cartesian3.fromDegrees(extent.West, extent.South, 0), new Cesium.Cartesian3());
40129
+ const northEast = Cesium.Matrix4.multiplyByPoint(toLocal, Cesium.Cartesian3.fromDegrees(extent.East, extent.North, 0), new Cesium.Cartesian3());
40130
+ return (fx, fy) => {
40131
+ const local = new Cesium.Cartesian3(Cesium.Math.lerp(southWest.x, northEast.x, fx), Cesium.Math.lerp(northEast.y, southWest.y, fy), 0);
40132
+ const world = Cesium.Matrix4.multiplyByPoint(frame, local, new Cesium.Cartesian3());
40133
+ return ellipsoid.scaleToGeodeticSurface(world, world);
40134
+ };
40135
+ }
40136
+ /*
40137
+ * Builds one tile's grid, plus a duplicated perimeter ring the shader pushes downwards to cover
40138
+ * the crack where a neighbouring tile at a different density interpolates the shared edge.
40139
+ */
40140
+ function buildGeometry(extent, resolution, patch) {
40141
+ const cols = resolution;
40142
+ const rows = resolution;
40143
+ const gridCount = (cols + 1) * (rows + 1);
40144
+ const perimeterCount = 2 * (cols + 1) + 2 * (rows + 1) - 4;
40145
+ const count = gridCount + perimeterCount;
40146
+ const positions = new Float64Array(count * 3);
40147
+ const normals = new Float32Array(count * 3);
40148
+ const sts = new Float32Array(count * 2);
40149
+ const skirts = new Float32Array(count);
40150
+ const ellipsoid = Cesium.Ellipsoid.WGS84;
40151
+ const place = makePlacer(extent);
40152
+ let v = 0;
40153
+ for (let row = 0; row <= rows; row++) {
40154
+ // Texture row 0 is the north edge, so t runs opposite to northing.
40155
+ const fy = Cesium.Math.lerp(patch.v0, patch.v1, row / rows);
40156
+ for (let col = 0; col <= cols; col++) {
40157
+ const fx = Cesium.Math.lerp(patch.u0, patch.u1, col / cols);
40158
+ const position = place(fx, fy);
40159
+ positions[v * 3] = position.x;
40160
+ positions[v * 3 + 1] = position.y;
40161
+ positions[v * 3 + 2] = position.z;
40162
+ const up = ellipsoid.geodeticSurfaceNormal(position, new Cesium.Cartesian3());
40163
+ normals[v * 3] = up.x;
40164
+ normals[v * 3 + 1] = up.y;
40165
+ normals[v * 3 + 2] = up.z;
40166
+ sts[v * 2] = fx;
40167
+ sts[v * 2 + 1] = fy;
40168
+ v++;
40169
+ }
40170
+ }
40171
+ const indices = new Uint32Array(cols * rows * 6 + perimeterCount * 6);
40172
+ let i = 0;
40173
+ for (let row = 0; row < rows; row++) {
40174
+ for (let col = 0; col < cols; col++) {
40175
+ const topLeft = row * (cols + 1) + col;
40176
+ const topRight = topLeft + 1;
40177
+ const bottomLeft = topLeft + (cols + 1);
40178
+ const bottomRight = bottomLeft + 1;
40179
+ indices[i++] = topLeft;
40180
+ indices[i++] = bottomLeft;
40181
+ indices[i++] = topRight;
40182
+ indices[i++] = topRight;
40183
+ indices[i++] = bottomLeft;
40184
+ indices[i++] = bottomRight;
40185
+ }
40186
+ }
40187
+ // Perimeter walked as one closed loop, so consecutive entries are always neighbours.
40188
+ const loop = [];
40189
+ for (let col = 0; col <= cols; col++) {
40190
+ loop.push(col);
40191
+ }
40192
+ for (let row = 1; row <= rows; row++) {
40193
+ loop.push(row * (cols + 1) + cols);
40194
+ }
40195
+ for (let col = cols - 1; col >= 0; col--) {
40196
+ loop.push(rows * (cols + 1) + col);
40197
+ }
40198
+ for (let row = rows - 1; row >= 1; row--) {
40199
+ loop.push(row * (cols + 1));
40200
+ }
40201
+ const skirtStart = gridCount;
40202
+ loop.forEach((gridIndex, k) => {
40203
+ const target = skirtStart + k;
40204
+ positions[target * 3] = positions[gridIndex * 3];
40205
+ positions[target * 3 + 1] = positions[gridIndex * 3 + 1];
40206
+ positions[target * 3 + 2] = positions[gridIndex * 3 + 2];
40207
+ normals[target * 3] = normals[gridIndex * 3];
40208
+ normals[target * 3 + 1] = normals[gridIndex * 3 + 1];
40209
+ normals[target * 3 + 2] = normals[gridIndex * 3 + 2];
40210
+ sts[target * 2] = sts[gridIndex * 2];
40211
+ sts[target * 2 + 1] = sts[gridIndex * 2 + 1];
40212
+ skirts[target] = 1;
40213
+ });
40214
+ for (let k = 0; k < loop.length; k++) {
40215
+ const nextK = (k + 1) % loop.length;
40216
+ indices[i++] = loop[k];
40217
+ indices[i++] = skirtStart + k;
40218
+ indices[i++] = loop[nextK];
40219
+ indices[i++] = loop[nextK];
40220
+ indices[i++] = skirtStart + k;
40221
+ indices[i++] = skirtStart + nextK;
40222
+ }
40223
+ const geometry = new Cesium.Geometry({
40224
+ attributes: {
40225
+ position: new Cesium.GeometryAttribute({
40226
+ componentDatatype: Cesium.ComponentDatatype.DOUBLE,
40227
+ componentsPerAttribute: 3,
40228
+ values: positions
40229
+ }),
40230
+ normal: new Cesium.GeometryAttribute({
40231
+ componentDatatype: Cesium.ComponentDatatype.FLOAT,
40232
+ componentsPerAttribute: 3,
40233
+ values: normals
40234
+ }),
40235
+ st: new Cesium.GeometryAttribute({
40236
+ componentDatatype: Cesium.ComponentDatatype.FLOAT,
40237
+ componentsPerAttribute: 2,
40238
+ values: sts
40239
+ }),
40240
+ skirt: new Cesium.GeometryAttribute({
40241
+ componentDatatype: Cesium.ComponentDatatype.FLOAT,
40242
+ componentsPerAttribute: 1,
40243
+ values: skirts
40244
+ })
40245
+ },
40246
+ indices: indices,
40247
+ primitiveType: Cesium.PrimitiveType.TRIANGLES,
40248
+ boundingSphere: Cesium.BoundingSphere.fromVertices(Array.prototype.slice.call(positions))
40249
+ });
40250
+ // Splits position into the high/low pair Cesium's relative-to-eye shaders expect.
40251
+ Cesium.GeometryPipeline.encodeAttribute(geometry, "position", "position3DHigh", "position3DLow");
40252
+ return geometry;
40253
+ }
40254
+ class Surface {
40255
+ constructor(options, hasCoverage) {
40256
+ var _a, _b, _c, _d;
40257
+ this.show = true;
40258
+ this.valueSource = null;
40259
+ this.lastPresentVersion = -1;
40260
+ this.texture = null;
40261
+ this.textureDirty = true;
40262
+ this.shaderProgram = null;
40263
+ this.renderState = null;
40264
+ this.destroyed = false;
40265
+ this.lastEyePosition = null;
40266
+ this.lastSelectionMs = -Infinity;
40267
+ this.selectionDirty = true;
40268
+ this.groundTexture = null;
40269
+ this.groundPixels = null;
40270
+ this.groundRequested = false;
40271
+ this.followGround = Boolean(options.followGround);
40272
+ this.terrainProvider = options.terrainProvider || null;
40273
+ this.extent = options.extent;
40274
+ this.source = options.source || null;
40275
+ this.valueMin = (_a = options.valueMin) !== null && _a !== void 0 ? _a : 0;
40276
+ this.valueMax = (_b = options.valueMax) !== null && _b !== void 0 ? _b : 1;
40277
+ this.exaggeration = (_c = options.exaggeration) !== null && _c !== void 0 ? _c : autoExaggeration(this.extent, this.valueMin, this.valueMax);
40278
+ this.baseHeight = (_d = options.baseHeight) !== null && _d !== void 0 ? _d : 0;
40279
+ this.lowColor = toCesiumColor(options.lowColor || DEFAULT_LOW_COLOR);
40280
+ this.highColor = toCesiumColor(options.highColor || DEFAULT_HIGH_COLOR);
40281
+ this.tiles = buildTiles(hasCoverage);
40282
+ }
40283
+ GetFollowsGround() {
40284
+ return this.followGround;
40285
+ }
40286
+ GetBaseHeight() {
40287
+ return this.baseHeight;
40288
+ }
40289
+ GetExaggeration() {
40290
+ return this.exaggeration;
40291
+ }
40292
+ GetExtent() {
40293
+ return this.extent;
40294
+ }
40295
+ SetExaggeration(exaggeration) {
40296
+ this.exaggeration = exaggeration;
40297
+ }
40298
+ SetBaseHeight(baseHeight) {
40299
+ this.baseHeight = baseHeight;
40300
+ }
40301
+ /**
40302
+ * Feeds the surface from an animated frame series.
40303
+ * Height and colour advance with the clock without the caller having to push each frame in.
40304
+ */
40305
+ BindAnimator(source) {
40306
+ this.valueSource = source;
40307
+ }
40308
+ /**
40309
+ * Tells the surface its source canvas has been redrawn and the texture needs re-uploading.
40310
+ */
40311
+ MarkSourceDirty() {
40312
+ this.textureDirty = true;
40313
+ }
40314
+ update(frameState) {
40315
+ if (!this.show || this.destroyed || this.tiles.length === 0) {
40316
+ return;
40317
+ }
40318
+ const context = frameState.context;
40319
+ if (this.valueSource) {
40320
+ const canvas = this.valueSource.GetValueCanvas();
40321
+ if (!canvas) {
40322
+ // No frame decoded yet, so there is nothing to displace against.
40323
+ return;
40324
+ }
40325
+ const version = this.valueSource.GetPresentVersion();
40326
+ if (canvas !== this.source || version !== this.lastPresentVersion) {
40327
+ this.source = canvas;
40328
+ this.lastPresentVersion = version;
40329
+ this.textureDirty = true;
40330
+ }
40331
+ }
40332
+ if (!this.source || !this.source.width || !this.source.height) {
40333
+ return;
40334
+ }
40335
+ this.syncTexture(context);
40336
+ this.requestGround();
40337
+ this.syncGroundTexture(context);
40338
+ // A tile needs one mesh before it has a bounding sphere to measure against.
40339
+ for (const tile of this.tiles) {
40340
+ if (!tile.centre) {
40341
+ this.ensureMesh(context, tile, TIERS[0]);
40342
+ }
40343
+ }
40344
+ const nowMs = now();
40345
+ if (this.shouldReselect(frameState, nowMs)) {
40346
+ this.selectTiers(frameState);
40347
+ this.lastSelectionMs = nowMs;
40348
+ this.lastEyePosition = Cesium.Cartesian3.clone(frameState.camera.positionWC, this.lastEyePosition);
40349
+ this.selectionDirty = false;
40350
+ }
40351
+ for (const tile of this.tiles) {
40352
+ const mesh = this.ensureMesh(context, tile, tile.tier);
40353
+ frameState.commandList.push(this.commandFor(context, tile, mesh));
40354
+ }
40355
+ }
40356
+ isDestroyed() {
40357
+ return this.destroyed;
40358
+ }
40359
+ destroy() {
40360
+ this.destroyed = true;
40361
+ for (const tile of this.tiles) {
40362
+ tile.meshes.forEach((mesh) => mesh.vertexArray.destroy());
40363
+ tile.meshes.clear();
40364
+ tile.command = null;
40365
+ }
40366
+ if (this.shaderProgram) {
40367
+ this.shaderProgram.destroy();
40368
+ this.shaderProgram = null;
40369
+ }
40370
+ if (this.texture) {
40371
+ this.texture.destroy();
40372
+ this.texture = null;
40373
+ }
40374
+ if (this.groundTexture) {
40375
+ this.groundTexture.destroy();
40376
+ this.groundTexture = null;
40377
+ }
40378
+ return undefined;
40379
+ }
40380
+ /*
40381
+ * Samples terrain across the extent once and packs it into a texture the vertex shader reads.
40382
+ *
40383
+ * A texture rather than heights baked into the meshes, because the samples arrive
40384
+ * asynchronously and every cached tile mesh would otherwise have to be rebuilt when they land.
40385
+ */
40386
+ requestGround() {
40387
+ if (this.groundRequested || !this.followGround) {
40388
+ return;
40389
+ }
40390
+ this.groundRequested = true;
40391
+ const sampler = Cesium.sampleTerrain;
40392
+ if (!this.terrainProvider || typeof sampler !== "function") {
40393
+ return;
40394
+ }
40395
+ const positions = [];
40396
+ for (let row = 0; row < GROUND_SAMPLES_PER_SIDE; row++) {
40397
+ const fy = row / (GROUND_SAMPLES_PER_SIDE - 1);
40398
+ const latitude = Cesium.Math.lerp(this.extent.North, this.extent.South, fy);
40399
+ for (let col = 0; col < GROUND_SAMPLES_PER_SIDE; col++) {
40400
+ const fx = col / (GROUND_SAMPLES_PER_SIDE - 1);
40401
+ const longitude = Cesium.Math.lerp(this.extent.West, this.extent.East, fx);
40402
+ positions.push(Cesium.Cartographic.fromDegrees(longitude, latitude));
40403
+ }
40404
+ }
40405
+ Promise.resolve(sampler(this.terrainProvider, GROUND_SAMPLE_LEVEL, positions))
40406
+ .then((sampled) => {
40407
+ if (this.destroyed || !sampled) {
40408
+ return;
40409
+ }
40410
+ const pixels = new Uint8Array(GROUND_SAMPLES_PER_SIDE * GROUND_SAMPLES_PER_SIDE * 4);
40411
+ const span = GROUND_MAX_METRES - GROUND_MIN_METRES;
40412
+ for (let i = 0; i < sampled.length; i++) {
40413
+ const height = sampled[i] && isFinite(sampled[i].height) ? sampled[i].height : 0;
40414
+ const clamped = Math.min(GROUND_MAX_METRES, Math.max(GROUND_MIN_METRES, height));
40415
+ const packed = Math.round(((clamped - GROUND_MIN_METRES) / span) * 16777215);
40416
+ pixels[i * 4] = (packed >> 16) & 255;
40417
+ pixels[i * 4 + 1] = (packed >> 8) & 255;
40418
+ pixels[i * 4 + 2] = packed & 255;
40419
+ pixels[i * 4 + 3] = 255;
40420
+ }
40421
+ this.groundPixels = pixels;
40422
+ })
40423
+ .catch(() => {
40424
+ // Terrain that cannot be sampled leaves the surface on the ellipsoid, which is
40425
+ // where a clamped polygon sat before this existed.
40426
+ });
40427
+ }
40428
+ /*
40429
+ * Uploads whatever ground samples exist, and a flat stand-in until they arrive, so the
40430
+ * shader always has a texture bound whether or not this surface follows the ground.
40431
+ */
40432
+ syncGroundTexture(context) {
40433
+ if (this.groundTexture && (!this.groundPixels || this.groundTexture.width === GROUND_SAMPLES_PER_SIDE)) {
40434
+ return;
40435
+ }
40436
+ if (this.groundTexture) {
40437
+ this.groundTexture.destroy();
40438
+ }
40439
+ const zero = Math.round(((0 - GROUND_MIN_METRES) / (GROUND_MAX_METRES - GROUND_MIN_METRES)) * 16777215);
40440
+ const size = this.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1;
40441
+ const arrayBufferView = this.groundPixels
40442
+ || new Uint8Array([(zero >> 16) & 255, (zero >> 8) & 255, zero & 255, 255]);
40443
+ this.groundTexture = new Cesium.Texture({
40444
+ context,
40445
+ pixelFormat: Cesium.PixelFormat.RGBA,
40446
+ source: { width: size, height: size, arrayBufferView },
40447
+ flipY: false,
40448
+ sampler: new Cesium.Sampler({
40449
+ wrapS: Cesium.TextureWrap.CLAMP_TO_EDGE,
40450
+ wrapT: Cesium.TextureWrap.CLAMP_TO_EDGE,
40451
+ // Nearest, because the shader interpolates decoded heights itself.
40452
+ minificationFilter: Cesium.TextureMinificationFilter.NEAREST,
40453
+ magnificationFilter: Cesium.TextureMagnificationFilter.NEAREST
40454
+ })
40455
+ });
40456
+ }
40457
+ /*
40458
+ * Ground distance between neighbouring texels, so the shading normal is built from a real
40459
+ * slope rather than a made up one.
40460
+ */
40461
+ metresPerTexel() {
40462
+ const southWest = Cesium.Cartesian3.fromDegrees(this.extent.West, this.extent.South, 0);
40463
+ const southEast = Cesium.Cartesian3.fromDegrees(this.extent.East, this.extent.South, 0);
40464
+ const width = Cesium.Cartesian3.distance(southWest, southEast);
40465
+ return Math.max(1, width / Math.max(1, this.source ? this.source.width : 1));
40466
+ }
40467
+ syncTexture(context) {
40468
+ const sizeChanged = this.texture
40469
+ && (this.texture.width !== this.source.width || this.texture.height !== this.source.height);
40470
+ if (!this.texture || sizeChanged) {
40471
+ if (this.texture) {
40472
+ this.texture.destroy();
40473
+ }
40474
+ this.texture = new Cesium.Texture({
40475
+ context,
40476
+ source: this.source,
40477
+ // Cesium flips canvas rows on upload by default, which would put the archive's
40478
+ // row 0 (its north edge) at the south edge and mirror the whole surface.
40479
+ flipY: false,
40480
+ sampler: new Cesium.Sampler({
40481
+ wrapS: Cesium.TextureWrap.CLAMP_TO_EDGE,
40482
+ wrapT: Cesium.TextureWrap.CLAMP_TO_EDGE,
40483
+ minificationFilter: Cesium.TextureMinificationFilter.LINEAR,
40484
+ magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR
40485
+ })
40486
+ });
40487
+ this.textureDirty = false;
40488
+ return;
40489
+ }
40490
+ if (this.textureDirty) {
40491
+ this.texture.copyFrom({ source: this.source });
40492
+ this.textureDirty = false;
40493
+ }
40494
+ }
40495
+ ensureMesh(context, tile, tier) {
40496
+ const cached = tile.meshes.get(tier);
40497
+ if (cached) {
40498
+ return cached;
40499
+ }
40500
+ const geometry = buildGeometry(this.extent, tier, tile.patch);
40501
+ const attributeLocations = Cesium.GeometryPipeline.createAttributeLocations(geometry);
40502
+ const mesh = {
40503
+ attributeLocations,
40504
+ vertexArray: Cesium.VertexArray.fromGeometry({
40505
+ context,
40506
+ geometry,
40507
+ attributeLocations,
40508
+ bufferUsage: Cesium.BufferUsage.STATIC_DRAW
40509
+ }),
40510
+ boundingSphere: geometry.boundingSphere
40511
+ };
40512
+ tile.meshes.set(tier, mesh);
40513
+ tile.centre = geometry.boundingSphere.center;
40514
+ tile.radius = geometry.boundingSphere.radius;
40515
+ return mesh;
40516
+ }
40517
+ /*
40518
+ * Picks each tile's density from how much screen it covers.
40519
+ */
40520
+ selectTiers(frameState) {
40521
+ const camera = frameState.camera;
40522
+ const fovy = camera.frustum && camera.frustum.fovy ? camera.frustum.fovy : Cesium.Math.PI_OVER_THREE;
40523
+ const pixelsPerRadian = frameState.context.drawingBufferHeight / fovy;
40524
+ for (const tile of this.tiles) {
40525
+ if (!tile.centre) {
40526
+ tile.tier = TIERS[0];
40527
+ continue;
40528
+ }
40529
+ const distance = Math.max(1, Cesium.Cartesian3.distance(camera.positionWC, tile.centre) - tile.radius);
40530
+ const screenPixels = (2 * tile.radius / distance) * pixelsPerRadian;
40531
+ const wanted = screenPixels / TARGET_CELL_PIXELS;
40532
+ let tier = TIERS[0];
40533
+ for (const candidate of TIERS) {
40534
+ if (candidate <= wanted) {
40535
+ tier = candidate;
40536
+ }
40537
+ }
40538
+ tile.tier = tier;
40539
+ }
40540
+ }
40541
+ shouldReselect(frameState, nowMs) {
40542
+ if (this.selectionDirty) {
40543
+ return true;
40544
+ }
40545
+ if (nowMs - this.lastSelectionMs < RESELECT_INTERVAL_MS) {
40546
+ return false;
40547
+ }
40548
+ const eye = frameState.camera.positionWC;
40549
+ if (!this.lastEyePosition) {
40550
+ return true;
40551
+ }
40552
+ const moved = Cesium.Cartesian3.distance(eye, this.lastEyePosition);
40553
+ const anchor = this.tiles[0].centre;
40554
+ const reference = anchor ? Math.max(1, Cesium.Cartesian3.distance(eye, anchor)) : 1;
40555
+ return moved / reference > MOVEMENT_THRESHOLD;
40556
+ }
40557
+ commandFor(context, tile, mesh) {
40558
+ if (!this.shaderProgram) {
40559
+ const glsl300 = usesGlsl300(context);
40560
+ this.shaderProgram = Cesium.ShaderProgram.fromCache({
40561
+ context,
40562
+ vertexShaderSource: glsl300 ? VERTEX_SHADER_GLSL300 : toGlsl100(VERTEX_SHADER_GLSL300, false),
40563
+ fragmentShaderSource: glsl300 ? FRAGMENT_SHADER_GLSL300 : toGlsl100(FRAGMENT_SHADER_GLSL300, true),
40564
+ attributeLocations: mesh.attributeLocations
40565
+ });
40566
+ this.renderState = Cesium.RenderState.fromCache({
40567
+ depthTest: { enabled: true },
40568
+ // Translucent ramp: blend, and leave the depth buffer to the opaque world.
40569
+ depthMask: false,
40570
+ blending: Cesium.BlendingState.ALPHA_BLEND,
40571
+ cull: { enabled: false }
40572
+ });
40573
+ }
40574
+ if (!tile.command) {
40575
+ const self = this;
40576
+ tile.command = new Cesium.DrawCommand({
40577
+ shaderProgram: this.shaderProgram,
40578
+ renderState: this.renderState,
40579
+ pass: Cesium.Pass.TRANSLUCENT,
40580
+ modelMatrix: Cesium.Matrix4.IDENTITY,
40581
+ owner: this,
40582
+ uniformMap: {
40583
+ u_valueTexture: () => self.texture,
40584
+ u_groundTexture: () => self.groundTexture,
40585
+ u_groundWeight: () => (self.followGround && self.groundPixels ? 1 : 0),
40586
+ u_groundRange: () => new Cesium.Cartesian2(GROUND_MIN_METRES, GROUND_MAX_METRES),
40587
+ u_groundSize: () => new Cesium.Cartesian2(self.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1, self.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1),
40588
+ u_metresPerTexel: () => self.metresPerTexel(),
40589
+ u_valueMin: () => self.valueMin,
40590
+ u_valueRange: () => self.valueMax - self.valueMin,
40591
+ u_exaggeration: () => self.exaggeration,
40592
+ u_baseHeight: () => self.baseHeight,
40593
+ u_skirtDepth: () => Math.max(2, Math.abs(self.valueMax - self.valueMin) * self.exaggeration * SKIRT_FRACTION),
40594
+ u_coverageCutoff: () => COVERAGE_CUTOFF,
40595
+ u_lowColor: () => self.lowColor,
40596
+ u_highColor: () => self.highColor,
40597
+ u_texelSize: () => new Cesium.Cartesian2(1 / self.source.width, 1 / self.source.height)
40598
+ }
40599
+ });
40600
+ }
40601
+ // Bounding volume belongs to the mesh, so it follows whichever tier is drawing.
40602
+ tile.command.vertexArray = mesh.vertexArray;
40603
+ tile.command.boundingVolume = mesh.boundingSphere;
40604
+ return tile.command;
40605
+ }
40606
+ }
40607
+ DisplacedSurfacePrimitive.Surface = Surface;
40608
+ /*
40609
+ * Cuts the extent into tiles, dropping any the caller says carry no data.
40610
+ */
40611
+ function buildTiles(hasCoverage) {
40612
+ const tiles = [];
40613
+ for (let row = 0; row < TILES_PER_SIDE; row++) {
40614
+ for (let col = 0; col < TILES_PER_SIDE; col++) {
40615
+ const patch = {
40616
+ u0: col / TILES_PER_SIDE,
40617
+ u1: (col + 1) / TILES_PER_SIDE,
40618
+ v0: row / TILES_PER_SIDE,
40619
+ v1: (row + 1) / TILES_PER_SIDE
40620
+ };
40621
+ if (hasCoverage && !hasCoverage(patch)) {
40622
+ continue;
40623
+ }
40624
+ tiles.push({
40625
+ patch,
40626
+ meshes: new Map(),
40627
+ tier: TIERS[0],
40628
+ command: null,
40629
+ centre: null,
40630
+ radius: 0
40631
+ });
40632
+ }
40633
+ }
40634
+ return tiles;
40635
+ }
40636
+ /**
40637
+ * Exaggeration that makes the full value range stand AUTO_RELIEF_FRACTION of the extent tall.
40638
+ *
40639
+ * Derived from the whole extent rather than per tile: scaling each tile to its own size would
40640
+ * make neighbours disagree along their shared edge and tear the surface apart.
40641
+ * @param extent
40642
+ * @param valueMin
40643
+ * @param valueMax
40644
+ */
40645
+ function autoExaggeration(extent, valueMin, valueMax) {
40646
+ const range = Math.abs(valueMax - valueMin);
40647
+ if (!(range > 0)) {
40648
+ return 1;
40649
+ }
40650
+ const southWest = Cesium.Cartesian3.fromDegrees(extent.West, extent.South, 0);
40651
+ const southEast = Cesium.Cartesian3.fromDegrees(extent.East, extent.South, 0);
40652
+ const northWest = Cesium.Cartesian3.fromDegrees(extent.West, extent.North, 0);
40653
+ const shorterSide = Math.min(Cesium.Cartesian3.distance(southWest, southEast), Cesium.Cartesian3.distance(southWest, northWest));
40654
+ if (!(shorterSide > 0)) {
40655
+ return 1;
40656
+ }
40657
+ return (shorterSide * AUTO_RELIEF_FRACTION) / range;
40658
+ }
40659
+ DisplacedSurfacePrimitive.autoExaggeration = autoExaggeration;
40660
+ function toCesiumColor(color) {
40661
+ return new Cesium.Color(color.red / 255, color.green / 255, color.blue / 255, color.alpha);
40662
+ }
40663
+ function now() {
40664
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
40665
+ }
40666
+ })(exports.DisplacedSurfacePrimitive || (exports.DisplacedSurfacePrimitive = {}));
40667
+
39897
40668
  const TEXTURE_FRAME_SERIES_ANIMATOR_KEY = "TextureFrameSeriesAnimator.Animator";
40669
+ const EXTRUSION_ANIMATOR_KEY = "TextureFrameSeriesAnimator.ExtrusionAnimator";
40670
+ const DISPLACED_SURFACE_KEY = "DisplacedSurfacePrimitive.Surface";
39898
40671
  (function (EntityRenderEnginePolygon) {
39899
40672
  async function Render(params) {
39900
- var _a, _b, _c;
40673
+ var _a, _b, _c, _d;
39901
40674
  const entity = params.entity;
39902
40675
  const style = params.style;
39903
40676
  const pRings = BModels.Entity.GetValue({
@@ -39930,14 +40703,36 @@
39930
40703
  console.error(`Polygon.Render: failed to resolve textured fill for entity ${((_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.ID) || "<NONE>"}:`, e);
39931
40704
  }
39932
40705
  }
39933
- const hasFill = fillType === BModels.Style.EPolygonFillType.Texture
39934
- ? Boolean(textureDataUri || frameArchive || cFillColor.alpha > 0)
39935
- : cFillColor.alpha > 0;
39936
- const fillMaterial = textureDataUri
39937
- ? new Cesium.ImageMaterialProperty({ image: textureDataUri, transparent: true })
39938
- : frameArchive
39939
- ? Cesium.Color.WHITE.withAlpha(0)
39940
- : cFillColor;
40706
+ const usesTextureExtrusion = (Boolean(style.useExtrusion) &&
40707
+ style.extrusionType === BModels.Style.EPolygonExtrusionType.Texture);
40708
+ let extrusionArchive = null;
40709
+ if (usesTextureExtrusion && !params.offline) {
40710
+ try {
40711
+ const resolved = await resolveTextureValue({
40712
+ api: params.api,
40713
+ textureValue: style.extrusionTexture || style.texture,
40714
+ style,
40715
+ entity,
40716
+ tags: params.tags
40717
+ });
40718
+ extrusionArchive = resolved.frameArchive || null;
40719
+ }
40720
+ catch (e) {
40721
+ console.error(`Polygon.Render: failed to resolve extrusion texture for entity ${((_b = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _b === void 0 ? void 0 : _b.ID) || "<NONE>"}:`, e);
40722
+ }
40723
+ }
40724
+ const drawsDisplacedSurface = Boolean(extrusionArchive);
40725
+ const hasFill = (drawsDisplacedSurface ? true
40726
+ : fillType === BModels.Style.EPolygonFillType.Texture
40727
+ ? Boolean(textureDataUri || frameArchive || cFillColor.alpha > 0)
40728
+ : cFillColor.alpha > 0);
40729
+ const fillMaterial = drawsDisplacedSurface
40730
+ ? Cesium.Color.WHITE.withAlpha(0)
40731
+ : textureDataUri
40732
+ ? new Cesium.ImageMaterialProperty({ image: textureDataUri, transparent: true })
40733
+ : frameArchive
40734
+ ? Cesium.Color.WHITE.withAlpha(0)
40735
+ : cFillColor;
39941
40736
  const lineColorTrace = style.lineColor ? BModels.Calculator.TraceGetColor(style.lineColor, entity, params.tags) : { value: null, effective: null };
39942
40737
  const bLineColor = lineColorTrace.value;
39943
40738
  const cLineColor = bLineColor ? ColorToCColor(bLineColor) : Cesium.Color.fromCssColorString("rgba(80, 80, 80, 0.8)");
@@ -39964,7 +40759,9 @@
39964
40759
  const points = BModels.Geometry.ParsePoints(outerRing === null || outerRing === void 0 ? void 0 : outerRing.LinearRing);
39965
40760
  let posses = points.map(x => Cesium.Cartesian3.fromDegrees(EnsureNumber(x.longitude), EnsureNumber(x.latitude), EnsureNumber(x.altitude)));
39966
40761
  BModels.Cartes.CloseRing3(posses);
39967
- const extrusion = getPolygonExtrusion(entity, params.tags, outerRing, posses, heightRef, style);
40762
+ const extrusion = drawsDisplacedSurface
40763
+ ? { posses, value: undefined, exHeightRef: Cesium.HeightReference.NONE }
40764
+ : getPolygonExtrusion(entity, params.tags, outerRing, posses, heightRef, style);
39968
40765
  posses = extrusion.posses;
39969
40766
  posses = CullDuplicateCPosses(posses);
39970
40767
  if (posses.length < 4) {
@@ -40096,7 +40893,16 @@
40096
40893
  }
40097
40894
  // Must run before any material is baked below: the Animator takes over polygon.material, so a bake
40098
40895
  // has to know whether it is baking over a live animation or over a disposed one's restored material.
40099
- const animator = syncTextureFrameArchive(cEntity, frameArchive, params.viewer, style.textureColorMask);
40896
+ const animator = syncTextureFrameArchive(cEntity, drawsDisplacedSurface ? null : frameArchive, params.viewer, style.textureColorMask);
40897
+ syncDisplacedSurface({
40898
+ cEntity,
40899
+ extrusionArchive,
40900
+ viewer: params.viewer,
40901
+ style,
40902
+ entity,
40903
+ heightRef,
40904
+ outerRingPosses: posses
40905
+ });
40100
40906
  if (animator) {
40101
40907
  exports.CesiumEntityStyler.SetDefaultTextureImage({
40102
40908
  entity: cEntity,
@@ -40156,7 +40962,7 @@
40156
40962
  }
40157
40963
  }
40158
40964
  let borderPosses = posses.map(x => x.clone ? x.clone() : { ...x });
40159
- let cEntityBorder = (_c = (_b = params.rendered) === null || _b === void 0 ? void 0 : _b._siblingGraphics) === null || _c === void 0 ? void 0 : _c[0];
40965
+ let cEntityBorder = (_d = (_c = params.rendered) === null || _c === void 0 ? void 0 : _c._siblingGraphics) === null || _d === void 0 ? void 0 : _d[0];
40160
40966
  cEntity._siblingGraphics = [];
40161
40967
  if (!cEntityBorder || ((!cEntityBorder.polyline && units == "px") ||
40162
40968
  (!cEntityBorder.corridor && units == "m"))) {
@@ -40406,12 +41212,13 @@
40406
41212
  /**
40407
41213
  * Disposes a cEntity's TextureFrameSeriesAnimator.Animator (if any).
40408
41214
  */
40409
- function DisposeTextureFrameSeriesAnimator(cEntity) {
41215
+ function DisposeTextureFrameSeriesAnimator(cEntity, viewer) {
40410
41216
  const existing = cEntity === null || cEntity === void 0 ? void 0 : cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY];
40411
41217
  if (existing && !existing.IsDisposed()) {
40412
41218
  existing.Dispose();
40413
41219
  cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY] = null;
40414
41220
  }
41221
+ disposeDisplacedSurface(cEntity, viewer);
40415
41222
  }
40416
41223
  EntityRenderEnginePolygon.DisposeTextureFrameSeriesAnimator = DisposeTextureFrameSeriesAnimator;
40417
41224
  })(exports.EntityRenderEnginePolygon || (exports.EntityRenderEnginePolygon = {}));
@@ -40525,6 +41332,140 @@
40525
41332
  // Cache of tinted texture data URIs, keyed by resolved url + color mask, so repeated renders of the
40526
41333
  // same polygon (or multiple polygons sharing a texture) don't refetch/reprocess the same image.
40527
41334
  const _textureCache = new BModels.LRUCache(50);
41335
+ /**
41336
+ * Reconciles the displaced surface (and the animator feeding it) against this render's resolved
41337
+ * extrusion texture, creating, reusing or tearing down as the style demands.
41338
+ * @param params
41339
+ */
41340
+ function syncDisplacedSurface(params) {
41341
+ var _a;
41342
+ const { cEntity, extrusionArchive, viewer, style, entity, heightRef, outerRingPosses } = params;
41343
+ const existingAnimator = cEntity[EXTRUSION_ANIMATOR_KEY];
41344
+ const existingSurface = cEntity[DISPLACED_SURFACE_KEY];
41345
+ // Nothing to draw and nothing left over, which is every polygon that does not use this feature.
41346
+ if (!extrusionArchive && !existingAnimator && !existingSurface) {
41347
+ return;
41348
+ }
41349
+ const reusable = extrusionArchive
41350
+ && existingAnimator
41351
+ && !existingAnimator.IsDisposed()
41352
+ && existingAnimator.GetArchiveUrl() === extrusionArchive.url
41353
+ && existingSurface
41354
+ && !existingSurface.isDestroyed();
41355
+ const placement = surfacePlacement(entity, heightRef);
41356
+ // Whether the surface follows terrain is baked into its ground sampling, so a change there has
41357
+ // to rebuild rather than update in place.
41358
+ if (reusable && existingSurface.GetFollowsGround() === placement.followGround) {
41359
+ existingSurface.SetBaseHeight(placement.baseHeight);
41360
+ existingSurface.SetExaggeration((_a = style.extrusionExaggeration) !== null && _a !== void 0 ? _a : exports.DisplacedSurfacePrimitive.autoExaggeration(existingSurface.GetExtent(), extrusionArchive.metadata.ValueMin, extrusionArchive.metadata.ValueMax));
41361
+ return;
41362
+ }
41363
+ disposeDisplacedSurface(cEntity, viewer);
41364
+ if (!extrusionArchive) {
41365
+ return;
41366
+ }
41367
+ const extent = archiveExtent(extrusionArchive.metadata, outerRingPosses);
41368
+ if (!extent) {
41369
+ console.warn("Polygon.Render: extrusion texture has no usable extent, skipping displaced surface.");
41370
+ return;
41371
+ }
41372
+ // Drives the value canvas only: the surface paints the colour, so the polygon's own material is
41373
+ // left transparent rather than animated underneath it.
41374
+ const animator = new TextureFrameSeriesAnimator.Animator({
41375
+ viewer,
41376
+ entity: cEntity,
41377
+ archiveUrl: extrusionArchive.url,
41378
+ frames: extrusionArchive.metadata.Frames,
41379
+ textureColorMask: style.textureColorMask,
41380
+ driveMaterial: false,
41381
+ produceValueCanvas: true
41382
+ });
41383
+ cEntity[EXTRUSION_ANIMATOR_KEY] = animator;
41384
+ const mask = style.textureColorMask;
41385
+ const surface = new exports.DisplacedSurfacePrimitive.Surface({
41386
+ extent,
41387
+ source: null,
41388
+ baseHeight: placement.baseHeight,
41389
+ followGround: placement.followGround,
41390
+ terrainProvider: viewer && viewer.terrainProvider,
41391
+ valueMin: extrusionArchive.metadata.ValueMin,
41392
+ valueMax: extrusionArchive.metadata.ValueMax,
41393
+ exaggeration: style.extrusionExaggeration,
41394
+ lowColor: mask ? BModels.Color.ColorFromStr(mask.minColor) : null,
41395
+ highColor: mask ? BModels.Color.ColorFromStr(mask.maxColor) : null
41396
+ });
41397
+ surface.BindAnimator(animator);
41398
+ viewer.scene.primitives.add(surface);
41399
+ cEntity[DISPLACED_SURFACE_KEY] = surface;
41400
+ }
41401
+ /**
41402
+ * Turns the polygon's altitude option into the two things the displaced surface needs:
41403
+ * - how high value zero sits.
41404
+ * - whether that is measured from the terrain or from the ellipsoid.
41405
+ * @param entity
41406
+ * @param heightRef
41407
+ */
41408
+ function surfacePlacement(entity, heightRef) {
41409
+ const rawAltitude = BModels.Entity.GetValue({
41410
+ entity,
41411
+ path: ["Bruce", "Location", "altitude"]
41412
+ });
41413
+ const altitude = rawAltitude ? EnsureNumber(rawAltitude) : 0;
41414
+ if (heightRef === Cesium.HeightReference.CLAMP_TO_GROUND) {
41415
+ return { baseHeight: 0, followGround: true };
41416
+ }
41417
+ if (heightRef === Cesium.HeightReference.RELATIVE_TO_GROUND) {
41418
+ return { baseHeight: altitude, followGround: true };
41419
+ }
41420
+ return { baseHeight: altitude, followGround: false };
41421
+ }
41422
+ /**
41423
+ * The archive's own extent when it recorded one, otherwise the polygon's own bounds so archives
41424
+ * generated before the extent was written still render somewhere sensible.
41425
+ * @param metadata
41426
+ * @param posses
41427
+ */
41428
+ function archiveExtent(metadata, posses) {
41429
+ if (metadata.West != null && metadata.East != null && metadata.South != null && metadata.North != null) {
41430
+ return { West: metadata.West, East: metadata.East, South: metadata.South, North: metadata.North };
41431
+ }
41432
+ if (!posses || posses.length === 0) {
41433
+ return null;
41434
+ }
41435
+ const rectangle = Cesium.Rectangle.fromCartesianArray(posses);
41436
+ return {
41437
+ West: Cesium.Math.toDegrees(rectangle.west),
41438
+ East: Cesium.Math.toDegrees(rectangle.east),
41439
+ South: Cesium.Math.toDegrees(rectangle.south),
41440
+ North: Cesium.Math.toDegrees(rectangle.north)
41441
+ };
41442
+ }
41443
+ /**
41444
+ * Tears down the displaced surface and its feeding animator, if this entity has them.
41445
+ * @param cEntity
41446
+ * @param viewer
41447
+ */
41448
+ function disposeDisplacedSurface(cEntity, viewer) {
41449
+ const surface = cEntity === null || cEntity === void 0 ? void 0 : cEntity[DISPLACED_SURFACE_KEY];
41450
+ if (surface) {
41451
+ const viewerAlive = viewer && (typeof viewer.isDestroyed !== "function" || !viewer.isDestroyed());
41452
+ if (viewerAlive && viewer.scene && viewer.scene.primitives) {
41453
+ // Removing from the collection destroys the primitive, releasing its meshes and texture.
41454
+ viewer.scene.primitives.remove(surface);
41455
+ }
41456
+ else if (!surface.isDestroyed()) {
41457
+ surface.destroy();
41458
+ }
41459
+ cEntity[DISPLACED_SURFACE_KEY] = null;
41460
+ }
41461
+ const animator = cEntity === null || cEntity === void 0 ? void 0 : cEntity[EXTRUSION_ANIMATOR_KEY];
41462
+ if (animator && !animator.IsDisposed()) {
41463
+ animator.Dispose();
41464
+ }
41465
+ if (cEntity) {
41466
+ cEntity[EXTRUSION_ANIMATOR_KEY] = null;
41467
+ }
41468
+ }
40528
41469
  /**
40529
41470
  * Reconciles cEntity's TextureFrameSeriesAnimator.Animator (if any) against this render's resolved texture.
40530
41471
  * Called once cEntity is fully created/updated and definitely has a `polygon` graphics.
@@ -40615,8 +41556,15 @@
40615
41556
  * @param params
40616
41557
  */
40617
41558
  async function resolveTexturedFill(params) {
40618
- const { api, style, entity, tags } = params;
40619
- const textureValue = style.texture;
41559
+ return resolveTextureValue({ ...params, textureValue: params.style.texture });
41560
+ }
41561
+ /**
41562
+ * Resolves either shape a texture setting can take (a calculated file id/URL, or a search condition)
41563
+ * into a tinted data URI or, when the resolved file is a frame archive, the archive itself.
41564
+ * @param params
41565
+ */
41566
+ async function resolveTextureValue(params) {
41567
+ const { api, style, entity, tags, textureValue } = params;
40620
41568
  if (textureValue && !Array.isArray(textureValue)) {
40621
41569
  return resolveTextureCondition({ api, condition: textureValue });
40622
41570
  }
@@ -41393,7 +42341,7 @@
41393
42341
  entity._dispose();
41394
42342
  entity._dispose = null;
41395
42343
  }
41396
- exports.EntityRenderEnginePolygon.DisposeTextureFrameSeriesAnimator(entity);
42344
+ exports.EntityRenderEnginePolygon.DisposeTextureFrameSeriesAnimator(entity, viewer);
41397
42345
  if (entity._parentEntity && !ignoreParent) {
41398
42346
  doRemove({
41399
42347
  viewer,
@@ -42141,7 +43089,7 @@
42141
43089
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
42142
43090
  })(exports.StyleUtils || (exports.StyleUtils = {}));
42143
43091
 
42144
- const VERSION = "7.1.2";
43092
+ const VERSION = "7.1.4";
42145
43093
  /**
42146
43094
  * Updates the environment instance used by bruce-cesium to one specified.
42147
43095
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.