bruce-cesium 6.9.6 → 6.9.8

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 (28) hide show
  1. package/dist/bruce-cesium.es5.js +483 -43
  2. package/dist/bruce-cesium.es5.js.map +1 -1
  3. package/dist/bruce-cesium.umd.js +481 -40
  4. package/dist/bruce-cesium.umd.js.map +1 -1
  5. package/dist/lib/bruce-cesium.js +1 -1
  6. package/dist/lib/internal/image-utils.js +35 -0
  7. package/dist/lib/internal/image-utils.js.map +1 -0
  8. package/dist/lib/internal/series-segment.js +3 -0
  9. package/dist/lib/internal/series-segment.js.map +1 -0
  10. package/dist/lib/rendering/entity-render-engine-polygon.js +114 -34
  11. package/dist/lib/rendering/entity-render-engine-polygon.js.map +1 -1
  12. package/dist/lib/rendering/entity-render-engine.js +16 -5
  13. package/dist/lib/rendering/entity-render-engine.js.map +1 -1
  14. package/dist/lib/rendering/menu-item-manager.js +46 -1
  15. package/dist/lib/rendering/menu-item-manager.js.map +1 -1
  16. package/dist/lib/rendering/render-managers/entities/entities-render-manager.js +3 -1
  17. package/dist/lib/rendering/render-managers/entities/entities-render-manager.js.map +1 -1
  18. package/dist/lib/rendering/texture-frame-series-animator.js +281 -0
  19. package/dist/lib/rendering/texture-frame-series-animator.js.map +1 -0
  20. package/dist/types/bruce-cesium.d.ts +1 -1
  21. package/dist/types/internal/image-utils.d.ts +10 -0
  22. package/dist/types/internal/series-segment.d.ts +10 -0
  23. package/dist/types/rendering/entity-render-engine-polygon.d.ts +4 -0
  24. package/dist/types/rendering/entity-render-engine.d.ts +3 -5
  25. package/dist/types/rendering/menu-item-manager.d.ts +19 -0
  26. package/dist/types/rendering/render-managers/entities/entities-render-manager.d.ts +3 -0
  27. package/dist/types/rendering/texture-frame-series-animator.d.ts +93 -0
  28. package/package.json +1 -1
@@ -9661,13 +9661,14 @@
9661
9661
  // Type IDs that we'll allow to be rendered when receiving Entities.
9662
9662
  // This might be exactly the Menu Item Type ID because of local data sources.
9663
9663
  this.allowedTypeIDs = [];
9664
- const { viewer, apiGetter, monitor, item, register: visualsManager, sharedGetters } = params;
9664
+ const { viewer, apiGetter, monitor, item, register: visualsManager, sharedGetters, onSeriesDiscovered } = params;
9665
9665
  this.viewer = viewer;
9666
9666
  this.sharedGetters = sharedGetters;
9667
9667
  this.monitor = monitor;
9668
9668
  this.apiGetter = apiGetter;
9669
9669
  this.item = item;
9670
9670
  this.visualsManager = visualsManager;
9671
+ this.onSeriesDiscovered = onSeriesDiscovered;
9671
9672
  this.useGeojson = item.renderAsGeojson == true;
9672
9673
  if (item.enableClustering) {
9673
9674
  this.clustering = new PointClustering(this.visualsManager, this.item.id);
@@ -10636,6 +10637,7 @@
10636
10637
  apiGetter: this.apiGetter,
10637
10638
  entities: entities,
10638
10639
  menuItemId: this.item.id,
10640
+ onSeriesDiscovered: this.onSeriesDiscovered,
10639
10641
  visualRegister: this.visualsManager,
10640
10642
  zoomControl: this.zoomControl,
10641
10643
  entitiesHistoric: entitiesHistoric,
@@ -22197,6 +22199,46 @@
22197
22199
  }
22198
22200
  return this.onUpdate;
22199
22201
  }
