bruce-cesium 7.1.4 → 7.1.6

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.
@@ -3576,6 +3576,7 @@
3576
3576
  if (!geometry || !isTurfAvailable() || !turf.simplify) {
3577
3577
  return geometry;
3578
3578
  }
3579
+ const original = geometry;
3579
3580
  // Convert to geojson so that we can interact with turf.
3580
3581
  const gFeature = BModels.Geometry.ToGeoJsonFeature({
3581
3582
  geometry: geometry
@@ -3596,13 +3597,97 @@
3596
3597
  });
3597
3598
  }
3598
3599
  // Converting back to Bruce geometry.
3599
- geometry = (_a = BModels.Geometry.FromGeoJson({
3600
+ const simplified = (_a = BModels.Geometry.FromGeoJson({
3600
3601
  geoJson: gFeature
3601
3602
  })) === null || _a === void 0 ? void 0 : _a.geometry;
3602
- return geometry;
3603
+ // A polygon we cannot vouch for is worse than an unsimplified one, so fall back instead of shipping it.
3604
+ if (!simplified || !isSafeToRender(simplified)) {
3605
+ return original;
3606
+ }
3607
+ return simplified;
3603
3608
  }
3604
3609
  SimplifyGeometry.Simplify = Simplify;
3610
+ /**
3611
+ * Whether a simplified geometry still describes something a renderer can triangulate.
3612
+ * @param geometry the simplification result.
3613
+ * @returns false when any polygon has a collapsed ring, a hole outside its outer ring, or a hole
3614
+ * wound the same way as its outer ring.
3615
+ */
3616
+ function IsSafeToRender(geometry) {
3617
+ return isSafeToRender(geometry);
3618
+ }
3619
+ SimplifyGeometry.IsSafeToRender = IsSafeToRender;
3605
3620
  })(SimplifyGeometry || (SimplifyGeometry = {}));
