bruce-cesium 7.5.4 → 7.5.6

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 (25) hide show
  1. package/dist/bruce-cesium.es5.js +850 -348
  2. package/dist/bruce-cesium.es5.js.map +1 -1
  3. package/dist/bruce-cesium.umd.js +848 -346
  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 +24 -1
  7. package/dist/lib/internal/image-utils.js.map +1 -1
  8. package/dist/lib/rendering/entity-render-engine-point.js +16 -25
  9. package/dist/lib/rendering/entity-render-engine-point.js.map +1 -1
  10. package/dist/lib/rendering/entity-render-engine-polygon.js +16 -14
  11. package/dist/lib/rendering/entity-render-engine-polygon.js.map +1 -1
  12. package/dist/lib/rendering/render-managers/common/entity-label-group.js +178 -64
  13. package/dist/lib/rendering/render-managers/common/entity-label-group.js.map +1 -1
  14. package/dist/lib/rendering/render-managers/common/entity-label.js +437 -71
  15. package/dist/lib/rendering/render-managers/common/entity-label.js.map +1 -1
  16. package/dist/lib/rendering/render-managers/other/google-search-render-manager.js +32 -37
  17. package/dist/lib/rendering/render-managers/other/google-search-render-manager.js.map +1 -1
  18. package/dist/lib/rendering/visuals-register.js +16 -2
  19. package/dist/lib/rendering/visuals-register.js.map +1 -1
  20. package/dist/types/bruce-cesium.d.ts +1 -1
  21. package/dist/types/internal/image-utils.d.ts +1 -0
  22. package/dist/types/rendering/render-managers/common/entity-label-group.d.ts +9 -1
  23. package/dist/types/rendering/render-managers/common/entity-label.d.ts +13 -2
  24. package/dist/types/rendering/visuals-register.d.ts +1 -0
  25. package/package.json +1 -1
@@ -5515,11 +5515,196 @@
5515
5515
  const ICON_RING_PADDING = 4;
5516
5516
  // Small points still get a ring as large as the default near ring.
5517
5517
  const ICON_RING_MIN = 10;
5518
+ // A photo grows by this much while its entity is hovered, since it is content worth a closer look.
5519
+ const PHOTO_HOVER_SCALE = 2.5;
5520
+ // How long a label must stay on show before its photo is fetched, so labels that only flash past while
5521
+ // clusters settle or the camera sweeps by cost nothing.
5522
+ const PHOTO_SHOW_DELAY = 250;
5523
+ // Photos rest this much larger than an icon on its chip, since they are content rather than a marker.
5524
+ const PHOTO_SIZE_SCALE = 1.5;
5525
+ // Corner radius of a photo too oblong for a circle, relative to its longest side.
5526
+ const PHOTO_CORNER = 0.15;
5527
+ // How long a photo takes to grow into its label, and to grow on hover.
5528
+ const PHOTO_TRANSITION = 250;
5529
+ // Downsized photos kept per viewer before the least recently used is released.
5530
+ const PHOTO_CACHE_LIMIT = 500;
5531
+ // Key where the viewer keeps its downsized photos.
5532
+ const PHOTO_CACHE_KEY = "_nextspaceLabelPhotos";
5518
5533
  // One per tinted icon, since an SVG mask is addressed by id across the whole document.
5519
5534
  let tintCounter = 0;
5520
5535
  // Key where the label instance is stored within the cesium entity.
5521
5536
  // Eg: rego[ATTACH_KEY] = new EntityLabel(...);
5522
5537
  EntityLabel.ATTACH_KEY = "nextspace-navigator-entity-label";