22202
+ get OnSeriesUpdated() {
22203
+ if (!this.onSeriesUpdated) {
22204
+ this.onSeriesUpdated = new BModels.BruceEvent();
22205
+ }
22206
+ return this.onSeriesUpdated;
22207
+ }
22208
+ /**
22209
+ * Currently known series segments for a menu item (empty array if none discovered yet).
22210
+ */
22211
+ GetSeriesSegments(itemId) {
22212
+ return this.seriesSegments.get(itemId) || [];
22213
+ }
22214
+ /**
22215
+ * @param itemId
22216
+ * @param segment
22217
+ */
22218
+ RecordSeriesSegment(itemId, segment) {
22219
+ const existing = this.seriesSegments.get(itemId) || [];
22220
+ const idx = existing.findIndex((s) => s.key === segment.key);
22221
+ if (idx !== -1) {
22222
+ const current = existing[idx];
22223
+ if (current.min.getTime() === segment.min.getTime() && current.max.getTime() === segment.max.getTime() && current.label === segment.label) {
22224
+ return; // No-op.
22225
+ }
22226
+ existing[idx] = segment;
22227
+ }
22228
+ else {
22229
+ existing.push(segment);
22230
+ }
22231
+ this.seriesSegments.set(itemId, existing);
22232
+ this.OnSeriesUpdated.Trigger({ itemId, segments: existing });
22233
+ }
22234
+ /**
22235
+ * Clears any series segments recorded for a menu item, eg when it's disabled/removed.
22236
+ */
22237
+ ClearSeriesSegments(itemId) {
22238
+ if (this.seriesSegments.delete(itemId)) {
22239
+ this.OnSeriesUpdated.Trigger({ itemId, segments: [] });
22240
+ }
22241
+ }
22200
22242
  get Monitor() {
22201
22243
  return this.sharedMonitor;
22202
22244
  }
@@ -22204,6 +22246,9 @@
22204
22246
  this.items = [];
22205
22247
  this.pendingHandoffPool = [];
22206
22248
  this.onUpdate = null;
22249
+ // Segments discovered so far per menu item ID.
22250
+ this.seriesSegments = new Map();
22251
+ this.onSeriesUpdated = null;
22207
22252
  let { viewer, visualsRegister, getters } = params;
22208
22253
  this.viewer = viewer;
22209
22254
  if (!getters) {
@@ -22429,7 +22474,8 @@
22429
22474
  apiGetter: params.apiGetter,
22430
22475
  monitor: this.sharedMonitor,
22431
22476
  item: params.item,
22432
- sharedGetters: this.sharedGetters
22477
+ sharedGetters: this.sharedGetters,
22478
+ onSeriesDiscovered: (segment) => this.RecordSeriesSegment(rItem.id, segment)
22433
22479
  });
22434
22480
  break;
22435
22481
  case BModels.MenuItem.EType.EntitiesLoaded:
@@ -22655,6 +22701,7 @@
22655
22701
  }
22656
22702
  }
22657
22703
  this.items = this.items.filter(x => x.id !== id);
22704
+ this.ClearSeriesSegments(item.id);
22658
22705
  (_b = this.onUpdate) === null || _b === void 0 ? void 0 : _b.Trigger({ isEnabling: false, itemId: item.id });
22659
22706
  }
22660
22707
  }
@@ -36618,6 +36665,312 @@
36618
36665
  return heightRef;
36619
36666
  }
36620
36667
 
