bruce-cesium 7.1.4 → 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
@@ -39670,6 +39723,8 @@
39670
39723
  this.frameDims = new Array(this.frames.length).fill(null);
39671
39724
  this.valueCache = new Array(this.frames.length).fill(null);
39672
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);
39673
39728
  this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
39674
39729
  this.driveMaterial = options.driveMaterial !== false;
39675
39730
  this.originalMaterial = options.entity.polygon.material;
@@ -39839,6 +39894,12 @@
39839
39894
  if (this.disposed) {
39840
39895
  return;
39841
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
+ }
39842
39903
  const decoded = await this.decodeAndTint(buffer);
39843
39904
  if (this.disposed) {
39844
39905
  return;
@@ -39873,8 +39934,81 @@
39873
39934
  // recovered from the tinted pixels afterwards.
39874
39935
  const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
39875
39936
  ApplyGrayscaleColorMask(imageData, this.lowColor, this.highColor);
39937
+ this.applyBaselineMask(imageData.data, valuePixels, canvas.width, canvas.height);
39938
+ if (valuePixels) {
39939
+ dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES);
39940
+ }
39876
39941
  return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
39877
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
+ }
40011
+ }
39878
40012
  beginCrossfadeTo(idx) {
39879
40013
  const to = this.frameCache[idx];
39880
40014
  this.fadeFromValuePixels = this.displayedValuePixels === null && this.valueCache[idx]
@@ -40116,11 +40250,13 @@ void main() {
40116
40250
  return _glsl300Support;
40117
40251
  }
40118
40252
  /*
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.
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.
40122
40255
  */
40123
- function makePlacer(extent) {
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
+ }
40124
40260
  const ellipsoid = Cesium.Ellipsoid.WGS84;
40125
40261
  const centre = Cesium.Cartesian3.fromDegrees((extent.West + extent.East) / 2, (extent.South + extent.North) / 2, 0);
40126
40262
  const frame = Cesium.Transforms.eastNorthUpToFixedFrame(centre);
@@ -40134,21 +40270,21 @@ void main() {
40134
40270
  };
40135
40271
  }
40136
40272
  /*
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.
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.
40139
40275
  */