5538
+ // Image types a label draws, read from the file extension. Anything else stays text.
5539
+ const IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp"];
5540
+ // Shortest side a thumbnail needs before it is preferred over the full file.
5541
+ const IMAGE_THUMBNAIL_MIN = 64;
5542
+ // Size a found image is drawn at when the group names none, so a photo never lands at its natural size.
5543
+ const IMAGE_SIZE = 24;
5544
+ function isImageFile(name) {
5545
+ return typeof name === "string" && IMAGE_EXTENSIONS.includes(name.split(".").pop().toLowerCase());
5546
+ }
5547
+ // Whether a label was configured with an icon of its own, which always outranks a found photo.
5548
+ function HasIcon(options) {
5549
+ return Boolean(options.iconUrl || options.iconClass || options.icon);
5550
+ }
5551
+ EntityLabel.HasIcon = HasIcon;
5552
+ /*
5553
+ * Whether a label line is an http(s) link to an image, judged by its path so a query string cannot pass for one.
5554
+ */
5555
+ function IsImageUrl(value) {
5556
+ const text = value === null || value === void 0 ? void 0 : value.trim();
5557
+ if (!text || /\s/.test(text)) {
5558
+ return false;
5559
+ }
5560
+ let url;
5561
+ try {
5562
+ url = new URL(text);
5563
+ }
5564
+ catch {
5565
+ return false;
5566
+ }
5567
+ return (url.protocol === "http:" || url.protocol === "https:") && isImageFile(url.pathname);
5568
+ }
5569
+ EntityLabel.IsImageUrl = IsImageUrl;
5570
+ /*
5571
+ * The entity's default image as InfoView picks it: its first photo attachment by display order that is an image file.
5572
+ * Reads attachments expanded onto the entity, and prefers the smallest thumbnail that still covers an icon.
5573
+ */
5574
+ function GetDefaultImageUrl(entity, api) {
5575
+ var _a, _b, _c;
5576
+ const attachments = (_b = (_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.Attachment) !== null && _b !== void 0 ? _b : [];
5577
+ const photo = attachments
5578
+ .filter(x => (x === null || x === void 0 ? void 0 : x["EntityAttachmentType.ID"]) == BModels.EntityAttachmentType.EType.Photo && x.ClientFile)
5579
+ .filter(x => isImageFile(x.ClientFile.FileExt || x.ClientFile.OriginalFileName))
5580
+ .sort((a, b) => { var _a, _b; return ((_a = a.DisplayOrder) !== null && _a !== void 0 ? _a : 0) - ((_b = b.DisplayOrder) !== null && _b !== void 0 ? _b : 0); })[0];
5581
+ if (!photo) {
5582
+ return null;
5583
+ }
5584
+ const thumbnail = ((_c = photo.ClientFile.Thumbnails) !== null && _c !== void 0 ? _c : [])
5585
+ .filter(x => Math.min(x.Width, x.Height) >= IMAGE_THUMBNAIL_MIN && (x.URL || x["ClientFile.ID"]))
5586
+ .sort((a, b) => a.Width * a.Height - b.Width * b.Height)[0];
5587
+ if (thumbnail) {
5588
+ return thumbnail.URL || BModels.ClientFile.GetUrl({ fileId: thumbnail["ClientFile.ID"], api, viaCdn: true });
5589
+ }
5590
+ return photo.ClientFile.URL || photo.URL || BModels.ClientFile.GetUrl({ fileId: photo["ClientFile.ID"], api, viaCdn: true });
5591
+ }
5592
+ EntityLabel.GetDefaultImageUrl = GetDefaultImageUrl;
5593
+ /*
5594
+ * Gives a label an image it found rather than one it was configured with.
5595
+ * A text label gains it beside the text, or alone when the image was all the text there was.
5596
+ */
5597
+ function WithPhoto(options, url) {
5598
+ var _a, _b, _c, _d;
5599
+ const presentation = (_a = options.presentation) !== null && _a !== void 0 ? _a : "text";
5600
+ options.iconUrl = url;
5601
+ options.iconPhoto = true;
5602
+ // A tint would flatten a photo into a silhouette of its own outline.
5603
+ options.iconColor = undefined;
5604
+ (_b = options.iconSize) !== null && _b !== void 0 ? _b : (options.iconSize = IMAGE_SIZE);
5605
+ if (!((_c = options.text) === null || _c === void 0 ? void 0 : _c.trim())) {
5606
+ options.presentation = "icon";
5607
+ }
5608
+ else if (presentation === "text") {
5609
+ options.presentation = "icon-text-side";
5610
+ }
5611
+ if (presentation === "text") {
5612
+ // Keeps the leader a text label draws by default.
5613
+ (_d = options.leader) !== null && _d !== void 0 ? _d : (options.leader = true);
5614
+ }
5615
+ return options;
5616
+ }
5617
+ EntityLabel.WithPhoto = WithPhoto;
5618
+ /*
5619
+ * Near-square photos are cropped to a circle the size of an icon chip; the rest keep their shape with rounded corners.
5620
+ * An unknown size reads as square, since the circle crops whatever arrives.
5621
+ */
5622
+ function PhotoShape(width, height) {
5623
+ if (!(width > 0 && height > 0)) {
5624
+ return "circle";
5625
+ }
5626
+ const longest = Math.max(width, height);
5627
+ return (longest - Math.min(width, height)) / longest <= ICON_BACKGROUND_UNIFORM_TOLERANCE ? "circle" : "rect";
5628
+ }
5629
+ EntityLabel.PhotoShape = PhotoShape;
5630
+ /*
5631
+ * Loads a photo through a plain image element, so a host without CORS still works, and redraws it once onto a canvas
5632
+ * no larger than it is shown. Labels draw from that canvas, so none of them holds a camera original.
5633
+ */
5634
+ async function shrinkPhoto(url, pixels) {
5635
+ const none = { source: null, width: 0, height: 0 };
5636
+ const image = new Image();
5637
+ image.decoding = "async";
5638
+ image.src = url;
5639
+ try {
5640
+ await image.decode();
5641
+ }
5642
+ catch {
5643
+ return none;
5644
+ }
5645
+ const width = image.naturalWidth;
5646
+ const height = image.naturalHeight;
5647
+ if (!width || !height) {
5648
+ return none;
5649
+ }
5650
+ // A circle only ever shows the centre square, so that is all worth keeping.
5651
+ const circle = PhotoShape(width, height) === "circle";
5652
+ const sourceWidth = circle ? Math.min(width, height) : width;
5653
+ const sourceHeight = circle ? Math.min(width, height) : height;
5654
+ const scale = Math.min(1, pixels / Math.max(sourceWidth, sourceHeight));
5655
+ const canvas = document.createElement("canvas");
5656
+ canvas.width = Math.max(1, Math.round(sourceWidth * scale));
5657
+ canvas.height = Math.max(1, Math.round(sourceHeight * scale));
5658
+ const context = canvas.getContext("2d");
5659
+ if (!context) {
5660
+ return none;
5661
+ }
5662
+ context.imageSmoothingQuality = "high";
5663
+ context.drawImage(image, (width - sourceWidth) / 2, (height - sourceHeight) / 2, sourceWidth, sourceHeight, 0, 0, canvas.width, canvas.height);
5664
+ return { source: canvas, width, height };
5665
+ }
5666
+ /*
5667
+ * Downsized label photos, kept on the viewer so every label in a scene shares one load and one decode.
5668
+ */
5669
+ class Photos {
5670
+ constructor() {
5671
+ this.entries = new Map();
5672
+ this.ready = new Map();
5673
+ }
5674
+ static Get(viewer) {
5675
+ var _a;
5676
+ return (_a = viewer[PHOTO_CACHE_KEY]) !== null && _a !== void 0 ? _a : (viewer[PHOTO_CACHE_KEY] = new Photos());
5677
+ }
5678
+ // The photo when it is already drawn, so a rebuilt label shows it at once instead of growing it again.
5679
+ Peek(url, pixels) {
5680
+ return this.ready.get(`${pixels} ${url}`);
5681
+ }
5682
+ Load(url, pixels) {
5683
+ const key = `${pixels} ${url}`;
5684
+ let entry = this.entries.get(key);
5685
+ if (entry) {
5686
+ // Reinsertion keeps the map in least recently used order.
5687
+ this.entries.delete(key);
5688
+ this.entries.set(key, entry);
5689
+ return entry;
5690
+ }
5691
+ entry = shrinkPhoto(url, pixels).then(photo => {
5692
+ if (this.entries.get(key) === entry) {
5693
+ this.ready.set(key, photo);
5694
+ }
5695
+ return photo;
5696
+ });
5697
+ this.entries.set(key, entry);
5698
+ for (const oldest of this.entries.keys()) {
5699
+ if (this.entries.size <= PHOTO_CACHE_LIMIT) {
5700
+ break;
5701
+ }
5702
+ this.entries.delete(oldest);
5703
+ this.ready.delete(oldest);
5704
+ }
5705
+ return entry;
5706
+ }
5707
+ }
5523
5708
  /*
5524
5709
  * Indexes screen rectangles so sparse scenes do not compare every label with every other label.
5525
5710
  */
@@ -6235,6 +6420,10 @@
6235
6420
  this.cursorMotion = { amount: 0, from: 0, target: 0, start: 0, position: undefined };
6236
6421
  // Counter to help differentiate between createLabel calls.
6237
6422
  this.counter = 0;
6423
+ // Whether this label has asked its group for a default image, which it does once on screen.
6424
+ this.imageAsked = false;
6425
+ // The photo a label outside any group found while reading its attributes.
6426
+ this.photo = null;
6238
6427
  // The label element itself.
6239
6428
  this.label = null;
6240
6429
  // Callback to kill render listener.
@@ -6269,6 +6458,7 @@
6269
6458
  this.visible = params.visible;
6270
6459
  if (((_c = params.options) === null || _c === void 0 ? void 0 : _c.text) != null)
6271
6460
  this.additionalTexts = [];
6461
+ this.photo = null;
6272
6462
  this.prePositioned = !isNaN((_d = params.pos3d) === null || _d === void 0 ? void 0 : _d.x);
6273
6463
  this.createLabel(params.pos3d);
6274
6464
  if (!params.api && !BModels.ENVIRONMENT.IS_SELF_MANAGED && this.options.text == null) {
@@ -6331,6 +6521,7 @@
6331
6521
  this.options = options || {};
6332
6522
  this.groupId = groupId;
6333
6523
  this.additionalTexts = this.options.text != null ? [] : null;
6524
+ this.photo = null;
6334
6525
  this.createLabel();
6335
6526
  }
6336
6527
  /**
@@ -6338,8 +6529,8 @@
6338
6529
  * @param starterPos3d optional position to create label at. Ignored for billboard labels.
6339
6530
  */
6340
6531
  async createLabel(starterPos3d) {
6341
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
6342
- this.removeLabel();
6532
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
6533
+ // The old label stays until its replacement is built, so a rebuild never blinks while a location loads.
6343
6534
  let pos3d = starterPos3d ? starterPos3d.clone() : null;
6344
6535
  const counter = ++this.counter;
6345
6536
  if (this.disposed) {
@@ -6497,6 +6688,7 @@
6497
6688
  }
6498
6689
  }
6499
6690
  if (isNaN(pos3d === null || pos3d === void 0 ? void 0 : pos3d.x)) {
6691
+ this.removeLabel();
6500
6692
  return;
6501
6693
  }
6502
6694
  pos3d = exports.DrawingUtils.EnsurePosHeight({
@@ -6506,6 +6698,7 @@
6506
6698
  viewer: this.viewer
6507
6699
  });
6508
6700
  if (isNaN(pos3d === null || pos3d === void 0 ? void 0 : pos3d.x)) {
6701
+ this.removeLabel();
6509
6702
  return;
6510
6703
  }
6511
6704
  this._pos3d = pos3d;
@@ -6571,35 +6764,39 @@
6571
6764
  catch (e) {
6572
6765
  console.error(e);
6573
6766
  }
6574
- // Type could be deleted, however we'll just catch that to show a label instead of nothing.
6575
- // This is not unusual for Tilesets where the Type that is baked might be outdated.
6576
- if (entityType || this.options.attributes) {
6577
- const attributes = (_a = this.options.attributes) !== null && _a !== void 0 ? _a : (_c = (_b = entityType.VisualSettings) === null || _b === void 0 ? void 0 : _b.label) === null || _c === void 0 ? void 0 : _c.attributes;
6578
- if (attributes) {
6579
- try {
6580
- const { entity } = await BModels.Entity.Get({
6581
- api: this.api,
6582
- entityId: this.rego.entityId,
6583
- 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,
6584
- schemaId: this.rego.schema,
6585
- migrated: true
6586
- });
6587
- for (let i = 0; i < attributes.length; i++) {
6588
- const value = BModels.Entity.GetValue({
6589
- entity: entity,
6590
- path: attributes[i]
6591
- });
6592
- if (value != null) {
6593
- additionalTexts.push(value);
6594
- }
6595
- }
6596
- }
6597
- // Entity doesn't exist.
6598
- // Likely a graphic with an ID that points to no record.
6599
- catch (e) {
6600
- console.error(e);
6767
+ 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;
6768
+ // Read for its attributes, and for a photo when the label has no icon of its own.
6769
+ if (attributes || !HasIcon(this.options)) {
6770
+ let entity = null;
6771
+ try {
6772
+ entity = (await BModels.Entity.Get({
6773
+ api: this.api,
6774
+ entityId: this.rego.entityId,
6775
+ 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,
6776
+ schemaId: this.rego.schema,
6777
+ migrated: true,
6778
+ expandAttachments: !HasIcon(this.options)
6779
+ })).entity;
6780
+ }
6781
+ // Entity doesn't exist.
6782
+ // Likely a graphic with an ID that points to no record.
6783
+ catch (e) {
6784
+ console.error(e);
6785
+ }
6786
+ for (let i = 0; entity && attributes && i < attributes.length; i++) {
6787
+ const value = BModels.Entity.GetValue({
6788
+ entity: entity,
6789
+ path: attributes[i]
6790
+ });
6791
+ if (value != null) {
6792
+ additionalTexts.push(value);
6601
6793
  }
6602
6794
  }
6795
+ // A linked image outranks the default one, as it does for group labels.
6796
+ if (entity && !HasIcon(this.options)) {
6797
+ const at = additionalTexts.findIndex(value => IsImageUrl(String(value)));
6798
+ this.photo = at >= 0 ? String(additionalTexts.splice(at, 1)[0]).trim() : GetDefaultImageUrl(entity, this.api);
6799
+ }
6603
6800
  }
6604
6801
  }
6605
6802
  this.additionalTexts = additionalTexts;
@@ -6618,9 +6815,13 @@
6618
6815
  if (this.disposed || this.counter != counter) {
6619
6816
  return;
6620
6817
  }
6621
- const presentation = this.options.presentation || "text";
6818
+ this.removeLabel();
6819
+ // A found photo draws as if configured, without changing the options clustering compares and measures.
6820
+ const photo = HasIcon(this.options) ? null : (_j = this.options.photo) !== null && _j !== void 0 ? _j : this.photo;
6821
+ const look = photo ? WithPhoto({ ...this.options, text: (_k = this.options.text) !== null && _k !== void 0 ? _k : [text, ...additionalTexts].join("\n") }, photo) : this.options;
6822
+ const presentation = look.presentation || "text";
6622
6823
  let clickEntityIds = [];
6623
- const selectOnClick = (_j = this.options.selectOnClick) !== null && _j !== void 0 ? _j : Boolean(this.groupId);
6824
+ const selectOnClick = (_l = this.options.selectOnClick) !== null && _l !== void 0 ? _l : Boolean(this.groupId);
6624
6825
  const select = (event) => {
6625
6826
  event.preventDefault();
6626
6827
  event.stopPropagation();
@@ -6649,11 +6850,11 @@
6649
6850
  target.addEventListener(type, event => event.stopPropagation());
6650
6851
  }
6651
6852
  };
6652
- const normalLeader = (_k = this.options.leader) !== null && _k !== void 0 ? _k : presentation === "text";
6853
+ const normalLeader = (_m = look.leader) !== null && _m !== void 0 ? _m : presentation === "text";
6653
6854
  const angled = this.options.leaderStyle === "angled";
6654
6855
  let leader = normalLeader;
6655
6856
  const content = document.createElement("span");
6656
- const originalText = (_l = this.options.text) !== null && _l !== void 0 ? _l : [text, ...additionalTexts].join("\n");
6857
+ const originalText = (_o = this.options.text) !== null && _o !== void 0 ? _o : [text, ...additionalTexts].join("\n");
6657
6858
  let textElement;
6658
6859
  let sourceText;
6659
6860
  let sourceNaturalText;
@@ -6668,10 +6869,33 @@
6668
6869
  if (presentation === "icon-text-above") {
6669
6870
  content.style.flexDirection = "column-reverse";
6670
6871
  }
6872
+ // A found photo's shape decides its layout once it loads: a rectangle sits under the name it adds to, a circle beside it.
6873
+ let photoShape = null;
6874
+ let growPhoto = null;
6875
+ // Loads the photo; called once the label is actually showing, so labels out of view or folded into a cluster never fetch one.
6876
+ let startPhoto = null;
6877
+ let photoTimer = null;
6878
+ const placePhoto = (shape, grow) => {
6879
+ photoShape = shape;
6880
+ growPhoto = grow;
6881
+ content.style.flexDirection = shape === "rect" ? "column-reverse" : "";
6882
+ };
6671
6883
  if (presentation !== "text") {
6672
- const built = this.buildIcon(this.options, counter);
6884
+ const built = this.buildIcon(look, counter, placePhoto);
6885
+ startPhoto = built.start;
6673
6886
  const icon = iconElement = built.icon;
6674
6887
  fitIconBackground = built.fit;
6888
+ if (built.arriving) {
6889
+ // The gap opens with the photo, so a text label does not shift before its photo grows in.
6890
+ content.style.gap = "0px";
6891
+ built.arriving.then(() => {
6892
+ if (iconElement.style.display === "none") {
6893
+ return;
6894
+ }
6895
+ content.style.transition = `gap ${PHOTO_TRANSITION}ms ease-out`;
6896
+ content.style.gap = "6px";
6897
+ });
6898
+ }
6675
6899
  makeClickable(icon);
6676
6900
  // Keep badges outside the glyph so font classes and image masks cannot affect them.
6677
6901
  iconContainer = document.createElement("span");
@@ -6693,7 +6917,7 @@
6693
6917
  if (presentation === "text")
6694
6918
  makeClickable(textElement);
6695
6919
  content.appendChild(textElement);
6696
- if ((_m = this.options.cluster) === null || _m === void 0 ? void 0 : _m.enabled) {
6920
+ if ((_p = this.options.cluster) === null || _p === void 0 ? void 0 : _p.enabled) {
6697
6921
  sourceText = document.createElement("span");
6698
6922
  sourceText.className = "entity-label-source-text";
6699
6923
  sourceText.setAttribute("aria-hidden", "true");
@@ -6713,7 +6937,9 @@
6713
6937
  countElement.style.cssText = "flex:none;align-self:center;white-space:nowrap;background:#fff;color:#000;text-shadow:none;border-radius:8px;padding:0 3px;display:none;";
6714
6938
  // Text above the icon would have the badge land in it, so the free corner is the lower one.
6715
6939
  const badgeBelow = presentation === "icon-text-above";
6716
- if (iconContainer) {
6940
+ // A photo is put away while its label stands for a cluster, so its count sits with the text instead.
6941
+ const badgeOnIcon = Boolean(iconContainer) && !look.iconPhoto;
6942
+ if (badgeOnIcon) {
6717
6943
  countElement.style.cssText += `position:absolute;left:100%;top:${badgeBelow ? "100%" : "0"};transform:translate(-50%,-50%);`;
6718
6944
  iconContainer.appendChild(countElement);
6719
6945
  }
@@ -6730,11 +6956,11 @@
6730
6956
  else {
6731
6957
  ele = document.createElement("div");
6732
6958
  ele.appendChild(content);
6733
- if (!iconContainer) {
6959
+ if (!badgeOnIcon) {
6734
6960
  ele.appendChild(countElement);
6735
6961
  }
6736
6962
  ele.className = "entity-label";
6737
- ele.dataset.entityId = ((_o = this.rego) === null || _o === void 0 ? void 0 : _o.entityId) || "";
6963
+ ele.dataset.entityId = ((_q = this.rego) === null || _q === void 0 ? void 0 : _q.entityId) || "";
6738
6964
  ele.dataset.presentation = presentation;
6739
6965
  if (this.groupId) {
6740
6966
  ele.dataset.groupId = this.groupId;
@@ -6887,7 +7113,7 @@
6887
7113
  };
6888
7114
  let cursorMotion = this.cursorMotion;
6889
7115
  const project = () => {
6890
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
7116
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
6891
7117
  if (this.disposed || !this.viewer || this.viewer.isDestroyed() || ((_a = this.visible) === null || _a === void 0 ? void 0 : _a.call(this)) === false ||
6892
7118
  (presentation === "icon" && this.options.iconScale === 0)) {
6893
7119
  return null;
@@ -6925,13 +7151,18 @@
6925
7151
  if (!Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.x) || !Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.y)) {
6926
7152
  return null;
6927
7153
  }
6928
- if (leader && (anchor.x < 0 || anchor.y < 0 || anchor.x >= this.viewer.canvas.clientWidth || anchor.y >= this.viewer.canvas.clientHeight))
7154
+ const onScreen = anchor.x >= 0 && anchor.y >= 0 && anchor.x < this.viewer.canvas.clientWidth && anchor.y < this.viewer.canvas.clientHeight;
7155
+ if (leader && !onScreen)
6929
7156
  return null;
7157
+ if (onScreen && !this.imageAsked && this.groupId && !HasIcon(this.options) && !this.options.photo) {
7158
+ this.imageAsked = true;
7159
+ (_e = this.visualRegister) === null || _e === void 0 ? void 0 : _e.RequestLabelImage((_f = this.rego) === null || _f === void 0 ? void 0 : _f.entityId);
7160
+ }
6930
7161
  const transientPivot = (this.Transient && !this.Persistent ?
6931
- (this.hoverFade.to === 0 ? this.retainedHoverPosition : (_e = this.visualRegister) === null || _e === void 0 ? void 0 : _e.GetLabelHoverPosition((_f = this.rego) === null || _f === void 0 ? void 0 : _f.entityId, (_g = this.rego) === null || _g === void 0 ? void 0 : _g.menuItemId)) : undefined);
7162
+ (this.hoverFade.to === 0 ? this.retainedHoverPosition : (_g = this.visualRegister) === null || _g === void 0 ? void 0 : _g.GetLabelHoverPosition((_h = this.rego) === null || _h === void 0 ? void 0 : _h.entityId, (_j = this.rego) === null || _j === void 0 ? void 0 : _j.menuItemId)) : undefined);
6932
7163
  const hover = (presentation !== "icon" ?
6933
7164
  (this.Transient && !this.Persistent && this.hoverFade.to === 0 ? this.retainedHoverPosition :
6934
- (_h = this.visualRegister) === null || _h === void 0 ? void 0 : _h.GetLabelHoverPosition((_j = this.rego) === null || _j === void 0 ? void 0 : _j.entityId, (_k = this.rego) === null || _k === void 0 ? void 0 : _k.menuItemId)) : undefined);
7165
+ (_k = this.visualRegister) === null || _k === void 0 ? void 0 : _k.GetLabelHoverPosition((_l = this.rego) === null || _l === void 0 ? void 0 : _l.entityId, (_m = this.rego) === null || _m === void 0 ? void 0 : _m.menuItemId)) : undefined);
6935
7166
  const reused = !this.Transient || this.Persistent;
6936
7167
  const now = Date.now();
6937
7168
  const animateCursor = () => {
@@ -6958,8 +7189,8 @@
6958
7189
  const icon = measureIcon();
6959
7190
  const iconRing = icon ? Math.max(ICON_RING_MIN, icon.radius + ICON_RING_PADDING) : 0;
6960
7191
  const pivot = icon ? undefined : transientPivot;
6961
- let x = ((_l = pivot === null || pivot === void 0 ? void 0 : pivot.x) !== null && _l !== void 0 ? _l : anchor.x) + (icon || pixelOffset).x;
6962
- let y = ((_m = pivot === null || pivot === void 0 ? void 0 : pivot.y) !== null && _m !== void 0 ? _m : anchor.y) + (icon || pixelOffset).y;
7192
+ let x = ((_o = pivot === null || pivot === void 0 ? void 0 : pivot.x) !== null && _o !== void 0 ? _o : anchor.x) + (icon || pixelOffset).x;
7193
+ let y = ((_p = pivot === null || pivot === void 0 ? void 0 : pivot.y) !== null && _p !== void 0 ? _p : anchor.y) + (icon || pixelOffset).y;
6963
7194
  if (cursorAmount > 0 && !icon) {
6964
7195
  // A leader-less label starts centred on its anchor; preserve that box as it lifts away.
6965
7196
  if (!normalLeader) {
@@ -7011,11 +7242,12 @@
7011
7242
  const width = ele.offsetWidth / coordinates.scaleX;
7012
7243
  const height = ele.offsetHeight / coordinates.scaleY;
7013
7244
  let clusterBounds;
7014
- if ((_o = this.options.cluster) === null || _o === void 0 ? void 0 : _o.enabled) {
7245
+ if ((_q = this.options.cluster) === null || _q === void 0 ? void 0 : _q.enabled) {
7015
7246
  const textWidth = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetWidth) || 0;
7016
7247
  const textHeight = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetHeight) || 0;
7017
- const iconWidth = (iconElement === null || iconElement === void 0 ? void 0 : iconElement.offsetWidth) || 0;
7018
- const iconHeight = (iconElement === null || iconElement === void 0 ? void 0 : iconElement.offsetHeight) || 0;
7248
+ // A photo is never part of the footprint, so its arrival cannot change which cluster a label joins.
7249
+ const iconWidth = look.iconPhoto ? 0 : (iconElement === null || iconElement === void 0 ? void 0 : iconElement.offsetWidth) || 0;
7250
+ const iconHeight = look.iconPhoto ? 0 : (iconElement === null || iconElement === void 0 ? void 0 : iconElement.offsetHeight) || 0;
7019
7251
  const gap = sourceText && iconWidth ? 6 : 0;
7020
7252
  const above = presentation === "icon-text-above";
7021
7253
  // Match the fixed content gap and label padding, excluding the count badge.
@@ -7044,7 +7276,7 @@
7044
7276
  if (!leader && (bounds.x + bounds.width < 0 || bounds.y + bounds.height < 0 || bounds.x > this.viewer.canvas.clientWidth || bounds.y > this.viewer.canvas.clientHeight)) {
7045
7277
  return null;
7046
7278
  }
7047
- const state = (_p = this.visualRegister) === null || _p === void 0 ? void 0 : _p.GetState({ entityId: (_q = this.rego) === null || _q === void 0 ? void 0 : _q.entityId, menuItemId: (_r = this.rego) === null || _r === void 0 ? void 0 : _r.menuItemId });
7279
+ const state = (_r = this.visualRegister) === null || _r === void 0 ? void 0 : _r.GetState({ entityId: (_s = this.rego) === null || _s === void 0 ? void 0 : _s.entityId, menuItemId: (_t = this.rego) === null || _t === void 0 ? void 0 : _t.menuItemId });
7048
7280
  const opacity = this.prePositioned || hoverOnly || MAX_DISTANCE === Infinity ? 1 : 1 - distance / MAX_DISTANCE;
7049
7281
  return {
7050
7282
  canFlip: leader && presentation !== "icon", hasLeader: leader,
@@ -7052,8 +7284,8 @@
7052
7284
  transient: this.Transient || Boolean(hover) || cursorAmount > 0,
7053
7285
  extendedGeometry: visual instanceof Cesium.Entity && Boolean(visual.polygon || visual.polyline || visual.corridor),
7054
7286
  viewportWidth: this.viewer.canvas.clientWidth,
7055
- id: this.id, entityKey: ((_s = this.rego) === null || _s === void 0 ? void 0 : _s.entityId) ? JSON.stringify([this.rego.entityId, this.rego.menuItemId || "", this.Transient && !this.Persistent ? "hover" : ""]) : undefined,
7056
- entityId: (_t = this.rego) === null || _t === void 0 ? void 0 : _t.entityId, group: this.groupId, x, y, width, height, anchorX, anchorY, depth, leaderLength, leaderRun, leaderSide, distance,
7287
+ id: this.id, entityKey: ((_u = this.rego) === null || _u === void 0 ? void 0 : _u.entityId) ? JSON.stringify([this.rego.entityId, this.rego.menuItemId || "", this.Transient && !this.Persistent ? "hover" : ""]) : undefined,
7288
+ entityId: (_v = this.rego) === null || _v === void 0 ? void 0 : _v.entityId, group: this.groupId, x, y, width, height, anchorX, anchorY, depth, leaderLength, leaderRun, leaderSide, distance,
7057
7289
  iconRadius: icon === null || icon === void 0 ? void 0 : icon.radius,
7058
7290
  // Priority shows the hovered member individually without dragging its cluster along.
7059
7291
  opacity, priority: hover || cursorAmount > 0 || (state === null || state === void 0 ? void 0 : state.highlighted) ? 1 : 0, options: this.options, text: originalText, clusterBounds
@@ -7062,7 +7294,7 @@
7062
7294
  let displacement = { x: 0, y: 0 };
7063
7295
  let nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
7064
7296
  const apply = (placement) => {
7065
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2;
7297
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4;
7066
7298
  const show = (placement === null || placement === void 0 ? void 0 : placement.visible) && placement.opacity > 0;
7067
7299
  clickEntityIds = show ? placement.entityIds : [];
7068
7300
  ele.style.visibility = show ? "visible" : "hidden";
@@ -7076,9 +7308,30 @@
7076
7308
  if (!this.Transient) {
7077
7309
  this.ringAnimation = undefined;
7078
7310
  }
7311
+ clearTimeout(photoTimer);
7312
+ photoTimer = null;
7079
7313
  return;
7080
7314
  }
7081
- const displayText = (_a = placement.text) !== null && _a !== void 0 ? _a : originalText;
7315
+ // A label standing for a cluster shows its summary only; its photo is put away and never fetched.
7316
+ const solo = !((_a = placement.members) === null || _a === void 0 ? void 0 : _a.length);
7317
+ if (look.iconPhoto && iconElement && iconElement.dataset.failed !== "true") {
7318
+ const display = solo ? "inline-flex" : "none";
7319
+ if (iconElement.style.display !== display) {
7320
+ iconElement.style.display = display;
7321
+ }
7322
+ }
7323
+ if (!solo) {
7324
+ clearTimeout(photoTimer);
7325
+ photoTimer = null;
7326
+ }
7327
+ else if (startPhoto && photoTimer == null) {
7328
+ photoTimer = setTimeout(() => {
7329
+ if (!this.disposed && this.counter === counter) {
7330
+ startPhoto();
7331
+ }
7332
+ }, PHOTO_SHOW_DELAY);
7333
+ }
7334
+ const displayText = (_b = placement.text) !== null && _b !== void 0 ? _b : originalText;
7082
7335
  if (textElement && textElement.textContent !== displayText) {
7083
7336
  textElement.textContent = displayText;
7084
7337
  // Measure the summary's footprint next frame without replacing each member's source text.
@@ -7102,10 +7355,12 @@
7102
7355
  const stack = JSON.stringify(placement.icons || []);
7103
7356
  if (iconStack && stack !== displayedStack) {
7104
7357
  displayedStack = stack;
7105
- const looks = ((_b = placement.icons) === null || _b === void 0 ? void 0 : _b.length) ? [iconLook(this.options), ...placement.icons] : [];
7358
+ const looks = ((_c = placement.icons) === null || _c === void 0 ? void 0 : _c.length) ? [iconLook(this.options), ...placement.icons] : [];
7106
7359
  // One small size for all, so no icon in the arrangement reads as more important than another.
7107
- const size = (this.options.iconSize || 24) * ((_c = this.options.iconScale) !== null && _c !== void 0 ? _c : 1) * ICON_STACK_SCALE;
7360
+ const size = (this.options.iconSize || 24) * ((_d = this.options.iconScale) !== null && _d !== void 0 ? _d : 1) * ICON_STACK_SCALE;
7108
7361
  const built = looks.map(look => this.buildIcon({ ...look, iconSize: size }, counter));
7362
+ // A stack is on show already, so its photos load straight away.
7363
+ built.forEach(({ start }) => start === null || start === void 0 ? void 0 : start());
7109
7364
  // Two sit side by side; three make a triangle with the representative on top.
7110
7365
  iconStack.style.gridTemplateColumns = built.length === 3 ? "auto auto" : `repeat(${built.length}, auto)`;
7111
7366
  if (built.length === 3) {
@@ -7122,7 +7377,7 @@
7122
7377
  }
7123
7378
  for (const fit of stackedFits)
7124
7379
  fit();
7125
- if (iconContainer) {
7380
+ if (badgeOnIcon) {
7126
7381
  const badgeWidth = countElement.offsetWidth;
7127
7382
  const iconWidth = iconElement.offsetWidth;
7128
7383
  const narrow = iconWidth < iconElement.offsetHeight * 0.6;
@@ -7137,16 +7392,28 @@
7137
7392
  // Summary and badge changes must fit on their first visible frame, including after resize.
7138
7393
  const ring = this.options.anchorRing;
7139
7394
  const pointer = (layer.pointer || (cursorMotion.target ? cursorMotion.position : undefined) || (this.Transient && !this.Persistent ?
7140
- (this.hoverFade.to === 0 ? this.retainedHoverPosition : (_d = this.visualRegister) === null || _d === void 0 ? void 0 : _d.GetLabelHoverPosition((_e = this.rego) === null || _e === void 0 ? void 0 : _e.entityId, (_f = this.rego) === null || _f === void 0 ? void 0 : _f.menuItemId)) : undefined));
7395
+ (this.hoverFade.to === 0 ? this.retainedHoverPosition : (_e = this.visualRegister) === null || _e === void 0 ? void 0 : _e.GetLabelHoverPosition((_f = this.rego) === null || _f === void 0 ? void 0 : _f.entityId, (_g = this.rego) === null || _g === void 0 ? void 0 : _g.menuItemId)) : undefined));
7141
7396
  const iconRing = frame.iconRadius ? Math.max(ICON_RING_MIN, frame.iconRadius + ICON_RING_PADDING) : 0;
7142
- const hovered = pointer && Math.hypot(pointer.x - frame.anchorX, pointer.y - frame.anchorY) <= Math.max((_g = ring === null || ring === void 0 ? void 0 : ring.hoverDistance) !== null && _g !== void 0 ? _g : 24, iconRing);
7143
- const normalRadius = (!leader || (ring === null || ring === void 0 ? void 0 : ring.enabled) === false ? 0 : Math.max(frame.distance <= ((_h = ring === null || ring === void 0 ? void 0 : ring.nearDistance) !== null && _h !== void 0 ? _h : 100) ? ((_j = ring === null || ring === void 0 ? void 0 : ring.nearRadius) !== null && _j !== void 0 ? _j : 10) : 0, hovered ? ((_k = ring === null || ring === void 0 ? void 0 : ring.hoverRadius) !== null && _k !== void 0 ? _k : (this.Transient && !this.Persistent ? 15.4 : 22)) : 0));
7397
+ const hovered = pointer && Math.hypot(pointer.x - frame.anchorX, pointer.y - frame.anchorY) <= Math.max((_h = ring === null || ring === void 0 ? void 0 : ring.hoverDistance) !== null && _h !== void 0 ? _h : 24, iconRing);
7398
+ if (look.iconPhoto && growPhoto) {
7399
+ // A rectangle leans on the leader's side of the label.
7400
+ if (photoShape === "rect") {
7401
+ const align = !leader ? "center" : placement.textSide === "left" ? "flex-end" : "flex-start";
7402
+ if (content.style.alignItems !== align) {
7403
+ content.style.alignItems = align;
7404
+ }
7405
+ }
7406
+ // Grown in layout rather than scaled, so the border keeps its width, and the label, whose bottom
7407
+ // sits on the leader, grows up and sideways without changing the leader's length.
7408
+ growPhoto(Boolean(hovered) && !((_j = placement.members) === null || _j === void 0 ? void 0 : _j.length));
7409
+ }
7410
+ const normalRadius = (!leader || (ring === null || ring === void 0 ? void 0 : ring.enabled) === false ? 0 : Math.max(frame.distance <= ((_k = ring === null || ring === void 0 ? void 0 : ring.nearDistance) !== null && _k !== void 0 ? _k : 100) ? ((_l = ring === null || ring === void 0 ? void 0 : ring.nearRadius) !== null && _l !== void 0 ? _l : 10) : 0, hovered ? ((_m = ring === null || ring === void 0 ? void 0 : ring.hoverRadius) !== null && _m !== void 0 ? _m : (this.Transient && !this.Persistent ? 15.4 : 22)) : 0));
7144
7411
  const targetRadius = normalRadius + (15.4 - normalRadius) * cursorMotion.amount;
7145
7412
  // Around a glyph the ring keeps the glyph's size and only fades its stroke in, so it never covers the icon.
7146
7413
  const targetOuter = iconRing || targetRadius || 2.5;
7147
7414
  const targetInner = iconRing ? (targetRadius > 0 ? iconRing - 2 : iconRing) : Math.max(0, targetRadius - 2);
7148
7415
  const now = Date.now();
7149
- const duration = (_l = ring === null || ring === void 0 ? void 0 : ring.transitionDuration) !== null && _l !== void 0 ? _l : 200;
7416
+ const duration = (_o = ring === null || ring === void 0 ? void 0 : ring.transitionDuration) !== null && _o !== void 0 ? _o : 200;
7150
7417
  const sharedRing = this.Transient && !this.Persistent && this.hoverFade.to === 1 ? this.hoverRingState : undefined;
7151
7418
  let ringAnimation = sharedRing ? sharedRing.animation : this.ringAnimation;
7152
7419
  if (!ringAnimation) {
@@ -7188,19 +7455,19 @@
7188
7455
  if (leader && angled) {
7189
7456
  // The declutter owns the side; the fit only says which one had the room to begin with.
7190
7457
  const run = Math.max(ANGLED_LEADER_RUN, radius + 4);
7191
- const side = (_o = (_m = placement.textSide) !== null && _m !== void 0 ? _m : frame.leaderSide) !== null && _o !== void 0 ? _o : "right";
7458
+ const side = (_q = (_p = placement.textSide) !== null && _p !== void 0 ? _p : frame.leaderSide) !== null && _q !== void 0 ? _q : "right";
7192
7459
  const beside = side === "left" ? frame.anchorX - run - width : frame.anchorX + run;
7193
7460
  position.x = Math.max(0, Math.min(beside, this.viewer.canvas.clientWidth - width));
7194
7461
  }
7195
7462
  else if (leader && placement.textSide === "left") {
7196
7463
  position.x = Math.max(0, Math.min(frame.anchorX - width, this.viewer.canvas.clientWidth - width));
7197
7464
  }
7198
- const gap = (_q = (_p = this.options.pointerAvoidance) === null || _p === void 0 ? void 0 : _p.gap) !== null && _q !== void 0 ? _q : 12;
7465
+ const gap = (_s = (_r = this.options.pointerAvoidance) === null || _r === void 0 ? void 0 : _r.gap) !== null && _s !== void 0 ? _s : 12;
7199
7466
  const containsPointer = (x, y, margin) => {
7200
7467
  return pointer && pointer.x >= x - margin && pointer.x <= x + width + margin && pointer.y >= y - margin && pointer.y <= y + height + margin;
7201
7468
  };
7202
7469
  const passive = !selectOnClick || !this.visualRegister;
7203
- if (passive && ((_r = this.options.pointerAvoidance) === null || _r === void 0 ? void 0 : _r.enabled) !== false && pointer &&
7470
+ if (passive && ((_t = this.options.pointerAvoidance) === null || _t === void 0 ? void 0 : _t.enabled) !== false && pointer &&
7204
7471
  (containsPointer(position.x, position.y, gap) || containsPointer(position.x + displacement.x, position.y + displacement.y, gap))) {
7205
7472
  if (containsPointer(position.x + displacement.x, position.y + displacement.y, 0)) {
7206
7473
  // Choose against the original box and retain the offset until the pointer leaves both boxes.
@@ -7218,7 +7485,7 @@
7218
7485
  else {
7219
7486
  displacement = { x: 0, y: 0 };
7220
7487
  }
7221
- const nudgeDuration = (_t = (_s = this.options.pointerAvoidance) === null || _s === void 0 ? void 0 : _s.transitionDuration) !== null && _t !== void 0 ? _t : 200;
7488
+ const nudgeDuration = (_v = (_u = this.options.pointerAvoidance) === null || _u === void 0 ? void 0 : _u.transitionDuration) !== null && _v !== void 0 ? _v : 200;
7222
7489
  const animateNudge = () => {
7223
7490
  const progress = now < nudge.start ? 0 : nudgeDuration > 0 ? Math.min(1, (now - nudge.start) / nudgeDuration) : 1;
7224
7491
  const eased = progress * progress * (3 - 2 * progress);
@@ -7234,7 +7501,7 @@
7234
7501
  fromY: nudge.y,
7235
7502
  targetX: displacement.x,
7236
7503
  targetY: displacement.y,
7237
- start: now + ((_v = (_u = this.options.pointerAvoidance) === null || _u === void 0 ? void 0 : _u.delay) !== null && _v !== void 0 ? _v : 180)
7504
+ start: now + ((_x = (_w = this.options.pointerAvoidance) === null || _w === void 0 ? void 0 : _w.delay) !== null && _x !== void 0 ? _x : 180)
7238
7505
  };
7239
7506
  }
7240
7507
  if (animateNudge() < 1 && (nudge.fromX !== nudge.targetX || nudge.fromY !== nudge.targetY)) {
@@ -7245,7 +7512,7 @@
7245
7512
  ele.style.left = `${coordinates.x + position.x * coordinates.scaleX}px`;
7246
7513
  ele.style.top = `${coordinates.y + position.y * coordinates.scaleY}px`;
7247
7514
  for (const clickTarget of Array.from(content.querySelectorAll('[role="button"]'))) {
7248
- clickTarget.setAttribute("aria-label", placement.members.length ? `Select ${placement.entityIds.length} entities` : `Select ${(_x = (_w = this.options.text) !== null && _w !== void 0 ? _w : text) !== null && _x !== void 0 ? _x : (_y = this.rego) === null || _y === void 0 ? void 0 : _y.entityId}`);
7515
+ clickTarget.setAttribute("aria-label", placement.members.length ? `Select ${placement.entityIds.length} entities` : `Select ${(_z = (_y = this.options.text) !== null && _y !== void 0 ? _y : text) !== null && _z !== void 0 ? _z : (_0 = this.rego) === null || _0 === void 0 ? void 0 : _0.entityId}`);
7249
7516
  }
7250
7517
  if (leader) {
7251
7518
  const nodeRadius = ringAnimation.outer;
@@ -7285,13 +7552,13 @@
7285
7552
  // Keep the stem inside the 126-degree arc, away from antialiased endpoints.
7286
7553
  const arcFraction = 0.35;
7287
7554
  const rotationLimit = Math.PI * arcFraction - Math.PI / 30;
7288
- const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_z = ringAnimation.arcOffset) !== null && _z !== void 0 ? _z : 0);
7555
+ const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_1 = ringAnimation.arcOffset) !== null && _1 !== void 0 ? _1 : 0);
7289
7556
  const targetOpen = cursorMotion.target || (hovered && (ring === null || ring === void 0 ? void 0 : ring.enabled) !== false && targetRadius > 0) ? 1 : 0;
7290
- const elapsed = Math.min(32, Math.max(0, now - ((_0 = ringAnimation.arcTime) !== null && _0 !== void 0 ? _0 : now)));
7557
+ const elapsed = Math.min(32, Math.max(0, now - ((_2 = ringAnimation.arcTime) !== null && _2 !== void 0 ? _2 : now)));
7291
7558
  const blend = duration > 0 ? 1 - Math.exp(-elapsed / (duration / 3)) : 1;
7292
7559
  const approach = (value, target) => Math.abs(value - target) < 0.001 ? target : value + (target - value) * blend;
7293
- ringAnimation.arcOffset = approach((_1 = ringAnimation.arcOffset) !== null && _1 !== void 0 ? _1 : 0, targetOffset);
7294
- ringAnimation.arcOpen = approach((_2 = ringAnimation.arcOpen) !== null && _2 !== void 0 ? _2 : 0, targetOpen);
7560
+ ringAnimation.arcOffset = approach((_3 = ringAnimation.arcOffset) !== null && _3 !== void 0 ? _3 : 0, targetOffset);
7561
+ ringAnimation.arcOpen = approach((_4 = ringAnimation.arcOpen) !== null && _4 !== void 0 ? _4 : 0, targetOpen);
7295
7562
  ringAnimation.arcTime = now;
7296
7563
  if (ringAnimation.arcOffset !== targetOffset || ringAnimation.arcOpen !== targetOpen) {
7297
7564
  this.viewer.scene.requestRender();
@@ -7346,14 +7613,16 @@
7346
7613
  }
7347
7614
  /*
7348
7615
  * Builds one icon glyph from label options, with its background chip when asked for.
7349
- *
7350
7616
  * The returned fit squares the chip off once the glyph has a measurable size.
7351
7617
  */
7352
- buildIcon(options, counter) {
7618
+ buildIcon(options, counter, placePhoto) {
7353
7619
  var _a, _b, _c;
7354
7620
  let fit;
7355
7621
  const icon = document.createElement("span");
7356
7622
  icon.className = "entity-label-icon";
7623
+ if (options.iconPhoto && options.iconUrl) {
7624
+ return this.buildPhoto(icon, options, counter, placePhoto);
7625
+ }
7357
7626
  icon.style.cssText = `font-size:${(options.iconSize || 24) * ((_a = options.iconScale) !== null && _a !== void 0 ? _a : 1)}px;line-height:1;`;
7358
7627
  icon.style.textShadow = "0 1px 2px rgba(0, 0, 0, 0.25)";
7359
7628
  const iconColor = typeof options.iconColor === "string" ? options.iconColor : undefined;
@@ -7406,6 +7675,8 @@
7406
7675
  }
7407
7676
  this.viewer.scene.requestRender();
7408
7677
  };
7678
+ // A photo is rarely square; fit it inside the icon's box rather than stretching it.
7679
+ image.style.objectFit = "contain";
7409
7680
  image.onload = resize;
7410
7681
  resize();
7411
7682
  image.src = options.iconUrl;
@@ -7461,6 +7732,101 @@
7461
7732
  }
7462
7733
  return { icon, fit };
7463
7734
  }
7735
+ /*
7736
+ * Draws an entity's own photo at the size of an icon on its chip: a circle, or a rounded rectangle when too oblong for one.
7737
+ */
7738
+ buildPhoto(icon, options, counter, placePhoto) {
7739
+ var _a;
7740
+ const size = (options.iconSize || 24) * ((_a = options.iconScale) !== null && _a !== void 0 ? _a : 1);
7741
+ // The glyph plus the chip's padding of a third either side, enlarged for a photo.
7742
+ const diameter = (size + 2 * (size / 3)) * PHOTO_SIZE_SCALE;
7743
+ // Enough pixels to stay sharp at the hover size.
7744
+ const pixels = Math.ceil(diameter * PHOTO_HOVER_SCALE * (window.devicePixelRatio || 1));
7745
+ const canvas = document.createElement("canvas");
7746
+ canvas.className = "entity-label-photo";
7747
+ canvas.style.cssText = "display:block;box-sizing:border-box;border:2px solid rgba(255,255,255,0.9);box-shadow:0 1px 3px rgba(0,0,0,0.35);";
7748
+ canvas.style.transition = ["width", "height", "opacity"].map(property => `${property} ${PHOTO_TRANSITION}ms ease-out`).join(",");
7749
+ icon.classList.add("entity-label-icon-photo");
7750
+ icon.style.display = options.iconScale === 0 ? "none" : "inline-flex";
7751
+ icon.appendChild(canvas);
7752
+ // Resting size, set once the photo's shape is known.
7753
+ let width = 0;
7754
+ let height = 0;
7755
+ let grown = false;
7756
+ const grow = (on) => {
7757
+ if (on === grown || !width) {
7758
+ return;
7759
+ }
7760
+ grown = on;
7761
+ const scale = on ? PHOTO_HOVER_SCALE : 1;
7762
+ canvas.style.width = `${width * scale}px`;
7763
+ canvas.style.height = `${height * scale}px`;
7764
+ this.renderFor(PHOTO_TRANSITION);
7765
+ };
7766
+ // Draws the photo in, or drops the icon when it would not load so the label stays as its text.
7767
+ const show = (photo) => {
7768
+ var _a;
7769
+ if (!photo.source) {
7770
+ icon.dataset.failed = "true";
7771
+ icon.style.display = "none";
7772
+ return false;
7773
+ }
7774
+ canvas.width = photo.source.width;
7775
+ canvas.height = photo.source.height;
7776
+ (_a = canvas.getContext("2d")) === null || _a === void 0 ? void 0 : _a.drawImage(photo.source, 0, 0);
7777
+ const longest = Math.max(photo.width, photo.height);
7778
+ const circle = PhotoShape(photo.width, photo.height) === "circle";
7779
+ width = circle ? diameter : diameter * photo.width / longest;
7780
+ height = circle ? diameter : diameter * photo.height / longest;
7781
+ canvas.style.width = `${width}px`;
7782
+ canvas.style.height = `${height}px`;
7783
+ canvas.style.borderRadius = circle ? "50%" : `${diameter * PHOTO_CORNER}px`;
7784
+ canvas.style.opacity = "1";
7785
+ placePhoto === null || placePhoto === void 0 ? void 0 : placePhoto(circle ? "circle" : "rect", grow);
7786
+ return true;
7787
+ };
7788
+ const photos = Photos.Get(this.viewer);
7789
+ const ready = photos.Peek(options.iconUrl, pixels);
7790
+ if (ready) {
7791
+ show(ready);
7792
+ return { icon };
7793
+ }
7794
+ canvas.style.width = "0px";
7795
+ canvas.style.height = "0px";
7796
+ canvas.style.opacity = "0";
7797
+ let arrived;
7798
+ const arriving = new Promise(resolve => arrived = resolve);
7799
+ let started = false;
7800
+ const start = () => {
7801
+ if (started) {
7802
+ return;
7803
+ }
7804
+ started = true;
7805
+ photos.Load(options.iconUrl, pixels).then(photo => {
7806
+ if (!this.disposed && this.counter === counter && show(photo)) {
7807
+ this.renderFor(PHOTO_TRANSITION);
7808
+ }
7809
+ arrived();
7810
+ });
7811
+ };
7812
+ return { icon, arriving, start };
7813
+ }
7814
+ /*
7815
+ * Keeps rendering while a CSS transition changes a label's size, since placement measures it every frame.
7816
+ */
7817
+ renderFor(duration) {
7818
+ const end = Date.now() + duration;
7819
+ const step = () => {
7820
+ if (this.disposed || !this.viewer || this.viewer.isDestroyed()) {
7821
+ return;
7822
+ }
7823
+ this.viewer.scene.requestRender();
7824
+ if (Date.now() < end) {
7825
+ requestAnimationFrame(step);
7826
+ }
7827
+ };
7828
+ step();
7829
+ }
7464
7830
  /**
7465
7831
  * Removes label from the viewer and unsubscribes from events.
7466
7832
  */
@@ -7618,34 +7984,40 @@
7618
7984
  ].some(Array.isArray);
7619
7985
  }
7620
7986
  EntityLabelGroup.NeedsEntityData = NeedsEntityData;
7621
- // How many entities to read at once, so a long hand-picked list does not arrive as a burst.
7622
- const HYDRATE_AT_ONCE = 8;
7987
+ // How many entities one read asks for, so a long hand-picked list does not arrive as one request.
7988
+ const HYDRATE_AT_ONCE = 100;
7623
7989
  /*
7624
- * Reads the entities behind items that name one without carrying it.
7990
+ * Reads the entities behind items that name one without carrying it, attachments included,
7991
+ * so a label's default image needs no second request.
7625
7992
  */
7626
7993
  async function Hydrate(items, api, signal) {
7994
+ var _a, _b;
7627
7995
  if (!api) {
7628
7996
  return items;
7629
7997
  }
7630
7998
  const hydrated = items.slice();
7631
- for (let start = 0; start < hydrated.length; start += HYDRATE_AT_ONCE) {
7999
+ const missing = hydrated.map((item, at) => (item === null || item === void 0 ? void 0 : item.entityId) && !item.entity ? at : -1).filter(at => at >= 0);
8000
+ for (let start = 0; start < missing.length; start += HYDRATE_AT_ONCE) {
7632
8001
  if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
7633
8002
  return hydrated;
7634
8003
  }
7635
- await Promise.all(hydrated.slice(start, start + HYDRATE_AT_ONCE).map(async (item, at) => {
7636
- if (!(item === null || item === void 0 ? void 0 : item.entityId) || item.entity) {
7637
- return;
7638
- }
7639
- try {
7640
- const { entity } = await BModels.Entity.Get({ api, entityId: item.entityId, migrated: true });
8004
+ const chunk = missing.slice(start, start + HYDRATE_AT_ONCE);
8005
+ try {
8006
+ const { entities } = await BModels.Entity.GetListByIds({
8007
+ api, entityIds: chunk.map(at => hydrated[at].entityId), expandAttachments: true, migrated: true
8008
+ });
8009
+ 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]; }));
8010
+ for (const at of chunk) {
8011
+ const entity = found.get(hydrated[at].entityId);
7641
8012
  if (entity) {
7642
- hydrated[start + at] = { ...item, entity };
8013
+ // The API leaves Attachment out when there are none; an empty list records that they were read.
8014
+ 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 : [] } } };
7643
8015
  }
7644
8016
  }
7645
- catch {
7646
- // Rip.
7647
- }
7648
- }));
8017
+ }
8018
+ catch {
8019
+ // Entities that cannot be read keep their labels, just without attribute data.
8020
+ }
7649
8021
  }
7650
8022
  return hydrated;
7651
8023
  }
@@ -7732,6 +8104,10 @@
7732
8104
  return null;
7733
8105
  }
7734
8106
  }