36668
+ /**
36669
+ * Remaps a (assumed grayscale, server-rendered black-to-white) image's pixels into a gradient between minColor/maxColor,
36670
+ * preserving source alpha blended with the mask's own alpha.
36671
+ * @param imageData
36672
+ * @param minColor
36673
+ * @param maxColor
36674
+ */
36675
+ function ApplyGrayscaleColorMask(imageData, minColor, maxColor) {
36676
+ const data = imageData.data;
36677
+ for (let i = 0; i < data.length; i += 4) {
36678
+ // The texture is rendered black-to-white server-side, so any channel carries the value.
36679
+ const t = data[i] / 255;
36680
+ data[i] = Math.round(minColor.red + (maxColor.red - minColor.red) * t);
36681
+ data[i + 1] = Math.round(minColor.green + (maxColor.green - minColor.green) * t);
36682
+ data[i + 2] = Math.round(minColor.blue + (maxColor.blue - minColor.blue) * t);
36683
+ // Preserve source transparency (eg unrasterized pixels) blended with the mask's own alpha.
36684
+ const srcAlpha = data[i + 3] / 255;
36685
+ const maskAlpha = minColor.alpha + (maxColor.alpha - minColor.alpha) * t;
36686
+ data[i + 3] = Math.round(srcAlpha * maskAlpha * 255);
36687
+ }
36688
+ }
36689
+ function loadImage(src) {
36690
+ return new Promise((res, rej) => {
36691
+ const image = new Image();
36692
+ image.onload = () => res(image);
36693
+ image.onerror = (e) => rej(e);
36694
+ image.src = src;
36695
+ });
36696
+ }
36697
+
36698
+ var TextureFrameSeriesAnimator;
36699
+ (function (TextureFrameSeriesAnimator) {
36700
+ /**
36701
+ * Detects whether a ClientFile's `Data.generation` metadata describes a frame archive rather than a single static image,
36702
+ * so a caller can decide whether to construct an Animator or fall back to the existing static-texture path.
36703
+ * @param data ClientFile.Data (the `IFile.Data` field), as returned by ClientFile.Get().
36704
+ */
36705
+ function IsFrameArchiveMetadata(data) {
36706
+ return Boolean(data && data.generation && Array.isArray(data.generation.Frames) && data.generation.Frames.length > 0);
36707
+ }
36708
+ TextureFrameSeriesAnimator.IsFrameArchiveMetadata = IsFrameArchiveMetadata;
36709
+ const DEFAULT_CROSSFADE_MS = 900;
36710
+ const DEFAULT_LOW_COLOR = { red: 255, green: 255, blue: 255, alpha: 0.12 };
36711
+ const DEFAULT_HIGH_COLOR = { red: 21, green: 96, blue: 196, alpha: 0.92 };
36712
+ class Animator {
36713
+ constructor(options) {
36714
+ var _a;
36715
+ this.removeOnTick = null;
36716
+ this.removeCrossfadeTick = null;
36717
+ this.disposed = false;
36718
+ this.currentFrameIndex = -1;
36719
+ // Single-in-flight fetch queue: only one Range GET outstanding at a time, and the NEXT one
36720
+ // is chosen right before sending, this is what makes reprioritizing mid-scrub take effect
36721
+ // immediately instead of draining a backlog of stale requests first.
36722
+ this.inFlightIndex = -1;
36723
+ this.pendingIndex = -1;
36724
+ this.displayedPixels = null;
36725
+ this.fadeFromPixels = null;
36726
+ this.fadeToIndex = -1;
36727
+ this.fadeToDims = null;
36728
+ this.fadeStartMs = null;
36729
+ // Two-canvas pool: Cesium compares image references each frame, so alternating between
36730
+ // pool[0]/pool[1] gives a new object reference whenever we actually present a new frame,
36731
+ // forcing Cesium to re-upload the texture only when something changed.
36732
+ this.pool = [document.createElement("canvas"), document.createElement("canvas")];
36733
+ this.poolIdx = 0;
36734
+ if (!options.entity.polygon) {
36735
+ throw new Error("TextureFrameSeriesAnimator requires an entity with polygon graphics.");
36736
+ }
36737
+ if (!options.frames || options.frames.length === 0) {
36738
+ throw new Error("TextureFrameSeriesAnimator requires at least one frame.");
36739
+ }
36740
+ this.viewer = options.viewer;
36741
+ this.entity = options.entity;
36742
+ this.archiveUrl = options.archiveUrl;
36743
+ this.frames = options.frames;
36744
+ this.crossfadeMs = (_a = options.crossfadeMs) !== null && _a !== void 0 ? _a : DEFAULT_CROSSFADE_MS;
36745
+ const mask = options.textureColorMask;
36746
+ this.lowColor = (mask && BModels.Color.ColorFromStr(mask.minColor)) || DEFAULT_LOW_COLOR;
36747
+ this.highColor = (mask && BModels.Color.ColorFromStr(mask.maxColor)) || DEFAULT_HIGH_COLOR;
36748
+ this.frameDates = this.frames.map((f) => Cesium.JulianDate.fromIso8601(f.Timestamp));
36749
+ this.frameCache = new Array(this.frames.length).fill(null);
36750
+ this.frameDims = new Array(this.frames.length).fill(null);
36751
+ this.originalMaterial = options.entity.polygon.material;
36752
+ this.imageProperty = new Cesium.CallbackProperty(() => this.pool[this.poolIdx], false);
36753
+ options.entity.polygon.material = new Cesium.ImageMaterialProperty({ image: this.imageProperty, transparent: true });
36754
+ this.removeCrossfadeTick = this.viewer.scene.postUpdate.addEventListener(() => this.tickCrossfade());
36755
+ this.removeOnTick = this.viewer.clock.onTick.addEventListener((clock) => this.onClockTick(clock.currentTime));
36756
+ this.onClockTick(this.viewer.clock.currentTime);
36757
+ }
36758
+ /**
36759
+ * Explicit teardown: stops driving the entity's material, abandons any in-flight/pending fetch (results are simply ignored when they land),
36760
+ * and restores whatever material the entity had before construction. Never called automatically.
36761
+ */
36762
+ Dispose() {
36763
+ if (this.disposed) {
36764
+ return;
36765
+ }
36766
+ this.disposed = true;
36767
+ if (this.removeOnTick) {
36768
+ this.removeOnTick();
36769
+ this.removeOnTick = null;
36770
+ }
36771
+ if (this.removeCrossfadeTick) {
36772
+ this.removeCrossfadeTick();
36773
+ this.removeCrossfadeTick = null;
36774
+ }
36775
+ if (this.entity && this.entity.polygon) {
36776
+ this.entity.polygon.material = this.originalMaterial;
36777
+ }
36778
+ }
36779
+ IsDisposed() {
36780
+ return this.disposed;
36781
+ }
36782
+ /**
36783
+ * The archive URL this instance was constructed with,
36784
+ * lets a caller re-rendering the same entity tell whether an existing instance is already correct, without reaching into private state.
36785
+ */
36786
+ GetArchiveUrl() {
36787
+ return this.archiveUrl;
36788
+ }
36789
+ onClockTick(currentTime) {
36790
+ if (this.disposed) {
36791
+ return;
36792
+ }
36793
+ this.swapToFrame(this.findFrameIndex(currentTime));
36794
+ }
36795
+ // Binary-search the frame whose timestamp is <= currentTime.
36796
+ findFrameIndex(currentTime) {
36797
+ const dates = this.frameDates;
36798
+ let lo = 0;
36799
+ let hi = dates.length - 1;
36800
+ if (Cesium.JulianDate.lessThanOrEquals(currentTime, dates[0])) {
36801
+ return 0;
36802
+ }
36803
+ if (Cesium.JulianDate.greaterThanOrEquals(currentTime, dates[hi])) {
36804
+ return hi;
36805
+ }
36806
+ while (lo < hi) {
36807
+ const mid = (lo + hi + 1) >> 1;
36808
+ if (Cesium.JulianDate.lessThanOrEquals(dates[mid], currentTime)) {
36809
+ lo = mid;
36810
+ }
36811
+ else {
36812
+ hi = mid - 1;
36813
+ }
36814
+ }
36815
+ return lo;
36816
+ }
36817
+ // Called with the discrete target frame (never a fractional/interpolated index).
36818
+ // Scrubbing rapidly just retargets what's fetched next.
36819
+ swapToFrame(idx) {
36820
+ if (idx === this.currentFrameIndex) {
36821
+ return;
36822
+ }
36823
+ this.currentFrameIndex = idx;
36824
+ if (this.frameCache[idx]) {
36825
+ this.beginCrossfadeTo(idx);
36826
+ return;
36827
+ }
36828
+ this.ensureFrame(idx);
36829
+ }
36830
+ ensureFrame(idx) {
36831
+ if (this.frameCache[idx]) {
36832
+ return;
36833
+ }
36834
+ if (this.inFlightIndex === idx) {
36835
+ return;
36836
+ }
36837
+ this.pendingIndex = idx;
36838
+ this.pumpFetchQueue();
36839
+ }
36840
+ // Nearest still-uncached frame to wherever the viewer CURRENTLY is.
36841
+ nearestUncachedFrame() {
36842
+ const n = this.frameCache.length;
36843
+ if (this.currentFrameIndex < 0) {
36844
+ return this.frameCache[0] ? -1 : 0;
36845
+ }
36846
+ for (let d = 0; d < n; d++) {
36847
+ const forward = this.currentFrameIndex + d;
36848
+ if (forward < n && !this.frameCache[forward]) {
36849
+ return forward;
36850
+ }
36851
+ const backward = this.currentFrameIndex - d;
36852
+ if (backward >= 0 && !this.frameCache[backward]) {
36853
+ return backward;
36854
+ }
36855
+ }
36856
+ return -1;
36857
+ }
36858
+ pumpFetchQueue() {
36859
+ if (this.disposed || this.inFlightIndex !== -1) {
36860
+ return;
36861
+ }
36862
+ let next = -1;
36863
+ if (this.pendingIndex !== -1) {
36864
+ next = this.pendingIndex;
36865
+ this.pendingIndex = -1;
36866
+ }
36867
+ else {
36868
+ next = this.nearestUncachedFrame();
36869
+ }
36870
+ if (next === -1) {
36871
+ return;
36872
+ }
36873
+ this.inFlightIndex = next;
36874
+ this.fetchAndTint(next)
36875
+ .catch(() => {
36876
+ // Eating it.
36877
+ })
36878
+ .finally(() => {
36879
+ this.inFlightIndex = -1;
36880
+ if (!this.disposed) {
36881
+ this.pumpFetchQueue();
36882
+ }
36883
+ });
36884
+ }
36885
+ async fetchAndTint(idx) {
36886
+ const entry = this.frames[idx];
36887
+ const start = entry.ByteOffset;
36888
+ const end = entry.ByteOffset + entry.ByteLength - 1;
36889
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
36890
+ const buffer = await response.arrayBuffer();
36891
+ if (this.disposed) {
36892
+ return;
36893
+ }
36894
+ const decoded = await this.decodeAndTint(buffer);
36895
+ if (this.disposed) {
36896
+ return;
36897
+ }
36898
+ this.frameCache[idx] = decoded.pixels;
36899
+ this.frameDims[idx] = { width: decoded.width, height: decoded.height };
36900
+ if (idx === this.currentFrameIndex) {
36901
+ this.beginCrossfadeTo(idx);
36902
+ }
36903
+ }
36904
+ /**
36905
+ * Decodes one frame's raw PNG bytes (as returned by a Range GET) and applies the grayscale color mask.
36906
+ */
36907
+ async decodeAndTint(buffer) {
36908
+ const blob = new Blob([buffer], { type: "image/png" });
36909
+ const objectUrl = URL.createObjectURL(blob);
36910
+ let image;
36911
+ try {
36912
+ image = await loadImage(objectUrl);
36913
+ }
36914
+ finally {
36915
+ URL.revokeObjectURL(objectUrl);
36916
+ }
36917
+ const canvas = document.createElement("canvas");
36918
+ canvas.width = image.width;
36919
+ canvas.height = image.height;
36920
+ const ctx = canvas.getContext("2d");
36921
+ ctx.drawImage(image, 0, 0);
36922
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
36923
+ ApplyGrayscaleColorMask(imageData, this.lowColor, this.highColor);
36924
+ return { pixels: imageData.data, width: canvas.width, height: canvas.height };
36925
+ }
36926
+ beginCrossfadeTo(idx) {
36927
+ const to = this.frameCache[idx];
36928
+ this.fadeFromPixels = this.displayedPixels === null ? new Uint8ClampedArray(to.length) : this.displayedPixels;
36929
+ this.fadeToIndex = idx;
36930
+ this.fadeToDims = this.frameDims[idx];
36931
+ this.fadeStartMs = Now();
36932
+ }
36933
+ tickCrossfade() {
36934
+ if (this.disposed || this.fadeStartMs === null) {
36935
+ return;
36936
+ }
36937
+ const t = Math.min(1, (Now() - this.fadeStartMs) / this.crossfadeMs);
36938
+ const eased = easeInOutQuad(t);
36939
+ const from = this.fadeFromPixels;
36940
+ const to = this.frameCache[this.fadeToIndex];
36941
+ const n = Math.min(from.length, to.length);
36942
+ const blended = new Uint8ClampedArray(to.length);
36943
+ for (let i = 0; i < n; i++) {
36944
+ blended[i] = from[i] + (to[i] - from[i]) * eased;
36945
+ }
36946
+ this.presentPixels(blended, this.fadeToDims);
36947
+ if (t >= 1) {
36948
+ this.fadeStartMs = null;
36949
+ this.fadeFromPixels = null;
36950
+ }
36951
+ }
36952
+ presentPixels(pixels, dims) {
36953
+ this.poolIdx = 1 - this.poolIdx;
36954
+ const target = this.pool[this.poolIdx];
36955
+ target.width = dims.width;
36956
+ target.height = dims.height;
36957
+ const ctx = target.getContext("2d");
36958
+ const imgData = ctx.createImageData(dims.width, dims.height);
36959
+ imgData.data.set(pixels);
36960
+ ctx.putImageData(imgData, 0, 0);
36961
+ this.displayedPixels = pixels;
36962
+ }
36963
+ }
36964
+ TextureFrameSeriesAnimator.Animator = Animator;
36965
+ function easeInOutQuad(t) {
36966
+ return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
36967
+ }
36968
+ function Now() {
36969
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
36970
+ }
36971
+ })(TextureFrameSeriesAnimator || (TextureFrameSeriesAnimator = {}));
36972
+
36973
+ const TEXTURE_FRAME_SERIES_ANIMATOR_KEY = "TextureFrameSeriesAnimator.Animator";
36621
36974
  (function (EntityRenderEnginePolygon) {
36622
36975
  async function Render(params) {
36623
36976
  var _a, _b;
@@ -36636,6 +36989,7 @@
36636
36989
  const cFillColor = bFillColor ? ColorToCColor(bFillColor) : Cesium.Color.fromCssColorString("rgba(139, 195, 74, 0.8)");
36637
36990
  let textureDataUri = null;
36638
36991
  let textureTraceEffective = null;
36992
+ let frameArchive = null;
36639
36993
  if (fillType === BModels.Style.EPolygonFillType.Texture) {
36640
36994
  const resolved = await resolveTexturedFill({
36641
36995
  api: params.api,
@@ -36645,11 +36999,14 @@
36645
36999
  });
36646
37000
  textureDataUri = resolved.dataUri;
36647
37001
  textureTraceEffective = resolved.effective;
37002
+ frameArchive = resolved.frameArchive || null;
36648
37003
  }
36649
- const hasFill = fillType === BModels.Style.EPolygonFillType.Texture ? Boolean(textureDataUri) : cFillColor.alpha > 0;
37004
+ const hasFill = fillType === BModels.Style.EPolygonFillType.Texture ? Boolean(textureDataUri || frameArchive) : cFillColor.alpha > 0;
36650
37005
  const fillMaterial = textureDataUri
36651
37006
  ? new Cesium.ImageMaterialProperty({ image: textureDataUri, transparent: true })
36652
- : cFillColor;
37007
+ : frameArchive
37008
+ ? Cesium.Color.WHITE.withAlpha(0)
37009
+ : cFillColor;
36653
37010
  const lineColorTrace = style.lineColor ? BModels.Calculator.TraceGetColor(style.lineColor, entity, params.tags) : { value: null, effective: null };
36654
37011
  const bLineColor = lineColorTrace.value;
36655
37012
  const cLineColor = bLineColor ? ColorToCColor(bLineColor) : Cesium.Color.fromCssColorString("rgba(80, 80, 80, 0.8)");
@@ -36819,6 +37176,13 @@
36819
37176
  }
36820
37177
  cEntity.show = true;
36821
37178
  }
37179
+ syncTextureFrameArchive(cEntity, frameArchive, params.viewer, style.textureColorMask);
37180
+ if (frameArchive && params.onSeriesDiscovered) {
37181
+ const segment = frameArchiveToSeriesSegment(frameArchive, params.entity);
37182
+ if (segment) {
37183
+ params.onSeriesDiscovered(segment);
37184
+ }
37185
+ }
36822
37186
  if (hasOutline) {
36823
37187
  let borderHeight = undefined;
36824
37188
  if (heightRef != Cesium.HeightReference.CLAMP_TO_GROUND) {
@@ -37027,7 +37391,9 @@
37027
37391
  // Temporarily disabling re-using graphics for polygons as they have some bugs, and
37028
37392
  // they don't support any animation yet.
37029
37393
  // rendered: null
37030
- rendered: (_c = params.rendered) === null || _c === void 0 ? void 0 : _c.get(entity.Bruce.ID)
37394
+ rendered: (_c = params.rendered) === null || _c === void 0 ? void 0 : _c.get(entity.Bruce.ID),
37395
+ menuItemId: params.menuItemId,
37396
+ onSeriesDiscovered: params.onSeriesDiscovered
37031
37397
  });
37032
37398
  if (cEntity) {
37033
37399
  const name = await getName$3(api, entity);
@@ -37143,11 +37509,86 @@
37143
37509
  // same polygon (or multiple polygons sharing a texture) don't refetch/reprocess the same image.
37144
37510
  const _textureCache = new BModels.LRUCache(50);
37145
37511
  /**
37146
- * Returns if a resolved texture value looks like a URL rather than a client file ID.
37512
+ * Reconciles cEntity's TextureFrameSeriesAnimator.Animator (if any) against this render's resolved texture.
37513
+ * Called once cEntity is fully created/updated and definitely has a `polygon` graphics.
37514
+ * @param cEntity
37515
+ * @param frameArchive
37516
+ * @param viewer
37517
+ * @param textureColorMask
37518
+ */
37519
+ function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask) {
37520
+ const existing = cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY];
37521
+ if (frameArchive && existing && !existing.IsDisposed() && existing.GetArchiveUrl() === frameArchive.url) {
37522
+ return;
37523
+ }
37524
+ if (existing && !existing.IsDisposed()) {
37525
+ existing.Dispose();
37526
+ cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY] = null;
37527
+ }
37528
+ if (frameArchive) {
37529
+ const animator = new TextureFrameSeriesAnimator.Animator({
37530
+ viewer,
37531
+ entity: cEntity,
37532
+ archiveUrl: frameArchive.url,
37533
+ frames: frameArchive.metadata.Frames,
37534
+ textureColorMask
37535
+ });
37536
+ cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY] = animator;
37537
+ }
37538
+ }
37539
+ /**
37540
+ * Derives a min/max/label time-range segment from a resolved frame archive's Frames metadata,
37541
+ * for reporting up to whoever owns the menu item this entity belongs to (see IParams.onSeriesDiscovered).
37542
+ * @param frameArchive
37543
+ * @param entity
37544
+ */
37545
+ function frameArchiveToSeriesSegment(frameArchive, entity) {
37546
+ var _a;
37547
+ const timestamps = frameArchive.metadata.Frames
37548
+ .map((f) => new Date(f.Timestamp))
37549
+ .filter((d) => !isNaN(d.getTime()));
37550
+ if (!timestamps.length) {
37551
+ return null;
37552
+ }
37553
+ return {
37554
+ key: frameArchive.url,
37555
+ min: new Date(Math.min(...timestamps.map((d) => d.getTime()))),
37556
+ max: new Date(Math.max(...timestamps.map((d) => d.getTime()))),
37557
+ label: ((_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.Name) || undefined
37558
+ };
37559
+ }
37560
+ /**
37561
+ * Returns if a value looks like a URL rather than a bare client file ID.
37147
37562
  * @param value
37148
37563
  */
37149
- function looksLikeTextureUrl(value) {
37150
- return /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value) || value.startsWith("//") || value.startsWith("data:");
37564
+ function looksLikeUrl(value) {
37565
+ return /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value) || value.startsWith("//");
37566
+ }
37567
+ /**
37568
+ * Extracts a client file ID from a resolved polygon "texture" value, which is either a bare
37569
+ * file ID or a URL. Follows the "/file/<id>" route path regardless of domain,
37570
+ * eg: "https://host/file/AGPWMAQR6F5ZVDJXZVRODGLCJE.png" or "https://host/RV:1/file/AGPWMAQR6F5ZVDJXZVRODGLCJE?cc=1".
37571
+ * @param raw
37572
+ */
37573
+ function extractClientFileIdFromTextureValue(raw) {
37574
+ if (!raw || typeof raw !== "string") {
37575
+ return null;
37576
+ }
37577
+ if (!looksLikeUrl(raw)) {
37578
+ return raw;
37579
+ }
37580
+ try {
37581
+ const withoutQuery = raw.split("?")[0];
37582
+ const match = withoutQuery.match(/\/file\/([^\/]+)/);
37583
+ if (!match) {
37584
+ return null;
37585
+ }
37586
+ return match[1].split(".")[0];
37587
+ }
37588
+ catch (e) {
37589
+ console.warn("Failed to parse client file ID from texture URL:", e, raw);
37590
+ return null;
37591
+ }
37151
37592
  }
