bruce-cesium 7.1.3 → 7.1.5

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.
@@ -39622,15 +39622,68 @@
39622
39622
  return Boolean(data && data.generation && Array.isArray(data.generation.Frames) && data.generation.Frames.length > 0);
39623
39623
  }
39624
39624
  TextureFrameSeriesAnimator.IsFrameArchiveMetadata = IsFrameArchiveMetadata;
39625
+ // How far covered values bleed outward into uncovered texels before the GPU samples them.
39626
+ const VALUE_DILATE_PASSES = 2;
39627
+ /*
39628
+ * Bleeds covered values outward into uncovered texels, leaving alpha untouched.
39629
+ *
39630
+ * The GPU samples the value texture with linear filtering, so an uncovered texel's RGB still gets
39631
+ * averaged into the vertices next to it even though its alpha is zero. Land sits at one end of the
39632
+ * ramp, so without this the coastline grows a row of spikes exactly where the data stops. Masking the
39633
+ * baseline creates more of these edges, which is what makes this necessary rather than cosmetic.
39634
+ */
39635
+ function dilateValues(value, width, height, passes) {
39636
+ const texels = width * height;
39637
+ const filled = new Uint8Array(texels);
39638
+ for (let p = 0; p < texels; p++) {
39639
+ filled[p] = value[p * 4 + 3] !== 0 ? 1 : 0;
39640
+ }
39641
+ for (let pass = 0; pass < passes; pass++) {
39642
+ const source = value.slice();
39643
+ const wasFilled = filled.slice();
39644
+ for (let y = 0; y < height; y++) {
39645
+ for (let x = 0; x < width; x++) {
39646
+ const p = y * width + x;
39647
+ if (wasFilled[p]) {
39648
+ continue;
39649
+ }
39650
+ let sum = 0;
39651
+ let hits = 0;
39652
+ for (let dy = -1; dy <= 1; dy++) {
39653
+ for (let dx = -1; dx <= 1; dx++) {
39654
+ const nx = x + dx;
39655
+ const ny = y + dy;
39656
+ if (nx < 0 || ny < 0 || nx >= width || ny >= height || !wasFilled[ny * width + nx]) {
39657
+ continue;
39658
+ }
39659
+ sum += source[(ny * width + nx) * 4];
39660
+ hits++;
39661
+ }
39662
+ }
39663
+ if (hits > 0) {
39664
+ const v = Math.round(sum / hits);
39665
+ value[p * 4] = v;
39666
+ value[p * 4 + 1] = v;
39667
+ value[p * 4 + 2] = v;
39668
+ filled[p] = 1;
39669
+ }
39670
+ }
39671
+ }
39672
+ }
39673
+ }
39625
39674
  const DEFAULT_CROSSFADE_MS = 900;
39626
39675
  const DEFAULT_LOW_COLOR = { red: 255, green: 255, blue: 255, alpha: 0.12 };
39627
39676
  const DEFAULT_HIGH_COLOR = { red: 21, green: 96, blue: 196, alpha: 0.92 };
39628
39677
  class Animator {
39629
39678
  constructor(options) {
39630
- var _a;
39679
+ var _a, _b;
39631
39680
  this.removeOnTick = null;
39632
39681
  this.removeCrossfadeTick = null;
39633
39682
  this.disposed = false;
39683
+ // One alpha byte per texel, 1 meaning the texel is baseline. Fetched once, the mesh does not move.
39684
+ this.baselineFlags = null;
39685
+ this.baselineDims = null;
39686
+ this.baselineLoad = null;
39634
39687
  this.currentFrameIndex = -1;
39635
39688
  // Single-in-flight fetch queue: only one Range GET outstanding at a time, and the NEXT one
39636
39689
  // is chosen right before sending, this is what makes reprioritizing mid-scrub take effect
@@ -39639,6 +39692,8 @@
39639
39692
  this.pendingIndex = -1;
39640
39693
  this.displayedPixels = null;
39641
39694
  this.fadeFromPixels = null;
39695
+ this.displayedValuePixels = null;
39696
+ this.fadeFromValuePixels = null;
39642
39697
  this.fadeToIndex = -1;
39643
39698
  this.fadeToDims = null;
39644
39699
  this.fadeStartMs = null;
@@ -39647,6 +39702,8 @@
39647
39702
  // forcing Cesium to re-upload the texture only when something changed.
39648
39703
  this.pool = [document.createElement("canvas"), document.createElement("canvas")];
39649
39704
  this.poolIdx = 0;
39705
+ this.valueDims = null;
39706
+ this.presentVersion = 0;
39650
39707
  if (!options.entity.polygon) {
39651
39708
  throw new Error("TextureFrameSeriesAnimator requires an entity with polygon graphics.");
39652
39709
  }
@@ -39664,9 +39721,17 @@
39664
39721
  this.frameDates = this.frames.map((f) => Cesium.JulianDate.fromIso8601(f.Timestamp));
39665
39722
  this.frameCache = new Array(this.frames.length).fill(null);
39666
39723
  this.frameDims = new Array(this.frames.length).fill(null);
39724
+ this.valueCache = new Array(this.frames.length).fill(null);
39725
+ this.produceValueCanvas = Boolean(options.produceValueCanvas);
39726
+ this.baselineMaskEntry = options.baselineMask || null;
39727
+ this.maskBaseline = (_b = options.maskBaseline) !== null && _b !== void 0 ? _b : Boolean(options.baselineMask);
39728
+ this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
39729
+ this.driveMaterial = options.driveMaterial !== false;
39667
39730
  this.originalMaterial = options.entity.polygon.material;
39668
39731
  this.imageProperty = new Cesium.CallbackProperty(() => this.pool[this.poolIdx], false);
39669
- options.entity.polygon.material = new Cesium.ImageMaterialProperty({ image: this.imageProperty, transparent: true });
39732
+ if (this.driveMaterial) {
39733
+ options.entity.polygon.material = new Cesium.ImageMaterialProperty({ image: this.imageProperty, transparent: true });
39734
+ }
39670
39735
  this.removeCrossfadeTick = this.viewer.scene.postUpdate.addEventListener(() => this.tickCrossfade());
39671
39736
  this.removeOnTick = this.viewer.clock.onTick.addEventListener((clock) => this.onClockTick(clock.currentTime));
39672
39737
  this.onClockTick(this.viewer.clock.currentTime);
@@ -39688,7 +39753,7 @@
39688
39753
  this.removeCrossfadeTick();
39689
39754
  this.removeCrossfadeTick = null;
39690
39755
  }
39691
- if (this.entity && this.entity.polygon) {
39756
+ if (this.driveMaterial && this.entity && this.entity.polygon) {
39692
39757
  this.entity.polygon.material = this.originalMaterial;
39693
39758
  }
39694
39759
  }
@@ -39709,6 +39774,20 @@
39709
39774
  GetImageProperty() {
39710
39775
  return this.imageProperty;
39711
39776
  }
39777
+ /**
39778
+ * The untinted canvas carrying the presented frame's value in RGB and its coverage in alpha,
39779
+ * or null unless the instance was constructed with produceValueCanvas.
39780
+ */
39781
+ GetValueCanvas() {
39782
+ return this.valueDims ? this.valueCanvas : null;
39783
+ }
39784
+ /**
39785
+ * Increments every time a frame is presented, so a consumer holding the value canvas can tell
39786
+ * whether it needs re-uploading without diffing pixels or subscribing to anything.
39787
+ */
39788
+ GetPresentVersion() {
39789
+ return this.presentVersion;
39790
+ }
39712
39791
  onClockTick(currentTime) {
39713
39792
  if (this.disposed) {
39714
39793
  return;
@@ -39815,12 +39894,19 @@
39815
39894
  if (this.disposed) {
39816
39895
  return;
39817
39896
  }
39897
+ // Awaited before decoding rather than applied later, so no frame is ever cached unmasked and
39898
+ // then corrected once the mask lands.
39899
+ await this.ensureBaselineMask();
39900
+ if (this.disposed) {
39901
+ return;
39902
+ }
39818
39903
  const decoded = await this.decodeAndTint(buffer);
39819
39904
  if (this.disposed) {
39820
39905
  return;
39821
39906
  }
39822
39907
  this.frameCache[idx] = decoded.pixels;
39823
39908
  this.frameDims[idx] = { width: decoded.width, height: decoded.height };
39909
+ this.valueCache[idx] = decoded.valuePixels || null;
39824
39910
  if (idx === this.currentFrameIndex) {
39825
39911
  this.beginCrossfadeTo(idx);
39826
39912
  }
@@ -39844,11 +39930,90 @@
39844
39930
  const ctx = canvas.getContext("2d");
39845
39931
  ctx.drawImage(image, 0, 0);
39846
39932
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
39933
+ // Copied before tinting: the ramp is a lerp between two colours, so the value cannot be
39934
+ // recovered from the tinted pixels afterwards.
39935
+ const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
39847
39936
  ApplyGrayscaleColorMask(imageData, this.lowColor, this.highColor);
39848
- return { pixels: imageData.data, width: canvas.width, height: canvas.height };
39937
+ this.applyBaselineMask(imageData.data, valuePixels, canvas.width, canvas.height);
39938
+ if (valuePixels) {
39939
+ dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES);
39940
+ }
39941
+ return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
39942
+ }
39943
+ /**
39944
+ * Fetches and decodes the archive's static baseline mask, at most once.
39945
+ */
39946
+ ensureBaselineMask() {
39947
+ if (!this.maskBaseline || !this.baselineMaskEntry) {
39948
+ return Promise.resolve();
39949
+ }
39950
+ if (this.baselineLoad) {
39951
+ return this.baselineLoad;
39952
+ }
39953
+ const entry = this.baselineMaskEntry;
39954
+ const start = entry.ByteOffset;
39955
+ const end = entry.ByteOffset + entry.ByteLength - 1;
39956
+ this.baselineLoad = fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } })
39957
+ .then((response) => response.arrayBuffer())
39958
+ .then(async (buffer) => {
39959
+ if (this.disposed) {
39960
+ return;
39961
+ }
39962
+ const blob = new Blob([buffer], { type: "image/png" });
39963
+ const objectUrl = URL.createObjectURL(blob);
39964
+ let image;
39965
+ try {
39966
+ image = await loadImage(objectUrl);
39967
+ }
39968
+ finally {
39969
+ URL.revokeObjectURL(objectUrl);
39970
+ }
39971
+ const canvas = document.createElement("canvas");
39972
+ canvas.width = image.width;
39973
+ canvas.height = image.height;
39974
+ const ctx = canvas.getContext("2d");
39975
+ ctx.drawImage(image, 0, 0);
39976
+ const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
39977
+ const flags = new Uint8Array(canvas.width * canvas.height);
39978
+ for (let p = 0; p < flags.length; p++) {
39979
+ flags[p] = pixels[p * 4 + 3] >= 128 ? 1 : 0;
39980
+ }
39981
+ this.baselineFlags = flags;
39982
+ this.baselineDims = { width: canvas.width, height: canvas.height };
39983
+ })
39984
+ .catch((e) => {
39985
+ // A missing or unreadable mask must not stop the animation; it just means nothing is hidden.
39986
+ console.warn("TextureFrameSeriesAnimator: could not load the baseline mask, drawing it instead.", e);
39987
+ });
39988
+ return this.baselineLoad;
39989
+ }
39990
+ /**
39991
+ * Clears alpha on baseline texels, which is what makes hiding the dry/land sentinel a render choice.
39992
+ */
39993
+ applyBaselineMask(pixels, valuePixels, width, height) {
39994
+ const flags = this.baselineFlags;
39995
+ if (!flags || !this.baselineDims) {
39996
+ return;
39997
+ }
39998
+ // A mask from a different raster size cannot be indexed against this frame, and guessing a
39999
+ // resampling would silently hide the wrong ground.
40000
+ if (this.baselineDims.width !== width || this.baselineDims.height !== height) {
40001
+ return;
40002
+ }
40003
+ for (let p = 0; p < flags.length; p++) {
40004
+ if (flags[p]) {
40005
+ pixels[p * 4 + 3] = 0;
40006
+ if (valuePixels) {
40007
+ valuePixels[p * 4 + 3] = 0;
40008
+ }
40009
+ }
40010
+ }
39849
40011
  }