8107
+ // How many entities one attachment read asks for.
8108
+ const IMAGES_AT_ONCE = 100;
8109
+ // How long on-screen requests gather before they are read together.
8110
+ const IMAGES_GATHER = 60;
7735
8111
  /*
7736
8112
  * Owns membership independently of manual labelled states, with atomic replacement and stale-load cancellation.
7737
8113
  */
@@ -7747,6 +8123,12 @@
7747
8123
  this.definitions = new Map();
7748
8124
  this.visibility = new Map();
7749
8125
  this.errors = new Map();
8126
+ // Default image per entity, null once read and found to have none. Shared by every group.
8127
+ this.images = new Map();
8128
+ // Members whose labels came on screen and are waiting on, or being read for, their default image.
8129
+ this.imagesQueued = new Set();
8130
+ this.imagesAsking = new Set();
8131
+ this.imagesTimer = null;
7750
8132
  }
7751
8133
  notify() {
7752
8134
  try {
@@ -7892,7 +8274,7 @@
7892
8274
  return false;
7893
8275
  }
7894
8276
  Resolve(entityId, menuItemId) {
7895
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
8277
+ var _a, _b, _c, _d, _e;
7896
8278
  const ids = Array.from(this.members.get(entityId) || []).reverse();
7897
8279
  for (const id of ids) {
7898
8280
  const group = this.active.get(id);
@@ -7902,61 +8284,145 @@
7902
8284
  if (group.definition.menuItemId && group.definition.menuItemId !== menuItemId) {
7903
8285
  continue;
7904
8286
  }
8287
+ const options = this.compose(group, entityId, menuItemId);
7905
8288
  const item = group.items.get(entityId);
7906
- const entity = (_a = item.entity) !== null && _a !== void 0 ? _a : { Bruce: { ID: entityId, Name: item.text } };
7907
- let iconPriority = (_b = group.definition.options) === null || _b === void 0 ? void 0 : _b.iconPriority;
7908
- if (Array.isArray(iconPriority)) {
7909
- try {
7910
- iconPriority = BModels.Calculator.GetNumber(iconPriority, entity);
8289
+ const image = (_a = this.images.get(entityId)) !== null && _a !== void 0 ? _a : (Array.isArray((_c = (_b = item.entity) === null || _b === void 0 ? void 0 : _b.Bruce) === null || _c === void 0 ? void 0 : _c.Attachment) ? exports.EntityLabel.GetDefaultImageUrl(item.entity, (_d = this.register.ApiGetters) === null || _d === void 0 ? void 0 : _d.GetBruceApi()) : null);
8290
+ if (image && !exports.EntityLabel.HasIcon(options)) {
8291
+ (_e = options.photo) !== null && _e !== void 0 ? _e : (options.photo = image);
8292
+ }
8293
+ return { groupId: id, options, item };
8294
+ }
8295
+ return null;
8296
+ }
8297
+ /*
8298
+ * One member's label settings, with any image its own text links to but before its default image.
8299
+ */
8300
+ compose(group, entityId, menuItemId) {
8301
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
8302
+ const item = group.items.get(entityId);
8303
+ const entity = (_a = item.entity) !== null && _a !== void 0 ? _a : { Bruce: { ID: entityId, Name: item.text } };
8304
+ let iconPriority = (_b = group.definition.options) === null || _b === void 0 ? void 0 : _b.iconPriority;
8305
+ if (Array.isArray(iconPriority)) {
8306
+ try {
8307
+ iconPriority = BModels.Calculator.GetNumber(iconPriority, entity);
8308
+ }
8309
+ catch {
8310
+ iconPriority = null;
8311
+ }
8312
+ }
8313
+ let iconScale = (_c = group.definition.options) === null || _c === void 0 ? void 0 : _c.iconScale;
8314
+ if (Array.isArray(iconScale)) {
8315
+ try {
8316
+ iconScale = BModels.Calculator.GetNumber(iconScale, entity);
8317
+ }
8318
+ catch {
8319
+ iconScale = null;
8320
+ }
8321
+ if (!Number.isFinite(iconScale)) {
8322
+ iconScale = 1;
8323
+ }
8324
+ }
8325
+ if (iconScale != null) {
8326
+ iconScale = Math.max(0, iconScale);
8327
+ }
8328
+ const textSetting = (_d = group.definition.options) === null || _d === void 0 ? void 0 : _d.text;
8329
+ let resolvedText = resolveIcon(textSetting, entity);
8330
+ const expression = Array.isArray(textSetting);
8331
+ if (expression && !(resolvedText === null || resolvedText === void 0 ? void 0 : resolvedText.trim())) {
8332
+ resolvedText = undefined;
8333
+ }
8334
+ const fallbackText = [item.text, (_e = entity.Bruce) === null || _e === void 0 ? void 0 : _e.Name, (_f = this.register.GetRego({ entityId, menuItemId })) === null || _f === void 0 ? void 0 : _f.name, entityId]
8335
+ .find(text => typeof text === "string" && text.trim().length > 0);
8336
+ const options = {
8337
+ ...group.definition.options,
8338
+ text: resolvedText !== null && resolvedText !== void 0 ? resolvedText : fallbackText,
8339
+ iconScale: iconScale,
8340
+ icon: resolveIcon((_g = group.definition.options) === null || _g === void 0 ? void 0 : _g.icon, entity),
8341
+ iconUrl: resolveIcon((_h = group.definition.options) === null || _h === void 0 ? void 0 : _h.iconUrl, entity),
8342
+ iconClass: resolveIcon((_j = group.definition.options) === null || _j === void 0 ? void 0 : _j.iconClass, entity),
8343
+ iconColor: resolveIcon((_k = group.definition.options) === null || _k === void 0 ? void 0 : _k.iconColor, entity),
8344
+ iconBackground: (_l = group.definition.options) === null || _l === void 0 ? void 0 : _l.iconBackground,
8345
+ iconPriority: Number.isFinite(iconPriority) ? iconPriority : item.iconPriority,
8346
+ iconStatus: (_o = resolveIcon((_m = group.definition.options) === null || _m === void 0 ? void 0 : _m.iconStatus, entity)) !== null && _o !== void 0 ? _o : item.iconStatus
8347
+ };
8348
+ if (options.attributes && item.entity && !(expression && resolvedText != null)) {
8349
+ options.text = [
8350
+ (_r = (_p = options.text) !== null && _p !== void 0 ? _p : (_q = item.entity.Bruce) === null || _q === void 0 ? void 0 : _q.Name) !== null && _r !== void 0 ? _r : entityId,
8351
+ ...options.attributes.map(path => BModels.Entity.GetValue({ entity: item.entity, path })).filter(value => value != null).map(String)
8352
+ ].join("\n");
8353
+ }
8354
+ if (!exports.EntityLabel.HasIcon(options) && typeof options.text === "string") {
8355
+ const lines = options.text.split("\n");
8356
+ const at = lines.findIndex(line => exports.EntityLabel.IsImageUrl(line));
8357
+ if (at >= 0) {
8358
+ const [url] = lines.splice(at, 1);
8359
+ options.text = lines.join("\n");
8360
+ options.photo = url.trim();
8361
+ }
8362
+ }
8363
+ return options;
8364
+ }
8365
+ /*
8366
+ * Queues a member's default image once its label is on screen, so members out of view never cost a read.
8367
+ * Requests arriving close together are read as one.
8368
+ */
8369
+ RequestImage(entityId) {
8370
+ var _a, _b, _c, _d, _e, _f;
8371
+ const api = (_a = this.register.ApiGetters) === null || _a === void 0 ? void 0 : _a.GetBruceApi();
8372
+ if (!api || !entityId || this.images.has(entityId) || this.imagesAsking.has(entityId)) {
8373
+ return;
8374
+ }
8375
+ // Members read with their attachments already know their image, or that they have none.
8376
+ for (const id of this.members.get(entityId) || []) {
8377
+ if (Array.isArray((_e = (_d = (_c = (_b = this.active.get(id)) === null || _b === void 0 ? void 0 : _b.items.get(entityId)) === null || _c === void 0 ? void 0 : _c.entity) === null || _d === void 0 ? void 0 : _d.Bruce) === null || _e === void 0 ? void 0 : _e.Attachment)) {
8378
+ return;
8379
+ }
8380
+ }
8381
+ if (!this.members.has(entityId)) {
8382
+ return;
8383
+ }
8384
+ this.imagesAsking.add(entityId);
8385
+ this.imagesQueued.add(entityId);
8386
+ (_f = this.imagesTimer) !== null && _f !== void 0 ? _f : (this.imagesTimer = setTimeout(() => {
8387
+ this.imagesTimer = null;
8388
+ this.readImages(api).catch(error => console.error("Label images could not be loaded", error));
8389
+ }, IMAGES_GATHER));
8390
+ }
8391
+ async readImages(api) {
8392
+ var _a, _b;
8393
+ const ids = Array.from(this.imagesQueued);
8394
+ this.imagesQueued.clear();
8395
+ for (let start = 0; start < ids.length; start += IMAGES_AT_ONCE) {
8396
+ const chunk = ids.slice(start, start + IMAGES_AT_ONCE);
8397
+ let entities;
8398
+ try {
8399
+ entities = (_a = (await BModels.Entity.GetListByIds({ api, entityIds: chunk, expandAttachments: true, migrated: true })).entities) !== null && _a !== void 0 ? _a : [];
8400
+ }
8401
+ catch {
8402
+ // Left unread, so the next label to come on screen for these members asks again.
8403
+ entities = null;
8404
+ }
8405
+ if (this.register.Disposed) {
8406
+ return;
8407
+ }
8408
+ 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, exports.EntityLabel.GetDefaultImageUrl(entity, api)]; }));
8409
+ const gained = [];
8410
+ for (const id of chunk) {
8411
+ if (!this.imagesAsking.delete(id) || !entities) {
8412
+ continue;
7911
8413
  }
7912
- catch {
7913
- iconPriority = null;
8414
+ const url = (_b = found.get(id)) !== null && _b !== void 0 ? _b : null;
8415
+ this.images.set(id, url);
8416
+ if (url) {
8417
+ gained.push(id);
7914
8418
  }
7915
8419
  }
7916
- let iconScale = (_c = group.definition.options) === null || _c === void 0 ? void 0 : _c.iconScale;
7917
- if (Array.isArray(iconScale)) {
7918
- try {
7919
- iconScale = BModels.Calculator.GetNumber(iconScale, entity);
7920
- }
7921
- catch {
7922
- iconScale = null;
7923
- }
7924
- if (!Number.isFinite(iconScale)) {
7925
- iconScale = 1;
7926
- }
7927
- }
7928
- if (iconScale != null) {
7929
- iconScale = Math.max(0, iconScale);
7930
- }
7931
- const textSetting = (_d = group.definition.options) === null || _d === void 0 ? void 0 : _d.text;
7932
- let resolvedText = resolveIcon(textSetting, entity);
7933
- const expression = Array.isArray(textSetting);
7934
- if (expression && !(resolvedText === null || resolvedText === void 0 ? void 0 : resolvedText.trim())) {
7935
- resolvedText = undefined;
7936
- }
7937
- const fallbackText = [item.text, (_e = entity.Bruce) === null || _e === void 0 ? void 0 : _e.Name, (_f = this.register.GetRego({ entityId, menuItemId })) === null || _f === void 0 ? void 0 : _f.name, entityId]
7938
- .find(text => typeof text === "string" && text.trim().length > 0);
7939
- const options = {
7940
- ...group.definition.options,
7941
- text: resolvedText !== null && resolvedText !== void 0 ? resolvedText : fallbackText,
7942
- iconScale: iconScale,
7943
- icon: resolveIcon((_g = group.definition.options) === null || _g === void 0 ? void 0 : _g.icon, entity),
7944
- iconUrl: resolveIcon((_h = group.definition.options) === null || _h === void 0 ? void 0 : _h.iconUrl, entity),
7945
- iconClass: resolveIcon((_j = group.definition.options) === null || _j === void 0 ? void 0 : _j.iconClass, entity),
7946
- iconColor: resolveIcon((_k = group.definition.options) === null || _k === void 0 ? void 0 : _k.iconColor, entity),
7947
- iconBackground: (_l = group.definition.options) === null || _l === void 0 ? void 0 : _l.iconBackground,
7948
- iconPriority: Number.isFinite(iconPriority) ? iconPriority : item.iconPriority,
7949
- iconStatus: (_o = resolveIcon((_m = group.definition.options) === null || _m === void 0 ? void 0 : _m.iconStatus, entity)) !== null && _o !== void 0 ? _o : item.iconStatus
7950
- };
7951
- if (options.attributes && item.entity && !(expression && resolvedText != null)) {
7952
- options.text = [
7953
- (_r = (_p = options.text) !== null && _p !== void 0 ? _p : (_q = item.entity.Bruce) === null || _q === void 0 ? void 0 : _q.Name) !== null && _r !== void 0 ? _r : entityId,
7954
- ...options.attributes.map(path => BModels.Entity.GetValue({ entity: item.entity, path })).filter(value => value != null).map(String)
7955
- ].join("\n");
8420
+ // Reconfigured in place rather than through refresh, which would drop a standalone label and redraw it.
8421
+ if (gained.length) {
8422
+ this.register.ForceUpdate({ entityIds: gained });
8423
+ this.register.Viewer.scene.requestRender();
7956
8424
  }
7957
- return { groupId: id, options, item };
7958
8425
  }
7959
- return null;
7960
8426
  }
7961
8427
  RefreshEntity(entityId) {
7962
8428
  var _a, _b, _c, _d, _e, _f, _g;
@@ -8014,6 +8480,11 @@
8014
8480
  }
8015
8481
  this.unindex(id);
8016
8482
  this.active.delete(id);
8483
+ for (const entityId of group.items.keys()) {
8484
+ if (!this.members.has(entityId)) {
8485
+ this.images.delete(entityId);
8486
+ }
8487
+ }
8017
8488
  this.refresh(new Set(group.items.keys()));
8018
8489
  this.notify();
8019
8490
  }