37152
37593
  /**
37153
37594
  * Resolves a polygon style's texture field and if a color ask is needed it tints the resulting image.
@@ -37167,13 +37608,21 @@
37167
37608
  return { dataUri: null, effective: textureTrace.effective };
37168
37609
  }
37169
37610
  let url = raw;
37170
- if (!looksLikeTextureUrl(raw)) {
37171
- await api.Loading;
37172
- url = BModels.ClientFile.GetUrl({
37173
- api,
37174
- fileId: raw,
37175
- viaCdn: true
37176
- });
37611
+ if (!raw.startsWith("data:")) {
37612
+ const clientFileId = extractClientFileIdFromTextureValue(raw);
37613
+ if (clientFileId) {
37614
+ await api.Loading;
37615
+ const { clientFile } = await BModels.ClientFile.Get({ api, fileId: clientFileId });
37616
+ url = clientFile.URL;
37617
+ const clientFileData = clientFile.Data;
37618
+ if (TextureFrameSeriesAnimator.IsFrameArchiveMetadata(clientFileData)) {
37619
+ return {
37620
+ dataUri: null,
37621
+ effective: textureTrace.effective,
37622
+ frameArchive: { url, metadata: clientFileData.generation }
37623
+ };
37624
+ }
37625
+ }
37177
37626
  }
37178
37627
  const mask = style.textureColorMask;
37179
37628
  const cacheKey = "texture-" + url + "-" + (mask ? mask.type + "-" + mask.minColor + "-" + mask.maxColor : "none");
@@ -37210,30 +37659,11 @@
37210
37659
  const minColor = BModels.Color.ColorFromStr(mask.minColor) || { red: 0, green: 0, blue: 0, alpha: 1 };
37211
37660
  const maxColor = BModels.Color.ColorFromStr(mask.maxColor) || { red: 255, green: 255, blue: 255, alpha: 1 };
37212
37661
  const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
37213
- const data = imageData.data;
37214
- for (let i = 0; i < data.length; i += 4) {
37215
- // The texture is rendered black-to-white server-side, so any channel carries the value.
37216
- const t = data[i] / 255;
37217
- data[i] = Math.round(minColor.red + (maxColor.red - minColor.red) * t);
37218
- data[i + 1] = Math.round(minColor.green + (maxColor.green - minColor.green) * t);
37219
- data[i + 2] = Math.round(minColor.blue + (maxColor.blue - minColor.blue) * t);
37220
- // Preserve source transparency (eg unrasterized pixels) blended with the mask's own alpha.
37221
- const srcAlpha = data[i + 3] / 255;
37222
- const maskAlpha = minColor.alpha + (maxColor.alpha - minColor.alpha) * t;
37223
- data[i + 3] = Math.round(srcAlpha * maskAlpha * 255);
37224
- }
37662
+ ApplyGrayscaleColorMask(imageData, minColor, maxColor);
37225
37663
  context.putImageData(imageData, 0, 0);
37226
37664
  }
37227
37665
  return canvas.toDataURL("image/png");
37228
37666
  }
37229
- function loadImage(src) {
37230
- return new Promise((res, rej) => {
37231
- const image = new Image();
37232
- image.onload = () => res(image);
37233
- image.onerror = (e) => rej(e);
37234
- image.src = src;
37235
- });
37236
- }
37237
37667
  /**
37238
37668
  * Compares two color materials.
37239
37669
  * Returns if they are equal.
@@ -37447,11 +37877,19 @@
37447
37877
  return heightRef;
37448
37878
  }
37449
37879
 
37450
- /**
37451
- * Returns if the outline metadata is different.
37452
- * @param rego
37453
- * @param entity
37880
+ /*
37881
+ * Returns if the style applied to a rego differs from the one the zoom item now wants.
37454
37882
  */
