bruce-cesium 7.0.2 → 7.0.3

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.
Files changed (80) hide show
  1. package/dist/bruce-cesium.es5.js +1370 -243
  2. package/dist/bruce-cesium.es5.js.map +1 -1
  3. package/dist/bruce-cesium.umd.js +1368 -241
  4. package/dist/bruce-cesium.umd.js.map +1 -1
  5. package/dist/lib/bruce-cesium.js +1 -1
  6. package/dist/lib/internal/cesium-show-gate.js +100 -0
  7. package/dist/lib/internal/cesium-show-gate.js.map +1 -0
  8. package/dist/lib/internal/cesium-utils.js +207 -1
  9. package/dist/lib/internal/cesium-utils.js.map +1 -1
  10. package/dist/lib/internal/js-utils.js +25 -1
  11. package/dist/lib/internal/js-utils.js.map +1 -1
  12. package/dist/lib/rendering/cesium-animated-in-out.js +66 -7
  13. package/dist/lib/rendering/cesium-animated-in-out.js.map +1 -1
  14. package/dist/lib/rendering/entity-render-engine-model3d.js +17 -8
  15. package/dist/lib/rendering/entity-render-engine-model3d.js.map +1 -1
  16. package/dist/lib/rendering/entity-render-engine-point.js +46 -12
  17. package/dist/lib/rendering/entity-render-engine-point.js.map +1 -1
  18. package/dist/lib/rendering/entity-render-engine-polygon.js +107 -35
  19. package/dist/lib/rendering/entity-render-engine-polygon.js.map +1 -1
  20. package/dist/lib/rendering/entity-render-engine-polyline.js +27 -8
  21. package/dist/lib/rendering/entity-render-engine-polyline.js.map +1 -1
  22. package/dist/lib/rendering/entity-render-engine.js +86 -14
  23. package/dist/lib/rendering/entity-render-engine.js.map +1 -1
  24. package/dist/lib/rendering/menu-item-manager.js +30 -2
  25. package/dist/lib/rendering/menu-item-manager.js.map +1 -1
  26. package/dist/lib/rendering/relation-render-engine.js +2 -1
  27. package/dist/lib/rendering/relation-render-engine.js.map +1 -1
  28. package/dist/lib/rendering/render-managers/common/entity-label.js +10 -2
  29. package/dist/lib/rendering/render-managers/common/entity-label.js.map +1 -1
  30. package/dist/lib/rendering/render-managers/entities/entities-datalab-render-manager.js +45 -3
  31. package/dist/lib/rendering/render-managers/entities/entities-datalab-render-manager.js.map +1 -1
  32. package/dist/lib/rendering/render-managers/entities/entities-loaded-render-manager.js +26 -15
  33. package/dist/lib/rendering/render-managers/entities/entities-loaded-render-manager.js.map +1 -1
  34. package/dist/lib/rendering/render-managers/entities/entities-render-manager.js +50 -3
  35. package/dist/lib/rendering/render-managers/entities/entities-render-manager.js.map +1 -1
  36. package/dist/lib/rendering/render-managers/other/legacy-relations-render-manager.js +3 -2
  37. package/dist/lib/rendering/render-managers/other/legacy-relations-render-manager.js.map +1 -1
  38. package/dist/lib/rendering/render-managers/other/relations-render-manager.js +3 -2
  39. package/dist/lib/rendering/render-managers/other/relations-render-manager.js.map +1 -1
  40. package/dist/lib/rendering/render-managers/render-manager.js +67 -3
  41. package/dist/lib/rendering/render-managers/render-manager.js.map +1 -1
  42. package/dist/lib/rendering/render-managers/tilesets/tileset-arb-render-manager.js +2 -0
  43. package/dist/lib/rendering/render-managers/tilesets/tileset-arb-render-manager.js.map +1 -1
  44. package/dist/lib/rendering/render-managers/tilesets/tileset-cad-render-manager.js +2 -0
  45. package/dist/lib/rendering/render-managers/tilesets/tileset-cad-render-manager.js.map +1 -1
  46. package/dist/lib/rendering/render-managers/tilesets/tileset-entities-render-manager.js +2 -0
  47. package/dist/lib/rendering/render-managers/tilesets/tileset-entities-render-manager.js.map +1 -1
  48. package/dist/lib/rendering/render-managers/tilesets/tileset-pointcloud-render-manager.js +4 -1
  49. package/dist/lib/rendering/render-managers/tilesets/tileset-pointcloud-render-manager.js.map +1 -1
  50. package/dist/lib/rendering/texture-frame-series-animator.js +7 -0
  51. package/dist/lib/rendering/texture-frame-series-animator.js.map +1 -1
  52. package/dist/lib/rendering/tileset-render-engine.js +18 -0
  53. package/dist/lib/rendering/tileset-render-engine.js.map +1 -1
  54. package/dist/lib/rendering/visual-register-culler.js +191 -97
  55. package/dist/lib/rendering/visual-register-culler.js.map +1 -1
  56. package/dist/lib/rendering/visuals-register.js +263 -25
  57. package/dist/lib/rendering/visuals-register.js.map +1 -1
  58. package/dist/lib/utils/cesium-entity-styler.js +7 -3
  59. package/dist/lib/utils/cesium-entity-styler.js.map +1 -1
  60. package/dist/types/bruce-cesium.d.ts +1 -1
  61. package/dist/types/internal/cesium-show-gate.d.ts +29 -0
  62. package/dist/types/internal/cesium-utils.d.ts +63 -0
  63. package/dist/types/internal/js-utils.d.ts +10 -0
  64. package/dist/types/rendering/cesium-animated-in-out.d.ts +5 -0
  65. package/dist/types/rendering/entity-render-engine-model3d.d.ts +2 -0
  66. package/dist/types/rendering/entity-render-engine-point.d.ts +2 -0
  67. package/dist/types/rendering/entity-render-engine-polygon.d.ts +2 -0
  68. package/dist/types/rendering/entity-render-engine-polyline.d.ts +2 -0
  69. package/dist/types/rendering/entity-render-engine.d.ts +17 -1
  70. package/dist/types/rendering/menu-item-manager.d.ts +6 -0
  71. package/dist/types/rendering/render-managers/entities/entities-datalab-render-manager.d.ts +11 -0
  72. package/dist/types/rendering/render-managers/entities/entities-render-manager.d.ts +12 -0
  73. package/dist/types/rendering/render-managers/render-manager.d.ts +33 -0
  74. package/dist/types/rendering/render-managers/tilesets/tileset-pointcloud-render-manager.d.ts +3 -0
  75. package/dist/types/rendering/texture-frame-series-animator.d.ts +5 -0
  76. package/dist/types/rendering/tileset-render-engine.d.ts +14 -0
  77. package/dist/types/rendering/visual-register-culler.d.ts +7 -0
  78. package/dist/types/rendering/visuals-register.d.ts +65 -1
  79. package/dist/types/utils/cesium-entity-styler.d.ts +1 -0
  80. package/package.json +1 -1
@@ -21,6 +21,28 @@
21
21
  }
22
22
  return value;
23
23
  }