3621
+ // A ring needs three distinct corners plus the closing repeat before it encloses any area.
3622
+ const MIN_RING_POSITIONS = 4;
3623
+ function isSafeToRender(geometry) {
3624
+ let safe = true;
3625
+ const check = (g) => {
3626
+ if (!safe) {
3627
+ return;
3628
+ }
3629
+ if (g === null || g === void 0 ? void 0 : g.MultiGeometry) {
3630
+ g.MultiGeometry.forEach(check);
3631
+ return;
3632
+ }
3633
+ if (!(g === null || g === void 0 ? void 0 : g.Polygon) || g.Polygon.length === 0) {
3634
+ return;
3635
+ }
3636
+ let outer = null;
3637
+ let outerIsClockwise = false;
3638
+ for (let i = 0; i < g.Polygon.length; i++) {
3639
+ const points = BModels.Geometry.ParsePoints(g.Polygon[i].LinearRing);
3640
+ if (!points || points.length < MIN_RING_POSITIONS) {
3641
+ safe = false;
3642
+ return;
3643
+ }
3644
+ const clockwise = signedRingArea(points) < 0;
3645
+ if (g.Polygon[i].Facing !== "in") {
3646
+ outer = points;
3647
+ outerIsClockwise = clockwise;
3648
+ continue;
3649
+ }
3650
+ // A hole with no outer ring ahead of it cannot be subtracted from anything.
3651
+ if (!outer) {
3652
+ safe = false;
3653
+ return;
3654
+ }
3655
+ if (clockwise === outerIsClockwise) {
3656
+ safe = false;
3657
+ return;
3658
+ }
3659
+ for (let j = 0; j < points.length; j++) {
3660
+ if (!isPointInRing(points[j], outer)) {
3661
+ safe = false;
3662
+ return;
3663
+ }
3664
+ }
3665
+ }
3666
+ };
3667
+ check(geometry);
3668
+ return safe;
3669
+ }
3670
+ function signedRingArea(points) {
3671
+ let sum = 0;
3672
+ for (let i = 0; i < points.length; i++) {
3673
+ const a = points[i];
3674
+ const b = points[(i + 1) % points.length];
3675
+ sum += a.longitude * b.latitude - b.longitude * a.latitude;
3676
+ }
3677
+ return sum / 2;
3678
+ }
3679
+ function isPointInRing(point, ring) {
3680
+ let inside = false;
3681
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
3682
+ const a = ring[i];
3683
+ const b = ring[j];
3684
+ if ((a.latitude > point.latitude) !== (b.latitude > point.latitude)
3685
+ && point.longitude < (b.longitude - a.longitude) * (point.latitude - a.latitude) / (b.latitude - a.latitude) + a.longitude) {
3686
+ inside = !inside;
3687
+ }
3688
+ }
3689
+ return inside;
3690
+ }
3606
3691
  /**
3607
3692
  * Runs through all found polygons and runs unkink through turf on them.
3608
3693
  * @param feature
@@ -3644,9 +3729,26 @@
3644
3729
  }
3645
3730
  // Recreate the rings and reapply to the collection.
3646
3731
  if (outer.length > 0) {
3647
- const combinedCoords = [outer[0]];
3732
+ // Unkinking a self touching boundary splits it into pieces. Keeping whichever piece
3733
+ // happened to come back first can discard most of the shape, so keep the biggest one.
3734
+ let keep = outer[0];
3735
+ if (outer.length > 1) {
3736
+ let best = Math.abs(ringArea(keep));
3737
+ for (let i = 1; i < outer.length; i++) {
3738
+ const area = Math.abs(ringArea(outer[i]));
3739
+ if (area > best) {
3740
+ best = area;
3741
+ keep = outer[i];
3742
+ }
3743
+ }
3744
+ }
3745
+ const combinedCoords = [keep];
3648
3746
  for (let i = 0; i < inner.length; i++) {
3649
- combinedCoords.push(inner[i]);
3747
+ // A hole belonging to a piece that was just discarded is no longer inside anything,
3748
+ // and a hole crossing the boundary is what tears the fill.
3749
+ if (isRingInsideRing(inner[i], keep)) {
3750
+ combinedCoords.push(inner[i]);
3751
+ }
3650
3752
  }
3651
3753
  // Add the combined coordinates to the collection
3652
3754
  const polygon = {
@@ -3794,6 +3896,30 @@
3794
3896
  feature.geometry = collection;
3795
3897
  }
3796
3898
  }
3899
+ function ringArea(ring) {
3900
+ let sum = 0;
3901
+ for (let i = 0; i < ring.length; i++) {
3902
+ const a = ring[i];
3903
+ const b = ring[(i + 1) % ring.length];
3904
+ sum += a[0] * b[1] - b[0] * a[1];
3905
+ }
3906
+ return sum / 2;
3907
+ }
3908
+ function isRingInsideRing(inner, outer) {
3909
+ for (let i = 0; i < inner.length; i++) {
3910
+ let inside = false;
3911
+ for (let a = 0, b = outer.length - 1; a < outer.length; b = a++) {
3912
+ if ((outer[a][1] > inner[i][1]) !== (outer[b][1] > inner[i][1])
3913
+ && inner[i][0] < (outer[b][0] - outer[a][0]) * (inner[i][1] - outer[a][1]) / (outer[b][1] - outer[a][1]) + outer[a][0]) {
3914
+ inside = !inside;
3915
+ }
3916
+ }
3917
+ if (!inside) {
3918
+ return false;
3919
+ }
3920
+ }
3921
+ return true;
3922
+ }
3797
3923
  function dedupeCoordinates(inputCoords) {
3798
3924
  return inputCoords.map(ring => {
3799
3925
  if (ring.length < 2) {
@@ -39622,15 +39748,68 @@
39622
39748
  return Boolean(data && data.generation && Array.isArray(data.generation.Frames) && data.generation.Frames.length > 0);
39623
39749
  }
39624
39750
  TextureFrameSeriesAnimator.IsFrameArchiveMetadata = IsFrameArchiveMetadata;
39751
+ // How far covered values bleed outward into uncovered texels before the GPU samples them.
39752
+ const VALUE_DILATE_PASSES = 2;
39753
+ /*
39754
+ * Bleeds covered values outward into uncovered texels, leaving alpha untouched.
39755
+ *
39756
+ * The GPU samples the value texture with linear filtering, so an uncovered texel's RGB still gets
39757
+ * averaged into the vertices next to it even though its alpha is zero. Land sits at one end of the
39758
+ * ramp, so without this the coastline grows a row of spikes exactly where the data stops. Masking the
39759
+ * baseline creates more of these edges, which is what makes this necessary rather than cosmetic.
39760
+ */
39761
+ function dilateValues(value, width, height, passes) {
39762
+ const texels = width * height;
39763
+ const filled = new Uint8Array(texels);
39764
+ for (let p = 0; p < texels; p++) {
39765
+ filled[p] = value[p * 4 + 3] !== 0 ? 1 : 0;
39766
+ }
39767
+ for (let pass = 0; pass < passes; pass++) {
39768
+ const source = value.slice();
39769
+ const wasFilled = filled.slice();
39770
+ for (let y = 0; y < height; y++) {
39771
+ for (let x = 0; x < width; x++) {
39772
+ const p = y * width + x;
39773
+ if (wasFilled[p]) {
39774
+ continue;
39775
+ }
39776
+ let sum = 0;
39777
+ let hits = 0;
39778
+ for (let dy = -1; dy <= 1; dy++) {
39779
+ for (let dx = -1; dx <= 1; dx++) {
39780
+ const nx = x + dx;
39781
+ const ny = y + dy;
39782
+ if (nx < 0 || ny < 0 || nx >= width || ny >= height || !wasFilled[ny * width + nx]) {
39783
+ continue;
39784
+ }
39785
+ sum += source[(ny * width + nx) * 4];
39786
+ hits++;
39787
+ }
39788
+ }
39789
+ if (hits > 0) {
39790
+ const v = Math.round(sum / hits);
39791
+ value[p * 4] = v;
39792
+ value[p * 4 + 1] = v;
39793
+ value[p * 4 + 2] = v;
39794
+ filled[p] = 1;
39795
+ }
39796
+ }
39797
+ }
39798
+ }
39799
+ }
39625
39800
  const DEFAULT_CROSSFADE_MS = 900;