39850
40012
  beginCrossfadeTo(idx) {
39851
40013
  const to = this.frameCache[idx];
40014
+ this.fadeFromValuePixels = this.displayedValuePixels === null && this.valueCache[idx]
40015
+ ? new Uint8ClampedArray(this.valueCache[idx].length)
40016
+ : this.displayedValuePixels;
39852
40017
  this.fadeFromPixels = this.displayedPixels === null ? new Uint8ClampedArray(to.length) : this.displayedPixels;
39853
40018
  this.fadeToIndex = idx;
39854
40019
  this.fadeToDims = this.frameDims[idx];
@@ -39867,13 +40032,28 @@
39867
40032
  for (let i = 0; i < n; i++) {
39868
40033
  blended[i] = from[i] + (to[i] - from[i]) * eased;
39869
40034
  }
39870
- this.presentPixels(blended, this.fadeToDims);
40035
+ let blendedValue = null;
40036
+ const toValue = this.valueCache[this.fadeToIndex];
40037
+ if (toValue) {
40038
+ const fromValue = this.fadeFromValuePixels;
40039
+ blendedValue = new Uint8ClampedArray(toValue.length);
40040
+ if (fromValue && fromValue.length === toValue.length) {
40041
+ for (let i = 0; i < toValue.length; i++) {
40042
+ blendedValue[i] = fromValue[i] + (toValue[i] - fromValue[i]) * eased;
40043
+ }
40044
+ }
40045
+ else {
40046
+ blendedValue.set(toValue);
40047
+ }
40048
+ }
40049
+ this.presentPixels(blended, this.fadeToDims, blendedValue);
39871
40050
  if (t >= 1) {
39872
40051
  this.fadeStartMs = null;
39873
40052
  this.fadeFromPixels = null;
40053
+ this.fadeFromValuePixels = null;
39874
40054
  }
39875
40055
  }
39876
- presentPixels(pixels, dims) {
40056
+ presentPixels(pixels, dims, valuePixels) {
39877
40057
  this.poolIdx = 1 - this.poolIdx;
39878
40058
  const target = this.pool[this.poolIdx];
39879
40059
  target.width = dims.width;
@@ -39883,6 +40063,17 @@
39883
40063
  imgData.data.set(pixels);
39884
40064
  ctx.putImageData(imgData, 0, 0);
39885
40065
  this.displayedPixels = pixels;
40066
+ if (this.valueCanvas && valuePixels) {
40067
+ this.valueCanvas.width = dims.width;
40068
+ this.valueCanvas.height = dims.height;
40069
+ const valueCtx = this.valueCanvas.getContext("2d");
40070
+ const valueImgData = valueCtx.createImageData(dims.width, dims.height);
40071
+ valueImgData.data.set(valuePixels);
40072
+ valueCtx.putImageData(valueImgData, 0, 0);
40073
+ this.displayedValuePixels = valuePixels;
40074
+ this.valueDims = dims;
40075
+ }
40076
+ this.presentVersion++;
39886
40077
  }
39887
40078
  }
39888
40079
  TextureFrameSeriesAnimator.Animator = Animator;
@@ -39894,10 +40085,737 @@
39894
40085
  }
39895
40086
  })(TextureFrameSeriesAnimator || (TextureFrameSeriesAnimator = {}));
39896
40087
 
