bruce-cesium 7.6.1 → 7.6.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 (95) hide show
  1. package/dist/bruce-cesium.es5.js +2535 -385
  2. package/dist/bruce-cesium.es5.js.map +1 -1
  3. package/dist/bruce-cesium.umd.js +2524 -382
  4. package/dist/bruce-cesium.umd.js.map +1 -1
  5. package/dist/lib/bruce-cesium.js +3 -1
  6. package/dist/lib/bruce-cesium.js.map +1 -1
  7. package/dist/lib/rendering/cesium-animated-property.js +63 -107
  8. package/dist/lib/rendering/cesium-animated-property.js.map +1 -1
  9. package/dist/lib/rendering/entity-gatherer.js +11 -3
  10. package/dist/lib/rendering/entity-gatherer.js.map +1 -1
  11. package/dist/lib/rendering/menu-item-creator.js +44 -23
  12. package/dist/lib/rendering/menu-item-creator.js.map +1 -1
  13. package/dist/lib/rendering/menu-item-manager.js +199 -17
  14. package/dist/lib/rendering/menu-item-manager.js.map +1 -1
  15. package/dist/lib/rendering/menu-item-styles.js +151 -0
  16. package/dist/lib/rendering/menu-item-styles.js.map +1 -0
  17. package/dist/lib/rendering/relation-render-engine.js +24 -5
  18. package/dist/lib/rendering/relation-render-engine.js.map +1 -1
  19. package/dist/lib/rendering/render-managers/common/entity-label-group.js +13 -9
  20. package/dist/lib/rendering/render-managers/common/entity-label-group.js.map +1 -1
  21. package/dist/lib/rendering/render-managers/common/entity-label.js +68 -7
  22. package/dist/lib/rendering/render-managers/common/entity-label.js.map +1 -1
  23. package/dist/lib/rendering/render-managers/common/entity-rerender-maintain-state.js +44 -5
  24. package/dist/lib/rendering/render-managers/common/entity-rerender-maintain-state.js.map +1 -1
  25. package/dist/lib/rendering/render-managers/common/entity-simulation.js +483 -0
  26. package/dist/lib/rendering/render-managers/common/entity-simulation.js.map +1 -0
  27. package/dist/lib/rendering/render-managers/common/historic-trail.js +123 -0
  28. package/dist/lib/rendering/render-managers/common/historic-trail.js.map +1 -0
  29. package/dist/lib/rendering/render-managers/common/historic-utils.js +17 -6
  30. package/dist/lib/rendering/render-managers/common/historic-utils.js.map +1 -1
  31. package/dist/lib/rendering/render-managers/entities/entities-datalab-render-manager.js +13 -2
  32. package/dist/lib/rendering/render-managers/entities/entities-datalab-render-manager.js.map +1 -1
  33. package/dist/lib/rendering/render-managers/entities/entities-ids-render-manager.js +20 -2
  34. package/dist/lib/rendering/render-managers/entities/entities-ids-render-manager.js.map +1 -1
  35. package/dist/lib/rendering/render-managers/entities/entities-loaded-render-manager.js +13 -2
  36. package/dist/lib/rendering/render-managers/entities/entities-loaded-render-manager.js.map +1 -1
  37. package/dist/lib/rendering/render-managers/entities/entities-render-manager.js +66 -5
  38. package/dist/lib/rendering/render-managers/entities/entities-render-manager.js.map +1 -1
  39. package/dist/lib/rendering/render-managers/entities/entity-render-manager.js +11 -1
  40. package/dist/lib/rendering/render-managers/entities/entity-render-manager.js.map +1 -1
  41. package/dist/lib/rendering/render-managers/other/assembly-render-manager.js +745 -106
  42. package/dist/lib/rendering/render-managers/other/assembly-render-manager.js.map +1 -1
  43. package/dist/lib/rendering/render-managers/other/legacy-relations-render-manager.js +23 -1
  44. package/dist/lib/rendering/render-managers/other/legacy-relations-render-manager.js.map +1 -1
  45. package/dist/lib/rendering/render-managers/other/relations-render-manager.js +23 -1
  46. package/dist/lib/rendering/render-managers/other/relations-render-manager.js.map +1 -1
  47. package/dist/lib/rendering/render-managers/render-manager.js.map +1 -1
  48. package/dist/lib/rendering/render-managers/tilesets/tileset-arb-render-manager.js +19 -0
  49. package/dist/lib/rendering/render-managers/tilesets/tileset-arb-render-manager.js.map +1 -1
  50. package/dist/lib/rendering/render-managers/tilesets/tileset-cad-render-manager.js +145 -40
  51. package/dist/lib/rendering/render-managers/tilesets/tileset-cad-render-manager.js.map +1 -1
  52. package/dist/lib/rendering/render-managers/tilesets/tileset-entities-render-manager.js +18 -0
  53. package/dist/lib/rendering/render-managers/tilesets/tileset-entities-render-manager.js.map +1 -1
  54. package/dist/lib/rendering/tileset-styler.js +1 -1
  55. package/dist/lib/rendering/tileset-styler.js.map +1 -1
  56. package/dist/lib/rendering/visual-register-culler.js +2 -0
  57. package/dist/lib/rendering/visual-register-culler.js.map +1 -1
  58. package/dist/lib/rendering/visuals-register.js +89 -20
  59. package/dist/lib/rendering/visuals-register.js.map +1 -1
  60. package/dist/lib/utils/entity-utils.js +43 -2
  61. package/dist/lib/utils/entity-utils.js.map +1 -1
  62. package/dist/lib/widgets/tabs-left-panel/widget-left-panel-tab-menu-items.js +64 -12
  63. package/dist/lib/widgets/tabs-left-panel/widget-left-panel-tab-menu-items.js.map +1 -1
  64. package/dist/lib/widgets/widget-info-view.js +20 -7
  65. package/dist/lib/widgets/widget-info-view.js.map +1 -1
  66. package/dist/types/bruce-cesium.d.ts +3 -1
  67. package/dist/types/rendering/cesium-animated-property.d.ts +8 -6
  68. package/dist/types/rendering/entity-gatherer.d.ts +6 -1
  69. package/dist/types/rendering/menu-item-manager.d.ts +58 -1
  70. package/dist/types/rendering/menu-item-styles.d.ts +30 -0
  71. package/dist/types/rendering/relation-render-engine.d.ts +10 -0
  72. package/dist/types/rendering/render-managers/common/entity-label-group.d.ts +3 -2
  73. package/dist/types/rendering/render-managers/common/entity-label.d.ts +4 -0
  74. package/dist/types/rendering/render-managers/common/entity-rerender-maintain-state.d.ts +20 -0
  75. package/dist/types/rendering/render-managers/common/entity-simulation.d.ts +126 -0
  76. package/dist/types/rendering/render-managers/common/historic-trail.d.ts +30 -0
  77. package/dist/types/rendering/render-managers/common/historic-utils.d.ts +4 -0
  78. package/dist/types/rendering/render-managers/entities/entities-datalab-render-manager.d.ts +7 -1
  79. package/dist/types/rendering/render-managers/entities/entities-ids-render-manager.d.ts +7 -1
  80. package/dist/types/rendering/render-managers/entities/entities-loaded-render-manager.d.ts +7 -1
  81. package/dist/types/rendering/render-managers/entities/entities-render-manager.d.ts +10 -1
  82. package/dist/types/rendering/render-managers/entities/entity-render-manager.d.ts +7 -1
  83. package/dist/types/rendering/render-managers/other/assembly-render-manager.d.ts +79 -2
  84. package/dist/types/rendering/render-managers/other/legacy-relations-render-manager.d.ts +5 -0
  85. package/dist/types/rendering/render-managers/other/relations-render-manager.d.ts +5 -0
  86. package/dist/types/rendering/render-managers/render-manager.d.ts +14 -1
  87. package/dist/types/rendering/render-managers/tilesets/tileset-arb-render-manager.d.ts +8 -1
  88. package/dist/types/rendering/render-managers/tilesets/tileset-cad-render-manager.d.ts +16 -2
  89. package/dist/types/rendering/render-managers/tilesets/tileset-entities-render-manager.d.ts +8 -1
  90. package/dist/types/rendering/tileset-styler.d.ts +2 -0
  91. package/dist/types/rendering/visuals-register.d.ts +13 -0
  92. package/dist/types/utils/entity-utils.d.ts +6 -0
  93. package/dist/types/widgets/tabs-left-panel/widget-left-panel-tab-menu-items.d.ts +1 -0
  94. package/dist/types/widgets/widget-info-view.d.ts +1 -0
  95. package/package.json +2 -2
@@ -2621,13 +2621,13 @@
2621
2621
  class AnimateTPositionSeries {
2622
2622
  constructor(params) {
2623
2623
  this.lastCalcAttitude = null;
2624
- // Cache for calculated values.
2624
+ // Cache for calculated values: the scene time and set of positions they were worked out for.
2625
2625
  this.lastCalcTime = null;
2626
+ this.lastCalcRevision = -1;
2626
2627
  this.lastCalcPos3d = null;
2627
- this.lastCalcPosApplyOverTime = false;
2628
- this.lastCalcPos3dApplyOverTimeTarget = null;
2629
- this.lastCalcPos3dApplyOverTimeTimeStart = null;
2630
2628
  this.lastCalcHeading = null;
2629
+ // Bumped whenever the positions change, so a still clock re-applies a changed series.
2630
+ this.revision = 0;
2631
2631
  this.removal = null;
2632
2632
  // Track the timeline range for which we have data.
2633
2633
  this.minDate = null;
@@ -2657,6 +2657,7 @@
2657
2657
  if (!newPositions || newPositions.length === 0) {
2658
2658
  return;
2659
2659
  }
2660
+ this.revision++;
2660
2661
  // Add new positions to our map, overwriting any existing positions with the same timestamp
2661
2662
  for (const pos of newPositions) {
2662
2663
  const key = pos.dateTime.toISOString();
@@ -2680,6 +2681,17 @@
2680
2681
  }
2681
2682
  }
2682
2683
  }
2684
+ /**
2685
+ * Replaces every position with these, eg. once some that were simulated have been cleared.
2686
+ * @param positions Positions to animate through from now on.
2687
+ */
2688
+ replacePositions(positions) {
2689
+ this.positionsMap.clear();
2690
+ this.positions = [];
2691
+ this.minDate = null;
2692
+ this.maxDate = null;
2693
+ this.addPositions(positions);
2694
+ }
2683
2695
  /**
2684
2696
  * Get the current date range covered by our positions.
2685
2697
  */
@@ -2727,106 +2739,47 @@
2727
2739
  /*
2728
2740
  * Returns the attitude of the record at an index, or nothing recorded when it names none.
2729
2741
  */
2730
- attitudeAt(index) {
2731
- var _a, _b, _c;
2732
- const pos = index >= 0 ? (_a = this.positions) === null || _a === void 0 ? void 0 : _a[index] : null;
2733
- return { pitch: (_b = pos === null || pos === void 0 ? void 0 : pos.pitch) !== null && _b !== void 0 ? _b : null, roll: (_c = pos === null || pos === void 0 ? void 0 : pos.roll) !== null && _c !== void 0 ? _c : null };
2742
+ /*
2743
+ * Pitch and roll between two positions the short way round; an angle only one names is held.
2744
+ */
2745
+ attitudeBetween(indexLast, indexNext, timeMs) {
2746
+ var _a, _b, _c, _d, _e, _f;
2747
+ const a = indexLast >= 0 ? (_a = this.positions) === null || _a === void 0 ? void 0 : _a[indexLast] : null;
2748
+ const b = indexNext >= 0 ? (_b = this.positions) === null || _b === void 0 ? void 0 : _b[indexNext] : null;
2749
+ const span = a && b ? b.dateTime.getTime() - a.dateTime.getTime() : 0;
2750
+ const t = span > 0 ? (timeMs - a.dateTime.getTime()) / span : 0;
2751
+ const turn = (x, y) => {
2752
+ if (x == null || y == null) {
2753
+ return x !== null && x !== void 0 ? x : null;
2754
+ }
2755
+ return x + (Cesium.Math.negativePiToPi(Cesium.Math.toRadians(y - x)) * 180 / Math.PI) * t;
2756
+ };
2757
+ return { pitch: turn((_c = a === null || a === void 0 ? void 0 : a.pitch) !== null && _c !== void 0 ? _c : null, (_d = b === null || b === void 0 ? void 0 : b.pitch) !== null && _d !== void 0 ? _d : null), roll: turn((_e = a === null || a === void 0 ? void 0 : a.roll) !== null && _e !== void 0 ? _e : null, (_f = b === null || b === void 0 ? void 0 : b.roll) !== null && _f !== void 0 ? _f : null) };
2734
2758
  }
2759
+ /*
2760
+ * Places the series at the scene's time, exactly rather than eased, so what moves with it never lags the clock.
2761
+ */
2735
2762
  update() {
2763
+ var _a;
2736
2764
  if (!this.positions || this.positions.length === 0) {
2737
2765
  return;
2738
2766
  }
2739
- // Get current time.
2740
- let now = this.viewer.scene.lastRenderTime || this.viewer.clock.currentTime;
2741
- const nowTime = Cesium.JulianDate.toDate(now);
2742
- const nowTimeMs = nowTime.getTime();
2743
- const currentRealTimeMs = Date.now();
2744
- // Calculate the ideal position based on current time.
2745
- const targetPosition = this.calculatePosition(nowTimeMs);
2746
- // Initialize if this is the first update.
2747
- if (!this.lastCalcPos3d) {
2748
- this.lastCalcPos3d = targetPosition.pos3d;
2749
- this.lastCalcAttitude = this.attitudeAt(targetPosition.indexLast);
2750
- this.onUpdate(targetPosition.pos3d, this.lastCalcHeading, this.lastCalcAttitude);
2751
- return;
2752
- }
2753
- // Check if target has changed.
2754
- if (!this.lastCalcPos3dApplyOverTimeTarget ||
2755
- !Cesium.Cartesian3.equals(this.lastCalcPos3dApplyOverTimeTarget, targetPosition.pos3d)) {
2756
- // If we were already interpolating, get the current interpolated position as our new starting point.
2757
- if (this.lastCalcPosApplyOverTime && this.lastCalcPos3dApplyOverTimeTimeStart) {
2758
- // Calculate where we are in the current interpolation.
2759
- const currentInterpolatedPos = this.interpolatePos3dOverTime(this.lastCalcPos3dApplyOverTimeTimeStart, currentRealTimeMs);
2760
- // Use this as our new starting position.
2761
- this.lastCalcPos3d = currentInterpolatedPos;
2762
- }
2763
- // Set new target and start time.
2764
- this.lastCalcPos3dApplyOverTimeTarget = targetPosition.pos3d;
2765
- this.lastCalcPos3dApplyOverTimeTimeStart = currentRealTimeMs;
2766
- this.lastCalcPosApplyOverTime = true;
2767
- }
2768
- // Calculate the interpolated position.
2769
- let pos3d;
2770
- if (this.lastCalcPosApplyOverTime) {
2771
- pos3d = this.interpolatePos3dOverTime(this.lastCalcPos3dApplyOverTimeTimeStart, currentRealTimeMs);
2772
- // If we've reached the target, update our base position.
2773
- if (Cesium.Cartesian3.distance(pos3d, this.lastCalcPos3dApplyOverTimeTarget) < 0.01) {
2774
- this.lastCalcPos3d = this.lastCalcPos3dApplyOverTimeTarget;
2775
- this.lastCalcPosApplyOverTime = false;
2776
- }
2767
+ // The clock's time is the frame being drawn; the last render time is the frame before it.
2768
+ const nowTimeMs = Cesium.JulianDate.toDate(this.viewer.clock.currentTime).getTime();
2769
+ if (nowTimeMs === this.lastCalcTime && this.revision === this.lastCalcRevision) {
2770
+ return;
2777
2771
  }
2778
- else {
2779
- pos3d = this.lastCalcPos3d;
2780
- }
2781
- // Calculate heading.
2782
- if (this.lastCalcPos3dApplyOverTimeTarget) {
2783
- if (this.lastCalcPos3d) {
2784
- const minDistanceThreshold = 0.05;
2785
- const currentToTargetDistance = Cesium.Cartesian3.distance(this.lastCalcPos3d, this.lastCalcPos3dApplyOverTimeTarget);
2786
- if (currentToTargetDistance >= minDistanceThreshold) {
2787
- const lastPos = {
2788
- pos3d: this.lastCalcPos3d,
2789
- dateTime: this.lastCalcPos3dApplyOverTimeTimeStart ? new Date(this.lastCalcPos3dApplyOverTimeTimeStart) : null,
2790
- heading: null
2791
- };
2792
- const nextPos = {
2793
- pos3d: this.lastCalcPos3dApplyOverTimeTarget,
2794
- dateTime: null,
2795
- heading: null
2796
- };
2797
- if (lastPos && nextPos) {
2798
- this.lastCalcHeading = this.calculateHeading(nowTimeMs, lastPos, nextPos);
2799
- }
2800
- }
2801
- }
2772
+ this.lastCalcTime = nowTimeMs;
2773
+ this.lastCalcRevision = this.revision;
2774
+ const target = this.calculatePosition(nowTimeMs);
2775
+ const lastPos = this.positions[target.indexLast];
2776
+ const nextPos = this.positions[target.indexNext];
2777
+ if (lastPos && nextPos) {
2778
+ this.lastCalcHeading = (_a = this.calculateHeading(nowTimeMs, lastPos, nextPos)) !== null && _a !== void 0 ? _a : this.lastCalcHeading;
2802
2779
  }
2803
- else {
2804
- const posses = this.positions;
2805
- const lastPos = posses[targetPosition.indexLast];
2806
- const nextPos = posses[targetPosition.indexNext];
2807
- if (lastPos && nextPos) {
2808
- this.lastCalcHeading = this.calculateHeading(nowTimeMs, lastPos, nextPos);
2809
- }
2810
- }
2811
- // Provide the calculated values to the caller.
2812
- this.lastCalcAttitude = this.attitudeAt(targetPosition.indexLast);
2813
- this.onUpdate(pos3d, this.lastCalcHeading, this.lastCalcAttitude);
2814
- }
2815
- interpolatePos3dOverTime(startTimeMs, currentTimeMs) {
2816
- const lastPos = this.lastCalcPos3d;
2817
- const targetPos = this.lastCalcPos3dApplyOverTimeTarget;
2818
- if (!lastPos || !targetPos) {
2819
- return lastPos ? lastPos : targetPos;
2820
- }
2821
- const interpolationDuration = 200;
2822
- const elapsedTime = currentTimeMs - startTimeMs;
2823
- let progress = Math.min(elapsedTime / interpolationDuration, 1.0);
2824
- // Optional step to make it smoother.
2825
- progress = this.easeInOutQuad(progress);
2826
- return Cesium.Cartesian3.lerp(lastPos, targetPos, progress, new Cesium.Cartesian3());
2827
- }
2828
- easeInOutQuad(t) {
2829
- return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
2780
+ this.lastCalcPos3d = target.pos3d;
2781
+ this.lastCalcAttitude = this.attitudeBetween(target.indexLast, target.indexNext, nowTimeMs);
2782
+ this.onUpdate(target.pos3d, this.lastCalcHeading, this.lastCalcAttitude);
2830
2783
  }
2831
2784
  /**
2832
2785
  * Pre-process headings in the series.
@@ -2852,14 +2805,14 @@
2852
2805
  * Calculate the position at the given time.
2853
2806
  */
2854
2807
  calculatePosition(currentTimeMs) {
2808
+ var _a, _b;
2855
2809
  const posses = this.positions;
2856
2810
  // No positions to interpolate
2857
2811
  if (!posses || posses.length === 0) {
2858
2812
  return {
2859
2813
  pos3d: new Cesium.Cartesian3(),
2860
2814
  indexLast: -1,
2861
- indexNext: -1,
2862
- applyOverTime: false
2815
+ indexNext: -1
2863
2816
  };
2864
2817
  }
2865
2818
  // See if we're before the first position.
@@ -2867,8 +2820,7 @@
2867
2820
  return {
2868
2821
  pos3d: posses[0].pos3d,
2869
2822
  indexLast: 0,
2870
- indexNext: 0,
2871
- applyOverTime: true
2823
+ indexNext: 0
2872
2824
  };
2873
2825
  }
2874
2826
  // See if we're after the last position.
@@ -2877,8 +2829,7 @@
2877
2829
  return {
2878
2830
  pos3d: posses[lastIndex].pos3d,
2879
2831
  indexLast: lastIndex,
2880
- indexNext: lastIndex,
2881
- applyOverTime: true
2832
+ indexNext: lastIndex
2882
2833
  };
2883
2834
  }
2884
2835
  // Binary search to find the closest position.
@@ -2904,20 +2855,25 @@
2904
2855
  return {
2905
2856
  pos3d: last.pos3d,
2906
2857
  indexLast: lastIndex,
2907
- indexNext: lastIndex,
2908
- applyOverTime: true
2858
+ indexNext: lastIndex
2909
2859
  };
2910
2860
  }
2911
2861
  // Interpolate between the two closest positions.
2912
2862
  const lastTime = last.dateTime.getTime();
2913
2863
  const nextTime = next.dateTime.getTime();
2914
2864
  const progress = (currentTimeMs - lastTime) / (nextTime - lastTime);
2915
- const interpolatedPos = Cesium.Cartesian3.lerp(last.pos3d, next.pos3d, progress, new Cesium.Cartesian3());
2865
+ // A straight line between two points on a curved path cuts inside it, so it is lifted back out.
2866
+ const chord = Cesium.Cartesian3.lerp(last.pos3d, next.pos3d, progress, new Cesium.Cartesian3());
2867
+ const chordCarto = Cesium.Cartographic.fromCartesian(chord);
2868
+ const lastHeight = (_a = Cesium.Cartographic.fromCartesian(last.pos3d)) === null || _a === void 0 ? void 0 : _a.height;
2869
+ const nextHeight = (_b = Cesium.Cartographic.fromCartesian(next.pos3d)) === null || _b === void 0 ? void 0 : _b.height;
2870
+ const interpolatedPos = chordCarto && lastHeight != null && nextHeight != null
2871
+ ? Cesium.Cartesian3.fromRadians(chordCarto.longitude, chordCarto.latitude, Cesium.Math.lerp(lastHeight, nextHeight, progress))
2872
+ : chord;
2916
2873
  return {
2917
2874
  pos3d: interpolatedPos,
2918
2875
  indexLast: lastIndex,
2919
- indexNext: nextIndex,
2920
- applyOverTime: false
2876
+ indexNext: nextIndex
2921
2877
  };
2922
2878
  }