24
+ /**
25
+ * Returns an entity's own name when it is good enough to use as-is, or null when it has to be resolved properly.
26
+ * @param entity
27
+ */
28
+ function GetUsableEntityName(entity) {
29
+ var _a;
30
+ return IsUsableEntityName((_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.Name) ? String(entity.Bruce.Name).trim() : null;
31
+ }
32
+ /**
33
+ * Returns whether a name is a real one rather than empty or a placeholder.
34
+ * @param name
35
+ */
36
+ function IsUsableEntityName(name) {
37
+ if (typeof name !== "string") {
38
+ return false;
39
+ }
40
+ const trimmed = name.trim();
41
+ if (!trimmed) {
42
+ return false;
43
+ }
44
+ return !trimmed.toLowerCase().includes("unnamed");
45
+ }
24
46
 
25
47
  (function (MeasureUtils) {
26
48
  /**
@@ -1955,7 +1977,11 @@
1955
1977
  }
1956
1978
  }
1957
1979
  else if (graphic instanceof Cesium.CorridorGraphics) {
1958
- graphic.material = new Cesium.ColorMaterialProperty(color);
1980
+ // See if it's changed before applying, same flicker/rebuild concern as the polyline above.
1981
+ const currentColor = calculateCurColor(viewer, graphic);
1982
+ if (currentColor == null || !currentColor.equals(color)) {
1983
+ graphic.material = new Cesium.ColorMaterialProperty(color);
1984
+ }
1959
1985
  }
1960
1986
  else if (graphic instanceof Cesium.PointGraphics) {
1961
1987
  const animateColor = new exports.CesiumAnimatedProperty.AnimateColor({
@@ -2198,11 +2224,11 @@
2198
2224
  * @returns
2199
2225
  */
2200
2226
  function SetDefaultColor(params) {
2201
- const { viewer, entity, requestRender, override } = params;
2227
+ const { viewer, entity, requestRender, override, includeSiblings } = params;
2202
2228
  if (!entity) {
2203
2229
  return;
2204
2230
  }
2205
- const parts = exports.EntityUtils.GatherEntity({
2231
+ const parts = includeSiblings == false ? [entity] : exports.EntityUtils.GatherEntity({
2206
2232
  entity: entity,
2207
2233
  selectable: true
2208
2234
  });
@@ -3835,6 +3861,202 @@
3835
3861
  }
3836
3862
  return obj;
3837
3863
  }
3864
+ /*
3865
+ * Marks an EntityCollection as one this library created and fully controls (a menu item's own data source).
3866
+ */
3867
+ const OWNED_CCOLLECTION_KEY = "_bruceOwnedCollection";
3868
+ function MarkCCollectionOwned(collection) {
3869
+ if (collection) {
3870
+ collection[OWNED_CCOLLECTION_KEY] = true;
3871
+ }
3872
+ }
3873
+ function IsCCollectionOwned(collection) {
3874
+ return Boolean(collection && collection[OWNED_CCOLLECTION_KEY]);
3875
+ }
3876
+ /**
3877
+ * Runs a bulk collection mutation with every given collection's events suspended, so Cesium raises one
3878
+ * collectionChanged per collection for the whole pass instead of one per entity.
3879
+ *
3880
+ * Takes a set of collections rather than assuming `viewer.entities`, because managed visuals live in their menu
3881
+ * item's own data source: suspending the wrong collection silently does nothing.
3882
+ * @param collections Duplicates and empty entries are ignored.
3883
+ * @param run
3884
+ */
3885
+ function WithSuspendedCCollections(collections, run) {
3886
+ const unique = [];
3887
+ for (const collection of collections) {
3888
+ if (collection && unique.indexOf(collection) === -1) {
3889
+ unique.push(collection);
3890
+ }
3891
+ }
3892
+ for (const collection of unique) {
3893
+ collection.suspendEvents();
3894
+ }
3895
+ try {
3896
+ return run();
3897
+ }
3898
+ finally {
3899
+ for (const collection of unique) {
3900
+ try {
3901
+ collection.resumeEvents();
3902
+ }
3903
+ catch (e) {
3904
+ console.error(e);
3905
+ }
3906
+ }
3907
+ }
3908
+ }
3909
+ /**
3910
+ * Returns the collection a graphic belongs to, falling back to the viewer's own when it has never been added.
3911
+ * @param viewer
3912
+ * @param entity
3913
+ */
3914
+ function GetCEntityCollection(viewer, entity) {
3915
+ const owner = entity === null || entity === void 0 ? void 0 : entity.entityCollection;
3916
+ return owner ? owner : viewer === null || viewer === void 0 ? void 0 : viewer.entities;
3917
+ }
3918
+ /**
3919
+ * Returns whether a graphic is currently in the scene, looking in whichever collection owns it.
3920
+ * @param viewer
3921
+ * @param entity
3922
+ */
3923
+ function IsCEntityInScene(viewer, entity) {
3924
+ const collection = GetCEntityCollection(viewer, entity);
3925
+ return Boolean(collection && entity && collection.contains(entity));
3926
+ }
3927
+ /**
3928
+ * Removes a graphic from whichever collection owns it. Returns whether it removed anything.
3929
+ * @param viewer
3930
+ * @param entity
3931
+ */
3932
+ function RemoveCEntityFromScene(viewer, entity) {
3933
+ var _a;
3934
+ if (!entity) {
3935
+ return false;
3936
+ }
3937
+ const collection = GetCEntityCollection(viewer, entity);
3938
+ if (collection === null || collection === void 0 ? void 0 : collection.contains(entity)) {
3939
+ return collection.remove(entity);
3940
+ }
3941
+ // A stale owner reference and the graphic ended up in the viewer's collection anyway.
3942
+ if (collection !== (viewer === null || viewer === void 0 ? void 0 : viewer.entities) && ((_a = viewer === null || viewer === void 0 ? void 0 : viewer.entities) === null || _a === void 0 ? void 0 : _a.contains(entity))) {
3943
+ return viewer.entities.remove(entity);
3944
+ }
3945
+ return false;
3946
+ }
3947
+ /**
3948
+ * Adds a graphic back into whichever collection owns it, or the viewer's if it has no owner yet.
3949
+ * @param viewer
3950
+ * @param entity
3951
+ */
3952
+ function AddCEntityToScene(viewer, entity, fallbackCollection) {
3953
+ if (!entity) {
3954
+ return false;
3955
+ }
3956
+ // An existing owner always wins, so a graphic is never migrated out from under whoever holds it.
3957
+ const owner = entity.entityCollection;
3958
+ const collection = owner ? owner : (fallbackCollection ? fallbackCollection : viewer === null || viewer === void 0 ? void 0 : viewer.entities);
3959
+ if (!collection || collection.contains(entity)) {
3960
+ return false;
3961
+ }
3962
+ collection.add(entity);
3963
+ return true;
3964
+ }
3965
+ /**
3966
+ * Returns whether toggling `show` on these graphics is safe, rather than having to add/remove them from the
3967
+ * entity collection to hide them.
3968
+ *
3969
+ * Toggling show is far cheaper than re-adding the graphic to the viewer however a Cesium bug means we can't always use it.
3970
+ * See: https://github.com/CesiumGS/cesium/issues/9370
3971
+ * @param parts The graphic and any sibling graphics, eg. from EntityUtils.GatherEntity.
3972
+ */
3973
+ function CanToggleShowSafely(parts) {
3974
+ if (!(parts === null || parts === void 0 ? void 0 : parts.length)) {
3975
+ return false;
3976
+ }
3977
+ for (const part of parts) {
3978
+ if (!(part instanceof Cesium.Entity)) {
3979
+ // Not an Entity, so not something whose `show` we can drive.
3980
+ return false;
3981
+ }
3982
+ if (part.polyline && isTruthyCValue(part.polyline.clampToGround)) {
3983
+ return false;
3984
+ }
3985
+ const materialGraphics = [part.polygon, part.corridor, part.ellipse, part.rectangle, part.wall, part.polylineVolume];
3986
+ for (const graphic of materialGraphics) {
3987
+ if (graphic && !isPlainColorMaterial(graphic.material)) {
3988
+ return false;
3989
+ }
3990
+ }
3991
+ }
3992
+ return true;
3993
+ }
3994
+ /**
3995
+ * Returns whether a material is a plain colour, which is what keeps a graphic out of Cesium's per-material batches.
3996
+ * Anything unrecognised counts as not plain, so callers stay on the conservative side.
3997
+ * @param material
3998
+ */
3999
+ function isPlainColorMaterial(material) {
4000
+ if (material == null) {
4001
+ // Cesium defaults an absent material to white, a plain colour.
4002
+ return true;
4003
+ }
4004
+ return material instanceof Cesium.ColorMaterialProperty || material instanceof Cesium.Color;
4005
+ }
4006
+ /**
4007
+ * Resolves a possibly-property boolean without needing a viewer clock, treating anything undeterminable as true
4008
+ * so callers stay on the conservative side.
4009
+ * @param value
4010
+ */
4011
+ function isTruthyCValue(value) {
4012
+ if (value == null) {
4013
+ return false;
4014
+ }
4015
+ if (typeof value.getValue === "function") {
4016
+ if (value instanceof Cesium.ConstantProperty) {
4017
+ return Boolean(value.valueOf());
4018
+ }
4019
+ // A non-constant property could be true at any moment, so assume it is.
4020
+ return true;
4021
+ }
4022
+ return Boolean(value);
4023
+ }
4024
+ /**
4025
+ * Returns if two resolved Cesium property values are equal, using the value's own equals() when it has one.
4026
+ * @param a
4027
+ * @param b
4028
+ */
4029
+ function AreCValuesEqual(a, b) {
4030
+ if (a === b) {
4031
+ return true;
4032
+ }
4033
+ // One side is null/undefined and the other isn't.
4034
+ if (a == null || b == null) {
4035
+ return false;
4036
+ }
4037
+ if (typeof a.equals === "function") {
4038
+ return a.equals(b) === true;
4039
+ }
4040
+ return false;
4041
+ }
4042
+ /**
4043
+ * Wraps a value in a ConstantProperty and assigns it to a graphic's property, but only when the resolved
4044
+ * value actually differs. Returns whether it assigned.
4045
+ * @param params
4046
+ */
4047
+ function SetCPropIfChanged(params) {
4048
+ const { viewer, graphic, property, value, compare } = params;
4049
+ if (!graphic) {
4050
+ return false;
4051
+ }
4052
+ const oldValue = GetCValue(viewer, graphic[property]);
4053
+ const isEqual = compare ? compare(oldValue, value) : AreCValuesEqual(oldValue, value);
4054
+ if (isEqual) {
4055
+ return false;
4056
+ }
4057
+ graphic[property] = new Cesium.ConstantProperty(value);
4058
+ return true;
4059
+ }
3838
4060
  function ColorToCColor(color) {
3839
4061
  return new Cesium.Color(color.red ? color.red / 255 : 0, color.green ? color.green / 255 : 0, color.blue ? color.blue / 255 : 0, color.alpha);
3840
4062
  }
@@ -4322,6 +4544,85 @@
4322
4544
  ViewUtils.GetModelSpace = GetModelSpace;
4323
4545
  })(exports.ViewUtils || (exports.ViewUtils = {}));
4324
4546
 
4547
+ /*
4548
+ * Lets something decide WHEN a `show` flag is honoured, without owning WHAT it should be.
4549
+ */
4550
+ const SHOW_GATE_KEY = "_bruceShowGate";
4551
+ /**
4552
+ * Installs a show gate on an object, or returns the one already installed.
4553
+ * Returns null when the property cannot be intercepted or restored, in which case the caller must leave `show` alone rather than fall back to writing it directly.
4554
+ * @param target Anything with a plain, configurable `show` property.
4555
+ */
4556
+ function InstallShowGate(target) {
4557
+ if (!target) {
4558
+ return null;
4559
+ }
4560
+ const existing = target[SHOW_GATE_KEY];
4561
+ if (existing) {
4562
+ return existing;
4563
+ }
4564
+ const descriptor = Object.getOwnPropertyDescriptor(target, "show");
4565
+ // An accessor already, or something we could not put back, so refuse rather than half-apply.
4566
+ if (descriptor && (!descriptor.configurable || descriptor.get || descriptor.set)) {
4567
+ return null;
4568
+ }
4569
+ let intended = target.show !== false;
4570
+ let suppressed = false;
4571
+ try {
4572
+ Object.defineProperty(target, "show", {
4573
+ configurable: true,
4574
+ enumerable: descriptor ? descriptor.enumerable : true,
4575
+ get: () => intended && !suppressed,
4576
+ set: (value) => {
4577
+ intended = value !== false;
4578
+ }
4579
+ });
4580
+ }
4581
+ catch (e) {
4582
+ console.error("Failed to install a show gate.", e);
4583
+ return null;
4584
+ }
4585
+ const gate = {
4586
+ get Intended() {
4587
+ return intended;
4588
+ },
4589
+ get Suppressed() {
4590
+ return suppressed;
4591
+ },
4592
+ SetSuppressed(value) {
4593
+ const next = Boolean(value);
4594
+ if (next === suppressed) {
4595
+ return false;
4596
+ }
4597
+ const before = intended && !suppressed;
4598
+ suppressed = next;
4599
+ return (intended && !suppressed) !== before;
4600
+ },
4601
+ Release() {
4602
+ try {
4603
+ delete target[SHOW_GATE_KEY];
4604
+ Object.defineProperty(target, "show", {
4605
+ configurable: true,
4606
+ enumerable: descriptor ? descriptor.enumerable : true,
4607
+ writable: true,
4608
+ // The intent, not the gated value: releasing must not leave something culled forever.
4609
+ value: intended
4610
+ });
4611
+ }
4612
+ catch (e) {
4613
+ console.error("Failed to release a show gate.", e);
4614
+ }
4615
+ }
4616
+ };
4617
+ Object.defineProperty(target, SHOW_GATE_KEY, {
4618
+ value: gate,
4619
+ configurable: true,
4620
+ enumerable: false,
4621
+ writable: true
4622
+ });
4623
+ return gate;
4624
+ }
4625
+
4325
4626
  /**
4326
4627
  * Returns cesium property's value.
4327
4628
  * This will check if it's one that changes over time, or just a fixed value.
@@ -4395,10 +4696,17 @@
4395
4696
  * Loads the Entity's name and updates the corresponding visual rego.
4396
4697
  */
4397
4698
  async loadName() {
4699
+ var _a;
4398
4700
  if (this.loadedName) {
4399
4701
  return;
4400
4702
  }
4401
4703
  try {
4704
+ // A rego that already holds a real name needs no lookup. Placeholders do not count, or a cached
4705
+ // "Unnamed Entity" would stop the real name ever being resolved.
4706
+ if (IsUsableEntityName((_a = this.rego) === null || _a === void 0 ? void 0 : _a.name)) {
4707
+ this.loadedName = true;
4708
+ return;
4709
+ }
4402
4710
  if (this.api) {
4403
4711
  const rego = this.rego;
4404
4712
  this.rego.name = await BModels.Entity.CalculateNameAsync({
@@ -5585,7 +5893,7 @@
5585
5893
  return;
5586
5894
  }
5587
5895
  if (parabola) {
5588
- const visible = cEntity && cEntity.show != false && params.viewer.entities.contains(cEntity);
5896
+ const visible = cEntity && cEntity.show != false && IsCEntityInScene(params.viewer, cEntity);
5589
5897
  parabola.Hidden = !visible;
5590
5898
  }
5591
5899
  updatePosses();
@@ -5613,18 +5921,58 @@
5613
5921
  // If a shape has more than this amount of points, we'll avoid checking it because it will take too long.
5614
5922
  // We will put responsibility on the bookmark creator to view shapes they can see.
5615
5923
  const DO_NOT_CULL_AT_LEN_POINTS = 5000;
5616
- // Returns terrain height at a given position.
5617
- // This will cache the results based on the terrain provider and position.
5618
- const _terrainCache = new BModels.LRUCache(1000);
5619
- // Positions within this tolerance from each-other will be re-used.
5620
- const TERRAIN_HEIGHT_TOLERANCE = 400;
5621
- // Tolerance to use when number of entities is lower than x in the scene.
5622
- const TERRAIN_HEIGHT_TOLERANCE_LOW_COUNT = 30;
5623
- const TERRAIN_HEIGHT_TOLERANCE_LOW_ENTITY_THRESHOLD = 100;
5924
+ const _terrainCache = new BModels.LRUCache(20000);
5925
+ /*
5926
+ * Past this camera distance the terrain sample is skipped entirely (resolving a long-standing TODO here).
5927
+ * The sphere's radius is padded by TERRAIN_SKIP_RADIUS_PAD instead, which covers any plausible unresolved ground height.
5928
+ */
5929
+ const TERRAIN_SKIP_DISTANCE = 60000;
5930
+ const TERRAIN_SKIP_RADIUS_PAD = 1500;
5931
+ // Edge length of a coarse grid cell, and of the finer cell used where the ground turns out to be varied.
5932
+ const TERRAIN_CELL_SIZE = 400;
5933
+ const TERRAIN_CELL_SIZE_FINE = 50;
5934
+ // A cell spanning more than this much height cannot be answered with a single average.
5935
+ const TERRAIN_CELL_MAX_SPREAD = 25;
5936
+ /**
5937
+ * Builds the grid key for a position at a given cell size.
5938
+ */
5939
+ function terrainCellKey(terrainId, pos3d, cellSize) {
5940
+ const x = Math.floor(pos3d.x / cellSize);
5941
+ const y = Math.floor(pos3d.y / cellSize);
5942
+ const z = Math.floor(pos3d.z / cellSize);
5943
+ return `${terrainId}_${cellSize}_${x}_${y}_${z}`;
5944
+ }
5945
+ /**
5946
+ * Folds a sample into a cell, refining its estimate and noticing when the region is too varied to average.
5947
+ */
5948
+ function recordTerrainSample(key, height) {
5949
+ const existing = _terrainCache.Get(key);
5950
+ if (!existing) {
5951
+ const cell = {
5952
+ offset: height,
5953
+ count: 1,
5954
+ min: height,
5955
+ max: height,
5956
+ needsDetail: false
5957
+ };
5958
+ _terrainCache.Set(key, cell);
5959
+ return cell;
5960
+ }
5961
+ existing.count++;
5962
+ // Running mean, so the estimate keeps improving rather than being whatever landed first.
5963
+ existing.offset += (height - existing.offset) / existing.count;
5964
+ existing.min = Math.min(existing.min, height);
5965
+ existing.max = Math.max(existing.max, height);
5966
+ if (existing.max - existing.min > TERRAIN_CELL_MAX_SPREAD) {
5967
+ existing.needsDetail = true;
5968
+ }
5969
+ _terrainCache.Set(key, existing);
5970
+ return existing;
5971
+ }
5624
5972
  /**
5625
5973
  * Returns the terrain height for a given position.
5626
5974
  * This will not sample most-detailed and instead the current state of the terrain.
5627
- * TODO: If something is VERY far from the camera, we should just return 0.
5975
+ * Callers past TERRAIN_SKIP_DISTANCE should not be asking at all, see shouldCullEntity.
5628
5976
  * @param viewer
5629
5977
  * @param pos3d
5630
5978
  * @returns
@@ -5641,37 +5989,36 @@
5641
5989
  return 0;
5642
5990
  }
5643
5991
  const terrainId = terrain.tilesetId;
5644
- // Check for existing values within TOLERANCE distance.
5645
- const keys = Array.from(_terrainCache.Entries());
5646
- const tolerance = viewer.entities.values.length > TERRAIN_HEIGHT_TOLERANCE_LOW_ENTITY_THRESHOLD ? TERRAIN_HEIGHT_TOLERANCE : TERRAIN_HEIGHT_TOLERANCE_LOW_COUNT;
5647
- for (let i = 0; i < keys.length; i++) {
5648
- const [key, offset] = keys[i];
5649
- const [terrainId2, x, y, z] = key.split("_");
5650
- if (terrainId2 != terrainId) {
5651
- continue;
5652
- }
5653
- const pos3d2 = new Cesium.Cartesian3(parseFloat(x), parseFloat(y), parseFloat(z));
5654
- const distance = Cesium.Cartesian3.distance(pos3d, pos3d2);
5655
- if (distance < tolerance) {
5656
- return offset;
5657
- }
5992
+ // Always start coarse and let the region teach us where that is not good enough, rather than guessing from a scene-wide entity count.
5993
+ // A cell that turns out to span varied ground flags itself and its lookups move to the finer grid.
5994
+ const coarseKey = terrainCellKey(terrainId, pos3d, TERRAIN_CELL_SIZE);
5995
+ const coarseCell = _terrainCache.Get(coarseKey);
5996
+ const useFine = Boolean(coarseCell === null || coarseCell === void 0 ? void 0 : coarseCell.needsDetail);
5997
+ const key = useFine ? terrainCellKey(terrainId, pos3d, TERRAIN_CELL_SIZE_FINE) : coarseKey;
5998
+ const cell = useFine ? _terrainCache.Get(key) : coarseCell;
5999
+ if (cell && !cell.needsDetail) {
6000
+ return cell.offset;
5658
6001
  }
5659
- const key = terrainId + "_" + pos3d.x + "_" + pos3d.y + "_" + pos3d.z;
5660
- let offset = _terrainCache.Get(key);
5661
- if (offset == null) {
5662
- const carto = Cesium.Cartographic.fromCartesian(pos3d);
5663
- let height = 0;
5664
- try {
5665
- height = viewer.scene.globe.getHeight(carto);
5666
- }
5667
- catch (e) {
5668
- // Likely out of rendered area.
5669
- // This means it's not our problem since the user isn't looking there :)
5670
- }
5671
- offset = height;
5672
- _terrainCache.Set(key, offset);
6002
+ const carto = Cesium.Cartographic.fromCartesian(pos3d);
6003
+ let height = 0;
6004
+ try {
6005
+ height = viewer.scene.globe.getHeight(carto);
6006
+ }
6007
+ catch (e) {
6008
+ // Likely out of rendered area.
6009
+ // This means it's not our problem since the user isn't looking there :)
6010
+ }
6011
+ if (height == null || isNaN(height)) {
6012
+ // Terrain tile not loaded yet. Don't record it, so a later pass can sample it properly.
6013
+ return coarseCell ? coarseCell.offset : 0;
6014
+ }
6015
+ // Always fold into the coarse cell as well, so its spread keeps being learned even while the finer grid is
6016
+ // answering lookups for this region.
6017
+ const recorded = recordTerrainSample(key, height);
6018
+ if (useFine) {
6019
+ recordTerrainSample(coarseKey, height);
5673
6020
  }
5674
- return offset;
6021
+ return recorded.offset;
5675
6022
  };
5676
6023
  /**
5677
6024
  * Converts given pos3d into a an absolute position.
@@ -5855,6 +6202,10 @@
5855
6202
  if (cEntity.model) {
5856
6203
  const model = cEntity.model;
5857
6204
  if (model._radius && !isNaN(model._radius)) {
6205
+ // _radius is already measured from the model's origin rather than its own centre: the model3d
6206
+ // engine folds the bounding sphere's centre offset into it (plus padding for badly-centred
6207
+ // models), and measures it at the entity's scale. So it can be used as-is against a sphere
6208
+ // centred on the entity position.
5858
6209
  sphere.radius = Math.max(model._radius, MODEL_MIN_RADIUS);
5859
6210
  }
5860
6211
  else {
@@ -5868,12 +6219,10 @@
5868
6219
  let radius = getValue$1(viewer, cEntity.ellipse.semiMajorAxis);
5869
6220
  const extrusion = getValue$1(viewer, cEntity.ellipse.extrudedHeight);
5870
6221
  if (extrusion && !isNaN(extrusion)) {
5871
- if (!radius) {
5872
- radius = extrusion;
5873
- }
5874
- else if (extrusion > radius / 2) {
5875
- radius = extrusion;
5876
- }
6222
+ // Expand rather than replace: measured from the base centre, the top rim of an extruded ellipse
6223
+ // sits at sqrt(radius^2 + height^2). Taking whichever of the two is larger leaves the rim outside
6224
+ // the sphere, and for something both wide and tall it would actually shrink it.
6225
+ radius = radius ? Math.sqrt((radius * radius) + (extrusion * extrusion)) : extrusion;
5877
6226
  }
5878
6227
  if (radius && !isNaN(radius)) {
5879
6228
  sphere.radius = radius;
@@ -5883,12 +6232,11 @@
5883
6232
  else if (cEntity.polygon) {
5884
6233
  const extrusion = getValue$1(viewer, cEntity.polygon.extrudedHeight);
5885
6234
  if (extrusion && !isNaN(extrusion)) {
5886
- if (!sphere.radius) {
5887
- sphere.radius = extrusion;
5888
- }
5889
- else if (extrusion > sphere.radius / 2) {
5890
- sphere.radius = extrusion;
5891
- }
6235
+ // Same as the ellipse above: the footprint sphere has to grow to reach the extruded top corners,
6236
+ // at sqrt(footprint^2 + height^2) from the footprint centre.
6237
+ sphere.radius = sphere.radius
6238
+ ? Math.sqrt((sphere.radius * sphere.radius) + (extrusion * extrusion))
6239
+ : extrusion;
5892
6240
  }
5893
6241
  }
5894
6242
  else if (cEntity.point || cEntity.billboard) {
@@ -5910,7 +6258,15 @@
5910
6258
  return false;
5911
6259
  }
5912
6260
  const adjustedSphere = boundingSphere.sphere.clone();
5913
- adjustedSphere.center = adjustPos3d(viewer, boundingSphere.heightRef, adjustedSphere.center);
6261
+ // Terrain only matters here because it moves the sphere onto the ground.
6262
+ // Far enough away that a plausible ground height is a rounding error against the viewing distance, sampling it is wasted work.
6263
+ const cameraDistance = Cesium.Cartesian3.distance(camera.position, adjustedSphere.center);
6264
+ if (cameraDistance > TERRAIN_SKIP_DISTANCE) {
6265
+ adjustedSphere.radius += TERRAIN_SKIP_RADIUS_PAD;
6266
+ }
6267
+ else {
6268
+ adjustedSphere.center = adjustPos3d(viewer, boundingSphere.heightRef, adjustedSphere.center);
6269
+ }
5914
6270
  const cullingVolume = camera.frustum.computeCullingVolume(camera.position, camera.direction, camera.up);
5915
6271
  const visibility = cullingVolume.computeVisibility(adjustedSphere);
5916
6272
  return visibility !== Cesium.Intersect.INSIDE && visibility !== Cesium.Intersect.INTERSECTING;
@@ -5983,38 +6339,67 @@
5983
6339
  rego.relation != null ||
5984
6340
  // Explicitly being either shown or hidden.
5985
6341
  rego.overrideShow != null ||
5986
- // Part of an independent collection.
5987
- // Won't touch in case there is special logic.
5988
- rego.collection ||
6342
+ // Part of a collection we did not create, eg. a geojson data source, which may have its own rules for
6343
+ // what is in the scene. Our own per-menu-item data sources are fair game.
6344
+ (rego.collection && !IsCCollectionOwned(rego.collection)) ||
5989
6345
  // We won't cull historic records as they may be interpolating their locations.
5990
6346
  Boolean(rego.outline && rego.outline.some(x => !!x.DateTime))) {
5991
6347
  return true;
5992
6348
  }
5993
- const visual = rego.visual;
5994
- // We will only cull clamped stuff since it seems that's where we get benefits from.
5995
- let heightRef;
5996
- if (visual.polygon) {
5997
- heightRef = getValue$1(viewer, visual.polygon.heightReference);
5998
- }
5999
- else if (visual.polyline) {
6000
- heightRef = getValue$1(viewer, visual.polyline.clampToGround) == false ? Cesium.HeightReference.NONE : Cesium.HeightReference.CLAMP_TO_GROUND;
6001
- }
6002
- else if (visual.corridor) {
6003
- heightRef = getValue$1(viewer, visual.corridor.heightReference);
6004
- }
6005
- else if (visual.ellipse) {
6006
- heightRef = getValue$1(viewer, visual.ellipse.heightReference);
6007
- }
6008
- else if (visual.model) {
6009
- heightRef = getValue$1(viewer, visual.model.heightReference);
6349
+ return false;
6350
+ }
6351
+ // A tileset's bounds are padded by this fraction before testing, so panning does not immediately evict something
6352
+ // at the edge of view and pay to re-traverse it moments later.
6353
+ const TILESET_CULL_PADDING = 0.25;
6354
+ /**
6355
+ * Suppresses tilesets whose padded bounds are outside the view, and restores ones that come back.
6356
+ *
6357
+ * Worth doing separately from entity culling because Cesium gates a tileset's whole update on its show flag: a
6358
+ * hidden tileset skips traversal, tile selection and request scheduling entirely, where hiding its features still
6359
+ * does all of that and merely draws nothing. There are only ever a handful of tilesets, so this needs no batching.
6360
+ * @param register
6361
+ */
6362
+ function cullTilesets(register) {
6363
+ var _a;
6364
+ const viewer = register.Viewer;
6365
+ const tilesets = register.GetTilesets();
6366
+ if (!tilesets.length || viewer.isDestroyed() || !viewer.scene) {
6367
+ return;
6010
6368
  }
6011
- else if (visual.point) {
6012
- heightRef = getValue$1(viewer, visual.point.heightReference);
6369
+ const camera = viewer.scene.camera;
6370
+ const cullingVolume = camera.frustum.computeCullingVolume(camera.position, camera.direction, camera.up);
6371
+ let changed = false;
6372
+ for (const rego of tilesets) {
6373
+ const tileset = rego.tileset;
6374
+ if (!tileset || !rego.gate) {
6375
+ continue;
6376
+ }
6377
+ // A destroyed tileset should not be held onto, let alone culled.
6378
+ if ((_a = tileset.isDestroyed) === null || _a === void 0 ? void 0 : _a.call(tileset)) {
6379
+ register.RemoveTileset(tileset);
6380
+ continue;
6381
+ }
6382
+ let sphere;
6383
+ try {
6384
+ sphere = tileset.boundingSphere;
6385
+ }
6386
+ catch (e) {
6387
+ // Not ready yet. Leave it alone rather than guessing.
6388
+ continue;
6389
+ }
6390
+ if (!sphere || isNaN(sphere.radius) || sphere.radius <= 0) {
6391
+ continue;
6392
+ }
6393
+ const padded = sphere.clone();
6394
+ padded.radius = sphere.radius * (1 + TILESET_CULL_PADDING);
6395
+ const visible = cullingVolume.computeVisibility(padded) !== Cesium.Intersect.OUTSIDE;
6396
+ if (register.SetTilesetCulled(tileset, !visible)) {
6397
+ changed = true;
6398
+ }
6013
6399
  }
6014
- else if (visual.billboard) {
6015
- heightRef = getValue$1(viewer, visual.billboard.heightReference);
6400
+ if (changed) {
6401
+ viewer.scene.requestRender();
6016
6402
  }
6017
- return heightRef != Cesium.HeightReference.CLAMP_TO_GROUND;
6018
6403
  }
6019
6404
  /**
6020
6405
  * Runs through all entities in the register and culls them if they are out of the viewport.
@@ -6030,6 +6415,7 @@
6030
6415
  const entityIds = register.GetEntityIds();
6031
6416
  // We'll use the terrain cache per culler cycle.
6032
6417
  _terrainCache.Clear();
6418
+ cullTilesets(register);
6033
6419
  checkInterval = setInterval(() => {
6034
6420
  var _a;
6035
6421
  if (viewer.isDestroyed() || !viewer.scene) {
@@ -6082,7 +6468,7 @@
6082
6468
  else {
6083
6469
  delete part[VisualRegisterCuller.VISUAL_CULL_KEY];
6084
6470
  if (rego.overrideShow != false &&
6085
- !viewer.entities.contains(part) &&
6471
+ !IsCEntityInScene(viewer, part) &&
6086
6472
  !state.hidden &&
6087
6473
  (!isIsolatedAny || state.isolated)) {
6088
6474
  toAdd.push(part);
@@ -6122,16 +6508,17 @@
6122
6508
  }
6123
6509
  const removal = viewer.scene.postRender.addEventListener(() => {
6124
6510
  removal();
6125
- for (let i = 0; i < entities.length; i++) {
6126
- const entity = entities[i];
6127
- if (entityRemoveAddState.get(entity.id) != false) {
6128
- continue;
6129
- }
6130
- entityRemoveAddState.delete(entity.id);
6131
- if (viewer.entities.contains(entity)) {
6132
- viewer.entities.remove(entity);
6511
+ // Batched, so Cesium raises one collectionChanged for the whole cull pass instead of one per entity.
6512
+ WithSuspendedCCollections(entities.map(x => GetCEntityCollection(viewer, x)), () => {
6513
+ for (let i = 0; i < entities.length; i++) {
6514
+ const entity = entities[i];
6515
+ if (entityRemoveAddState.get(entity.id) != false) {
6516
+ continue;
6517
+ }
6518
+ entityRemoveAddState.delete(entity.id);
6519
+ RemoveCEntityFromScene(viewer, entity);
6133
6520
  }
6134
- }
6521
+ });
6135
6522
  });
6136
6523
  }
6137
6524
  /**
@@ -6146,16 +6533,17 @@
6146
6533
  }
6147
6534
  const removal = viewer.scene.postRender.addEventListener(() => {
6148
6535
  removal();
6149
- for (let i = 0; i < entities.length; i++) {
6150
- const entity = entities[i];
6151
- if (entityRemoveAddState.get(entity.id) != true) {
6152
- continue;
6153
- }
6154
- entityRemoveAddState.delete(entity.id);
6155
- if (!viewer.entities.contains(entity)) {
6156
- viewer.entities.add(entity);
6536
+ // Batched, so Cesium raises one collectionChanged for the whole cull pass instead of one per entity.
6537
+ WithSuspendedCCollections(entities.map(x => GetCEntityCollection(viewer, x)), () => {
6538
+ for (let i = 0; i < entities.length; i++) {
6539
+ const entity = entities[i];
6540
+ if (entityRemoveAddState.get(entity.id) != true) {
6541
+ continue;
6542
+ }
6543
+ entityRemoveAddState.delete(entity.id);
6544
+ AddCEntityToScene(viewer, entity);
6157
6545
  }
6158
- }
6546
+ });
6159
6547
  });
6160
6548
  }
6161
6549
  var VisualRegisterCuller;
@@ -6226,6 +6614,19 @@
6226
6614
  };
6227
6615
  }
6228
6616
  VisualRegisterCuller.Monitor = Monitor;
6617
+ /**
6618
+ * Runs the tileset visibility pass immediately, rather than waiting for the next cull cycle.
6619
+ * Exposed so the behaviour is directly assertable, and for a caller that needs it applied now.
6620
+ * @param register
6621
+ */
6622
+ function CullTilesets(register) {
6623
+ cullTilesets(register);
6624
+ }
6625
+ VisualRegisterCuller.CullTilesets = CullTilesets;
6626
+ function IsCullingIgnored(viewer, rego) {
6627
+ return isCullingIgnored(viewer, rego);
6628
+ }
6629
+ VisualRegisterCuller.IsCullingIgnored = IsCullingIgnored;
6229
6630
  function IsCulled(viewer, rego, visual) {
6230
6631
  if (isCullingIgnored(viewer, rego)) {
6231
6632
  return false;
@@ -6273,7 +6674,7 @@
6273
6674
  return false;
6274
6675
  }
6275
6676
  if (visual instanceof Cesium.Entity) {
6276
- return viewer.entities.contains(visual);
6677
+ return IsCEntityInScene(viewer, visual);
6277
6678
  }
6278
6679
  else if (visual instanceof Cesium.Primitive) {
6279
6680
  return viewer.scene.primitives.contains(visual);
@@ -6300,6 +6701,32 @@
6300
6701
  viewer
6301
6702
  });
6302
6703
  }
6704
+ /**
6705
+ * Returns the highest zIndex across a graphic and its siblings, or null when none of them set one.
6706
+ * @param viewer
6707
+ * @param visual
6708
+ */
6709
+ function getHighestZIndex(viewer, visual) {
6710
+ if (!(visual instanceof Cesium.Entity)) {
6711
+ return null;
6712
+ }
6713
+ let highest = null;
6714
+ const parts = exports.EntityUtils.GatherEntity({
6715
+ entity: visual
6716
+ });
6717
+ for (const part of parts) {
6718
+ if (!(part instanceof Cesium.Entity)) {
6719
+ continue;
6720
+ }
6721
+ for (const graphic of [part.polygon, part.polyline, part.corridor, part.ellipse, part.rectangle]) {
6722
+ const zIndex = graphic ? GetCValue(viewer, graphic.zIndex) : null;
6723
+ if (zIndex != null && !isNaN(zIndex) && (highest == null || zIndex > highest)) {
6724
+ highest = zIndex;
6725
+ }
6726
+ }
6727
+ }
6728
+ return highest;
6729
+ }
6303
6730
  const MAX_SHOW_DEPTH = 10;
6304
6731
  function updateCEntityShow(viewer, visual, rego, show, ignoreParent, depth = 0) {
6305
6732
  if (depth > MAX_SHOW_DEPTH) {
@@ -6318,26 +6745,24 @@
6318
6745
  updateCEntityShow(viewer, sibling, rego, show, true, depth + 1);
6319
6746
  }
6320
6747
  }
6321
- /**
6322
- * Do NOT use ".show" as it causes crashes in Cesium polylines that are clamped to ground.
6323
- * We could target them specifically here but we may be getting overall performance gain by just removing stuff from the scene.
6324
- */
6325
- if (rego.collection) {
6326
- if (!show && rego.collection.contains(visual)) {
6327
- rego.collection.remove(visual);
6328
- }
6329
- else if (show && !rego.collection.contains(visual)) {
6330
- rego.collection.add(visual);
6748
+ if (show) {
6749
+ // Always restore membership on the way back in. Something else may have evicted this graphic while it
6750
+ // was hidden (the culler removes outright), and `show` alone would leave it invisible.
6751
+ AddCEntityToScene(viewer, visual, rego.collection);
6752
+ if (visual.show !== true) {
6753
+ visual.show = true;
6331
6754
  }
6755
+ return;
6332
6756
  }
6333
- else {
6334
- if (!show && viewer.entities.contains(visual)) {
6335
- viewer.entities.remove(visual);
6336
- }
6337
- else if (show && !viewer.entities.contains(visual)) {
6338
- viewer.entities.add(visual);
6757
+ // Hiding via `show` only updates a per-instance attribute, where leaving the collection rebuilds the whole
6758
+ // geometry batch. But it is not safe for every graphic, see CanToggleShowSafely for exactly why.
6759
+ if (CanToggleShowSafely([visual])) {
6760
+ if (visual.show !== false) {
6761
+ visual.show = false;
6339
6762
  }
6763
+ return;
6340
6764
  }
6765
+ RemoveCEntityFromScene(viewer, visual);
6341
6766
  }
6342
6767
  function updateEntityShow(viewer, rego, show) {
6343
6768
  const visual = rego.visual;
@@ -6646,12 +7071,128 @@
6646
7071
  // This lets us modify the update without re-adding the item to the queue if it's already in it.
6647
7072
  this.updateQueueSettings = {};
6648
7073
  this.updateQueueInterval = null;
7074
+ // Per menu item collections so we can refresh sets of Entities without affecting others.
7075
+ this.menuItemSources = new Map();
7076
+ // Set when the data source order no longer matches the zIndexes seen, so it is re-applied once.
7077
+ this.sourceOrderDirty = false;
7078
+ /*
7079
+ * Tilesets in the scene, keyed by the tileset itself.
7080
+ */
7081
+ this.tilesets = new Map();
6649
7082
  this.viewer = params.viewer;
6650
7083
  this.apiGetters = params.apiGetters;
6651
7084
  this.cameraCullerDispose = VisualRegisterCuller.Monitor({
6652
7085
  register: this
6653
7086
  });
6654
7087
  }
7088
+ /**
7089
+ * Returns the collection a menu item's visuals belong in, creating its data source on first use.
7090
+ * Returns null when there is nothing to key off, leaving the caller on the viewer's own collection.
7091
+ * @param menuItemId
7092
+ */
7093
+ ensureMenuItemCollection(menuItemId) {
7094
+ var _a;
7095
+ if (!menuItemId || this.disposed || !((_a = this.viewer) === null || _a === void 0 ? void 0 : _a.dataSources)) {
7096
+ return null;
7097
+ }
7098
+ const existing = this.menuItemSources.get(menuItemId);
7099
+ if (existing) {
7100
+ return existing.source.entities;
7101
+ }
7102
+ const source = new Cesium.CustomDataSource(`menu-item-${menuItemId}`);
7103
+ const record = {
7104
+ source,
7105
+ maxZIndex: 0,
7106
+ added: false
7107
+ };
7108
+ MarkCCollectionOwned(source.entities);
7109
+ this.menuItemSources.set(menuItemId, record);
7110
+ // Adding is asynchronous, so ordering has to wait until it is actually in the collection.
7111
+ Promise.resolve(this.viewer.dataSources.add(source))
7112
+ .then(() => {
7113
+ record.added = true;
7114
+ this.applySourceOrder();
7115
+ })
7116
+ .catch((e) => {
7117
+ console.error(e);
7118
+ });
7119
+ return source.entities;
7120
+ }
7121
+ /**
7122
+ * Notes the highest zIndex seen for a menu item, since that is what its data source has to sort by.
7123
+ * zIndex orders geometry within a data source but not between them, so a whole layer stacks by the
7124
+ * highest value anything in it asked for.
7125
+ * @param menuItemId
7126
+ * @param visual
7127
+ */
7128
+ trackSourceZIndex(menuItemId, visual) {
7129
+ const record = this.menuItemSources.get(menuItemId);
7130
+ if (!record) {
7131
+ return;
7132
+ }
7133
+ const zIndex = getHighestZIndex(this.viewer, visual);
7134
+ if (zIndex == null || zIndex <= record.maxZIndex) {
7135
+ return;
7136
+ }
7137
+ record.maxZIndex = zIndex;
7138
+ this.sourceOrderDirty = true;
7139
+ this.applySourceOrder();
7140
+ }
7141
+ /**
7142
+ * Re-stacks the menu item data sources so a higher zIndex sits above a lower one.
7143
+ */
7144
+ applySourceOrder() {
7145
+ var _a;
7146
+ if (this.disposed || !this.sourceOrderDirty || !((_a = this.viewer) === null || _a === void 0 ? void 0 : _a.dataSources)) {
7147
+ return;
7148
+ }
7149
+ const records = Array.from(this.menuItemSources.values()).filter(x => x.added);
7150
+ if (records.length < 2) {
7151
+ // Nothing to order against yet, so leave the flag set for when there is.
7152
+ return;
7153
+ }
7154
+ this.sourceOrderDirty = false;
7155
+ // Raising each in ascending order leaves the highest on top.
7156
+ records.sort((a, b) => a.maxZIndex - b.maxZIndex);
7157
+ for (const record of records) {
7158
+ try {
7159
+ this.viewer.dataSources.raiseToTop(record.source);
7160
+ }
7161
+ catch (e) {
7162
+ console.error(e);
7163
+ }
7164
+ }
7165
+ }
7166
+ /**
7167
+ * Drops a menu item's data source once nothing is registered under it any more.
7168
+ * @param menuItemId
7169
+ */
7170
+ releaseMenuItemCollection(menuItemId) {
7171
+ var _a, _b, _c;
7172
+ const record = this.menuItemSources.get(menuItemId);
7173
+ if (!record) {
7174
+ return;
7175
+ }
7176
+ if ((_a = this.entityIdsByMenuItem[menuItemId]) === null || _a === void 0 ? void 0 : _a.size) {
7177
+ return;
7178
+ }
7179
+ this.menuItemSources.delete(menuItemId);
7180
+ try {
7181
+ (_c = (_b = this.viewer) === null || _b === void 0 ? void 0 : _b.dataSources) === null || _c === void 0 ? void 0 : _c.remove(record.source, true);
7182
+ }
7183
+ catch (e) {
7184
+ console.error(e);
7185
+ }
7186
+ }
7187
+ /** Every collection a bulk pass over this register's visuals could mutate. */
7188
+ allSceneCollections() {
7189
+ var _a;
7190
+ const collections = [(_a = this.viewer) === null || _a === void 0 ? void 0 : _a.entities];
7191
+ for (const record of Array.from(this.menuItemSources.values())) {
7192
+ collections.push(record.source.entities);
7193
+ }
7194
+ return collections;
7195
+ }
6655
7196
  indexMenuItemEntity(menuItemId, entityId) {
6656
7197
  if (!menuItemId) {
6657
7198
  return;
@@ -7241,9 +7782,11 @@
7241
7782
  return;
7242
7783
  }
7243
7784
  const end = Math.min(index + BATCH_SIZE, items.length);
7244
- for (; index < end; index++) {
7245
- perItem(items[index]);
7246
- }
7785
+ WithSuspendedCCollections(this.allSceneCollections(), () => {
7786
+ for (; index < end; index++) {
7787
+ perItem(items[index]);
7788
+ }
7789
+ });
7247
7790
  this.viewer.scene.requestRender();
7248
7791
  if (index >= items.length) {
7249
7792
  clearInterval(interval);
@@ -7400,7 +7943,7 @@
7400
7943
  markEntity(this, rego, rego.visual, false);
7401
7944
  }
7402
7945
  Dispose() {
7403
- var _a;
7946
+ var _a, _b, _c, _d;
7404
7947
  (_a = this.cameraCullerDispose) === null || _a === void 0 ? void 0 : _a.call(this, {
7405
7948
  uncull: false
7406
7949
  });
@@ -7415,6 +7958,20 @@
7415
7958
  }
7416
7959
  this.activeChunkedIntervals.clear();
7417
7960
  this.disposed = true;
7961
+ for (const record of Array.from(this.menuItemSources.values())) {
7962
+ try {
7963
+ (_c = (_b = this.viewer) === null || _b === void 0 ? void 0 : _b.dataSources) === null || _c === void 0 ? void 0 : _c.remove(record.source, true);
7964
+ }
7965
+ catch (e) {
7966
+ console.error(e);
7967
+ }
7968
+ }
7969
+ this.menuItemSources.clear();
7970
+ // Hand every tileset's show flag back to its owner, holding whatever the owner last asked for.
7971
+ for (const rego of Array.from(this.tilesets.values())) {
7972
+ (_d = rego.gate) === null || _d === void 0 ? void 0 : _d.Release();
7973
+ }
7974
+ this.tilesets.clear();
7418
7975
  }
7419
7976
  ForceUpdate(params) {
7420
7977
  var _a;
@@ -8010,6 +8567,14 @@
8010
8567
  });
8011
8568
  }
8012
8569
  const entityId = rego.entityId;
8570
+ // Scope this visual to its menu item's own data source, so a rebuild of it cannot re-tessellate
8571
+ // other layers. A caller that already picked a collection (eg. geojson) keeps its own.
8572
+ if (!rego.collection && rego.menuItemId) {
8573
+ const collection = this.ensureMenuItemCollection(rego.menuItemId);
8574
+ if (collection) {
8575
+ rego.collection = collection;
8576
+ }
8577
+ }
8013
8578
  // Add the new visual to the register.
8014
8579
  const entityRegos = (_a = this.rego[entityId]) !== null && _a !== void 0 ? _a : [];
8015
8580
  entityRegos.push(rego);
@@ -8017,6 +8582,7 @@
8017
8582
  this.indexMenuItemEntity(rego.menuItemId, entityId);
8018
8583
  // Mark the visual as part of this register so selection works.
8019
8584
  markEntity(this, rego, rego.visual, false);
8585
+ this.trackSourceZIndex(rego.menuItemId, rego.visual);
8020
8586
  // Run any updates on the visual based on the calculated state.
8021
8587
  // Eg: visibility, opacity, selection, etc.
8022
8588
  this.queueUpdate({
@@ -8321,9 +8887,13 @@
8321
8887
  this.startLazyRemoval(entityIds, removeOne, onComplete);
8322
8888
  return;
8323
8889
  }
8324
- for (const entityId of entityIds) {
8325
- removeOne(entityId);
8326
- }
8890
+ WithSuspendedCCollections(this.allSceneCollections(), () => {
8891
+ for (const entityId of entityIds) {
8892
+ removeOne(entityId);
8893
+ }
8894
+ });
8895
+ this.releaseMenuItemCollection(menuItemId);
8896
+ this.RemoveTilesetsByMenuItem(menuItemId);
8327
8897
  }
8328
8898
  else if (entityId) {
8329
8899
  const entityRegos = this.rego[entityId];
@@ -8506,8 +9076,75 @@
8506
9076
  };
8507
9077
  }
8508
9078
  /**
8509
- * Returns all registered entity ids.
9079
+ * Registers a tileset so the register can decide when it is worth drawing.
9080
+ * @param params
9081
+ */
9082
+ AddTileset(params) {
9083
+ const { tileset, menuItemId } = params;
9084
+ if (!tileset || this.disposed) {
9085
+ return;
9086
+ }
9087
+ const existing = this.tilesets.get(tileset);
9088
+ if (existing) {
9089
+ existing.menuItemId = menuItemId !== null && menuItemId !== void 0 ? menuItemId : existing.menuItemId;
9090
+ return;
9091
+ }
9092
+ this.tilesets.set(tileset, {
9093
+ tileset,
9094
+ menuItemId,
9095
+ // Null when the property could not be intercepted, in which case this tileset simply never gets
9096
+ // culled rather than us fighting its owner over the same field.
9097
+ gate: InstallShowGate(tileset),
9098
+ culled: false
9099
+ });
9100
+ }
9101
+ /**
9102
+ * Stops tracking a tileset and hands its show flag back to its owner.
9103
+ * @param tileset
8510
9104
  */
9105
+ RemoveTileset(tileset) {
9106
+ var _a;
9107
+ const rego = this.tilesets.get(tileset);
9108
+ if (!rego) {
9109
+ return;
9110
+ }
9111
+ this.tilesets.delete(tileset);
9112
+ (_a = rego.gate) === null || _a === void 0 ? void 0 : _a.Release();
9113
+ }
9114
+ /**
9115
+ * Stops tracking every tileset belonging to a menu item.
9116
+ * @param menuItemId
9117
+ */
9118
+ RemoveTilesetsByMenuItem(menuItemId) {
9119
+ if (!menuItemId) {
9120
+ return;
9121
+ }
9122
+ for (const rego of Array.from(this.tilesets.values())) {
9123
+ if (rego.menuItemId === menuItemId) {
9124
+ this.RemoveTileset(rego.tileset);
9125
+ }
9126
+ }
9127
+ }
9128
+ /**
9129
+ * Every tileset currently tracked.
9130
+ */
9131
+ GetTilesets() {
9132
+ return Array.from(this.tilesets.values());
9133
+ }
9134
+ /**
9135
+ * Suppresses or restores a tileset.
9136
+ * Returns whether its effective visibility changed, so a caller can skip requesting a render when nothing did.
9137
+ * @param tileset
9138
+ * @param culled
9139
+ */
9140
+ SetTilesetCulled(tileset, culled) {
9141
+ const rego = this.tilesets.get(tileset);
9142
+ if (!(rego === null || rego === void 0 ? void 0 : rego.gate)) {
9143
+ return false;
9144
+ }
9145
+ rego.culled = Boolean(culled);
9146
+ return rego.gate.SetSuppressed(rego.culled);
9147
+ }
8511
9148
  GetEntityIds() {
8512
9149
  const ids = [];
8513
9150
  for (const entityId in this.rego) {
@@ -9876,7 +10513,11 @@
9876
10513
  const displayItems = this.zoomControl ? (_d = this.zoomControl) === null || _d === void 0 ? void 0 : _d.filter(x => x.DisplayType != BModels.ZoomControl.EDisplayType.Hidden) : [];
9877
10514
  const shouldCheck = displayItems.length > 1;
9878
10515
  this.entityCheckQueue = new BModels.DelayQueue(() => {
9879
- this.doEntityCheck(Object.keys(this.renderedEntities));
10516
+ // With a single band this pass is not looking for band changes at all, it is the periodic registry cleanup, so it always runs over everything.
10517
+ const ids = shouldCheck
10518
+ ? this.filterPossibleBandChanges(Object.keys(this.renderedEntities))
10519
+ : Object.keys(this.renderedEntities);
10520
+ this.doEntityCheck(ids);
9880
10521
  }, shouldCheck ? 3000 : 30000);
9881
10522
  const feed = (_e = this.apiGetter.getApi()) === null || _e === void 0 ? void 0 : _e.RecordChangeFeed;
9882
10523
  if (feed) {
@@ -10119,6 +10760,30 @@
10119
10760
  this.doEntityCheck(entityIds);
10120
10761
  }
10121
10762
  }
10763
+ /**
10764
+ * Narrows a camera-driven check to the entities whose zoom band could actually have changed.
10765
+ * @param ids
10766
+ */
10767
+ filterPossibleBandChanges(ids) {
10768
+ return ids.filter((id) => {
10769
+ const rego = this.visualsManager.GetRego({
10770
+ entityId: id,
10771
+ menuItemId: this.item.id
10772
+ });
10773
+ // Anything without a live graphic to measure against has to be checked.
10774
+ if (!(rego === null || rego === void 0 ? void 0 : rego.visual) || rego.stale) {
10775
+ return true;
10776
+ }
10777
+ const bandCheck = rego.visual._bandCheck;
10778
+ if (!bandCheck) {
10779
+ return true;
10780
+ }
10781
+ return exports.RenderManager.CouldBandHaveChanged({
10782
+ viewer: this.viewer,
10783
+ bandCheck
10784
+ });
10785
+ });
10786
+ }
10122
10787
  async doEntityCheck(ids) {
10123
10788
  var _a, _b;
10124
10789
  if (this.isRunningCheck) {
@@ -10790,6 +11455,19 @@
10790
11455
  optimizeTolerance: (_o = item.optimizeTolerance) !== null && _o !== void 0 ? _o : ""
10791
11456
  });
10792
11457
  }
11458
+ /**
11459
+ * Everything about a menu item that changes how its entities are drawn, as a comparable string.
11460
+ * Two items with the same signature produce identical graphics, so a hand-off between them needs no
11461
+ * re-render (and so no re-fetch of entity data the hand-off just avoided requesting).
11462
+ * A false mismatch only costs the optimization, so key ordering inside an inline Style is not normalized.
11463
+ */
11464
+ static GetRenderSignature(item) {
11465
+ return JSON.stringify({
11466
+ zoomControl: exports.RenderManager.GetZoomControlSignature(item.CameraZoomSettings),
11467
+ historicDrawTrack: Boolean(item.historicDrawTrack),
11468
+ historicInterpolation: Boolean(item.historicInterpolation)
11469
+ });
11470
+ }
10793
11471
  PrepareHandoff() {
10794
11472
  var _a;
10795
11473
  if (this.disposed || this.useGeojson || this.item.enableClustering) {
@@ -10801,7 +11479,8 @@
10801
11479
  internalByEntityId: new Map(this.internalByEntityId),
10802
11480
  allowedTypeIDs: [...this.allowedTypeIDs],
10803
11481
  renderQueue: [...this.renderQueue],
10804
- sourceMenuItemId: this.item.id
11482
+ sourceMenuItemId: this.item.id,
11483
+ renderSignature: Manager.GetRenderSignature(this.item)
10805
11484
  };
10806
11485
  this.unsetGetter();
10807
11486
  (_a = this.viewMonitorRemoval) === null || _a === void 0 ? void 0 : _a.call(this);
@@ -10841,7 +11520,12 @@
10841
11520
  if ((_a = payload.renderQueue) === null || _a === void 0 ? void 0 : _a.length) {
10842
11521
  this.distributeForRender(payload.renderQueue);
10843
11522
  }
10844
- this.ReRender({ entityIds: Object.keys(this.renderedEntities), force: true });
11523
+ // Adopted graphics are already drawn the way this item wants them, so re-rendering would only
11524
+ // re-request every entity from the API, which is the cost the hand-off exists to avoid.
11525
+ const needsRestyle = payload.renderSignature !== Manager.GetRenderSignature(this.item);
11526
+ if (needsRestyle) {
11527
+ this.ReRender({ entityIds: Object.keys(this.renderedEntities), force: true });
11528
+ }
10845
11529
  this.viewer.scene.requestRender();
10846
11530
  }
10847
11531
  }
@@ -11283,7 +11967,7 @@
11283
11967
  * @param force TODO: This should re-render entities that are already rendered.
11284
11968
  */
11285
11969
  async renderAsGeojson(entities, force) {
11286
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
11970
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
11287
11971
  entities = entities.filter((entity) => {
11288
11972
  var _a;
11289
11973
  return !this.renderedEntities[(_a = entity.Bruce) === null || _a === void 0 ? void 0 : _a.ID];
@@ -11334,38 +12018,42 @@
11334
12018
  if (!entities.length) {
11335
12019
  return;
11336
12020
  }
12021
+ // A menu item can declare that nothing about its render may touch the API. Styling then has to come from
12022
+ // the zoom item's inline Style, and names from what the entities already carry.
12023
+ const offline = Boolean((_a = this.item) === null || _a === void 0 ? void 0 : _a.offline);
11337
12024
  let style = null;
11338
- if ((zoomItem === null || zoomItem === void 0 ? void 0 : zoomItem.StyleID) && zoomItem.StyleID != -1) {
12025
+ if (!offline && (zoomItem === null || zoomItem === void 0 ? void 0 : zoomItem.StyleID) && zoomItem.StyleID != -1) {
11339
12026
  try {
11340
- style = (_a = (await BModels.Style.Get({
12027
+ style = (_b = (await BModels.Style.Get({
11341
12028
  api: this.apiGetter.getApi(),
11342
12029
  styleId: zoomItem === null || zoomItem === void 0 ? void 0 : zoomItem.StyleID
11343
- })).style) === null || _a === void 0 ? void 0 : _a.Settings;
12030
+ })).style) === null || _b === void 0 ? void 0 : _b.Settings;
11344
12031
  }
11345
12032
  // Probably deleted.
11346
12033
  catch (e) {
11347
12034
  console.error(e);
11348
12035
  }
11349
12036
  }
11350
- let entityTypeId = (_b = this.item.BruceEntity) === null || _b === void 0 ? void 0 : _b["EntityType.ID"];
12037
+ let entityTypeId = (_c = this.item.BruceEntity) === null || _c === void 0 ? void 0 : _c["EntityType.ID"];
11351
12038
  if (!entityTypeId) {
11352
- entityTypeId = (_d = (_c = entities.find(x => { var _a; return !!((_a = x.Bruce) === null || _a === void 0 ? void 0 : _a["EntityType.ID"]); })) === null || _c === void 0 ? void 0 : _c.Bruce) === null || _d === void 0 ? void 0 : _d["EntityType.ID"];
12039
+ entityTypeId = (_e = (_d = entities.find(x => { var _a; return !!((_a = x.Bruce) === null || _a === void 0 ? void 0 : _a["EntityType.ID"]); })) === null || _d === void 0 ? void 0 : _d.Bruce) === null || _e === void 0 ? void 0 : _e["EntityType.ID"];
11353
12040
  }
11354
- // Getting type regardless cause it's needed for name calculations.
12041
+ // Normally fetched regardless, since name calculation needs it. Offline callers take the names the
12042
+ // entities already carry instead.
11355
12043
  let entityType;
11356
- if (entityTypeId) {
12044
+ if (entityTypeId && !offline) {
11357
12045
  // Try using the Entity Type default if one is specified in the menu item.
11358
12046
  try {
11359
- entityType = (_e = (await BModels.EntityType.Get({
12047
+ entityType = (_f = (await BModels.EntityType.Get({
11360
12048
  entityTypeId: entityTypeId,
11361
12049
  api: this.apiGetter.getApi()
11362
- }))) === null || _e === void 0 ? void 0 : _e.entityType;
12050
+ }))) === null || _f === void 0 ? void 0 : _f.entityType;
11363
12051
  if (!style && entityType) {
11364
12052
  if (entityType["DisplaySetting.ID"] && entityType["DisplaySetting.ID"] > 0) {
11365
- style = (_f = (await BModels.Style.Get({
12053
+ style = (_g = (await BModels.Style.Get({
11366
12054
  api: this.apiGetter.getApi(),
11367
12055
  styleId: entityType["DisplaySetting.ID"]
11368
- })).style) === null || _f === void 0 ? void 0 : _f.Settings;
12056
+ })).style) === null || _g === void 0 ? void 0 : _g.Settings;
11369
12057
  }
11370
12058
  }
11371
12059
  }
@@ -11373,9 +12061,13 @@
11373
12061
  console.error(e);
11374
12062
  }
11375
12063
  }
11376
- const pStyle = (_g = style === null || style === void 0 ? void 0 : style.polygonStyle) !== null && _g !== void 0 ? _g : {};
11377
- const lStyle = (_h = style === null || style === void 0 ? void 0 : style.polylineStyle) !== null && _h !== void 0 ? _h : {};
11378
- const polygonsClamped = ((_j = pStyle === null || pStyle === void 0 ? void 0 : pStyle.altitudeOption) === null || _j === void 0 ? void 0 : _j.id) == null ? true : ((_k = pStyle === null || pStyle === void 0 ? void 0 : pStyle.altitudeOption) === null || _k === void 0 ? void 0 : _k.id) == 0;
12064
+ // Fall back to whatever the zoom item carried inline, which is the only source offline.
12065
+ if (!style) {
12066
+ style = zoomItem === null || zoomItem === void 0 ? void 0 : zoomItem.Style;
12067
+ }
12068
+ const pStyle = (_h = style === null || style === void 0 ? void 0 : style.polygonStyle) !== null && _h !== void 0 ? _h : {};
12069
+ const lStyle = (_j = style === null || style === void 0 ? void 0 : style.polylineStyle) !== null && _j !== void 0 ? _j : {};
12070
+ const polygonsClamped = ((_k = pStyle === null || pStyle === void 0 ? void 0 : pStyle.altitudeOption) === null || _k === void 0 ? void 0 : _k.id) == null ? true : ((_l = pStyle === null || pStyle === void 0 ? void 0 : pStyle.altitudeOption) === null || _l === void 0 ? void 0 : _l.id) == 0;
11379
12071
  const bFillColor = BModels.Calculator.GetColor(pStyle.fillColor, {}, []);
11380
12072
  const cFillColor = bFillColor ? colorToCColor$2(bFillColor) : Cesium.Color.fromCssColorString("rgba(139, 195, 74, 0.8)");
11381
12073
  const bLineColor = BModels.Calculator.GetColor(pStyle.lineColor, {}, []);
@@ -11609,6 +12301,7 @@
11609
12301
  return next;
11610
12302
  }
11611
12303
  async doRenderAsIndividuals(entities, force = false) {
12304
+ var _a;
11612
12305
  const { updated, entities: cEntities } = await exports.EntityRenderEngine.Render({
11613
12306
  viewer: this.viewer,
11614
12307
  apiGetter: this.apiGetter,
@@ -11616,6 +12309,8 @@
11616
12309
  menuItemId: this.item.id,
11617
12310
  visualRegister: this.visualsManager,
11618
12311
  zoomControl: this.item.CameraZoomSettings,
12312
+ // Carries the promise through to the engines, which do the rest of the API avoidance.
12313
+ offline: Boolean((_a = this.item) === null || _a === void 0 ? void 0 : _a.offline),
11619
12314
  force
11620
12315
  });
11621
12316
  if (this.disposed) {
@@ -13836,6 +14531,7 @@
13836
14531
  }
13837
14532
  TilesetRenderEngine.ApplySettings = ApplySettings;
13838
14533
  async function Render(params) {
14534
+ var _a, _b, _c;
13839
14535
  const api = params.apiGetter.getApi(params.accountId);
13840
14536
  // Ensure regional url is loaded.
13841
14537
  await api.Loading;
@@ -13899,6 +14595,10 @@
13899
14595
  console.error(e);
13900
14596
  }
13901
14597
  });
14598
+ (_a = params.register) === null || _a === void 0 ? void 0 : _a.AddTileset({
14599
+ tileset: cTileset,
14600
+ menuItemId: params.menuItemId
14601
+ });
13902
14602
  return cTileset;
13903
14603
  }
13904
14604
  else if (params.tileset.type === BModels.Tileset.EType.EntitiesSet) {
@@ -13915,6 +14615,10 @@
13915
14615
  const cTileset = await createTileset(loadUrl, null, params.noMemoryLimit);
13916
14616
  params.viewer.scene.primitives.add(cTileset);
13917
14617
  watchTilesetDateRange(params.viewer, cTileset, params.tileset);
14618
+ (_b = params.register) === null || _b === void 0 ? void 0 : _b.AddTileset({
14619
+ tileset: cTileset,
14620
+ menuItemId: params.menuItemId
14621
+ });
13918
14622
  return cTileset;
13919
14623
  }
13920
14624
  else if (params.tileset.type == BModels.Tileset.EType.PointCloud) {
@@ -13960,6 +14664,10 @@
13960
14664
  console.error(e);
13961
14665
  }
13962
14666
  });
14667
+ (_c = params.register) === null || _c === void 0 ? void 0 : _c.AddTileset({
14668
+ tileset: cTileset,
14669
+ menuItemId: params.menuItemId
14670
+ });
13963
14671
  return cTileset;
13964
14672
  }
13965
14673
  else {
@@ -13968,6 +14676,7 @@
13968
14676
  }
13969
14677
  TilesetRenderEngine.Render = Render;
13970
14678
  async function RenderLegacy(params) {
14679
+ var _a;
13971
14680
  let { apiGetter, viewer, tileset, ionId, loadUrl, viaCdn, accountId, noMemoryLimit, backFaceCulling } = params;
13972
14681
  let settings = tileset === null || tileset === void 0 ? void 0 : tileset.Settings;
13973
14682
  settings = {
@@ -14090,6 +14799,10 @@
14090
14799
  console.error(e);
14091
14800
  }
14092
14801
  });
14802
+ (_a = params.register) === null || _a === void 0 ? void 0 : _a.AddTileset({
14803
+ tileset: cTileset,
14804
+ menuItemId: params.menuItemId
14805
+ });
14093
14806
  return cTileset;
14094
14807
  }
14095
14808
  TilesetRenderEngine.RenderLegacy = RenderLegacy;
@@ -14599,6 +15312,8 @@
14599
15312
  }
14600
15313
  // Render outside the shared process because we can't control file requests from Cesium.
14601
15314
  const cTileset = this.cTileset = await exports.TilesetRenderEngine.Render({
15315
+ register: this.visualsManager,
15316
+ menuItemId: this.item.id,
14602
15317
  loadUrl: loadUrl,
14603
15318
  apiGetter: this.getters.GetBruceGetter(),
14604
15319
  tileset: tileset,
@@ -16930,7 +17645,12 @@
16930
17645
  const displayItems = this.zoomControl ? this.zoomControl.filter(x => x.DisplayType != BModels.ZoomControl.EDisplayType.Hidden) : [];
16931
17646
  const shouldCheck = displayItems.length > 1;
16932
17647
  this.entityCheckQueue = new BModels.DelayQueue(() => {
16933
- this.doEntityCheck(Object.keys(this.renderedEntities));
17648
+ // With a single band this pass is not looking for band changes at all, so it always runs over
17649
+ // everything. With more than one, most entities can be ruled out without asking the server.
17650
+ const ids = shouldCheck
17651
+ ? this.filterPossibleBandChanges(Object.keys(this.renderedEntities))
17652
+ : Object.keys(this.renderedEntities);
17653
+ this.doEntityCheck(ids);
16934
17654
  }, shouldCheck ? 3000 : 30000);
16935
17655
  (_b = this.viewMonitorRemoval) === null || _b === void 0 ? void 0 : _b.call(this);
16936
17656
  this.viewMonitorRemoval = this.monitor.Updated().Subscribe(() => {
@@ -16938,6 +17658,30 @@
16938
17658
  (_a = this.entityCheckQueue) === null || _a === void 0 ? void 0 : _a.Call();
16939
17659
  });
16940
17660
  }
17661
+ /**
17662
+ * Narrows a camera-driven check to the entities whose zoom band could actually have changed, so a pan or
17663
+ * zoom stops re-requesting the whole rendered set. See RenderManager.CouldBandHaveChanged.
17664
+ * @param ids
17665
+ */
17666
+ filterPossibleBandChanges(ids) {
17667
+ return ids.filter((id) => {
17668
+ const rego = this.visualsManager.GetRego({
17669
+ entityId: id,
17670
+ menuItemId: this.item.id
17671
+ });
17672
+ if (!(rego === null || rego === void 0 ? void 0 : rego.visual) || rego.stale) {
17673
+ return true;
17674
+ }
17675
+ const bandCheck = rego.visual._bandCheck;
17676
+ if (!bandCheck) {
17677
+ return true;
17678
+ }
17679
+ return exports.RenderManager.CouldBandHaveChanged({
17680
+ viewer: this.viewer,
17681
+ bandCheck
17682
+ });
17683
+ });
17684
+ }
16941
17685
  preventCurrentCheckApiRefresh(entityIds) {
16942
17686
  var _a;
16943
17687
  this.reRenderState.markSkipForCurrentRun(entityIds, this.isRunningCheck);
@@ -17264,6 +18008,15 @@
17264
18008
  }
17265
18009
  return `datalab:${queryId}`;
17266
18010
  }
18011
+ /**
18012
+ * Everything about this menu item that changes how its entities are drawn, as a comparable string.
18013
+ * The hand-off key is only the query id, so this is what catches a styling difference between two items sharing that query.
18014
+ */
18015
+ static GetRenderSignature(item) {
18016
+ return JSON.stringify({
18017
+ zoomControl: exports.RenderManager.GetZoomControlSignature(item.CameraZoomSettings)
18018
+ });
18019
+ }
17267
18020
  PrepareHandoff() {
17268
18021
  var _a, _b, _c;
17269
18022
  if (this.disposed || !this.getter) {
@@ -17284,7 +18037,8 @@
17284
18037
  getter,
17285
18038
  renderedEntities: this.renderedEntities,
17286
18039
  renderQueue: [...this.renderQueue],
17287
- sourceMenuItemId: this.item.id
18040
+ sourceMenuItemId: this.item.id,
18041
+ renderSignature: Manager.GetRenderSignature(this.item)
17288
18042
  };
17289
18043
  this.renderQueue = [];
17290
18044
  this.queryLoadId += 1;
@@ -17307,7 +18061,10 @@
17307
18061
  if ((_a = payload.renderQueue) === null || _a === void 0 ? void 0 : _a.length) {
17308
18062
  this.distributeForRender(payload.renderQueue);
17309
18063
  }
17310
- this.ReRender({ entityIds: Object.keys(this.renderedEntities), force: true });
18064
+ const needsRestyle = payload.renderSignature !== Manager.GetRenderSignature(this.item);
18065
+ if (needsRestyle) {
18066
+ this.ReRender({ entityIds: Object.keys(this.renderedEntities), force: true });
18067
+ }
17311
18068
  this.viewer.scene.requestRender();
17312
18069
  }
17313
18070
  }