40088
+ (function (DisplacedSurfacePrimitive) {
40089
+ // Grid densities a tile can draw at, coarsest first.
40090
+ const TIERS = [32, 64, 128, 192];
40091
+ // Tiles per side the extent is cut into.
40092
+ const TILES_PER_SIDE = 6;
40093
+ // Screen pixels one grid cell should cover. Lower means finer meshes sooner.
40094
+ const TARGET_CELL_PIXELS = 12;
40095
+ // Fraction of its distance to the surface the camera must move before densities are reconsidered.
40096
+ const MOVEMENT_THRESHOLD = 0.12;
40097
+ const RESELECT_INTERVAL_MS = 400;
40098
+ // Skirt depth as a fraction of the displaced range. The sheet is translucent, so an oversized
40099
+ // skirt blends through it and paints a seam down every tile boundary.
40100
+ const SKIRT_FRACTION = 0.015;
40101
+ // Alpha below which a texel counts as no-data and is not drawn at all.
40102
+ const COVERAGE_CUTOFF = 0.35;
40103
+ // With no exaggeration stated, the full value range is drawn as this fraction of the extent's shorter side.
40104
+ const AUTO_RELIEF_FRACTION = 0.02;
40105
+ // Terrain is sampled on this grid and interpolated between samples.
40106
+ // Per-texel sampling would be hundreds of thousands of lookups for a surface whose own data is far coarser than that.
40107
+ const GROUND_SAMPLES_PER_SIDE = 64;
40108
+ // Terrain detail level the samples are taken at.
40109
+ // Most-detailed would fetch every tile under a parcel that can be a hundred kilometres across.
40110
+ const GROUND_SAMPLE_LEVEL = 11;
40111
+ // Height window the packed ground texture covers, wide enough for any terrain on Earth.
40112
+ const GROUND_MIN_METRES = -1000;
40113
+ const GROUND_MAX_METRES = 9000;
40114
+ const DEFAULT_LOW_COLOR = { red: 255, green: 255, blue: 255, alpha: 0.12 };
40115
+ const DEFAULT_HIGH_COLOR = { red: 21, green: 96, blue: 196, alpha: 0.92 };
40116
+ const VERTEX_SHADER_GLSL300 = `
40117
+ in vec3 position3DHigh;
40118
+ in vec3 position3DLow;
40119
+ in vec3 normal;
40120
+ in vec2 st;
40121
+ in float skirt;
40122
+
40123
+ uniform sampler2D u_valueTexture;
40124
+ uniform sampler2D u_groundTexture;
40125
+ uniform float u_valueMin;
40126
+ uniform float u_valueRange;
40127
+ uniform float u_exaggeration;
40128
+ uniform float u_baseHeight;
40129
+ uniform float u_skirtDepth;
40130
+ uniform float u_groundWeight;
40131
+ uniform vec2 u_groundRange;
40132
+ uniform vec2 u_groundSize;
40133
+
40134
+ out vec2 v_st;
40135
+ out float v_value;
40136
+ out float v_coverage;
40137
+
40138
+ float decodeGround(vec2 uv) {
40139
+ // Channels arrive normalised to 0..1, so they go back to bytes before being reassembled.
40140
+ vec3 packed = texture(u_groundTexture, uv).rgb * 255.0;
40141
+ float normalised = dot(packed, vec3(65536.0, 256.0, 1.0)) / 16777215.0;
40142
+ return u_groundRange.x + normalised * (u_groundRange.y - u_groundRange.x);
40143
+ }
40144
+
40145
+ /*
40146
+ * Bilinear by hand, because the height is packed across three channels and hardware filtering would
40147
+ * blend the packing rather than the heights, turning a smooth slope into noise at every texel edge.
40148
+ */
40149
+ float sampleGround(vec2 uv) {
40150
+ vec2 texel = 1.0 / u_groundSize;
40151
+ vec2 coord = uv * u_groundSize - 0.5;
40152
+ vec2 base = floor(coord);
40153
+ vec2 f = coord - base;
40154
+ vec2 origin = (base + 0.5) * texel;
40155
+ float h00 = decodeGround(origin);
40156
+ float h10 = decodeGround(origin + vec2(texel.x, 0.0));
40157
+ float h01 = decodeGround(origin + vec2(0.0, texel.y));
40158
+ float h11 = decodeGround(origin + texel);
40159
+ return mix(mix(h00, h10, f.x), mix(h01, h11, f.x), f.y);
40160
+ }
40161
+
40162
+ void main() {
40163
+ vec4 texel = texture(u_valueTexture, st);
40164
+ v_st = st;
40165
+ v_value = texel.r;
40166
+ v_coverage = texel.a;
40167
+
40168
+ float metres = u_valueMin + texel.r * u_valueRange;
40169
+
40170
+ // Value zero sits at the polygon's own altitude, plus the terrain underneath when the polygon
40171
+ // is following the ground rather than absolutely positioned.
40172
+ float ground = sampleGround(st) * u_groundWeight;
40173
+ float displacement = u_baseHeight + ground + metres * u_exaggeration * texel.a - skirt * u_skirtDepth;
40174
+
40175
+ // Added to the LOW half of the encoded position: a metre-scale offset added to the high half
40176
+ // would be lost to float32 rounding at an earth radius.
40177
+ vec4 p = czm_translateRelativeToEye(position3DHigh, position3DLow + normal * displacement);
40178
+ gl_Position = czm_modelViewProjectionRelativeToEye * p;
40179
+ }
40180
+ `;
40181
+ const FRAGMENT_SHADER_GLSL300 = `
40182
+ uniform sampler2D u_valueTexture;
40183
+ uniform vec4 u_lowColor;
40184
+ uniform vec4 u_highColor;
40185
+ uniform float u_coverageCutoff;
40186
+ uniform vec2 u_texelSize;
40187
+ uniform float u_valueRange;
40188
+ uniform float u_exaggeration;
40189
+ uniform float u_metresPerTexel;
40190
+
40191
+ in vec2 v_st;
40192
+ in float v_value;
40193
+ in float v_coverage;
40194
+
40195
+ void main() {
40196
+ if (v_coverage < u_coverageCutoff) {
40197
+ discard;
40198
+ }
40199
+ vec4 color = mix(u_lowColor, u_highColor, v_value);
40200
+
40201
+ // Relief shading from the value gradient, so the displacement reads at a distance without
40202
+ // recomputing vertex normals every time the texture changes.
40203
+ float left = texture(u_valueTexture, v_st - vec2(u_texelSize.x, 0.0)).r;
40204
+ float right = texture(u_valueTexture, v_st + vec2(u_texelSize.x, 0.0)).r;
40205
+ float down = texture(u_valueTexture, v_st - vec2(0.0, u_texelSize.y)).r;
40206
+ float up = texture(u_valueTexture, v_st + vec2(0.0, u_texelSize.y)).r;
40207
+ float scale = u_valueRange * u_exaggeration;
40208
+ vec3 n = normalize(vec3((left - right) * scale, (down - up) * scale, 2.0 * u_metresPerTexel));
40209
+ float lambert = clamp(dot(n, normalize(vec3(-0.5, -0.6, 0.62))), 0.0, 1.0);
40210
+ color.rgb *= 0.45 + 0.9 * lambert;
40211
+
40212
+ out_FragColor = vec4(color.rgb, color.a * v_coverage);
40213
+ }
40214
+ `;
40215
+ /*
40216
+ * Rewrites a GLSL 300 es source into the GLSL 100 dialect older Cesium builds author in.
40217
+ */
40218
+ function toGlsl100(source, isFragment) {
40219
+ let result = source
40220
+ .replace(/\btexture\s*\(/g, "texture2D(")
40221
+ .replace(/^in\s+/gm, isFragment ? "varying " : "attribute ")
40222
+ .replace(/^out\s+/gm, "varying ");
40223
+ if (isFragment) {
40224
+ result = result.replace(/\bout_FragColor\b/g, "gl_FragColor");
40225
+ }
40226
+ return result;
40227
+ }
40228
+ let _glsl300Support = null;
40229
+ /*
40230
+ * Works out which GLSL dialect this host's Cesium authors shaders in.
40231
+ */
40232
+ function usesGlsl300(context) {
40233
+ if (_glsl300Support !== null) {
40234
+ return _glsl300Support;
40235
+ }
40236
+ let modernizes = false;
40237
+ try {
40238
+ const probe = new Cesium.ShaderSource({
40239
+ sources: ["varying float v_probe;\nvoid main() { gl_Position = vec4(0.0); }"],
40240
+ includeBuiltIns: false
40241
+ });
40242
+ const combined = probe.createCombinedVertexShader(context);
40243
+ modernizes = combined.indexOf("varying") < 0;
40244
+ }
40245
+ catch (e) {
40246
+ // No probe means no evidence the host rewrites anything, so trust the context alone.
40247
+ modernizes = false;
40248
+ }
40249
+ _glsl300Support = Boolean(context.webgl2) && !modernizes;
40250
+ return _glsl300Support;
40251
+ }
40252
+ /*
40253
+ * Places grid node (fx, fy) on the ellipsoid, stepping the way the raster was sampled.
40254
+ * A raster sampled at lon/lat has to be walked in degrees; one sampled linearly across a projected CRS has to be walked in tangent-plane metres.
40255
+ */
40256
+ function makePlacer(extent, pixelPlacement) {
40257
+ if (pixelPlacement === "lonlat") {
40258
+ return (fx, fy) => Cesium.Cartesian3.fromDegrees(Cesium.Math.lerp(extent.West, extent.East, fx), Cesium.Math.lerp(extent.North, extent.South, fy), 0);
40259
+ }
40260
+ const ellipsoid = Cesium.Ellipsoid.WGS84;
40261
+ const centre = Cesium.Cartesian3.fromDegrees((extent.West + extent.East) / 2, (extent.South + extent.North) / 2, 0);
40262
+ const frame = Cesium.Transforms.eastNorthUpToFixedFrame(centre);
40263
+ const toLocal = Cesium.Matrix4.inverse(frame, new Cesium.Matrix4());
40264
+ const southWest = Cesium.Matrix4.multiplyByPoint(toLocal, Cesium.Cartesian3.fromDegrees(extent.West, extent.South, 0), new Cesium.Cartesian3());
40265
+ const northEast = Cesium.Matrix4.multiplyByPoint(toLocal, Cesium.Cartesian3.fromDegrees(extent.East, extent.North, 0), new Cesium.Cartesian3());
40266
+ return (fx, fy) => {
40267
+ const local = new Cesium.Cartesian3(Cesium.Math.lerp(southWest.x, northEast.x, fx), Cesium.Math.lerp(northEast.y, southWest.y, fy), 0);
40268
+ const world = Cesium.Matrix4.multiplyByPoint(frame, local, new Cesium.Cartesian3());
40269
+ return ellipsoid.scaleToGeodeticSurface(world, world);
40270
+ };
40271
+ }
40272
+ /*
40273
+ * Builds one tile's grid, optionally plus a duplicated perimeter ring the shader pushes downwards to
40274
+ * cover the crack where a neighbouring tile at a different density interpolates the shared edge.
40275
+ */
40276
+ function buildGeometry(extent, resolution, patch, pixelPlacement, withSkirt = false) {
40277
+ const cols = resolution;
40278
+ const rows = resolution;
40279
+ const gridCount = (cols + 1) * (rows + 1);
40280
+ const perimeterCount = withSkirt ? 2 * (cols + 1) + 2 * (rows + 1) - 4 : 0;
40281
+ const count = gridCount + perimeterCount;
40282
+ const positions = new Float64Array(count * 3);
40283
+ const normals = new Float32Array(count * 3);
40284
+ const sts = new Float32Array(count * 2);
40285
+ const skirts = new Float32Array(count);
40286
+ const ellipsoid = Cesium.Ellipsoid.WGS84;
40287
+ const place = makePlacer(extent, pixelPlacement);
40288
+ let v = 0;
40289
+ for (let row = 0; row <= rows; row++) {
40290
+ // Texture row 0 is the north edge, so t runs opposite to northing.
40291
+ const fy = Cesium.Math.lerp(patch.v0, patch.v1, row / rows);
40292
+ for (let col = 0; col <= cols; col++) {
40293
+ const fx = Cesium.Math.lerp(patch.u0, patch.u1, col / cols);
40294
+ const position = place(fx, fy);
40295
+ positions[v * 3] = position.x;
40296
+ positions[v * 3 + 1] = position.y;
40297
+ positions[v * 3 + 2] = position.z;
40298
+ const up = ellipsoid.geodeticSurfaceNormal(position, new Cesium.Cartesian3());
40299
+ normals[v * 3] = up.x;
40300
+ normals[v * 3 + 1] = up.y;
40301
+ normals[v * 3 + 2] = up.z;
40302
+ sts[v * 2] = fx;
40303
+ sts[v * 2 + 1] = fy;
40304
+ v++;
40305
+ }
40306
+ }
40307
+ const indices = new Uint32Array(cols * rows * 6 + perimeterCount * 6);
40308
+ let i = 0;
40309
+ for (let row = 0; row < rows; row++) {
40310
+ for (let col = 0; col < cols; col++) {
40311
+ const topLeft = row * (cols + 1) + col;
40312
+ const topRight = topLeft + 1;
40313
+ const bottomLeft = topLeft + (cols + 1);
40314
+ const bottomRight = bottomLeft + 1;
40315
+ indices[i++] = topLeft;
40316
+ indices[i++] = bottomLeft;
40317
+ indices[i++] = topRight;
40318
+ indices[i++] = topRight;
40319
+ indices[i++] = bottomLeft;
40320
+ indices[i++] = bottomRight;
40321
+ }
40322
+ }
40323
+ // Perimeter walked as one closed loop, so consecutive entries are always neighbours.
40324
+ const loop = [];
40325
+ for (let col = 0; col <= cols; col++) {
40326
+ loop.push(col);
40327
+ }
40328
+ for (let row = 1; row <= rows; row++) {
40329
+ loop.push(row * (cols + 1) + cols);
40330
+ }
40331
+ for (let col = cols - 1; col >= 0; col--) {
40332
+ loop.push(rows * (cols + 1) + col);
40333
+ }
40334
+ for (let row = rows - 1; row >= 1; row--) {
40335
+ loop.push(row * (cols + 1));
40336
+ }
40337
+ const skirtStart = gridCount;
40338
+ if (withSkirt) {
40339
+ loop.forEach((gridIndex, k) => {
40340
+ const target = skirtStart + k;
40341
+ positions[target * 3] = positions[gridIndex * 3];
40342
+ positions[target * 3 + 1] = positions[gridIndex * 3 + 1];
40343
+ positions[target * 3 + 2] = positions[gridIndex * 3 + 2];
40344
+ normals[target * 3] = normals[gridIndex * 3];
40345
+ normals[target * 3 + 1] = normals[gridIndex * 3 + 1];
40346
+ normals[target * 3 + 2] = normals[gridIndex * 3 + 2];
40347
+ sts[target * 2] = sts[gridIndex * 2];
40348
+ sts[target * 2 + 1] = sts[gridIndex * 2 + 1];
40349
+ skirts[target] = 1;
40350
+ });
40351
+ for (let k = 0; k < loop.length; k++) {
40352
+ const nextK = (k + 1) % loop.length;
40353
+ indices[i++] = loop[k];
40354
+ indices[i++] = skirtStart + k;
40355
+ indices[i++] = loop[nextK];
40356
+ indices[i++] = loop[nextK];
40357
+ indices[i++] = skirtStart + k;
40358
+ indices[i++] = skirtStart + nextK;
40359
+ }
40360
+ }
40361
+ const geometry = new Cesium.Geometry({
40362
+ attributes: {
40363
+ position: new Cesium.GeometryAttribute({
40364
+ componentDatatype: Cesium.ComponentDatatype.DOUBLE,
40365
+ componentsPerAttribute: 3,
40366
+ values: positions
40367
+ }),
40368
+ normal: new Cesium.GeometryAttribute({
40369
+ componentDatatype: Cesium.ComponentDatatype.FLOAT,
40370
+ componentsPerAttribute: 3,
40371
+ values: normals
40372
+ }),
40373
+ st: new Cesium.GeometryAttribute({
40374
+ componentDatatype: Cesium.ComponentDatatype.FLOAT,
40375
+ componentsPerAttribute: 2,
40376
+ values: sts
40377
+ }),
40378
+ skirt: new Cesium.GeometryAttribute({
40379
+ componentDatatype: Cesium.ComponentDatatype.FLOAT,
40380
+ componentsPerAttribute: 1,
40381
+ values: skirts
40382
+ })
40383
+ },
40384
+ indices: indices,
40385
+ primitiveType: Cesium.PrimitiveType.TRIANGLES,
40386
+ boundingSphere: Cesium.BoundingSphere.fromVertices(Array.prototype.slice.call(positions))
40387
+ });
40388
+ // Splits position into the high/low pair Cesium's relative-to-eye shaders expect.
40389
+ Cesium.GeometryPipeline.encodeAttribute(geometry, "position", "position3DHigh", "position3DLow");
40390
+ return geometry;
40391
+ }
40392
+ class Surface {
40393
+ constructor(options, hasCoverage) {
40394
+ var _a, _b, _c, _d;
40395
+ this.show = true;
40396
+ this.valueSource = null;
40397
+ this.lastPresentVersion = -1;
40398
+ this.texture = null;
40399
+ this.textureDirty = true;
40400
+ this.shaderProgram = null;
40401
+ this.renderState = null;
40402
+ this.destroyed = false;
40403
+ this.lastEyePosition = null;
40404
+ this.lastSelectionMs = -Infinity;
40405
+ this.selectionDirty = true;
40406
+ this.groundTexture = null;
40407
+ this.groundPixels = null;
40408
+ this.groundRequested = false;
40409
+ this.followGround = Boolean(options.followGround);
40410
+ this.terrainProvider = options.terrainProvider || null;
40411
+ this.extent = options.extent;
40412
+ this.source = options.source || null;
40413
+ this.valueMin = (_a = options.valueMin) !== null && _a !== void 0 ? _a : 0;
40414
+ this.valueMax = (_b = options.valueMax) !== null && _b !== void 0 ? _b : 1;
40415
+ this.exaggeration = (_c = options.exaggeration) !== null && _c !== void 0 ? _c : autoExaggeration(this.extent, this.valueMin, this.valueMax);
40416
+ this.baseHeight = (_d = options.baseHeight) !== null && _d !== void 0 ? _d : 0;
40417
+ this.pixelPlacement = options.pixelPlacement;
40418
+ this.tileSkirts = Boolean(options.tileSkirts);
40419
+ this.lowColor = toCesiumColor(options.lowColor || DEFAULT_LOW_COLOR);
40420
+ this.highColor = toCesiumColor(options.highColor || DEFAULT_HIGH_COLOR);
40421
+ this.tiles = buildTiles(hasCoverage);
40422
+ }
40423
+ GetFollowsGround() {
40424
+ return this.followGround;
40425
+ }
40426
+ GetPixelPlacement() {
40427
+ return this.pixelPlacement;
40428
+ }
40429
+ GetBaseHeight() {
40430
+ return this.baseHeight;
40431
+ }
40432
+ GetExaggeration() {
40433
+ return this.exaggeration;
40434
+ }
40435
+ GetExtent() {
40436
+ return this.extent;
40437
+ }
40438
+ SetExaggeration(exaggeration) {
40439
+ this.exaggeration = exaggeration;
40440
+ }
40441
+ SetBaseHeight(baseHeight) {
40442
+ this.baseHeight = baseHeight;
40443
+ }
40444
+ /**
40445
+ * Feeds the surface from an animated frame series.
40446
+ * Height and colour advance with the clock without the caller having to push each frame in.
40447
+ */
40448
+ BindAnimator(source) {
40449
+ this.valueSource = source;
40450
+ }
40451
+ /**
40452
+ * Tells the surface its source canvas has been redrawn and the texture needs re-uploading.
40453
+ */
40454
+ MarkSourceDirty() {
40455
+ this.textureDirty = true;
40456
+ }
40457
+ update(frameState) {
40458
+ if (!this.show || this.destroyed || this.tiles.length === 0) {
40459
+ return;
40460
+ }
40461
+ const context = frameState.context;
40462
+ if (this.valueSource) {
40463
+ const canvas = this.valueSource.GetValueCanvas();
40464
+ if (!canvas) {
40465
+ // No frame decoded yet, so there is nothing to displace against.
40466
+ return;
40467
+ }
40468
+ const version = this.valueSource.GetPresentVersion();
40469
+ if (canvas !== this.source || version !== this.lastPresentVersion) {
40470
+ this.source = canvas;
40471
+ this.lastPresentVersion = version;
40472
+ this.textureDirty = true;
40473
+ }
40474
+ }
40475
+ if (!this.source || !this.source.width || !this.source.height) {
40476
+ return;
40477
+ }
40478
+ this.syncTexture(context);
40479
+ this.requestGround();
40480
+ this.syncGroundTexture(context);
40481
+ // A tile needs one mesh before it has a bounding sphere to measure against.
40482
+ for (const tile of this.tiles) {
40483
+ if (!tile.centre) {
40484
+ this.ensureMesh(context, tile, TIERS[0]);
40485
+ }
40486
+ }
40487
+ const nowMs = now();
40488
+ if (this.shouldReselect(frameState, nowMs)) {
40489
+ this.selectTiers(frameState);
40490
+ this.lastSelectionMs = nowMs;
40491
+ this.lastEyePosition = Cesium.Cartesian3.clone(frameState.camera.positionWC, this.lastEyePosition);
40492
+ this.selectionDirty = false;
40493
+ }
40494
+ for (const tile of this.tiles) {
40495
+ const mesh = this.ensureMesh(context, tile, tile.tier);
40496
+ frameState.commandList.push(this.commandFor(context, tile, mesh));
40497
+ }
40498
+ }
40499
+ isDestroyed() {
40500
+ return this.destroyed;
40501
+ }
40502
+ destroy() {
40503
+ this.destroyed = true;
40504
+ for (const tile of this.tiles) {
40505
+ tile.meshes.forEach((mesh) => mesh.vertexArray.destroy());
40506
+ tile.meshes.clear();
40507
+ tile.command = null;
40508
+ }
40509
+ if (this.shaderProgram) {
40510
+ this.shaderProgram.destroy();
40511
+ this.shaderProgram = null;
40512
+ }
40513
+ if (this.texture) {
40514
+ this.texture.destroy();
40515
+ this.texture = null;
40516
+ }
40517
+ if (this.groundTexture) {
40518
+ this.groundTexture.destroy();
40519
+ this.groundTexture = null;
40520
+ }
40521
+ return undefined;
40522
+ }
40523
+ /*
40524
+ * Samples terrain across the extent once and packs it into a texture the vertex shader reads.
40525
+ *
40526
+ * A texture rather than heights baked into the meshes, because the samples arrive
40527
+ * asynchronously and every cached tile mesh would otherwise have to be rebuilt when they land.
40528
+ */
40529
+ requestGround() {
40530
+ if (this.groundRequested || !this.followGround) {
40531
+ return;
40532
+ }
40533
+ this.groundRequested = true;
40534
+ const sampler = Cesium.sampleTerrain;
40535
+ if (!this.terrainProvider || typeof sampler !== "function") {
40536
+ return;
40537
+ }
40538
+ const positions = [];
40539
+ for (let row = 0; row < GROUND_SAMPLES_PER_SIDE; row++) {
40540
+ const fy = row / (GROUND_SAMPLES_PER_SIDE - 1);
40541
+ const latitude = Cesium.Math.lerp(this.extent.North, this.extent.South, fy);
40542
+ for (let col = 0; col < GROUND_SAMPLES_PER_SIDE; col++) {
40543
+ const fx = col / (GROUND_SAMPLES_PER_SIDE - 1);
40544
+ const longitude = Cesium.Math.lerp(this.extent.West, this.extent.East, fx);
40545
+ positions.push(Cesium.Cartographic.fromDegrees(longitude, latitude));
40546
+ }
40547
+ }
40548
+ Promise.resolve(sampler(this.terrainProvider, GROUND_SAMPLE_LEVEL, positions))
40549
+ .then((sampled) => {
40550
+ if (this.destroyed || !sampled) {
40551
+ return;
40552
+ }
40553
+ const pixels = new Uint8Array(GROUND_SAMPLES_PER_SIDE * GROUND_SAMPLES_PER_SIDE * 4);
40554
+ const span = GROUND_MAX_METRES - GROUND_MIN_METRES;
40555
+ for (let i = 0; i < sampled.length; i++) {
40556
+ const height = sampled[i] && isFinite(sampled[i].height) ? sampled[i].height : 0;
40557
+ const clamped = Math.min(GROUND_MAX_METRES, Math.max(GROUND_MIN_METRES, height));
40558
+ const packed = Math.round(((clamped - GROUND_MIN_METRES) / span) * 16777215);
40559
+ pixels[i * 4] = (packed >> 16) & 255;
40560
+ pixels[i * 4 + 1] = (packed >> 8) & 255;
40561
+ pixels[i * 4 + 2] = packed & 255;
40562
+ pixels[i * 4 + 3] = 255;
40563
+ }
40564
+ this.groundPixels = pixels;
40565
+ })
40566
+ .catch(() => {
40567
+ // Terrain that cannot be sampled leaves the surface on the ellipsoid, which is
40568
+ // where a clamped polygon sat before this existed.
40569
+ });
40570
+ }
40571
+ /*
40572
+ * Uploads whatever ground samples exist, and a flat stand-in until they arrive, so the
40573
+ * shader always has a texture bound whether or not this surface follows the ground.
40574
+ */
40575
+ syncGroundTexture(context) {
40576
+ if (this.groundTexture && (!this.groundPixels || this.groundTexture.width === GROUND_SAMPLES_PER_SIDE)) {
40577
+ return;
40578
+ }
40579
+ if (this.groundTexture) {
40580
+ this.groundTexture.destroy();
40581
+ }
40582
+ const zero = Math.round(((0 - GROUND_MIN_METRES) / (GROUND_MAX_METRES - GROUND_MIN_METRES)) * 16777215);
40583
+ const size = this.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1;
40584
+ const arrayBufferView = this.groundPixels
40585
+ || new Uint8Array([(zero >> 16) & 255, (zero >> 8) & 255, zero & 255, 255]);
40586
+ this.groundTexture = new Cesium.Texture({
40587
+ context,
40588
+ pixelFormat: Cesium.PixelFormat.RGBA,
40589
+ source: { width: size, height: size, arrayBufferView },
40590
+ flipY: false,
40591
+ sampler: new Cesium.Sampler({
40592
+ wrapS: Cesium.TextureWrap.CLAMP_TO_EDGE,
40593
+ wrapT: Cesium.TextureWrap.CLAMP_TO_EDGE,
40594
+ // Nearest, because the shader interpolates decoded heights itself.
40595
+ minificationFilter: Cesium.TextureMinificationFilter.NEAREST,
40596
+ magnificationFilter: Cesium.TextureMagnificationFilter.NEAREST
40597
+ })
40598
+ });
40599
+ }
40600
+ /*
40601
+ * Ground distance between neighbouring texels, so the shading normal is built from a real
40602
+ * slope rather than a made up one.
40603
+ */
40604
+ metresPerTexel() {
40605
+ const southWest = Cesium.Cartesian3.fromDegrees(this.extent.West, this.extent.South, 0);
40606
+ const southEast = Cesium.Cartesian3.fromDegrees(this.extent.East, this.extent.South, 0);
40607
+ const width = Cesium.Cartesian3.distance(southWest, southEast);
40608
+ return Math.max(1, width / Math.max(1, this.source ? this.source.width : 1));
40609
+ }
40610
+ syncTexture(context) {
40611
+ const sizeChanged = this.texture
40612
+ && (this.texture.width !== this.source.width || this.texture.height !== this.source.height);
40613
+ if (!this.texture || sizeChanged) {
40614
+ if (this.texture) {
40615
+ this.texture.destroy();
40616
+ }
40617
+ this.texture = new Cesium.Texture({
40618
+ context,
40619
+ source: this.source,
40620
+ // Cesium flips canvas rows on upload by default, which would put the archive's
40621
+ // row 0 (its north edge) at the south edge and mirror the whole surface.
40622
+ flipY: false,
40623
+ sampler: new Cesium.Sampler({
40624
+ wrapS: Cesium.TextureWrap.CLAMP_TO_EDGE,
40625
+ wrapT: Cesium.TextureWrap.CLAMP_TO_EDGE,
40626
+ minificationFilter: Cesium.TextureMinificationFilter.LINEAR,
40627
+ magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR
40628
+ })
40629
+ });
40630
+ this.textureDirty = false;
40631
+ return;
40632
+ }
40633
+ if (this.textureDirty) {
40634
+ this.texture.copyFrom({ source: this.source });
40635
+ this.textureDirty = false;
40636
+ }
40637
+ }
40638
+ ensureMesh(context, tile, tier) {
40639
+ const cached = tile.meshes.get(tier);
40640
+ if (cached) {
40641
+ return cached;
40642
+ }
40643
+ const geometry = buildGeometry(this.extent, tier, tile.patch, this.pixelPlacement, this.tileSkirts);
40644
+ const attributeLocations = Cesium.GeometryPipeline.createAttributeLocations(geometry);
40645
+ const mesh = {
40646
+ attributeLocations,
40647
+ vertexArray: Cesium.VertexArray.fromGeometry({
40648
+ context,
40649
+ geometry,
40650
+ attributeLocations,
40651
+ bufferUsage: Cesium.BufferUsage.STATIC_DRAW
40652
+ }),
40653
+ boundingSphere: geometry.boundingSphere
40654
+ };
40655
+ tile.meshes.set(tier, mesh);
40656
+ tile.centre = geometry.boundingSphere.center;
40657
+ tile.radius = geometry.boundingSphere.radius;
40658
+ return mesh;
40659
+ }
40660
+ /*
40661
+ * Picks each tile's density from how much screen it covers.
40662
+ */
40663
+ selectTiers(frameState) {
40664
+ const camera = frameState.camera;
40665
+ const fovy = camera.frustum && camera.frustum.fovy ? camera.frustum.fovy : Cesium.Math.PI_OVER_THREE;
40666
+ const pixelsPerRadian = frameState.context.drawingBufferHeight / fovy;
40667
+ for (const tile of this.tiles) {
40668
+ if (!tile.centre) {
40669
+ tile.tier = TIERS[0];
40670
+ continue;
40671
+ }
40672
+ const distance = Math.max(1, Cesium.Cartesian3.distance(camera.positionWC, tile.centre) - tile.radius);
40673
+ const screenPixels = (2 * tile.radius / distance) * pixelsPerRadian;
40674
+ const wanted = screenPixels / TARGET_CELL_PIXELS;
40675
+ let tier = TIERS[0];
40676
+ for (const candidate of TIERS) {
40677
+ if (candidate <= wanted) {
40678
+ tier = candidate;
40679
+ }
40680
+ }
40681
+ tile.tier = tier;
40682
+ }
40683
+ }
40684
+ shouldReselect(frameState, nowMs) {
40685
+ if (this.selectionDirty) {
40686
+ return true;
40687
+ }
40688
+ if (nowMs - this.lastSelectionMs < RESELECT_INTERVAL_MS) {
40689
+ return false;
40690
+ }
40691
+ const eye = frameState.camera.positionWC;
40692
+ if (!this.lastEyePosition) {
40693
+ return true;
40694
+ }
40695
+ const moved = Cesium.Cartesian3.distance(eye, this.lastEyePosition);
40696
+ const anchor = this.tiles[0].centre;
40697
+ const reference = anchor ? Math.max(1, Cesium.Cartesian3.distance(eye, anchor)) : 1;
40698
+ return moved / reference > MOVEMENT_THRESHOLD;
40699
+ }
40700
+ commandFor(context, tile, mesh) {
40701
+ if (!this.shaderProgram) {
40702
+ const glsl300 = usesGlsl300(context);
40703
+ this.shaderProgram = Cesium.ShaderProgram.fromCache({
40704
+ context,
40705
+ vertexShaderSource: glsl300 ? VERTEX_SHADER_GLSL300 : toGlsl100(VERTEX_SHADER_GLSL300, false),
40706
+ fragmentShaderSource: glsl300 ? FRAGMENT_SHADER_GLSL300 : toGlsl100(FRAGMENT_SHADER_GLSL300, true),
40707
+ attributeLocations: mesh.attributeLocations
40708
+ });
40709
+ this.renderState = Cesium.RenderState.fromCache({
40710
+ depthTest: { enabled: true },
40711
+ // Translucent ramp: blend, and leave the depth buffer to the opaque world.
40712
+ depthMask: false,
40713
+ blending: Cesium.BlendingState.ALPHA_BLEND,
40714
+ cull: { enabled: false }
40715
+ });
40716
+ }
40717
+ if (!tile.command) {
40718
+ const self = this;
40719
+ tile.command = new Cesium.DrawCommand({
40720
+ shaderProgram: this.shaderProgram,
40721
+ renderState: this.renderState,
40722
+ pass: Cesium.Pass.TRANSLUCENT,
40723
+ modelMatrix: Cesium.Matrix4.IDENTITY,
40724
+ owner: this,
40725
+ uniformMap: {
40726
+ u_valueTexture: () => self.texture,
40727
+ u_groundTexture: () => self.groundTexture,
40728
+ u_groundWeight: () => (self.followGround && self.groundPixels ? 1 : 0),
40729
+ u_groundRange: () => new Cesium.Cartesian2(GROUND_MIN_METRES, GROUND_MAX_METRES),
40730
+ u_groundSize: () => new Cesium.Cartesian2(self.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1, self.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1),
40731
+ u_metresPerTexel: () => self.metresPerTexel(),
40732
+ u_valueMin: () => self.valueMin,
40733
+ u_valueRange: () => self.valueMax - self.valueMin,
40734
+ u_exaggeration: () => self.exaggeration,
40735
+ u_baseHeight: () => self.baseHeight,
40736
+ u_skirtDepth: () => self.tileSkirts
40737
+ ? Math.max(2, Math.abs(self.valueMax - self.valueMin) * self.exaggeration * SKIRT_FRACTION)
40738
+ : 0,
40739
+ u_coverageCutoff: () => COVERAGE_CUTOFF,
40740
+ u_lowColor: () => self.lowColor,
40741
+ u_highColor: () => self.highColor,
40742
+ u_texelSize: () => new Cesium.Cartesian2(1 / self.source.width, 1 / self.source.height)
40743
+ }
40744
+ });
40745
+ }
40746
+ // Bounding volume belongs to the mesh, so it follows whichever tier is drawing.
40747
+ tile.command.vertexArray = mesh.vertexArray;
40748
+ tile.command.boundingVolume = mesh.boundingSphere;
40749
+ return tile.command;
40750
+ }
40751
+ }
40752
+ DisplacedSurfacePrimitive.Surface = Surface;
40753
+ /*
40754
+ * Cuts the extent into tiles, dropping any the caller says carry no data.
40755
+ */
40756
+ function buildTiles(hasCoverage) {
40757
+ const tiles = [];
40758
+ for (let row = 0; row < TILES_PER_SIDE; row++) {
40759
+ for (let col = 0; col < TILES_PER_SIDE; col++) {
40760
+ const patch = {
40761
+ u0: col / TILES_PER_SIDE,
40762
+ u1: (col + 1) / TILES_PER_SIDE,
40763
+ v0: row / TILES_PER_SIDE,
40764
+ v1: (row + 1) / TILES_PER_SIDE
40765
+ };
40766
+ if (hasCoverage && !hasCoverage(patch)) {
40767
+ continue;
40768
+ }
40769
+ tiles.push({
40770
+ patch,
40771
+ meshes: new Map(),
40772
+ tier: TIERS[0],
40773
+ command: null,
40774
+ centre: null,
40775
+ radius: 0
40776
+ });
40777
+ }
40778
+ }
40779
+ return tiles;
40780
+ }
40781
+ /**
40782
+ * Exaggeration that makes the full value range stand AUTO_RELIEF_FRACTION of the extent tall.
40783
+ *
40784
+ * Derived from the whole extent rather than per tile: scaling each tile to its own size would
40785
+ * make neighbours disagree along their shared edge and tear the surface apart.
40786
+ * @param extent
40787
+ * @param valueMin
40788
+ * @param valueMax
40789
+ */
40790
+ function autoExaggeration(extent, valueMin, valueMax) {
40791
+ const range = Math.abs(valueMax - valueMin);
40792
+ if (!(range > 0)) {
40793
+ return 1;
40794
+ }
40795
+ const southWest = Cesium.Cartesian3.fromDegrees(extent.West, extent.South, 0);
40796
+ const southEast = Cesium.Cartesian3.fromDegrees(extent.East, extent.South, 0);
40797
+ const northWest = Cesium.Cartesian3.fromDegrees(extent.West, extent.North, 0);
40798
+ const shorterSide = Math.min(Cesium.Cartesian3.distance(southWest, southEast), Cesium.Cartesian3.distance(southWest, northWest));
40799
+ if (!(shorterSide > 0)) {
40800
+ return 1;
40801
+ }
40802
+ return (shorterSide * AUTO_RELIEF_FRACTION) / range;
40803
+ }
40804
+ DisplacedSurfacePrimitive.autoExaggeration = autoExaggeration;
40805
+ function toCesiumColor(color) {
40806
+ return new Cesium.Color(color.red / 255, color.green / 255, color.blue / 255, color.alpha);
40807
+ }
40808
+ function now() {
40809
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
40810
+ }
40811
+ })(exports.DisplacedSurfacePrimitive || (exports.DisplacedSurfacePrimitive = {}));
40812
+
39897
40813
  const TEXTURE_FRAME_SERIES_ANIMATOR_KEY = "TextureFrameSeriesAnimator.Animator";
40814
+ const EXTRUSION_ANIMATOR_KEY = "TextureFrameSeriesAnimator.ExtrusionAnimator";
40815
+ const DISPLACED_SURFACE_KEY = "DisplacedSurfacePrimitive.Surface";
39898
40816
  (function (EntityRenderEnginePolygon) {
39899
40817
  async function Render(params) {
39900
- var _a, _b, _c;
40818
+ var _a, _b, _c, _d;
39901
40819
  const entity = params.entity;
39902
40820
  const style = params.style;
39903
40821
  const pRings = BModels.Entity.GetValue({
@@ -39930,14 +40848,38 @@
39930
40848
  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
40849
  }
39932
40850
  }
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;
40851
+ const usesTextureExtrusion = (Boolean(style.useExtrusion) &&
40852
+ style.extrusionType === BModels.Style.EPolygonExtrusionType.Texture);
40853
+ let extrusionArchive = null;
40854
+ if (usesTextureExtrusion && !params.offline) {
40855
+ try {
40856
+ const resolved = await resolveTextureValue({
40857
+ api: params.api,
40858
+ textureValue: style.extrusionTexture || style.texture,
40859
+ style,
40860
+ entity,
40861
+ tags: params.tags
40862
+ });
40863
+ extrusionArchive = resolved.frameArchive || null;
40864
+ }
40865
+ catch (e) {
40866
+ 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);
40867
+ }
40868
+ }
40869
+ const drawsDisplacedSurface = Boolean(extrusionArchive);
40870
+ // The displaced surface IS the entity's fill, drawn as its own primitive at the value's height.
40871
+ // Leaving the flat polygon filled as well shows a second copy of the parcel on the ground under it.
40872
+ const hasFill = (drawsDisplacedSurface ? true
40873
+ : fillType === BModels.Style.EPolygonFillType.Texture
40874
+ ? Boolean(textureDataUri || frameArchive || cFillColor.alpha > 0)
40875
+ : cFillColor.alpha > 0);
40876
+ const fillMaterial = drawsDisplacedSurface
40877
+ ? Cesium.Color.WHITE.withAlpha(0)
40878
+ : textureDataUri
40879
+ ? new Cesium.ImageMaterialProperty({ image: textureDataUri, transparent: true })
40880
+ : frameArchive
40881
+ ? Cesium.Color.WHITE.withAlpha(0)
40882
+ : cFillColor;
39941
40883
  const lineColorTrace = style.lineColor ? BModels.Calculator.TraceGetColor(style.lineColor, entity, params.tags) : { value: null, effective: null };
39942
40884
  const bLineColor = lineColorTrace.value;
39943
40885
  const cLineColor = bLineColor ? ColorToCColor(bLineColor) : Cesium.Color.fromCssColorString("rgba(80, 80, 80, 0.8)");
@@ -39964,7 +40906,9 @@
39964
40906
  const points = BModels.Geometry.ParsePoints(outerRing === null || outerRing === void 0 ? void 0 : outerRing.LinearRing);
39965
40907
  let posses = points.map(x => Cesium.Cartesian3.fromDegrees(EnsureNumber(x.longitude), EnsureNumber(x.latitude), EnsureNumber(x.altitude)));
39966
40908
  BModels.Cartes.CloseRing3(posses);
39967
- const extrusion = getPolygonExtrusion(entity, params.tags, outerRing, posses, heightRef, style);
40909
+ const extrusion = drawsDisplacedSurface
40910
+ ? { posses, value: undefined, exHeightRef: Cesium.HeightReference.NONE }
40911
+ : getPolygonExtrusion(entity, params.tags, outerRing, posses, heightRef, style);
39968
40912
  posses = extrusion.posses;
39969
40913
  posses = CullDuplicateCPosses(posses);
39970
40914
  if (posses.length < 4) {
@@ -40013,6 +40957,7 @@
40013
40957
  polygon: {
40014
40958
  hierarchy: new Cesium.PolygonHierarchy(posses, holePosses.map(x => new Cesium.PolygonHierarchy(x))),
40015
40959
  material: fillMaterial,
40960
+ fill: !drawsDisplacedSurface,
40016
40961
  extrudedHeight: extrusion.value,
40017
40962
  extrudedHeightReference: extrusion.exHeightRef,
40018
40963
  shadows: Cesium.ShadowMode.ENABLED,
@@ -40043,6 +40988,12 @@
40043
40988
  if (!CompareCPolygonHierarchies(oldHierarchy, newHierarchy)) {
40044
40989
  cEntity.polygon.hierarchy = new Cesium.ConstantProperty(newHierarchy);
40045
40990
  }
40991
+ // Toggling extrusion styling on an already-rendered entity flips this,
40992
+ // so it has to be reconciled rather than set only at creation.
40993
+ const oldFill = GetCValue(params.viewer, cEntity.polygon.fill);
40994
+ if (oldFill !== !drawsDisplacedSurface) {
40995
+ cEntity.polygon.fill = new Cesium.ConstantProperty(!drawsDisplacedSurface);
40996
+ }
40046
40997
  const oldExtrusion = GetCValue(params.viewer, cEntity.polygon.extrudedHeight);
40047
40998
  if (oldExtrusion != extrusion.value) {
40048
40999
  cEntity.polygon.extrudedHeight = new Cesium.ConstantProperty(extrusion.value);
@@ -40096,7 +41047,16 @@
40096
41047
  }
40097
41048
  // Must run before any material is baked below: the Animator takes over polygon.material, so a bake
40098
41049
  // 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);
41050
+ const animator = syncTextureFrameArchive(cEntity, drawsDisplacedSurface ? null : frameArchive, params.viewer, style.textureColorMask, style.maskTextureBaseline);
41051
+ syncDisplacedSurface({
41052
+ cEntity,
41053
+ extrusionArchive,
41054
+ viewer: params.viewer,
41055
+ style,
41056
+ entity,
41057
+ heightRef,
41058
+ outerRingPosses: posses
41059
+ });
40100
41060
  if (animator) {
40101
41061
  exports.CesiumEntityStyler.SetDefaultTextureImage({
40102
41062
  entity: cEntity,
@@ -40156,7 +41116,7 @@
40156
41116
  }
40157
41117
  }
40158
41118
  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];
41119
+ let cEntityBorder = (_d = (_c = params.rendered) === null || _c === void 0 ? void 0 : _c._siblingGraphics) === null || _d === void 0 ? void 0 : _d[0];
40160
41120
  cEntity._siblingGraphics = [];
40161
41121
  if (!cEntityBorder || ((!cEntityBorder.polyline && units == "px") ||
40162
41122
  (!cEntityBorder.corridor && units == "m"))) {
@@ -40406,12 +41366,13 @@
40406
41366
  /**
40407
41367
  * Disposes a cEntity's TextureFrameSeriesAnimator.Animator (if any).
40408
41368
  */
40409
- function DisposeTextureFrameSeriesAnimator(cEntity) {
41369
+ function DisposeTextureFrameSeriesAnimator(cEntity, viewer) {
40410
41370
  const existing = cEntity === null || cEntity === void 0 ? void 0 : cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY];
40411
41371
  if (existing && !existing.IsDisposed()) {
40412
41372
  existing.Dispose();
40413
41373
  cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY] = null;
40414
41374
  }
41375
+ disposeDisplacedSurface(cEntity, viewer);
40415
41376
  }
40416
41377
  EntityRenderEnginePolygon.DisposeTextureFrameSeriesAnimator = DisposeTextureFrameSeriesAnimator;
40417
41378
  })(exports.EntityRenderEnginePolygon || (exports.EntityRenderEnginePolygon = {}));
@@ -40525,6 +41486,145 @@
40525
41486
  // Cache of tinted texture data URIs, keyed by resolved url + color mask, so repeated renders of the
40526
41487
  // same polygon (or multiple polygons sharing a texture) don't refetch/reprocess the same image.
40527
41488
  const _textureCache = new BModels.LRUCache(50);
41489
+ /**
41490
+ * Reconciles the displaced surface (and the animator feeding it) against this render's resolved
41491
+ * extrusion texture, creating, reusing or tearing down as the style demands.
41492
+ * @param params
41493
+ */
41494
+ function syncDisplacedSurface(params) {
41495
+ var _a;
41496
+ const { cEntity, extrusionArchive, viewer, style, entity, heightRef, outerRingPosses } = params;
41497
+ const existingAnimator = cEntity[EXTRUSION_ANIMATOR_KEY];
41498
+ const existingSurface = cEntity[DISPLACED_SURFACE_KEY];
41499
+ // Nothing to draw and nothing left over, which is every polygon that does not use this feature.
41500
+ if (!extrusionArchive && !existingAnimator && !existingSurface) {
41501
+ return;
41502
+ }
41503
+ const reusable = extrusionArchive
41504
+ && existingAnimator
41505
+ && !existingAnimator.IsDisposed()
41506
+ && existingAnimator.GetArchiveUrl() === extrusionArchive.url
41507
+ && existingSurface
41508
+ && !existingSurface.isDestroyed();
41509
+ const placement = surfacePlacement(entity, heightRef);
41510
+ // Whether the surface follows terrain is baked into its ground sampling, so a change there has
41511
+ // to rebuild rather than update in place.
41512
+ if (reusable && existingSurface.GetFollowsGround() === placement.followGround) {
41513
+ existingSurface.SetBaseHeight(placement.baseHeight);
41514
+ existingSurface.SetExaggeration((_a = style.extrusionExaggeration) !== null && _a !== void 0 ? _a : exports.DisplacedSurfacePrimitive.autoExaggeration(existingSurface.GetExtent(), extrusionArchive.metadata.ValueMin, extrusionArchive.metadata.ValueMax));
41515
+ return;
41516
+ }
41517
+ disposeDisplacedSurface(cEntity, viewer);
41518
+ if (!extrusionArchive) {
41519
+ return;
41520
+ }
41521
+ const extent = archiveExtent(extrusionArchive.metadata, outerRingPosses);
41522
+ if (!extent) {
41523
+ console.warn("Polygon.Render: extrusion texture has no usable extent, skipping displaced surface.");
41524
+ return;
41525
+ }
41526
+ // Drives the value canvas only: the surface paints the colour, so the polygon's own material is
41527
+ // left transparent rather than animated underneath it.
41528
+ const animator = new TextureFrameSeriesAnimator.Animator({
41529
+ viewer,
41530
+ entity: cEntity,
41531
+ archiveUrl: extrusionArchive.url,
41532
+ frames: extrusionArchive.metadata.Frames,
41533
+ textureColorMask: style.textureColorMask,
41534
+ driveMaterial: false,
41535
+ produceValueCanvas: true,
41536
+ baselineMask: extrusionArchive.metadata.BaselineMask,
41537
+ // Off-type for the same reason as the fill path: the pinned bruce-models predates the field.
41538
+ maskBaseline: style.maskTextureBaseline
41539
+ });
41540
+ cEntity[EXTRUSION_ANIMATOR_KEY] = animator;
41541
+ const mask = style.textureColorMask;
41542
+ const surface = new exports.DisplacedSurfacePrimitive.Surface({
41543
+ extent,
41544
+ // The grid has to be walked the way the raster was sampled, or the surface skews across the extent.
41545
+ pixelPlacement: extrusionArchive.metadata.PixelPlacement,
41546
+ source: null,
41547
+ baseHeight: placement.baseHeight,
41548
+ followGround: placement.followGround,
41549
+ terrainProvider: viewer && viewer.terrainProvider,
41550
+ valueMin: extrusionArchive.metadata.ValueMin,
41551
+ valueMax: extrusionArchive.metadata.ValueMax,
41552
+ exaggeration: style.extrusionExaggeration,
41553
+ lowColor: mask ? BModels.Color.ColorFromStr(mask.minColor) : null,
41554
+ highColor: mask ? BModels.Color.ColorFromStr(mask.maxColor) : null
41555
+ });
41556
+ surface.BindAnimator(animator);
41557
+ viewer.scene.primitives.add(surface);
41558
+ cEntity[DISPLACED_SURFACE_KEY] = surface;
41559
+ }
41560
+ /**
41561
+ * Turns the polygon's altitude option into the two things the displaced surface needs:
41562
+ * - how high value zero sits.
41563
+ * - whether that is measured from the terrain or from the ellipsoid.
41564
+ * @param entity
41565
+ * @param heightRef
41566
+ */
41567
+ function surfacePlacement(entity, heightRef) {
41568
+ const rawAltitude = BModels.Entity.GetValue({
41569
+ entity,
41570
+ path: ["Bruce", "Location", "altitude"]
41571
+ });
41572
+ const altitude = rawAltitude ? EnsureNumber(rawAltitude) : 0;
41573
+ if (heightRef === Cesium.HeightReference.CLAMP_TO_GROUND) {
41574
+ return { baseHeight: 0, followGround: true };
41575
+ }
41576
+ if (heightRef === Cesium.HeightReference.RELATIVE_TO_GROUND) {
41577
+ return { baseHeight: altitude, followGround: true };
41578
+ }
41579
+ return { baseHeight: altitude, followGround: false };
41580
+ }
41581
+ /**
41582
+ * The archive's own extent when it recorded one, otherwise the polygon's own bounds so archives
41583
+ * generated before the extent was written still render somewhere sensible.
41584
+ * @param metadata
41585
+ * @param posses
41586
+ */
41587
+ function archiveExtent(metadata, posses) {
41588
+ if (metadata.West != null && metadata.East != null && metadata.South != null && metadata.North != null) {
41589
+ return { West: metadata.West, East: metadata.East, South: metadata.South, North: metadata.North };
41590
+ }
41591
+ if (!posses || posses.length === 0) {
41592
+ return null;
41593
+ }
41594
+ const rectangle = Cesium.Rectangle.fromCartesianArray(posses);
41595
+ return {
41596
+ West: Cesium.Math.toDegrees(rectangle.west),
41597
+ East: Cesium.Math.toDegrees(rectangle.east),
41598
+ South: Cesium.Math.toDegrees(rectangle.south),
41599
+ North: Cesium.Math.toDegrees(rectangle.north)
41600
+ };
41601
+ }
41602
+ /**
41603
+ * Tears down the displaced surface and its feeding animator, if this entity has them.
41604
+ * @param cEntity
41605
+ * @param viewer
41606
+ */
41607
+ function disposeDisplacedSurface(cEntity, viewer) {
41608
+ const surface = cEntity === null || cEntity === void 0 ? void 0 : cEntity[DISPLACED_SURFACE_KEY];
41609
+ if (surface) {
41610
+ const viewerAlive = viewer && (typeof viewer.isDestroyed !== "function" || !viewer.isDestroyed());
41611
+ if (viewerAlive && viewer.scene && viewer.scene.primitives) {
41612
+ // Removing from the collection destroys the primitive, releasing its meshes and texture.
41613
+ viewer.scene.primitives.remove(surface);
41614
+ }
41615
+ else if (!surface.isDestroyed()) {
41616
+ surface.destroy();
41617
+ }
41618
+ cEntity[DISPLACED_SURFACE_KEY] = null;
41619
+ }
41620
+ const animator = cEntity === null || cEntity === void 0 ? void 0 : cEntity[EXTRUSION_ANIMATOR_KEY];
41621
+ if (animator && !animator.IsDisposed()) {
41622
+ animator.Dispose();
41623
+ }
41624
+ if (cEntity) {
41625
+ cEntity[EXTRUSION_ANIMATOR_KEY] = null;
41626
+ }
41627
+ }
40528
41628
  /**
40529
41629
  * Reconciles cEntity's TextureFrameSeriesAnimator.Animator (if any) against this render's resolved texture.
40530
41630
  * Called once cEntity is fully created/updated and definitely has a `polygon` graphics.
@@ -40534,7 +41634,7 @@
40534
41634
  * @param viewer
40535
41635
  * @param textureColorMask
40536
41636
  */
40537
- function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask) {
41637
+ function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask, maskTextureBaseline) {
40538
41638
  const existing = cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY];
40539
41639
  if (frameArchive && existing && !existing.IsDisposed() && existing.GetArchiveUrl() === frameArchive.url) {
40540
41640
  return existing;
@@ -40551,7 +41651,9 @@
40551
41651
  entity: cEntity,
40552
41652
  archiveUrl: frameArchive.url,
40553
41653
  frames: frameArchive.metadata.Frames,
40554
- textureColorMask
41654
+ textureColorMask,
41655
+ baselineMask: frameArchive.metadata.BaselineMask,
41656
+ maskBaseline: maskTextureBaseline
40555
41657
  });
40556
41658
  cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY] = animator;