2923
2879
  /**
@@ -5481,6 +5437,481 @@
5481
5437
  return result;
5482
5438
  }
5483
5439
 
5440
+ (function (EntitySimulation) {
5441
+ const VIEWER_KEY = "_nextspaceSimulation";
5442
+ /**
5443
+ * The set used when a caller does not name one.
5444
+ */
5445
+ EntitySimulation.DEFAULT_SET = "default";
5446
+ // Record parts replaced whole rather than merged field by field, eg. a matrix or a geometry.
5447
+ const WHOLE_RECORD_PARTS = new Set(["geometry", "boundaries", "assemblyPosition"]);
5448
+ // Record parts that carry Entity data, in the order they are laid over an Entity.
5449
+ const RECORD_PARTS = ["location", "transform", "geometry", "boundaries", "assemblyPosition", "data"];
5450
+ /**
5451
+ * The store for a viewer, made on first use and shared by everything drawing into that scene.
5452
+ */
5453
+ function Get(viewer) {
5454
+ if (!viewer) {
5455
+ return null;
5456
+ }
5457
+ if (!viewer[VIEWER_KEY]) {
5458
+ viewer[VIEWER_KEY] = new Store();
5459
+ }
5460
+ return viewer[VIEWER_KEY];
5461
+ }
5462
+ EntitySimulation.Get = Get;
5463
+ function isPlainObject(value) {
5464
+ return value != null && typeof value === "object" && !Array.isArray(value);
5465
+ }
5466
+ // Entity values replaced whole by a patch, since half of one geometry over another describes neither.
5467
+ const WHOLE_ENTITY_PARTS = new Set(["VectorGeometry", "Boundaries", "AssemblyPosition"]);
5468
+ /**
5469
+ * Returns base with patch laid over it: objects merge key by key, anything else replaces.
5470
+ * @param whole names keys at the top level, or under Bruce, whose value is replaced rather than merged.
5471
+ */
5472
+ function Overlay(base, patch, whole = WHOLE_ENTITY_PARTS) {
5473
+ const lay = (into, over, depth, underBruce) => {
5474
+ if (over === undefined) {
5475
+ return into;
5476
+ }
5477
+ if (!isPlainObject(over) || !isPlainObject(into)) {
5478
+ return over;
5479
+ }
5480
+ const merged = { ...into };
5481
+ for (const key of Object.keys(over)) {
5482
+ const replace = (depth === 0 || underBruce) && whole.has(key);
5483
+ merged[key] = replace ? over[key] : lay(merged[key], over[key], depth + 1, depth === 0 && key === "Bruce");
5484
+ }
5485
+ return merged;
5486
+ };
5487
+ return lay(base, patch, 0, false);
5488
+ }
5489
+ EntitySimulation.Overlay = Overlay;
5490
+ /*
5491
+ * Lays one record's parts over another's, each part replaced whole where a part is one value.
5492
+ */
5493
+ function overlayRecord(base, over) {
5494
+ return Overlay(base, over, WHOLE_RECORD_PARTS);
5495
+ }
5496
+ /**
5497
+ * Each value a record names, as a path into the record. A part replaced whole is one value.
5498
+ */
5499
+ function recordPaths(record) {
5500
+ const paths = [];
5501
+ const walk = (value, path) => {
5502
+ if (isPlainObject(value) && !(path.length === 1 && WHOLE_RECORD_PARTS.has(path[0]))) {
5503
+ for (const key of Object.keys(value)) {
5504
+ walk(value[key], [...path, key]);
5505
+ }
5506
+ return;
5507
+ }
5508
+ if (value !== undefined) {
5509
+ paths.push(path);
5510
+ }
5511
+ };
5512
+ for (const part of RECORD_PARTS) {
5513
+ if ((record === null || record === void 0 ? void 0 : record[part]) != null) {
5514
+ walk(record[part], [part]);
5515
+ }
5516
+ }
5517
+ return paths;
5518
+ }
5519
+ /*
5520
+ * A copy of the record without the values at these paths, dropping objects left empty.
5521
+ */
5522
+ function withoutPaths(record, paths) {
5523
+ const strip = (value, path) => {
5524
+ if (!isPlainObject(value) || !(path[0] in value)) {
5525
+ return value;
5526
+ }
5527
+ const copy = { ...value };
5528
+ if (path.length === 1) {
5529
+ delete copy[path[0]];
5530
+ }
5531
+ else {
5532
+ const inner = strip(copy[path[0]], path.slice(1));
5533
+ if (isPlainObject(inner) && !Object.keys(inner).length) {
5534
+ delete copy[path[0]];
5535
+ }
5536
+ else {
5537
+ copy[path[0]] = inner;
5538
+ }
5539
+ }
5540
+ return copy;
5541
+ };
5542
+ let stripped = record;
5543
+ for (const path of paths) {
5544
+ stripped = strip(stripped, path);
5545
+ }
5546
+ return stripped;
5547
+ }
5548
+ /*
5549
+ * Whether a record still says anything about its Entity.
5550
+ */
5551
+ function hasContent(record) {
5552
+ return RECORD_PARTS.some(part => {
5553
+ const value = record === null || record === void 0 ? void 0 : record[part];
5554
+ return value != null && !(isPlainObject(value) && !Object.keys(value).length);
5555
+ });
5556
+ }
5557
+ function timeOf(record) {
5558
+ return (record === null || record === void 0 ? void 0 : record.dateTime) ? new Date(record.dateTime).getTime() : NaN;
5559
+ }
5560
+ class Store {
5561
+ constructor() {
5562
+ // Insertion order is precedence: a later set wins.
5563
+ this.sets = new Map();
5564
+ this.onChanged = new BModels.BruceEvent();
5565
+ }
5566
+ /**
5567
+ * Triggered after anything is simulated or cleared, naming the Entities affected.
5568
+ */
5569
+ get OnChanged() {
5570
+ return this.onChanged;
5571
+ }
5572
+ /**
5573
+ * Whether anything at all is simulated, so a caller can skip the work when nothing is.
5574
+ */
5575
+ get IsEmpty() {
5576
+ for (const set of this.sets.values()) {
5577
+ if (set.entities.size || set.patches.size || set.records.size) {
5578
+ return false;
5579
+ }
5580
+ }
5581
+ return true;
5582
+ }
5583
+ /**
5584
+ * The names of the sets holding anything, oldest first.
5585
+ */
5586
+ GetSets() {
5587
+ return Array.from(this.sets.keys());
5588
+ }
5589
+ /**
5590
+ * The time each set's records span, oldest set first, for sets holding any records.
5591
+ */
5592
+ GetSetRanges() {
5593
+ const ranges = [];
5594
+ for (const [name, set] of this.sets) {
5595
+ let startMs = Infinity;
5596
+ let endMs = -Infinity;
5597
+ for (const byTime of set.records.values()) {
5598
+ for (const ms of byTime.keys()) {
5599
+ startMs = Math.min(startMs, ms);
5600
+ endMs = Math.max(endMs, ms);
5601
+ }
5602
+ }
5603
+ if (set.records.size) {
5604
+ ranges.push({ set: name, startMs, endMs, entityIds: Array.from(set.records.keys()) });
5605
+ }
5606
+ }
5607
+ return ranges;
5608
+ }
5609
+ setFor(name) {
5610
+ const key = name || EntitySimulation.DEFAULT_SET;
5611
+ let set = this.sets.get(key);
5612
+ if (!set) {
5613
+ set = { entities: new Map(), patches: new Map(), records: new Map() };
5614
+ this.sets.set(key, set);
5615
+ }
5616
+ return set;
5617
+ }
5618
+ /**
5619
+ * Replaces the stand-in for each Entity given, within a set.
5620
+ */
5621
+ SetEntities(entities, set) {
5622
+ this.setBlobs(entities, set, false);
5623
+ }
5624
+ /**
5625
+ * Replaces the patch for each Entity given, within a set. A patch names only the data it changes.
5626
+ */
5627
+ SetPatches(patches, set) {
5628
+ this.setBlobs(patches, set, true);
5629
+ }
5630
+ setBlobs(blobs, set, patch) {
5631
+ var _a;
5632
+ const ids = [];
5633
+ for (const blob of blobs || []) {
5634
+ const id = (_a = blob === null || blob === void 0 ? void 0 : blob.Bruce) === null || _a === void 0 ? void 0 : _a.ID;
5635
+ if (!id) {
5636
+ continue;
5637
+ }
5638
+ const held = this.setFor(set);
5639
+ (patch ? held.patches : held.entities).set(id, blob);
5640
+ ids.push(id);
5641
+ }
5642
+ if (ids.length) {
5643
+ this.onChanged.Trigger({ entityIds: ids, entities: true, records: false, set: set || EntitySimulation.DEFAULT_SET });
5644
+ }
5645
+ }
5646
+ /**
5647
+ * Adds records to each Entity's simulated history within a set, replacing any at the same instant.
5648
+ */
5649
+ SetRecords(records, set) {
5650
+ const ids = new Set();
5651
+ for (const record of records || []) {
5652
+ const id = record === null || record === void 0 ? void 0 : record.entityId;
5653
+ const timeMs = timeOf(record);
5654
+ if (!id || isNaN(timeMs)) {
5655
+ continue;
5656
+ }
5657
+ const held = this.setFor(set);
5658
+ let byTime = held.records.get(id);
5659
+ if (!byTime) {
5660
+ byTime = new Map();
5661
+ held.records.set(id, byTime);
5662
+ }
5663
+ byTime.set(timeMs, record);
5664
+ ids.add(id);
5665
+ }
5666
+ if (ids.size) {
5667
+ this.onChanged.Trigger({ entityIds: Array.from(ids), entities: false, records: true, set: set || EntitySimulation.DEFAULT_SET });
5668
+ }
5669
+ }
5670
+ /**
5671
+ * Forgets what is simulated for these Entities, or for every Entity when none are named.
5672
+ * @param params.set the set to clear. By default every set.
5673
+ * @param params.entities clear the stand-ins and patches. Default true.
5674
+ * @param params.records clear the historic records. Default true.
5675
+ */
5676
+ Clear(params) {
5677
+ var _a, _b;
5678
+ const entities = (params === null || params === void 0 ? void 0 : params.entities) !== false;
5679
+ const records = (params === null || params === void 0 ? void 0 : params.records) !== false;
5680
+ const names = (params === null || params === void 0 ? void 0 : params.set) ? [params.set] : Array.from(this.sets.keys());
5681
+ const cleared = new Set();
5682
+ for (const name of names) {
5683
+ const set = this.sets.get(name);
5684
+ if (!set) {
5685
+ continue;
5686
+ }
5687
+ const ids = (_a = params === null || params === void 0 ? void 0 : params.entityIds) !== null && _a !== void 0 ? _a : [...set.entities.keys(), ...set.patches.keys(), ...set.records.keys()];
5688
+ for (const id of ids) {
5689
+ const hadEntity = entities && set.entities.delete(id);
5690
+ const hadPatch = entities && set.patches.delete(id);
5691
+ const hadRecords = records && set.records.delete(id);
5692
+ if (hadEntity || hadPatch || hadRecords) {
5693
+ cleared.add(id);
5694
+ }
5695
+ }
5696
+ if (!set.entities.size && !set.patches.size && !set.records.size) {
5697
+ this.sets.delete(name);
5698
+ }
5699
+ }
5700
+ if (cleared.size) {
5701
+ this.onChanged.Trigger({ entityIds: Array.from(cleared), entities, records, set: (_b = params === null || params === void 0 ? void 0 : params.set) !== null && _b !== void 0 ? _b : null });
5702
+ }
5703
+ }
5704
+ /**
5705
+ * The Entity's stand-in, with any patches laid over it, or null when nothing stands in for it.
5706
+ */
5707
+ GetEntity(entityId) {
5708
+ var _a;
5709
+ let standIn = null;
5710
+ for (const set of this.sets.values()) {
5711
+ standIn = (_a = set.entities.get(entityId)) !== null && _a !== void 0 ? _a : standIn;
5712
+ }
5713
+ return standIn ? this.Overlay(standIn) : null;
5714
+ }
5715
+ /**
5716
+ * Whether a stand-in replaces this Entity, so the API need not be asked for it.
5717
+ */
5718
+ HasEntity(entityId) {
5719
+ for (const set of this.sets.values()) {
5720
+ if (set.entities.has(entityId)) {
5721
+ return true;
5722
+ }
5723
+ }
5724
+ return false;
5725
+ }
5726
+ /**
5727
+ * Whether a stand-in or a patch changes this Entity.
5728
+ */
5729
+ HasBlob(entityId) {
5730
+ for (const set of this.sets.values()) {
5731
+ if (set.entities.has(entityId) || set.patches.has(entityId)) {
5732
+ return true;
5733
+ }
5734
+ }
5735
+ return false;
5736
+ }
5737
+ /**
5738
+ * Returns an Entity with its simulated patches laid over it. The input is never changed.
5739
+ */
5740
+ Overlay(entity) {
5741
+ var _a;
5742
+ const id = (_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.ID;
5743
+ if (!id) {
5744
+ return entity;
5745
+ }
5746
+ let result = entity;
5747
+ for (const set of this.sets.values()) {
5748
+ const patch = set.patches.get(id);
5749
+ if (patch) {
5750
+ result = Overlay(result, patch);
5751
+ }
5752
+ }
5753
+ return result;
5754
+ }
5755
+ /**
5756
+ * The Entity as simulated: its stand-in if there is one, otherwise what was read with patches laid over it.
5757
+ * @param entityId the Entity.
5758
+ * @param read what the API gave for it, if it was asked.
5759
+ */
5760
+ Resolve(entityId, read) {
5761
+ var _a;
5762
+ return (_a = this.GetEntity(entityId)) !== null && _a !== void 0 ? _a : (read ? this.Overlay(read) : null);
5763
+ }
5764
+ /**
5765
+ * Every Entity with a simulated stand-in or patch.
5766
+ */
5767
+ GetEntityIds() {
5768
+ const ids = new Set();
5769
+ for (const set of this.sets.values()) {
5770
+ set.entities.forEach((_, id) => ids.add(id));
5771
+ set.patches.forEach((_, id) => ids.add(id));
5772
+ }
5773
+ return Array.from(ids);
5774
+ }
5775
+ /**
5776
+ * Every Entity with simulated history.
5777
+ */
5778
+ GetRecordEntityIds() {
5779
+ const ids = new Set();
5780
+ for (const set of this.sets.values()) {
5781
+ set.records.forEach((_, id) => ids.add(id));
5782
+ }
5783
+ return Array.from(ids);
5784
+ }
5785
+ /**
5786
+ * An Entity's simulated history for a historic key, oldest first, every set's record at an instant merged.
5787
+ */
5788
+ GetRecords(entityId, attrKey) {
5789
+ const byTime = new Map();
5790
+ for (const set of this.sets.values()) {
5791
+ for (const [ms, record] of set.records.get(entityId) || []) {
5792
+ if (attrKey && record.attrKey && record.attrKey !== attrKey) {
5793
+ continue;
5794
+ }
5795
+ const held = byTime.get(ms);
5796
+ byTime.set(ms, held ? overlayRecord(held, record) : record);
5797
+ }
5798
+ }
5799
+ return Array.from(byTime.entries()).sort(([a], [b]) => a - b).map(([, record]) => record);
5800
+ }
5801
+ /**
5802
+ * Each value an Entity's simulated history names, with the instant from which it is the simulation's.
5803
+ */
5804
+ ownedFrom(records) {
5805
+ const owned = new Map();
5806
+ for (const record of records) {
5807
+ const ms = timeOf(record);
5808
+ for (const path of recordPaths(record)) {
5809
+ const key = path.join("\u0000");
5810
+ if (!owned.has(key)) {
5811
+ owned.set(key, { path, fromMs: ms });
5812
+ }
5813
+ }
5814
+ }
5815
+ return Array.from(owned.values());
5816
+ }
5817
+ /**
5818
+ * An Entity as it stands at an instant, with each value its simulated records have named by then laid over it.
5819
+ * @param entity the Entity as read for that instant, from the API or a simulated stand-in.
5820
+ * @param timeMs the instant in milliseconds.
5821
+ * @param attrKey the historic attribute key being followed, or null for any.
5822
+ */
5823
+ ApplyAt(entity, timeMs, attrKey) {
5824
+ var _a, _b, _c;
5825
+ const id = (_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.ID;
5826
+ if (!id || timeMs == null || isNaN(timeMs)) {
5827
+ return entity;
5828
+ }
5829
+ // Laid oldest first, each record replacing only what it names, which leaves every value at its newest.
5830
+ let state = null;
5831
+ let stateMs = -Infinity;
5832
+ for (const record of this.GetRecords(id, attrKey)) {
5833
+ const ms = timeOf(record);
5834
+ if (ms > timeMs) {
5835
+ break;
5836
+ }
5837
+ const parts = {};
5838
+ for (const part of RECORD_PARTS) {
5839
+ if (record[part] != null) {
5840
+ parts[part] = record[part];
5841
+ }
5842
+ }
5843
+ state = state ? overlayRecord(state, parts) : parts;
5844
+ stateMs = ms;
5845
+ }
5846
+ if (!state) {
5847
+ return entity;
5848
+ }
5849
+ const bruce = { ...entity.Bruce };
5850
+ if (state.location) {
5851
+ bruce.Location = Overlay(bruce.Location, state.location);
5852
+ }
5853
+ if (state.transform) {
5854
+ bruce.Transform = Overlay(bruce.Transform, state.transform);
5855
+ }
5856
+ if (state.geometry) {
5857
+ bruce.VectorGeometry = state.geometry;
5858
+ }
5859
+ if (state.boundaries) {
5860
+ bruce.Boundaries = state.boundaries;
5861
+ }
5862
+ if (state.assemblyPosition) {
5863
+ bruce.AssemblyPosition = state.assemblyPosition;
5864
+ }
5865
+ // Says the newest record the Entity now reflects, the way a read at a point does.
5866
+ const outline = entity.Bruce.Outline || [];
5867
+ const readAt = outline.find(x => !!x.DateTime);
5868
+ const readMs = readAt ? new Date(readAt.DateTime).getTime() : -Infinity;
5869
+ const dated = {
5870
+ ...(readAt || { "Source.Name": "Simulation", Editable: false, Kind: BModels.Entity.EOutlineKind.Attribute }),
5871
+ DateTime: new Date(Math.max(readMs, stateMs)).toISOString(),
5872
+ ForeignKey: (_b = readAt === null || readAt === void 0 ? void 0 : readAt.ForeignKey) !== null && _b !== void 0 ? _b : attrKey
5873
+ };
5874
+ bruce.Outline = [dated, ...outline.filter(x => x !== readAt)];
5875
+ const data = state.data ? Overlay({ ...entity, Bruce: undefined }, state.data) : entity;
5876
+ return { ...data, Bruce: Overlay(bruce, (_c = state.data) === null || _c === void 0 ? void 0 : _c.Bruce) };
5877
+ }
5878
+ /**
5879
+ * API records with the simulated ones joined in, for the named Entities or every simulated one.
5880
+ */
5881
+ MergeRecords(recordsByIds, attrKey, entityIds) {
5882
+ if (this.IsEmpty) {
5883
+ return recordsByIds;
5884
+ }
5885
+ const ids = entityIds !== null && entityIds !== void 0 ? entityIds : this.GetRecordEntityIds();
5886
+ let merged = null;
5887
+ for (const id of ids) {
5888
+ const simulated = this.GetRecords(id, attrKey);
5889
+ if (!simulated.length) {
5890
+ continue;
5891
+ }
5892
+ const owned = this.ownedFrom(simulated);
5893
+ const byTime = new Map();
5894
+ for (const record of (recordsByIds === null || recordsByIds === void 0 ? void 0 : recordsByIds[id]) || []) {
5895
+ const ms = timeOf(record);
5896
+ const kept = withoutPaths(record, owned.filter(x => x.fromMs <= ms).map(x => x.path));
5897
+ if (hasContent(kept)) {
5898
+ byTime.set(ms, kept);
5899
+ }
5900
+ }
5901
+ for (const record of simulated) {
5902
+ const ms = timeOf(record);
5903
+ const held = byTime.get(ms);
5904
+ byTime.set(ms, held ? overlayRecord(held, record) : record);
5905
+ }
5906
+ merged = merged !== null && merged !== void 0 ? merged : { ...(recordsByIds || {}) };
5907
+ merged[id] = Array.from(byTime.entries()).sort(([a], [b]) => a - b).map(([, record]) => record);
5908
+ }
5909
+ return merged !== null && merged !== void 0 ? merged : recordsByIds;
5910
+ }
5911
+ }
5912
+ EntitySimulation.Store = Store;
5913
+ })(exports.EntitySimulation || (exports.EntitySimulation = {}));
5914
+
5484
5915
  /**
5485
5916
  * Returns cesium property's value.
5486
5917
  * This will check if it's one that changes over time, or just a fixed value.
@@ -5532,6 +5963,8 @@
5532
5963
  const PHOTO_CACHE_KEY = "_nextspaceLabelPhotos";
5533
5964
  // Clearance around a model's box, as a share of its half-extent on each axis.
5534
5965
  const MODEL_BOX_PADDING = 0.02;
5966
+ // How far a moving model's box may drift before it is moved, in metres.
5967
+ const MODEL_BOX_FOLLOW_TOLERANCE = 0.001;
5535
5968
  // Share of each edge drawn out from its corners while the box is idle. Hovering draws the edges whole.
5536
5969
  const MODEL_BOX_BRACKET = 0.2;
5537
5970
  // Under this many pixels across, a box reads as a smudge and the ring takes over.
@@ -6748,6 +7181,7 @@
6748
7181
  constructor() {
6749
7182
  // Whether a box competes for space comes only from inputs (camera distance, hover, the pointer), never from
6750
7183
  // how its label was laid out, so an animation moving a label cannot feed back into which boxes show.
7184
+ // corners is where the box is drawn this frame: stored, moved by the entry's motion.
6751
7185
  this.entries = new Map();
6752
7186
  this.groups = null;
6753
7187
  // The hovered Entity. Its box is drawn on its own and whole, ahead of every other box.
@@ -6765,7 +7199,7 @@
6765
7199
  Set(id, corners, entityId, inputs) {
6766
7200
  const current = this.entries.get(id);
6767
7201
  // A label rebuilt over the same box keeps its place and how far it has animated.
6768
- if (corners && (current === null || current === void 0 ? void 0 : current.corners) === corners && current.entityId === entityId) {
7202
+ if (corners && (current === null || current === void 0 ? void 0 : current.stored) === corners && current.entityId === entityId) {
6769
7203
  Object.assign(current, inputs);
6770
7204
  return;
6771
7205
  }
@@ -6773,17 +7207,41 @@
6773
7207
  return;
6774
7208
  }
6775
7209
  if (corners) {
6776
- this.entries.set(id, { ...inputs, corners, entityId, active: false, wanted: false });
7210
+ this.entries.set(id, { ...inputs, corners, stored: corners, entityId, active: false, wanted: false });
6777
7211
  }
6778
7212
  else
6779
7213
  this.entries.delete(id);
6780
7214
  this.since.delete(id);
7215
+ this.forget(id);
7216
+ }
7217
+ /*
7218
+ * Drops the groups and outlines a box was in, so they are worked out again from where it is now.
7219
+ */
7220
+ forget(id) {
6781
7221
  this.groups = null;
6782
7222
  for (const [key, group] of this.outlines) {
6783
7223
  if (group.members.includes(id))
6784
7224
  this.outlines.delete(key);
6785
7225
  }
6786
7226
  }
7227
+ /*
7228
+ * Moves each box whose model moved since the last frame, keeping where it has got to in showing.
7229
+ */
7230
+ follow() {
7231
+ var _a;
7232
+ for (const [id, entry] of this.entries) {
7233
+ const motion = (_a = entry.motion) === null || _a === void 0 ? void 0 : _a.call(entry);
7234
+ if (!motion) {
7235
+ continue;
7236
+ }
7237
+ const moved = entry.stored.map(corner => Cesium.Matrix4.multiplyByPoint(motion, corner, new Cesium.Cartesian3()));
7238
+ if (moved.every((corner, i) => Cesium.Cartesian3.equalsEpsilon(corner, entry.corners[i], 0, MODEL_BOX_FOLLOW_TOLERANCE))) {
7239
+ continue;
7240
+ }
7241
+ entry.corners = moved;
7242
+ this.forget(id);
7243
+ }
7244
+ }
6787
7245
  /*
6788
7246
  * The box drawn for a label this frame, or undefined when it has none or lost its place.
6789
7247
  */
@@ -6851,6 +7309,7 @@
6851
7309
  */
6852
7310
  Resolve(viewer, pointer) {
6853
7311
  var _a, _b, _c, _d;
7312
+ this.follow();
6854
7313
  this.frames.clear();
6855
7314
  this.hulls.clear();
6856
7315
  this.drawn = [];
@@ -7273,9 +7732,16 @@
7273
7732
  }
7274
7733
  if (this.api) {
7275
7734
  const rego = this.rego;
7735
+ const simulation = exports.EntitySimulation.Get(this.viewer);
7736
+ let simulated = null;
7737
+ if (simulation === null || simulation === void 0 ? void 0 : simulation.HasBlob(rego.entityId)) {
7738
+ // A stand-in is named as is; a patch is laid over the API's record first.
7739
+ const read = simulation.HasEntity(rego.entityId) ? null : (await BModels.Entity.Get({ api: this.api, entityId: rego.entityId, migrated: true })).entity;
7740
+ simulated = simulation.Resolve(rego.entityId, read);
7741
+ }
7276
7742
  this.rego.name = await BModels.Entity.CalculateNameAsync({
7277
7743
  api: this.api,
7278
- entity: null,
7744
+ entity: simulated,
7279
7745
  entityId: rego.entityId,
7280
7746
  defaultToId: false
7281
7747
  });
@@ -7304,6 +7770,21 @@
7304
7770
  clearTimeout(this.loadTimeout);
7305
7771
  this.loadTimeout = null;
7306
7772
  }
7773
+ /*
7774
+ * Reads the label's Entity again and redraws it, eg. after what is simulated for it changed.
7775
+ */
7776
+ Reload() {
7777
+ if (this.disposed) {
7778
+ return;
7779
+ }
7780
+ this.additionalTexts = this.options.text != null ? [] : null;
7781
+ this.loadedName = false;
7782
+ // The name came from whatever record the visual was drawn from, so it is worked out again too.
7783
+ if (this.rego) {
7784
+ this.rego.name = null;
7785
+ }
7786
+ this.createLabel();
7787
+ }
7307
7788
  /*
7308
7789
  * Rebuilds only when configuration changes; state refreshes must not recreate DOM every frame.
7309
7790
  */
@@ -7534,7 +8015,7 @@
7534
8015
  }
7535
8016
  clearTimeout(this.loadTimeout);
