bruce-cesium 7.2.9 → 7.3.1

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.
@@ -1,6 +1,6 @@
1
1
  import * as Cesium from 'cesium';
2
- import { Cartographic, ColorMaterialProperty, Entity, Color, ConstantProperty, CallbackProperty, Primitive, Cesium3DTileFeature, DistanceDisplayCondition, ShaderSource, Cartesian3, Math as Math$1, Ellipsoid, Transforms, Matrix4, Geometry, GeometryAttribute, ComponentDatatype, PrimitiveType, BoundingSphere, GeometryPipeline, sampleTerrain, Texture, PixelFormat, Sampler, TextureWrap, TextureMinificationFilter, TextureMagnificationFilter, VertexArray, BufferUsage, ShaderProgram, RenderState, BlendingState, DrawCommand, Pass, Cartesian2, JulianDate, Quaternion, HeadingPitchRoll, HeightReference, ColorBlendMode, ShadowMode, ClassificationType, Model, ArcType, CornerType, ConstantPositionProperty, ImageMaterialProperty, PolygonHierarchy, PolylineGraphics, Rectangle, HorizontalOrigin, VerticalOrigin, SceneTransforms, NearFarScalar, Matrix3, KmlDataSource, GeoJsonDataSource, SceneMode, Ion, IonResource, Cesium3DTileStyle, HeadingPitchRange, Cesium3DTileColorBlendMode, EllipsoidTerrainProvider, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, Cesium3DTileset, Intersect, OrthographicFrustum, EasingFunction, EllipsoidGeodesic, sampleTerrainMostDetailed, defined, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, PolygonPipeline, CustomDataSource, GeometryInstance, CesiumInspector, ClockRange, DynamicEnvironmentMapManager, ScreenSpaceEventHandler, ScreenSpaceEventType, Fullscreen, CzmlDataSource } from 'cesium';
3
- import { Cartes, Entity as Entity$1, ProjectViewTile, ENVIRONMENT, Calculator, ClientFile, EntityLod, EntityTag, EntityType, LRUCache, ObjectUtils, Style, Carto, Geometry as Geometry$1, Bounds, ClientFileValueMap, Color as Color$1, Api, MenuItem, ProjectView, ProjectViewBookmark, Tileset, ZoomControl, BruceEvent, EntityCoords, DelayQueue, DataLab, EntityRelation, BruceApi, AccountConcept, RecordChangeFeed, EntitySource, ProgramKey, ProjectViewLegacyTile, Camera, Account, AccountLimits, Session, UrlUtils, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, ProjectViewBookmarkGroup, EntityRelationType, EntityHistoricData } from 'bruce-models';
2
+ import { Cartographic, Entity, DistanceDisplayCondition, ColorMaterialProperty, Color, Math as Math$1, Cartesian3, JulianDate, Quaternion, Transforms, HeadingPitchRoll, Matrix4, ConstantProperty, CallbackProperty, Primitive, Cesium3DTileFeature, ShaderSource, Ellipsoid, Geometry, GeometryAttribute, ComponentDatatype, PrimitiveType, BoundingSphere, GeometryPipeline, sampleTerrain, Texture, PixelFormat, Sampler, TextureWrap, TextureMinificationFilter, TextureMagnificationFilter, VertexArray, BufferUsage, ShaderProgram, RenderState, BlendingState, DrawCommand, Pass, Cartesian2, Cartesian4, HeightReference, ColorBlendMode, ShadowMode, ClassificationType, Model, ArcType, CornerType, ConstantPositionProperty, HorizontalOrigin, VerticalOrigin, ImageMaterialProperty, PolygonHierarchy, PolylineGraphics, Rectangle, SceneTransforms, NearFarScalar, Matrix3, KmlDataSource, GeoJsonDataSource, SceneMode, Cesium3DTileStyle, HeadingPitchRange, Ion, IonResource, Cesium3DTileColorBlendMode, Cesium3DTileset, OrthographicFrustum, EasingFunction, EllipsoidTerrainProvider, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, Intersect, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, EllipsoidGeodesic, sampleTerrainMostDetailed, defined, PolygonPipeline, CesiumInspector, ClockRange, DynamicEnvironmentMapManager, CustomDataSource, GeometryInstance, ScreenSpaceEventHandler, ScreenSpaceEventType, Fullscreen, CzmlDataSource } from 'cesium';
3
+ import { Cartes, Entity as Entity$1, ProjectViewTile, ENVIRONMENT, Calculator, ClientFile, EntityLod, EntityTag, EntityType, LRUCache, ObjectUtils, Style, Carto, Geometry as Geometry$1, Api, MenuItem, ProjectView, ProjectViewBookmark, Tileset, ZoomControl, ClientFileValueMap, Color as Color$1, Bounds, BruceEvent, EntityCoords, DelayQueue, DataLab, AccountConcept, RecordChangeFeed, BruceApi, EntityRelation, EntitySource, Camera, ProgramKey, ProjectViewLegacyTile, Account, AccountLimits, Session, UrlUtils, ProjectViewBookmarkGroup, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, EntityRelationType, EntityHistoricData } from 'bruce-models';
4
4
 