40557
41659
  return animator;
@@ -40615,8 +41717,15 @@
40615
41717
  * @param params
40616
41718
  */
40617
41719
  async function resolveTexturedFill(params) {
40618
- const { api, style, entity, tags } = params;
40619
- const textureValue = style.texture;
41720
+ return resolveTextureValue({ ...params, textureValue: params.style.texture });
41721
+ }
41722
+ /**
41723
+ * Resolves either shape a texture setting can take (a calculated file id/URL, or a search condition)
41724
+ * into a tinted data URI or, when the resolved file is a frame archive, the archive itself.
41725
+ * @param params
41726
+ */
41727
+ async function resolveTextureValue(params) {
41728
+ const { api, style, entity, tags, textureValue } = params;
40620
41729
  if (textureValue && !Array.isArray(textureValue)) {
40621
41730
  return resolveTextureCondition({ api, condition: textureValue });
40622
41731
  }
@@ -41393,7 +42502,7 @@
41393
42502
  entity._dispose();
41394
42503
  entity._dispose = null;
41395
42504
  }
41396
- exports.EntityRenderEnginePolygon.DisposeTextureFrameSeriesAnimator(entity);
42505
+ exports.EntityRenderEnginePolygon.DisposeTextureFrameSeriesAnimator(entity, viewer);
41397
42506
  if (entity._parentEntity && !ignoreParent) {
41398
42507
  doRemove({
41399
42508
  viewer,
@@ -42141,7 +43250,7 @@
42141
43250
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
42142
43251
  })(exports.StyleUtils || (exports.StyleUtils = {}));
42143
43252
 
42144
- const VERSION = "7.1.3";
43253
+ const VERSION = "7.1.5";
42145
43254
  /**
42146
43255
  * Updates the environment instance used by bruce-cesium to one specified.
42147
43256
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.