7536
8017
  this.loadTimeout = setTimeout(async () => {
7537
- var _a, _b, _c, _d, _e;
8018
+ var _a, _b, _c, _d, _e, _f;
7538
8019
  if (this.disposed || this.counter != counter) {
7539
8020
  return;
7540
8021
  }
@@ -7561,16 +8042,19 @@
7561
8042
  const attributes = (_a = this.options.attributes) !== null && _a !== void 0 ? _a : (_c = (_b = entityType === null || entityType === void 0 ? void 0 : entityType.VisualSettings) === null || _b === void 0 ? void 0 : _b.label) === null || _c === void 0 ? void 0 : _c.attributes;
7562
8043
  // Read for its attributes, and for a photo when the label has no icon of its own.
7563
8044
  if (attributes || !HasIcon(this.options)) {
7564
- let entity = null;
8045
+ // A simulated stand-in is labelled as is, and a patch is laid over the API's.
8046
+ const simulation = exports.EntitySimulation.Get(this.viewer);
8047
+ let entity = (_d = simulation === null || simulation === void 0 ? void 0 : simulation.GetEntity(this.rego.entityId)) !== null && _d !== void 0 ? _d : null;
7565
8048
  try {
7566
- entity = (await BModels.Entity.Get({
8049
+ entity = entity !== null && entity !== void 0 ? entity : (await BModels.Entity.Get({
7567
8050
  api: this.api,
7568
8051
  entityId: this.rego.entityId,
7569
- scenario: ((_d = this.rego.outline) === null || _d === void 0 ? void 0 : _d.length) ? (_e = this.rego.outline.find(x => !!x.Scenario)) === null || _e === void 0 ? void 0 : _e.Scenario : null,
8052
+ scenario: ((_e = this.rego.outline) === null || _e === void 0 ? void 0 : _e.length) ? (_f = this.rego.outline.find(x => !!x.Scenario)) === null || _f === void 0 ? void 0 : _f.Scenario : null,
7570
8053
  schemaId: this.rego.schema,
7571
8054
  migrated: true,
7572
8055
  expandAttachments: !HasIcon(this.options)
7573
8056
  })).entity;
8057
+ entity = entity && simulation ? simulation.Resolve(this.rego.entityId, entity) : entity;
7574
8058
  }
7575
8059
  // Entity doesn't exist.
7576
8060
  // Likely a graphic with an ID that points to no record.
@@ -7954,11 +8438,17 @@
7954
8438
  // Past the wait a slow record no longer holds the label back; a box that comes later eases in.
7955
8439
  setTimeout(settle, MODEL_BOX_WAIT);
7956
8440
  this.modelBox.load.then(corners => {
8441
+ var _a, _b;
7957
8442
  if (!corners || this.disposed || this.counter !== counter) {
7958
8443
  return;
7959
8444
  }
7960
8445
  // A record that disagrees with where the model was found describes another placement, such as a moved tileset.
8446
+ // A part known to have moved from its record, eg. a joint turning, is judged where it has moved to.
8447
+ const motion = (_b = (_a = this.rego) === null || _a === void 0 ? void 0 : _a.motion) === null || _b === void 0 ? void 0 : _b.call(_a, this.viewer.clock.currentTime);
7961
8448
  const centre = Cesium.Cartesian3.midpoint(corners[0], corners[7], new Cesium.Cartesian3());
8449
+ if (motion) {
8450
+ Cesium.Matrix4.multiplyByPoint(motion, centre, centre);
8451
+ }
7962
8452
  if (Cesium.Cartesian3.distance(centre, found) > Cesium.Cartesian3.distance(corners[0], corners[7]) * 0.75 + 2) {
7963
8453
  return;
7964
8454
  }
@@ -7966,6 +8456,7 @@
7966
8456
  // Let go a little further out than it is taken, so a camera resting at the limit does not flip it.
7967
8457
  let near = false;
7968
8458
  layer.modelBoxes.Set(this.id, corners, rego.entityId, {
8459
+ motion: rego.motion ? () => rego.motion(this.viewer.clock.currentTime) : undefined,
7969
8460
  selected: () => { var _a, _b; return Boolean((_b = (_a = this.visualRegister) === null || _a === void 0 ? void 0 : _a.GetState({ entityId: rego.entityId, menuItemId: rego.menuItemId })) === null || _b === void 0 ? void 0 : _b.selected); },
7970
8461
  hovered: () => { var _a, _b; return Boolean((_b = (_a = this.visualRegister) === null || _a === void 0 ? void 0 : _a.IsLabelHovered) === null || _b === void 0 ? void 0 : _b.call(_a, rego.entityId, rego.menuItemId)); },
7971
8462
  near: () => {
@@ -8946,9 +9437,9 @@
8946
9437
  * Reads the entities behind items that name one without carrying it, attachments included,
8947
9438
  * so a label's default image needs no second request.
8948
9439
  */
8949
- async function Hydrate(items, api, signal) {
9440
+ async function Hydrate(items, api, signal, simulation) {
8950
9441
  var _a, _b;
8951
- if (!api) {
9442
+ if (!api && !simulation) {
8952
9443
  return items;
8953
9444
  }
8954
9445
  const hydrated = items.slice();
@@ -8959,12 +9450,15 @@
8959
9450
  }
8960
9451
  const chunk = missing.slice(start, start + HYDRATE_AT_ONCE);
8961
9452
  try {
8962
- const { entities } = await BModels.Entity.GetListByIds({
8963
- api, entityIds: chunk.map(at => hydrated[at].entityId), expandAttachments: true, migrated: true
8964
- });
9453
+ // A simulated stand-in is labelled as is, never from the API, and a patch is laid over the API's.
9454
+ const asked = chunk.filter(at => !(simulation === null || simulation === void 0 ? void 0 : simulation.HasEntity(hydrated[at].entityId)));
9455
+ const { entities } = api && asked.length ? await BModels.Entity.GetListByIds({
9456
+ api, entityIds: asked.map(at => hydrated[at].entityId), expandAttachments: true, migrated: true
9457
+ }) : { entities: [] };
8965
9458
  const found = new Map((entities !== null && entities !== void 0 ? entities : []).map(entity => { var _a; return [(_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.ID, entity]; }));
8966
9459
  for (const at of chunk) {
8967
- const entity = found.get(hydrated[at].entityId);
9460
+ const id = hydrated[at].entityId;
9461
+ const entity = simulation ? simulation.Resolve(id, found.get(id)) : found.get(id);
8968
9462
  if (entity) {
8969
9463
  // The API leaves Attachment out when there are none; an empty list records that they were read.
8970
9464
  hydrated[at] = { ...hydrated[at], entity: { ...entity, Bruce: { ...entity.Bruce, Attachment: (_b = (_a = entity.Bruce) === null || _a === void 0 ? void 0 : _a.Attachment) !== null && _b !== void 0 ? _b : [] } } };
@@ -8981,7 +9475,7 @@
8981
9475
  /*
8982
9476
  * Uses the existing DataLab grammar, discarding actions and projections that cannot return Entities.
8983
9477
  */
8984
- async function Loader(source, api, signal, hydrate) {
9478
+ async function Loader(source, api, signal, hydrate, simulation) {
8985
9479
  var _a;
8986
9480
  var _b;
8987
9481
  if (source.type === "provider") {
@@ -8996,7 +9490,7 @@
8996
9490
  return async (offset, limit, signal) => {
8997
9491
  const page = items.slice(offset, offset + limit);
8998
9492
  return {
8999
- items: hydrate ? await Hydrate(page, api, signal) : page,
9493
+ items: hydrate ? await Hydrate(page, api, signal, simulation) : page,
9000
9494
  hasMore: offset + limit < items.length
9001
9495
  };
9002
9496
  };
@@ -9138,7 +9632,7 @@
9138
9632
  const items = new Map();
9139
9633
  let truncated = false;
9140
9634
  try {
9141
- const load = await Loader(definition.source, (_g = this.register.ApiGetters) === null || _g === void 0 ? void 0 : _g.GetBruceApi(), controller.signal, NeedsEntityData(definition.options));
9635
+ const load = await Loader(definition.source, (_g = this.register.ApiGetters) === null || _g === void 0 ? void 0 : _g.GetBruceApi(), controller.signal, NeedsEntityData(definition.options), exports.EntitySimulation.Get(this.register.Viewer));
9142
9636
  let offset = 0;
9143
9637
  for (let pageNumber = 0;; pageNumber++) {
9144
9638
  if (controller.signal.aborted || this.register.Disposed) {
@@ -9960,8 +10454,25 @@
9960
10454
  return `${relation["Related.Entity.ID"]}-${relation["Principal.Entity.ID"]}-${relation["Relation.Type.ID"]}`;
9961
10455
  }
9962
10456
  RelationRenderEngine.GetRenderGroupId = GetRenderGroupId;
10457
+ /**
10458
+ * Removes and returns a menu item's drawn relations that end at, or carry data from, any of these Entities.
10459
+ */
10460
+ function TakeRelationsTouching(params) {
10461
+ const { visualRegister, menuItemId } = params;
10462
+ const ids = new Set(params.entityIds);
10463
+ const touched = visualRegister.GetRegos({ menuItemId }).filter((rego) => {
10464
+ const relation = rego.relation;
10465
+ return relation && [relation["Principal.Entity.ID"], relation["Related.Entity.ID"], relation["Data.Entity.ID"]]
10466
+ .some(id => id && ids.has(id));
10467
+ });
10468
+ for (const rego of touched) {
10469
+ visualRegister.RemoveRegos({ relation: rego.relation, entityId: rego.entityId, menuItemId, requestRender: false });
10470
+ }
10471
+ return touched.map(rego => rego.relation);
10472
+ }
10473
+ RelationRenderEngine.TakeRelationsTouching = TakeRelationsTouching;
9963
10474
  async function Render(params) {
9964
- const { apiGetter, viewer, visualRegister, menuItemId, relations } = params;
10475
+ const { apiGetter, viewer, visualRegister, menuItemId, relations, simulation } = params;
9965
10476
  const api = apiGetter.getApi(apiGetter.accountId);
9966
10477
  const rendered = {};
9967
10478
  let neededEntityIdsIds = [];
@@ -9979,13 +10490,15 @@
9979
10490
  }
9980
10491
  // Turn the set unique.
9981
10492
  neededEntityIdsIds = neededEntityIdsIds.filter((v, i, a) => a.indexOf(v) === i);
9982
- // Get all entities in one go.
9983
- let entities = (await BModels.Entity.GetListByIds({
9984
- entityIds: neededEntityIdsIds,
10493
+ // Get all entities in one go, other than the simulated ones.
10494
+ const simulated = neededEntityIdsIds.map(id => simulation === null || simulation === void 0 ? void 0 : simulation.GetEntity(id)).filter(x => !!x);
10495
+ const apiIds = neededEntityIdsIds.filter(id => !(simulation === null || simulation === void 0 ? void 0 : simulation.HasEntity(id)));
10496
+ let entities = (apiIds.length ? (await BModels.Entity.GetListByIds({
10497
+ entityIds: apiIds,
9985
10498
  api: apiGetter.getApi(),
9986
10499
  migrated: true,
9987
10500
  maxSearchTimeSec: 60 * 2
9988
- })).entities;
10501
+ })).entities.map(entity => simulation ? simulation.Overlay(entity) : entity) : []).concat(simulated);
9989
10502
  // Create a map for quick reference.
9990
10503
  const entitiesMap = {};
9991
10504
  for (let i = 0; i < entities.length; i++) {
@@ -10479,6 +10992,8 @@
10479
10992
  rego.overrideShow != null ||
10480
10993
  // Being dragged around right now, so its extent is changing faster than we could measure it.
10481
10994
  rego.editing == true ||
10995
+ // Placed by the clock every frame, so no measurement of where it is stays true.
10996
+ rego.moving == true ||
10482
10997
  // Part of a collection we did not create, eg. a geojson data source, which may have its own rules for
10483
10998
  // what is in the scene. Our own per-menu-item data sources are fair game.
10484
10999
  exports.VisualsRegister.IsForeignCollection(rego) ||
@@ -11313,6 +11828,7 @@
11313
11828
  }
11314
11829
  get OnLabelGroupsChanged() { return this.labelGroupsChanged; }
11315
11830
  constructor(params) {
11831
+ var _a, _b;
11316
11832
  this.id = BModels.ObjectUtils.UId();
11317
11833
  this.disposed = false;
11318
11834
  this.rego = {};
@@ -11352,6 +11868,8 @@
11352
11868
  this.menuItemSources = new Map();
11353
11869
  // Set when the data source order no longer matches the zIndexes seen, so it is re-applied once.
11354
11870
  this.sourceOrderDirty = false;
11871
+ // Stops following what is simulated, so labels can read it again when it changes.
11872
+ this.simulationRemoval = null;
11355
11873
  this.labelGroupsChanged = new BModels.BruceEvent();
11356
11874
  this.hoverRingState = {};
11357
11875
  this.retiringHoverLabels = new Map();
@@ -11386,6 +11904,33 @@
11386
11904
  this.cameraCullerDispose = exports.VisualRegisterCuller.Monitor({
11387
11905
  register: this
11388
11906
  });
11907
+ this.simulationRemoval = (_b = (_a = exports.EntitySimulation.Get(this.viewer)) === null || _a === void 0 ? void 0 : _a.OnChanged.Subscribe((change) => this.reloadLabels(change.entityIds))) !== null && _b !== void 0 ? _b : null;
11908
+ }
11909
+ /*
11910
+ * Tells labels to read their Entity again, since they keep what they read when drawn.
11911
+ */
11912
+ reloadLabels(entityIds) {
11913
+ var _a, _b, _c, _d;
11914
+ if (this.disposed) {
11915
+ return;
11916
+ }
11917
+ const groupIds = new Set();
11918
+ for (const entityId of entityIds) {
11919
+ for (const rego of this.GetRegos({ entityId })) {
11920
+ (_a = exports.EntityLabel.GetLabel({ rego })) === null || _a === void 0 ? void 0 : _a.Reload();
11921
+ }
11922
+ if (((_b = this.labelHover) === null || _b === void 0 ? void 0 : _b.entityId) === entityId) {
11923
+ (_c = this.hoverLabel) === null || _c === void 0 ? void 0 : _c.Reload();
11924
+ }
11925
+ const groupId = (_d = this.GetLabelGroupFor(entityId)) === null || _d === void 0 ? void 0 : _d.groupId;
11926
+ if (groupId) {
11927
+ groupIds.add(groupId);
11928
+ }
11929
+ }
11930
+ // A group may have read the Entity in with its items, so the group is read again as a whole.
11931
+ for (const groupId of groupIds) {
11932
+ this.RefreshLabelGroup(groupId).catch((e) => console.error(e));
11933
+ }
11389
11934
  }
11390
11935
  /**
11391
11936
  * Returns the collection a menu item's visuals belong in, creating its data source on first use.
@@ -12288,7 +12833,9 @@
12288
12833
  (_b = this.viewer.scene) === null || _b === void 0 ? void 0 : _b.requestRender();
12289
12834
  }
12290
12835
  Dispose() {
12291
- var _a, _b, _c, _d, _e;
12836
+ var _a, _b, _c, _d, _e, _f;
12837
+ (_a = this.simulationRemoval) === null || _a === void 0 ? void 0 : _a.call(this);
12838
+ this.simulationRemoval = null;
12292
12839
  exports.CesiumEntityStyler.SetDimming(this.viewer, this.id, false);
12293
12840
  this.ClearLabelHover();
12294
12841
  clearTimeout(this.hoverRingReset);
@@ -12298,14 +12845,14 @@
12298
12845
  exports.EntityLabel.Detatch({ label });
12299
12846
  }
12300
12847
  this.retiringHoverLabels.clear();
12301
- (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.Dispose();
12848
+ (_b = this.labelGroups) === null || _b === void 0 ? void 0 : _b.Dispose();
12302
12849
  this.labelGroupsChanged.Clear();
12303
12850
  for (const regos of Object.values(this.rego)) {
12304
12851
  for (const rego of regos)
12305
12852
  exports.EntityLabel.Detatch({ rego, force: true });
12306
12853
  }
12307
12854
  exports.EntityLabel.SetContainer(this.viewer);
12308
- (_b = this.cameraCullerDispose) === null || _b === void 0 ? void 0 : _b.call(this, {
12855
+ (_c = this.cameraCullerDispose) === null || _c === void 0 ? void 0 : _c.call(this, {
12309
12856
  uncull: false
12310
12857
  });
12311
12858
  this.cameraCullerDispose = null;
@@ -12321,7 +12868,7 @@
12321
12868
  this.disposed = true;
12322
12869
  for (const record of Array.from(this.menuItemSources.values())) {
12323
12870
  try {
12324
- (_d = (_c = this.viewer) === null || _c === void 0 ? void 0 : _c.dataSources) === null || _d === void 0 ? void 0 : _d.remove(record.source, true);
12871
+ (_e = (_d = this.viewer) === null || _d === void 0 ? void 0 : _d.dataSources) === null || _e === void 0 ? void 0 : _e.remove(record.source, true);
12325
12872
  }
12326
12873
  catch (e) {
12327
12874
  console.error(e);
@@ -12330,7 +12877,7 @@
12330
12877
  this.menuItemSources.clear();
12331
12878
  // Hand every tileset's show flag back to its owner, holding whatever the owner last asked for.
12332
12879
  for (const rego of Array.from(this.tilesets.values())) {
12333
- (_e = rego.gate) === null || _e === void 0 ? void 0 : _e.Release();
12880
+ (_f = rego.gate) === null || _f === void 0 ? void 0 : _f.Release();
12334
12881
  }
12335
12882
  this.tilesets.clear();
12336
12883
  }
@@ -12504,21 +13051,7 @@
12504
13051
  changed = true;
12505
13052
  }
12506
13053
  if (editing) {
12507
- const parts = exports.EntityUtils.GatherEntity({
12508
- entity: rego.visual
12509
- });
12510
- for (let k = 0; k < parts.length; k++) {
12511
- const part = parts[k];
12512
- if (!(part instanceof Cesium.Entity)) {
12513
- continue;
12514
- }
12515
- const removalPending = exports.VisualRegisterCuller.CancelQueuedSceneChange(part);
12516
- const wasCulled = part[exports.VisualRegisterCuller.VISUAL_CULL_KEY] == true || removalPending;
12517
- delete part[exports.VisualRegisterCuller.VISUAL_CULL_KEY];
12518
- if (wasCulled && !IsCEntityInScene(this.viewer, part)) {
12519
- AddCEntityToScene(this.viewer, part, rego.collection);
12520
- }
12521
- }
13054
+ this.restoreCulled(rego);
12522
13055
  }
12523
13056
  else {
12524
13057
  exports.VisualRegisterCuller.InvalidateBounds(rego.visual);
@@ -12530,6 +13063,56 @@
12530
13063
  (_b = (_a = this.viewer) === null || _a === void 0 ? void 0 : _a.scene) === null || _b === void 0 ? void 0 : _b.requestRender();
12531
13064
  }
12532
13065
  }
13066
+ /**
13067
+ * Marks visuals as placed by the clock, exempting them from culling until it is turned back off.
13068
+ */
13069
+ SetMoving(params) {
13070
+ const { entityIds, moving, menuItemId } = params;
13071
+ if (!(entityIds === null || entityIds === void 0 ? void 0 : entityIds.length)) {
13072
+ return;
13073
+ }
13074
+ for (let i = 0; i < entityIds.length; i++) {
13075
+ const regos = this.rego[entityIds[i]];
13076
+ if (!regos) {
13077
+ continue;
13078
+ }
13079
+ for (let j = 0; j < regos.length; j++) {
13080
+ const rego = regos[j];
13081
+ if ((menuItemId && rego.menuItemId !== menuItemId) || Boolean(rego.moving) == Boolean(moving)) {
13082
+ continue;
13083
+ }
13084
+ // Null rather than false, so the rego does not carry a key for every visual that ever moved.
13085
+ rego.moving = moving ? true : null;
13086
+ if (moving) {
13087
+ this.restoreCulled(rego);
13088
+ }
13089
+ else {
13090
+ exports.VisualRegisterCuller.InvalidateBounds(rego.visual);
13091
+ exports.VisualRegisterCuller.MarkShouldRecheck(this.viewer);
13092
+ }
13093
+ }
13094
+ }
13095
+ }
13096
+ /*
13097
+ * Puts back whatever the culler took out of a visual that is now exempt from it.
13098
+ */
13099
+ restoreCulled(rego) {
13100
+ const parts = exports.EntityUtils.GatherEntity({
13101
+ entity: rego.visual
13102
+ });
13103
+ for (let k = 0; k < parts.length; k++) {
13104
+ const part = parts[k];
13105
+ if (!(part instanceof Cesium.Entity)) {
13106
+ continue;
13107
+ }
13108
+ const removalPending = exports.VisualRegisterCuller.CancelQueuedSceneChange(part);
13109
+ const wasCulled = part[exports.VisualRegisterCuller.VISUAL_CULL_KEY] == true || removalPending;
13110
+ delete part[exports.VisualRegisterCuller.VISUAL_CULL_KEY];
13111
+ if (wasCulled && !IsCEntityInScene(this.viewer, part)) {
13112
+ AddCEntityToScene(this.viewer, part, rego.collection);
13113
+ }
13114
+ }
13115
+ }
12533
13116
  SetSelected(params) {
12534
13117
  var _a;
12535
13118
  let { entityIds, selected, refreshIfSelected, requestRender, menuItemId, source } = params;
@@ -14022,12 +14605,41 @@
14022
14605
  * Tracks per-entity rerender state for unsaved blob maintenance and stale API response suppression.
14023
14606
  */
14024
14607
  class EntityReRenderMaintainState {
14025
- constructor() {
14608
+ constructor(simulation, pointInTime) {
14609
+ this.simulation = simulation;
14610
+ this.pointInTime = pointInTime;
14026
14611
  this.maintainedEntitiesById = {};
14027
14612
  this.apiRevisionByEntityId = {};
14028
14613
  this.skipApiRunByEntityId = {};
14029
14614
  this.checkRunId = 0;
14030
14615
  }
14616
+ /*
14617
+ * Lays each Entity's simulated record for the current point over it, when the manager reads at one.
14618
+ */
14619
+ atPoint(entities) {
14620
+ var _a;
14621
+ const point = (_a = this.pointInTime) === null || _a === void 0 ? void 0 : _a.call(this);
14622
+ if (!point || !this.simulation || this.simulation.IsEmpty || !(entities === null || entities === void 0 ? void 0 : entities.length)) {
14623
+ return entities;
14624
+ }
14625
+ return entities.map((entity) => this.simulation.ApplyAt(entity, point.timeMs, point.attrKey));
14626
+ }
14627
+ /**
14628
+ * Whether a simulated stand-in replaces this Entity, so nothing from the API may replace what is drawn.
14629
+ */
14630
+ isSimulated(entityId) {
14631
+ var _a;
14632
+ return Boolean(entityId && ((_a = this.simulation) === null || _a === void 0 ? void 0 : _a.HasEntity(entityId)));
14633
+ }
14634
+ /**
14635
+ * Swaps any simulated Entity in a set the API returned for its stand-in, or lays its patch over it.
14636
+ */
14637
+ withSimulated(entities) {
14638
+ if (!this.simulation || this.simulation.IsEmpty || !(entities === null || entities === void 0 ? void 0 : entities.length)) {
14639
+ return entities;
14640
+ }
14641
+ return this.atPoint(entities.map((entity) => { var _a, _b; return (_b = this.simulation.Resolve((_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.ID, entity)) !== null && _b !== void 0 ? _b : entity; }));
14642
+ }
14031
14643
  setMaintainedEntities(entities) {
14032
14644
  var _a;
14033
14645
  for (let i = 0; i < entities.length; i++) {
@@ -14049,15 +14661,21 @@
14049
14661
  }
14050
14662
  }
14051
14663
  getMaintainedEntities(entityIds) {
14664
+ var _a;
14052
14665
  const entities = [];
14053
14666
  for (let i = 0; i < entityIds.length; i++) {
14054
14667
  const id = entityIds[i];
14055
14668
  if (!id) {
14056
14669
  continue;
14057
14670
  }
14058
- const entity = this.maintainedEntitiesById[id];
14059
- if (entity) {
14060
- entities.push(entity);
14671
+ // A maintained blob is this manager's own unsaved edit, newer than a simulation, so it is drawn as is.
14672
+ const maintained = this.maintainedEntitiesById[id];
14673
+ const simulated = maintained ? null : (_a = this.simulation) === null || _a === void 0 ? void 0 : _a.GetEntity(id);
14674
+ if (maintained) {
14675
+ entities.push(maintained);
14676
+ }
14677
+ else if (simulated) {
14678
+ entities.push(this.atPoint([simulated])[0]);
14061
14679
  }
14062
14680
  }
14063
14681
  return entities;
@@ -14114,7 +14732,7 @@
14114
14732
  }
14115
14733
  filterCurrentApiEntities(params) {
14116
14734
  const { entities, revisions, runId } = params;
14117
- return entities.filter((entity) => {
14735
+ const current = entities.filter((entity) => {
14118
14736
  var _a, _b, _c;
14119
14737
  const id = (_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.ID;
14120
14738
  if (!id) {
@@ -14123,8 +14741,12 @@
14123
14741
  if (runId != null && this.shouldSkipInRun(id, runId)) {
14124
14742
  return false;
14125
14743
  }
14744
+ if (this.isSimulated(id)) {
14745
+ return false;
14746
+ }
14126
14747
  return ((_b = this.apiRevisionByEntityId[id]) !== null && _b !== void 0 ? _b : 0) === ((_c = revisions[id]) !== null && _c !== void 0 ? _c : 0);
14127
14748
  });
14749
+ return this.atPoint(this.simulation && !this.simulation.IsEmpty ? current.map((entity) => this.simulation.Overlay(entity)) : current);
14128
14750
  }
14129
14751
  }
14130
14752
 
@@ -15560,7 +16182,7 @@
15560
16182
  this.cache = new HistoricRangeCache();
15561
16183
  }
15562
16184
  get Records() {
15563
- return this.cache.GetRecords();
16185
+ return this.withSimulated(this.cache.GetRecords());
15564
16186
  }
15565
16187
  Clear() {
15566
16188
  this.cache.Clear();
@@ -15584,8 +16206,9 @@
15584
16206
  */
15585
16207
  async Gather(entities) {
15586
16208
  var _a;
15587
- if (this.disposed || !(entities === null || entities === void 0 ? void 0 : entities.length)) {
15588
- return { fetched: false, recordsByIds: this.cache.GetRecords() };
16209
+ const entityIds = entities === null || entities === void 0 ? void 0 : entities.map(x => { var _a; return (_a = x.Bruce) === null || _a === void 0 ? void 0 : _a.ID; }).filter(x => !!x);
16210
+ if (this.disposed || !(entities === null || entities === void 0 ? void 0 : entities.length) || this.params.simulatedOnly) {
16211
+ return { fetched: false, recordsByIds: this.withSimulated(this.cache.GetRecords(), entityIds) };
15589
16212
  }
15590
16213
  const { viewer } = this.params;
15591
16214
  const windowStart = new Date(viewer.clock.startTime.toString()).getTime();
@@ -15605,7 +16228,7 @@
15605
16228
  if (plan.coarse && !this.cache.CoversCoarse(plan.coarse.start, plan.coarse.stop)) {
15606
16229
  const records = await this.request(plan.coarse.start, plan.coarse.stop, entities, plan.coarse.sample);
15607
16230
  if (this.disposed) {
15608
- return { fetched, recordsByIds: this.cache.GetRecords() };
16231
+ return { fetched, recordsByIds: this.withSimulated(this.cache.GetRecords(), entityIds) };
15609
16232
  }
15610
16233
  this.cache.Absorb(plan.coarse.start, this.holdTo(plan.coarse.stop, liveEdge, liveTail), records, true);
15611
16234
  fetched = true;
@@ -15640,7 +16263,17 @@
15640
16263
  }
15641
16264
  }
15642
16265
  }
15643
- return { fetched, recordsByIds: this.cache.GetRecords() };
16266
+ return { fetched, recordsByIds: this.withSimulated(this.cache.GetRecords(), entityIds) };
16267
+ }
16268
+ /*
16269
+ * Joins the simulated records for these Entities, or every simulated Entity, into an answer.
16270
+ */
16271
+ withSimulated(recordsByIds, entityIds) {
16272
+ const simulation = this.params.simulation;
16273
+ if (!simulation || simulation.IsEmpty) {
16274
+ return recordsByIds;
16275
+ }
16276
+ return simulation.MergeRecords(recordsByIds, this.params.getAttrKey(), entityIds);
15644
16277
  }
15645
16278
  /*
15646
16279
  * Returns how much of a range that was just read may be marked as held.
@@ -15675,7 +16308,7 @@
15675
16308
  }).then((res) => {
15676
16309
  const records = res.recordsByIds || {};
15677
16310
  if (!this.disposed && this.params.onRecords) {
15678
- this.params.onRecords(records);
16311
+ this.params.onRecords(this.withSimulated(records, entityIds));
15679
16312
  }
15680
16313
  return records;
15681
16314
  }).finally(() => {
@@ -15747,7 +16380,6 @@
15747
16380
  this.renderQueue = [];
15748
16381
  this.renderQueueInterval = null;
15749
16382
  this.sources = [];
15750
- this.reRenderState = new EntityReRenderMaintainState();
15751
16383
  // Highly experimental flag to try improve rendering large sets of polygons and polylines.
15752
16384
  // Many things are not supported when this is enabled.
15753
16385
  this.useGeojson = false;
@@ -15755,6 +16387,10 @@
15755
16387
  this.zoomControl = [];
15756
16388
  // Scenario for retrieving Entities.
15757
16389
  this.scenario = null;
16390
+ // Hand-off key of the data currently drawn, or null when it is unknown or nothing is drawn.
16391
+ this.drawnKey = null;
16392
+ // How the drawn entities were last told to look, see getDrawnSettingsSignature.
16393
+ this.drawnSettings = null;
15758
16394
  // Type IDs that we'll allow to be rendered when receiving Entities.
15759
16395
  // This might be exactly the Menu Item Type ID because of local data sources.
15760
16396
  this.allowedTypeIDs = [];
@@ -15767,6 +16403,14 @@
15767
16403
  */
15768
16404
  this.renderAsIndividualsChain = Promise.resolve();
15769
16405
  const { viewer, apiGetter, monitor, item, register: visualsManager, sharedGetters, onSeriesDiscovered } = params;
16406
+ this.reRenderState = new EntityReRenderMaintainState(params.simulation, () => {
16407
+ // Read at the scene's time only when the item follows history, the same choice its reads make.
16408
+ const bruceEntity = this.item.BruceEntity;
16409
+ if (!(bruceEntity === null || bruceEntity === void 0 ? void 0 : bruceEntity.historic) && !(bruceEntity === null || bruceEntity === void 0 ? void 0 : bruceEntity.historicAttrKey)) {
16410
+ return null;
16411
+ }
16412
+ return { timeMs: Cesium.JulianDate.toDate(this.viewer.clock.currentTime).getTime(), attrKey: bruceEntity.historicAttrKey };
16413
+ });
15770
16414
  this.viewer = viewer;
15771
16415
  this.sharedGetters = sharedGetters;
15772
16416
  this.monitor = monitor;
@@ -15780,7 +16424,8 @@
15780
16424
  getApi: () => this.apiGetter.getApi(),
15781
16425
  getAttrKey: () => { var _a; return (_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historicAttrKey; },
15782
16426
  // A second either side, to take in records sitting exactly on the boundary.
15783
- paddingMs: 1000
16427
+ paddingMs: 1000,
16428
+ simulation: params.simulation
15784
16429
  });
15785
16430
  this.useGeojson = item.renderAsGeojson == true;
15786
16431
  if (item.enableClustering) {
@@ -15804,9 +16449,26 @@
15804
16449
  if (this.disposed) {
15805
16450
  return;
15806
16451
  }
16452
+ // Landing on the data already drawn re-applies settings instead of removing and re-fetching it.
16453
+ const nextKey = shouldRender ? Manager.GetHandoffKey(this.item) : null;
16454
+ const keepDrawn = nextKey != null && nextKey === this.drawnKey;
16455
+ const restyle = keepDrawn && this.drawnSettings !== this.getDrawnSettingsSignature();
16456
+ this.drawnKey = nextKey;
16457
+ this.drawnSettings = this.getDrawnSettingsSignature();
15807
16458
  this.unsetGetter();
15808
16459
  (_a = this.viewMonitorRemoval) === null || _a === void 0 ? void 0 : _a.call(this);
15809
16460
  this.viewMonitorRemoval = null;
16461
+ if (keepDrawn) {
16462
+ this.setGetter();
16463
+ this.viewMonitorRemoval = this.monitor.Updated().Subscribe(() => {
16464
+ var _a;
16465
+ (_a = this.entityCheckQueue) === null || _a === void 0 ? void 0 : _a.Call();
16466
+ });
16467
+ if (restyle) {
16468
+ this.ReRender({ entityIds: Object.keys(this.renderedEntities), force: true });
16469
+ }
16470
+ return;
16471
+ }
15810
16472
  clearInterval(this.renderQueueInterval);
15811
16473
  this.renderQueueInterval = null;
15812
16474
  this.renderQueue = [];
@@ -15840,6 +16502,18 @@
15840
16502
  }
15841
16503
  })();
15842
16504
  }
16505
+ /*
16506
+ * Everything about how drawn entities look that applying the item's settings can change.
16507
+ */
16508
+ getDrawnSettingsSignature() {
16509
+ var _a;
16510
+ return JSON.stringify({
16511
+ zoomControl: exports.RenderManager.GetZoomControlSignature(this.zoomControl),
16512
+ renderPriority: (_a = this.renderPriority) !== null && _a !== void 0 ? _a : null,
16513
+ historicDrawTrack: Boolean(this.item.historicDrawTrack),
16514
+ historicInterpolation: Boolean(this.item.historicInterpolation)
16515
+ });
16516
+ }
15843
16517
  applyItemSettings() {
15844
16518
  var _a, _b, _c, _d, _e, _f;
15845
16519
  // Reset allowed list if there's change.
@@ -15961,8 +16635,9 @@
15961
16635
  return;
15962
16636
  }
15963
16637
  this.updateAllowedTypeIDs(data.entityTypeSources, data.entities);
16638
+ // The getter answers from the API, so a simulated Entity in its answer is swapped for its blob.
15964
16639
  if (isTagItem) {
15965
- this.distributeForRender(data.entities.filter((entity) => {
16640
+ this.distributeForRender(this.reRenderState.withSimulated(data.entities.filter((entity) => {
15966
16641
  let entityTags = entity.Bruce["Layer.ID"];
15967
16642
  if (!entityTags) {
15968
16643
  entityTags = [];
@@ -15970,10 +16645,10 @@
15970
16645
  return entityTags.some((tag) => {
15971
16646
  return tagsToRender.indexOf(tag) !== -1;
15972
16647
  });
15973
- }));
16648
+ })));
15974
16649
  }
15975
16650
  else {
15976
- this.distributeForRender(data.entities);
16651
+ this.distributeForRender(this.reRenderState.withSimulated(data.entities));
15977
16652
  }
15978
16653
  });
15979
16654
  // Cesium itself handles single-visibility ranges very well.
@@ -16013,6 +16688,10 @@
16013
16688
  // Remove regos directly.
16014
16689
  if (event.action === "D") {
16015
16690
  for (const entityId of affectedIds) {
16691
+ // Deleted in the API, but drawn from its simulation until that is cleared.
16692
+ if (this.reRenderState.isSimulated(entityId)) {
16693
+ continue;
16694
+ }
16016
16695
  this.visualsManager.RemoveRegos({
16017
16696
  entityId,
16018
16697
  menuItemId: this.item.id,
@@ -16210,10 +16889,13 @@
16210
16889
  console.warn("Geojson rendering does not support multiple zoom controls. Only the first one will be used.");
16211
16890
  this.zoomControl = [this.zoomControl[0]];
16212
16891
  }
16892
+ this.drawnSettings = this.getDrawnSettingsSignature();
16213
16893
  changed = true;
16214
16894
  }
16215
16895
  if (scenario != null) {
16216
16896
  this.scenario = scenario;
16897
+ // The drawn records now belong to a scenario the item does not name.
16898
+ this.drawnKey = null;
16217
16899
  changed = true;
16218
16900
  }
16219
16901
  if (changed) {
@@ -16228,6 +16910,16 @@
16228
16910
  this.doEntityCheck(entityIds);
16229
16911
  }
16230
16912
  }
16913
+ /**
16914
+ * Switches to another of the item's own styles, restyling what is drawn without re-creating it.
16915
+ */
16916
+ SwapStyle(item) {
16917
+ if (this.disposed) {
16918
+ return;
16919
+ }
16920
+ this.item = item;
16921
+ this.UpdateSettings({ zoomControl: item.CameraZoomSettings });
16922
+ }
16231
16923
  /**
16232
16924
  * Narrows a camera-driven check to the entities whose zoom band could actually have changed.
16233
16925
  * @param ids
@@ -16981,6 +17673,8 @@
16981
17673
  AdoptHandoff(payload) {
16982
17674
  var _a;
16983
17675
  const { shouldRender } = this.applyItemSettings();
17676
+ this.drawnKey = shouldRender ? Manager.GetHandoffKey(this.item) : null;
17677
+ this.drawnSettings = this.getDrawnSettingsSignature();
16984
17678
  this.visualsManager.ReassignMenuItem({
16985
17679
  fromMenuItemId: payload.sourceMenuItemId,
16986
17680
  toMenuItemId: this.item.id,
@@ -17136,7 +17830,6 @@
17136
17830
  // Highly experimental flag to try improve rendering large sets of polygons and polylines.
17137
17831
  // Many things are not supported when this is enabled.
17138
17832
  this.useGeojson = false;
17139
- this.reRenderState = new EntityReRenderMaintainState();
17140
17833
  /**
17141
17834
  * Our default path.
17142
17835
  * We render each entity individually.
@@ -17146,6 +17839,7 @@
17146
17839
  */
17147
17840
  this.renderAsIndividualsChain = Promise.resolve();
17148
17841
  const { viewer, apiGetter, monitor, item, register: visualsManager } = params;
17842
+ this.reRenderState = new EntityReRenderMaintainState(params.simulation);
17149
17843
  this.viewer = viewer;
17150
17844
  this.apiGetter = apiGetter;
17151
17845
  this.monitor = monitor;
@@ -17268,7 +17962,8 @@
17268
17962
  continue;
17269
17963
  }
17270
17964
  }
17271
- this.onGetterUpdate(entities);
17965
+ // The item carries its own Entities, so a simulated one among them is swapped for its blob.
17966
+ this.onGetterUpdate(this.reRenderState.withSimulated(entities));
17272
17967
  });
17273
17968
  this.getter.Start();
17274
17969
  }
@@ -17355,6 +18050,16 @@
17355
18050
  this.doEntityCheck(entityIds);
17356
18051
  }
17357
18052
  }
18053
+ /**
18054
+ * Switches to another of the item's own styles, restyling what is drawn without re-creating it.
18055
+ */
18056
+ SwapStyle(item) {
18057
+ if (this.disposed) {
18058
+ return;
18059
+ }
18060
+ this.item = item;
18061
+ this.UpdateSettings({ zoomControl: item.CameraZoomSettings });
18062
+ }
17358
18063
  async doEntityCheck(ids, force = false) {
17359
18064
  var _a, _b;
17360
18065
  const api = this.apiGetter.getApi();
@@ -17910,8 +18615,15 @@
17910
18615
  this.renderQueueActive = false;
17911
18616
  this.renderQueue = [];
17912
18617
  this.renderQueueForceFlags = [];
17913
- this.reRenderState = new EntityReRenderMaintainState();
17914
18618
  const { viewer, apiGetter, monitor, item, register: visualsManager } = params;
18619
+ this.reRenderState = new EntityReRenderMaintainState(params.simulation, () => {
18620
+ // Read at the scene's time only when the item follows history, the same choice its reads make.
18621
+ const bruceEntity = this.item.BruceEntity;
18622
+ if (!(bruceEntity === null || bruceEntity === void 0 ? void 0 : bruceEntity.historic) && !(bruceEntity === null || bruceEntity === void 0 ? void 0 : bruceEntity.historicAttrKey)) {
18623
+ return null;
18624
+ }
18625
+ return { timeMs: Cesium.JulianDate.toDate(this.viewer.clock.currentTime).getTime(), attrKey: bruceEntity.historicAttrKey };
18626
+ });
17915
18627
  this.sharedHistoric = params.sharedHistoric;
17916
18628
  this.viewer = viewer;
17917
18629
  this.apiGetter = apiGetter;
@@ -17924,7 +18636,8 @@
17924
18636
  getApi: () => this.apiGetter.getApi(),
17925
18637
  getAttrKey: () => { var _a; return (_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historicAttrKey; },
17926
18638
  // Padding either side of a requested range, for clock desync between us and the server.
17927
- paddingMs: 15000
18639
+ paddingMs: 15000,
18640
+ simulation: params.simulation
17928
18641
  });
17929
18642
  if (this.item.enableClustering) {
17930
18643
  this.clustering = new PointClustering(visualsManager, this.item.id, (_a = this.item) === null || _a === void 0 ? void 0 : _a.clustering);
@@ -18047,6 +18760,16 @@
18047
18760
  this.onGetterUpdate(entityIds);
18048
18761
  }
18049
18762
  }
18763
+ /**
18764
+ * Switches to another of the item's own styles, restyling what is drawn without re-creating it.
18765
+ */
18766
+ SwapStyle(item) {
18767
+ if (this.disposed) {
18768
+ return;
18769
+ }
18770
+ this.item = item;
18771
+ this.UpdateSettings({ zoomControl: item.CameraZoomSettings });
18772
+ }
18050
18773
  /*
18051
18774
  * The lane this item reads through, or null when it does not follow the clock.
18052
18775
  */
@@ -18382,9 +19105,9 @@
18382
19105
  this.getter = null;
18383
19106
  this.getterSub = null;
18384
19107
  this.disposed = false;
18385
- this.reRenderState = new EntityReRenderMaintainState();
18386
19108
  this.renderEntityChain = Promise.resolve();
18387
19109
  const { viewer, apiGetter, monitor, item, register: visualsManager } = params;
19110
+ this.reRenderState = new EntityReRenderMaintainState(params.simulation);
18388
19111
  this.viewer = viewer;
18389
19112
  this.apiGetter = apiGetter;
18390
19113
  this.monitor = monitor;
@@ -18506,6 +19229,16 @@
18506
19229
  this.onGetterUpdate(entityId, true);
18507
19230
  }
18508
19231
  }
19232
+ /**
19233
+ * Switches to another of the item's own styles, restyling what is drawn without re-creating it.
19234
+ */
19235
+ SwapStyle(item) {
19236
+ if (this.disposed) {
19237
+ return;
19238
+ }
19239
+ this.item = item;
19240
+ this.UpdateSettings({ zoomControl: item.CameraZoomSettings });
19241
+ }
18509
19242
  renderEntity(entity, force = false) {
18510
19243
  // Waiting for prior call to finish resolving.
18511
19244
  // This avoids race conditions against two sets of overlapping Entities.
@@ -18658,6 +19391,122 @@
18658
19391
  ];
18659
19392
  })(CodeLogicExceptions || (CodeLogicExceptions = {}));
18660
19393
 
19394
+ /**
19395
+ * The line drawn through where a moving thing has been, from its history, as a whole path or a fading trail.
19396
+ */
19397
+ var HistoricTrail;
19398
+ (function (HistoricTrail) {
19399
+ const BANDS = 12;
19400
+ const SAMPLES = 96;
19401
+ const WIDTH_FAR = 1;
19402
+ const ALPHA_FAR = 0.05;
19403
+ // Above 1, so width and alpha hold near the head and give way further back.
19404
+ const WIDTH_CURVE = 1.4;
19405
+ const ALPHA_CURVE = 1.25;
19406
+ // How strongly a line is drawn where it passes behind the globe, against its own alpha.
19407
+ const BEHIND_ALPHA = 0.35;
19408
+ /**
19409
+ * The trail length a menu item asks for in its historicTrackSeconds, or null for the whole path.
19410
+ */
19411
+ function GetTrailSeconds(item) {
19412
+ const seconds = Number(item === null || item === void 0 ? void 0 : item.historicTrackSeconds);
19413
+ return seconds > 0 ? seconds : null;
19414
+ }
19415
+ HistoricTrail.GetTrailSeconds = GetTrailSeconds;
19416
+ class Trail {
19417
+ constructor(params) {
19418
+ this.entities = [];
19419
+ this.memoTime = null;
19420
+ this.memo = [];
19421
+ this.viewer = params.viewer;
19422
+ const trailing = params.trailSeconds > 0 && Boolean(params.positionAt);
19423
+ if (!trailing) {
19424
+ this.add(params, params.color, params.width, () => { var _a; return (_a = params.getSeries()) !== null && _a !== void 0 ? _a : []; });
19425
+ return;
19426
+ }
19427
+ for (let band = 0; band < BANDS; band++) {
19428
+ // 0 at the head, 1 at the far end.
19429
+ const age = band / (BANDS - 1);
19430
+ const width = params.width - (params.width - WIDTH_FAR) * Math.pow(age, WIDTH_CURVE);
19431
+ const alpha = params.color.alpha - (params.color.alpha - ALPHA_FAR) * Math.pow(age, ALPHA_CURVE);
19432
+ this.add(params, params.color.withAlpha(alpha), Math.max(WIDTH_FAR, width), (time) => {
19433
+ return this.slice(this.sample(params, time), band);
19434
+ });
19435
+ }
19436
+ }
19437
+ /*
19438
+ * One polyline of the trail, its positions read from the scene's time.
19439
+ */
19440
+ add(params, color, width, positions) {
19441
+ const entity = this.viewer.entities.add({
19442
+ id: BModels.ObjectUtils.UId(15),
19443
+ polyline: {
19444
+ positions: new Cesium.CallbackProperty((time) => positions(time), false),
19445
+ width: width,
19446
+ material: color,
19447
+ clampToGround: params.clampToGround,
19448
+ // A path through the air follows no ellipsoid, so it is drawn as the straight runs it holds.
19449
+ arcType: params.clampToGround ? Cesium.ArcType.GEODESIC : Cesium.ArcType.NONE,
19450
+ // Drawn dimmer behind the globe rather than winking in and out against the terrain along the limb.
19451
+ depthFailMaterial: params.clampToGround ? undefined : color.withAlpha(color.alpha * BEHIND_ALPHA),
19452
+ zIndex: 0,
19453
+ classificationType: params.clampToGround ? Cesium.ClassificationType.BOTH : undefined
19454
+ }
19455
+ });
19456
+ entity._noEdit = true;
19457
+ entity._noSelect = true;
19458
+ exports.CesiumEntityStyler.SetDefaultColor({
19459
+ color: color,
19460
+ entity: entity,
19461
+ viewer: this.viewer,
19462
+ override: true,
19463
+ requestRender: false
19464
+ });
19465
+ this.entities.push(entity);
19466
+ }
19467
+ /*
19468
+ * The trail's positions at a time, newest last, kept for that time so every band reads the same set.
19469
+ */
19470
+ sample(params, time) {
19471
+ var _a;
19472
+ if (this.memoTime && Cesium.JulianDate.equals(this.memoTime, time)) {
19473
+ return this.memo;
19474
+ }
19475
+ this.memoTime = Cesium.JulianDate.clone(time, (_a = this.memoTime) !== null && _a !== void 0 ? _a : new Cesium.JulianDate());
19476
+ const nowMs = Cesium.JulianDate.toDate(time).getTime();
19477
+ const spanMs = params.trailSeconds * 1000;
19478
+ const points = [];
19479
+ for (let i = 0; i <= SAMPLES; i++) {
19480
+ const pos3d = params.positionAt(nowMs - spanMs + spanMs * i / SAMPLES);
19481
+ if (pos3d) {
19482
+ points.push(pos3d);
19483
+ }
19484
+ }
19485
+ this.memo = points;
19486
+ return points;
19487
+ }
19488
+ /*
19489
+ * A band's run of the samples, counted back from the head, sharing its end point with the next band.
19490
+ */
19491
+ slice(points, band) {
19492
+ if (points.length < 2) {
19493
+ return [];
19494
+ }
19495
+ const per = (points.length - 1) / BANDS;
19496
+ const end = points.length - 1 - Math.round(band * per);
19497
+ const start = points.length - 1 - Math.round((band + 1) * per);
19498
+ return start < end ? points.slice(Math.max(0, start), end + 1) : [];
19499
+ }
19500
+ Dispose() {
19501
+ for (const entity of this.entities) {
19502
+ RemoveCEntityFromScene(this.viewer, entity);
19503
+ }
19504
+ this.entities = [];
19505
+ }
19506
+ }
19507
+ HistoricTrail.Trail = Trail;
19508
+ })(HistoricTrail || (HistoricTrail = {}));
19509
+
18661
19510
  class EntityGatherer {
18662
19511
  get OnQueueProgress() {
18663
19512
  if (!this._onQueueProgress) {
@@ -18665,7 +19514,11 @@
18665
19514
  }
18666
19515
  return this._onQueueProgress;
18667
19516
  }
18668
- constructor(api, viewer, emitEntities) {
19517
+ /**
19518
+ * @param simulation what callers simulate in place of API data, used where this gatherer has no override.
19519
+ */
19520
+ constructor(api, viewer, emitEntities, simulation) {
19521
+ this.simulation = simulation;
18669
19522
  this.disposed = false;
18670
19523
  this.expandSources = false;
18671
19524
  this.historic = null;
@@ -19242,15 +20095,19 @@
19242
20095
  }
19243
20096
  }
19244
20097
  }
20098
+ // A historic read is as at the scene's time, and so is any simulated record laid over it.
20099
+ const rTimeMs = new Date(rTime).getTime();
20100
+ const atPoint = (entity) => this.historic && this.simulation ? this.simulation.ApplyAt(entity, rTimeMs) : entity;
19245
20101
  for (const entityId of requestedBatch || []) {
20102
+ // A caller's own override is an unsaved edit, newer than any simulation, so it is used as is.
19246
20103
  const override = this.entityDataOverrides.get(entityId);
19247
20104
  if (override) {
19248
20105
  resolvedEntities.push(override);
19249
20106
  continue;
19250
20107
  }
19251
- const found = foundById.get(entityId);
20108
+ const found = this.simulation ? this.simulation.Resolve(entityId, foundById.get(entityId)) : foundById.get(entityId);
19252
20109
  if (found) {
19253
- resolvedEntities.push(found);
20110
+ resolvedEntities.push(atPoint(found));
19254
20111
  continue;
19255
20112
  }
19256
20113
  resolvedEntities.push(this.createFallbackEntity(entityId));
@@ -19614,7 +20471,7 @@
19614
20471
  this.styleTilesetFeatureFullData(feature, entity, eTags);
19615
20472
  }
19616
20473
  }
19617
- });
20474
+ }, params.simulation);
19618
20475
  // Only Entities Sets bake coordinates into their features, so for any other source this
19619
20476
  // resolves nothing and the gather stays in insertion order.
19620
20477
  this.entityGatherer.SetViewMonitor(this.monitor);
@@ -21138,6 +21995,9 @@
21138
21995
  return this.modelTreeUpdate;
21139
21996
  }
21140
21997
  constructor(params) {
21998
+ this.simulationRemoval = null;
21999
+ // Where the tileset stands by its own record, so a cleared simulation puts it back without a refetch.
22000
+ this.restCoords = null;
21141
22001
  this.disposed = false;
21142
22002
  this.modelSpace = false;
21143
22003
  this.cTileset = null;
@@ -21175,7 +22035,7 @@
21175
22035
  this.historicAnimation = null;
21176
22036
  this.historicGatherer = null;
21177
22037
  // The trail through the root's own movement, which describes the whole assembly.
21178
- this.trackEntity = null;
22038
+ this.trail = null;
21179
22039
  this.historicPossesLoadingProm = null;
21180
22040
  // Queue of loaded in features that we haven't processed yet.
21181
22041
  this.featureQueue = [];
@@ -21189,6 +22049,7 @@
21189
22049
  this.featurePropCache = new Map();
21190
22050
  this.featurePropsChecked = 0;
21191
22051
  const { viewer, register: visualsManager, getters, item, modelSpace, initQueue } = params;
22052
+ this.simulation = params.simulation;
21192
22053
  this.viewer = viewer;
21193
22054
  this.getters = getters;
21194
22055
  this.item = item;
@@ -21353,7 +22214,7 @@
21353
22214
  historicPoint: this.viewer.clock.currentTime.toString(),
21354
22215
  api: api
21355
22216
  });
21356
- coords = coordsFromHistoricEntity(root);
22217
+ coords = coordsFromHistoricEntity(this.atClock(root));
21357
22218
  }
21358
22219
  catch (e) {
21359
22220
  console.error(e);
@@ -21394,6 +22255,10 @@
21394
22255
  this.doDispose();
21395
22256
  return;
21396
22257
  }
22258
+ this.restCoords = coords;
22259
+ if (this.simulatedRootCoords()) {
22260
+ this.applyCoords(tileset, this.simulatedRootCoords());
22261
+ }
21397
22262
  this.viewer.scene.requestRender();
21398
22263
  exports.TilesetRenderEngine.OnTilesetReady(this.cTileset).then(() => {
21399
22264
  var _a, _b, _c;
@@ -21413,6 +22278,7 @@
21413
22278
  }
21414
22279
  this.onCTilesetLoad();
21415
22280
  this.styler.Init({
22281
+ simulation: this.simulation,
21416
22282
  viewer: this.viewer,
21417
22283
  // Default API instead of the account one.
21418
22284
  // Records are driven by the account loading it instead of the source account.
@@ -21428,6 +22294,7 @@
21428
22294
  });
21429
22295
  this.viewer.scene.requestRender();
21430
22296
  // Monitor scene time changes and update the root Entity.
22297
+ this.followSimulation(tileset);
21431
22298
  this.viewerDateTimeSub(tileset);
21432
22299
  }
21433
22300
  catch (e) {
@@ -21483,7 +22350,7 @@
21483
22350
  */
21484
22351
  applyRootTrack() {
21485
22352
  var _a;
21486
- if (!this.item.historicDrawTrack || !this.historicAnimation || this.trackEntity) {
22353
+ if (!this.item.historicDrawTrack || !this.historicAnimation || this.trail) {
21487
22354
  return;
21488
22355
  }
21489
22356
  const trackStyle = exports.EntityRenderEngine.GetTrackStyle({
@@ -21493,26 +22360,14 @@
21493
22360
  if (!trackStyle) {
21494
22361
  return;
21495
22362
  }
21496
- this.trackEntity = this.viewer.entities.add({
21497
- id: BModels.ObjectUtils.UId(15),
21498
- polyline: {
21499
- positions: new Cesium.CallbackProperty(() => { var _a, _b; return (_b = (_a = this.historicAnimation) === null || _a === void 0 ? void 0 : _a.GetSeries()) !== null && _b !== void 0 ? _b : []; }, false),
21500
- width: trackStyle.width,
21501
- material: trackStyle.color,
21502
- clampToGround: exports.EntityRenderEngine.GetHeightRefPolyline(Manager.GetRecordHeightRef(this.item))
21503
- == Cesium.HeightReference.CLAMP_TO_GROUND,
21504
- zIndex: 0,
21505
- classificationType: Cesium.ClassificationType.BOTH
21506
- }
21507
- });
21508
- this.trackEntity._noEdit = true;
21509
- this.trackEntity._noSelect = true;
21510
- exports.CesiumEntityStyler.SetDefaultColor({
21511
- color: trackStyle.color,
21512
- entity: this.trackEntity,
22363
+ // The series has no position at an arbitrary time, so this draws the whole path rather than a trail.
22364
+ this.trail = new HistoricTrail.Trail({
21513
22365
  viewer: this.viewer,
21514
- override: true,
21515
- requestRender: false
22366
+ color: trackStyle.color,
22367
+ width: trackStyle.width,
22368
+ clampToGround: exports.EntityRenderEngine.GetHeightRefPolyline(Manager.GetRecordHeightRef(this.item))
22369
+ == Cesium.HeightReference.CLAMP_TO_GROUND,
22370
+ getSeries: () => { var _a, _b; return (_b = (_a = this.historicAnimation) === null || _a === void 0 ? void 0 : _a.GetSeries()) !== null && _b !== void 0 ? _b : []; }
21516
22371
  });
21517
22372
  }
21518
22373
  /**
@@ -21565,7 +22420,7 @@
21565
22420
  }
21566
22421
  // Returns null (fall back to a normal Dispose()) if the tileset hasn't finished loading yet.
21567
22422
  PrepareHandoff() {
21568
- var _a, _b, _c, _d;
22423
+ var _a, _b, _c, _d, _e;
21569
22424
  if (this.disposed || !this.cTileset || this.cTileset.isDestroyed()) {
21570
22425
  return null;
21571
22426
  }
@@ -21583,9 +22438,11 @@
21583
22438
  this.handoffSourceMenuItemId = null;
21584
22439
  // Not transferred, the receiving manager sets up its own fresh copy if needed.
21585
22440
  this.viewerDateTimeDispose();
21586
- (_b = this.styler) === null || _b === void 0 ? void 0 : _b.Dispose();
21587
- (_c = this.tileLoadRemoval) === null || _c === void 0 ? void 0 : _c.call(this);
21588
- (_d = this.tileUnloadRemoval) === null || _d === void 0 ? void 0 : _d.call(this);
22441
+ (_b = this.simulationRemoval) === null || _b === void 0 ? void 0 : _b.call(this);
22442
+ this.simulationRemoval = null;
22443
+ (_c = this.styler) === null || _c === void 0 ? void 0 : _c.Dispose();
22444
+ (_d = this.tileLoadRemoval) === null || _d === void 0 ? void 0 : _d.call(this);
22445
+ (_e = this.tileUnloadRemoval) === null || _e === void 0 ? void 0 : _e.call(this);
21589
22446
  this.tileLoadRemoval = null;
21590
22447
  this.tileUnloadRemoval = null;
21591
22448
  const payload = {
@@ -21606,7 +22463,7 @@
21606
22463
  // Takes ownership of a PrepareHandoff() payload instead of loading via Init().
21607
22464
  // Then re-parents every already-registered rego to this menu item and re-runs styling.
21608
22465
  AdoptHandoff(payload) {
21609
- var _a, _b, _c, _d;
22466
+ var _a, _b, _c, _d, _e;
21610
22467
  this.cTileset = payload.cTileset;
21611
22468
  this.tileset = payload.tileset;
21612
22469
  this.rootId = payload.rootId;
@@ -21637,6 +22494,7 @@
21637
22494
  // Same visuals, same registrations so just re-keyed onto this menu item.
21638
22495
  this.onCTilesetLoad();
21639
22496
  this.styler.Init({
22497
+ simulation: this.simulation,
21640
22498
  viewer: this.viewer,
21641
22499
  api: this.getters.GetBruceApi(),
21642
22500
  cTileset: this.cTileset,
@@ -21670,7 +22528,9 @@
21670
22528
  }
21671
22529
  });
21672
22530
  this.viewer.scene.requestRender();
21673
- if (((_d = this.item.BruceEntity) === null || _d === void 0 ? void 0 : _d.historic) && this.tileset) {
22531
+ if (this.tileset) {
22532
+ this.restCoords = (_d = this.restCoords) !== null && _d !== void 0 ? _d : (_e = this.cTileset) === null || _e === void 0 ? void 0 : _e._bruceCoords;
22533
+ this.followSimulation(this.tileset);
21674
22534
  this.viewerDateTimeSub(this.tileset);
21675
22535
  }
21676
22536
  }
@@ -22248,6 +23108,21 @@
22248
23108
  }
22249
23109
  return children;
22250
23110
  }
23111
+ /**
23112
+ * Switches to another of the item's own styles, restyling the loaded tileset without reloading it.
23113
+ */
23114
+ SwapStyle(item) {
23115
+ var _a;
23116
+ if (this.disposed) {
23117
+ return;
23118
+ }
23119
+ this.item = item;
23120
+ // Before the tileset loads this only stores the pick, and the styler's Init reads the new item.
23121
+ this.styler.UpdateStyleMapping({
23122
+ styleMapping: (_a = item.StyleMapping) !== null && _a !== void 0 ? _a : [],
23123
+ fallbackStyleId: item.styleId
23124
+ });
23125
+ }
22251
23126
  Dispose() {
22252
23127
  var _a;
22253
23128
  if (this.disposed) {
@@ -22258,7 +23133,7 @@
22258
23133
  this.doDispose();
22259
23134
  }
22260
23135
  doDispose() {
22261
- var _a, _b, _c;
23136
+ var _a, _b, _c, _d;
22262
23137
  if (this.featureQueueInterval) {
22263
23138
  clearInterval(this.featureQueueInterval);
22264
23139
  this.featureQueueInterval = null;
@@ -22293,6 +23168,8 @@
22293
23168
  this.handoffSourceMenuItemId = null;
22294
23169
  }
22295
23170
  this.viewerDateTimeDispose();
23171
+ (_d = this.simulationRemoval) === null || _d === void 0 ? void 0 : _d.call(this);
23172
+ this.simulationRemoval = null;
22296
23173
  }
22297
23174
  async ReRender(params) {
22298
23175
  let { entityIds, force, entities, maintain } = params;
@@ -22318,8 +23195,11 @@
22318
23195
  * @param tileset
22319
23196
  */
22320
23197
  viewerDateTimeSub(tileset) {
22321
- var _a;
22322
- if (!((_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historic) || this.viewerDateTimeChangeRemoval) {
23198
+ var _a, _b;
23199
+ // An item that is not historic still follows the root's simulated records, once there are some.
23200
+ const historic = Boolean((_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historic);
23201
+ const simulated = Boolean(this.rootId && ((_b = this.simulation) === null || _b === void 0 ? void 0 : _b.GetRecords(this.rootId).length));
23202
+ if ((!historic && !simulated) || this.historicGatherer || this.viewerDateTimeChangeRemoval) {
22323
23203
  return;
22324
23204
  }
22325
23205
  const accountId = this.accountIdForRecords();
@@ -22348,6 +23228,8 @@
22348
23228
  // Time padding in milliseconds (15 seconds).
22349
23229
  // Helps account for desync between client and server.
22350
23230
  paddingMs: 15000,
23231
+ simulation: this.simulation,
23232
+ simulatedOnly: !historic,
22351
23233
  onRecords: (recordsByIds) => {
22352
23234
  var _a;
22353
23235
  const records = recordsByIds[this.rootId];
@@ -22375,6 +23257,7 @@
22375
23257
  */
22376
23258
  const getInitialPosses = async () => {
22377
23259
  this.historicPossesLoadingProm = new Promise(async (res) => {
23260
+ var _a;
22378
23261
  try {
22379
23262
  if (this.disposed) {
22380
23263
  res(false);
@@ -22387,11 +23270,16 @@
22387
23270
  return;
22388
23271
  }
22389
23272
  // Also request the current position in case its not in the range.
22390
- const { entity } = await BModels.Entity.Get({
23273
+ // A simulated root, or an item that is not historic, has no current record to ask the API for.
23274
+ if (!historic || ((_a = this.simulation) === null || _a === void 0 ? void 0 : _a.HasEntity(this.rootId))) {
23275
+ res(positions);
23276
+ return;
23277
+ }
23278
+ const entity = this.atClock((await BModels.Entity.Get({
22391
23279
  entityId: this.rootId,
22392
23280
  historicPoint: this.viewer.clock.currentTime.toString(),
22393
23281
  api: api
22394
- });
23282
+ })).entity);
22395
23283
  if (this.disposed || !entity) {
22396
23284
  res(false);
22397
23285
  return;
@@ -22471,14 +23359,15 @@
22471
23359
  }
22472
23360
  });
22473
23361
  this.viewerDateTimeChangeRemoval = () => {
23362
+ var _a, _b;
22474
23363
  if (this.historicAnimation) {
22475
23364
  this.historicAnimation.stop();
22476
23365
  this.historicAnimation = null;
22477
23366
  }
22478
- if (this.trackEntity) {
22479
- RemoveCEntityFromScene(this.viewer, this.trackEntity);
22480
- this.trackEntity = null;
22481
- }
23367
+ (_a = this.historicGatherer) === null || _a === void 0 ? void 0 : _a.Dispose();
23368
+ this.historicGatherer = null;
23369
+ (_b = this.trail) === null || _b === void 0 ? void 0 : _b.Dispose();
23370
+ this.trail = null;
22482
23371
  if (clockTickRemoval) {
22483
23372
  clockTickRemoval();
22484
23373
  clockTickRemoval = null;
@@ -22486,10 +23375,84 @@
22486
23375
  };
22487
23376
  });
22488
23377
  }
22489
- viewerDateTimeDispose() {
23378
+ /*
23379
+ * Keeps the tileset in line with what is simulated for its root Entity.
23380
+ */
23381
+ followSimulation(tileset) {
23382
+ if (this.simulationRemoval || !this.simulation) {
23383
+ return;
23384
+ }
23385
+ this.simulationRemoval = this.simulation.OnChanged.Subscribe((change) => {
23386
+ var _a;
23387
+ if (this.disposed || !this.rootId || !change.entityIds.includes(this.rootId)) {
23388
+ return;
23389
+ }
23390
+ if (change.records) {
23391
+ this.regatherRoot(tileset);
23392
+ }
23393
+ // A simulated root is where the tileset stands outside its history; recorded positions still win.
23394
+ if (change.entities && !((_a = this.historicAnimation) === null || _a === void 0 ? void 0 : _a.getDateRange().minDate)) {
23395
+ this.applyRootRest(tileset);
23396
+ }
23397
+ });
23398
+ }
23399
+ /*
23400
+ * Re-reads the root's history after its simulated records changed, following the clock if it was not.
23401
+ */
23402
+ async regatherRoot(tileset) {
22490
23403
  var _a;
23404
+ if (!this.historicGatherer) {
23405
+ this.viewerDateTimeSub(tileset);
23406
+ return;
23407
+ }
23408
+ await this.historicPossesLoadingProm;
23409
+ const { recordsByIds } = await this.historicGatherer.Gather([{ Bruce: { ID: this.rootId } }]);
23410
+ if (this.disposed) {
23411
+ return;
23412
+ }
23413
+ const posses = Manager.HistoricPossesFor({ viewer: this.viewer, item: this.item, records: recordsByIds[this.rootId] || [] });
23414
+ (_a = this.historicAnimation) === null || _a === void 0 ? void 0 : _a.replacePositions(posses);
23415
+ if (!posses.length) {
23416
+ this.applyRootRest(tileset);
23417
+ }
23418
+ }
23419
+ /*
23420
+ * The root as read at the scene's time, with any simulated record at that time laid over it.
23421
+ */
23422
+ atClock(root) {
23423
+ return this.simulation && root
23424
+ ? this.simulation.ApplyAt(root, Cesium.JulianDate.toDate(this.viewer.clock.currentTime).getTime())
23425
+ : root;
23426
+ }
23427
+ /*
23428
+ * Stands the tileset where its root is outside history: the simulated root, else the root's own record.
23429
+ */
23430
+ applyRootRest(tileset) {
23431
+ var _a;
23432
+ this.applyCoords(tileset, (_a = this.simulatedRootCoords()) !== null && _a !== void 0 ? _a : this.restCoords);
23433
+ }
23434
+ /*
23435
+ * Where a simulated root puts the tileset, or null; a patch only places it when it names a whole location.
23436
+ */
23437
+ simulatedRootCoords() {
23438
+ var _a, _b;
23439
+ const simulation = this.rootId ? this.simulation : null;
23440
+ const root = (_a = simulation === null || simulation === void 0 ? void 0 : simulation.GetEntity(this.rootId)) !== null && _a !== void 0 ? _a : ((simulation === null || simulation === void 0 ? void 0 : simulation.HasBlob(this.rootId)) ? simulation.Overlay({ Bruce: { ID: this.rootId } }) : null);
23441
+ const location = (_b = root === null || root === void 0 ? void 0 : root.Bruce) === null || _b === void 0 ? void 0 : _b.Location;
23442
+ if ((location === null || location === void 0 ? void 0 : location.latitude) == null || (location === null || location === void 0 ? void 0 : location.longitude) == null) {
23443
+ return null;
23444
+ }
23445
+ const pos3d = Cesium.Cartesian3.fromDegrees(+location.longitude, +location.latitude, EnsureNumber(location.altitude, 0));
23446
+ const transform = root.Bruce.Transform;
23447
+ return coordsFromHistoricPos3d(this.cTileset, this.rootId, pos3d, EnsureNumber(transform === null || transform === void 0 ? void 0 : transform.heading, 0), transform === null || transform === void 0 ? void 0 : transform.pitch, transform === null || transform === void 0 ? void 0 : transform.roll);
23448
+ }
23449
+ viewerDateTimeDispose() {
23450
+ var _a, _b;
22491
23451
  (_a = this.viewerDateTimeChangeRemoval) === null || _a === void 0 ? void 0 : _a.call(this);
22492
23452
  this.viewerDateTimeChangeRemoval = null;
23453
+ // Made before the first positions land, so it can outlive a removal that was never set.
23454
+ (_b = this.historicGatherer) === null || _b === void 0 ? void 0 : _b.Dispose();
23455
+ this.historicGatherer = null;
22493
23456
  }
22494
23457
  /**
22495
23458
  * Updates the Tileset with latest coordinates.
@@ -23630,11 +24593,11 @@
23630
24593
  this.viewMonitorRemoval = null;
23631
24594
  this.renderQueue = [];
23632
24595
  this.renderQueueInterval = null;
23633
- this.reRenderState = new EntityReRenderMaintainState();
23634
24596
  this.zoomControl = [];
23635
24597
  this.queryLoadId = 0;
23636
24598
  this.renderDataLabEntitiesChain = Promise.resolve();
23637
24599
  const { viewer, item, apiGetter, monitor, visualsRegister } = params;
24600
+ this.reRenderState = new EntityReRenderMaintainState(params.simulation);
23638
24601
  this.viewer = viewer;
23639
24602
  this.item = item;
23640
24603
  this.apiGetter = apiGetter;
@@ -23731,7 +24694,8 @@
23731
24694
  if (!((_a = data === null || data === void 0 ? void 0 : data.entities) === null || _a === void 0 ? void 0 : _a.length)) {
23732
24695
  return;
23733
24696
  }
23734
- this.distributeForRender(data.entities);
24697
+ // The getter answers from the API, so a simulated Entity in its answer is swapped for its blob.
24698
+ this.distributeForRender(this.reRenderState.withSimulated(data.entities));
23735
24699
  });
23736
24700
  }
23737
24701
  unsetGetter() {
@@ -23886,6 +24850,16 @@
23886
24850
  this.doEntityCheck(entityIds);
23887
24851
  }
23888
24852
  }
24853
+ /**
24854
+ * Switches to another of the item's own styles, restyling what is drawn without re-creating it.
24855
+ */
24856
+ SwapStyle(item) {
24857
+ if (this.disposed) {
24858
+ return;
24859
+ }
24860
+ this.item = item;
24861
+ this.UpdateSettings({ zoomControl: item.CameraZoomSettings });
24862
+ }
23889
24863
  async doEntityCheck(ids, force = false) {
23890
24864
  var _a, _b, _c, _d;
23891
24865
  if (this.isRunningCheck) {
@@ -25478,6 +26452,7 @@
25478
26452
  this.featurePropCache = new Map();
25479
26453
  this.featurePropsChecked = 0;
25480
26454
  const { viewer, register: visualsManager, getters: apiGetter, item, initQueue, monitor } = params;
26455
+ this.simulation = params.simulation;
25481
26456
  this.monitor = monitor;
25482
26457
  this.viewer = viewer;
25483
26458
  this.getters = apiGetter;
@@ -25578,6 +26553,7 @@
25578
26553
  if (Manager.NeedsStyler(this.item)) {
25579
26554
  const api = this.getters.GetBruceApi();
25580
26555
  this.styler.Init({
26556
+ simulation: this.simulation,
25581
26557
  viewer: this.viewer,
25582
26558
  api: api,
25583
26559
  cTileset: this.cTileset,
@@ -25793,6 +26769,21 @@
25793
26769
  }
25794
26770
  }
25795
26771
  }
26772
+ /**
26773
+ * Switches to another of the item's own styles, restyling the loaded tileset without reloading it.
26774
+ */
26775
+ SwapStyle(item) {
26776
+ if (this.disposed) {
26777
+ return;
26778
+ }
26779
+ this.item = item;
26780
+ if (Manager.NeedsStyler(item)) {
26781
+ this.styler.UpdateStyleMapping({
26782
+ styleMapping: [],
26783
+ fallbackStyleId: item.styleId
26784
+ });
26785
+ }
26786
+ }
25796
26787
  Dispose() {
25797
26788
  var _a;
25798
26789
  if (this.disposed) {
@@ -25935,6 +26926,7 @@
25935
26926
  const needsStyler = Manager.NeedsStyler(this.item);
25936
26927
  if (needsStyler) {
25937
26928
  this.styler.Init({
26929
+ simulation: this.simulation,
25938
26930
  viewer: this.viewer,
25939
26931
  api: this.getters.GetBruceApi(),
25940
26932
  cTileset: this.cTileset,
@@ -26619,6 +27611,7 @@
26619
27611
  // LCC object if we're using XGrids to render LCC data.
26620
27612
  this.lccObject = null;
26621
27613
  this.viewer = params.viewer;
27614
+ this.simulation = params.simulation;
26622
27615
  this.getters = params.getters;
26623
27616
  this.visualsManager = params.register;
26624
27617
  // We modify the object due to a lot of backwards compatibility.
@@ -26853,6 +27846,7 @@
26853
27846
  this.item.Type == BModels.MenuItem.EType.IonTileset) {
26854
27847
  const api = this.getters.GetBruceApi();
26855
27848
  this.styler.Init({
27849
+ simulation: this.simulation,
26856
27850
  viewer: this.viewer,
26857
27851
  api: api,
26858
27852
  cTileset: this.cTileset,
@@ -27039,6 +28033,23 @@
27039
28033
  this.viewer.zoomTo(this.cTileset, new Cesium.HeadingPitchRange(0.0, -0.5, this.cTileset.boundingSphere.radius / 4.0));
27040
28034
  }
27041
28035
  }
28036
+ /**
28037
+ * Switches to another of the item's own styles, restyling the loaded tileset without reloading it.
28038
+ */
28039
+ SwapStyle(item) {
28040
+ var _a;
28041
+ if (this.disposed) {
28042
+ return;
28043
+ }
28044
+ // Copied for the same reason the constructor copies it.
28045
+ this.item = JSON.parse(JSON.stringify(item));
28046
+ if (this.item.ApplyStyles || this.item.Type == BModels.MenuItem.EType.IonTileset) {
28047
+ this.styler.UpdateStyleMapping({
28048
+ styleMapping: (_a = this.item.StyleMapping) !== null && _a !== void 0 ? _a : [],
28049
+ fallbackStyleId: this.item.styleId
28050
+ });
28051
+ }
28052
+ }
27042
28053
  Dispose() {
27043
28054
  if (this.disposed) {
27044
28055
  return;
@@ -27193,14 +28204,32 @@
27193
28204
  }
27194
28205
  ;
27195
28206
  constructor(params) {
28207
+ var _a, _b;
27196
28208
  // If true, this menu item is disposed and should not be used.
27197
28209
  // Any rendering will be ignored.
27198
28210
  this.disposed = false;
28211
+ this.simulationRemoval = null;
28212
+ this.FollowsSimulation = true;
27199
28213
  const { apiGetter, item, register, viewer } = params;
27200
28214
  this.apiGetter = apiGetter;
27201
28215
  this.item = item;
27202
28216
  this.register = register;
27203
28217
  this.viewer = viewer;
28218
+ this.simulation = params.simulation;
28219
+ // Ends are not this item's own Entities, so it watches the store itself rather than wait to be asked.
28220
+ this.simulationRemoval = (_b = (_a = this.simulation) === null || _a === void 0 ? void 0 : _a.OnChanged.Subscribe((change) => {
28221
+ if (this.disposed) {
28222
+ return;
28223
+ }
28224
+ const relations = RelationRenderEngine.TakeRelationsTouching({
28225
+ visualRegister: this.register,
28226
+ menuItemId: this.item.id,
28227
+ entityIds: change.entityIds
28228
+ });
28229
+ if (relations.length) {
28230
+ this.onGetterUpdate(relations);
28231
+ }
28232
+ })) !== null && _b !== void 0 ? _b : null;
27204
28233
  }
27205
28234
  /**
27206
28235
  * Starts rendering the menu item.
@@ -27269,10 +28298,13 @@
27269
28298
  return items;
27270
28299
  }
27271
28300
  Dispose() {
28301
+ var _a;
27272
28302
  if (this.disposed) {
27273
28303
  return;
27274
28304
  }
27275
28305
  this.disposed = true;
28306
+ (_a = this.simulationRemoval) === null || _a === void 0 ? void 0 : _a.call(this);
28307
+ this.simulationRemoval = null;
27276
28308
  this.register.RemoveRegos({
27277
28309
  menuItemId: this.item.id
27278
28310
  });
@@ -27511,7 +28543,8 @@
27511
28543
  menuItemId: this.item.id,
27512
28544
  relations: relations,
27513
28545
  viewer: this.viewer,
27514
- visualRegister: this.register
28546
+ visualRegister: this.register,
28547
+ simulation: this.simulation
27515
28548
  });
27516
28549
  if (this.disposed) {
27517
28550
  this.register.RemoveRegos({
@@ -27793,9 +28826,12 @@
27793
28826
  }
27794
28827
  ;
27795
28828
  constructor(params) {
28829
+ var _a, _b;
27796
28830
  // If true, this menu item is disposed and should not be used.
27797
28831
  // Any rendering will be ignored.
27798
28832
  this.disposed = false;
28833
+ this.simulationRemoval = null;
28834
+ this.FollowsSimulation = true;
27799
28835
  // Record of the currently drawn entity IDs.
27800
28836
  // This is to avoid re-queueing already drawn entities when Init is called multiple times.
27801
28837
  this.drawnIds = [];
@@ -27804,6 +28840,21 @@
27804
28840
  this.item = item;
27805
28841
  this.register = register;
27806
28842
  this.viewer = viewer;
28843
+ this.simulation = params.simulation;
28844
+ // Ends are not this item's own Entities, so it watches the store itself rather than wait to be asked.
28845
+ this.simulationRemoval = (_b = (_a = this.simulation) === null || _a === void 0 ? void 0 : _a.OnChanged.Subscribe((change) => {
28846
+ if (this.disposed) {
28847
+ return;
28848
+ }
28849
+ const relations = RelationRenderEngine.TakeRelationsTouching({
28850
+ visualRegister: this.register,
28851
+ menuItemId: this.item.id,
28852
+ entityIds: change.entityIds
28853
+ });
28854
+ if (relations.length) {
28855
+ this.onGetterUpdate(relations);
28856
+ }
28857
+ })) !== null && _b !== void 0 ? _b : null;
27807
28858
  }
27808
28859
  /**
27809
28860
  * Starts rendering the menu item.
@@ -27846,10 +28897,13 @@
27846
28897
  this.drawnIds = [].concat(entityIds);
27847
28898
  }
27848
28899
  Dispose() {
28900
+ var _a;
27849
28901
  if (this.disposed) {
27850
28902
  return;
27851
28903
  }
27852
28904
  this.disposed = true;
28905
+ (_a = this.simulationRemoval) === null || _a === void 0 ? void 0 : _a.call(this);
28906
+ this.simulationRemoval = null;
27853
28907
  this.register.RemoveRegos({
27854
28908
  menuItemId: this.item.id
27855
28909
  });
@@ -28010,7 +29064,8 @@
28010
29064
  menuItemId: this.item.id,
28011
29065
  relations: relations,
28012
29066
  viewer: this.viewer,
28013
- visualRegister: this.register
29067
+ visualRegister: this.register,
29068
+ simulation: this.simulation
28014
29069
  });
28015
29070
  if (this.disposed) {
28016
29071
  this.register.RemoveRegos({
@@ -29235,6 +30290,55 @@
29235
30290
  get ModelSpace() {
29236
30291
  return this.modelSpace;
29237
30292
  }
30293
+ /**
30294
+ * Every node of the Assembly read so far, so a caller can work out motion in the Assembly's own terms.
30295
+ * @param timeMs the scene time for each node's current pose. Default: the clock's.
30296
+ * @returns per node its pose within its parent from the API (local) and as drawn at the time (current).
30297
+ */
30298
+ GetNodes(timeMs) {
30299
+ const at = timeMs !== null && timeMs !== void 0 ? timeMs : Cesium.JulianDate.toDate(this.viewer.clock.currentTime).getTime();
30300
+ const rowsOf = (m) => m ? [0, 1, 2, 3].map(row => [0, 1, 2, 3].map(column => m[(column * 4) + row])) : null;
30301
+ return Object.values(this.hierarchyNodeByEntityId || {}).map(node => {
30302
+ var _a, _b, _c;
30303
+ const entity = (_a = this.apiEntities.get(node.entityId)) !== null && _a !== void 0 ? _a : null;
30304
+ const rows = (_b = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _b === void 0 ? void 0 : _b.AssemblyPosition;
30305
+ return {
30306
+ entityId: node.entityId,
30307
+ name: node.name,
30308
+ parentId: (_c = node.parentId) !== null && _c !== void 0 ? _c : null,
30309
+ local: Manager.MatrixFromRows(rows) ? rows : null,
30310
+ current: rowsOf(this.localAt(node.entityId, at)),
30311
+ entity
30312
+ };
30313
+ });
30314
+ }
30315
+ /**
30316
+ * The root's world frame at a time, history and simulation included, eg. to follow or frame the Assembly.
30317
+ * @param timeMs the scene time. Default: the clock's.
30318
+ * @returns null until the root has been read.
30319
+ */
30320
+ GetRootFrame(timeMs) {
30321
+ const root = this.rootAt(timeMs !== null && timeMs !== void 0 ? timeMs : Cesium.JulianDate.toDate(this.viewer.clock.currentTime).getTime());
30322
+ return root ? Manager.rootFrame(root.pos3d, root.heading, root.pitch, root.roll) : null;
30323
+ }
30324
+ /**
30325
+ * A part's world frame at a time as its records and its parents place it, size included.
30326
+ * @param entityId the part.
30327
+ * @param timeMs the scene time. Default: the clock's.
30328
+ * @returns null until the root and the part have been read.
30329
+ */
30330
+ GetPartFrame(entityId, timeMs) {
30331
+ const at = timeMs !== null && timeMs !== void 0 ? timeMs : Cesium.JulianDate.toDate(this.viewer.clock.currentTime).getTime();
30332
+ const rootFrame = this.GetRootFrame(at);
30333
+ const world = entityId === this.rootId ? Cesium.Matrix4.IDENTITY : this.composeWorld(entityId, at);
30334
+ if (!rootFrame || !world) {
30335
+ return null;
30336
+ }
30337
+ // Where the part is drawn, so its own ground offset is part of its frame.
30338
+ const ground = this.groundOffsets.get(entityId);
30339
+ const placed = ground ? Cesium.Matrix4.multiply(Cesium.Matrix4.fromTranslation(ground), world, new Cesium.Matrix4()) : world;
30340
+ return Cesium.Matrix4.multiply(rootFrame, placed, new Cesium.Matrix4());
30341
+ }
29238
30342
  constructor(params) {
29239
30343
  this.disposed = false;
29240
30344
  // Cache of the hierarchy so that our scene-tree can reference it.
@@ -29244,19 +30348,41 @@
29244
30348
  this.hierarchyNodeByEntityId = null;
29245
30349
  this.modelSpace = false;
29246
30350
  this.rootId = null;
29247
- // Where the root sits now, and which way it faces.
29248
- // Every child is placed through this, so a root move is one value changing rather than a refetch of the whole Assembly.
30351
+ // Where the root sits outside its history, and which way it faces in degrees. Children are placed through it.
29249
30352
  this.rootPos3d = null;
29250
30353
  this.rootHeading = 0;
29251
- // Entity ID -> what was drawn for it and where it sits relative to the root.
30354
+ this.rootPitch = 0;
30355
+ this.rootRoll = 0;
30356
+ // The root's recorded positions and attitudes, oldest first, kept apart since a record may name only one.
30357
+ this.rootSeries = [];
30358
+ this.rootAttitude = [];
30359
+ // Every placement worked out for the scene time last asked about, shared by all parts drawn at it.
30360
+ this.pose = null;
30361
+ // Entity ID -> what was drawn for it.
29252
30362
  this.children = new Map();
30363
+ // Entity ID -> its live position within its parent, for every node fetched including undrawn groups.
30364
+ this.localPositions = new Map();
30365
+ // Entity ID -> how much higher the ground under it is than under the root, along the root's up.
30366
+ this.groundOffsets = new Map();
30367
+ // Entity ID -> its recorded poses within its parent, oldest first.
30368
+ this.jointSeries = new Map();
30369
+ // Drawn children that can move with the clock, so a tick places only these and not the whole Assembly.
30370
+ this.animatedIds = new Set();
29253
30371
  // The trail through the root's own movement, which describes the whole Assembly.
29254
- this.trackEntity = null;
30372
+ this.trail = null;
30373
+ this.simulationRemoval = null;
30374
+ this.FollowsSimulation = true;
30375
+ // Entity ID -> the record the API gave, so a cleared simulation falls back to it without a refetch.
30376
+ this.apiEntities = new Map();
30377
+ // Re-reads history and re-applies it, eg. after simulated records change. Set while the clock is followed.
30378
+ this.regather = null;
30379
+ this.followingClock = false;
29255
30380
  // Callback to dispose the link between scene time and the root's historic position.
29256
30381
  this.viewerDateTimeChangeRemoval = null;
29257
30382
  this.historicGatherer = null;
29258
30383
  this.historicAnimation = null;
29259
30384
  const { viewer, register: visualsManager, getters, item } = params;
30385
+ this.simulation = params.simulation;
29260
30386
  this.viewer = viewer;
29261
30387
  this.getters = getters;
29262
30388
  this.item = item;
@@ -29280,10 +30406,10 @@
29280
30406
  return exports.EntityRenderEngine.GetHeightRef((_b = (_a = item === null || item === void 0 ? void 0 : item.Style) === null || _a === void 0 ? void 0 : _a.Settings) === null || _b === void 0 ? void 0 : _b["modelStyle"]);
29281
30407
  }
29282
30408
  /*
29283
- * The root's frame: east-north-up at its position, turned by its heading.
30409
+ * The root's frame: east-north-up at its position, turned by its heading, pitch and roll.
29284
30410
  */
29285
- static rootFrame(rootPos3d, headingDegrees) {
29286
- const hpr = new Cesium.HeadingPitchRoll(Cesium.Math.toRadians(headingDegrees || 0), 0, 0);
30411
+ static rootFrame(rootPos3d, headingDegrees, pitchDegrees = 0, rollDegrees = 0) {
30412
+ const hpr = new Cesium.HeadingPitchRoll(Cesium.Math.toRadians(headingDegrees || 0), Cesium.Math.toRadians(pitchDegrees || 0), Cesium.Math.toRadians(rollDegrees || 0));
29287
30413
  return Cesium.Transforms.headingPitchRollToFixedFrame(rootPos3d, hpr);
29288
30414
  }
29289
30415
  /**
@@ -29306,28 +30432,553 @@
29306
30432
  return Cesium.Matrix4.multiplyByPoint(Manager.rootFrame(rootPos3d, headingDegrees), offset, new Cesium.Cartesian3());
29307
30433
  }
29308
30434
  /**
29309
- * Moves the whole Assembly to a new root position, without refetching any of its children.
30435
+ * Moves where the root sits outside its history, without refetching children; recorded poses still win.
29310
30436
  * @param params
29311
30437
  */
29312
30438
  ApplyRootPosition(params) {
29313
- var _a, _b;
30439
+ var _a, _b, _c, _d, _e, _f;
29314
30440
  if (this.disposed || !(params === null || params === void 0 ? void 0 : params.pos3d)) {
29315
30441
  return;
29316
30442
  }
29317
30443
  this.rootPos3d = params.pos3d;
29318
30444
  this.rootHeading = (_b = (_a = params.heading) !== null && _a !== void 0 ? _a : this.rootHeading) !== null && _b !== void 0 ? _b : 0;
30445
+ this.rootPitch = (_d = (_c = params.pitch) !== null && _c !== void 0 ? _c : this.rootPitch) !== null && _d !== void 0 ? _d : 0;
30446
+ this.rootRoll = (_f = (_e = params.roll) !== null && _e !== void 0 ? _e : this.rootRoll) !== null && _f !== void 0 ? _f : 0;
30447
+ this.repose(true);
30448
+ }
30449
+ /*
30450
+ * Drops every placement worked out so far, after the root or a joint's records change.
30451
+ * @param everyChild re-place the parts that cannot move as well, because where they rest has changed.
30452
+ */
30453
+ repose(everyChild = false) {
30454
+ this.pose = null;
29319
30455
  for (const child of this.children.values()) {
29320
- const pos3d = Manager.ChildPositionFrom(this.rootPos3d, this.rootHeading, child.offset);
29321
- if (!pos3d) {
30456
+ if (everyChild || Boolean(child.moving) != this.canMove(child.entityId)) {
30457
+ this.bindPlacement(child);
30458
+ }
30459
+ }
30460
+ this.viewer.scene.requestRender();
30461
+ }
30462
+ /*
30463
+ * Takes a node's pose outside its history from its record, and for the root where it stands and faces.
30464
+ */
30465
+ absorbNode(entity) {
30466
+ var _a, _b, _c, _d, _e;
30467
+ const id = (_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.ID;
30468
+ const local = Manager.MatrixFromRows((_b = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _b === void 0 ? void 0 : _b.AssemblyPosition);
30469
+ if (local) {
30470
+ this.localPositions.set(id, local);
30471
+ }
30472
+ // Anchored on the root, which is usually never drawn; everything under it is placed from the hierarchy.
30473
+ if (id === this.rootId || (!this.rootId && !this.rootPos3d)) {
30474
+ const rootPos3d = exports.EntityUtils.GetPos({
30475
+ viewer: this.viewer,
30476
+ entity,
30477
+ recordHeightRef: Manager.GetRecordHeightRef(this.item),
30478
+ returnHeightRef: Cesium.HeightReference.NONE,
30479
+ allowRendered: false
30480
+ });
30481
+ if (rootPos3d) {
30482
+ this.rootPos3d = rootPos3d;
30483
+ this.rootHeading = EnsureNumber((_c = entity.Bruce.Transform) === null || _c === void 0 ? void 0 : _c.heading, 0);
30484
+ this.rootPitch = EnsureNumber((_d = entity.Bruce.Transform) === null || _d === void 0 ? void 0 : _d.pitch, 0);
30485
+ this.rootRoll = EnsureNumber((_e = entity.Bruce.Transform) === null || _e === void 0 ? void 0 : _e.roll, 0);
30486
+ }
30487
+ }
30488
+ }
30489
+ /*
30490
+ * A node's record with its simulation laid over it, keeping what a stand-in leaves out, eg. its model.
30491
+ */
30492
+ withSimulation(entity) {
30493
+ var _a;
30494
+ const id = (_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.ID;
30495
+ if (!this.simulation || !id) {
30496
+ return entity;
30497
+ }
30498
+ const standIn = this.simulation.GetEntity(id);
30499
+ if (!standIn) {
30500
+ return this.simulation.Overlay(entity);
30501
+ }
30502
+ return { ...entity, ...standIn, Bruce: { ...entity.Bruce, ...standIn.Bruce } };
30503
+ }
30504
+ /*
30505
+ * Brings the Assembly in line with a change to what is simulated.
30506
+ */
30507
+ onSimulationChanged(change) {
30508
+ var _a;
30509
+ if (this.disposed || !this.hierarchyNodeByEntityId) {
30510
+ return;
30511
+ }
30512
+ const ids = change.entityIds.filter(id => this.hierarchyNodeByEntityId[id]);
30513
+ if (!ids.length) {
30514
+ return;
30515
+ }
30516
+ if (change.entities) {
30517
+ for (const id of ids) {
30518
+ const api = this.apiEntities.get(id);
30519
+ if (!api) {
30520
+ // Not loaded yet, and it will be drawn from its simulation when it is.
30521
+ continue;
30522
+ }
30523
+ const entity = this.withSimulation(api);
30524
+ // A cleared blob leaves the node's own matrix where the API has none, so it is reset.
30525
+ if (!((_a = entity.Bruce) === null || _a === void 0 ? void 0 : _a.AssemblyPosition)) {
30526
+ this.localPositions.delete(id);
30527
+ }
30528
+ this.absorbNode(entity);
30529
+ const child = this.children.get(id);
30530
+ if (child) {
30531
+ child.entity = entity;
30532
+ }
30533
+ }
30534
+ this.repose(true);
30535
+ }
30536
+ if (change.records) {
30537
+ if (this.regather) {
30538
+ this.regather(true);
30539
+ }
30540
+ else {
30541
+ this.viewerDateTimeSub();
30542
+ }
30543
+ }
30544
+ }
30545
+ /*
30546
+ * Nodes of this Assembly with simulated history.
30547
+ */
30548
+ simulatedNodeIds() {
30549
+ if (!this.simulation || !this.hierarchyNodeByEntityId) {
30550
+ return [];
30551
+ }
30552
+ return this.simulation.GetRecordEntityIds().filter(id => this.hierarchyNodeByEntityId[id]);
30553
+ }
30554
+ /*
30555
+ * Whether a node can move with time: the root has history, or it or an ancestor does.
30556
+ */
30557
+ canMove(entityId) {
30558
+ return this.rootSeries.length > 0 || this.rootAttitude.length > 0 || this.animatedIds.has(entityId);
30559
+ }
30560
+ /*
30561
+ * Gives a child its position and orientation, as functions of scene time only when it can move.
30562
+ */
30563
+ bindPlacement(child) {
30564
+ const id = child.entityId;
30565
+ const moving = this.canMove(id);
30566
+ if (moving) {
30567
+ if (!child.moving) {
30568
+ child.cEntity.position = new Cesium.CallbackProperty((time, result) => { var _a; return Manager.cloneOrUndefined((_a = this.placementAt(id, time)) === null || _a === void 0 ? void 0 : _a.pos3d, result); }, false);
30569
+ child.cEntity.orientation = new Cesium.CallbackProperty((time, result) => {
30570
+ var _a;
30571
+ const orient = (_a = this.placementAt(id, time)) === null || _a === void 0 ? void 0 : _a.orient;
30572
+ return orient ? Cesium.Quaternion.clone(orient, result) : undefined;
30573
+ }, false);
30574
+ child.cEntity.model.scale = new Cesium.CallbackProperty((time) => { var _a, _b; return (_b = (_a = this.placementAt(id, time)) === null || _a === void 0 ? void 0 : _a.scale) !== null && _b !== void 0 ? _b : 1; }, false);
30575
+ }
30576
+ }
30577
+ else {
30578
+ const placed = this.placementAt(id, this.viewer.clock.currentTime);
30579
+ if (placed) {
30580
+ child.cEntity.position = new Cesium.ConstantPositionProperty(placed.pos3d);
30581
+ if (placed.orient) {
30582
+ child.cEntity.orientation = new Cesium.ConstantProperty(placed.orient);
30583
+ }
30584
+ child.cEntity.model.scale = new Cesium.ConstantProperty(placed.scale);
30585
+ }
30586
+ }
30587
+ if (Boolean(child.moving) != moving) {
30588
+ child.moving = moving;
30589
+ this.visualsManager.SetMoving({ moving, entityIds: [id], menuItemId: this.item.id });
30590
+ }
30591
+ }
30592
+ /*
30593
+ * The one scale a model takes from its part's composed matrix, the largest when they are uneven.
30594
+ */
30595
+ static uniformScale(world) {
30596
+ return Cesium.Cartesian3.maximumComponent(Cesium.Matrix4.getScale(world, new Cesium.Cartesian3()));
30597
+ }
30598
+ static cloneOrUndefined(pos3d, result) {
30599
+ return pos3d ? Cesium.Cartesian3.clone(pos3d, result) : undefined;
30600
+ }
30601
+ /*
30602
+ * Where a node draws at a scene time, from one composition shared by every part asked about at that time.
30603
+ */
30604
+ placementAt(entityId, time) {
30605
+ if (this.disposed || !time) {
30606
+ return null;
30607
+ }
30608
+ const timeMs = Cesium.JulianDate.toDate(time).getTime();
30609
+ if (!this.pose || this.pose.timeMs !== timeMs) {
30610
+ const root = this.rootAt(timeMs);
30611
+ this.pose = {
30612
+ timeMs,
30613
+ rootFrame: root ? Manager.rootFrame(root.pos3d, root.heading, root.pitch, root.roll) : null,
30614
+ worlds: new Map(),
30615
+ placements: new Map()
30616
+ };
30617
+ }
30618
+ const pose = this.pose;
30619
+ if (!pose.rootFrame) {
30620
+ return null;
30621
+ }
30622
+ let placed = pose.placements.get(entityId);
30623
+ if (!placed) {
30624
+ const world = this.composeWorld(entityId, timeMs, pose.worlds);
30625
+ const offset = world
30626
+ ? Cesium.Matrix4.getTranslation(world, new Cesium.Cartesian3())
30627
+ : new Cesium.Cartesian3(0, 0, 0);
30628
+ const ground = this.groundOffsets.get(entityId);
30629
+ if (ground) {
30630
+ Cesium.Cartesian3.add(offset, ground, offset);
30631
+ }
30632
+ placed = {
30633
+ pos3d: Cesium.Matrix4.multiplyByPoint(pose.rootFrame, offset, new Cesium.Cartesian3()),
30634
+ // Without a matrix, eg. the root itself, there is no orientation of its own to apply.
30635
+ orient: world ? exports.EntityUtils.GetAssemblyOrientationInFrame(pose.rootFrame, world) : null,
30636
+ scale: world ? Manager.uniformScale(world) : 1
30637
+ };
30638
+ pose.placements.set(entityId, placed);
30639
+ }
30640
+ return placed;
30641
+ }
30642
+ /*
30643
+ * The root's pose at a time, position and attitude each following their own records as a joint's do.
30644
+ */
30645
+ rootAt(timeMs) {
30646
+ const pos3d = this.rootPositionAt(timeMs);
30647
+ if (!pos3d) {
30648
+ return null;
30649
+ }
30650
+ const live = { heading: this.rootHeading, pitch: this.rootPitch, roll: this.rootRoll };
30651
+ const series = this.rootAttitude;
30652
+ if (!series.length || timeMs < series[0].timeMs) {
30653
+ return { pos3d, ...live };
30654
+ }
30655
+ let i = 0;
30656
+ while (i < series.length - 1 && series[i + 1].timeMs <= timeMs) {
30657
+ i++;
30658
+ }
30659
+ const a = series[i];
30660
+ const b = series[i + 1];
30661
+ const t = b ? (timeMs - a.timeMs) / (b.timeMs - a.timeMs) : 0;
30662
+ const turn = (from, to, fallback) => {
30663
+ var _a, _b, _c;
30664
+ const x = (_a = from !== null && from !== void 0 ? from : fallback) !== null && _a !== void 0 ? _a : 0;
30665
+ const y = (_c = (_b = (b ? to : from)) !== null && _b !== void 0 ? _b : fallback) !== null && _c !== void 0 ? _c : 0;
30666
+ // The short way round, so 350 to 10 degrees turns through north rather than all the way back.
30667
+ return x + (Cesium.Math.negativePiToPi(Cesium.Math.toRadians(y - x)) * 180 / Math.PI) * t;
30668
+ };
30669
+ return {
30670
+ pos3d,
30671
+ heading: turn(a.heading, b === null || b === void 0 ? void 0 : b.heading, live.heading),
30672
+ pitch: turn(a.pitch, b === null || b === void 0 ? void 0 : b.pitch, live.pitch),
30673
+ roll: turn(a.roll, b === null || b === void 0 ? void 0 : b.roll, live.roll)
30674
+ };
30675
+ }
30676
+ /*
30677
+ * Where the root is at a time, from its recorded positions.
30678
+ */
30679
+ rootPositionAt(timeMs) {
30680
+ const series = this.rootSeries;
30681
+ if (!series.length || timeMs < series[0].timeMs) {
30682
+ return this.rootPos3d;
30683
+ }
30684
+ let i = 0;
30685
+ while (i < series.length - 1 && series[i + 1].timeMs <= timeMs) {
30686
+ i++;
30687
+ }
30688
+ const a = series[i];
30689
+ const b = series[i + 1];
30690
+ if (!b) {
30691
+ return a.pos3d;
30692
+ }
30693
+ const t = (timeMs - a.timeMs) / (b.timeMs - a.timeMs);
30694
+ // A straight line between orbit samples cuts up to a kilometre inside the path, so it is lifted back out.
30695
+ const chord = Cesium.Cartesian3.lerp(a.pos3d, b.pos3d, t, new Cesium.Cartesian3());
30696
+ const carto = Cesium.Cartographic.fromCartesian(chord);
30697
+ return carto
30698
+ ? Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, Cesium.Math.lerp(a.height, b.height, t))
30699
+ : chord;
30700
+ }
30701
+ /*
30702
+ * Takes the root's records into its series.
30703
+ * @param replace the records are everything held for the root, so the series becomes exactly them.
30704
+ * @returns whether the series changed.
30705
+ */
30706
+ absorbRootRecords(posses, replace) {
30707
+ if (!(posses === null || posses === void 0 ? void 0 : posses.length) && !(replace && this.rootSeries.length)) {
30708
+ return false;
30709
+ }
30710
+ const byTime = new Map(replace ? [] : this.rootSeries.map(k => [k.timeMs, k]));
30711
+ for (const pos of posses) {
30712
+ const carto = Cesium.Cartographic.fromCartesian(pos.pos3d);
30713
+ if (!carto) {
29322
30714
  continue;
29323
30715
  }
29324
- child.cEntity.position = new Cesium.ConstantPositionProperty(pos3d);
29325
- const orient = exports.EntityUtils.GetAssemblyOrientation(child.entity, pos3d);
29326
- if (orient) {
29327
- child.cEntity.orientation = new Cesium.ConstantProperty(orient);
30716
+ byTime.set(pos.dateTime.getTime(), {
30717
+ timeMs: pos.dateTime.getTime(),
30718
+ pos3d: pos.pos3d,
30719
+ height: carto.height
30720
+ });
30721
+ }
30722
+ const series = Array.from(byTime.values()).sort((x, y) => x.timeMs - y.timeMs);
30723
+ const changed = (series.length !== this.rootSeries.length ||
30724
+ series.some((k, i) => !Cesium.Cartesian3.equals(k.pos3d, this.rootSeries[i].pos3d)));
30725
+ this.rootSeries = series;
30726
+ return changed;
30727
+ }
30728
+ /*
30729
+ * Takes the angles the root's records name into its attitude series, apart from where it is.
30730
+ * @param replace the records are everything held for the root, as for absorbRootRecords.
30731
+ * @returns whether the series changed.
30732
+ */
30733
+ absorbRootAttitude(records, replace) {
30734
+ const angle = (transform, key) => {
30735
+ var _a;
30736
+ const value = (_a = transform === null || transform === void 0 ? void 0 : transform[key]) !== null && _a !== void 0 ? _a : transform === null || transform === void 0 ? void 0 : transform[key[0].toUpperCase() + key.slice(1)];
30737
+ return value == null || isNaN(+value) ? null : +value;
30738
+ };
30739
+ const byTime = new Map(replace ? [] : this.rootAttitude.map(k => [k.timeMs, k]));
30740
+ for (const record of records || []) {
30741
+ const timeMs = (record === null || record === void 0 ? void 0 : record.dateTime) ? new Date(record.dateTime).getTime() : NaN;
30742
+ const key = {
30743
+ timeMs,
30744
+ heading: angle(record === null || record === void 0 ? void 0 : record.transform, "heading"),
30745
+ pitch: angle(record === null || record === void 0 ? void 0 : record.transform, "pitch"),
30746
+ roll: angle(record === null || record === void 0 ? void 0 : record.transform, "roll")
30747
+ };
30748
+ if (isNaN(timeMs) || (key.heading == null && key.pitch == null && key.roll == null)) {
30749
+ continue;
30750
+ }
30751
+ byTime.set(timeMs, key);
30752
+ }
30753
+ const series = Array.from(byTime.values()).sort((x, y) => x.timeMs - y.timeMs);
30754
+ const held = this.rootAttitude;
30755
+ const differs = (k, i) => (k.timeMs !== held[i].timeMs ||
30756
+ k.heading !== held[i].heading ||
30757
+ k.pitch !== held[i].pitch ||
30758
+ k.roll !== held[i].roll);
30759
+ const changed = series.length !== held.length || series.some(differs);
30760
+ this.rootAttitude = series;
30761
+ return changed;
30762
+ }
30763
+ /*
30764
+ * A node's matrix in the root's frame at a time, from each ancestor's pose within its parent.
30765
+ */
30766
+ composeWorld(entityId, timeMs, memo, localOf = (id) => this.localAt(id, timeMs)) {
30767
+ var _a, _b, _c, _d;
30768
+ if (memo === null || memo === void 0 ? void 0 : memo.has(entityId)) {
30769
+ return memo.get(entityId);
30770
+ }
30771
+ let world = null;
30772
+ const local = localOf(entityId);
30773
+ if (local) {
30774
+ const parentId = (_b = (_a = this.hierarchyNodeByEntityId) === null || _a === void 0 ? void 0 : _a[entityId]) === null || _b === void 0 ? void 0 : _b.parentId;
30775
+ const parent = parentId ? this.composeWorld(parentId, timeMs, memo, localOf) : null;
30776
+ world = parent ? Cesium.Matrix4.multiply(parent, local, new Cesium.Matrix4()) : Cesium.Matrix4.clone(local);
30777
+ }
30778
+ else if (entityId !== this.rootId) {
30779
+ // A node whose own position was not fetched falls back to the API's composed matrix.
30780
+ const child = this.children.get(entityId);
30781
+ world = Manager.MatrixFromRows((_d = (_c = child === null || child === void 0 ? void 0 : child.entity) === null || _c === void 0 ? void 0 : _c.Bruce) === null || _d === void 0 ? void 0 : _d.AssemblyWorldPosition);
30782
+ }
30783
+ memo === null || memo === void 0 ? void 0 : memo.set(entityId, world);
30784
+ return world;
30785
+ }
30786
+ /*
30787
+ * Under a clamped or relative style, lifts a part by how much higher its ground is than the root's.
30788
+ */
30789
+ measureGroundOffset(entity) {
30790
+ var _a;
30791
+ const entityId = (_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.ID;
30792
+ this.groundOffsets.delete(entityId);
30793
+ const root = this.apiEntities.get(this.rootId);
30794
+ if (!entityId ||
30795
+ entityId === this.rootId ||
30796
+ !root ||
30797
+ !this.rootPos3d ||
30798
+ Manager.GetRecordHeightRef(this.item) === Cesium.HeightReference.NONE) {
30799
+ return;
30800
+ }
30801
+ const rootFrame = Manager.rootFrame(this.rootPos3d, this.rootHeading, this.rootPitch, this.rootRoll);
30802
+ const toRoot = Cesium.Matrix4.inverse(rootFrame, new Cesium.Matrix4());
30803
+ const groundUp = (record) => {
30804
+ const pos3d = exports.EntityUtils.GetPos({
30805
+ viewer: this.viewer,
30806
+ entity: record,
30807
+ recordHeightRef: Cesium.HeightReference.CLAMP_TO_GROUND,
30808
+ returnHeightRef: Cesium.HeightReference.NONE,
30809
+ allowRendered: false
30810
+ });
30811
+ return pos3d ? Cesium.Matrix4.multiplyByPoint(toRoot, pos3d, new Cesium.Cartesian3()).z : null;
30812
+ };
30813
+ const part = groundUp(entity);
30814
+ const under = groundUp(root);
30815
+ if (part == null || under == null || Math.abs(part - under) < 1e-6) {
30816
+ return;
30817
+ }
30818
+ this.groundOffsets.set(entityId, new Cesium.Cartesian3(0, 0, part - under));
30819
+ }
30820
+ /*
30821
+ * The rigid Earth-fixed motion from where a part's record places it to where it is drawn at a time.
30822
+ */
30823
+ motionAt(entityId, time) {
30824
+ if (this.disposed || !time || !this.rootPos3d) {
30825
+ return null;
30826
+ }
30827
+ const timeMs = Cesium.JulianDate.toDate(time).getTime();
30828
+ const root = this.rootAt(timeMs);
30829
+ const drawn = this.composeWorld(entityId, timeMs, new Map());
30830
+ const stored = this.composeWorld(entityId, timeMs, new Map(), (id) => {
30831
+ var _a, _b, _c, _d;
30832
+ return ((_d = (_c = Manager.MatrixFromRows((_b = (_a = this.apiEntities.get(id)) === null || _a === void 0 ? void 0 : _a.Bruce) === null || _b === void 0 ? void 0 : _b.AssemblyPosition)) !== null && _c !== void 0 ? _c : this.localPositions.get(id)) !== null && _d !== void 0 ? _d : null);
30833
+ });
30834
+ if (!root || !drawn || !stored) {
30835
+ return null;
30836
+ }
30837
+ const drawnFixed = Cesium.Matrix4.multiply(Manager.rootFrame(root.pos3d, root.heading, root.pitch, root.roll), drawn, new Cesium.Matrix4());
30838
+ const storedFixed = Cesium.Matrix4.multiply(Manager.rootFrame(this.rootPos3d, this.rootHeading, this.rootPitch, this.rootRoll), stored, new Cesium.Matrix4());
30839
+ return Cesium.Matrix4.multiply(drawnFixed, Cesium.Matrix4.inverse(storedFixed, new Cesium.Matrix4()), new Cesium.Matrix4());
30840
+ }
30841
+ /*
30842
+ * A node's pose within its parent at a time, interpolated between the records either side.
30843
+ */
30844
+ localAt(entityId, timeMs) {
30845
+ var _a;
30846
+ // The live pose stands before the first record and the last record holds after it, as in an API read.
30847
+ const live = (_a = this.localPositions.get(entityId)) !== null && _a !== void 0 ? _a : null;
30848
+ const series = this.jointSeries.get(entityId);
30849
+ if (!(series === null || series === void 0 ? void 0 : series.length) || timeMs == null || timeMs < series[0].timeMs) {
30850
+ return live;
30851
+ }
30852
+ let i = 0;
30853
+ while (i < series.length - 1 && series[i + 1].timeMs <= timeMs) {
30854
+ i++;
30855
+ }
30856
+ const a = series[i];
30857
+ const b = series[i + 1];
30858
+ if (!b) {
30859
+ return Cesium.Matrix4.fromTranslationQuaternionRotationScale(a.translation, a.rotation, a.scale);
30860
+ }
30861
+ const t = (timeMs - a.timeMs) / (b.timeMs - a.timeMs);
30862
+ return Cesium.Matrix4.fromTranslationQuaternionRotationScale(Cesium.Cartesian3.lerp(a.translation, b.translation, t, new Cesium.Cartesian3()), Cesium.Quaternion.slerp(a.rotation, b.rotation, t, new Cesium.Quaternion()), Cesium.Cartesian3.lerp(a.scale, b.scale, t, new Cesium.Cartesian3()));
30863
+ }
30864
+ /*
30865
+ * Rebuilds each node's keyframes from the historic records held so far.
30866
+ * @returns whether any node has keyframes.
30867
+ */
30868
+ absorbJointRecords(recordsByIds) {
30869
+ var _a;
30870
+ // Everything held is given, so a node with nothing left, eg. its simulated records cleared, stops moving.
30871
+ for (const entityId of Array.from(this.jointSeries.keys())) {
30872
+ if (!((_a = recordsByIds === null || recordsByIds === void 0 ? void 0 : recordsByIds[entityId]) === null || _a === void 0 ? void 0 : _a.length)) {
30873
+ this.jointSeries.delete(entityId);
30874
+ }
30875
+ }
30876
+ for (const entityId of Object.keys(recordsByIds || {})) {
30877
+ const keyframes = [];
30878
+ for (const record of recordsByIds[entityId] || []) {
30879
+ const matrix = Manager.MatrixFromRows(record.assemblyPosition);
30880
+ const timeMs = new Date(record.dateTime).getTime();
30881
+ if (!matrix || isNaN(timeMs)) {
30882
+ continue;
30883
+ }
30884
+ const scale = Cesium.Matrix4.getScale(matrix, new Cesium.Cartesian3());
30885
+ const rotation = Cesium.Matrix4.getRotation(matrix, new Cesium.Matrix3());
30886
+ keyframes.push({
30887
+ timeMs,
30888
+ translation: Cesium.Matrix4.getTranslation(matrix, new Cesium.Cartesian3()),
30889
+ rotation: Cesium.Quaternion.fromRotationMatrix(rotation),
30890
+ scale
30891
+ });
30892
+ }
30893
+ if (keyframes.length) {
30894
+ keyframes.sort((x, y) => x.timeMs - y.timeMs);
30895
+ this.jointSeries.set(entityId, keyframes);
30896
+ }
30897
+ }
30898
+ this.refreshAnimated();
30899
+ return this.jointSeries.size > 0;
30900
+ }
30901
+ /*
30902
+ * Rebuilds which drawn children can move with the clock, by walking each one up to the root.
30903
+ */
30904
+ refreshAnimated() {
30905
+ this.animatedIds.clear();
30906
+ if (!this.jointSeries.size) {
30907
+ return;
30908
+ }
30909
+ for (const entityId of this.children.keys()) {
30910
+ if (this.hasAnimatedAncestry(entityId)) {
30911
+ this.animatedIds.add(entityId);
29328
30912
  }
29329
30913
  }
29330
- this.viewer.scene.requestRender();
30914
+ }
30915
+ /*
30916
+ * Whether a node or anything above it has keyframes, so the clock can move it.
30917
+ */
30918
+ hasAnimatedAncestry(entityId) {
30919
+ var _a, _b;
30920
+ let at = entityId;
30921
+ while (at && this.jointSeries.size) {
30922
+ if (this.jointSeries.has(at)) {
30923
+ return true;
30924
+ }
30925
+ at = (_b = (_a = this.hierarchyNodeByEntityId) === null || _a === void 0 ? void 0 : _a[at]) === null || _b === void 0 ? void 0 : _b.parentId;
30926
+ }
30927
+ return false;
30928
+ }
30929
+ /*
30930
+ * Which nodes to ask history for.
30931
+ */
30932
+ async historicEntityIds(api) {
30933
+ var _a, _b;
30934
+ // Through the Assembly when there is one, so the hierarchy follows the relation type on its record.
30935
+ const assemblyId = this.item["Assembly.ID"];
30936
+ const root = assemblyId ? { key: "root", AssemblyID: assemblyId } : { key: "root", RootID: this.rootId };
30937
+ const historic = {
30938
+ key: "historic",
30939
+ Historic: { HistoricKey: ((_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historicAttrKey) || undefined }
30940
+ };
30941
+ const query = {
30942
+ [BModels.DataLab.EReqKey.Primary]: { Items: [root, historic], Select: { Type: BModels.DataLab.ESelectType.Id } }
30943
+ };
30944
+ const load = 1000;
30945
+ try {
30946
+ const ids = [];
30947
+ for (let skip = 0;; skip += load) {
30948
+ const { items } = await BModels.DataLab.Run({ api, query, skip, load });
30949
+ ids.push(...(items !== null && items !== void 0 ? items : []));
30950
+ if (!items || items.length < load) {
30951
+ return ids;
30952
+ }
30953
+ }
30954
+ }
30955
+ catch (e) {
30956
+ // An API without the historic criterion gets asked about every node, as before it existed.
30957
+ console.warn("AssemblyRenderManager.historicEntityIds()", e);
30958
+ }
30959
+ return Object.keys((_b = this.hierarchyNodeByEntityId) !== null && _b !== void 0 ? _b : { [this.rootId]: null });
30960
+ }
30961
+ /**
30962
+ * Builds a matrix from the row-major rows that Bruce/AssemblyPosition and AssemblyWorldPosition use.
30963
+ * @param rows
30964
+ */
30965
+ static MatrixFromRows(rows) {
30966
+ if (!Array.isArray(rows) || rows.length !== 4 || rows.some(r => !Array.isArray(r) || r.length !== 4)) {
30967
+ return null;
30968
+ }
30969
+ return new Cesium.Matrix4(+rows[0][0], +rows[0][1], +rows[0][2], +rows[0][3], +rows[1][0], +rows[1][1], +rows[1][2], +rows[1][3], +rows[2][0], +rows[2][1], +rows[2][2], +rows[2][3], +rows[3][0], +rows[3][1], +rows[3][2], +rows[3][3]);
30970
+ }
30971
+ /**
30972
+ * The inverse of MatrixFromRows.
30973
+ * @param matrix
30974
+ */
30975
+ static RowsFromMatrix(matrix) {
30976
+ const rows = [];
30977
+ for (let r = 0; r < 4; r++) {
30978
+ const row = Cesium.Matrix4.getRow(matrix, r, new Cesium.Cartesian4());
30979
+ rows.push([row.x, row.y, row.z, row.w]);
30980
+ }
30981
+ return rows;
29331
30982
  }
29332
30983
  Init() {
29333
30984
  this.renderPriority = this.item.renderPriority;
@@ -29335,6 +30986,7 @@
29335
30986
  this.renderPriority = 1;
29336
30987
  }
29337
30988
  (async () => {
30989
+ var _a, _b;
29338
30990
  if (this.disposed) {
29339
30991
  return;
29340
30992
  }
@@ -29350,6 +31002,8 @@
29350
31002
  this.hierarchyUpdate.Trigger(hierarchy);
29351
31003
  // Build hierarchy node cache for quick lookups
29352
31004
  this.buildHierarchyNodes(hierarchy);
31005
+ // A set for membership, since searching the list took minutes at 200k nodes.
31006
+ const seenIds = new Set();
29353
31007
  const traverseHierarchy = (node, leavesOnly) => {
29354
31008
  var _a;
29355
31009
  if (node.Children) {
@@ -29358,7 +31012,8 @@
29358
31012
  }
29359
31013
  }
29360
31014
  const addNode = !leavesOnly || !((_a = node.Children) === null || _a === void 0 ? void 0 : _a.length);
29361
- if (addNode && node.ID && !flatEntityIds.includes(node.ID)) {
31015
+ if (addNode && node.ID && !seenIds.has(node.ID)) {
31016
+ seenIds.add(node.ID);
29362
31017
  flatEntityIds.push(node.ID);
29363
31018
  }
29364
31019
  };
@@ -29377,17 +31032,17 @@
29377
31032
  }
29378
31033
  }
29379
31034
  this.rootId = rootId;
31035
+ this.simulationRemoval = (_b = (_a = this.simulation) === null || _a === void 0 ? void 0 : _a.OnChanged.Subscribe((change) => this.onSimulationChanged(change))) !== null && _b !== void 0 ? _b : null;
29380
31036
  if (this.disposed) {
29381
31037
  return;
29382
31038
  }
29383
31039
  else if (flatEntityIds.length === 0) {
29384
31040
  return;
29385
31041
  }
31042
+ // Followed while the parts load: in a large Assembly loading takes far longer than anyone waits.
31043
+ const following = this.viewerDateTimeSub();
29386
31044
  await this.processEntityIds(flatEntityIds);
29387
- if (this.disposed) {
29388
- return;
29389
- }
29390
- this.viewerDateTimeSub();
31045
+ await following;
29391
31046
  }
29392
31047
  catch (e) {
29393
31048
  console.error("AssemblyRenderManager.Manager.Init()", e);
@@ -29414,7 +31069,7 @@
29414
31069
  if (!batch.length) {
29415
31070
  continue;
29416
31071
  }
29417
- const { entities: fetched } = await BModels.Entity.GetListByIds({
31072
+ const { entities: apiEntities } = await BModels.Entity.GetListByIds({
29418
31073
  entityIds: batch,
29419
31074
  api: this.getters.GetBruceApi(),
29420
31075
  expandLODs: true,
@@ -29426,6 +31081,16 @@
29426
31081
  if (this.disposed) {
29427
31082
  return;
29428
31083
  }
31084
+ const fetched = apiEntities.map((entity) => {
31085
+ var _a;
31086
+ this.apiEntities.set((_a = entity.Bruce) === null || _a === void 0 ? void 0 : _a.ID, entity);
31087
+ return this.withSimulation(entity);
31088
+ });
31089
+ for (const entity of fetched) {
31090
+ this.absorbNode(entity);
31091
+ }
31092
+ // Placements already worked out did not know about this batch's positions.
31093
+ this.pose = null;
29429
31094
  for (const entity of fetched) {
29430
31095
  // Filter for valid LODs then take the highest quality one (lowest level).
29431
31096
  let lods = entity.Bruce.LOD || [];
@@ -29448,57 +31113,46 @@
29448
31113
  // Won't bother with vector data for this experiment.
29449
31114
  continue;
29450
31115
  }
29451
- const recordHeightRef = Manager.GetRecordHeightRef(this.item);
29452
- const recordPos3d = exports.EntityUtils.GetPos({
29453
- viewer: this.viewer,
29454
- entity,
29455
- recordHeightRef: recordHeightRef,
29456
- returnHeightRef: Cesium.HeightReference.NONE,
29457
- allowRendered: false
29458
- });
29459
- if (!recordPos3d) {
31116
+ const entityId = entity.Bruce.ID;
31117
+ this.measureGroundOffset(entity);
31118
+ // A placement cached before the offset was known would leave the part off its ground.
31119
+ (_a = this.pose) === null || _a === void 0 ? void 0 : _a.placements.delete(entityId);
31120
+ if (!this.canMove(entityId) && !this.placementAt(entityId, this.viewer.clock.currentTime)) {
29460
31121
  continue;
29461
31122
  }
29462
- const entityId = entity.Bruce.ID;
29463
- if (entityId === this.rootId || !this.rootPos3d) {
29464
- // Nothing has a frame to be relative to until the root has one.
29465
- this.rootPos3d = recordPos3d;
29466
- this.rootHeading = EnsureNumber((_a = entity.Bruce.Transform) === null || _a === void 0 ? void 0 : _a.heading, 0);
29467
- }
29468
- // Held as an offset in the root's frame, so a root move places it again without
29469
- // anything being fetched. Where the root already moved, that is where it goes.
29470
- const offset = Manager.ChildOffsetIn(this.rootPos3d, this.rootHeading, recordPos3d);
29471
- const pos3d = Manager.ChildPositionFrom(this.rootPos3d, this.rootHeading, offset);
29472
- // Optional: a child with no world matrix is still worth drawing, it just has no
29473
- // orientation of its own to apply.
29474
- const orient = exports.EntityUtils.GetAssemblyOrientation(entity, pos3d);
29475
31123
  const cEntity = this.viewer.entities.add({
29476
31124
  model: {
29477
31125
  uri: lod["ClientFile"].URL,
29478
31126
  ...exports.ViewUtils.DefaultLightingProps(),
29479
31127
  heightReference: Cesium.HeightReference.NONE
29480
- },
29481
- position: pos3d,
29482
- orientation: orient ? new Cesium.ConstantProperty(orient) : undefined
31128
+ }
29483
31129
  });
29484
31130
  const prior = this.children.get(entityId);
29485
31131
  if (prior && prior.cEntity !== cEntity) {
29486
31132
  RemoveCEntityFromScene(this.viewer, prior.cEntity);
29487
31133
  }
29488
- this.children.set(entityId, {
31134
+ const child = {
29489
31135
  entityId: entityId,
29490
31136
  entity: entity,
29491
- cEntity: cEntity,
29492
- offset: offset
29493
- });
31137
+ cEntity: cEntity
31138
+ };
31139
+ this.children.set(entityId, child);
31140
+ // Joins the animated set by its own ancestry, so a late part never re-walks the rest.
31141
+ if (this.hasAnimatedAncestry(entityId)) {
31142
+ this.animatedIds.add(entityId);
31143
+ }
29494
31144
  const geometryRadius = BModels.Entity.GetValue({
29495
31145
  entity: entity,
29496
31146
  path: ["Bruce", "GeometryRadius"]
29497
31147
  });
29498
31148
  cEntity.model["_radius"] = geometryRadius;
29499
31149
  cEntity.model["_radiusLoaded"] = true;
31150
+ // Placed before it is registered, so the register never measures a visual with no position.
31151
+ this.bindPlacement(child);
29500
31152
  this.visualsManager.AddRego({
29501
31153
  rego: {
31154
+ moving: child.moving ? true : null,
31155
+ motion: (time) => this.motionAt(entity.Bruce.ID, time),
29502
31156
  canEdit: true,
29503
31157
  entityId: entity.Bruce.ID,
29504
31158
  menuItemId: this.item.id,
@@ -29518,47 +31172,89 @@
29518
31172
  }
29519
31173
  }
29520
31174
  /*
29521
- * Follows the scene clock and moves the whole Assembly along the root Entity's historic path.
29522
- * Only the root is fetched: everything under it is placed through ApplyRootPosition.
31175
+ * Follows the scene clock, moving the Assembly along the root's history and each joint along its own.
29523
31176
  */
29524
- viewerDateTimeSub() {
29525
- if (!Manager.IsHistoric(this.item) || this.viewerDateTimeChangeRemoval || !this.rootId) {
31177
+ async viewerDateTimeSub() {
31178
+ var _a;
31179
+ const historic = Manager.IsHistoric(this.item);
31180
+ if (this.followingClock || !this.rootId || (!historic && !this.simulatedNodeIds().length)) {
29526
31181
  return;
29527
31182
  }
31183
+ this.followingClock = true;
29528
31184
  const api = this.getters.GetBruceApi();
29529
- const rootEntity = { Bruce: { ID: this.rootId } };
31185
+ const historicIds = historic ? await this.historicEntityIds(api) : [];
31186
+ if (this.disposed) {
31187
+ return;
31188
+ }
31189
+ const attrKey = (_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historicAttrKey;
29530
31190
  const recordHeightRef = Manager.GetRecordHeightRef(this.item);
29531
31191
  const toPosses = (records) => {
29532
31192
  return exports.CesiumAnimatedProperty.GetSeriesPossesForHistoricEntity(this.viewer, recordHeightRef, Cesium.HeightReference.NONE, records);
29533
31193
  };
29534
- this.historicGatherer = new HistoricGatherer({
29535
- viewer: this.viewer,
29536
- getApi: () => api,
29537
- getAttrKey: () => { var _a; return (_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historicAttrKey; },
29538
- // For clock desync between whatever wrote the record and this client.
29539
- paddingMs: 15000,
29540
- onRecords: (recordsByIds) => {
29541
- const records = recordsByIds[this.rootId];
29542
- if ((records === null || records === void 0 ? void 0 : records.length) && this.historicAnimation) {
29543
- this.historicAnimation.SupplementSeries(toPosses(records));
31194
+ if (historicIds.length) {
31195
+ this.historicGatherer = new HistoricGatherer({
31196
+ viewer: this.viewer,
31197
+ getApi: () => api,
31198
+ getAttrKey: () => attrKey,
31199
+ // For clock desync between whatever wrote the record and this client.
31200
+ paddingMs: 15000,
31201
+ simulation: this.simulation,
31202
+ onRecords: (recordsByIds) => {
31203
+ var _a;
31204
+ const records = recordsByIds[this.rootId];
31205
+ if (records === null || records === void 0 ? void 0 : records.length) {
31206
+ const posses = toPosses(records);
31207
+ (_a = this.historicAnimation) === null || _a === void 0 ? void 0 : _a.SupplementSeries(posses);
31208
+ const moved = this.absorbRootRecords(posses, false);
31209
+ if (this.absorbRootAttitude(records, false) || moved) {
31210
+ this.repose();
31211
+ }
31212
+ }
31213
+ // Joint records go in together once the gather lands, so parents never draw apart from children.
29544
31214
  }
29545
- }
29546
- });
31215
+ });
31216
+ }
31217
+ // The nodes with history: what the API has records for, and whatever has been simulated since.
31218
+ const nodes = () => Array.from(new Set([...historicIds, ...this.simulatedNodeIds()]));
29547
31219
  let gathering = false;
29548
- const gather = async () => {
31220
+ // Set when what is held changed without a fetch, eg. a simulated record arriving.
31221
+ let stale = true;
31222
+ const gather = async (force) => {
31223
+ var _a, _b;
31224
+ stale = stale || Boolean(force);
29549
31225
  if (this.disposed || gathering) {
29550
31226
  return;
29551
31227
  }
29552
31228
  gathering = true;
29553
31229
  try {
29554
- const { recordsByIds } = await this.historicGatherer.Gather([rootEntity]);
31230
+ const ids = nodes();
31231
+ let fetched = false;
31232
+ let recordsByIds;
31233
+ if (this.historicGatherer) {
31234
+ ({ fetched, recordsByIds } = await this.historicGatherer.Gather(ids.map(id => ({ Bruce: { ID: id } }))));
31235
+ }
31236
+ else {
31237
+ recordsByIds = (_b = (_a = this.simulation) === null || _a === void 0 ? void 0 : _a.MergeRecords({}, attrKey, ids)) !== null && _b !== void 0 ? _b : {};
31238
+ }
29555
31239
  if (this.disposed) {
29556
31240
  return;
29557
31241
  }
31242
+ // A gather served from what is held changes nothing and runs every tick, so it stops here.
31243
+ if (!fetched && !stale && this.jointSeries.size) {
31244
+ return;
31245
+ }
31246
+ stale = false;
31247
+ let changed = this.absorbJointRecords(recordsByIds);
29558
31248
  const posses = toPosses(recordsByIds[this.rootId] || []);
31249
+ changed = this.absorbRootRecords(posses, true) || changed;
31250
+ changed = this.absorbRootAttitude(recordsByIds[this.rootId] || [], true) || changed;
31251
+ if (changed) {
31252
+ this.repose();
31253
+ }
29559
31254
  if (!posses.length) {
29560
31255
  return;
29561
31256
  }
31257
+ // Held for the trail alone; where the root is drawn comes from rootAt(), never from its smoothing.
29562
31258
  if (!this.historicAnimation) {
29563
31259
  this.historicAnimation = new exports.CesiumAnimatedProperty.AnimatePositionSeries({
29564
31260
  posses: posses,
@@ -29572,15 +31268,21 @@
29572
31268
  }
29573
31269
  catch (e) {
29574
31270
  console.error("AssemblyRenderManager.viewerDateTimeSub()", e);
31271
+ // A gather that throws would throw again straight away, so the retry waits for the next tick.
31272
+ stale = false;
29575
31273
  }
29576
31274
  finally {
29577
31275
  gathering = false;
31276
+ // A change that landed mid-gather is not left for a tick that may never come on a paused clock.
31277
+ if (stale && !this.disposed) {
31278
+ gather();
31279
+ }
29578
31280
  }
29579
31281
  };
31282
+ this.regather = gather;
29580
31283
  gather();
29581
31284
  let lastTime = null;
29582
31285
  const tickRemoval = this.viewer.clock.onTick.addEventListener(() => {
29583
- var _a;
29584
31286
  if (this.disposed) {
29585
31287
  return;
29586
31288
  }
@@ -29590,15 +31292,9 @@
29590
31292
  }
29591
31293
  lastTime = now;
29592
31294
  gather();
29593
- if (!this.historicAnimation) {
29594
- return;
29595
- }
29596
- const pos3d = this.historicAnimation.GetValue();
29597
- if (pos3d) {
29598
- this.ApplyRootPosition({
29599
- pos3d: pos3d,
29600
- heading: (_a = this.historicAnimation.GetHeadingDegrees()) !== null && _a !== void 0 ? _a : this.rootHeading
29601
- });
31295
+ // Moving parts read the clock themselves when drawn, so the scene only needs a new frame.
31296
+ if (this.rootSeries.length || this.rootAttitude.length || this.animatedIds.size) {
31297
+ this.viewer.scene.requestRender();
29602
31298
  }
29603
31299
  });
29604
31300
  this.viewerDateTimeChangeRemoval = () => {
@@ -29607,6 +31303,10 @@
29607
31303
  (_a = this.historicGatherer) === null || _a === void 0 ? void 0 : _a.Dispose();
29608
31304
  this.historicGatherer = null;
29609
31305
  this.historicAnimation = null;
31306
+ this.regather = null;
31307
+ this.rootSeries = [];
31308
+ this.rootAttitude = [];
31309
+ this.pose = null;
29610
31310
  };
29611
31311
  }
29612
31312
  /*
@@ -29614,7 +31314,7 @@
29614
31314
  */
29615
31315
  applyRootTrack() {
29616
31316
  var _a;
29617
- if (!this.item.historicDrawTrack || !this.historicAnimation || this.trackEntity) {
31317
+ if (!this.item.historicDrawTrack || !this.historicAnimation || this.trail) {
29618
31318
  return;
29619
31319
  }
29620
31320
  const trackStyle = exports.EntityRenderEngine.GetTrackStyle({
@@ -29624,41 +31324,35 @@
29624
31324
  if (!trackStyle) {
29625
31325
  return;
29626
31326
  }
29627
- this.trackEntity = this.viewer.entities.add({
29628
- id: BModels.ObjectUtils.UId(15),
29629
- polyline: {
29630
- positions: new Cesium.CallbackProperty(() => { var _a, _b; return (_b = (_a = this.historicAnimation) === null || _a === void 0 ? void 0 : _a.GetSeries()) !== null && _b !== void 0 ? _b : []; }, false),
29631
- width: trackStyle.width,
29632
- material: trackStyle.color,
29633
- clampToGround: exports.EntityRenderEngine.GetHeightRefPolyline(Manager.GetRecordHeightRef(this.item))
29634
- == Cesium.HeightReference.CLAMP_TO_GROUND,
29635
- zIndex: 0,
29636
- classificationType: Cesium.ClassificationType.BOTH
29637
- }
29638
- });
29639
- this.trackEntity._noEdit = true;
29640
- this.trackEntity._noSelect = true;
29641
- exports.CesiumEntityStyler.SetDefaultColor({
29642
- color: trackStyle.color,
29643
- entity: this.trackEntity,
31327
+ this.trail = new HistoricTrail.Trail({
29644
31328
  viewer: this.viewer,
29645
- override: true,
29646
- requestRender: false
31329
+ color: trackStyle.color,
31330
+ width: trackStyle.width,
31331
+ clampToGround: exports.EntityRenderEngine.GetHeightRefPolyline(Manager.GetRecordHeightRef(this.item))
31332
+ == Cesium.HeightReference.CLAMP_TO_GROUND,
31333
+ getSeries: () => { var _a, _b; return (_b = (_a = this.historicAnimation) === null || _a === void 0 ? void 0 : _a.GetSeries()) !== null && _b !== void 0 ? _b : []; },
31334
+ trailSeconds: HistoricTrail.GetTrailSeconds(this.item),
31335
+ // Where the root is drawn, so the trail ends on the model; nothing before the first record held.
31336
+ positionAt: (timeMs) => (this.rootSeries.length && timeMs >= this.rootSeries[0].timeMs ? this.rootPositionAt(timeMs) : null)
29647
31337
  });
29648
31338
  }
29649
31339
  Dispose() {
29650
- var _a;
31340
+ var _a, _b, _c;
29651
31341
  if (this.disposed) {
29652
31342
  return;
29653
31343
  }
29654
31344
  this.disposed = true;
29655
31345
  (_a = this.viewerDateTimeChangeRemoval) === null || _a === void 0 ? void 0 : _a.call(this);
29656
31346
  this.viewerDateTimeChangeRemoval = null;
29657
- if (this.trackEntity) {
29658
- RemoveCEntityFromScene(this.viewer, this.trackEntity);
29659
- this.trackEntity = null;
29660
- }
31347
+ (_b = this.simulationRemoval) === null || _b === void 0 ? void 0 : _b.call(this);
31348
+ this.simulationRemoval = null;
31349
+ this.apiEntities.clear();
31350
+ (_c = this.trail) === null || _c === void 0 ? void 0 : _c.Dispose();
31351
+ this.trail = null;
29661
31352
  this.children.clear();
31353
+ this.localPositions.clear();
31354
+ this.jointSeries.clear();
31355
+ this.animatedIds.clear();
29662
31356
  this.visualsManager.RemoveRegos({
29663
31357
  menuItemId: this.item.id
29664
31358
  });
@@ -29881,6 +31575,149 @@
29881
31575
  AssemblyRenderManager.Manager = Manager;
29882
31576
  })(exports.AssemblyRenderManager || (exports.AssemblyRenderManager = {}));
29883
31577
 
31578
+ (function (MenuItemStyles) {
31579
+ // Tileset items whose render manager applies styleId through a Styler.
31580
+ const TILESET_TYPES = [
31581
+ BModels.MenuItem.EType.CadTileset,
31582
+ BModels.MenuItem.EType.ArbTileset,
31583
+ BModels.MenuItem.EType.IonTileset,
31584
+ BModels.MenuItem.EType.EntityTileset
31585
+ ];
31586
+ // Items whose style lives on their zoom control rows.
31587
+ const ZOOM_CONTROL_TYPES = [
31588
+ BModels.MenuItem.EType.Entities,
31589
+ BModels.MenuItem.EType.Entity,
31590
+ BModels.MenuItem.EType.EntitiesIds,
31591
+ BModels.MenuItem.EType.EntitiesLoaded,
31592
+ BModels.MenuItem.EType.DataLab
31593
+ ];
31594
+ /**
31595
+ * Returns the ordered styles a user can swap between for the item, or an empty array when it offers no choice.
31596
+ */
31597
+ function GetStyleIds(item) {
31598
+ var _a;
31599
+ if (isStyledTileset(item)) {
31600
+ return cleanIds(item.styleIds);
31601
+ }
31602
+ if (ZOOM_CONTROL_TYPES.includes(item === null || item === void 0 ? void 0 : item.Type)) {
31603
+ return cleanIds((_a = getVisibleRow(item.CameraZoomSettings)) === null || _a === void 0 ? void 0 : _a.StyleIDs);
31604
+ }
31605
+ return [];
31606
+ }
31607
+ MenuItemStyles.GetStyleIds = GetStyleIds;
31608
+ /**
31609
+ * Returns the style the item currently renders with, from styleId or its one non-hidden zoom row.
31610
+ */
31611
+ function GetStyleId(item) {
31612
+ var _a;
31613
+ if (TILESET_TYPES.includes(item === null || item === void 0 ? void 0 : item.Type)) {
31614
+ return item.styleId;
31615
+ }
31616
+ if (ZOOM_CONTROL_TYPES.includes(item === null || item === void 0 ? void 0 : item.Type)) {
31617
+ return (_a = getVisibleRow(item.CameraZoomSettings)) === null || _a === void 0 ? void 0 : _a.StyleID;
31618
+ }
31619
+ return undefined;
31620
+ }
31621
+ MenuItemStyles.GetStyleId = GetStyleId;
31622
+ /**
31623
+ * Returns a copy of the item rendering with the given style, or null when that style is not one of its options.
31624
+ */
31625
+ function WithStyleId(item, styleId) {
31626
+ const styleIds = GetStyleIds(item);
31627
+ const picked = styleIds.find(x => isSameId(x, styleId));
31628
+ if (picked == null) {
31629
+ return null;
31630
+ }
31631
+ if (isStyledTileset(item)) {
31632
+ return { ...item, styleId: picked };
31633
+ }
31634
+ const zItem = item;
31635
+ const visible = getVisibleRow(zItem.CameraZoomSettings);
31636
+ return {
31637
+ ...zItem,
31638
+ CameraZoomSettings: zItem.CameraZoomSettings.map(row => {
31639
+ if (row !== visible) {
31640
+ return row;
31641
+ }
31642
+ // An inline style would win over the picked ID.
31643
+ const next = { ...row, StyleID: picked };
31644
+ delete next.Style;
31645
+ return next;
31646
+ })
31647
+ };
31648
+ }
31649
+ MenuItemStyles.WithStyleId = WithStyleId;
31650
+ /**
31651
+ * Returns the item on the first of its styles unless it already uses one, or the same object when it has none.
31652
+ */
31653
+ function Resolve(item) {
31654
+ var _a;
31655
+ const styleIds = GetStyleIds(item);
31656
+ if (!styleIds.length) {
31657
+ return item;
31658
+ }
31659
+ const current = GetStyleId(item);
31660
+ if (current != null && styleIds.some(x => isSameId(x, current))) {
31661
+ return item;
31662
+ }
31663
+ return (_a = WithStyleId(item, styleIds[0])) !== null && _a !== void 0 ? _a : item;
31664
+ }
31665
+ MenuItemStyles.Resolve = Resolve;
31666
+ /**
31667
+ * Returns a copy on the style a bookmark alternation picked, or the item itself when that is not an option.
31668
+ */
31669
+ function ApplyAlternation(item, alternation) {
31670
+ var _a, _b, _c;
31671
+ // Alternations are partial items and may carry no Type, so the pick is read from whichever field is present.
31672
+ const alt = alternation;
31673
+ const picked = (_a = alt === null || alt === void 0 ? void 0 : alt.styleId) !== null && _a !== void 0 ? _a : (_b = getVisibleRow(alt === null || alt === void 0 ? void 0 : alt.CameraZoomSettings)) === null || _b === void 0 ? void 0 : _b.StyleID;
31674
+ if (picked == null) {
31675
+ return item;
31676
+ }
31677
+ return (_c = WithStyleId(item, picked)) !== null && _c !== void 0 ? _c : item;
31678
+ }
31679
+ MenuItemStyles.ApplyAlternation = ApplyAlternation;
31680
+ /**
31681
+ * Returns true when the two items differ by nothing but which of their styles is picked.
31682
+ */
31683
+ function IsStyleChangeOnly(from, to) {
31684
+ if (!from || !to || from.Type !== to.Type) {
31685
+ return false;
31686
+ }
31687
+ const toStyleId = GetStyleId(to);
31688
+ if (toStyleId == null || isSameId(GetStyleId(from), toStyleId)) {
31689
+ return false;
31690
+ }
31691
+ const moved = WithStyleId(from, toStyleId);
31692
+ // A false mismatch only costs the in-place swap, so key order is not normalized.
31693
+ return Boolean(moved) && JSON.stringify(moved) === JSON.stringify(to);
31694
+ }
31695
+ MenuItemStyles.IsStyleChangeOnly = IsStyleChangeOnly;
31696
+ function isStyledTileset(item) {
31697
+ if (!TILESET_TYPES.includes(item === null || item === void 0 ? void 0 : item.Type)) {
31698
+ return false;
31699
+ }
31700
+ // These two only run a Styler when asked to.
31701
+ if (item.Type === BModels.MenuItem.EType.ArbTileset || item.Type === BModels.MenuItem.EType.EntityTileset) {
31702
+ return Boolean(item.ApplyStyles);
31703
+ }
31704
+ return true;
31705
+ }
31706
+ function getVisibleRow(rows) {
31707
+ const visible = (rows !== null && rows !== void 0 ? rows : []).filter(x => x && x.DisplayType !== BModels.ZoomControl.EDisplayType.Hidden);
31708
+ return visible.length === 1 ? visible[0] : null;
31709
+ }
31710
+ function cleanIds(ids) {
31711
+ return (ids !== null && ids !== void 0 ? ids : []).filter(x => x != null && x !== "");
31712
+ }
31713
+ /*
31714
+ * Compares style references, which can be an ID typed as a string or a number.
31715
+ */
31716
+ function isSameId(a, b) {
31717
+ return a != null && b != null && String(a) === String(b);
31718
+ }
31719
+ })(exports.MenuItemStyles || (exports.MenuItemStyles = {}));
31720
+
29884
31721
  /**
29885
31722
  * Util to batch Tileset and Root Entity coord requests between Menu Items.
29886
31723
  * This avoids per-menu-item requests and allows for them to request together if enabled within N ms of each other.
@@ -30112,12 +31949,17 @@
30112
31949
  this.OnSeriesUpdated.Trigger({ itemId, segments: [] });
30113
31950
  }
30114
31951
  }
31952
+ get Simulation() {
31953
+ return this.simulation;
31954
+ }
30115
31955
  get Monitor() {
30116
31956
  return this.sharedMonitor;
30117
31957
  }
30118
31958
  constructor(params) {
30119
31959
  this.items = [];
30120
31960
  this.pendingHandoffPool = [];
31961
+ // The settings bookmark alternations overwrite on a view's items, as that view authored them.
31962
+ this.authoredAlternations = new WeakMap();
30121
31963
  this.onUpdate = null;
30122
31964
  // Segments discovered so far per menu item ID.
30123
31965
  this.seriesSegments = new Map();
@@ -30137,6 +31979,7 @@
30137
31979
  this.visualsRegister = visualsRegister;
30138
31980
  this.sharedGetters = new exports.SharedGetters.Cache();
30139
31981
  this.sharedHistoric = new SharedHistoricGetters.Cache();
31982
+ this.simulation = exports.EntitySimulation.Get(this.viewer);
30140
31983
  this.sharedMonitor = new exports.CesiumViewMonitor.Monitor(this.viewer);
30141
31984
  this.tilesetInitQueue = new InitQueueTileset(this.getters);
30142
31985
  }
@@ -30172,7 +32015,7 @@
30172
32015
  * @returns
30173
32016
  */
30174
32017
  RenderItem(params) {
30175
- var _a, _b, _c, _d, _e, _f, _g;
32018
+ var _a, _b, _c, _d, _e, _f, _g, _h;
30176
32019
  if (this.viewer.isDestroyed()) {
30177
32020
  return null;
30178
32021
  }
@@ -30196,13 +32039,21 @@
30196
32039
  if (params.modelSpace == null) {
30197
32040
  params.modelSpace = false;
30198
32041
  }
32042
+ // An item with a style set renders its first style unless the caller already picked one of them.
32043
+ params = { ...params, item: exports.MenuItemStyles.Resolve(params.item) };
30199
32044
  let rItem = this.items.find(x => x.id == params.item.id);
30200
32045
  let create = true;
30201
32046
  // Item already exists so we'll check if we should re-create it.
30202
32047
  if (rItem) {
32048
+ // Only the picked style changed, so what is drawn gets restyled rather than rebuilt.
32049
+ if (exports.MenuItemStyles.IsStyleChangeOnly(rItem.item, params.item) && this.isStyleSwapCapable(rItem.renderManager)) {
32050
+ rItem.renderManager.SwapStyle(params.item);
32051
+ rItem.item = params.item;
32052
+ create = false;
32053
+ }
30203
32054
  // This means we're updating an entities menu item re-using the same id.
30204
32055
  // Eg: a bookmark swap that keeps the same item but changes tags, style, or zoom settings.
30205
- if (rItem.type == BModels.MenuItem.EType.Entities && params.item.Type == BModels.MenuItem.EType.Entities) {
32056
+ else if (rItem.type == BModels.MenuItem.EType.Entities && params.item.Type == BModels.MenuItem.EType.Entities) {
30206
32057
  rItem.renderManager.Init({
30207
32058
  item: params.item,
30208
32059
  });
@@ -30350,7 +32201,8 @@
30350
32201
  monitor: this.sharedMonitor,
30351
32202
  item: params.item,
30352
32203
  sharedGetters: this.sharedGetters,
30353
- onSeriesDiscovered: (segment) => this.RecordSeriesSegment(rItem.id, segment)
32204
+ onSeriesDiscovered: (segment) => this.RecordSeriesSegment(rItem.id, segment),
32205
+ simulation: this.simulation
30354
32206
  });
30355
32207
  break;
30356
32208
  case BModels.MenuItem.EType.EntitiesLoaded:
@@ -30359,7 +32211,8 @@
30359
32211
  register: this.visualsRegister,
30360
32212
  apiGetter: params.apiGetter,
30361
32213
  monitor: this.sharedMonitor,
30362
- item: params.item
32214
+ item: params.item,
32215
+ simulation: this.simulation
30363
32216
  });
30364
32217
  break;
30365
32218
  case BModels.MenuItem.EType.EntitiesIds:
@@ -30369,7 +32222,8 @@
30369
32222
  apiGetter: params.apiGetter,
30370
32223
  monitor: this.sharedMonitor,
30371
32224
  item: params.item,
30372
- sharedHistoric: this.sharedHistoric
32225
+ sharedHistoric: this.sharedHistoric,
32226
+ simulation: this.simulation
30373
32227
  });
30374
32228
  break;
30375
32229
  case BModels.MenuItem.EType.Entity:
@@ -30378,7 +32232,8 @@
30378
32232
  register: this.visualsRegister,
30379
32233
  apiGetter: params.apiGetter,
30380
32234
  monitor: this.sharedMonitor,
30381
- item: params.item
32235
+ item: params.item,
32236
+ simulation: this.simulation
30382
32237
  });
30383
32238
  break;
30384
32239
  case BModels.MenuItem.EType.CadTileset:
@@ -30388,7 +32243,8 @@
30388
32243
  getters: this.getters,
30389
32244
  item: params.item,
30390
32245
  modelSpace: params.modelSpace,
30391
- initQueue: this.tilesetInitQueue
32246
+ initQueue: this.tilesetInitQueue,
32247
+ simulation: this.simulation
30392
32248
  });
30393
32249
  break;
30394
32250
  case BModels.MenuItem.EType.Assembly:
@@ -30398,6 +32254,7 @@
30398
32254
  getters: params.getters,
30399
32255
  item: params.item,
30400
32256
  modelSpace: params.modelSpace,
32257
+ simulation: this.simulation
30401
32258
  });
30402
32259
  break;
30403
32260
  case BModels.MenuItem.EType.Osm:
@@ -30425,7 +32282,8 @@
30425
32282
  getters: this.getters,
30426
32283
  monitor: this.sharedMonitor,
30427
32284
  item: params.item,
30428
- initQueue: this.tilesetInitQueue
32285
+ initQueue: this.tilesetInitQueue,
32286
+ simulation: this.simulation
30429
32287
  });
30430
32288
  break;
30431
32289
  case BModels.MenuItem.EType.Kml:
@@ -30449,7 +32307,8 @@
30449
32307
  getters: params.getters,
30450
32308
  item: params.item,
30451
32309
  viewer: this.viewer,
30452
- register: this.visualsRegister
32310
+ register: this.visualsRegister,
32311
+ simulation: this.simulation
30453
32312
  });
30454
32313
  break;
30455
32314
  // Deprecated to support a few demos until new one is phased in.
@@ -30459,7 +32318,8 @@
30459
32318
  apiGetter: params.apiGetter,
30460
32319
  item: params.item,
30461
32320
  register: this.visualsRegister,
30462
- viewer: this.viewer
32321
+ viewer: this.viewer,
32322
+ simulation: this.simulation
30463
32323
  });
30464
32324
  break;
30465
32325
  case BModels.MenuItem.EType.Relationships:
@@ -30467,7 +32327,8 @@
30467
32327
  apiGetter: params.apiGetter,
30468
32328
  item: params.item,
30469
32329
  register: this.visualsRegister,
30470
- viewer: this.viewer
32330
+ viewer: this.viewer,
32331
+ simulation: this.simulation
30471
32332
  });
30472
32333
  break;
30473
32334
  case BModels.MenuItem.EType.GooglePhotoTileset:
@@ -30499,7 +32360,8 @@
30499
32360
  item: params.item,
30500
32361
  apiGetter: params.apiGetter,
30501
32362
  monitor: this.sharedMonitor,
30502
- visualsRegister: this.visualsRegister
32363
+ visualsRegister: this.visualsRegister,
32364
+ simulation: this.simulation
30503
32365
  });
30504
32366
  break;
30505
32367
  // Any "none" type can be ignored. These are just containers for other items.
@@ -30526,7 +32388,17 @@
30526
32388
  // We no longer support folder items being both a folder and an action.
30527
32389
  // Now an item must have no action to be a folder.
30528
32390
  const isFolder = !params.item.Type || params.item.Type == BModels.MenuItem.EType.None;
30529
- if (params.item.Children && params.recursive != false && isFolder) {
32391
+ const isRadioFolder = isFolder && Boolean(params.item.childrenAsRadios);
32392
+ if (((_f = params.item.Children) === null || _f === void 0 ? void 0 : _f.length) && params.recursive != false && isRadioFolder) {
32393
+ const picked = this.pickRadioChild(params.item.Children, params.preferredIds);
32394
+ const others = params.item.Children.filter(x => x !== picked && this.items.some(y => y.id === x.id));
32395
+ const childId = this.renderInPlaceOf(others.map(x => x.id), () => this.RenderItem({
32396
+ ...params,
32397
+ item: picked
32398
+ }));
32399
+ rItem.childIds = childId ? [childId] : [];
32400
+ }
32401
+ else if (params.item.Children && params.recursive != false && isFolder) {
30530
32402
  for (let i = 0; i < params.item.Children.length; i++) {
30531
32403
  const childId = this.RenderItem({
30532
32404
  ...params,
@@ -30535,8 +32407,8 @@
30535
32407
  rItem.childIds.push(childId);
30536
32408
  }
30537
32409
  }
30538
- if (rItem.renderManager && !((_f = rItem.renderManager) === null || _f === void 0 ? void 0 : _f.Disposed)) {
30539
- (_g = this.onUpdate) === null || _g === void 0 ? void 0 : _g.Trigger({ isEnabling: true, itemId: rItem.id });
32410
+ if (rItem.renderManager && !((_g = rItem.renderManager) === null || _g === void 0 ? void 0 : _g.Disposed)) {
32411
+ (_h = this.onUpdate) === null || _h === void 0 ? void 0 : _h.Trigger({ isEnabling: true, itemId: rItem.id });
30540
32412
  }
30541
32413
  return rItem.id;
30542
32414
  }
@@ -30585,9 +32457,74 @@
30585
32457
  (_b = this.onUpdate) === null || _b === void 0 ? void 0 : _b.Trigger({ isEnabling: false, itemId: item.id });
30586
32458
  }
30587
32459
  }
32460
+ /**
32461
+ * Replaces enabled menu items with another, which reuses their graphics where both draw the same data.
32462
+ */
32463
+ SwapItem(params) {
32464
+ const { fromIds, item, modelSpace } = params;
32465
+ const parents = this.items.filter(x => x.childIds.some(id => fromIds.includes(id)));
32466
+ const id = this.renderInPlaceOf(fromIds, () => this.RenderItem({
32467
+ item,
32468
+ modelSpace
32469
+ }));
32470
+ // Keeps a radio folder's removal reaching the child that replaced its old one.
32471
+ for (const parent of parents) {
32472
+ const kept = parent.childIds.filter(x => !fromIds.includes(x));
32473
+ parent.childIds = id && !kept.includes(id) ? kept.concat(id) : kept;
32474
+ }
32475
+ return id;
32476
+ }
32477
+ /**
32478
+ * Restyles an enabled menu item in place with another of its own styles, returning false when it cannot.
32479
+ */
32480
+ SetItemStyle(params) {
32481
+ var _a;
32482
+ const { menuItemId, styleId } = params;
32483
+ const rItem = this.items.find(x => x.id === menuItemId);
32484
+ if (!rItem || !this.isStyleSwapCapable(rItem.renderManager) || rItem.renderManager.Disposed) {
32485
+ return false;
32486
+ }
32487
+ const next = exports.MenuItemStyles.WithStyleId(rItem.item, styleId);
32488
+ if (!next) {
32489
+ return false;
32490
+ }
32491
+ rItem.renderManager.SwapStyle(next);
32492
+ rItem.item = next;
32493
+ (_a = this.onUpdate) === null || _a === void 0 ? void 0 : _a.Trigger({ isEnabling: true, itemId: rItem.id });
32494
+ return true;
32495
+ }
32496
+ /*
32497
+ * Picks the child the caller asked for, else the one already enabled, else the first.
32498
+ */
32499
+ pickRadioChild(children, preferredIds) {
32500
+ var _a, _b;
32501
+ return ((_b = (_a = children.find(x => preferredIds === null || preferredIds === void 0 ? void 0 : preferredIds.includes(x.id))) !== null && _a !== void 0 ? _a : children.find(x => this.items.some(y => y.id === x.id))) !== null && _b !== void 0 ? _b : children[0]);
32502
+ }
32503
+ /*
32504
+ * Removes the items so the render can claim their graphics, then disposes whatever it left unclaimed.
32505
+ */
32506
+ renderInPlaceOf(removeIds, render) {
32507
+ const pooledBefore = new Set(this.pendingHandoffPool);
32508
+ for (const id of removeIds) {
32509
+ this.RemoveItemById({
32510
+ menuItemId: id,
32511
+ allowHandoff: true
32512
+ });
32513
+ }
32514
+ const pooled = this.pendingHandoffPool.filter(x => !pooledBefore.has(x));
32515
+ try {
32516
+ return render();
32517
+ }
32518
+ finally {
32519
+ this.disposePooled(x => pooled.includes(x));
32520
+ }
32521
+ }
30588
32522
  isHandoffCapable(renderManager) {
30589
32523
  return Boolean(renderManager) && typeof renderManager.PrepareHandoff === "function";
30590
32524
  }
32525
+ isStyleSwapCapable(renderManager) {
32526
+ return Boolean(renderManager) && typeof renderManager.SwapStyle === "function";
32527
+ }
30591
32528
  // Resolves the hand-off key a menu item's data would have.
30592
32529
  computeHandoffKey(item, modelSpace) {
30593
32530
  if (item.Type === BModels.MenuItem.EType.CadTileset) {
@@ -30663,12 +32600,15 @@
30663
32600
  }
30664
32601
  // Disposes everything left unclaimed in the pending-hand-off pool.
30665
32602
  FlushPendingHandoffs() {
32603
+ this.disposePooled(() => true);
32604
+ }
32605
+ disposePooled(match) {
30666
32606
  var _a;
30667
32607
  if (!this.pendingHandoffPool.length) {
30668
32608
  return;
30669
32609
  }
30670
- const pool = this.pendingHandoffPool;
30671
- this.pendingHandoffPool = [];
32610
+ const pool = this.pendingHandoffPool.filter(match);
32611
+ this.pendingHandoffPool = this.pendingHandoffPool.filter(x => !match(x));
30672
32612
  for (const item of pool) {
30673
32613
  try {
30674
32614
  (_a = item.renderManager) === null || _a === void 0 ? void 0 : _a.Dispose();
@@ -30679,6 +32619,26 @@
30679
32619
  this.ClearSeriesSegments(item.id);
30680
32620
  }
30681
32621
  }
32622
+ /**
32623
+ * Returns the tags and attenuation the item's view authored, before any bookmark wrote over them.
32624
+ */
32625
+ GetAuthoredAlternation(item) {
32626
+ var _a;
32627
+ let authored = this.authoredAlternations.get(item);
32628
+ if (!authored) {
32629
+ const eItem = item;
32630
+ const tItem = item;
32631
+ authored = JSON.parse(JSON.stringify({
32632
+ id: item.id,
32633
+ Type: item.Type,
32634
+ BruceEntity: { SelectedExpandLayers: (_a = eItem.BruceEntity) === null || _a === void 0 ? void 0 : _a.SelectedExpandLayers },
32635
+ attenuation: tItem.attenuation,
32636
+ attenuationMax: tItem.attenuationMax
32637
+ }));
32638
+ this.authoredAlternations.set(item, authored);
32639
+ }
32640
+ return authored;
32641
+ }
30682
32642
  GetEnabledItemIds() {
30683
32643
  return this.items.map(x => x.id);
30684
32644
  }
@@ -30712,6 +32672,63 @@
30712
32672
  }
30713
32673
  }
30714
32674
  }
32675
+ /**
32676
+ * Draws Entities from data given here as well as, or instead of, the API's, until ClearSimulation is called.
32677
+ * @param params.set the simulation this belongs to, so it can be cleared apart from others. Default "default".
32678
+ * @param params.entities stand-ins that replace each Entity's record; API data for them is ignored.
32679
+ * @param params.patches partial Entities laid over each Entity; live API data shows where a patch is silent.
32680
+ * @param params.records historic records naming entityId, dateTime and only the data they change.
32681
+ * @param params.menuItemIds the items to redraw. By default every item already drawing one of the Entities.
32682
+ */
32683
+ async Simulate(params) {
32684
+ const { set, entities, patches, records, menuItemIds } = params;
32685
+ this.simulation.SetEntities(entities, set);
32686
+ this.simulation.SetPatches(patches, set);
32687
+ this.simulation.SetRecords(records, set);
32688
+ const entityIds = [
32689
+ ...(entities || []).map(x => { var _a; return (_a = x === null || x === void 0 ? void 0 : x.Bruce) === null || _a === void 0 ? void 0 : _a.ID; }),
32690
+ ...(patches || []).map(x => { var _a; return (_a = x === null || x === void 0 ? void 0 : x.Bruce) === null || _a === void 0 ? void 0 : _a.ID; }),
32691
+ ...(records || []).map(x => x === null || x === void 0 ? void 0 : x.entityId)
32692
+ ];
32693
+ await this.reRenderSimulated(entityIds, menuItemIds);
32694
+ }
32695
+ /**
32696
+ * Stops simulating these Entities, or every Entity when none are named, and redraws them.
32697
+ * @param params.set the simulation to clear. By default every one.
32698
+ * @param params.entities clear the stand-ins and patches. Default true.
32699
+ * @param params.records clear the historic records. Default true.
32700
+ */
32701
+ async ClearSimulation(params) {
32702
+ var _a;
32703
+ const entityIds = (_a = params === null || params === void 0 ? void 0 : params.entityIds) !== null && _a !== void 0 ? _a : [
32704
+ ...this.simulation.GetEntityIds(),
32705
+ ...this.simulation.GetRecordEntityIds()
32706
+ ];
32707
+ this.simulation.Clear({ set: params === null || params === void 0 ? void 0 : params.set, entityIds, entities: params === null || params === void 0 ? void 0 : params.entities, records: params === null || params === void 0 ? void 0 : params.records });
32708
+ await this.reRenderSimulated(entityIds, params === null || params === void 0 ? void 0 : params.menuItemIds);
32709
+ }
32710
+ /*
32711
+ * Redraws simulated Entities only in the items holding a visual for them, unless the caller names the items.
32712
+ */
32713
+ async reRenderSimulated(entityIds, menuItemIds) {
32714
+ const ids = Array.from(new Set(entityIds.filter(x => !!x)));
32715
+ if (!ids.length) {
32716
+ return;
32717
+ }
32718
+ const items = (menuItemIds === null || menuItemIds === void 0 ? void 0 : menuItemIds.length) ? this.items.filter(x => menuItemIds.includes(x.id)) : this.items;
32719
+ for (const item of items) {
32720
+ // Some managers follow the store themselves, and redrawing them here would only repeat that.
32721
+ if (!item.renderManager || item.renderManager.FollowsSimulation) {
32722
+ continue;
32723
+ }
32724
+ const drawn = (menuItemIds === null || menuItemIds === void 0 ? void 0 : menuItemIds.length)
32725
+ ? ids
32726
+ : ids.filter(id => this.visualsRegister.GetRego({ entityId: id, menuItemId: item.id }));
32727
+ if (drawn.length) {
32728
+ await item.renderManager.ReRender({ entityIds: drawn, force: true });
32729
+ }
32730
+ }
32731
+ }
30715
32732
  }
30716
32733
  MenuItemManager.Manager = Manager;
30717
32734
  })(exports.MenuItemManager || (exports.MenuItemManager = {}));
@@ -31685,6 +33702,7 @@
31685
33702
  })(exports.EMenuItemState || (exports.EMenuItemState = {}));
31686
33703
  // Icons are inline so the library stays dependency free.
31687
33704
  const ICON_CHECK = `<svg viewBox="0 0 11 8" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 4L4 7L10 1" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
33705
+ const ICON_RADIO = `<svg viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="6" cy="6" r="3.5" fill="white"/></svg>`;
31688
33706
  const ICON_PARTIAL_CHECK = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8 12H16" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
31689
33707
  const ICON_CHEV_DOWN = `<svg viewBox="0 0 16 9" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M15 1L8 8L1 1" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
31690
33708
  const ICON_CHEV_RIGHT = `<svg viewBox="0 0 10 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1.5 1.5L8.5 8.5L1.5 15.5" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
@@ -31888,6 +33906,10 @@
31888
33906
  margin-right: 8px;
31889
33907
  }
31890
33908
 
33909
+ .NextspaceMenuItemRow >.NextspaceMenuItemRowItem >.NextspaceMenuItemRowCheckbox[is-radio="true"] {
33910
+ border-radius: 50%;
33911
+ }
33912
+
31891
33913
  .NextspaceMenuItemRow >.NextspaceMenuItemRowItem >.NextspaceMenuItemRowToggle {
31892
33914
  margin-right: 3px;
31893
33915
  }
@@ -32138,13 +34160,15 @@
32138
34160
  }
32139
34161
  return this._waitedForView ? "No view is loaded." : "Loading..";
32140
34162
  }
32141
- _renderRow(item, depth, parent) {
34163
+ _renderRow(item, depth, parent, radioParent) {
32142
34164
  var _a;
32143
34165
  if (!(item === null || item === void 0 ? void 0 : item.id)) {
32144
34166
  return;
32145
34167
  }
32146
34168
  const isFolder = !item.Type || item.Type === BModels.MenuItem.EType.None;
32147
- const children = item.hideChildren ? [] : ((_a = item.Children) !== null && _a !== void 0 ? _a : []);
34169
+ const isRadioFolder = isFolder && Boolean(item.childrenAsRadios);
34170
+ // Radios are the folder's whole point, so they show even when the folder also hides its children.
34171
+ const children = item.hideChildren && !isRadioFolder ? [] : ((_a = item.Children) !== null && _a !== void 0 ? _a : []);
32148
34172
  const hasChildren = children.length > 0;
32149
34173
  const row = document.createElement("div");
32150
34174
  row.className = "NextspaceMenuItemRow";
@@ -32176,8 +34200,9 @@
32176
34200
  }
32177
34201
  const checkbox = document.createElement("div");
32178
34202
  checkbox.className = "NextspaceMenuItemRowCheckbox";
34203
+ checkbox.setAttribute("is-radio", radioParent ? "true" : "false");
32179
34204
  checkbox.onclick = () => {
32180
- this._toggleItem(item);
34205
+ this._toggleItem(item, radioParent);
32181
34206
  };
32182
34207
  itemElement.appendChild(checkbox);
32183
34208
  const title = document.createElement("div");
@@ -32227,10 +34252,11 @@
32227
34252
  itemElement,
32228
34253
  checkboxElement: checkbox,
32229
34254
  countElement: count,
32230
- childrenElement
34255
+ childrenElement,
34256
+ radioParent
32231
34257
  });
32232
34258
  for (const child of children) {
32233
- this._renderRow(child, depth + 1, childrenElement);
34259
+ this._renderRow(child, depth + 1, childrenElement, isRadioFolder ? item : undefined);
32234
34260
  }
32235
34261
  }
32236
34262
  _getOpen(itemId, depth) {
@@ -32265,7 +34291,10 @@
32265
34291
  for (const row of Array.from(this._rows.values())) {
32266
34292
  const state = this._resolveState(row.item, enabledIds);
32267
34293
  row.itemElement.setAttribute("is-enabled", state === exports.EMenuItemState.Disabled ? "false" : "true");
32268
- if (state === exports.EMenuItemState.Enabled) {
34294
+ if (row.radioParent && state !== exports.EMenuItemState.Disabled) {
34295
+ row.checkboxElement.innerHTML = ICON_RADIO;
34296
+ }
34297
+ else if (state === exports.EMenuItemState.Enabled) {
32269
34298
  row.checkboxElement.innerHTML = ICON_CHECK;
32270
34299
  }
32271
34300
  else if (state === exports.EMenuItemState.ChildEnabled) {
@@ -32280,17 +34309,33 @@
32280
34309
  * Works out a single item's state, rolling up children so a folder can report a partial state.
32281
34310
  */
32282
34311
  _resolveState(item, enabledIds) {
32283
- var _a;
34312
+ var _a, _b;
32284
34313
  const isFolder = !item.Type || item.Type === BModels.MenuItem.EType.None;
32285
34314
  if (!isFolder) {
32286
34315
  return enabledIds.has(item.id) ? exports.EMenuItemState.Enabled : exports.EMenuItemState.Disabled;
32287
34316
  }
34317
+ // Only one radio is ever enabled, so the folder is as enabled as that one.
34318
+ if (item.childrenAsRadios && ((_a = item.Children) === null || _a === void 0 ? void 0 : _a.length)) {
34319
+ const states = item.Children.map(x => this._resolveState(x, enabledIds));
34320
+ if (states.includes(exports.EMenuItemState.Enabled)) {
34321
+ return exports.EMenuItemState.Enabled;
34322
+ }
34323
+ return states.includes(exports.EMenuItemState.ChildEnabled) ? exports.EMenuItemState.ChildEnabled : exports.EMenuItemState.Disabled;
34324
+ }
32288
34325
  // Folders derive from the leaves below them rather than their own enabled flag.
32289
34326
  let actionCount = 0;
32290
34327
  let enabledActionCount = 0;
32291
34328
  const traverse = (current) => {
32292
- var _a;
34329
+ var _a, _b;
32293
34330
  const currentIsFolder = !current.Type || current.Type === BModels.MenuItem.EType.None;
34331
+ // A radio folder counts as one choice rather than as each of its radios.
34332
+ if (currentIsFolder && current.childrenAsRadios && ((_a = current.Children) === null || _a === void 0 ? void 0 : _a.length)) {
34333
+ actionCount += 1;
34334
+ if (this._resolveState(current, enabledIds) === exports.EMenuItemState.Enabled) {
34335
+ enabledActionCount += 1;
34336
+ }
34337
+ return;
34338
+ }
32294
34339
  if (!currentIsFolder) {
32295
34340
  actionCount += 1;
32296
34341
  if (enabledIds.has(current.id)) {
@@ -32298,11 +34343,11 @@
32298
34343
  }
32299
34344
  return;
32300
34345
  }
32301
- if ((_a = current.Children) === null || _a === void 0 ? void 0 : _a.length) {
34346
+ if ((_b = current.Children) === null || _b === void 0 ? void 0 : _b.length) {
32302
34347
  current.Children.forEach(traverse);
32303
34348
  }
32304
34349
  };
32305
- (_a = item.Children) === null || _a === void 0 ? void 0 : _a.forEach(traverse);
34350
+ (_b = item.Children) === null || _b === void 0 ? void 0 : _b.forEach(traverse);
32306
34351
  if (actionCount <= 0) {
32307
34352
  return enabledIds.has(item.id) ? exports.EMenuItemState.Enabled : exports.EMenuItemState.Disabled;
32308
34353
  }
@@ -32328,7 +34373,7 @@
32328
34373
  row.countElement.textContent = count.toLocaleString();
32329
34374
  }
32330
34375
  }
32331
- _toggleItem(item) {
34376
+ _toggleItem(item, radioParent) {
32332
34377
  var _a;
32333
34378
  const manager = (_a = this._widget) === null || _a === void 0 ? void 0 : _a._getManager();
32334
34379
  if (!manager) {
@@ -32336,7 +34381,21 @@
32336
34381
  }
32337
34382
  const enabledIds = new Set(manager.GetEnabledItemIds());
32338
34383
  const state = this._resolveState(item, enabledIds);
32339
- if (state === exports.EMenuItemState.Enabled) {
34384
+ // A radio stays picked until another one is, the folder's own checkbox turns them all off.
34385
+ if (radioParent && state === exports.EMenuItemState.Enabled) {
34386
+ return;
34387
+ }
34388
+ let siblingIds = [];
34389
+ if (radioParent && state === exports.EMenuItemState.Disabled) {
34390
+ siblingIds = radioParent.Children.filter(x => x !== item).flatMap(x => this._getEnabledIdsUnder(x, enabledIds));
34391
+ }
34392
+ if (siblingIds.length) {
34393
+ manager.SwapItem({
34394
+ fromIds: siblingIds,
34395
+ item
34396
+ });
34397
+ }
34398
+ else if (state === exports.EMenuItemState.Enabled) {
32340
34399
  this._disableItem(item);
32341
34400
  }
32342
34401
  else {
@@ -32346,6 +34405,16 @@
32346
34405
  }
32347
34406
  this._stateQueue.Call(true);
32348
34407
  }
34408
+ /*
34409
+ * Returns the highest enabled ids at or below the item, since removing one removes everything under it.
34410
+ */
34411
+ _getEnabledIdsUnder(item, enabledIds) {
34412
+ var _a;
34413
+ if (enabledIds.has(item.id)) {
34414
+ return [item.id];
34415
+ }
34416
+ return ((_a = item.Children) !== null && _a !== void 0 ? _a : []).flatMap(x => this._getEnabledIdsUnder(x, enabledIds));
34417
+ }
32349
34418
  _disableItem(item) {
32350
34419
  var _a, _b;
32351
34420
  const manager = (_a = this._widget) === null || _a === void 0 ? void 0 : _a._getManager();
@@ -33874,33 +35943,50 @@
33874
35943
  }
33875
35944
  const renderedItemIds = [];
33876
35945
  if ((menuItems === null || menuItems === void 0 ? void 0 : menuItems.length) && (enabledIds === null || enabledIds === void 0 ? void 0 : enabledIds.length)) {
33877
- const traverseItems = async (item) => {
35946
+ const alternateItems = (item) => {
35947
+ var _a, _b, _c, _d;
35948
+ // The project view menu item contains the instruction to enable tags, the bookmark specifies which ones.
35949
+ if (item.Type == BModels.MenuItem.EType.Entities && ((_a = item.BruceEntity) === null || _a === void 0 ? void 0 : _a.ExpandLayers)) {
35950
+ const eItem = item;
35951
+ // A setting the bookmark leaves out gets the view's own value back, not the last bookmark's.
35952
+ const authored = manager.GetAuthoredAlternation(item);
35953
+ const eItemAlt = alternations[item.id];
35954
+ const tags = (_b = eItemAlt === null || eItemAlt === void 0 ? void 0 : eItemAlt.BruceEntity) === null || _b === void 0 ? void 0 : _b.SelectedExpandLayers;
35955
+ eItem.BruceEntity = {
35956
+ ...eItem.BruceEntity,
35957
+ SelectedExpandLayers: tags !== undefined ? tags : (_c = authored.BruceEntity) === null || _c === void 0 ? void 0 : _c.SelectedExpandLayers,
35958
+ };
35959
+ }
35960
+ else if (item.Type == BModels.MenuItem.EType.EntityTileset) {
35961
+ const tItem = item;
35962
+ const authored = manager.GetAuthoredAlternation(item);
35963
+ const tItemAlt = alternations[item.id];
35964
+ const hasAttenuation = (tItemAlt === null || tItemAlt === void 0 ? void 0 : tItemAlt.attenuation) !== undefined || (tItemAlt === null || tItemAlt === void 0 ? void 0 : tItemAlt.attenuationMax) !== undefined;
35965
+ const source = hasAttenuation ? tItemAlt : authored;
35966
+ tItem.attenuation = source.attenuation;
35967
+ tItem.attenuationMax = source.attenuationMax;
35968
+ }
35969
+ (_d = item.Children) === null || _d === void 0 ? void 0 : _d.forEach(alternateItems);
35970
+ };
35971
+ /*
35972
+ * Lays each picked style over a copy, so it never stays on the view's item for a later bookmark.
35973
+ */
35974
+ const styleItems = (item) => {
33878
35975
  var _a;
33879
- if (alternations[item.id]) {
33880
- // The only alternation we support right now is tag menu item ones.
33881
- // The project view menu item contains the instruction to enable tags, the bookmark specifies which ones.
33882
- if (item.Type == BModels.MenuItem.EType.Entities) {
33883
- const eItem = item;
33884
- if (eItem.BruceEntity.ExpandLayers) {
33885
- const eItemAlt = alternations[item.id];
33886
- eItem.BruceEntity = {
33887
- ...eItem.BruceEntity,
33888
- SelectedExpandLayers: (_a = eItemAlt.BruceEntity) === null || _a === void 0 ? void 0 : _a.SelectedExpandLayers,
33889
- };
33890
- }
33891
- }
33892
- else if (item.Type == BModels.MenuItem.EType.EntityTileset) {
33893
- const tItem = item;
33894
- const tItemAlt = alternations[item.id];
33895
- tItem.attenuation = tItemAlt.attenuation;
33896
- tItem.attenuationMax = tItemAlt.attenuationMax;
33897
- }
35976
+ let next = alternations[item.id] ? exports.MenuItemStyles.ApplyAlternation(item, alternations[item.id]) : item;
35977
+ const children = (_a = item.Children) === null || _a === void 0 ? void 0 : _a.map(styleItems);
35978
+ if (children === null || children === void 0 ? void 0 : children.some((child, i) => child !== item.Children[i])) {
35979
+ next = { ...next, Children: children };
33898
35980
  }
35981
+ return next;
35982
+ };
35983
+ const traverseItems = async (item) => {
33899
35984
  if (enabledIds.includes(item.id)) {
33900
35985
  const itemId = await manager.RenderItem({
33901
35986
  item: item,
33902
35987
  getters: getters,
33903
- modelSpace: contentType === BModels.ProjectViewBookmark.EContentType.WEB_3D_MODEL_SPACE
35988
+ modelSpace: contentType === BModels.ProjectViewBookmark.EContentType.WEB_3D_MODEL_SPACE,
35989
+ preferredIds: enabledIds
33904
35990
  });
33905
35991
  renderedItemIds.push(itemId);
33906
35992
  }
@@ -33910,8 +35996,11 @@
33910
35996
  }
33911
35997
  }
33912
35998
  };
33913
- for (let i = 0; i < menuItems.length; i++) {
33914
- await traverseItems(menuItems[i]);
35999
+ // Tag and attenuation alternations write into the view's items, since Navigator's tag tree reads them back.
36000
+ menuItems.forEach(alternateItems);
36001
+ const styled = menuItems.map(styleItems);
36002
+ for (let i = 0; i < styled.length; i++) {
36003
+ await traverseItems(styled[i]);
33915
36004
  }
33916
36005
  }
33917
36006
  return {
@@ -39248,6 +41337,7 @@
39248
41337
  }
39249
41338
  class WidgetInfoView extends exports.Widget.AWidget {
39250
41339
  constructor(params) {
41340
+ var _a;
39251
41341
  super(params);
39252
41342
  this.STYLESHEET_ID = "nextspace-info-view-stylesheet";
39253
41343
  this._generateStyles();
@@ -39257,10 +41347,19 @@
39257
41347
  this._updateInfoView();
39258
41348
  }
39259
41349
  });
41350
+ // The selected Entity is read once per selection, so a change to its simulation reads it again.
41351
+ this._simulationRemoval = (_a = this._manager.Simulation) === null || _a === void 0 ? void 0 : _a.OnChanged.Subscribe((change) => {
41352
+ if (this._lastSelectedId && change.entityIds.includes(this._lastSelectedId)) {
41353
+ this._lastSelectedId = null;
41354
+ this._updateInfoView();
41355
+ }
41356
+ });
39260
41357
  }
39261
41358
  Dispose() {
41359
+ var _a;
39262
41360
  super.Dispose();
39263
41361
  this._updateRemoval();
41362
+ (_a = this._simulationRemoval) === null || _a === void 0 ? void 0 : _a.call(this);
39264
41363
  }
39265
41364
  _generateStyles() {
39266
41365
  if (document.getElementById(this.STYLESHEET_ID)) {
@@ -39722,7 +41821,7 @@
39722
41821
  }
39723
41822
  }
39724
41823
  async _updateInfoView() {
39725
- var _b, _c, _d, _e;
41824
+ var _a, _c, _d, _e, _f;
39726
41825
  const selectedIds = this._manager.VisualsRegister.GetSelected();
39727
41826
  if (selectedIds.length <= 0) {
39728
41827
  this._element.style.display = "none";
@@ -39744,17 +41843,20 @@
39744
41843
  this._summary.innerHTML = "";
39745
41844
  this._imageContainer.style.display = "none";
39746
41845
  const api = this._apiGetters.GetBruceApi();
39747
- const { entity } = await BModels.Entity.Get({
41846
+ // A simulated Entity is shown as simulated: a stand-in as is, a patch over what the API has.
41847
+ const simulation = this._manager.Simulation;
41848
+ let entity = (_a = simulation === null || simulation === void 0 ? void 0 : simulation.GetEntity(firstId)) !== null && _a !== void 0 ? _a : (await BModels.Entity.Get({
39748
41849
  entityId: firstId,
39749
41850
  api: api,
39750
41851
  migrated: true,
39751
41852
  expandLocation: DISPLAY_ASSEMBLY_INFO
39752
- });
41853
+ })).entity;
41854
+ entity = entity && simulation ? simulation.Resolve(firstId, entity) : entity;
39753
41855
  if (!entity) {
39754
41856
  throw (new Error("Could not get entity."));
39755
41857
  }
39756
41858
  const { entityType } = await BModels.EntityType.Get({
39757
- entityTypeId: (_b = entity.Bruce) === null || _b === void 0 ? void 0 : _b["EntityType.ID"],
41859
+ entityTypeId: (_c = entity.Bruce) === null || _c === void 0 ? void 0 : _c["EntityType.ID"],
39758
41860
  api: api
39759
41861
  });
39760
41862
  let name = BModels.Entity.CalculateName({
@@ -39777,7 +41879,7 @@
39777
41879
  const defaultImage = images.length ? images[0] : null;
39778
41880
  if (defaultImage) {
39779
41881
  this._imageContainer.style.display = "flex";
39780
- const url = (_c = defaultImage.URL) !== null && _c !== void 0 ? _c : BModels.ClientFile.GetUrl({
41882
+ const url = (_d = defaultImage.URL) !== null && _d !== void 0 ? _d : BModels.ClientFile.GetUrl({
39781
41883
  fileId: defaultImage["ClientFile.ID"],
39782
41884
  api: api,
39783
41885
  viaCdn: true
@@ -39790,7 +41892,7 @@
39790
41892
  this._summary.appendChild(detailsTitle);
39791
41893
  // TODO: Selection for what mode instead of always doing "all".
39792
41894
  const attributes = gatherAttributes(EMode.ShowAll, entity, entityType);
39793
- const groups = groupAttributes(attributes, (_e = (_d = entityType.DataSchema) === null || _d === void 0 ? void 0 : _d.Structure) !== null && _e !== void 0 ? _e : []);
41895
+ const groups = groupAttributes(attributes, (_f = (_e = entityType.DataSchema) === null || _e === void 0 ? void 0 : _e.Structure) !== null && _f !== void 0 ? _f : []);
39794
41896
  for (const group of groups) {
39795
41897
  this._generateAttrGroup(entityType, entity, group);
39796
41898
  }
@@ -42384,7 +44486,7 @@
42384
44486
  * @param params
42385
44487
  */
42386
44488
  async function GetLocation(params) {
42387
- var _a, _b, _c, _d;
44489
+ var _a, _b, _c, _d, _e;
42388
44490
  let { api, viewer, samples, visualRegister, minimumAlt, paddingAlt } = params;
42389
44491
  if (!paddingAlt) {
42390
44492
  paddingAlt = 0;
@@ -42932,6 +45034,13 @@
42932
45034
  recordSamples.push(sample);
42933
45035
  }
42934
45036
  }
45037
+ // A simulated Entity is where its blob says, never where the API has it.
45038
+ const simulation = exports.EntitySimulation.Get(viewer);
45039
+ for (const sample of recordSamples) {
45040
+ if (!sample.entity && sample.entityId) {
45041
+ sample.entity = (_e = simulation === null || simulation === void 0 ? void 0 : simulation.GetEntity(sample.entityId)) !== null && _e !== void 0 ? _e : sample.entity;
45042
+ }
45043
+ }
42935
45044
  // Bulk requesting Entity records.
42936
45045
  try {
42937
45046
  const toSearchIds = recordSamples.filter(x => !x.entity && x.entityId).map(x => x.entityId);
@@ -42952,7 +45061,8 @@
42952
45061
  for (let i = 0; i < recordSamples.length; i++) {
42953
45062
  const sample = recordSamples[i];
42954
45063
  if (!sample.entity && sample.entityId) {
42955
- sample.entity = entitiesMap.get(sample.entityId);
45064
+ const read = entitiesMap.get(sample.entityId);
45065
+ sample.entity = read && simulation ? simulation.Overlay(read) : read;
42956
45066
  }
42957
45067
  }
42958
45068
  }
@@ -42966,6 +45076,11 @@
42966
45076
  // We have to detect whatever is an Assembly Entity and expand it.
42967
45077
  try {
42968
45078
  const needsExpansion = recordSamples.filter(x => {
45079
+ var _a;
45080
+ // Expanding reads the API's record, which would replace the stand-in or undo the patch.
45081
+ if (simulation === null || simulation === void 0 ? void 0 : simulation.HasBlob((_a = x.entity.Bruce) === null || _a === void 0 ? void 0 : _a.ID)) {
45082
+ return false;
45083
+ }
42969
45084
  const assemblyPos = BModels.Entity.GetValue({
42970
45085
  entity: x.entity,
42971
45086
  path: ["Bruce", "AssemblyPosition"]
@@ -43324,6 +45439,21 @@
43324
45439
  return Cesium.Quaternion.multiply(combined, _assemblyAxisShift, new Cesium.Quaternion());
43325
45440
  }
43326
45441
  EntityUtils.GetAssemblyOrientation = GetAssemblyOrientation;
45442
+ /**
45443
+ * Returns the orientation a part's model is drawn with, for its world matrix within the root's frame.
45444
+ * @param rootFrame the root's frame in the world, eg. from Transforms.headingPitchRollToFixedFrame.
45445
+ * @param world the part's matrix within the root's frame.
45446
+ */
45447
+ function GetAssemblyOrientationInFrame(rootFrame, world) {
45448
+ if (!rootFrame || !world) {
45449
+ return null;
45450
+ }
45451
+ const combined = Cesium.Matrix4.multiply(rootFrame, world, new Cesium.Matrix4());
45452
+ const rotation = Cesium.Matrix4.getRotation(combined, new Cesium.Matrix3());
45453
+ const q = Cesium.Quaternion.fromRotationMatrix(rotation);
45454
+ return Cesium.Quaternion.multiply(q, _assemblyAxisShift, new Cesium.Quaternion());
45455
+ }
45456
+ EntityUtils.GetAssemblyOrientationInFrame = GetAssemblyOrientationInFrame;
43327
45457
  /**
43328
45458
  * Returns the position of the entity.
43329
45459
  * This is an async variant that checks for related assemblies.
@@ -43333,6 +45463,18 @@
43333
45463
  */
43334
45464
  async function GetPosAsync(params) {
43335
45465
  let { viewer, entityId, entity, visualRegister, returnHeightRef, recordHeightRef, api } = params;
45466
+ // A simulated Entity is where its stand-in says, or where its patch puts the API's record.
45467
+ const simulation = exports.EntitySimulation.Get(viewer);
45468
+ entity = entity !== null && entity !== void 0 ? entity : simulation === null || simulation === void 0 ? void 0 : simulation.GetEntity(entityId);
45469
+ if (!entity && entityId && (simulation === null || simulation === void 0 ? void 0 : simulation.HasBlob(entityId))) {
45470
+ try {
45471
+ const read = (await BModels.Entity.Get({ api, entityId, expandLocation: false, migrated: true })).entity;
45472
+ entity = read ? simulation.Overlay(read) : null;
45473
+ }
45474
+ catch (e) {
45475
+ console.error(e);
45476
+ }
45477
+ }
43336
45478
  // We have issues where imported BRZ files have improper expandLocation=true info.
43337
45479
  // As a result we'll double-request the data.
43338
45480
  // If there is a location then we won't bother using expanded information.
@@ -53294,7 +55436,7 @@ void main() {
53294
55436
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
53295
55437
  })(exports.StyleUtils || (exports.StyleUtils = {}));
53296
55438
 
53297
- const VERSION = "7.6.1";
55439
+ const VERSION = "7.6.3";
53298
55440
  /**
53299
55441
  * Updates the environment instance used by bruce-cesium to one specified.
53300
55442
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.