5
5
  /**
6
6
  * Ensures a number is returned from a given value.
@@ -6591,7 +6591,7 @@ function shouldCullEntity(viewer, cEntity) {
6591
6591
  const adjustedSphere = boundingSphere.sphere.clone();
6592
6592
  // Terrain only matters here because it moves the sphere onto the ground.
6593
6593
  // Far enough away that a plausible ground height is a rounding error against the viewing distance, sampling it is wasted work.
6594
- const cameraDistance = Cartesian3.distance(camera.position, adjustedSphere.center);
6594
+ const cameraDistance = Cartesian3.distance(camera.positionWC, adjustedSphere.center);
6595
6595
  if (cameraDistance > TERRAIN_SKIP_DISTANCE) {
6596
6596
  adjustedSphere.radius += TERRAIN_SKIP_RADIUS_PAD;
6597
6597
  }
@@ -6604,7 +6604,8 @@ function shouldCullEntity(viewer, cEntity) {
6604
6604
  }
6605
6605
  adjustedSphere.center = grounded;
6606
6606
  }
6607
- const cullingVolume = camera.frustum.computeCullingVolume(camera.position, camera.direction, camera.up);
6607
+ // Camera vectors are frame relative while a reference frame is set (the compass orbits via lookAt).
6608
+ const cullingVolume = camera.frustum.computeCullingVolume(camera.positionWC, camera.directionWC, camera.upWC);
6608
6609
  const visibility = cullingVolume.computeVisibility(adjustedSphere);
6609
6610
  return visibility !== Intersect.INSIDE && visibility !== Intersect.INTERSECTING;
6610
6611
  }
@@ -6631,14 +6632,14 @@ function shouldRecheck(viewer) {
6631
6632
  return true;
6632
6633
  }
6633
6634
  const camera = viewer.scene.camera;
6634
- if (!((_a = camera === null || camera === void 0 ? void 0 : camera.position) === null || _a === void 0 ? void 0 : _a.x)) {
6635
+ if (!((_a = camera === null || camera === void 0 ? void 0 : camera.positionWC) === null || _a === void 0 ? void 0 : _a.x)) {
6635
6636
  return false;
6636
6637
  }
6637
6638
  const lastRecheck = camera[LAST_RECHECK_KEY];
6638
6639
  if (!lastRecheck) {
6639
6640
  camera[LAST_RECHECK_KEY] = {
6640
- position: camera.position.clone(),
6641
- direction: camera.direction.clone(),
6641
+ position: camera.positionWC.clone(),
6642
+ direction: camera.directionWC.clone(),
6642
6643
  tilesLoading: viewer.scene.globe.tilesLoaded
6643
6644
  };
6644
6645
  return true;
@@ -6647,14 +6648,14 @@ function shouldRecheck(viewer) {
6647
6648
  const tilesLoadingDiff = lastRecheck.tilesLoading != viewer.scene.globe.tilesLoaded;
6648
6649
  let recheck = tilesLoadingDiff;
6649
6650
  if (!recheck) {
6650
- const posDiff = Cartesian3.magnitude(Cartesian3.subtract(position, camera.position, new Cartesian3()));
6651
- const dirDiff = Cartesian3.magnitude(Cartesian3.subtract(direction, camera.direction, new Cartesian3()));
6651
+ const posDiff = Cartesian3.magnitude(Cartesian3.subtract(position, camera.positionWC, new Cartesian3()));
6652
+ const dirDiff = Cartesian3.magnitude(Cartesian3.subtract(direction, camera.directionWC, new Cartesian3()));
6652
6653
  recheck = posDiff > 100 || dirDiff > 0.01;
6653
6654
  }
6654
6655
  if (recheck) {
6655
6656
  camera[LAST_RECHECK_KEY] = {
6656
- position: camera.position.clone(),
6657
- direction: camera.direction.clone(),
6657
+ position: camera.positionWC.clone(),
6658
+ direction: camera.directionWC.clone(),
6658
6659
  tilesLoading: viewer.scene.globe.tilesLoaded
6659
6660
  };
6660
6661
  }
@@ -6707,7 +6708,9 @@ function cullTilesets(register) {
6707
6708
  return;
6708
6709
  }
6709
6710
  const camera = viewer.scene.camera;
6710
- const cullingVolume = camera.frustum.computeCullingVolume(camera.position, camera.direction, camera.up);
6711
+ // Camera vectors are frame relative while a reference frame is set (the compass orbits via lookAt).
6712
+ // These spheres are world space, so only the world coordinate vectors compare against them correctly.
6713
+ const cullingVolume = camera.frustum.computeCullingVolume(camera.positionWC, camera.directionWC, camera.upWC);
6711
6714
  let changed = false;
6712
6715
  for (const rego of tilesets) {
6713
6716
  const tileset = rego.tileset;
@@ -42488,6 +42491,17 @@ var DisplacedSurfacePrimitive;
42488
42491
  // Most-detailed would fetch every tile under a parcel that can be a hundred kilometres across.
42489
42492
  const GROUND_SAMPLE_LEVEL = 11;
42490
42493
  // Height window the packed ground texture covers, wide enough for any terrain on Earth.
42494
+ // Ground samples across the clearance regions rather than the whole extent.
42495
+ const MASK_GROUND_SAMPLES_PER_SIDE = 128;
42496
+ // Finest and coarsest terrain level a clearance raster is sampled at, and how many terrain tiles it is allowed to pull.
42497
+ const MASK_GROUND_LEVEL_MAX = 13;
42498
+ const MASK_GROUND_LEVEL_MIN = 8;
42499
+ const MASK_GROUND_TILE_BUDGET = 64;
42500
+ // Longest side of the rasterised region mask. The mask decides whether a texel is held up, so it
42501
+ // wants to be finer than the value raster it is compared against.
42502
+ const MASK_TEXTURE_MAX_SIDE = 1024;
42503
+ // Stands in for "no floor" and "no clearance", far below anything the sheet can reach.
42504
+ const NO_LIMIT_METRES = -1e6;
42491
42505
  const GROUND_MIN_METRES = -1000;
42492
42506
  const GROUND_MAX_METRES = 9000;
42493
42507
  const DEFAULT_LOW_COLOR = { red: 255, green: 255, blue: 255, alpha: 0.12 };
@@ -42510,6 +42524,19 @@ uniform float u_skirtDepth;
42510
42524
  uniform float u_groundWeight;
42511
42525
  uniform vec2 u_groundRange;
42512
42526
  uniform vec2 u_groundSize;
42527
+ uniform sampler2D u_maskTexture;
42528
+ uniform sampler2D u_maskGroundTexture;
42529
+ uniform float u_maskWeight;
42530
+ uniform float u_maskGroundWeight;
42531
+ uniform float u_maskFill;
42532
+ uniform float u_maskFillValue;
42533
+ uniform float u_maskClearance;
42534
+ uniform float u_maskFloor;
42535
+ uniform vec2 u_maskGroundSize;
42536
+ // West, East, North, South in degrees, for the surface and for the sampled region box.
42537
+ uniform vec4 u_extentDegrees;
42538
+ uniform vec4 u_maskGroundDegrees;
42539
+ uniform float u_coverageCutoffVertex;
42513
42540
 
42514
42541
  out vec2 v_st;
42515
42542
  out float v_value;
@@ -42539,6 +42566,46 @@ float sampleGround(vec2 uv) {
42539
42566
  return mix(mix(h00, h10, f.x), mix(h01, h11, f.x), f.y);
42540
42567
  }
42541
42568
 
42569
+ /*
42570
+ * Height of the terrain under a point of the surface, read from the region's own ground raster.
42571
+ *
42572
+ * Sampled over the regions rather than over the whole extent, so the raster is fine enough for a
42573
+ * clearance test to mean something.
42574
+ */
42575
+ float sampleMaskGround(vec2 uv) {
42576
+ float longitude = mix(u_extentDegrees.x, u_extentDegrees.y, uv.x);
42577
+ float latitude = mix(u_extentDegrees.z, u_extentDegrees.w, uv.y);
42578
+ vec2 inBox = vec2(
42579
+ (longitude - u_maskGroundDegrees.x) / max(u_maskGroundDegrees.y - u_maskGroundDegrees.x, 1e-9),
42580
+ (u_maskGroundDegrees.z - latitude) / max(u_maskGroundDegrees.z - u_maskGroundDegrees.w, 1e-9));
42581
+ vec2 clamped = clamp(inBox, vec2(0.0), vec2(1.0));
42582
+ vec2 texel = 1.0 / u_maskGroundSize;
42583
+ vec2 coord = clamped * u_maskGroundSize - 0.5;
42584
+ vec2 base = floor(coord);
42585
+ vec2 f = coord - base;
42586
+ vec2 origin = (base + 0.5) * texel;
42587
+ vec4 t00 = texture(u_maskGroundTexture, origin);
42588
+ vec4 t10 = texture(u_maskGroundTexture, origin + vec2(texel.x, 0.0));
42589
+ vec4 t01 = texture(u_maskGroundTexture, origin + vec2(0.0, texel.y));
42590
+ vec4 t11 = texture(u_maskGroundTexture, origin + texel);
42591
+ // One unknown corner makes the interpolation meaningless, so the whole point is treated as
42592
+ // unsampled and answers low enough that no clearance built on it can win.
42593
+ float known = min(min(t00.a, t10.a), min(t01.a, t11.a));
42594
+ if (known < 0.5) {
42595
+ return u_groundRange.x;
42596
+ }
42597
+ vec3 p00 = t00.rgb * 255.0;
42598
+ vec3 p10 = t10.rgb * 255.0;
42599
+ vec3 p01 = t01.rgb * 255.0;
42600
+ vec3 p11 = t11.rgb * 255.0;
42601
+ vec3 weights = vec3(65536.0, 256.0, 1.0);
42602
+ float h00 = u_groundRange.x + dot(p00, weights) / 16777215.0 * (u_groundRange.y - u_groundRange.x);
42603
+ float h10 = u_groundRange.x + dot(p10, weights) / 16777215.0 * (u_groundRange.y - u_groundRange.x);
42604
+ float h01 = u_groundRange.x + dot(p01, weights) / 16777215.0 * (u_groundRange.y - u_groundRange.x);
42605
+ float h11 = u_groundRange.x + dot(p11, weights) / 16777215.0 * (u_groundRange.y - u_groundRange.x);
42606
+ return mix(mix(h00, h10, f.x), mix(h01, h11, f.x), f.y);
42607
+ }
42608
+
42542
42609
  /*
42543
42610
  * The texel's normalised value, whichever way the archive packed it.
42544
42611
  *
@@ -42558,8 +42625,16 @@ float decodeValue(vec4 texel) {
42558
42625
  void main() {
42559
42626
  vec4 texel = texture(u_valueTexture, st);
42560
42627
  v_st = st;
42561
- v_value = decodeValue(texel);
42562
- v_coverage = texel.a;
42628
+
42629
+ // Inside a region the scene knows is water, a texel with no value still has to paint, or the
42630
+ // hole the region exists to close is still a hole.
42631
+ // The value it paints is the shallowest the ramp is not transparent at.
42632
+ float inMask = u_maskWeight * texture(u_maskTexture, st).a;
42633
+ float hasData = step(u_coverageCutoffVertex, texel.a);
42634
+ float raw = decodeValue(texel);
42635
+ float filled = mix(u_maskFillValue, max(raw, u_maskFillValue), hasData);
42636
+ v_value = mix(raw, filled, inMask * u_maskFill);
42637
+ v_coverage = max(texel.a, inMask * u_maskFill);
42563
42638
 
42564
42639
  // Exaggeration stretches the span above the archive's minimum rather than the height itself. On an
42565
42640
  // elevation the height is measured from the ellipsoid, tens of metres from any of the data, so
@@ -42572,8 +42647,14 @@ void main() {
42572
42647
 
42573
42648
  // Coverage scales the displacement only where it is measured from the terrain, because there a
42574
42649
  // zero means the surface sits on the ground.
42575
- float coverageScale = mix(1.0, texel.a, u_groundWeight);
42576
- float displacement = u_baseHeight + ground + metres * coverageScale - skirt * u_skirtDepth;
42650
+ float coverageScale = mix(1.0, v_coverage, u_groundWeight);
42651
+ float placed = u_baseHeight + ground + metres * coverageScale;
42652
+
42653
+ // Held above whichever is higher, the region's own floor or the terrain under it plus a margin.
42654
+ // Only inside the region: lifting the whole sheet would raise it over dry land as well.
42655
+ float held = max(u_maskFloor, sampleMaskGround(st) * u_maskGroundWeight + u_maskClearance);
42656
+ float lifted = mix(placed, max(placed, held), step(0.5, inMask));
42657
+ float displacement = lifted - skirt * u_skirtDepth;
42577
42658
 
42578
42659
  // Added to the LOW half of the encoded position: a metre-scale offset added to the high half
42579
42660
  // would be lost to float32 rounding at an earth radius.
@@ -42808,6 +42889,147 @@ void main() {
42808
42889
  GeometryPipeline.encodeAttribute(geometry, "position", "position3DHigh", "position3DLow");
42809
42890
  return geometry;
42810
42891
  }
42892
+ /*
42893
+ * Where the ramp first paints something, as a normalised value.
42894
+ *
42895
+ * A style with a hidden floor is transparent over its lowest band, so filling a gap at zero would
42896
+ * fill it with nothing. This is the lowest value that actually shows.
42897
+ */
42898
+ function firstOpaqueStop(ramp) {
42899
+ const stops = ramp.length / 4;
42900
+ for (let i = 0; i < stops; i++) {
42901
+ if (ramp[i * 4 + 3] > 0) {
42902
+ return stops > 1 ? i / (stops - 1) : 0;
42903
+ }
42904
+ }
42905
+ return 0;
42906
+ }
42907
+ /*
42908
+ * Rasterises the clearance regions into a mask over the surface's extent, alpha inside.
42909
+ *
42910
+ * Drawn north row first, matching the value raster's own row order, so one set of texture
42911
+ * coordinates indexes both.
42912
+ */
42913
+ function rasteriseMask(extent, regions) {
42914
+ const spanX = extent.East - extent.West;
42915
+ const spanY = extent.North - extent.South;
42916
+ if (!(spanX > 0) || !(spanY > 0) || !regions.length) {
42917
+ return null;
42918
+ }
42919
+ // No document at all off the page, and the regions are still worth holding: the mask can be
42920
+ // rasterised the next time they are set, and until then the surface simply lifts nothing.
42921
+ if (typeof document === "undefined") {
42922
+ return null;
42923
+ }
42924
+ const wide = spanX >= spanY;
42925
+ const width = wide ? MASK_TEXTURE_MAX_SIDE : Math.max(1, Math.round(MASK_TEXTURE_MAX_SIDE * spanX / spanY));
42926
+ const height = wide ? Math.max(1, Math.round(MASK_TEXTURE_MAX_SIDE * spanY / spanX)) : MASK_TEXTURE_MAX_SIDE;
42927
+ const canvas = document.createElement("canvas");
42928
+ canvas.width = width;
42929
+ canvas.height = height;
42930
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
42931
+ if (!ctx) {
42932
+ return null;
42933
+ }
42934
+ ctx.clearRect(0, 0, width, height);
42935
+ ctx.fillStyle = "#ffffff";
42936
+ for (const region of regions) {
42937
+ const rings = region.rings || [];
42938
+ if (!rings.length) {
42939
+ continue;
42940
+ }
42941
+ ctx.beginPath();
42942
+ for (const ring of rings) {
42943
+ for (let i = 0; i + 1 < ring.length; i += 2) {
42944
+ const x = (ring[i] - extent.West) / spanX * width;
42945
+ const y = (extent.North - ring[i + 1]) / spanY * height;
42946
+ if (i === 0) {
42947
+ ctx.moveTo(x, y);
42948
+ }
42949
+ else {
42950
+ ctx.lineTo(x, y);
42951
+ }
42952
+ }
42953
+ ctx.closePath();
42954
+ }
42955
+ // Even odd, so a ring inside a ring is a hole without the caller having to wind them.
42956
+ ctx.fill("evenodd");
42957
+ }
42958
+ return { width, height, data: new Uint8Array(ctx.getImageData(0, 0, width, height).data.buffer.slice(0)) };
42959
+ }
42960
+ /*
42961
+ * The box every region fits inside, clipped to the surface, so the ground under them can be
42962
+ * sampled without sampling the whole extent.
42963
+ */
42964
+ function boxOf(extent, regions) {
42965
+ let west = Infinity;
42966
+ let east = -Infinity;
42967
+ let south = Infinity;
42968
+ let north = -Infinity;
42969
+ for (const region of regions) {
42970
+ for (const ring of region.rings || []) {
42971
+ for (let i = 0; i + 1 < ring.length; i += 2) {
42972
+ west = Math.min(west, ring[i]);
42973
+ east = Math.max(east, ring[i]);
42974
+ south = Math.min(south, ring[i + 1]);
42975
+ north = Math.max(north, ring[i + 1]);
42976
+ }
42977
+ }
42978
+ }
42979
+ if (!isFinite(west) || !isFinite(south)) {
42980
+ return null;
42981
+ }
42982
+ return {
42983
+ West: Math.max(west, extent.West),
42984
+ East: Math.min(east, extent.East),
42985
+ South: Math.max(south, extent.South),
42986
+ North: Math.min(north, extent.North)
42987
+ };
42988
+ }
42989
+ /*
42990
+ * Packs sampled terrain heights into a texture's RGB, three bytes a height.
42991
+ *
42992
+ * A height needs more than eight bits to be useful and the surface reads it in the vertex shader,
42993
+ * where float textures are not guaranteed, so it goes in as a fixed point integer.
42994
+ */
42995
+ function packHeights(sampled) {
42996
+ const pixels = new Uint8Array(sampled.length * 4);
42997
+ const span = GROUND_MAX_METRES - GROUND_MIN_METRES;
42998
+ for (let i = 0; i < sampled.length; i++) {
42999
+ const height = sampled[i] ? sampled[i].height : undefined;
43000
+ const known = typeof height === "number" && isFinite(height);
43001
+ const clamped = Math.min(GROUND_MAX_METRES, Math.max(GROUND_MIN_METRES, known ? height : 0));
43002
+ const packed = Math.round(((clamped - GROUND_MIN_METRES) / span) * 16777215);
43003
+ pixels[i * 4] = (packed >> 16) & 255;
43004
+ pixels[i * 4 + 1] = (packed >> 8) & 255;
43005
+ pixels[i * 4 + 2] = packed & 255;
43006
+ pixels[i * 4 + 3] = known ? 255 : 0;
43007
+ }
43008
+ return pixels;
43009
+ }
43010
+ /*
43011
+ * The finest terrain level a box can be sampled at inside the tile budget.
43012
+ *
43013
+ * Asked of the provider's own tiling scheme where it offers one, since a terrain set is free to
43014
+ * tile the world differently from the default two by one.
43015
+ */
43016
+ function groundLevelFor(box, provider) {
43017
+ const scheme = provider && provider.tilingScheme;
43018
+ for (let level = MASK_GROUND_LEVEL_MAX; level > MASK_GROUND_LEVEL_MIN; level--) {
43019
+ const across = scheme && typeof scheme.getNumberOfXTilesAtLevel === "function"
43020
+ ? scheme.getNumberOfXTilesAtLevel(level)
43021
+ : Math.pow(2, level + 1);
43022
+ const down = scheme && typeof scheme.getNumberOfYTilesAtLevel === "function"
43023
+ ? scheme.getNumberOfYTilesAtLevel(level)
43024
+ : Math.pow(2, level);
43025
+ const tiles = Math.ceil((box.East - box.West) / (360 / across) + 1)
43026
+ * Math.ceil((box.North - box.South) / (180 / down) + 1);
43027
+ if (tiles <= MASK_GROUND_TILE_BUDGET) {
43028
+ return level;
43029
+ }
43030
+ }
43031
+ return MASK_GROUND_LEVEL_MIN;
43032
+ }
42811
43033
  class Surface {
42812
43034
  constructor(options, hasCoverage) {
42813
43035
  var _a, _b, _c, _d;
@@ -42826,6 +43048,14 @@ void main() {
42826
43048
  this.groundTexture = null;
42827
43049
  this.groundPixels = null;
42828
43050
  this.groundRequested = false;
43051
+ this.groundClearance = null;
43052
+ this.maskTexture = null;
43053
+ this.maskPixels = null;
43054
+ this.maskGroundTexture = null;
43055
+ this.maskGroundPixels = null;
43056
+ this.maskGroundBox = null;
43057
+ this.maskGroundRequested = false;
43058
+ this.maskGroundLevel = 0;
42829
43059
  this.followGround = Boolean(options.followGround);
42830
43060
  this.terrainProvider = options.terrainProvider || null;
42831
43061
  this.extent = options.extent;
@@ -42838,7 +43068,9 @@ void main() {
42838
43068
  this.pixelPlacement = options.pixelPlacement;
42839
43069
  this.tileSkirts = Boolean(options.tileSkirts);
42840
43070
  this.rampPixels = RampLookup(options.rampStops || null, options.lowColor || DEFAULT_LOW_COLOR, options.highColor || DEFAULT_HIGH_COLOR);
43071
+ this.maskFillValue = firstOpaqueStop(this.rampPixels);
42841
43072
  this.tiles = buildTiles(hasCoverage);
43073
+ this.SetGroundClearance(options.groundClearance || null);
42842
43074
  }
42843
43075
  GetFollowsGround() {
42844
43076
  return this.followGround;
@@ -42861,6 +43093,97 @@ void main() {
42861
43093
  SetBaseHeight(baseHeight) {
42862
43094
  this.baseHeight = baseHeight;
42863
43095
  }
43096
+ GetGroundClearance() {
43097
+ return this.groundClearance;
43098
+ }
43099
+ /*
43100
+ * The terrain level the clearance regions were sampled at, once they have been.
43101
+ *
43102
+ * Worth reading rather than assuming: the level is chosen from how big the regions are, so a
43103
+ * region covering a whole sound gets a coarser test than a hand drawn harbour does.
43104
+ */
43105
+ GetGroundClearanceLevel() {
43106
+ return this.maskGroundLevel;
43107
+ }
43108
+ /*
43109
+ * What the clearance regions were measured against, once they have been.
43110
+ * The heights are read out of whatever the terrain stores, and a terrain referenced to sea
43111
+ * level reads a geoid apart from an archive delivering ellipsoidal metres.
43112
+ */
43113
+ GetGroundClearanceStats() {
43114
+ if (!this.maskGroundPixels) {
43115
+ return null;
43116
+ }
43117
+ const span = GROUND_MAX_METRES - GROUND_MIN_METRES;
43118
+ let min = Infinity;
43119
+ let max = -Infinity;
43120
+ let samples = 0;
43121
+ for (let i = 0; i < this.maskGroundPixels.length / 4; i++) {
43122
+ if (!this.maskGroundPixels[i * 4 + 3]) {
43123
+ continue;
43124
+ }
43125
+ const packed = (this.maskGroundPixels[i * 4] << 16)
43126
+ + (this.maskGroundPixels[i * 4 + 1] << 8) + this.maskGroundPixels[i * 4 + 2];
43127
+ const height = GROUND_MIN_METRES + (packed / 16777215) * span;
43128
+ min = Math.min(min, height);
43129
+ max = Math.max(max, height);
43130
+ samples++;
43131
+ }
43132
+ if (!samples) {
43133
+ return null;
43134
+ }
43135
+ return { level: this.maskGroundLevel, min, max, samples };
43136
+ }
43137
+ /*
43138
+ * The real world range the texture's 0 to 1 maps onto, so a caller can compare it against what
43139
+ * the terrain says without having kept its own copy.
43140
+ */
43141
+ GetValueRange() {
43142
+ return { min: this.valueMin, max: this.valueMax };
43143
+ }
43144
+ /*
43145
+ * Takes a different terrain to measure against.
43146
+ * The clearance raster is sampled once and cached, so a scene that swaps its terrain would
43147
+ * otherwise keep holding the sheet above heights read from the terrain it used to have.
43148
+ */
43149
+ SetTerrainProvider(provider) {
43150
+ if (provider === this.terrainProvider) {
43151
+ return;
43152
+ }
43153
+ this.terrainProvider = provider;
43154
+ this.groundPixels = null;
43155
+ this.groundRequested = false;
43156
+ this.maskGroundPixels = null;
43157
+ this.maskGroundRequested = false;
43158
+ this.maskGroundLevel = 0;
43159
+ }
43160
+ /*
43161
+ * Takes a new set of clearance regions, or drops them.
43162
+ * The mask is rasterised here rather than on the render thread, and the ground under the
43163
+ * regions is re-requested, so a plugin can hand over regions the moment it has them.
43164
+ */
43165
+ SetGroundClearance(clearance) {
43166
+ this.groundClearance = clearance && clearance.regions && clearance.regions.length
43167
+ ? clearance
43168
+ : null;
43169
+ this.maskPixels = this.groundClearance
43170
+ ? rasteriseMask(this.extent, this.groundClearance.regions)
43171
+ : null;
43172
+ if (this.maskTexture) {
43173
+ this.maskTexture.destroy();
43174
+ this.maskTexture = null;
43175
+ }
43176
+ this.maskGroundPixels = null;
43177
+ this.maskGroundRequested = false;
43178
+ this.maskGroundLevel = 0;
43179
+ this.maskGroundBox = this.groundClearance
43180
+ ? boxOf(this.extent, this.groundClearance.regions)
43181
+ : null;
43182
+ if (this.maskGroundTexture) {
43183
+ this.maskGroundTexture.destroy();
43184
+ this.maskGroundTexture = null;
43185
+ }
43186
+ }
42864
43187
  /**
42865
43188
  * Feeds the surface from an animated frame series.
42866
43189
  * Height and colour advance with the clock without the caller having to push each frame in.
@@ -42899,6 +43222,8 @@ void main() {
42899
43222
  this.syncRampTexture(context);
42900
43223
  this.requestGround();
42901
43224
  this.syncGroundTexture(context);
43225
+ this.requestMaskGround();
43226
+ this.syncMaskTextures(context);
42902
43227
  // A tile needs one mesh before it has a bounding sphere to measure against.
42903
43228
  for (const tile of this.tiles) {
42904
43229
  if (!tile.centre) {
@@ -42943,6 +43268,14 @@ void main() {
42943
43268
  this.groundTexture.destroy();
42944
43269
  this.groundTexture = null;
42945
43270
  }
43271
+ if (this.maskTexture) {
43272
+ this.maskTexture.destroy();
43273
+ this.maskTexture = null;
43274
+ }
43275
+ if (this.maskGroundTexture) {
43276
+ this.maskGroundTexture.destroy();
43277
+ this.maskGroundTexture = null;
43278
+ }
42946
43279
  return undefined;
42947
43280
  }
42948
43281
  /*
@@ -42975,24 +43308,123 @@ void main() {
42975
43308
  if (this.destroyed || !sampled) {
42976
43309
  return;
42977
43310
  }
42978
- const pixels = new Uint8Array(GROUND_SAMPLES_PER_SIDE * GROUND_SAMPLES_PER_SIDE * 4);
42979
- const span = GROUND_MAX_METRES - GROUND_MIN_METRES;
42980
- for (let i = 0; i < sampled.length; i++) {
42981
- const height = sampled[i] && isFinite(sampled[i].height) ? sampled[i].height : 0;
42982
- const clamped = Math.min(GROUND_MAX_METRES, Math.max(GROUND_MIN_METRES, height));
42983
- const packed = Math.round(((clamped - GROUND_MIN_METRES) / span) * 16777215);
42984
- pixels[i * 4] = (packed >> 16) & 255;
42985
- pixels[i * 4 + 1] = (packed >> 8) & 255;
42986
- pixels[i * 4 + 2] = packed & 255;
42987
- pixels[i * 4 + 3] = 255;
42988
- }
42989
- this.groundPixels = pixels;
43311
+ this.groundPixels = packHeights(sampled);
42990
43312
  })
42991
43313
  .catch(() => {
42992
43314
  // Terrain that cannot be sampled leaves the surface on the ellipsoid, which is
42993
43315
  // where a clamped polygon sat before this existed.
42994
43316
  });
42995
43317
  }
43318
+ /*
43319
+ * Samples the terrain under the clearance regions.
43320
+ *
43321
+ * Its own raster rather than the surface's, because the surface extent here is two degrees
43322
+ * wide and the region is a few kilometres, and a clearance test needs the finer one.
43323
+ */
43324
+ requestMaskGround() {
43325
+ if (this.maskGroundRequested || !this.groundClearance || !this.maskGroundBox) {
43326
+ return;
43327
+ }
43328
+ if (!(this.groundClearance.clearance > NO_LIMIT_METRES / 2)) {
43329
+ return;
43330
+ }
43331
+ this.maskGroundRequested = true;
43332
+ const sampler = sampleTerrain;
43333
+ if (!this.terrainProvider || typeof sampler !== "function") {
43334
+ return;
43335
+ }
43336
+ const box = this.maskGroundBox;
43337
+ const level = groundLevelFor(box, this.terrainProvider);
43338
+ this.maskGroundLevel = level;
43339
+ const total = MASK_GROUND_SAMPLES_PER_SIDE * MASK_GROUND_SAMPLES_PER_SIDE;
43340
+ const scheme = this.terrainProvider.tilingScheme;
43341
+ const asked = [];
43342
+ // Which slot in the raster each asked-for position belongs to, since the ones the terrain
43343
+ // does not cover are never asked for at all.
43344
+ const slots = [];
43345
+ for (let row = 0; row < MASK_GROUND_SAMPLES_PER_SIDE; row++) {
43346
+ const fy = row / (MASK_GROUND_SAMPLES_PER_SIDE - 1);
43347
+ const latitude = Math$1.lerp(box.North, box.South, fy);
43348
+ for (let col = 0; col < MASK_GROUND_SAMPLES_PER_SIDE; col++) {
43349
+ const fx = col / (MASK_GROUND_SAMPLES_PER_SIDE - 1);
43350
+ const at = Cartographic.fromDegrees(Math$1.lerp(box.West, box.East, fx), latitude);
43351
+ // An imported terrain covers a survey area, so asking outside it answers 404 and
43352
+ // fills the console with failures for points that were never going to have a height.
43353
+ if (scheme && typeof this.terrainProvider.getTileDataAvailable === "function") {
43354
+ const tile = scheme.positionToTileXY(at, level);
43355
+ if (!tile || this.terrainProvider.getTileDataAvailable(tile.x, tile.y, level) === false) {
43356
+ continue;
43357
+ }
43358
+ }
43359
+ asked.push(at);
43360
+ slots.push(row * MASK_GROUND_SAMPLES_PER_SIDE + col);
43361
+ }
43362
+ }
43363
+ if (!asked.length) {
43364
+ return;
43365
+ }
43366
+ Promise.resolve(sampler(this.terrainProvider, level, asked))
43367
+ .then((sampled) => {
43368
+ if (this.destroyed || !sampled) {
43369
+ return;
43370
+ }
43371
+ const spread = new Array(total).fill(undefined);
43372
+ for (let i = 0; i < slots.length; i++) {
43373
+ spread[slots[i]] = sampled[i];
43374
+ }
43375
+ this.maskGroundPixels = packHeights(spread);
43376
+ })
43377
+ .catch(() => {
43378
+ // Unsampled terrain leaves only the region's own floor to hold the sheet up,
43379
+ // which is the behaviour a region with no clearance already has.
43380
+ });
43381
+ }
43382
+ /*
43383
+ * Uploads the region mask and the ground under it, with a one texel stand-in until they land
43384
+ * so the shader always has something bound.
43385
+ */
43386
+ syncMaskTextures(context) {
43387
+ if (!this.maskTexture) {
43388
+ const held = this.maskPixels;
43389
+ this.maskTexture = new Texture({
43390
+ context,
43391
+ pixelFormat: PixelFormat.RGBA,
43392
+ source: held
43393
+ ? { width: held.width, height: held.height, arrayBufferView: held.data }
43394
+ : { width: 1, height: 1, arrayBufferView: new Uint8Array([0, 0, 0, 0]) },
43395
+ flipY: false,
43396
+ sampler: new Sampler({
43397
+ wrapS: TextureWrap.CLAMP_TO_EDGE,
43398
+ wrapT: TextureWrap.CLAMP_TO_EDGE
43399
+ })
43400
+ });
43401
+ }
43402
+ if (this.maskGroundTexture && (!this.maskGroundPixels
43403
+ || this.maskGroundTexture.width === MASK_GROUND_SAMPLES_PER_SIDE)) {
43404
+ return;
43405
+ }
43406
+ if (this.maskGroundTexture) {
43407
+ this.maskGroundTexture.destroy();
43408
+ }
43409
+ const size = this.maskGroundPixels ? MASK_GROUND_SAMPLES_PER_SIDE : 1;
43410
+ this.maskGroundTexture = new Texture({
43411
+ context,
43412
+ pixelFormat: PixelFormat.RGBA,
43413
+ source: {
43414
+ width: size,
43415
+ height: size,
43416
+ arrayBufferView: this.maskGroundPixels || packHeights([{ height: 0 }])
43417
+ },
43418
+ flipY: false,
43419
+ sampler: new Sampler({
43420
+ wrapS: TextureWrap.CLAMP_TO_EDGE,
43421
+ wrapT: TextureWrap.CLAMP_TO_EDGE,
43422
+ // Nearest, because the shader interpolates decoded heights itself.
43423
+ minificationFilter: TextureMinificationFilter.NEAREST,
43424
+ magnificationFilter: TextureMagnificationFilter.NEAREST
43425
+ })
43426
+ });
43427
+ }
42996
43428
  /*
42997
43429
  * Uploads whatever ground samples exist, and a flat stand-in until they arrive, so the
42998
43430
  * shader always has a texture bound whether or not this surface follows the ground.
@@ -43004,10 +43436,8 @@ void main() {
43004
43436
  if (this.groundTexture) {
43005
43437
  this.groundTexture.destroy();
43006
43438
  }
43007
- const zero = Math.round(((0 - GROUND_MIN_METRES) / (GROUND_MAX_METRES - GROUND_MIN_METRES)) * 16777215);
43008
43439
  const size = this.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1;
43009
- const arrayBufferView = this.groundPixels
43010
- || new Uint8Array([(zero >> 16) & 255, (zero >> 8) & 255, zero & 255, 255]);
43440
+ const arrayBufferView = this.groundPixels || packHeights([{ height: 0 }]);
43011
43441
  this.groundTexture = new Texture({
43012
43442
  context,
43013
43443
  pixelFormat: PixelFormat.RGBA,
@@ -43216,6 +43646,28 @@ void main() {
43216
43646
  u_groundWeight: () => (self.followGround && self.groundPixels ? 1 : 0),
43217
43647
  u_groundRange: () => new Cartesian2(GROUND_MIN_METRES, GROUND_MAX_METRES),
43218
43648
  u_groundSize: () => new Cartesian2(self.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1, self.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1),
43649
+ u_maskTexture: () => self.maskTexture,
43650
+ u_maskGroundTexture: () => self.maskGroundTexture,
43651
+ u_maskWeight: () => (self.groundClearance && self.maskPixels ? 1 : 0),
43652
+ u_maskGroundWeight: () => (self.maskGroundPixels ? 1 : 0),
43653
+ u_maskFill: () => (self.groundClearance && self.groundClearance.fillGaps ? 1 : 0),
43654
+ u_maskFillValue: () => self.maskFillValue,
43655
+ u_maskClearance: () => (self.groundClearance
43656
+ && typeof self.groundClearance.clearance === "number"
43657
+ && self.maskGroundPixels
43658
+ ? self.groundClearance.clearance
43659
+ : NO_LIMIT_METRES),
43660
+ u_maskFloor: () => (self.groundClearance
43661
+ && typeof self.groundClearance.floor === "number"
43662
+ ? self.groundClearance.floor
43663
+ : NO_LIMIT_METRES),
43664
+ u_maskGroundSize: () => new Cartesian2(self.maskGroundPixels ? MASK_GROUND_SAMPLES_PER_SIDE : 1, self.maskGroundPixels ? MASK_GROUND_SAMPLES_PER_SIDE : 1),
43665
+ u_extentDegrees: () => new Cartesian4(self.extent.West, self.extent.East, self.extent.North, self.extent.South),
43666
+ u_maskGroundDegrees: () => {
43667
+ const box = self.maskGroundBox || self.extent;
43668
+ return new Cartesian4(box.West, box.East, box.North, box.South);
43669
+ },
43670
+ u_coverageCutoffVertex: () => COVERAGE_CUTOFF,
43219
43671
  u_metresPerTexel: () => self.metresPerTexel(),
43220
43672
  u_valueMin: () => self.valueMin,
43221
43673
  u_valueRange: () => self.valueMax - self.valueMin,
@@ -43852,6 +44304,34 @@ var EntityRenderEnginePolygon;
43852
44304
  return cEntities;
43853
44305
  }
43854
44306
  EntityRenderEnginePolygon.RenderGroup = RenderGroup;
44307
+ /**
44308
+ * The displaced surface a cEntity is drawing, if it is drawing one.
44309
+ *
44310
+ * Exposed so a background plugin can hand a rendered polygon the water regions it is supposed to
44311
+ * stay above without having to know where the surface is kept.
44312
+ * @param cEntity
44313
+ */
44314
+ function GetDisplacedSurface(cEntity) {
44315
+ return (cEntity === null || cEntity === void 0 ? void 0 : cEntity[DISPLACED_SURFACE_KEY]) || null;
44316
+ }
44317
+ EntityRenderEnginePolygon.GetDisplacedSurface = GetDisplacedSurface;
44318
+ /**
44319
+ * Hands every rendered polygon in a set the regions its sheet must stay above.
44320
+ * @param cEntities
44321
+ * @param clearance Pass null to drop the regions again.
44322
+ */
44323
+ function SetGroundClearance(cEntities, clearance) {
44324
+ let touched = 0;
44325
+ for (const cEntity of cEntities || []) {
44326
+ const surface = GetDisplacedSurface(cEntity);
44327
+ if (surface) {
44328
+ surface.SetGroundClearance(clearance);
44329
+ touched++;
44330
+ }
44331
+ }
44332
+ return touched;
44333
+ }
44334
+ EntityRenderEnginePolygon.SetGroundClearance = SetGroundClearance;
43855
44335
  /**
43856
44336
  * Disposes a cEntity's TextureFrameSeriesAnimator.Animator (if any).
43857
44337
  */
@@ -45914,7 +46394,7 @@ var StyleUtils;
45914
46394
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
45915
46395
  })(StyleUtils || (StyleUtils = {}));
45916
46396
 
45917
- const VERSION = "7.2.9";
46397
+ const VERSION = "7.3.1";
45918
46398
  /**
45919
46399
  * Updates the environment instance used by bruce-cesium to one specified.
45920
46400
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.