@@ -18288,6 +19045,8 @@
18288
19045
  }
18289
19046
  }
18290
19047
  const cTileset = this.cTileset = await exports.TilesetRenderEngine.Render({
19048
+ register: this.visualsManager,
19049
+ menuItemId: this.item.id,
18291
19050
  apiGetter: this.getters.GetBruceGetter(),
18292
19051
  tileset: tileset,
18293
19052
  viewer: this.viewer,
@@ -18778,11 +19537,12 @@
18778
19537
  constructor(params) {
18779
19538
  this.disposed = false;
18780
19539
  this.cTileset = null;
18781
- const { viewer, getters: apiGetter, item, initQueue } = params;
19540
+ const { viewer, getters: apiGetter, item, initQueue, register } = params;
18782
19541
  this.viewer = viewer;
18783
19542
  this.getters = apiGetter;
18784
19543
  this.item = item;
18785
19544
  this.initQueue = initQueue;
19545
+ this.visualsManager = register;
18786
19546
  }
18787
19547
  Init() {
18788
19548
  var _a;
@@ -18833,6 +19593,8 @@
18833
19593
  return;
18834
19594
  }
18835
19595
  this.cTileset = await exports.TilesetRenderEngine.Render({
19596
+ register: this.visualsManager,
19597
+ menuItemId: this.item.id,
18836
19598
  apiGetter: this.getters.GetBruceGetter(),
18837
19599
  tileset: tileset,
18838
19600
  viewer: this.viewer,
@@ -19419,6 +20181,8 @@
19419
20181
  if (!accountId) {
19420
20182
  accountId = this.getters.GetAccountId();
19421
20183
  }
20184
+ // Deliberately NOT registered with the visuals register, so it is never whole-tileset culled.
20185
+ // An arbitrary tileset can be produced outside our system and its declared bounds cannot be trusted.
19422
20186
  this.cTileset = await exports.TilesetRenderEngine.RenderLegacy({
19423
20187
  apiGetter: this.getters.GetBruceGetter(),
19424
20188
  tileset: legacyTileset,
@@ -20086,8 +20850,8 @@
20086
20850
  * @param cEntity
20087
20851
  */
20088
20852
  const killCEntity = (cEntity) => {
20089
- if (cEntity && !this.viewer.isDestroyed() && this.viewer.entities.contains(cEntity)) {
20090
- this.viewer.entities.remove(cEntity);
20853
+ if (cEntity && !this.viewer.isDestroyed()) {
20854
+ RemoveCEntityFromScene(this.viewer, cEntity);
20091
20855
  }
20092
20856
  if (cEntity === null || cEntity === void 0 ? void 0 : cEntity._dispose) {
20093
20857
  cEntity._dispose();
@@ -20565,8 +21329,8 @@
20565
21329
  * @param cEntity
20566
21330
  */
20567
21331
  const killCEntity = (cEntity) => {
20568
- if (cEntity && !this.viewer.isDestroyed() && this.viewer.entities.contains(cEntity)) {
20569
- this.viewer.entities.remove(cEntity);
21332
+ if (cEntity && !this.viewer.isDestroyed()) {
21333
+ RemoveCEntityFromScene(this.viewer, cEntity);
20570
21334
  }
20571
21335
  if (cEntity === null || cEntity === void 0 ? void 0 : cEntity._dispose) {
20572
21336
  cEntity._dispose();
@@ -22585,6 +23349,7 @@
22585
23349
  rItem.renderManager = new exports.TilesetPointcloudRenderManager.Manager({
22586
23350
  viewer: this.viewer,
22587
23351
  getters: this.getters,
23352
+ register: this.visualsRegister,
22588
23353
  item: params.item,
22589
23354
  initQueue: this.tilesetInitQueue
22590
23355
  });
@@ -22739,6 +23504,8 @@
22739
23504
  }
22740
23505
  if (allowHandoff && this.isHandoffCapable(item.renderManager)) {
22741
23506
  this.pendingHandoffPool.push(item);
23507
+ // Series segments stay recorded under the pooled id for now. A claimed hand-off
23508
+ // transfers them to the claiming item, and a flushed one clears them.
22742
23509
  }
22743
23510
  else {
22744
23511
  try {
@@ -22747,9 +23514,9 @@
22747
23514
  catch (e) {
22748
23515
  console.error(e);
22749
23516
  }
23517
+ this.ClearSeriesSegments(item.id);
22750
23518
  }
22751
23519
  this.items = this.items.filter(x => x.id !== id);
22752
- this.ClearSeriesSegments(item.id);
22753
23520
  (_b = this.onUpdate) === null || _b === void 0 ? void 0 : _b.Trigger({ isEnabling: false, itemId: item.id });
22754
23521
  }
22755
23522
  }
@@ -22797,14 +23564,38 @@
22797
23564
  if (poolIndex < 0) {
22798
23565
  return null;
22799
23566
  }
22800
- const pooled = this.pendingHandoffPool[poolIndex].renderManager;
23567
+ const pooledItem = this.pendingHandoffPool[poolIndex];
23568
+ const pooled = pooledItem.renderManager;
22801
23569
  const payload = pooled.PrepareHandoff();
22802
23570
  if (!payload) {
22803
23571
  return null;
22804
23572
  }
22805
23573
  this.pendingHandoffPool.splice(poolIndex, 1);
23574
+ this.transferSeriesSegments(pooledItem.id, item.id);
22806
23575
  return payload;
22807
23576
  }
23577
+ /**
23578
+ * Moves any series segments recorded for one menu item over to another, for a claimed hand-off.
23579
+ * The adopting manager may legitimately skip re-rendering (nothing to restyle), and segments are only
23580
+ * ever reported from inside a render, so without this the claiming item would end up with none.
23581
+ */
23582
+ transferSeriesSegments(fromItemId, toItemId) {
23583
+ const segments = this.seriesSegments.get(fromItemId);
23584
+ if (!(segments === null || segments === void 0 ? void 0 : segments.length)) {
23585
+ this.ClearSeriesSegments(fromItemId);
23586
+ return;
23587
+ }
23588
+ this.seriesSegments.delete(fromItemId);
23589
+ this.OnSeriesUpdated.Trigger({ itemId: fromItemId, segments: [] });
23590
+ const existing = this.seriesSegments.get(toItemId) || [];
23591
+ for (const segment of segments) {
23592
+ if (!existing.some((s) => s.key === segment.key)) {
23593
+ existing.push(segment);
23594
+ }
23595
+ }
23596
+ this.seriesSegments.set(toItemId, existing);
23597
+ this.OnSeriesUpdated.Trigger({ itemId: toItemId, segments: existing });
23598
+ }
22808
23599
  // Disposes everything left unclaimed in the pending-hand-off pool.
22809
23600
  FlushPendingHandoffs() {
22810
23601
  var _a;
@@ -22820,6 +23611,7 @@
22820
23611
  catch (e) {
22821
23612
  console.error(e);
22822
23613
  }
23614
+ this.ClearSeriesSegments(item.id);
22823
23615
  }
22824
23616
  }
22825
23617
  GetEnabledItemIds() {
@@ -34176,6 +34968,10 @@
34176
34968
  }
34177
34969
  return minDistance;
34178
34970
  }
34971
+ // Slack added when proving a zoom band cannot have changed.
34972
+ // See CouldBandHaveChanged: an entity's measured distance can jump by its altitude spread as the camera crosses over its footprint,
34973
+ // so being this close to a band edge means re-checking rather than trusting the bound.
34974
+ const BAND_EDGE_MARGIN_METERS = 500;
34179
34975
  // Cache for distance calculations.
34180
34976
  // It will keep only the most recent 3000 entries.
34181
34977
  const distanceCache = new BModels.LRUCache(3000);
@@ -34318,7 +35114,7 @@
34318
35114
  }
34319
35115
  (function (RenderManager) {
34320
35116
  function DetermineZoomItem(params) {
34321
- const { viewer, entity, zoomControl } = params;
35117
+ const { viewer, entity, zoomControl, out } = params;
34322
35118
  const pos = exports.EntityUtils.GetPos({
34323
35119
  viewer,
34324
35120
  entity
@@ -34331,11 +35127,16 @@
34331
35127
  if (!cameraPos || !cameraPos.latitude) {
34332
35128
  return null;
34333
35129
  }
34334
- const distance = calculateDistance({
35130
+ const cameraCarto = {
34335
35131
  altitude: cameraPos.height,
34336
35132
  latitude: Cesium.Math.toDegrees(cameraPos.latitude),
34337
35133
  longitude: Cesium.Math.toDegrees(cameraPos.longitude)
34338
- }, entity);
35134
+ };
35135
+ const distance = calculateDistance(cameraCarto, entity);
35136
+ if (out) {
35137
+ out.distance = distance;
35138
+ out.cameraPos = Cesium.Cartesian3.fromDegrees(cameraCarto.longitude, cameraCarto.latitude, cameraCarto.altitude);
35139
+ }
34339
35140
  let zoomItem = (!distance && distance != 0) ? null : GetZoomControlFromDistance({
34340
35141
  zoomControl,
34341
35142
  distance
@@ -34348,6 +35149,41 @@
34348
35149
  return zoomItem;
34349
35150
  }
34350
35151
  RenderManager.DetermineZoomItem = DetermineZoomItem;
35152
+ /**
35153
+ * Returns whether an entity could have moved into a different zoom band since its graphic was rendered,
35154
+ * given where the camera is now.
35155
+ *
35156
+ * Answers true whenever it cannot prove otherwise, so a caller may safely skip only on a false.
35157
+ */
35158
+ function CouldBandHaveChanged(params) {
35159
+ var _a;
35160
+ const { viewer, bandCheck } = params;
35161
+ if (!bandCheck || bandCheck.distance == null || !bandCheck.cameraPos) {
35162
+ return true;
35163
+ }
35164
+ // An absent range is not something to reason about. Values that ARE present go through EnsureNumber so
35165
+ // this agrees with how GetZoomControlFromDistance matched the band in the first place.
35166
+ if (bandCheck.min == null || bandCheck.max == null) {
35167
+ return true;
35168
+ }
35169
+ const min = EnsureNumber(bandCheck.min);
35170
+ const max = EnsureNumber(bandCheck.max);
35171
+ if (!(max > min)) {
35172
+ return true;
35173
+ }
35174
+ const cameraPos = (_a = viewer.camera) === null || _a === void 0 ? void 0 : _a.positionCartographic;
35175
+ if (!cameraPos || !cameraPos.latitude) {
35176
+ return true;
35177
+ }
35178
+ const nowPos = Cesium.Cartesian3.fromDegrees(Cesium.Math.toDegrees(cameraPos.longitude), Cesium.Math.toDegrees(cameraPos.latitude), cameraPos.height);
35179
+ const travelled = Cesium.Cartesian3.distance(nowPos, bandCheck.cameraPos);
35180
+ if (isNaN(travelled)) {
35181
+ return true;
35182
+ }
35183
+ const slack = travelled + BAND_EDGE_MARGIN_METERS;
35184
+ return (bandCheck.distance - slack) < min || (bandCheck.distance + slack) > max;
35185
+ }
35186
+ RenderManager.CouldBandHaveChanged = CouldBandHaveChanged;
34351
35187
  function GetZoomControlFromDistance(params) {
34352
35188
  const { zoomControl, distance } = params;
34353
35189
  for (let i = 0; i < zoomControl.length; i++) {
@@ -34359,6 +35195,26 @@
34359
35195
  return null;
34360
35196
  }
34361
35197
  RenderManager.GetZoomControlFromDistance = GetZoomControlFromDistance;
35198
+ /**
35199
+ * Everything in a zoom control that changes how an entity is drawn, as a comparable string.
35200
+ * Used to tell whether two menu items would produce identical graphics (see a manager's GetRenderSignature).
35201
+ * A false mismatch only costs an optimization, so key ordering inside an inline Style is not normalized.
35202
+ */
35203
+ function GetZoomControlSignature(zoomControl) {
35204
+ return JSON.stringify([].concat(zoomControl !== null && zoomControl !== void 0 ? zoomControl : []).map((zoomItem) => {
35205
+ var _a, _b, _c, _d, _e, _f, _g;
35206
+ return ({
35207
+ MinZoom: (_a = zoomItem === null || zoomItem === void 0 ? void 0 : zoomItem.MinZoom) !== null && _a !== void 0 ? _a : null,
35208
+ MaxZoom: (_b = zoomItem === null || zoomItem === void 0 ? void 0 : zoomItem.MaxZoom) !== null && _b !== void 0 ? _b : null,
35209
+ DisplayType: (_c = zoomItem === null || zoomItem === void 0 ? void 0 : zoomItem.DisplayType) !== null && _c !== void 0 ? _c : null,
35210
+ StyleID: (_d = zoomItem === null || zoomItem === void 0 ? void 0 : zoomItem.StyleID) !== null && _d !== void 0 ? _d : null,
35211
+ Style: (_e = zoomItem === null || zoomItem === void 0 ? void 0 : zoomItem.Style) !== null && _e !== void 0 ? _e : null,
35212
+ LODCategoryID: (_f = zoomItem === null || zoomItem === void 0 ? void 0 : zoomItem.LODCategoryID) !== null && _f !== void 0 ? _f : null,
35213
+ LODLevel: (_g = zoomItem === null || zoomItem === void 0 ? void 0 : zoomItem.LODLevel) !== null && _g !== void 0 ? _g : null
35214
+ });
35215
+ }));
35216
+ }
35217
+ RenderManager.GetZoomControlSignature = GetZoomControlSignature;
34362
35218
  function GetZoomMinMax(params) {
34363
35219
  const { zoomControl } = params;
34364
35220
  let min = 0;
@@ -34443,8 +35299,8 @@
34443
35299
  // We'll remove all that aren't in the allowed (direct) list.
34444
35300
  for (let i = 0; i < parts.length - 1; i++) {
34445
35301
  const part = parts[i];
34446
- if (part && part instanceof Cesium.Entity && params.viewer.entities.contains(part) && !cEntity._siblingGraphics.includes(part)) {
34447
- params.viewer.entities.remove(part);
35302
+ if (part && part instanceof Cesium.Entity && !cEntity._siblingGraphics.includes(part)) {
35303
+ RemoveCEntityFromScene(params.viewer, part);
34448
35304
  }
34449
35305
  }
34450
35306
  if (cEntity._parentEntity) {
@@ -34788,7 +35644,6 @@
34788
35644
  cEntity.ellipse.semiMajorAxis = new Cesium.ConstantProperty(radius);
34789
35645
  cEntity.ellipse.semiMinorAxis = new Cesium.ConstantProperty(radius);
34790
35646
  }
34791
- cEntity.ellipse.outlineWidth = undefined;
34792
35647
  {
34793
35648
  const curHeightRef = GetCValue(params.viewer, cEntity.ellipse.heightReference);
34794
35649
  if (curHeightRef != heightRef) {
@@ -34809,13 +35664,26 @@
34809
35664
  cEntity.ellipse.extrudedHeight = new Cesium.ConstantProperty(extrusion.value);
34810
35665
  }
34811
35666
  }
34812
- cEntity.ellipse.zIndex = new Cesium.ConstantProperty(1);
34813
- cEntity.ellipse.distanceDisplayCondition = new Cesium.ConstantProperty(exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance));
35667
+ SetCPropIfChanged({
35668
+ viewer: params.viewer,
35669
+ graphic: cEntity.ellipse,
35670
+ property: "zIndex",
35671
+ value: 1
35672
+ });
35673
+ SetCPropIfChanged({
35674
+ viewer: params.viewer,
35675
+ graphic: cEntity.ellipse,
35676
+ property: "distanceDisplayCondition",
35677
+ value: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance)
35678
+ });
34814
35679
  const curClassification = GetCValue(params.viewer, cEntity.ellipse.classificationType);
34815
35680
  if (curClassification != classification) {
34816
35681
  cEntity.ellipse.classificationType = new Cesium.ConstantProperty(classification);
34817
35682
  }
34818
- cEntity.position = new Cesium.ConstantPositionProperty(pos3d === null || pos3d === void 0 ? void 0 : pos3d.clone());
35683
+ const curPos = GetCValue(params.viewer, cEntity.position);
35684
+ if (!curPos || !pos3d || !Cesium.Cartesian3.equals(curPos, pos3d)) {
35685
+ cEntity.position = new Cesium.ConstantPositionProperty(pos3d === null || pos3d === void 0 ? void 0 : pos3d.clone());
35686
+ }
34819
35687
  // We'll use "SetDefaultColor" to updating the internal reference and to allow for an animation.
34820
35688
  // WARNING: ellipse does not support animation (yet?).
34821
35689
  exports.CesiumEntityStyler.SetDefaultColor({
@@ -34823,6 +35691,8 @@
34823
35691
  entity: cEntity,
34824
35692
  viewer: params.viewer,
34825
35693
  override: true,
35694
+ // Fill colour only. The outline sibling gets its own cOutline baked below.
35695
+ includeSiblings: false,
34826
35696
  requestRender: false
34827
35697
  });
34828
35698
  cEntity.show = true;
@@ -34861,8 +35731,18 @@
34861
35731
  outlineEntity.ellipse.extrudedHeight = new Cesium.ConstantProperty(outlineExtrusion.value);
34862
35732
  }
34863
35733
  }
34864
- outlineEntity.ellipse.zIndex = new Cesium.ConstantProperty(2);
34865
- outlineEntity.ellipse.distanceDisplayCondition = new Cesium.ConstantProperty(exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance));
35734
+ SetCPropIfChanged({
35735
+ viewer: params.viewer,
35736
+ graphic: outlineEntity.ellipse,
35737
+ property: "zIndex",
35738
+ value: 2
35739
+ });
35740
+ SetCPropIfChanged({
35741
+ viewer: params.viewer,
35742
+ graphic: outlineEntity.ellipse,
35743
+ property: "distanceDisplayCondition",
35744
+ value: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance)
35745
+ });
34866
35746
  const curClassification = GetCValue(params.viewer, outlineEntity.ellipse.classificationType);
34867
35747
  if (curClassification != classification) {
34868
35748
  outlineEntity.ellipse.classificationType = new Cesium.ConstantProperty(classification);
@@ -34874,6 +35754,7 @@
34874
35754
  entity: outlineEntity,
34875
35755
  viewer: params.viewer,
34876
35756
  override: true,
35757
+ includeSiblings: false,
34877
35758
  requestRender: false
34878
35759
  });
34879
35760
  outlineEntity.show = true;
@@ -35138,7 +36019,7 @@
35138
36019
  // De-dupe the tag IDs.
35139
36020
  tagIds = Array.from(new Set(tagIds));
35140
36021
  // Request if at least one then create a quick lookup map for tags.
35141
- const tags = tagIds.length > 0 ? (await BModels.EntityTag.GetListByIds({
36022
+ const tags = (!params.offline && tagIds.length > 0) ? (await BModels.EntityTag.GetListByIds({
35142
36023
  api,
35143
36024
  tagIds
35144
36025
  })).tags : [];
@@ -35150,7 +36031,7 @@
35150
36031
  for (let i = 0; i < params.entities.length; i++) {
35151
36032
  const entity = params.entities[i];
35152
36033
  const zoomItem = params.zoomItems[entity.Bruce.ID];
35153
- const style = zoomItem.StyleID != -1 ? (_a = (await getStyle$1(api, entity, zoomItem.StyleID))) === null || _a === void 0 ? void 0 : _a.Settings : zoomItem.Style;
36034
+ const style = (!params.offline && zoomItem.StyleID != -1) ? (_a = (await getStyle$1(api, entity, zoomItem.StyleID))) === null || _a === void 0 ? void 0 : _a.Settings : zoomItem.Style;
35154
36035
  // Pass through tags that may be needed for styling.
35155
36036
  const tagIds = entity.Bruce["Layer.ID"];
35156
36037
  const tags = [];
@@ -35188,7 +36069,7 @@
35188
36069
  rego: rego
35189
36070
  });
35190
36071
  if (cEntity) {
35191
- const name = await getName(api, entity);
36072
+ const name = await getName(api, entity, params.offline);
35192
36073
  cEntity.name = name;
35193
36074
  cEntity._renderGroup = exports.EntityRenderEngine.GetRenderGroupId(zoomItem);
35194
36075
  cEntity.styleId = zoomItem.StyleID == -1 ? -1 : (+zoomItem.StyleID || null);
@@ -35359,7 +36240,16 @@
35359
36240
  }
35360
36241
  return data;
35361
36242
  }
35362
- async function getName(api, entity) {
36243
+ async function getName(api, entity, offline) {
36244
+ var _a;
36245
+ const ownName = GetUsableEntityName(entity);
36246
+ if (ownName) {
36247
+ return ownName;
36248
+ }
36249
+ if (offline) {
36250
+ // Anything further needs a request, so offline callers take what the entity carries, placeholder or not.
36251
+ return ((_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.Name) || "Unnamed Entity";
36252
+ }
35363
36253
  try {
35364
36254
  const typeId = entity.Bruce["EntityType.ID"];
35365
36255
  let type = null;
@@ -35535,8 +36425,8 @@
35535
36425
  // We'll remove all that aren't in the allowed (direct) list.
35536
36426
  for (let i = 0; i < parts.length - 1; i++) {
35537
36427
  const part = parts[i];
35538
- if (part && part instanceof Cesium.Entity && params.viewer.entities.contains(part) && !cEntity._siblingGraphics.includes(part)) {
35539
- params.viewer.entities.remove(part);
36428
+ if (part && part instanceof Cesium.Entity && !cEntity._siblingGraphics.includes(part)) {
36429
+ RemoveCEntityFromScene(params.viewer, part);
35540
36430
  }
35541
36431
  }
35542
36432
  if (cEntity._parentEntity) {
@@ -36029,7 +36919,7 @@
36029
36919
  // De-dupe the tag IDs.
36030
36920
  tagIds = Array.from(new Set(tagIds));
36031
36921
  // Request if at least one then create a quick lookup map for tags.
36032
- const tags = tagIds.length > 0 ? (await BModels.EntityTag.GetListByIds({
36922
+ const tags = (!params.offline && tagIds.length > 0) ? (await BModels.EntityTag.GetListByIds({
36033
36923
  api,
36034
36924
  tagIds
36035
36925
  })).tags : [];
@@ -36041,7 +36931,7 @@
36041
36931
  for (let i = 0; i < params.entities.length; i++) {
36042
36932
  const entity = params.entities[i];
36043
36933
  const zoomItem = params.zoomItems[entity.Bruce.ID];
36044
- const style = zoomItem.StyleID != -1 ? (_a = (await getStyle$2(api, entity, zoomItem.StyleID))) === null || _a === void 0 ? void 0 : _a.Settings : zoomItem.Style;
36934
+ const style = (!params.offline && zoomItem.StyleID != -1) ? (_a = (await getStyle$2(api, entity, zoomItem.StyleID))) === null || _a === void 0 ? void 0 : _a.Settings : zoomItem.Style;
36045
36935
  // Pass through tags that may be needed for styling.
36046
36936
  const tagIds = entity.Bruce["Layer.ID"];
36047
36937
  const tags = [];
@@ -36080,8 +36970,8 @@
36080
36970
  for (let i = 0; i < params.entities.length; i++) {
36081
36971
  const entity = params.entities[i];
36082
36972
  const zoomItem = params.zoomItems[entity.Bruce.ID];
36083
- const styleRecord = zoomItem.StyleID != -1 ? (await getStyle$2(api, entity, zoomItem.StyleID)) : null;
36084
- const style = zoomItem.StyleID != -1 ? styleRecord === null || styleRecord === void 0 ? void 0 : styleRecord.Settings : zoomItem.Style;
36973
+ const styleRecord = (!params.offline && zoomItem.StyleID != -1) ? (await getStyle$2(api, entity, zoomItem.StyleID)) : null;
36974
+ const style = (!params.offline && zoomItem.StyleID != -1) ? styleRecord === null || styleRecord === void 0 ? void 0 : styleRecord.Settings : zoomItem.Style;
36085
36975
  const lod = lodData.find(x => x.entityId == entity.Bruce.ID);
36086
36976
  if (!(lod === null || lod === void 0 ? void 0 : lod.clientFileId)) {
36087
36977
  continue;
@@ -36127,7 +37017,7 @@
36127
37017
  rego: rego
36128
37018
  });
36129
37019
  if (cEntity) {
36130
- const name = await getName$1(api, entity);
37020
+ const name = await getName$1(api, entity, params.offline);
36131
37021
  cEntity.name = name;
36132
37022
  cEntity._renderGroup = exports.EntityRenderEngine.GetRenderGroupId(zoomItem);
36133
37023
  cEntity.styleId = zoomItem.StyleID == -1 ? -1 : styleRecord === null || styleRecord === void 0 ? void 0 : styleRecord.ID;
@@ -36153,7 +37043,16 @@
36153
37043
  }
36154
37044
  return heightRef;
36155
37045
  }
36156
- async function getName$1(api, entity) {
37046
+ async function getName$1(api, entity, offline) {
37047
+ var _a;
37048
+ const ownName = GetUsableEntityName(entity);
37049
+ if (ownName) {
37050
+ return ownName;
37051
+ }
37052
+ if (offline) {
37053
+ // Anything further needs a request, so offline callers take what the entity carries, placeholder or not.
37054
+ return ((_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.Name) || "Unnamed Entity";
37055
+ }
36157
37056
  try {
36158
37057
  const typeId = entity.Bruce["EntityType.ID"];
36159
37058
  let type = null;
@@ -36422,8 +37321,8 @@
36422
37321
  // Kill all expect last part. Last one is the primary entity.
36423
37322
  for (let i = 0; i < parts.length - 1; i++) {
36424
37323
  const part = parts[i];
36425
- if (part && part instanceof Cesium.Entity && params.viewer.entities.contains(part)) {
36426
- params.viewer.entities.remove(part);
37324
+ if (part && part instanceof Cesium.Entity) {
37325
+ RemoveCEntityFromScene(params.viewer, part);
36427
37326
  }
36428
37327
  }
36429
37328
  cEntity._siblingGraphics = [];
@@ -36454,7 +37353,12 @@
36454
37353
  if (oldClampToGround != newClampToGround) {
36455
37354
  cEntity.polyline.clampToGround = new Cesium.ConstantProperty(newClampToGround);
36456
37355
  }
36457
- cEntity.polyline.distanceDisplayCondition = new Cesium.ConstantProperty(exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance));
37356
+ SetCPropIfChanged({
37357
+ viewer: params.viewer,
37358
+ graphic: cEntity.polyline,
37359
+ property: "distanceDisplayCondition",
37360
+ value: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance)
37361
+ });
36458
37362
  cEntity.corridor = undefined;
36459
37363
  }
36460
37364
  else {
@@ -36478,7 +37382,12 @@
36478
37382
  if (oldZIndex != getZIndex(style, entity, params.tags)) {
36479
37383
  cEntity.corridor.zIndex = new Cesium.ConstantProperty(getZIndex(style, entity, params.tags));
36480
37384
  }
36481
- cEntity.corridor.distanceDisplayCondition = new Cesium.ConstantProperty(exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance, width));
37385
+ SetCPropIfChanged({
37386
+ viewer: params.viewer,
37387
+ graphic: cEntity.corridor,
37388
+ property: "distanceDisplayCondition",
37389
+ value: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance, width)
37390
+ });
36482
37391
  cEntity.polyline = undefined;
36483
37392
  }
36484
37393
  // We'll use "SetDefaultColor" to updating the internal reference and to allow for an animation.
@@ -36546,7 +37455,7 @@
36546
37455
  // De-dupe the tag IDs.
36547
37456
  tagIds = Array.from(new Set(tagIds));
36548
37457
  // Request if at least one then create a quick lookup map for tags.
36549
- const tags = tagIds.length > 0 ? (await BModels.EntityTag.GetListByIds({
37458
+ const tags = (!params.offline && tagIds.length > 0) ? (await BModels.EntityTag.GetListByIds({
36550
37459
  api,
36551
37460
  tagIds
36552
37461
  })).tags : [];
@@ -36558,7 +37467,7 @@
36558
37467
  for (let i = 0; i < params.entities.length; i++) {
36559
37468
  const entity = params.entities[i];
36560
37469
  const zoomItem = params.zoomItems[entity.Bruce.ID];
36561
- const style = zoomItem.StyleID != -1 ? (_a = (await getStyle$3(api, entity, zoomItem.StyleID))) === null || _a === void 0 ? void 0 : _a.Settings : zoomItem.Style;
37470
+ const style = (!params.offline && zoomItem.StyleID != -1) ? (_a = (await getStyle$3(api, entity, zoomItem.StyleID))) === null || _a === void 0 ? void 0 : _a.Settings : zoomItem.Style;
36562
37471
  // Pass through tags that may be needed for styling.
36563
37472
  const tagIds = entity.Bruce["Layer.ID"];
36564
37473
  const tags = [];
@@ -36581,7 +37490,7 @@
36581
37490
  rendered: (_c = params.rendered) === null || _c === void 0 ? void 0 : _c.get(entity.Bruce.ID)
36582
37491
  });
36583
37492
  if (cEntity) {
36584
- const name = await getName$2(api, entity);
37493
+ const name = await getName$2(api, entity, params.offline);
36585
37494
  cEntity.name = name;
36586
37495
  cEntity._renderGroup = exports.EntityRenderEngine.GetRenderGroupId(zoomItem);
36587
37496
  cEntity.styleId = zoomItem.StyleID == -1 ? -1 : (+zoomItem.StyleID || null);
@@ -36592,7 +37501,16 @@
36592
37501
  }
36593
37502
  EntityRenderEnginePolyline.RenderGroup = RenderGroup;
36594
37503
  })(exports.EntityRenderEnginePolyline || (exports.EntityRenderEnginePolyline = {}));
36595
- async function getName$2(api, entity) {
37504
+ async function getName$2(api, entity, offline) {
37505
+ var _a;
37506
+ const ownName = GetUsableEntityName(entity);
37507
+ if (ownName) {
37508
+ return ownName;
37509
+ }
37510
+ if (offline) {
37511
+ // Anything further needs a request, so offline callers take what the entity carries, placeholder or not.
37512
+ return ((_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.Name) || "Unnamed Entity";
37513
+ }
36596
37514
  try {
36597
37515
  const typeId = entity.Bruce["EntityType.ID"];
36598
37516
  let type = null;
@@ -36834,6 +37752,13 @@
36834
37752
  GetArchiveUrl() {
36835
37753
  return this.archiveUrl;
36836
37754
  }
37755
+ /**
37756
+ * The property this instance presents frames through, for a caller that needs to rebuild the entity's
37757
+ * material (eg: re-tinting for opacity/selection) without detaching the animation.
37758
+ */
37759
+ GetImageProperty() {
37760
+ return this.imageProperty;
37761
+ }
36837
37762
  onClockTick(currentTime) {
36838
37763
  if (this.disposed) {
36839
37764
  return;
@@ -37039,7 +37964,7 @@
37039
37964
  let textureDataUri = null;
37040
37965
  let textureTraceEffective = null;
37041
37966
  let frameArchive = null;
37042
- if (fillType === BModels.Style.EPolygonFillType.Texture) {
37967
+ if (fillType === BModels.Style.EPolygonFillType.Texture && !params.offline) {
37043
37968
  try {
37044
37969
  const resolved = await resolveTexturedFill({
37045
37970
  api: params.api,
@@ -37120,8 +38045,8 @@
37120
38045
  // We'll remove all that aren't in the allowed (direct) list.
37121
38046
  for (let i = 0; i < parts.length - 1; i++) {
37122
38047
  const part = parts[i];
37123
- if (part && part instanceof Cesium.Entity && params.viewer.entities.contains(part) && !cEntity._siblingGraphics.includes(part)) {
37124
- params.viewer.entities.remove(part);
38048
+ if (part && part instanceof Cesium.Entity && !cEntity._siblingGraphics.includes(part)) {
38049
+ RemoveCEntityFromScene(params.viewer, part);
37125
38050
  }
37126
38051
  }
37127
38052
  if (cEntity._parentEntity) {
@@ -37131,7 +38056,8 @@
37131
38056
  };
37132
38057
  const hasOutline = width > 0 && cLineColor;
37133
38058
  let cEntity = null;
37134
- if (!params.rendered || !params.rendered.polygon) {
38059
+ const isExistingGraphic = Boolean(params.rendered && params.rendered.polygon);
38060
+ if (!isExistingGraphic) {
37135
38061
  cEntity = new Cesium.Entity({
37136
38062
  id: BModels.ObjectUtils.UId(15),
37137
38063
  polygon: {
@@ -37144,7 +38070,7 @@
37144
38070
  classificationType: classification,
37145
38071
  perPositionHeight: heightRef == Cesium.HeightReference.CLAMP_TO_GROUND ? false : true,
37146
38072
  zIndex: zIndex,
37147
- distanceDisplayCondition: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance, width <= 0 || !cLineColor || units == "m" ? size : null, true)
38073
+ distanceDisplayCondition: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance, width <= 0 || !cLineColor || units == "m" ? size : null, true, size)
37148
38074
  },
37149
38075
  position: exports.EntityUtils.GetPos({
37150
38076
  viewer: params.viewer,
@@ -37198,9 +38124,38 @@
37198
38124
  if (oldZIndex != zIndex) {
37199
38125
  cEntity.polygon.zIndex = new Cesium.ConstantProperty(zIndex);
37200
38126
  }
37201
- cEntity.polygon.distanceDisplayCondition = new Cesium.ConstantProperty(exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance, width <= 0 || !cLineColor || units == "m" ? size : null, true));
37202
- cEntity.polygon.shadows = new Cesium.ConstantProperty(Cesium.ShadowMode.ENABLED);
37203
- cEntity.polygon.classificationType = new Cesium.ConstantProperty(classification);
38127
+ SetCPropIfChanged({
38128
+ viewer: params.viewer,
38129
+ graphic: cEntity.polygon,
38130
+ property: "distanceDisplayCondition",
38131
+ value: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance, width <= 0 || !cLineColor || units == "m" ? size : null, true, size)
38132
+ });
38133
+ SetCPropIfChanged({
38134
+ viewer: params.viewer,
38135
+ graphic: cEntity.polygon,
38136
+ property: "shadows",
38137
+ value: Cesium.ShadowMode.ENABLED
38138
+ });
38139
+ SetCPropIfChanged({
38140
+ viewer: params.viewer,
38141
+ graphic: cEntity.polygon,
38142
+ property: "classificationType",
38143
+ value: classification
38144
+ });
38145
+ cEntity.show = true;
38146
+ }
38147
+ // Must run before any material is baked below: the Animator takes over polygon.material, so a bake
38148
+ // has to know whether it is baking over a live animation or over a disposed one's restored material.
38149
+ const animator = syncTextureFrameArchive(cEntity, frameArchive, params.viewer, style.textureColorMask);
38150
+ if (animator) {
38151
+ exports.CesiumEntityStyler.SetDefaultTextureImage({
38152
+ entity: cEntity,
38153
+ image: animator.GetImageProperty(),
38154
+ viewer: params.viewer,
38155
+ requestRender: false
38156
+ });
38157
+ }
38158
+ else if (isExistingGraphic) {
37204
38159
  // TODO: animate the texture!
37205
38160
  if (textureDataUri) {
37206
38161
  // Rebaking texture to ensure we don't revert to the wrong one later.
@@ -37227,12 +38182,12 @@
37227
38182
  entity: cEntity,
37228
38183
  viewer: params.viewer,
37229
38184
  override: true,
38185
+ // Fill colour only. The outline sibling gets its own line colour baked below.
38186
+ includeSiblings: false,
37230
38187
  requestRender: false
37231
38188
  });
37232
38189
  }
37233
- cEntity.show = true;
37234
38190
  }
37235
- syncTextureFrameArchive(cEntity, frameArchive, params.viewer, style.textureColorMask);
37236
38191
  if (frameArchive && params.onSeriesDiscovered) {
37237
38192
  const segment = frameArchiveToSeriesSegment(frameArchive, params.entity);
37238
38193
  if (segment) {
@@ -37265,7 +38220,7 @@
37265
38220
  classificationType: Cesium.ClassificationType.TERRAIN,
37266
38221
  arcType: Cesium.ArcType.GEODESIC,
37267
38222
  zIndex: zIndex,
37268
- distanceDisplayCondition: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance)
38223
+ distanceDisplayCondition: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance, null, false, size)
37269
38224
  }) : null,
37270
38225
  corridor: units == "m" ? {
37271
38226
  positions: borderPosses,
@@ -37277,7 +38232,7 @@
37277
38232
  zIndex: zIndex + 1,
37278
38233
  cornerType: Cesium.CornerType.MITERED,
37279
38234
  classificationType: classification,
37280
- distanceDisplayCondition: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance, width),
38235
+ distanceDisplayCondition: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance, width, false, size),
37281
38236
  shadows: Cesium.ShadowMode.ENABLED
37282
38237
  } : null,
37283
38238
  show: true
@@ -37304,11 +38259,21 @@
37304
38259
  }
37305
38260
  const oldMaterial = GetCValue(params.viewer, cEntityBorder.polyline.material);
37306
38261
  const newMaterial = new Cesium.ColorMaterialProperty(cLineColor);
37307
- if (compareColorMaterials(params.viewer, oldMaterial, newMaterial)) {
38262
+ if (!areColorMaterialsEqual(params.viewer, oldMaterial, newMaterial)) {
37308
38263
  cEntityBorder.polyline.material = newMaterial;
37309
38264
  }
37310
- cEntityBorder.polyline.classificationType = new Cesium.ConstantProperty(Cesium.ClassificationType.TERRAIN);
37311
- cEntityBorder.polyline.distanceDisplayCondition = new Cesium.ConstantProperty(exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance));
38265
+ SetCPropIfChanged({
38266
+ viewer: params.viewer,
38267
+ graphic: cEntityBorder.polyline,
38268
+ property: "classificationType",
38269
+ value: Cesium.ClassificationType.TERRAIN
38270
+ });
38271
+ SetCPropIfChanged({
38272
+ viewer: params.viewer,
38273
+ graphic: cEntityBorder.polyline,
38274
+ property: "distanceDisplayCondition",
38275
+ value: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance, null, false, size)
38276
+ });
37312
38277
  cEntityBorder.corridor = undefined;
37313
38278
  }
37314
38279
  else {
@@ -37334,15 +38299,35 @@
37334
38299
  }
37335
38300
  const oldMaterial = GetCValue(params.viewer, cEntityBorder.corridor.material);
37336
38301
  const newMaterial = new Cesium.ColorMaterialProperty(cLineColor);
37337
- if (compareColorMaterials(params.viewer, oldMaterial, newMaterial)) {
38302
+ if (!areColorMaterialsEqual(params.viewer, oldMaterial, newMaterial)) {
37338
38303
  cEntityBorder.corridor.material = newMaterial;
37339
38304
  }
37340
- cEntityBorder.corridor.fill = new Cesium.ConstantProperty(true);
37341
- cEntityBorder.corridor.distanceDisplayCondition = new Cesium.ConstantProperty(exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance, width));
38305
+ SetCPropIfChanged({
38306
+ viewer: params.viewer,
38307
+ graphic: cEntityBorder.corridor,
38308
+ property: "fill",
38309
+ value: true
38310
+ });
38311
+ SetCPropIfChanged({
38312
+ viewer: params.viewer,
38313
+ graphic: cEntityBorder.corridor,
38314
+ property: "distanceDisplayCondition",
38315
+ value: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance, width, false, size)
38316
+ });
37342
38317
  cEntityBorder.polyline = undefined;
37343
38318
  }
37344
38319
  cEntityBorder.show = true;
37345
38320
  }
38321
+ // Bake the outline's own colour as its default, so a deselect/opacity refresh restores the line
38322
+ // colour rather than falling back to the fill's.
38323
+ exports.CesiumEntityStyler.SetDefaultColor({
38324
+ color: cLineColor,
38325
+ entity: cEntityBorder,
38326
+ viewer: params.viewer,
38327
+ override: true,
38328
+ includeSiblings: false,
38329
+ requestRender: false
38330
+ });
37346
38331
  cEntityBorder._parentEntity = cEntity;
37347
38332
  cEntity._siblingGraphics.push(cEntityBorder);
37348
38333
  for (let i = 0; i < holePosses.length; i++) {
@@ -37358,7 +38343,7 @@
37358
38343
  classificationType: Cesium.ClassificationType.TERRAIN,
37359
38344
  arcType: Cesium.ArcType.GEODESIC,
37360
38345
  zIndex: zIndex,
37361
- distanceDisplayCondition: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance)
38346
+ distanceDisplayCondition: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance, null, false, size)
37362
38347
  }) : null,
37363
38348
  corridor: units == "m" ? {
37364
38349
  positions: borderPosses,
@@ -37370,7 +38355,7 @@
37370
38355
  zIndex: zIndex + 1,
37371
38356
  cornerType: Cesium.CornerType.MITERED,
37372
38357
  classificationType: classification,
37373
- distanceDisplayCondition: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance, width),
38358
+ distanceDisplayCondition: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance, width, false, size),
37374
38359
  shadows: Cesium.ShadowMode.ENABLED,
37375
38360
  } : null,
37376
38361
  show: true
@@ -37411,7 +38396,7 @@
37411
38396
  // De-dupe the tag IDs.
37412
38397
  tagIds = Array.from(new Set(tagIds));
37413
38398
  // Request if at least one then create a quick lookup map for tags.
37414
- const tags = tagIds.length > 0 ? (await BModels.EntityTag.GetListByIds({
38399
+ const tags = (!params.offline && tagIds.length > 0) ? (await BModels.EntityTag.GetListByIds({
37415
38400
  api,
37416
38401
  tagIds
37417
38402
  })).tags : [];
@@ -37423,7 +38408,7 @@
37423
38408
  for (let i = 0; i < params.entities.length; i++) {
37424
38409
  const entity = params.entities[i];
37425
38410
  const zoomItem = params.zoomItems[entity.Bruce.ID];
37426
- const style = zoomItem.StyleID != -1 ? (_a = (await getStyle$4(api, entity, zoomItem.StyleID))) === null || _a === void 0 ? void 0 : _a.Settings : zoomItem.Style;
38411
+ const style = (!params.offline && zoomItem.StyleID != -1) ? (_a = (await getStyle$4(api, entity, zoomItem.StyleID))) === null || _a === void 0 ? void 0 : _a.Settings : zoomItem.Style;
37427
38412
  // Pass through tags that may be needed for styling.
37428
38413
  const tagIds = entity.Bruce["Layer.ID"];
37429
38414
  const tags = [];
@@ -37458,7 +38443,7 @@
37458
38443
  console.error(`Polygon.RenderGroup: failed to render entity ${((_d = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _d === void 0 ? void 0 : _d.ID) || "<NONE>"}:`, e);
37459
38444
  }
37460
38445
  if (cEntity) {
37461
- const name = await getName$3(api, entity);
38446
+ const name = await getName$3(api, entity, params.offline);
37462
38447
  cEntity.name = name;
37463
38448
  cEntity._renderGroup = exports.EntityRenderEngine.GetRenderGroupId(zoomItem);
37464
38449
  cEntity.styleId = zoomItem.StyleID == -1 ? -1 : (+zoomItem.StyleID || null);
@@ -37480,7 +38465,16 @@
37480
38465
  }
37481
38466
  EntityRenderEnginePolygon.DisposeTextureFrameSeriesAnimator = DisposeTextureFrameSeriesAnimator;
37482
38467
  })(exports.EntityRenderEnginePolygon || (exports.EntityRenderEnginePolygon = {}));
37483
- async function getName$3(api, entity) {
38468
+ async function getName$3(api, entity, offline) {
38469
+ var _a;
38470
+ const ownName = GetUsableEntityName(entity);
38471
+ if (ownName) {
38472
+ return ownName;
38473
+ }
38474
+ if (offline) {
38475
+ // Anything further needs a request, so offline callers take what the entity carries, placeholder or not.
38476
+ return ((_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.Name) || "Unnamed Entity";
38477
+ }
37484
38478
  try {
37485
38479
  const typeId = entity.Bruce["EntityType.ID"];
37486
38480
  let type = null;
@@ -37584,6 +38578,7 @@
37584
38578
  /**
37585
38579
  * Reconciles cEntity's TextureFrameSeriesAnimator.Animator (if any) against this render's resolved texture.
37586
38580
  * Called once cEntity is fully created/updated and definitely has a `polygon` graphics.
38581
+ * Returns the animator now driving the entity, or null if this render has no frame series.
37587
38582
  * @param cEntity
37588
38583
  * @param frameArchive
37589
38584
  * @param viewer
@@ -37592,22 +38587,24 @@
37592
38587
  function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask) {
37593
38588
  const existing = cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY];
37594
38589
  if (frameArchive && existing && !existing.IsDisposed() && existing.GetArchiveUrl() === frameArchive.url) {
37595
- return;
38590
+ return existing;
37596
38591
  }
37597
38592
  if (existing && !existing.IsDisposed()) {
37598
38593
  existing.Dispose();
37599
38594
  cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY] = null;
37600
38595
  }
37601
- if (frameArchive) {
37602
- const animator = new TextureFrameSeriesAnimator.Animator({
37603
- viewer,
37604
- entity: cEntity,
37605
- archiveUrl: frameArchive.url,
37606
- frames: frameArchive.metadata.Frames,
37607
- textureColorMask
37608
- });
37609
- cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY] = animator;
38596
+ if (!frameArchive) {
38597
+ return null;
37610
38598
  }
38599
+ const animator = new TextureFrameSeriesAnimator.Animator({
38600
+ viewer,
38601
+ entity: cEntity,
38602
+ archiveUrl: frameArchive.url,
38603
+ frames: frameArchive.metadata.Frames,
38604
+ textureColorMask
38605
+ });
38606
+ cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY] = animator;
38607
+ return animator;
37611
38608
  }
37612
38609
  /**
37613
38610
  * Derives a min/max/label time-range segment from a resolved frame archive's Frames metadata,
@@ -37774,7 +38771,7 @@
37774
38771
  * @param b
37775
38772
  * @returns
37776
38773
  */
37777
- function compareColorMaterials(viewer, a, b) {
38774
+ function areColorMaterialsEqual(viewer, a, b) {
37778
38775
  // Same reference.
37779
38776
  if (a == b) {
37780
38777
  return true;
@@ -38083,15 +39080,44 @@
38083
39080
  }
38084
39081
  })(exports.StyleEffective || (exports.StyleEffective = {}));
38085
39082
  (function (EntityRenderEngine) {
39083
+ /**
39084
+ * Identifies the zoom band a graphic was last rendered for, covering everything in that band which
39085
+ * changes how the entity is drawn. Used to decide whether a re-render is needed at all.
39086
+ *
39087
+ * Deliberately NOT the reuse key: most of a band's identity is plain graphics properties that can be
39088
+ * patched onto the existing graphic, so see GetGraphicShapeId for that decision.
39089
+ */
38086
39090
  function GetRenderGroupId(zoomItem) {
38087
39091
  if (!zoomItem) {
38088
39092
  return null;
38089
39093
  }
38090
- return zoomItem.MinZoom + "-" + zoomItem.MaxZoom;
39094
+ return exports.RenderManager.GetZoomControlSignature([zoomItem]);
38091
39095
  }
38092
39096
  EntityRenderEngine.GetRenderGroupId = GetRenderGroupId;
39097
+ /**
39098
+ * Identifies the kind of Cesium graphic a zoom item + entity resolves to, ignoring anything that is merely a property on it.
39099
+ */
39100
+ function GetGraphicShapeId(zoomItem, entity) {
39101
+ var _a;
39102
+ let displayType = zoomItem === null || zoomItem === void 0 ? void 0 : zoomItem.DisplayType;
39103
+ if (!displayType) {
39104
+ displayType = BModels.ZoomControl.EDisplayType.Hidden;
39105
+ }
39106
+ // Backwards compatibility, same mapping the render pass applies.
39107
+ else if (displayType == "polygon") {
39108
+ displayType = BModels.ZoomControl.EDisplayType.Geometry;
39109
+ }
39110
+ // Multi-geometry builds a container graphic holding sibling graphics rather than a flat one.
39111
+ const geometry = BModels.Entity.GetValue({
39112
+ entity: entity,
39113
+ path: ["Bruce", "VectorGeometry"]
39114
+ });
39115
+ const isMulti = ((_a = geometry === null || geometry === void 0 ? void 0 : geometry.MultiGeometry) === null || _a === void 0 ? void 0 : _a.length) > 1;
39116
+ return displayType + "|" + (isMulti ? "multi" : "single");
39117
+ }
39118
+ EntityRenderEngine.GetGraphicShapeId = GetGraphicShapeId;
38093
39119
  async function Render(params) {
38094
- var _a, _b, _c, _d;
39120
+ var _a, _b, _c;
38095
39121
  const groupRenderParams = {
38096
39122
  apiGetter: params.apiGetter,
38097
39123
  viewer: params.viewer,
@@ -38099,10 +39125,15 @@
38099
39125
  zoomItems: {},
38100
39126
  menuItemId: params.menuItemId,
38101
39127
  onSeriesDiscovered: params.onSeriesDiscovered,
38102
- visualRegister: params.visualRegister
39128
+ visualRegister: params.visualRegister,
39129
+ offline: params.offline
38103
39130
  };
38104
39131
  const updated = new Map();
38105
39132
  const cEntities = new Map();
39133
+ // Entity ID -> the graphic shape resolved for this pass, stamped onto the results at the end.
39134
+ const shapeIds = new Map();
39135
+ // Entity ID -> what its zoom band was resolved from, stamped alongside the shape.
39136
+ const bandChecks = new Map();
38106
39137
  const models = [];
38107
39138
  const multiGeometry = [];
38108
39139
  const polygons = [];
@@ -38152,11 +39183,21 @@
38152
39183
  for (let i = 0; i < params.entities.length; i++) {
38153
39184
  let entity = params.entities[i];
38154
39185
  const id = entity.Bruce.ID;
39186
+ const resolution = {};
38155
39187
  const zoomItem = exports.RenderManager.DetermineZoomItem({
38156
39188
  viewer: params.viewer,
38157
39189
  entity,
38158
- zoomControl: params.zoomControl
38159
- });
39190
+ zoomControl: params.zoomControl,
39191
+ out: resolution
39192
+ });
39193
+ if (zoomItem && resolution.distance != null && resolution.cameraPos) {
39194
+ bandChecks.set(id, {
39195
+ distance: resolution.distance,
39196
+ cameraPos: resolution.cameraPos,
39197
+ min: zoomItem.MinZoom,
39198
+ max: zoomItem.MaxZoom
39199
+ });
39200
+ }
38160
39201
  let displayType = zoomItem === null || zoomItem === void 0 ? void 0 : zoomItem.DisplayType;
38161
39202
  if (!displayType) {
38162
39203
  displayType = BModels.ZoomControl.EDisplayType.Hidden;
@@ -38170,8 +39211,13 @@
38170
39211
  entityId: id,
38171
39212
  menuItemId: params.menuItemId
38172
39213
  });
39214
+ const existingVisual = existingRego === null || existingRego === void 0 ? void 0 : existingRego.visual;
38173
39215
  const newRenderId = GetRenderGroupId(zoomItem);
38174
- const oldRenderId = (_a = existingRego === null || existingRego === void 0 ? void 0 : existingRego.visual) === null || _a === void 0 ? void 0 : _a._renderGroup;
39216
+ const oldRenderId = existingVisual === null || existingVisual === void 0 ? void 0 : existingVisual._renderGroup;
39217
+ // Resolved before prepareGeometry() below, which flattens a single-item MultiGeometry and
39218
+ // would otherwise make the same entity resolve to a different shape than it was stamped with.
39219
+ const newShapeId = GetGraphicShapeId(zoomItem, entity);
39220
+ shapeIds.set(id, newShapeId);
38175
39221
  if (!params.force &&
38176
39222
  newRenderId == oldRenderId &&
38177
39223
  !(existingRego === null || existingRego === void 0 ? void 0 : existingRego.stale) &&
@@ -38181,13 +39227,13 @@
38181
39227
  cEntities.set(id, existingRego.visual);
38182
39228
  }
38183
39229
  else {
38184
- // Add so we can re-use the graphic and update it.
38185
- if (existingRego && newRenderId == oldRenderId) {
39230
+ const oldShapeId = existingVisual === null || existingVisual === void 0 ? void 0 : existingVisual._graphicShape;
39231
+ if (existingRego && newShapeId == oldShapeId) {
38186
39232
  cEntities.set(id, existingRego.visual);
38187
39233
  // Flag as no longer stale as we're unlikely to recreate the rego if we're reusing the graphic.
38188
39234
  existingRego.stale = false;
38189
39235
  // Update metadata for the same reason.
38190
- existingRego.outline = (_b = entity.Bruce) === null || _b === void 0 ? void 0 : _b.Outline;
39236
+ existingRego.outline = (_a = entity.Bruce) === null || _a === void 0 ? void 0 : _a.Outline;
38191
39237
  existingRego.entityTypeId = entity.Bruce["EntityType.ID"];
38192
39238
  updated.set(id, true);
38193
39239
  }
@@ -38201,7 +39247,7 @@
38201
39247
  entity: entity,
38202
39248
  path: ["Bruce", "VectorGeometry"]
38203
39249
  });
38204
- if ((_c = geometry === null || geometry === void 0 ? void 0 : geometry.MultiGeometry) === null || _c === void 0 ? void 0 : _c.length) {
39250
+ if ((_b = geometry === null || geometry === void 0 ? void 0 : geometry.MultiGeometry) === null || _b === void 0 ? void 0 : _b.length) {
38205
39251
  multiGeometry.push(entity);
38206
39252
  }
38207
39253
  else {
@@ -38244,7 +39290,7 @@
38244
39290
  entity: entity,
38245
39291
  path: ["Bruce", "VectorGeometry"]
38246
39292
  });
38247
- if (!((_d = geometry === null || geometry === void 0 ? void 0 : geometry.MultiGeometry) === null || _d === void 0 ? void 0 : _d.length)) {
39293
+ if (!((_c = geometry === null || geometry === void 0 ? void 0 : geometry.MultiGeometry) === null || _c === void 0 ? void 0 : _c.length)) {
38248
39294
  polygons.push(entity);
38249
39295
  continue;
38250
39296
  }
@@ -38363,6 +39409,21 @@
38363
39409
  }
38364
39410
  }
38365
39411
  }
39412
+ // Stamped centrally rather than per-engine so the value always matches what the gate above compared,
39413
+ // regardless of which engine (or fallback engine) ended up producing the graphic.
39414
+ cEntities.forEach((cEntity, id) => {
39415
+ if (!cEntity) {
39416
+ return;
39417
+ }
39418
+ const shapeId = shapeIds.get(id);
39419
+ if (shapeId) {
39420
+ cEntity._graphicShape = shapeId;
39421
+ }
39422
+ const bandCheck = bandChecks.get(id);
39423
+ if (bandCheck) {
39424
+ cEntity._bandCheck = bandCheck;
39425
+ }
39426
+ });
38366
39427
  return {
38367
39428
  entities: cEntities,
38368
39429
  updated
@@ -38398,9 +39459,7 @@
38398
39459
  });
38399
39460
  }
38400
39461
  }
38401
- if (viewer.entities.contains(entity)) {
38402
- viewer.entities.remove(entity);
38403
- }
39462
+ RemoveCEntityFromScene(viewer, entity);
38404
39463
  }
38405
39464
  doRemove({
38406
39465
  viewer: params.viewer,
@@ -38435,9 +39494,10 @@
38435
39494
  * @param max
38436
39495
  * @param meterSize size of "thing" in meters, eg polyline is 0.5 meters width. If size is provided we can potentially cull it earlier.
38437
39496
  * @param isPolygon
39497
+ * @param fitMeterSize overall extent of the "thing" in meters, eg a parcel's bounding-box diagonal. If provided we keep it visible far enough out to actually fit on screen.
38438
39498
  * @returns
38439
39499
  */
38440
- function GetDisplayCondition(min, max, meterSize, isPolygon) {
39500
+ function GetDisplayCondition(min, max, meterSize, isPolygon, fitMeterSize) {
38441
39501
  // Max is required.
38442
39502
  if (isNaN(+max)) {
38443
39503
  return undefined;
@@ -38467,6 +39527,15 @@
38467
39527
  return undefined;
38468
39528
  }
38469
39529
  }
39530
+ // The configured max is a guess about viewing distance, and for a large thing that guess is often short:
39531
+ // it vanishes while still overflowing the screen, so you can never see all of it at once.
39532
+ // A thing of size S needs roughly S * 0.87 of camera distance to fit a 60deg vertical fov, so we
39533
+ // floor the max at a margin above that rather than culling something you haven't finished zooming out to.
39534
+ fitMeterSize = +fitMeterSize;
39535
+ if (!isNaN(fitMeterSize) && fitMeterSize > 0) {
39536
+ const FIT_DISTANCE_PER_METER = 1.4;
39537
+ max = Math.max(max, fitMeterSize * FIT_DISTANCE_PER_METER);
39538
+ }
38470
39539
  return new Cesium.DistanceDisplayCondition(min, max);
38471
39540
  }
38472
39541
  EntityRenderEngine.GetDisplayCondition = GetDisplayCondition;
@@ -38489,7 +39558,71 @@
38489
39558
  }
38490
39559
  return value;
38491
39560
  }
39561
+ const pendingRemovalsByViewer = new WeakMap();
39562
+ function scheduleEntityRemoval(viewer, entity) {
39563
+ let pending = pendingRemovalsByViewer.get(viewer);
39564
+ if (!pending) {
39565
+ pending = { entities: new Set(), hook: null };
39566
+ pendingRemovalsByViewer.set(viewer, pending);
39567
+ }
39568
+ pending.entities.add(entity);
39569
+ if (pending.hook) {
39570
+ return;
39571
+ }
39572
+ pending.hook = viewer.scene.postRender.addEventListener(() => {
39573
+ var _a;
39574
+ (_a = pending.hook) === null || _a === void 0 ? void 0 : _a.call(pending);
39575
+ pending.hook = null;
39576
+ const batch = Array.from(pending.entities);
39577
+ pending.entities.clear();
39578
+ if (!batch.length) {
39579
+ return;
39580
+ }
39581
+ WithSuspendedCCollections(batch.map(x => GetCEntityCollection(viewer, x)), () => {
39582
+ for (const item of batch) {
39583
+ try {
39584
+ exports.EntityRenderEngine.Remove({
39585
+ viewer,
39586
+ entity: item
39587
+ });
39588
+ }
39589
+ catch (e) {
39590
+ console.error(e);
39591
+ }
39592
+ }
39593
+ });
39594
+ });
39595
+ }
38492
39596
  (function (CesiumAnimatedInOut) {
39597
+ /**
39598
+ * Runs any removals already queued for this viewer immediately, instead of waiting for the next render.
39599
+ * For teardown paths that need the scene emptied synchronously.
39600
+ */
39601
+ function FlushPendingRemovals(viewer) {
39602
+ var _a;
39603
+ const pending = pendingRemovalsByViewer.get(viewer);
39604
+ if (!(pending === null || pending === void 0 ? void 0 : pending.entities.size)) {
39605
+ return;
39606
+ }
39607
+ const batch = Array.from(pending.entities);
39608
+ pending.entities.clear();
39609
+ (_a = pending.hook) === null || _a === void 0 ? void 0 : _a.call(pending);
39610
+ pending.hook = null;
39611
+ WithSuspendedCCollections(batch.map(x => GetCEntityCollection(viewer, x)), () => {
39612
+ for (const item of batch) {
39613
+ try {
39614
+ exports.EntityRenderEngine.Remove({
39615
+ viewer,
39616
+ entity: item
39617
+ });
39618
+ }
39619
+ catch (e) {
39620
+ console.error(e);
39621
+ }
39622
+ }
39623
+ });
39624
+ }
39625
+ CesiumAnimatedInOut.FlushPendingRemovals = FlushPendingRemovals;
38493
39626
  /**
38494
39627
  * Animates an Entity out of the scene.
38495
39628
  * Removes it from the scene at the end of the animation.
@@ -38508,13 +39641,7 @@
38508
39641
  return;
38509
39642
  }
38510
39643
  removed = true;
38511
- const removal = viewer.scene.postRender.addEventListener(() => {
38512
- removal();
38513
- exports.EntityRenderEngine.Remove({
38514
- viewer,
38515
- entity
38516
- });
38517
- });
39644
+ scheduleEntityRemoval(viewer, entity);
38518
39645
  };
38519
39646
  for (let i = 0; i < pieces.length; i++) {
38520
39647
  const piece = pieces[i];
@@ -39062,7 +40189,7 @@
39062
40189
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
39063
40190
  })(exports.StyleUtils || (exports.StyleUtils = {}));
39064
40191
 
39065
- const VERSION = "7.0.2";
40192
+ const VERSION = "7.0.3";
39066
40193
  /**
39067
40194
  * Updates the environment instance used by bruce-cesium to one specified.
39068
40195
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.