37883
+ function isStyleChanged(rego, zoomItem) {
37884
+ var _a;
37885
+ if (!rego) {
37886
+ return false;
37887
+ }
37888
+ // Normalize the same way cEntity.styleId is set when rendering (see RenderGroup callers).
37889
+ const newStyleId = (zoomItem === null || zoomItem === void 0 ? void 0 : zoomItem.StyleID) == -1 ? -1 : (+(zoomItem === null || zoomItem === void 0 ? void 0 : zoomItem.StyleID) || null);
37890
+ const oldStyleId = (_a = rego.styleId) !== null && _a !== void 0 ? _a : null;
37891
+ return newStyleId != oldStyleId;
37892
+ }
37455
37893
  function isOutlineChanged$1(rego, entity) {
37456
37894
  var _a, _b, _c, _d, _e, _f;
37457
37895
  if (((_a = rego.outline) === null || _a === void 0 ? void 0 : _a.length) != ((_c = (_b = entity.Bruce) === null || _b === void 0 ? void 0 : _b.Outline) === null || _c === void 0 ? void 0 : _c.length)) {
@@ -37558,6 +37996,7 @@
37558
37996
  entities: [],
37559
37997
  zoomItems: {},
37560
37998
  menuItemId: params.menuItemId,
37999
+ onSeriesDiscovered: params.onSeriesDiscovered,
37561
38000
  visualRegister: params.visualRegister
37562
38001
  };
37563
38002
  const updated = new Map();
@@ -37634,6 +38073,7 @@
37634
38073
  if (!params.force &&
37635
38074
  newRenderId == oldRenderId &&
37636
38075
  !(existingRego === null || existingRego === void 0 ? void 0 : existingRego.stale) &&
38076
+ !isStyleChanged(existingRego, zoomItem) &&
37637
38077
  !isOutlineChanged$1(existingRego, entity)) {
37638
38078
  // No sorting category needed. Already rendered the way we want.
37639
38079
  cEntities.set(id, existingRego.visual);
@@ -38519,7 +38959,7 @@
38519
38959
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
38520
38960
  })(exports.StyleUtils || (exports.StyleUtils = {}));
38521
38961
 
38522
- const VERSION = "6.9.6";
38962
+ const VERSION = "6.9.8";
38523
38963
  /**
38524
38964
  * Updates the environment instance used by bruce-cesium to one specified.
38525
38965
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.
@@ -38539,6 +38979,7 @@
38539
38979
  exports.VERSION = VERSION;
38540
38980
  exports.setENVIRONMENT = setENVIRONMENT;
38541
38981
  exports.getENVIRONMENT = getENVIRONMENT;
38982
+ exports.isStyleChanged = isStyleChanged;
38542
38983
  exports.isOutlineChanged = isOutlineChanged$1;
38543
38984
  exports.CesiumParabola = CesiumParabola;
38544
38985
  exports.createTileset = createTileset;