39626
39801
  const DEFAULT_LOW_COLOR = { red: 255, green: 255, blue: 255, alpha: 0.12 };
39627
39802
  const DEFAULT_HIGH_COLOR = { red: 21, green: 96, blue: 196, alpha: 0.92 };
39628
39803
  class Animator {
39629
39804
  constructor(options) {
39630
- var _a;
39805
+ var _a, _b;
39631
39806
  this.removeOnTick = null;
39632
39807
  this.removeCrossfadeTick = null;
39633
39808
  this.disposed = false;
39809
+ // One alpha byte per texel, 1 meaning the texel is baseline. Fetched once, the mesh does not move.
39810
+ this.baselineFlags = null;
39811
+ this.baselineDims = null;
39812
+ this.baselineLoad = null;
39634
39813
  this.currentFrameIndex = -1;
39635
39814
  // Single-in-flight fetch queue: only one Range GET outstanding at a time, and the NEXT one
39636
39815
  // is chosen right before sending, this is what makes reprioritizing mid-scrub take effect
@@ -39670,6 +39849,8 @@
39670
39849
  this.frameDims = new Array(this.frames.length).fill(null);
39671
39850
  this.valueCache = new Array(this.frames.length).fill(null);
39672
39851
  this.produceValueCanvas = Boolean(options.produceValueCanvas);
39852
+ this.baselineMaskEntry = options.baselineMask || null;
39853
+ this.maskBaseline = (_b = options.maskBaseline) !== null && _b !== void 0 ? _b : Boolean(options.baselineMask);
39673
39854
  this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
39674
39855
  this.driveMaterial = options.driveMaterial !== false;
39675
39856
  this.originalMaterial = options.entity.polygon.material;
@@ -39839,6 +40020,12 @@
39839
40020
  if (this.disposed) {
39840
40021
  return;
39841
40022
  }
40023
+ // Awaited before decoding rather than applied later, so no frame is ever cached unmasked and
40024
+ // then corrected once the mask lands.
40025
+ await this.ensureBaselineMask();
40026
+ if (this.disposed) {
40027
+ return;
40028
+ }
39842
40029
  const decoded = await this.decodeAndTint(buffer);
39843
40030
  if (this.disposed) {
39844
40031
  return;
@@ -39873,8 +40060,81 @@
39873
40060
  // recovered from the tinted pixels afterwards.
39874
40061
  const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
39875
40062
  ApplyGrayscaleColorMask(imageData, this.lowColor, this.highColor);
40063
+ this.applyBaselineMask(imageData.data, valuePixels, canvas.width, canvas.height);
40064
+ if (valuePixels) {
40065
+ dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES);
40066
+ }
39876
40067
  return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
39877
40068
  }
40069
+ /**
40070
+ * Fetches and decodes the archive's static baseline mask, at most once.
40071
+ */
40072
+ ensureBaselineMask() {
40073
+ if (!this.maskBaseline || !this.baselineMaskEntry) {
40074
+ return Promise.resolve();
40075
+ }
40076
+ if (this.baselineLoad) {
40077
+ return this.baselineLoad;
40078
+ }
40079
+ const entry = this.baselineMaskEntry;
40080
+ const start = entry.ByteOffset;
40081
+ const end = entry.ByteOffset + entry.ByteLength - 1;
40082
+ this.baselineLoad = fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } })
40083
+ .then((response) => response.arrayBuffer())
40084
+ .then(async (buffer) => {
40085
+ if (this.disposed) {
40086
+ return;
40087
+ }
40088
+ const blob = new Blob([buffer], { type: "image/png" });
40089
+ const objectUrl = URL.createObjectURL(blob);
40090
+ let image;
40091
+ try {
40092
+ image = await loadImage(objectUrl);
40093
+ }
40094
+ finally {
40095
+ URL.revokeObjectURL(objectUrl);
40096
+ }
40097
+ const canvas = document.createElement("canvas");
40098
+ canvas.width = image.width;
40099
+ canvas.height = image.height;
40100
+ const ctx = canvas.getContext("2d");
40101
+ ctx.drawImage(image, 0, 0);
40102
+ const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
40103
+ const flags = new Uint8Array(canvas.width * canvas.height);
40104
+ for (let p = 0; p < flags.length; p++) {
40105
+ flags[p] = pixels[p * 4 + 3] >= 128 ? 1 : 0;
40106
+ }
40107
+ this.baselineFlags = flags;
40108
+ this.baselineDims = { width: canvas.width, height: canvas.height };
40109
+ })
40110
+ .catch((e) => {
40111
+ // A missing or unreadable mask must not stop the animation; it just means nothing is hidden.
40112
+ console.warn("TextureFrameSeriesAnimator: could not load the baseline mask, drawing it instead.", e);
40113
+ });
40114
+ return this.baselineLoad;
40115
+ }
40116
+ /**
40117
+ * Clears alpha on baseline texels, which is what makes hiding the dry/land sentinel a render choice.
40118
+ */
40119
+ applyBaselineMask(pixels, valuePixels, width, height) {
40120
+ const flags = this.baselineFlags;
40121
+ if (!flags || !this.baselineDims) {
40122
+ return;
40123
+ }
40124
+ // A mask from a different raster size cannot be indexed against this frame, and guessing a
40125
+ // resampling would silently hide the wrong ground.
40126
+ if (this.baselineDims.width !== width || this.baselineDims.height !== height) {
40127
+ return;
40128
+ }
40129
+ for (let p = 0; p < flags.length; p++) {
40130
+ if (flags[p]) {
40131
+ pixels[p * 4 + 3] = 0;
40132
+ if (valuePixels) {
40133
+ valuePixels[p * 4 + 3] = 0;
40134
+ }
40135
+ }
40136
+ }
40137
+ }
39878
40138
  beginCrossfadeTo(idx) {
39879
40139
  const to = this.frameCache[idx];
39880
40140
  this.fadeFromValuePixels = this.displayedValuePixels === null && this.valueCache[idx]
@@ -40116,11 +40376,13 @@ void main() {
40116
40376
  return _glsl300Support;
40117
40377
  }
40118
40378
  /*
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.
40379
+ * Places grid node (fx, fy) on the ellipsoid, stepping the way the raster was sampled.
40380
+ * 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.
40122
40381
  */
40123
- function makePlacer(extent) {
40382
+ function makePlacer(extent, pixelPlacement) {
40383
+ if (pixelPlacement === "lonlat") {
40384
+ return (fx, fy) => Cesium.Cartesian3.fromDegrees(Cesium.Math.lerp(extent.West, extent.East, fx), Cesium.Math.lerp(extent.North, extent.South, fy), 0);
40385
+ }
40124
40386
  const ellipsoid = Cesium.Ellipsoid.WGS84;
40125
40387
  const centre = Cesium.Cartesian3.fromDegrees((extent.West + extent.East) / 2, (extent.South + extent.North) / 2, 0);
40126
40388
  const frame = Cesium.Transforms.eastNorthUpToFixedFrame(centre);
@@ -40134,21 +40396,21 @@ void main() {
40134
40396
  };
40135
40397
  }
40136
40398
  /*
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.
40399
+ * Builds one tile's grid, optionally plus a duplicated perimeter ring the shader pushes downwards to
40400
+ * cover the crack where a neighbouring tile at a different density interpolates the shared edge.
40139
40401
  */
40140
- function buildGeometry(extent, resolution, patch) {
40402
+ function buildGeometry(extent, resolution, patch, pixelPlacement, withSkirt = false) {
40141
40403
  const cols = resolution;
40142
40404
  const rows = resolution;
40143
40405
  const gridCount = (cols + 1) * (rows + 1);
40144
- const perimeterCount = 2 * (cols + 1) + 2 * (rows + 1) - 4;
40406
+ const perimeterCount = withSkirt ? 2 * (cols + 1) + 2 * (rows + 1) - 4 : 0;
40145
40407
  const count = gridCount + perimeterCount;
40146
40408
  const positions = new Float64Array(count * 3);
40147
40409
  const normals = new Float32Array(count * 3);
40148
40410
  const sts = new Float32Array(count * 2);
40149
40411
  const skirts = new Float32Array(count);
40150
40412
  const ellipsoid = Cesium.Ellipsoid.WGS84;
40151
- const place = makePlacer(extent);
40413
+ const place = makePlacer(extent, pixelPlacement);
40152
40414
  let v = 0;
40153
40415
  for (let row = 0; row <= rows; row++) {
40154
40416
  // Texture row 0 is the north edge, so t runs opposite to northing.
@@ -40199,26 +40461,28 @@ void main() {
40199
40461
  loop.push(row * (cols + 1));
40200
40462
  }
40201
40463
  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;
40464
+ if (withSkirt) {
40465
+ loop.forEach((gridIndex, k) => {
40466
+ const target = skirtStart + k;
40467
+ positions[target * 3] = positions[gridIndex * 3];
40468
+ positions[target * 3 + 1] = positions[gridIndex * 3 + 1];
40469
+ positions[target * 3 + 2] = positions[gridIndex * 3 + 2];
40470
+ normals[target * 3] = normals[gridIndex * 3];
40471
+ normals[target * 3 + 1] = normals[gridIndex * 3 + 1];
40472
+ normals[target * 3 + 2] = normals[gridIndex * 3 + 2];
40473
+ sts[target * 2] = sts[gridIndex * 2];
40474
+ sts[target * 2 + 1] = sts[gridIndex * 2 + 1];
40475
+ skirts[target] = 1;
40476
+ });
40477
+ for (let k = 0; k < loop.length; k++) {
40478
+ const nextK = (k + 1) % loop.length;
40479
+ indices[i++] = loop[k];
40480
+ indices[i++] = skirtStart + k;
40481
+ indices[i++] = loop[nextK];
40482
+ indices[i++] = loop[nextK];
40483
+ indices[i++] = skirtStart + k;
40484
+ indices[i++] = skirtStart + nextK;
40485
+ }
40222
40486
  }
40223
40487
  const geometry = new Cesium.Geometry({
40224
40488
  attributes: {
@@ -40276,6 +40540,8 @@ void main() {
40276
40540
  this.valueMax = (_b = options.valueMax) !== null && _b !== void 0 ? _b : 1;
40277
40541
  this.exaggeration = (_c = options.exaggeration) !== null && _c !== void 0 ? _c : autoExaggeration(this.extent, this.valueMin, this.valueMax);
40278
40542
  this.baseHeight = (_d = options.baseHeight) !== null && _d !== void 0 ? _d : 0;
40543
+ this.pixelPlacement = options.pixelPlacement;
40544
+ this.tileSkirts = Boolean(options.tileSkirts);
40279
40545
  this.lowColor = toCesiumColor(options.lowColor || DEFAULT_LOW_COLOR);
40280
40546
  this.highColor = toCesiumColor(options.highColor || DEFAULT_HIGH_COLOR);
40281
40547
  this.tiles = buildTiles(hasCoverage);
@@ -40283,6 +40549,9 @@ void main() {
40283
40549
  GetFollowsGround() {
40284
40550
  return this.followGround;
40285
40551
  }
40552
+ GetPixelPlacement() {
40553
+ return this.pixelPlacement;
40554
+ }
40286
40555
  GetBaseHeight() {
40287
40556
  return this.baseHeight;
40288
40557
  }
@@ -40497,7 +40766,7 @@ void main() {
40497
40766
  if (cached) {
40498
40767
  return cached;
40499
40768
  }
40500
- const geometry = buildGeometry(this.extent, tier, tile.patch);
40769
+ const geometry = buildGeometry(this.extent, tier, tile.patch, this.pixelPlacement, this.tileSkirts);
40501
40770
  const attributeLocations = Cesium.GeometryPipeline.createAttributeLocations(geometry);
40502
40771
  const mesh = {
40503
40772
  attributeLocations,
@@ -40590,7 +40859,9 @@ void main() {
40590
40859
  u_valueRange: () => self.valueMax - self.valueMin,
40591
40860
  u_exaggeration: () => self.exaggeration,
40592
40861
  u_baseHeight: () => self.baseHeight,
40593
- u_skirtDepth: () => Math.max(2, Math.abs(self.valueMax - self.valueMin) * self.exaggeration * SKIRT_FRACTION),
40862
+ u_skirtDepth: () => self.tileSkirts
40863
+ ? Math.max(2, Math.abs(self.valueMax - self.valueMin) * self.exaggeration * SKIRT_FRACTION)
40864
+ : 0,
40594
40865
  u_coverageCutoff: () => COVERAGE_CUTOFF,
40595
40866
  u_lowColor: () => self.lowColor,
40596
40867
  u_highColor: () => self.highColor,
@@ -40722,6 +40993,8 @@ void main() {
40722
40993
  }
40723
40994
  }
40724
40995
  const drawsDisplacedSurface = Boolean(extrusionArchive);
40996
+ // The displaced surface IS the entity's fill, drawn as its own primitive at the value's height.
40997
+ // Leaving the flat polygon filled as well shows a second copy of the parcel on the ground under it.
40725
40998
  const hasFill = (drawsDisplacedSurface ? true
40726
40999
  : fillType === BModels.Style.EPolygonFillType.Texture
40727
41000
  ? Boolean(textureDataUri || frameArchive || cFillColor.alpha > 0)
@@ -40810,6 +41083,7 @@ void main() {
40810
41083
  polygon: {
40811
41084
  hierarchy: new Cesium.PolygonHierarchy(posses, holePosses.map(x => new Cesium.PolygonHierarchy(x))),
40812
41085
  material: fillMaterial,
41086
+ fill: !drawsDisplacedSurface,
40813
41087
  extrudedHeight: extrusion.value,
40814
41088
  extrudedHeightReference: extrusion.exHeightRef,
40815
41089
  shadows: Cesium.ShadowMode.ENABLED,
@@ -40840,6 +41114,12 @@ void main() {
40840
41114
  if (!CompareCPolygonHierarchies(oldHierarchy, newHierarchy)) {
40841
41115
  cEntity.polygon.hierarchy = new Cesium.ConstantProperty(newHierarchy);
40842
41116
  }
41117
+ // Toggling extrusion styling on an already-rendered entity flips this,
41118
+ // so it has to be reconciled rather than set only at creation.
41119
+ const oldFill = GetCValue(params.viewer, cEntity.polygon.fill);
41120
+ if (oldFill !== !drawsDisplacedSurface) {
41121
+ cEntity.polygon.fill = new Cesium.ConstantProperty(!drawsDisplacedSurface);
41122
+ }
40843
41123
  const oldExtrusion = GetCValue(params.viewer, cEntity.polygon.extrudedHeight);
40844
41124
  if (oldExtrusion != extrusion.value) {
40845
41125
  cEntity.polygon.extrudedHeight = new Cesium.ConstantProperty(extrusion.value);
@@ -40893,7 +41173,7 @@ void main() {
40893
41173
  }
40894
41174
  // Must run before any material is baked below: the Animator takes over polygon.material, so a bake
40895
41175
  // has to know whether it is baking over a live animation or over a disposed one's restored material.
40896
- const animator = syncTextureFrameArchive(cEntity, drawsDisplacedSurface ? null : frameArchive, params.viewer, style.textureColorMask);
41176
+ const animator = syncTextureFrameArchive(cEntity, drawsDisplacedSurface ? null : frameArchive, params.viewer, style.textureColorMask, style.maskTextureBaseline);
40897
41177
  syncDisplacedSurface({
40898
41178
  cEntity,
40899
41179
  extrusionArchive,
@@ -41378,12 +41658,17 @@ void main() {
41378
41658
  frames: extrusionArchive.metadata.Frames,
41379
41659
  textureColorMask: style.textureColorMask,
41380
41660
  driveMaterial: false,
41381
- produceValueCanvas: true
41661
+ produceValueCanvas: true,
41662
+ baselineMask: extrusionArchive.metadata.BaselineMask,
41663
+ // Off-type for the same reason as the fill path: the pinned bruce-models predates the field.
41664
+ maskBaseline: style.maskTextureBaseline
41382
41665
  });
41383
41666
  cEntity[EXTRUSION_ANIMATOR_KEY] = animator;
41384
41667
  const mask = style.textureColorMask;
41385
41668
  const surface = new exports.DisplacedSurfacePrimitive.Surface({
41386
41669
  extent,
41670
+ // The grid has to be walked the way the raster was sampled, or the surface skews across the extent.
41671
+ pixelPlacement: extrusionArchive.metadata.PixelPlacement,
41387
41672
  source: null,
41388
41673
  baseHeight: placement.baseHeight,
41389
41674
  followGround: placement.followGround,
@@ -41475,7 +41760,7 @@ void main() {
41475
41760
  * @param viewer
41476
41761
  * @param textureColorMask
41477
41762
  */
41478
- function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask) {
41763
+ function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask, maskTextureBaseline) {
41479
41764
  const existing = cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY];
41480
41765
  if (frameArchive && existing && !existing.IsDisposed() && existing.GetArchiveUrl() === frameArchive.url) {
41481
41766
  return existing;
@@ -41492,7 +41777,9 @@ void main() {
41492
41777
  entity: cEntity,
41493
41778
  archiveUrl: frameArchive.url,
41494
41779
  frames: frameArchive.metadata.Frames,
41495
- textureColorMask
41780
+ textureColorMask,
41781
+ baselineMask: frameArchive.metadata.BaselineMask,
41782
+ maskBaseline: maskTextureBaseline
41496
41783
  });
41497
41784
  cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY] = animator;
41498
41785
  return animator;
@@ -43089,7 +43376,7 @@ void main() {
43089
43376
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
43090
43377
  })(exports.StyleUtils || (exports.StyleUtils = {}));
43091
43378
 
43092
- const VERSION = "7.1.4";
43379
+ const VERSION = "7.1.6";
43093
43380
  /**
43094
43381
  * Updates the environment instance used by bruce-cesium to one specified.
43095
43382
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.