@@ -8022,6 +8493,7 @@
8022
8493
  controller.abort();
8023
8494
  }
8024
8495
  this.pending.clear();
8496
+ this.clearImages();
8025
8497
  const affected = new Set(this.members.keys());
8026
8498
  this.active.clear();
8027
8499
  this.members.clear();
@@ -8038,6 +8510,7 @@
8038
8510
  for (const label of this.standalone.values()) {
8039
8511
  label.Dispose();
8040
8512
  }
8513
+ this.clearImages();
8041
8514
  this.pending.clear();
8042
8515
  this.standalone.clear();
8043
8516
  this.standaloneItems.clear();
@@ -8047,6 +8520,13 @@
8047
8520
  this.visibility.clear();
8048
8521
  this.errors.clear();
8049
8522
  }
8523
+ clearImages() {
8524
+ clearTimeout(this.imagesTimer);
8525
+ this.imagesTimer = null;
8526
+ this.imagesQueued.clear();
8527
+ this.imagesAsking.clear();
8528
+ this.images.clear();
8529
+ }
8050
8530
  unindex(groupId) {
8051
8531
  var _a;
8052
8532
  for (const id of ((_a = this.active.get(groupId)) === null || _a === void 0 ? void 0 : _a.items.keys()) || []) {
@@ -12395,8 +12875,15 @@
12395
12875
  createIfMissing: params.createIfMissing !== false
12396
12876
  };
12397
12877
  const doc = this.viewer.container.ownerDocument;
12398
- doc.addEventListener("pointermove", this.CheckLabelHoverBounds, true);
12399
- this.viewer.container.addEventListener("pointerleave", this.CheckLabelHoverBounds);
12878
+ // Only a label following the cursor should go when the cursor leaves the canvas.
12879
+ if (params.position) {
12880
+ doc.addEventListener("pointermove", this.CheckLabelHoverBounds, true);
12881
+ this.viewer.container.addEventListener("pointerleave", this.CheckLabelHoverBounds);
12882
+ }
12883
+ else {
12884
+ doc.removeEventListener("pointermove", this.CheckLabelHoverBounds, true);
12885
+ this.viewer.container.removeEventListener("pointerleave", this.CheckLabelHoverBounds);
12886
+ }
12400
12887
  doc.defaultView.addEventListener("blur", this.ClearLabelHover);
12401
12888
  this.SyncLabelHover();
12402
12889
  this.viewer.scene.requestRender();
@@ -12532,6 +13019,13 @@
12532
13019
  var _a;
12533
13020
  return (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.Resolve(entityId, menuItemId);
12534
13021
  }
13022
+ /*
13023
+ * Called by a group label once it is on screen, so its entity's default image is read only when it can be seen.
13024
+ */
13025
+ RequestLabelImage(entityId) {
13026
+ var _a;
13027
+ (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.RequestImage(entityId);
13028
+ }
12535
13029
  RefreshLabelGroupEntity(entityId) {
12536
13030
  var _a, _b;
12537
13031
  (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.RefreshEntity(entityId);
@@ -27252,6 +27746,159 @@
27252
27746
  TilesetI3sRenderManager.Manager = Manager;
27253
27747
  })(TilesetI3sRenderManager || (TilesetI3sRenderManager = {}));
27254
27748
 
27749
+ /**
27750
+ * Remaps a (assumed grayscale, server-rendered black-to-white) image's pixels into a gradient between minColor/maxColor,
27751
+ * preserving source alpha blended with the mask's own alpha.
27752
+ * @param imageData
27753
+ * @param minColor
27754
+ * @param maxColor
27755
+ */
27756
+ function ApplyGrayscaleColorMask(imageData, minColor, maxColor) {
27757
+ const data = imageData.data;
27758
+ for (let i = 0; i < data.length; i += 4) {
27759
+ // The texture is rendered black-to-white server-side, so any channel carries the value.
27760
+ const t = data[i] / 255;
27761
+ data[i] = Math.round(minColor.red + (maxColor.red - minColor.red) * t);
27762
+ data[i + 1] = Math.round(minColor.green + (maxColor.green - minColor.green) * t);
27763
+ data[i + 2] = Math.round(minColor.blue + (maxColor.blue - minColor.blue) * t);
27764
+ // Preserve source transparency (eg unrasterized pixels) blended with the mask's own alpha.
27765
+ const srcAlpha = data[i + 3] / 255;
27766
+ const maskAlpha = minColor.alpha + (maxColor.alpha - minColor.alpha) * t;
27767
+ data[i + 3] = Math.round(srcAlpha * maskAlpha * 255);
27768
+ }
27769
+ }
27770
+ /**
27771
+ * The colour a ramp gives a normalised value.
27772
+ * @param sorted stops in ascending position, normalised into the texture's value range
27773
+ */
27774
+ function SampleRamp(sorted, t) {
27775
+ let upper = 0;
27776
+ while (upper < sorted.length && sorted[upper].position < t) {
27777
+ upper++;
27778
+ }
27779
+ if (upper === 0) {
27780
+ return sorted[0].color;
27781
+ }
27782
+ if (upper >= sorted.length) {
27783
+ return sorted[sorted.length - 1].color;
27784
+ }
27785
+ const a = sorted[upper - 1];
27786
+ const b = sorted[upper];
27787
+ const span = b.position - a.position;
27788
+ // Equal positions are a hard step rather than a division by zero, which is how a band
27789
+ // gets a crisp edge instead of a gradient into its neighbour.
27790
+ const f = span > 0 ? (t - a.position) / span : 1;
27791
+ return {
27792
+ red: a.color.red + (b.color.red - a.color.red) * f,
27793
+ green: a.color.green + (b.color.green - a.color.green) * f,
27794
+ blue: a.color.blue + (b.color.blue - a.color.blue) * f,
27795
+ alpha: a.color.alpha + (b.color.alpha - a.color.alpha) * f
27796
+ };
27797
+ }
27798
+ /**
27799
+ * A ramp baked into RGBA bytes, for a shader that cannot walk stops per fragment.
27800
+ * @param steps entries in the lookup, each covering an equal slice of the value range
27801
+ */
27802
+ function RampLookup(stops, low, high, steps = 256) {
27803
+ const sorted = stops && stops.length > 0
27804
+ ? stops.slice().sort((a, b) => a.position - b.position)
27805
+ : null;
27806
+ const out = new Uint8Array(steps * 4);
27807
+ for (let i = 0; i < steps; i++) {
27808
+ const t = steps > 1 ? i / (steps - 1) : 0;
27809
+ const color = sorted
27810
+ ? SampleRamp(sorted, t)
27811
+ : {
27812
+ red: low.red + (high.red - low.red) * t,
27813
+ green: low.green + (high.green - low.green) * t,
27814
+ blue: low.blue + (high.blue - low.blue) * t,
27815
+ alpha: low.alpha + (high.alpha - low.alpha) * t
27816
+ };
27817
+ out[i * 4] = Math.round(color.red);
27818
+ out[i * 4 + 1] = Math.round(color.green);
27819
+ out[i * 4 + 2] = Math.round(color.blue);
27820
+ out[i * 4 + 3] = Math.round(Math.min(1, Math.max(0, color.alpha)) * 255);
27821
+ }
27822
+ return out;
27823
+ }
27824
+ function ApplyGradientStops(imageData, stops) {
27825
+ if (!stops || stops.length === 0) {
27826
+ return;
27827
+ }
27828
+ const sorted = stops.slice().sort((a, b) => a.position - b.position);
27829
+ const data = imageData.data;
27830
+ for (let i = 0; i < data.length; i += 4) {
27831
+ const t = data[i] / 255;
27832
+ const color = SampleRamp(sorted, t);
27833
+ data[i] = Math.round(color.red);
27834
+ data[i + 1] = Math.round(color.green);
27835
+ data[i + 2] = Math.round(color.blue);
27836
+ const srcAlpha = data[i + 3] / 255;
27837
+ data[i + 3] = Math.round(srcAlpha * color.alpha * 255);
27838
+ }
27839
+ }
27840
+ /**
27841
+ * Draws the texture's own cell grid into the tinted pixels.
27842
+ *
27843
+ * Drawn in texture space rather than screen space so it stays with the cells it describes and needs
27844
+ * no per-frame work as the camera moves.
27845
+ */
27846
+ function ApplyCellBorders(imageData, cellTexels, widthTexels, color) {
27847
+ if (!(cellTexels > 0) || !(widthTexels > 0) || !color || color.alpha <= 0) {
27848
+ return;
27849
+ }
27850
+ const { width, height, data } = imageData;
27851
+ const edge = Math.max(1, Math.round(widthTexels));
27852
+ for (let y = 0; y < height; y++) {
27853
+ const onRow = (y % cellTexels) < edge;
27854
+ for (let x = 0; x < width; x++) {
27855
+ if (!onRow && (x % cellTexels) >= edge) {
27856
+ continue;
27857
+ }
27858
+ const i = (y * width + x) * 4;
27859
+ // Only where something is already drawn, so a border cannot invent coverage the data
27860
+ // does not have.
27861
+ if (data[i + 3] === 0) {
27862
+ continue;
27863
+ }
27864
+ const a = color.alpha;
27865
+ data[i] = Math.round(data[i] * (1 - a) + color.red * a);
27866
+ data[i + 1] = Math.round(data[i + 1] * (1 - a) + color.green * a);
27867
+ data[i + 2] = Math.round(data[i + 2] * (1 - a) + color.blue * a);
27868
+ }
27869
+ }
27870
+ }
27871
+ function loadImage(src) {
27872
+ return new Promise((res, rej) => {
27873
+ const image = new Image();
27874
+ image.onload = () => res(image);
27875
+ image.onerror = (e) => rej(e);
27876
+ image.src = src;
27877
+ });
27878
+ }
27879
+ /*
27880
+ * Loads an image whose pixels will be read back or uploaded to WebGL, which needs a CORS-clean copy.
27881
+ */
27882
+ async function loadImageForCanvas(url) {
27883
+ let response;
27884
+ try {
27885
+ response = await fetch(url);
27886
+ }
27887
+ catch {
27888
+ response = await fetch(url, { cache: "reload" });
27889
+ }
27890
+ if (!response.ok) {
27891
+ throw new Error(`Image request failed with ${response.status}: ${url}`);
27892
+ }
27893
+ const objectUrl = URL.createObjectURL(await response.blob());
27894
+ try {
27895
+ return await loadImage(objectUrl);
27896
+ }
27897
+ finally {
27898
+ URL.revokeObjectURL(objectUrl);
27899
+ }
27900
+ }
27901
+
27255
27902
  (function (GoogleSearchRenderManager) {
27256
27903
  class Manager {
27257
27904
  get Disposed() {
@@ -27557,45 +28204,39 @@
27557
28204
  return cacheData;
27558
28205
  }
27559
28206
  const IMAGE_PADDING = 8;
27560
- const prom = new Promise(async (res, rej) => {
27561
- try {
27562
- const response = await fetch(url);
27563
- const blob = await response.blob();
27564
- const image = new Image();
27565
- image.onload = () => {
27566
- const canvas = document.createElement("canvas");
27567
- canvas.width = size;
27568
- canvas.height = size;
27569
- const context = canvas.getContext("2d");
27570
- // Calculate padding and available space.
27571
- const availableSize = size - 2 * IMAGE_PADDING;
27572
- // Calculate resized dimensions preserving aspect ratio.
27573
- const { newWidth, newHeight } = calculateBillboardSpace(image.width, image.height, availableSize, availableSize);
27574
- // Draw the circular background.
27575
- context.beginPath();
27576
- context.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI, false);
27577
- context.fillStyle = bgColor;
27578
- context.fill();
27579
- // Draw the image centered within the circle.
27580
- const offsetX = (size - newWidth) / 2;
27581
- const offsetY = (size - newHeight) / 2;
27582
- context.drawImage(image, offsetX, offsetY, newWidth, newHeight);
27583
- // Draw a 2px darkened border around the circle.
27584
- context.beginPath();
27585
- context.arc(size / 2, size / 2, size / 2 - 1, 0, 2 * Math.PI, false);
27586
- context.lineWidth = 1;
27587
- context.strokeStyle = "rgba(0, 0, 0, 0.3)";
27588
- context.stroke();
27589
- res(canvas);
27590
- };
27591
- image.onerror = (e) => rej(e);
27592
- image.src = URL.createObjectURL(blob);
27593
- }
27594
- catch (e) {
27595
- rej(e);
27596
- }
28207
+ const prom = loadImageForCanvas(url).then(image => {
28208
+ const canvas = document.createElement("canvas");
28209
+ canvas.width = size;
28210
+ canvas.height = size;
28211
+ const context = canvas.getContext("2d");
28212
+ // Calculate padding and available space.
28213
+ const availableSize = size - 2 * IMAGE_PADDING;
28214
+ // Calculate resized dimensions preserving aspect ratio.
28215
+ const { newWidth, newHeight } = calculateBillboardSpace(image.width, image.height, availableSize, availableSize);
28216
+ // Draw the circular background.
28217
+ context.beginPath();
28218
+ context.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI, false);
28219
+ context.fillStyle = bgColor;
28220
+ context.fill();
28221
+ // Draw the image centered within the circle.
28222
+ const offsetX = (size - newWidth) / 2;
28223
+ const offsetY = (size - newHeight) / 2;
28224
+ context.drawImage(image, offsetX, offsetY, newWidth, newHeight);
28225
+ // Draw a 2px darkened border around the circle.
28226
+ context.beginPath();
28227
+ context.arc(size / 2, size / 2, size / 2 - 1, 0, 2 * Math.PI, false);
28228
+ context.lineWidth = 1;
28229
+ context.strokeStyle = "rgba(0, 0, 0, 0.3)";
28230
+ context.stroke();
28231
+ return canvas;
27597
28232
  });
27598
28233
  _billboardCache.Set(key, prom);
28234
+ // A failure is not kept, so the icon is tried again rather than staying missing for the session.
28235
+ prom.catch(() => {
28236
+ if (_billboardCache.Get(key) === prom) {
28237
+ _billboardCache.Delete(key);
28238
+ }
28239
+ });
27599
28240
  return prom;
27600
28241
  }
27601
28242
 
@@ -43434,33 +44075,23 @@
43434
44075
  if (cacheData) {
43435
44076
  return cacheData;
43436
44077
  }
43437
- const prom = new Promise(async (res, rej) => {
43438
- try {
43439
- const response = await fetch(url);
43440
- const blob = await response.blob();
43441
- const canvas = document.createElement("canvas");
43442
- const image = new Image();
43443
- image.onload = () => {
43444
- canvas.width = image.width;
43445
- canvas.height = image.height;
43446
- const context = canvas.getContext("2d");
43447
- context.drawImage(image, 0, 0);
43448
- const data = {
43449
- canvasDataUri: canvas.toDataURL("image/png"),
43450
- height: image.height
43451
- };
43452
- res(data);
43453
- };
43454
- image.onerror = (e) => {
43455
- rej(e);
43456
- };
43457
- image.src = URL.createObjectURL(blob);
43458
- }
43459
- catch (e) {
43460
- rej(e);
43461
- }
44078
+ const prom = loadImageForCanvas(url).then((image) => {
44079
+ const canvas = document.createElement("canvas");
44080
+ canvas.width = image.width;
44081
+ canvas.height = image.height;
44082
+ canvas.getContext("2d").drawImage(image, 0, 0);
44083
+ return {
44084
+ canvasDataUri: canvas.toDataURL("image/png"),
44085
+ height: image.height
44086
+ };
43462
44087
  });
43463
44088
  _billboardCache$1.Set(cacheKey, prom);
44089
+ // A failure is not kept, so the icon is tried again rather than staying missing for the session.
44090
+ prom.catch(() => {
44091
+ if (_billboardCache$1.Get(cacheKey) === prom) {
44092
+ _billboardCache$1.Delete(cacheKey);
44093
+ }
44094
+ });
43464
44095
  return prom;
43465
44096
  };
43466
44097
  /**
@@ -44912,137 +45543,6 @@
44912
45543
  return EnsureNumber(zIndex);
44913
45544
  }
44914
45545
 
44915
- /**
44916
- * Remaps a (assumed grayscale, server-rendered black-to-white) image's pixels into a gradient between minColor/maxColor,
44917
- * preserving source alpha blended with the mask's own alpha.
44918
- * @param imageData
44919
- * @param minColor
44920
- * @param maxColor
44921
- */
44922
- function ApplyGrayscaleColorMask(imageData, minColor, maxColor) {
44923
- const data = imageData.data;
44924
- for (let i = 0; i < data.length; i += 4) {
44925
- // The texture is rendered black-to-white server-side, so any channel carries the value.
44926
- const t = data[i] / 255;
44927
- data[i] = Math.round(minColor.red + (maxColor.red - minColor.red) * t);
44928
- data[i + 1] = Math.round(minColor.green + (maxColor.green - minColor.green) * t);
44929
- data[i + 2] = Math.round(minColor.blue + (maxColor.blue - minColor.blue) * t);
44930
- // Preserve source transparency (eg unrasterized pixels) blended with the mask's own alpha.
44931
- const srcAlpha = data[i + 3] / 255;
44932
- const maskAlpha = minColor.alpha + (maxColor.alpha - minColor.alpha) * t;
44933
- data[i + 3] = Math.round(srcAlpha * maskAlpha * 255);
44934
- }
44935
- }
44936
- /**
44937
- * The colour a ramp gives a normalised value.
44938
- * @param sorted stops in ascending position, normalised into the texture's value range
44939
- */
44940
- function SampleRamp(sorted, t) {
44941
- let upper = 0;
44942
- while (upper < sorted.length && sorted[upper].position < t) {
44943
- upper++;
44944
- }
44945
- if (upper === 0) {
44946
- return sorted[0].color;
44947
- }
44948
- if (upper >= sorted.length) {
44949
- return sorted[sorted.length - 1].color;
44950
- }
44951
- const a = sorted[upper - 1];
44952
- const b = sorted[upper];
44953
- const span = b.position - a.position;
44954
- // Equal positions are a hard step rather than a division by zero, which is how a band
44955
- // gets a crisp edge instead of a gradient into its neighbour.
44956
- const f = span > 0 ? (t - a.position) / span : 1;
44957
- return {
44958
- red: a.color.red + (b.color.red - a.color.red) * f,
44959
- green: a.color.green + (b.color.green - a.color.green) * f,
44960
- blue: a.color.blue + (b.color.blue - a.color.blue) * f,
44961
- alpha: a.color.alpha + (b.color.alpha - a.color.alpha) * f
44962
- };
44963
- }
44964
- /**
44965
- * A ramp baked into RGBA bytes, for a shader that cannot walk stops per fragment.
44966
- * @param steps entries in the lookup, each covering an equal slice of the value range
44967
- */
44968
- function RampLookup(stops, low, high, steps = 256) {
44969
- const sorted = stops && stops.length > 0
44970
- ? stops.slice().sort((a, b) => a.position - b.position)
44971
- : null;
44972
- const out = new Uint8Array(steps * 4);
44973
- for (let i = 0; i < steps; i++) {
44974
- const t = steps > 1 ? i / (steps - 1) : 0;
44975
- const color = sorted
44976
- ? SampleRamp(sorted, t)
44977
- : {
44978
- red: low.red + (high.red - low.red) * t,
44979
- green: low.green + (high.green - low.green) * t,
44980
- blue: low.blue + (high.blue - low.blue) * t,
44981
- alpha: low.alpha + (high.alpha - low.alpha) * t
44982
- };
44983
- out[i * 4] = Math.round(color.red);
44984
- out[i * 4 + 1] = Math.round(color.green);
44985
- out[i * 4 + 2] = Math.round(color.blue);
44986
- out[i * 4 + 3] = Math.round(Math.min(1, Math.max(0, color.alpha)) * 255);
44987
- }
44988
- return out;
44989
- }
44990
- function ApplyGradientStops(imageData, stops) {
44991
- if (!stops || stops.length === 0) {
44992
- return;
44993
- }
44994
- const sorted = stops.slice().sort((a, b) => a.position - b.position);
44995
- const data = imageData.data;
44996
- for (let i = 0; i < data.length; i += 4) {
44997
- const t = data[i] / 255;
44998
- const color = SampleRamp(sorted, t);
44999
- data[i] = Math.round(color.red);
45000
- data[i + 1] = Math.round(color.green);
45001
- data[i + 2] = Math.round(color.blue);
45002
- const srcAlpha = data[i + 3] / 255;
45003
- data[i + 3] = Math.round(srcAlpha * color.alpha * 255);
45004
- }
45005
- }
45006
- /**
45007
- * Draws the texture's own cell grid into the tinted pixels.
45008
- *
45009
- * Drawn in texture space rather than screen space so it stays with the cells it describes and needs
45010
- * no per-frame work as the camera moves.
45011
- */
45012
- function ApplyCellBorders(imageData, cellTexels, widthTexels, color) {
45013
- if (!(cellTexels > 0) || !(widthTexels > 0) || !color || color.alpha <= 0) {
45014
- return;
45015
- }
45016
- const { width, height, data } = imageData;
45017
- const edge = Math.max(1, Math.round(widthTexels));
45018
- for (let y = 0; y < height; y++) {
45019
- const onRow = (y % cellTexels) < edge;
45020
- for (let x = 0; x < width; x++) {
45021
- if (!onRow && (x % cellTexels) >= edge) {
45022
- continue;
45023
- }
45024
- const i = (y * width + x) * 4;
45025
- // Only where something is already drawn, so a border cannot invent coverage the data
45026
- // does not have.
45027
- if (data[i + 3] === 0) {
45028
- continue;
45029
- }
45030
- const a = color.alpha;
45031
- data[i] = Math.round(data[i] * (1 - a) + color.red * a);
45032
- data[i + 1] = Math.round(data[i + 1] * (1 - a) + color.green * a);
45033
- data[i + 2] = Math.round(data[i + 2] * (1 - a) + color.blue * a);
45034
- }
45035
- }
45036
- }
45037
- function loadImage(src) {
45038
- return new Promise((res, rej) => {
45039
- const image = new Image();
45040
- image.onload = () => res(image);
45041
- image.onerror = (e) => rej(e);
45042
- image.src = src;
45043
- });
45044
- }
45045
-
45046
45546
  (function (TextureFrameSeriesAnimator) {
45047
45547
  /**
45048
45548
  * Turns a published value map into the flat shape this renderer works in, or null when it cannot be drawn.
@@ -49577,8 +50077,7 @@ void main() {
49577
50077
  const cacheKey = "texture-" + url + "-" + (mask ? mask.type + "-" + mask.minColor + "-" + mask.maxColor : "none");
49578
50078
  let prom = _textureCache.Get(cacheKey);
49579
50079
  if (!prom) {
49580
- prom = buildTintedTextureDataUri(url, mask).catch(() => null);
49581
- _textureCache.Set(cacheKey, prom);
50080
+ prom = cacheTexture(cacheKey, buildTintedTextureDataUri(url, mask));
49582
50081
  }
49583
50082
  return { dataUri: await prom, effective: textureTrace.effective };
49584
50083
  }
@@ -49606,11 +50105,23 @@ void main() {
49606
50105
  const cacheKey = "texture-" + url + "-none";
49607
50106
  let prom = _textureCache.Get(cacheKey);
49608
50107
  if (!prom) {
49609
- prom = buildTintedTextureDataUri(url).catch(() => null);
49610
- _textureCache.Set(cacheKey, prom);
50108
+ prom = cacheTexture(cacheKey, buildTintedTextureDataUri(url));
49611
50109
  }
49612
50110
  return { dataUri: await prom, effective: null };
49613
50111
  }
50112
+ /*
50113
+ * Keeps a texture build for reuse, but drops a failed one so the texture is tried again next time.
50114
+ */
50115
+ function cacheTexture(cacheKey, build) {
50116
+ const prom = build.catch(() => {
50117
+ if (_textureCache.Get(cacheKey) === prom) {
50118
+ _textureCache.Delete(cacheKey);
50119
+ }
50120
+ return null;
50121
+ });
50122
+ _textureCache.Set(cacheKey, prom);
50123
+ return prom;
50124
+ }
49614
50125
  /**
49615
50126
  * Fetches an image and, if a color mask is provided, remaps its (assumed grayscale) pixel values
49616
50127
  * into a gradient between the mask's minColor/maxColor, preserving source alpha.
@@ -49618,16 +50129,7 @@ void main() {
49618
50129
  * @param mask
49619
50130
  */
49620
50131
  async function buildTintedTextureDataUri(url, mask) {
49621
- const response = await fetch(url);
49622
- const blob = await response.blob();
49623
- const objectUrl = URL.createObjectURL(blob);
49624
- let image;
49625
- try {
49626
- image = await loadImage(objectUrl);
49627
- }
49628
- finally {
49629
- URL.revokeObjectURL(objectUrl);
49630
- }
50132
+ const image = await loadImageForCanvas(url);
49631
50133
  const canvas = document.createElement("canvas");
49632
50134
  canvas.width = image.width;
49633
50135
  canvas.height = image.height;
@@ -51170,7 +51672,7 @@ void main() {
51170
51672
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
51171
51673
  })(exports.StyleUtils || (exports.StyleUtils = {}));
51172
51674
 
51173
- const VERSION = "7.5.4";
51675
+ const VERSION = "7.5.6";
51174
51676
  /**
51175
51677
  * Updates the environment instance used by bruce-cesium to one specified.
51176
51678
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.