40140
- function buildGeometry(extent, resolution, patch) {
40276
+ function buildGeometry(extent, resolution, patch, pixelPlacement, withSkirt = false) {
40141
40277
  const cols = resolution;
40142
40278
  const rows = resolution;
40143
40279
  const gridCount = (cols + 1) * (rows + 1);
40144
- const perimeterCount = 2 * (cols + 1) + 2 * (rows + 1) - 4;
40280
+ const perimeterCount = withSkirt ? 2 * (cols + 1) + 2 * (rows + 1) - 4 : 0;
40145
40281
  const count = gridCount + perimeterCount;
40146
40282
  const positions = new Float64Array(count * 3);
40147
40283
  const normals = new Float32Array(count * 3);
40148
40284
  const sts = new Float32Array(count * 2);
40149
40285
  const skirts = new Float32Array(count);
40150
40286
  const ellipsoid = Cesium.Ellipsoid.WGS84;
40151
- const place = makePlacer(extent);
40287
+ const place = makePlacer(extent, pixelPlacement);
40152
40288
  let v = 0;
40153
40289
  for (let row = 0; row <= rows; row++) {
40154
40290
  // Texture row 0 is the north edge, so t runs opposite to northing.
@@ -40199,26 +40335,28 @@ void main() {
40199
40335
  loop.push(row * (cols + 1));
40200
40336
  }
40201
40337
  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;
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
+ }
40222
40360
  }
40223
40361
  const geometry = new Cesium.Geometry({
40224
40362
  attributes: {
@@ -40276,6 +40414,8 @@ void main() {
40276
40414
  this.valueMax = (_b = options.valueMax) !== null && _b !== void 0 ? _b : 1;
40277
40415
  this.exaggeration = (_c = options.exaggeration) !== null && _c !== void 0 ? _c : autoExaggeration(this.extent, this.valueMin, this.valueMax);
40278
40416
  this.baseHeight = (_d = options.baseHeight) !== null && _d !== void 0 ? _d : 0;
40417
+ this.pixelPlacement = options.pixelPlacement;
40418
+ this.tileSkirts = Boolean(options.tileSkirts);
40279
40419
  this.lowColor = toCesiumColor(options.lowColor || DEFAULT_LOW_COLOR);
40280
40420
  this.highColor = toCesiumColor(options.highColor || DEFAULT_HIGH_COLOR);
40281
40421
  this.tiles = buildTiles(hasCoverage);
@@ -40283,6 +40423,9 @@ void main() {
40283
40423
  GetFollowsGround() {
40284
40424
  return this.followGround;
40285
40425
  }
40426
+ GetPixelPlacement() {
40427
+ return this.pixelPlacement;
40428
+ }
40286
40429
  GetBaseHeight() {
40287
40430
  return this.baseHeight;
40288
40431
  }
@@ -40497,7 +40640,7 @@ void main() {
40497
40640
  if (cached) {
40498
40641
  return cached;
40499
40642
  }
40500
- const geometry = buildGeometry(this.extent, tier, tile.patch);
40643
+ const geometry = buildGeometry(this.extent, tier, tile.patch, this.pixelPlacement, this.tileSkirts);
40501
40644
  const attributeLocations = Cesium.GeometryPipeline.createAttributeLocations(geometry);
40502
40645
  const mesh = {
40503
40646
  attributeLocations,
@@ -40590,7 +40733,9 @@ void main() {
40590
40733
  u_valueRange: () => self.valueMax - self.valueMin,
40591
40734
  u_exaggeration: () => self.exaggeration,
40592
40735
  u_baseHeight: () => self.baseHeight,
40593
- u_skirtDepth: () => Math.max(2, Math.abs(self.valueMax - self.valueMin) * self.exaggeration * SKIRT_FRACTION),
40736
+ u_skirtDepth: () => self.tileSkirts
40737
+ ? Math.max(2, Math.abs(self.valueMax - self.valueMin) * self.exaggeration * SKIRT_FRACTION)
40738
+ : 0,
40594
40739
  u_coverageCutoff: () => COVERAGE_CUTOFF,
40595
40740
  u_lowColor: () => self.lowColor,
40596
40741
  u_highColor: () => self.highColor,
@@ -40722,6 +40867,8 @@ void main() {
40722
40867
  }
40723
40868
  }
40724
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.
40725
40872
  const hasFill = (drawsDisplacedSurface ? true
40726
40873
  : fillType === BModels.Style.EPolygonFillType.Texture
40727
40874
  ? Boolean(textureDataUri || frameArchive || cFillColor.alpha > 0)
@@ -40810,6 +40957,7 @@ void main() {
40810
40957
  polygon: {
40811
40958
  hierarchy: new Cesium.PolygonHierarchy(posses, holePosses.map(x => new Cesium.PolygonHierarchy(x))),
40812
40959
  material: fillMaterial,
40960
+ fill: !drawsDisplacedSurface,
40813
40961
  extrudedHeight: extrusion.value,
40814
40962
  extrudedHeightReference: extrusion.exHeightRef,
40815
40963
  shadows: Cesium.ShadowMode.ENABLED,
@@ -40840,6 +40988,12 @@ void main() {
40840
40988
  if (!CompareCPolygonHierarchies(oldHierarchy, newHierarchy)) {
40841
40989
  cEntity.polygon.hierarchy = new Cesium.ConstantProperty(newHierarchy);
40842
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
+ }
40843
40997
  const oldExtrusion = GetCValue(params.viewer, cEntity.polygon.extrudedHeight);
40844
40998
  if (oldExtrusion != extrusion.value) {
40845
40999
  cEntity.polygon.extrudedHeight = new Cesium.ConstantProperty(extrusion.value);
@@ -40893,7 +41047,7 @@ void main() {
40893
41047
  }
40894
41048
  // Must run before any material is baked below: the Animator takes over polygon.material, so a bake
40895
41049
  // 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);
41050
+ const animator = syncTextureFrameArchive(cEntity, drawsDisplacedSurface ? null : frameArchive, params.viewer, style.textureColorMask, style.maskTextureBaseline);
40897
41051
  syncDisplacedSurface({
40898
41052
  cEntity,
40899
41053
  extrusionArchive,
@@ -41378,12 +41532,17 @@ void main() {
41378
41532
  frames: extrusionArchive.metadata.Frames,
41379
41533
  textureColorMask: style.textureColorMask,
41380
41534
  driveMaterial: false,
41381
- produceValueCanvas: true
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
41382
41539
  });
41383
41540
  cEntity[EXTRUSION_ANIMATOR_KEY] = animator;
41384
41541
  const mask = style.textureColorMask;
41385
41542
  const surface = new exports.DisplacedSurfacePrimitive.Surface({
41386
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,
41387
41546
  source: null,
41388
41547
  baseHeight: placement.baseHeight,
41389
41548
  followGround: placement.followGround,
@@ -41475,7 +41634,7 @@ void main() {
41475
41634
  * @param viewer
41476
41635
  * @param textureColorMask
41477
41636
  */
41478
- function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask) {
41637
+ function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask, maskTextureBaseline) {
41479
41638
  const existing = cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY];
41480
41639
  if (frameArchive && existing && !existing.IsDisposed() && existing.GetArchiveUrl() === frameArchive.url) {
41481
41640
  return existing;
@@ -41492,7 +41651,9 @@ void main() {
41492
41651
  entity: cEntity,
41493
41652
  archiveUrl: frameArchive.url,
41494
41653
  frames: frameArchive.metadata.Frames,
41495
- textureColorMask
41654
+ textureColorMask,
41655
+ baselineMask: frameArchive.metadata.BaselineMask,
41656
+ maskBaseline: maskTextureBaseline
41496
41657
  });
41497
41658
  cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY] = animator;
41498
41659
  return animator;
@@ -43089,7 +43250,7 @@ void main() {
43089
43250
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
43090
43251
  })(exports.StyleUtils || (exports.StyleUtils = {}));
43091
43252
 
43092
- const VERSION = "7.1.4";
43253
+ const VERSION = "7.1.5";
43093
43254
  /**
43094
43255
  * Updates the environment instance used by bruce-cesium to one specified.
43095
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.