bruce-cesium 7.5.5 → 7.5.7

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 +848 -357
  2. package/dist/bruce-cesium.es5.js.map +1 -1
  3. package/dist/bruce-cesium.umd.js +846 -354
  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-model3d.js +3 -2
  9. package/dist/lib/rendering/entity-render-engine-model3d.js.map +1 -1
  10. package/dist/lib/rendering/entity-render-engine-point.js +21 -34
  11. package/dist/lib/rendering/entity-render-engine-point.js.map +1 -1
  12. package/dist/lib/rendering/entity-render-engine-polygon.js +16 -14
  13. package/dist/lib/rendering/entity-render-engine-polygon.js.map +1 -1
  14. package/dist/lib/rendering/render-managers/common/entity-label-group.js +178 -64
  15. package/dist/lib/rendering/render-managers/common/entity-label-group.js.map +1 -1
  16. package/dist/lib/rendering/render-managers/common/entity-label.js +437 -71
  17. package/dist/lib/rendering/render-managers/common/entity-label.js.map +1 -1
  18. package/dist/lib/rendering/render-managers/other/google-search-render-manager.js +32 -37
  19. package/dist/lib/rendering/render-managers/other/google-search-render-manager.js.map +1 -1
  20. package/dist/lib/rendering/visuals-register.js +7 -0
  21. package/dist/lib/rendering/visuals-register.js.map +1 -1
  22. package/dist/types/bruce-cesium.d.ts +1 -1
  23. package/dist/types/internal/image-utils.d.ts +1 -0
  24. package/dist/types/rendering/entity-render-engine-point.d.ts +10 -1
  25. package/dist/types/rendering/render-managers/common/entity-label-group.d.ts +9 -1
  26. package/dist/types/rendering/render-managers/common/entity-label.d.ts +13 -2
  27. package/dist/types/rendering/visuals-register.d.ts +1 -0
  28. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  import * as Cesium from 'cesium';
2
- import { Cartographic, ColorMaterialProperty, Entity, Color, ConstantProperty, CallbackProperty, Primitive, Cesium3DTileFeature, ShaderSource, Cartesian3, Math as Math$1, Ellipsoid, Transforms, Matrix4, Geometry, GeometryAttribute, ComponentDatatype, PrimitiveType, BoundingSphere, GeometryPipeline, Cartesian2, Cartesian4, sampleTerrain, Texture, PixelFormat, Sampler, TextureWrap, TextureMinificationFilter, TextureMagnificationFilter, VertexArray, BufferUsage, ShaderProgram, RenderState, BlendingState, DrawCommand, Pass, HorizontalOrigin, VerticalOrigin, HeightReference, ClassificationType, ConstantPositionProperty, DistanceDisplayCondition, JulianDate, Quaternion, HeadingPitchRoll, ArcType, CornerType, ShadowMode, ColorBlendMode, Model, SceneTransforms, ImageMaterialProperty, PolygonHierarchy, PolylineGraphics, Rectangle, NearFarScalar, Matrix3, KmlDataSource, GeoJsonDataSource, FeatureDetection, SceneMode, Cesium3DTileStyle, HeadingPitchRange, Ion, IonResource, Cesium3DTileColorBlendMode, Cesium3DTileset, EllipsoidTerrainProvider, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, OrthographicFrustum, EasingFunction, Intersect, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, CustomDataSource, PolygonPipeline, CesiumInspector, defined, ClockRange, DynamicEnvironmentMapManager, EllipsoidGeodesic, sampleTerrainMostDetailed, ScreenSpaceEventHandler, ScreenSpaceEventType, GeometryInstance, Fullscreen, CzmlDataSource } from 'cesium';
3
- import { Cartes, LRUCache, ProjectViewTile, ENVIRONMENT, Api, Calculator, ClientFile, Entity as Entity$1, EntityTag, EntityType, ObjectUtils, Style, Carto, Geometry as Geometry$1, EntityLod, MenuItem, ProjectView, ProjectViewBookmark, Tileset, ZoomControl, BruceEvent, EntityCoords, Bounds, ClientFileValueMap, Color as Color$1, DataLab, DelayQueue, AccountConcept, BruceApi, EntityRelation, RecordChangeFeed, EntitySource, ProgramKey, ProjectViewLegacyTile, Camera, Account, AccountLimits, Session, UrlUtils, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, ProjectViewBookmarkGroup, EntityRelationType, EntityHistoricData } from 'bruce-models';
2
+ import { Cartographic, ShaderSource, Cartesian3, Math as Math$1, Ellipsoid, Transforms, Matrix4, Geometry, GeometryAttribute, ComponentDatatype, PrimitiveType, BoundingSphere, GeometryPipeline, Cartesian2, Cartesian4, sampleTerrain, Texture, PixelFormat, Sampler, TextureWrap, TextureMinificationFilter, TextureMagnificationFilter, VertexArray, BufferUsage, ShaderProgram, RenderState, BlendingState, DrawCommand, Pass, Entity, ConstantPositionProperty, HeightReference, Color, DistanceDisplayCondition, ColorMaterialProperty, JulianDate, Quaternion, HeadingPitchRoll, ClassificationType, ArcType, CornerType, ShadowMode, ConstantProperty, CallbackProperty, Primitive, Cesium3DTileFeature, HorizontalOrigin, VerticalOrigin, ImageMaterialProperty, SceneTransforms, PolygonHierarchy, PolylineGraphics, Rectangle, ColorBlendMode, Model, FeatureDetection, SceneMode, NearFarScalar, Matrix3, KmlDataSource, GeoJsonDataSource, Cesium3DTileStyle, HeadingPitchRange, Ion, IonResource, Cesium3DTileColorBlendMode, Cesium3DTileset, Intersect, OrthographicFrustum, EasingFunction, EllipsoidTerrainProvider, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, PolygonPipeline, EllipsoidGeodesic, sampleTerrainMostDetailed, defined, CesiumInspector, ClockRange, DynamicEnvironmentMapManager, GeometryInstance, ScreenSpaceEventHandler, ScreenSpaceEventType, CustomDataSource, Fullscreen, CzmlDataSource } from 'cesium';
3
+ import { Cartes, LRUCache, ProjectViewTile, ENVIRONMENT, Calculator, Carto, Entity as Entity$1, EntityTag, EntityType, Geometry as Geometry$1, ObjectUtils, Style, Api, ClientFile, ClientFileValueMap, Color as Color$1, MenuItem, ProjectView, ProjectViewBookmark, Tileset, ZoomControl, BruceEvent, EntityCoords, Bounds, EntityLod, UrlUtils, DataLab, AccountConcept, DelayQueue, BruceApi, EntityRelation, RecordChangeFeed, EntitySource, Camera, ProgramKey, ProjectViewLegacyTile, Account, AccountLimits, Session, ProjectViewBookmarkGroup, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, EntityRelationType, EntityHistoricData } from 'bruce-models';
4
4
 
5
5
  /**
6
6
  * Ensures a number is returned from a given value.
@@ -5531,11 +5531,196 @@ var EntityLabel;
5531
5531
  const ICON_RING_PADDING = 4;
5532
5532
  // Small points still get a ring as large as the default near ring.
5533
5533
  const ICON_RING_MIN = 10;
5534
+ // A photo grows by this much while its entity is hovered, since it is content worth a closer look.
5535
+ const PHOTO_HOVER_SCALE = 2.5;
5536
+ // How long a label must stay on show before its photo is fetched, so labels that only flash past while
5537
+ // clusters settle or the camera sweeps by cost nothing.
5538
+ const PHOTO_SHOW_DELAY = 250;
5539
+ // Photos rest this much larger than an icon on its chip, since they are content rather than a marker.
5540
+ const PHOTO_SIZE_SCALE = 1.5;
5541
+ // Corner radius of a photo too oblong for a circle, relative to its longest side.
5542
+ const PHOTO_CORNER = 0.15;
5543
+ // How long a photo takes to grow into its label, and to grow on hover.
5544
+ const PHOTO_TRANSITION = 250;
5545
+ // Downsized photos kept per viewer before the least recently used is released.
5546
+ const PHOTO_CACHE_LIMIT = 500;
5547
+ // Key where the viewer keeps its downsized photos.
5548
+ const PHOTO_CACHE_KEY = "_nextspaceLabelPhotos";
5534
5549
  // One per tinted icon, since an SVG mask is addressed by id across the whole document.
5535
5550
  let tintCounter = 0;
5536
5551
  // Key where the label instance is stored within the cesium entity.
5537
5552
  // Eg: rego[ATTACH_KEY] = new EntityLabel(...);
5538
5553
  EntityLabel.ATTACH_KEY = "nextspace-navigator-entity-label";
5554
+ // Image types a label draws, read from the file extension. Anything else stays text.
5555
+ const IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp"];
5556
+ // Shortest side a thumbnail needs before it is preferred over the full file.
5557
+ const IMAGE_THUMBNAIL_MIN = 64;
5558
+ // Size a found image is drawn at when the group names none, so a photo never lands at its natural size.
5559
+ const IMAGE_SIZE = 24;
5560
+ function isImageFile(name) {
5561
+ return typeof name === "string" && IMAGE_EXTENSIONS.includes(name.split(".").pop().toLowerCase());
5562
+ }
5563
+ // Whether a label was configured with an icon of its own, which always outranks a found photo.
5564
+ function HasIcon(options) {
5565
+ return Boolean(options.iconUrl || options.iconClass || options.icon);
5566
+ }
5567
+ EntityLabel.HasIcon = HasIcon;
5568
+ /*
5569
+ * Whether a label line is an http(s) link to an image, judged by its path so a query string cannot pass for one.
5570
+ */
5571
+ function IsImageUrl(value) {
5572
+ const text = value === null || value === void 0 ? void 0 : value.trim();
5573
+ if (!text || /\s/.test(text)) {
5574
+ return false;
5575
+ }
5576
+ let url;
5577
+ try {
5578
+ url = new URL(text);
5579
+ }
5580
+ catch {
5581
+ return false;
5582
+ }
5583
+ return (url.protocol === "http:" || url.protocol === "https:") && isImageFile(url.pathname);
5584
+ }
5585
+ EntityLabel.IsImageUrl = IsImageUrl;
5586
+ /*
5587
+ * The entity's default image as InfoView picks it: its first photo attachment by display order that is an image file.
5588
+ * Reads attachments expanded onto the entity, and prefers the smallest thumbnail that still covers an icon.
5589
+ */
5590
+ function GetDefaultImageUrl(entity, api) {
5591
+ var _a, _b, _c;
5592
+ 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 : [];
5593
+ const photo = attachments
5594
+ .filter(x => (x === null || x === void 0 ? void 0 : x["EntityAttachmentType.ID"]) == EntityAttachmentType.EType.Photo && x.ClientFile)
5595
+ .filter(x => isImageFile(x.ClientFile.FileExt || x.ClientFile.OriginalFileName))
5596
+ .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];
5597
+ if (!photo) {
5598
+ return null;
5599
+ }
5600
+ const thumbnail = ((_c = photo.ClientFile.Thumbnails) !== null && _c !== void 0 ? _c : [])
5601
+ .filter(x => Math.min(x.Width, x.Height) >= IMAGE_THUMBNAIL_MIN && (x.URL || x["ClientFile.ID"]))
5602
+ .sort((a, b) => a.Width * a.Height - b.Width * b.Height)[0];
5603
+ if (thumbnail) {
5604
+ return thumbnail.URL || ClientFile.GetUrl({ fileId: thumbnail["ClientFile.ID"], api, viaCdn: true });
5605
+ }
5606
+ return photo.ClientFile.URL || photo.URL || ClientFile.GetUrl({ fileId: photo["ClientFile.ID"], api, viaCdn: true });
5607
+ }
5608
+ EntityLabel.GetDefaultImageUrl = GetDefaultImageUrl;
5609
+ /*
5610
+ * Gives a label an image it found rather than one it was configured with.
5611
+ * A text label gains it beside the text, or alone when the image was all the text there was.
5612
+ */
5613
+ function WithPhoto(options, url) {
5614
+ var _a, _b, _c, _d;
5615
+ const presentation = (_a = options.presentation) !== null && _a !== void 0 ? _a : "text";
5616
+ options.iconUrl = url;
5617
+ options.iconPhoto = true;
5618
+ // A tint would flatten a photo into a silhouette of its own outline.
5619
+ options.iconColor = undefined;
5620
+ (_b = options.iconSize) !== null && _b !== void 0 ? _b : (options.iconSize = IMAGE_SIZE);
5621
+ if (!((_c = options.text) === null || _c === void 0 ? void 0 : _c.trim())) {
5622
+ options.presentation = "icon";
5623
+ }
5624
+ else if (presentation === "text") {
5625
+ options.presentation = "icon-text-side";
5626
+ }
5627
+ if (presentation === "text") {
5628
+ // Keeps the leader a text label draws by default.
5629
+ (_d = options.leader) !== null && _d !== void 0 ? _d : (options.leader = true);
5630
+ }
5631
+ return options;
5632
+ }
5633
+ EntityLabel.WithPhoto = WithPhoto;
5634
+ /*
5635
+ * Near-square photos are cropped to a circle the size of an icon chip; the rest keep their shape with rounded corners.
5636
+ * An unknown size reads as square, since the circle crops whatever arrives.
5637
+ */
5638
+ function PhotoShape(width, height) {
5639
+ if (!(width > 0 && height > 0)) {
5640
+ return "circle";
5641
+ }
5642
+ const longest = Math.max(width, height);
5643
+ return (longest - Math.min(width, height)) / longest <= ICON_BACKGROUND_UNIFORM_TOLERANCE ? "circle" : "rect";
5644
+ }
5645
+ EntityLabel.PhotoShape = PhotoShape;
5646
+ /*
5647
+ * Loads a photo through a plain image element, so a host without CORS still works, and redraws it once onto a canvas
5648
+ * no larger than it is shown. Labels draw from that canvas, so none of them holds a camera original.
5649
+ */
5650
+ async function shrinkPhoto(url, pixels) {
5651
+ const none = { source: null, width: 0, height: 0 };
5652
+ const image = new Image();
5653
+ image.decoding = "async";
5654
+ image.src = url;
5655
+ try {
5656
+ await image.decode();
5657
+ }
5658
+ catch {
5659
+ return none;
5660
+ }
5661
+ const width = image.naturalWidth;
5662
+ const height = image.naturalHeight;
5663
+ if (!width || !height) {
5664
+ return none;
5665
+ }
5666
+ // A circle only ever shows the centre square, so that is all worth keeping.
5667
+ const circle = PhotoShape(width, height) === "circle";
5668
+ const sourceWidth = circle ? Math.min(width, height) : width;
5669
+ const sourceHeight = circle ? Math.min(width, height) : height;
5670
+ const scale = Math.min(1, pixels / Math.max(sourceWidth, sourceHeight));
5671
+ const canvas = document.createElement("canvas");
5672
+ canvas.width = Math.max(1, Math.round(sourceWidth * scale));
5673
+ canvas.height = Math.max(1, Math.round(sourceHeight * scale));
5674
+ const context = canvas.getContext("2d");
5675
+ if (!context) {
5676
+ return none;
5677
+ }
5678
+ context.imageSmoothingQuality = "high";
5679
+ context.drawImage(image, (width - sourceWidth) / 2, (height - sourceHeight) / 2, sourceWidth, sourceHeight, 0, 0, canvas.width, canvas.height);
5680
+ return { source: canvas, width, height };
5681
+ }
5682
+ /*
5683
+ * Downsized label photos, kept on the viewer so every label in a scene shares one load and one decode.
5684
+ */
5685
+ class Photos {
5686
+ constructor() {
5687
+ this.entries = new Map();
5688
+ this.ready = new Map();
5689
+ }
5690
+ static Get(viewer) {
5691
+ var _a;
5692
+ return (_a = viewer[PHOTO_CACHE_KEY]) !== null && _a !== void 0 ? _a : (viewer[PHOTO_CACHE_KEY] = new Photos());
5693
+ }
5694
+ // The photo when it is already drawn, so a rebuilt label shows it at once instead of growing it again.
5695
+ Peek(url, pixels) {
5696
+ return this.ready.get(`${pixels} ${url}`);
5697
+ }
5698
+ Load(url, pixels) {
5699
+ const key = `${pixels} ${url}`;
5700
+ let entry = this.entries.get(key);
5701
+ if (entry) {
5702
+ // Reinsertion keeps the map in least recently used order.
5703
+ this.entries.delete(key);
5704
+ this.entries.set(key, entry);
5705
+ return entry;
5706
+ }
5707
+ entry = shrinkPhoto(url, pixels).then(photo => {
5708
+ if (this.entries.get(key) === entry) {
5709
+ this.ready.set(key, photo);
5710
+ }
5711
+ return photo;
5712
+ });
5713
+ this.entries.set(key, entry);
5714
+ for (const oldest of this.entries.keys()) {
5715
+ if (this.entries.size <= PHOTO_CACHE_LIMIT) {
5716
+ break;
5717
+ }
5718
+ this.entries.delete(oldest);
5719
+ this.ready.delete(oldest);
5720
+ }
5721
+ return entry;
5722
+ }
5723
+ }
5539
5724
  /*
5540
5725
  * Indexes screen rectangles so sparse scenes do not compare every label with every other label.
5541
5726
  */
@@ -6251,6 +6436,10 @@ var EntityLabel;
6251
6436
  this.cursorMotion = { amount: 0, from: 0, target: 0, start: 0, position: undefined };
6252
6437
  // Counter to help differentiate between createLabel calls.
6253
6438
  this.counter = 0;
6439
+ // Whether this label has asked its group for a default image, which it does once on screen.
6440
+ this.imageAsked = false;
6441
+ // The photo a label outside any group found while reading its attributes.
6442
+ this.photo = null;
6254
6443
  // The label element itself.
6255
6444
  this.label = null;
6256
6445
  // Callback to kill render listener.
@@ -6285,6 +6474,7 @@ var EntityLabel;
6285
6474
  this.visible = params.visible;
6286
6475
  if (((_c = params.options) === null || _c === void 0 ? void 0 : _c.text) != null)
6287
6476
  this.additionalTexts = [];
6477
+ this.photo = null;
6288
6478
  this.prePositioned = !isNaN((_d = params.pos3d) === null || _d === void 0 ? void 0 : _d.x);
6289
6479
  this.createLabel(params.pos3d);
6290
6480
  if (!params.api && !ENVIRONMENT.IS_SELF_MANAGED && this.options.text == null) {
@@ -6347,6 +6537,7 @@ var EntityLabel;
6347
6537
  this.options = options || {};
6348
6538
  this.groupId = groupId;
6349
6539
  this.additionalTexts = this.options.text != null ? [] : null;
6540
+ this.photo = null;
6350
6541
  this.createLabel();
6351
6542
  }
6352
6543
  /**
@@ -6354,8 +6545,8 @@ var EntityLabel;
6354
6545
  * @param starterPos3d optional position to create label at. Ignored for billboard labels.
6355
6546
  */
6356
6547
  async createLabel(starterPos3d) {
6357
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
6358
- this.removeLabel();
6548
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
6549
+ // The old label stays until its replacement is built, so a rebuild never blinks while a location loads.
6359
6550
  let pos3d = starterPos3d ? starterPos3d.clone() : null;
6360
6551
  const counter = ++this.counter;
6361
6552
  if (this.disposed) {
@@ -6513,6 +6704,7 @@ var EntityLabel;
6513
6704
  }
6514
6705
  }
6515
6706
  if (isNaN(pos3d === null || pos3d === void 0 ? void 0 : pos3d.x)) {
6707
+ this.removeLabel();
6516
6708
  return;
6517
6709
  }
6518
6710
  pos3d = DrawingUtils.EnsurePosHeight({
@@ -6522,6 +6714,7 @@ var EntityLabel;
6522
6714
  viewer: this.viewer
6523
6715
  });
6524
6716
  if (isNaN(pos3d === null || pos3d === void 0 ? void 0 : pos3d.x)) {
6717
+ this.removeLabel();
6525
6718
  return;
6526
6719
  }
6527
6720
  this._pos3d = pos3d;
@@ -6587,35 +6780,39 @@ var EntityLabel;
6587
6780
  catch (e) {
6588
6781
  console.error(e);
6589
6782
  }
6590
- // Type could be deleted, however we'll just catch that to show a label instead of nothing.
6591
- // This is not unusual for Tilesets where the Type that is baked might be outdated.
6592
- if (entityType || this.options.attributes) {
6593
- 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;
6594
- if (attributes) {
6595
- try {
6596
- const { entity } = await Entity$1.Get({
6597
- api: this.api,
6598
- entityId: this.rego.entityId,
6599
- 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,
6600
- schemaId: this.rego.schema,
6601
- migrated: true
6602
- });
6603
- for (let i = 0; i < attributes.length; i++) {
6604
- const value = Entity$1.GetValue({
6605
- entity: entity,
6606
- path: attributes[i]
6607
- });
6608
- if (value != null) {
6609
- additionalTexts.push(value);
6610
- }
6611
- }
6612
- }
6613
- // Entity doesn't exist.
6614
- // Likely a graphic with an ID that points to no record.
6615
- catch (e) {
6616
- console.error(e);
6783
+ 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;
6784
+ // Read for its attributes, and for a photo when the label has no icon of its own.
6785
+ if (attributes || !HasIcon(this.options)) {
6786
+ let entity = null;
6787
+ try {
6788
+ entity = (await Entity$1.Get({
6789
+ api: this.api,
6790
+ entityId: this.rego.entityId,
6791
+ 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,
6792
+ schemaId: this.rego.schema,
6793
+ migrated: true,
6794
+ expandAttachments: !HasIcon(this.options)
6795
+ })).entity;
6796
+ }
6797
+ // Entity doesn't exist.
6798
+ // Likely a graphic with an ID that points to no record.
6799
+ catch (e) {
6800
+ console.error(e);
6801
+ }
6802
+ for (let i = 0; entity && attributes && i < attributes.length; i++) {
6803
+ const value = Entity$1.GetValue({
6804
+ entity: entity,
6805
+ path: attributes[i]
6806
+ });
6807
+ if (value != null) {
6808
+ additionalTexts.push(value);
6617
6809
  }
6618
6810
  }
6811
+ // A linked image outranks the default one, as it does for group labels.
6812
+ if (entity && !HasIcon(this.options)) {
6813
+ const at = additionalTexts.findIndex(value => IsImageUrl(String(value)));
6814
+ this.photo = at >= 0 ? String(additionalTexts.splice(at, 1)[0]).trim() : GetDefaultImageUrl(entity, this.api);
6815
+ }
6619
6816
  }
6620
6817
  }
6621
6818
  this.additionalTexts = additionalTexts;
@@ -6634,9 +6831,13 @@ var EntityLabel;
6634
6831
  if (this.disposed || this.counter != counter) {
6635
6832
  return;
6636
6833
  }
6637
- const presentation = this.options.presentation || "text";
6834
+ this.removeLabel();
6835
+ // A found photo draws as if configured, without changing the options clustering compares and measures.
6836
+ const photo = HasIcon(this.options) ? null : (_j = this.options.photo) !== null && _j !== void 0 ? _j : this.photo;
6837
+ const look = photo ? WithPhoto({ ...this.options, text: (_k = this.options.text) !== null && _k !== void 0 ? _k : [text, ...additionalTexts].join("\n") }, photo) : this.options;
6838
+ const presentation = look.presentation || "text";
6638
6839
  let clickEntityIds = [];
6639
- const selectOnClick = (_j = this.options.selectOnClick) !== null && _j !== void 0 ? _j : Boolean(this.groupId);
6840
+ const selectOnClick = (_l = this.options.selectOnClick) !== null && _l !== void 0 ? _l : Boolean(this.groupId);
6640
6841
  const select = (event) => {
6641
6842
  event.preventDefault();
6642
6843
  event.stopPropagation();
@@ -6665,11 +6866,11 @@ var EntityLabel;
6665
6866
  target.addEventListener(type, event => event.stopPropagation());
6666
6867
  }
6667
6868
  };
6668
- const normalLeader = (_k = this.options.leader) !== null && _k !== void 0 ? _k : presentation === "text";
6869
+ const normalLeader = (_m = look.leader) !== null && _m !== void 0 ? _m : presentation === "text";
6669
6870
  const angled = this.options.leaderStyle === "angled";
6670
6871
  let leader = normalLeader;
6671
6872
  const content = document.createElement("span");
6672
- const originalText = (_l = this.options.text) !== null && _l !== void 0 ? _l : [text, ...additionalTexts].join("\n");
6873
+ const originalText = (_o = this.options.text) !== null && _o !== void 0 ? _o : [text, ...additionalTexts].join("\n");
6673
6874
  let textElement;
6674
6875
  let sourceText;
6675
6876
  let sourceNaturalText;
@@ -6684,10 +6885,33 @@ var EntityLabel;
6684
6885
  if (presentation === "icon-text-above") {
6685
6886
  content.style.flexDirection = "column-reverse";
6686
6887
  }
6888
+ // A found photo's shape decides its layout once it loads: a rectangle sits under the name it adds to, a circle beside it.
6889
+ let photoShape = null;
6890
+ let growPhoto = null;
6891
+ // Loads the photo; called once the label is actually showing, so labels out of view or folded into a cluster never fetch one.
6892
+ let startPhoto = null;
6893
+ let photoTimer = null;
6894
+ const placePhoto = (shape, grow) => {
6895
+ photoShape = shape;
6896
+ growPhoto = grow;
6897
+ content.style.flexDirection = shape === "rect" ? "column-reverse" : "";
6898
+ };
6687
6899
  if (presentation !== "text") {
6688
- const built = this.buildIcon(this.options, counter);
6900
+ const built = this.buildIcon(look, counter, placePhoto);
6901
+ startPhoto = built.start;
6689
6902
  const icon = iconElement = built.icon;
6690
6903
  fitIconBackground = built.fit;
6904
+ if (built.arriving) {
6905
+ // The gap opens with the photo, so a text label does not shift before its photo grows in.
6906
+ content.style.gap = "0px";
6907
+ built.arriving.then(() => {
6908
+ if (iconElement.style.display === "none") {
6909
+ return;
6910
+ }
6911
+ content.style.transition = `gap ${PHOTO_TRANSITION}ms ease-out`;
6912
+ content.style.gap = "6px";
6913
+ });
6914
+ }
6691
6915
  makeClickable(icon);
6692
6916
  // Keep badges outside the glyph so font classes and image masks cannot affect them.
6693
6917
  iconContainer = document.createElement("span");
@@ -6709,7 +6933,7 @@ var EntityLabel;
6709
6933
  if (presentation === "text")
6710
6934
  makeClickable(textElement);
6711
6935
  content.appendChild(textElement);
6712
- if ((_m = this.options.cluster) === null || _m === void 0 ? void 0 : _m.enabled) {
6936
+ if ((_p = this.options.cluster) === null || _p === void 0 ? void 0 : _p.enabled) {
6713
6937
  sourceText = document.createElement("span");
6714
6938
  sourceText.className = "entity-label-source-text";
6715
6939
  sourceText.setAttribute("aria-hidden", "true");
@@ -6729,7 +6953,9 @@ var EntityLabel;
6729
6953
  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;";
6730
6954
  // Text above the icon would have the badge land in it, so the free corner is the lower one.
6731
6955
  const badgeBelow = presentation === "icon-text-above";
6732
- if (iconContainer) {
6956
+ // A photo is put away while its label stands for a cluster, so its count sits with the text instead.
6957
+ const badgeOnIcon = Boolean(iconContainer) && !look.iconPhoto;
6958
+ if (badgeOnIcon) {
6733
6959
  countElement.style.cssText += `position:absolute;left:100%;top:${badgeBelow ? "100%" : "0"};transform:translate(-50%,-50%);`;
6734
6960
  iconContainer.appendChild(countElement);
6735
6961
  }
@@ -6746,11 +6972,11 @@ var EntityLabel;
6746
6972
  else {
6747
6973
  ele = document.createElement("div");
6748
6974
  ele.appendChild(content);
6749
- if (!iconContainer) {
6975
+ if (!badgeOnIcon) {
6750
6976
  ele.appendChild(countElement);
6751
6977
  }
6752
6978
  ele.className = "entity-label";
6753
- ele.dataset.entityId = ((_o = this.rego) === null || _o === void 0 ? void 0 : _o.entityId) || "";
6979
+ ele.dataset.entityId = ((_q = this.rego) === null || _q === void 0 ? void 0 : _q.entityId) || "";
6754
6980
  ele.dataset.presentation = presentation;
6755
6981
  if (this.groupId) {
6756
6982
  ele.dataset.groupId = this.groupId;
@@ -6903,7 +7129,7 @@ var EntityLabel;
6903
7129
  };
6904
7130
  let cursorMotion = this.cursorMotion;
6905
7131
  const project = () => {
6906
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
7132
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
6907
7133
  if (this.disposed || !this.viewer || this.viewer.isDestroyed() || ((_a = this.visible) === null || _a === void 0 ? void 0 : _a.call(this)) === false ||
6908
7134
  (presentation === "icon" && this.options.iconScale === 0)) {
6909
7135
  return null;
@@ -6941,13 +7167,18 @@ var EntityLabel;
6941
7167
  if (!Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.x) || !Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.y)) {
6942
7168
  return null;
6943
7169
  }
6944
- if (leader && (anchor.x < 0 || anchor.y < 0 || anchor.x >= this.viewer.canvas.clientWidth || anchor.y >= this.viewer.canvas.clientHeight))
7170
+ const onScreen = anchor.x >= 0 && anchor.y >= 0 && anchor.x < this.viewer.canvas.clientWidth && anchor.y < this.viewer.canvas.clientHeight;
7171
+ if (leader && !onScreen)
6945
7172
  return null;
7173
+ if (onScreen && !this.imageAsked && this.groupId && !HasIcon(this.options) && !this.options.photo) {
7174
+ this.imageAsked = true;
7175
+ (_e = this.visualRegister) === null || _e === void 0 ? void 0 : _e.RequestLabelImage((_f = this.rego) === null || _f === void 0 ? void 0 : _f.entityId);
7176
+ }
6946
7177
  const transientPivot = (this.Transient && !this.Persistent ?
6947
- (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);
7178
+ (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);
6948
7179
  const hover = (presentation !== "icon" ?
6949
7180
  (this.Transient && !this.Persistent && this.hoverFade.to === 0 ? this.retainedHoverPosition :
6950
- (_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);
7181
+ (_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);
6951
7182
  const reused = !this.Transient || this.Persistent;
6952
7183
  const now = Date.now();
6953
7184
  const animateCursor = () => {
@@ -6974,8 +7205,8 @@ var EntityLabel;
6974
7205
  const icon = measureIcon();
6975
7206
  const iconRing = icon ? Math.max(ICON_RING_MIN, icon.radius + ICON_RING_PADDING) : 0;
6976
7207
  const pivot = icon ? undefined : transientPivot;
6977
- let x = ((_l = pivot === null || pivot === void 0 ? void 0 : pivot.x) !== null && _l !== void 0 ? _l : anchor.x) + (icon || pixelOffset).x;
6978
- let y = ((_m = pivot === null || pivot === void 0 ? void 0 : pivot.y) !== null && _m !== void 0 ? _m : anchor.y) + (icon || pixelOffset).y;
7208
+ let x = ((_o = pivot === null || pivot === void 0 ? void 0 : pivot.x) !== null && _o !== void 0 ? _o : anchor.x) + (icon || pixelOffset).x;
7209
+ let y = ((_p = pivot === null || pivot === void 0 ? void 0 : pivot.y) !== null && _p !== void 0 ? _p : anchor.y) + (icon || pixelOffset).y;
6979
7210
  if (cursorAmount > 0 && !icon) {
6980
7211
  // A leader-less label starts centred on its anchor; preserve that box as it lifts away.
6981
7212
  if (!normalLeader) {
@@ -7027,11 +7258,12 @@ var EntityLabel;
7027
7258
  const width = ele.offsetWidth / coordinates.scaleX;
7028
7259
  const height = ele.offsetHeight / coordinates.scaleY;
7029
7260
  let clusterBounds;
7030
- if ((_o = this.options.cluster) === null || _o === void 0 ? void 0 : _o.enabled) {
7261
+ if ((_q = this.options.cluster) === null || _q === void 0 ? void 0 : _q.enabled) {
7031
7262
  const textWidth = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetWidth) || 0;
7032
7263
  const textHeight = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetHeight) || 0;
7033
- const iconWidth = (iconElement === null || iconElement === void 0 ? void 0 : iconElement.offsetWidth) || 0;
7034
- const iconHeight = (iconElement === null || iconElement === void 0 ? void 0 : iconElement.offsetHeight) || 0;
7264
+ // A photo is never part of the footprint, so its arrival cannot change which cluster a label joins.
7265
+ const iconWidth = look.iconPhoto ? 0 : (iconElement === null || iconElement === void 0 ? void 0 : iconElement.offsetWidth) || 0;
7266
+ const iconHeight = look.iconPhoto ? 0 : (iconElement === null || iconElement === void 0 ? void 0 : iconElement.offsetHeight) || 0;
7035
7267
  const gap = sourceText && iconWidth ? 6 : 0;
7036
7268
  const above = presentation === "icon-text-above";
7037
7269
  // Match the fixed content gap and label padding, excluding the count badge.
@@ -7060,7 +7292,7 @@ var EntityLabel;
7060
7292
  if (!leader && (bounds.x + bounds.width < 0 || bounds.y + bounds.height < 0 || bounds.x > this.viewer.canvas.clientWidth || bounds.y > this.viewer.canvas.clientHeight)) {
7061
7293
  return null;
7062
7294
  }
7063
- 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 });
7295
+ 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 });
7064
7296
  const opacity = this.prePositioned || hoverOnly || MAX_DISTANCE === Infinity ? 1 : 1 - distance / MAX_DISTANCE;
7065
7297
  return {
7066
7298
  canFlip: leader && presentation !== "icon", hasLeader: leader,
@@ -7068,8 +7300,8 @@ var EntityLabel;
7068
7300
  transient: this.Transient || Boolean(hover) || cursorAmount > 0,
7069
7301
  extendedGeometry: visual instanceof Entity && Boolean(visual.polygon || visual.polyline || visual.corridor),
7070
7302
  viewportWidth: this.viewer.canvas.clientWidth,
7071
- 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,
7072
- 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,
7303
+ 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,
7304
+ 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,
7073
7305
  iconRadius: icon === null || icon === void 0 ? void 0 : icon.radius,
7074
7306
  // Priority shows the hovered member individually without dragging its cluster along.
7075
7307
  opacity, priority: hover || cursorAmount > 0 || (state === null || state === void 0 ? void 0 : state.highlighted) ? 1 : 0, options: this.options, text: originalText, clusterBounds
@@ -7078,7 +7310,7 @@ var EntityLabel;
7078
7310
  let displacement = { x: 0, y: 0 };
7079
7311
  let nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
7080
7312
  const apply = (placement) => {
7081
- 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;
7313
+ 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;
7082
7314
  const show = (placement === null || placement === void 0 ? void 0 : placement.visible) && placement.opacity > 0;
7083
7315
  clickEntityIds = show ? placement.entityIds : [];
7084
7316
  ele.style.visibility = show ? "visible" : "hidden";
@@ -7092,9 +7324,30 @@ var EntityLabel;
7092
7324
  if (!this.Transient) {
7093
7325
  this.ringAnimation = undefined;
7094
7326
  }
7327
+ clearTimeout(photoTimer);
7328
+ photoTimer = null;
7095
7329
  return;
7096
7330
  }
7097
- const displayText = (_a = placement.text) !== null && _a !== void 0 ? _a : originalText;
7331
+ // A label standing for a cluster shows its summary only; its photo is put away and never fetched.
7332
+ const solo = !((_a = placement.members) === null || _a === void 0 ? void 0 : _a.length);
7333
+ if (look.iconPhoto && iconElement && iconElement.dataset.failed !== "true") {
7334
+ const display = solo ? "inline-flex" : "none";
7335
+ if (iconElement.style.display !== display) {
7336
+ iconElement.style.display = display;
7337
+ }
7338
+ }
7339
+ if (!solo) {
7340
+ clearTimeout(photoTimer);
7341
+ photoTimer = null;
7342
+ }
7343
+ else if (startPhoto && photoTimer == null) {
7344
+ photoTimer = setTimeout(() => {
7345
+ if (!this.disposed && this.counter === counter) {
7346
+ startPhoto();
7347
+ }
7348
+ }, PHOTO_SHOW_DELAY);
7349
+ }
7350
+ const displayText = (_b = placement.text) !== null && _b !== void 0 ? _b : originalText;
7098
7351
  if (textElement && textElement.textContent !== displayText) {
7099
7352
  textElement.textContent = displayText;
7100
7353
  // Measure the summary's footprint next frame without replacing each member's source text.
@@ -7118,10 +7371,12 @@ var EntityLabel;
7118
7371
  const stack = JSON.stringify(placement.icons || []);
7119
7372
  if (iconStack && stack !== displayedStack) {
7120
7373
  displayedStack = stack;
7121
- const looks = ((_b = placement.icons) === null || _b === void 0 ? void 0 : _b.length) ? [iconLook(this.options), ...placement.icons] : [];
7374
+ const looks = ((_c = placement.icons) === null || _c === void 0 ? void 0 : _c.length) ? [iconLook(this.options), ...placement.icons] : [];
7122
7375
  // One small size for all, so no icon in the arrangement reads as more important than another.
7123
- const size = (this.options.iconSize || 24) * ((_c = this.options.iconScale) !== null && _c !== void 0 ? _c : 1) * ICON_STACK_SCALE;
7376
+ const size = (this.options.iconSize || 24) * ((_d = this.options.iconScale) !== null && _d !== void 0 ? _d : 1) * ICON_STACK_SCALE;
7124
7377
  const built = looks.map(look => this.buildIcon({ ...look, iconSize: size }, counter));
7378
+ // A stack is on show already, so its photos load straight away.
7379
+ built.forEach(({ start }) => start === null || start === void 0 ? void 0 : start());
7125
7380
  // Two sit side by side; three make a triangle with the representative on top.
7126
7381
  iconStack.style.gridTemplateColumns = built.length === 3 ? "auto auto" : `repeat(${built.length}, auto)`;
7127
7382
  if (built.length === 3) {
@@ -7138,7 +7393,7 @@ var EntityLabel;
7138
7393
  }
7139
7394
  for (const fit of stackedFits)
7140
7395
  fit();
7141
- if (iconContainer) {
7396
+ if (badgeOnIcon) {
7142
7397
  const badgeWidth = countElement.offsetWidth;
7143
7398
  const iconWidth = iconElement.offsetWidth;
7144
7399
  const narrow = iconWidth < iconElement.offsetHeight * 0.6;
@@ -7153,16 +7408,28 @@ var EntityLabel;
7153
7408
  // Summary and badge changes must fit on their first visible frame, including after resize.
7154
7409
  const ring = this.options.anchorRing;
7155
7410
  const pointer = (layer.pointer || (cursorMotion.target ? cursorMotion.position : undefined) || (this.Transient && !this.Persistent ?
7156
- (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));
7411
+ (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));
7157
7412
  const iconRing = frame.iconRadius ? Math.max(ICON_RING_MIN, frame.iconRadius + ICON_RING_PADDING) : 0;
7158
- 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);
7159
- 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));
7413
+ 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);
7414
+ if (look.iconPhoto && growPhoto) {
7415
+ // A rectangle leans on the leader's side of the label.
7416
+ if (photoShape === "rect") {
7417
+ const align = !leader ? "center" : placement.textSide === "left" ? "flex-end" : "flex-start";
7418
+ if (content.style.alignItems !== align) {
7419
+ content.style.alignItems = align;
7420
+ }
7421
+ }
7422
+ // Grown in layout rather than scaled, so the border keeps its width, and the label, whose bottom
7423
+ // sits on the leader, grows up and sideways without changing the leader's length.
7424
+ growPhoto(Boolean(hovered) && !((_j = placement.members) === null || _j === void 0 ? void 0 : _j.length));
7425
+ }
7426
+ 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));
7160
7427
  const targetRadius = normalRadius + (15.4 - normalRadius) * cursorMotion.amount;
7161
7428
  // Around a glyph the ring keeps the glyph's size and only fades its stroke in, so it never covers the icon.
7162
7429
  const targetOuter = iconRing || targetRadius || 2.5;
7163
7430
  const targetInner = iconRing ? (targetRadius > 0 ? iconRing - 2 : iconRing) : Math.max(0, targetRadius - 2);
7164
7431
  const now = Date.now();
7165
- const duration = (_l = ring === null || ring === void 0 ? void 0 : ring.transitionDuration) !== null && _l !== void 0 ? _l : 200;
7432
+ const duration = (_o = ring === null || ring === void 0 ? void 0 : ring.transitionDuration) !== null && _o !== void 0 ? _o : 200;
7166
7433
  const sharedRing = this.Transient && !this.Persistent && this.hoverFade.to === 1 ? this.hoverRingState : undefined;
7167
7434
  let ringAnimation = sharedRing ? sharedRing.animation : this.ringAnimation;
7168
7435
  if (!ringAnimation) {
@@ -7204,19 +7471,19 @@ var EntityLabel;
7204
7471
  if (leader && angled) {
7205
7472
  // The declutter owns the side; the fit only says which one had the room to begin with.
7206
7473
  const run = Math.max(ANGLED_LEADER_RUN, radius + 4);
7207
- const side = (_o = (_m = placement.textSide) !== null && _m !== void 0 ? _m : frame.leaderSide) !== null && _o !== void 0 ? _o : "right";
7474
+ const side = (_q = (_p = placement.textSide) !== null && _p !== void 0 ? _p : frame.leaderSide) !== null && _q !== void 0 ? _q : "right";
7208
7475
  const beside = side === "left" ? frame.anchorX - run - width : frame.anchorX + run;
7209
7476
  position.x = Math.max(0, Math.min(beside, this.viewer.canvas.clientWidth - width));
7210
7477
  }
7211
7478
  else if (leader && placement.textSide === "left") {
7212
7479
  position.x = Math.max(0, Math.min(frame.anchorX - width, this.viewer.canvas.clientWidth - width));
7213
7480
  }
7214
- const gap = (_q = (_p = this.options.pointerAvoidance) === null || _p === void 0 ? void 0 : _p.gap) !== null && _q !== void 0 ? _q : 12;
7481
+ const gap = (_s = (_r = this.options.pointerAvoidance) === null || _r === void 0 ? void 0 : _r.gap) !== null && _s !== void 0 ? _s : 12;
7215
7482
  const containsPointer = (x, y, margin) => {
7216
7483
  return pointer && pointer.x >= x - margin && pointer.x <= x + width + margin && pointer.y >= y - margin && pointer.y <= y + height + margin;
7217
7484
  };
7218
7485
  const passive = !selectOnClick || !this.visualRegister;
7219
- if (passive && ((_r = this.options.pointerAvoidance) === null || _r === void 0 ? void 0 : _r.enabled) !== false && pointer &&
7486
+ if (passive && ((_t = this.options.pointerAvoidance) === null || _t === void 0 ? void 0 : _t.enabled) !== false && pointer &&
7220
7487
  (containsPointer(position.x, position.y, gap) || containsPointer(position.x + displacement.x, position.y + displacement.y, gap))) {
7221
7488
  if (containsPointer(position.x + displacement.x, position.y + displacement.y, 0)) {
7222
7489
  // Choose against the original box and retain the offset until the pointer leaves both boxes.
@@ -7234,7 +7501,7 @@ var EntityLabel;
7234
7501
  else {
7235
7502
  displacement = { x: 0, y: 0 };
7236
7503
  }
7237
- const nudgeDuration = (_t = (_s = this.options.pointerAvoidance) === null || _s === void 0 ? void 0 : _s.transitionDuration) !== null && _t !== void 0 ? _t : 200;
7504
+ const nudgeDuration = (_v = (_u = this.options.pointerAvoidance) === null || _u === void 0 ? void 0 : _u.transitionDuration) !== null && _v !== void 0 ? _v : 200;
7238
7505
  const animateNudge = () => {
7239
7506
  const progress = now < nudge.start ? 0 : nudgeDuration > 0 ? Math.min(1, (now - nudge.start) / nudgeDuration) : 1;
7240
7507
  const eased = progress * progress * (3 - 2 * progress);
@@ -7250,7 +7517,7 @@ var EntityLabel;
7250
7517
  fromY: nudge.y,
7251
7518
  targetX: displacement.x,
7252
7519
  targetY: displacement.y,
7253
- start: now + ((_v = (_u = this.options.pointerAvoidance) === null || _u === void 0 ? void 0 : _u.delay) !== null && _v !== void 0 ? _v : 180)
7520
+ start: now + ((_x = (_w = this.options.pointerAvoidance) === null || _w === void 0 ? void 0 : _w.delay) !== null && _x !== void 0 ? _x : 180)
7254
7521
  };
7255
7522
  }
7256
7523
  if (animateNudge() < 1 && (nudge.fromX !== nudge.targetX || nudge.fromY !== nudge.targetY)) {
@@ -7261,7 +7528,7 @@ var EntityLabel;
7261
7528
  ele.style.left = `${coordinates.x + position.x * coordinates.scaleX}px`;
7262
7529
  ele.style.top = `${coordinates.y + position.y * coordinates.scaleY}px`;
7263
7530
  for (const clickTarget of Array.from(content.querySelectorAll('[role="button"]'))) {
7264
- 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}`);
7531
+ 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}`);
7265
7532
  }
7266
7533
  if (leader) {
7267
7534
  const nodeRadius = ringAnimation.outer;
@@ -7301,13 +7568,13 @@ var EntityLabel;
7301
7568
  // Keep the stem inside the 126-degree arc, away from antialiased endpoints.
7302
7569
  const arcFraction = 0.35;
7303
7570
  const rotationLimit = Math.PI * arcFraction - Math.PI / 30;
7304
- const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_z = ringAnimation.arcOffset) !== null && _z !== void 0 ? _z : 0);
7571
+ const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_1 = ringAnimation.arcOffset) !== null && _1 !== void 0 ? _1 : 0);
7305
7572
  const targetOpen = cursorMotion.target || (hovered && (ring === null || ring === void 0 ? void 0 : ring.enabled) !== false && targetRadius > 0) ? 1 : 0;
7306
- const elapsed = Math.min(32, Math.max(0, now - ((_0 = ringAnimation.arcTime) !== null && _0 !== void 0 ? _0 : now)));
7573
+ const elapsed = Math.min(32, Math.max(0, now - ((_2 = ringAnimation.arcTime) !== null && _2 !== void 0 ? _2 : now)));
7307
7574
  const blend = duration > 0 ? 1 - Math.exp(-elapsed / (duration / 3)) : 1;
7308
7575
  const approach = (value, target) => Math.abs(value - target) < 0.001 ? target : value + (target - value) * blend;
7309
- ringAnimation.arcOffset = approach((_1 = ringAnimation.arcOffset) !== null && _1 !== void 0 ? _1 : 0, targetOffset);
7310
- ringAnimation.arcOpen = approach((_2 = ringAnimation.arcOpen) !== null && _2 !== void 0 ? _2 : 0, targetOpen);
7576
+ ringAnimation.arcOffset = approach((_3 = ringAnimation.arcOffset) !== null && _3 !== void 0 ? _3 : 0, targetOffset);
7577
+ ringAnimation.arcOpen = approach((_4 = ringAnimation.arcOpen) !== null && _4 !== void 0 ? _4 : 0, targetOpen);
7311
7578
  ringAnimation.arcTime = now;
7312
7579
  if (ringAnimation.arcOffset !== targetOffset || ringAnimation.arcOpen !== targetOpen) {
7313
7580
  this.viewer.scene.requestRender();
@@ -7362,14 +7629,16 @@ var EntityLabel;
7362
7629
  }
7363
7630
  /*
7364
7631
  * Builds one icon glyph from label options, with its background chip when asked for.
7365
- *
7366
7632
  * The returned fit squares the chip off once the glyph has a measurable size.
7367
7633
  */
7368
- buildIcon(options, counter) {
7634
+ buildIcon(options, counter, placePhoto) {
7369
7635
  var _a, _b, _c;
7370
7636
  let fit;
7371
7637
  const icon = document.createElement("span");
7372
7638
  icon.className = "entity-label-icon";
7639
+ if (options.iconPhoto && options.iconUrl) {
7640
+ return this.buildPhoto(icon, options, counter, placePhoto);
7641
+ }
7373
7642
  icon.style.cssText = `font-size:${(options.iconSize || 24) * ((_a = options.iconScale) !== null && _a !== void 0 ? _a : 1)}px;line-height:1;`;
7374
7643
  icon.style.textShadow = "0 1px 2px rgba(0, 0, 0, 0.25)";
7375
7644
  const iconColor = typeof options.iconColor === "string" ? options.iconColor : undefined;
@@ -7422,6 +7691,8 @@ var EntityLabel;
7422
7691
  }
7423
7692
  this.viewer.scene.requestRender();
7424
7693
  };
7694
+ // A photo is rarely square; fit it inside the icon's box rather than stretching it.
7695
+ image.style.objectFit = "contain";
7425
7696
  image.onload = resize;
7426
7697
  resize();
7427
7698
  image.src = options.iconUrl;
@@ -7477,6 +7748,101 @@ var EntityLabel;
7477
7748
  }
7478
7749
  return { icon, fit };
7479
7750
  }
7751
+ /*
7752
+ * 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.
7753
+ */
7754
+ buildPhoto(icon, options, counter, placePhoto) {
7755
+ var _a;
7756
+ const size = (options.iconSize || 24) * ((_a = options.iconScale) !== null && _a !== void 0 ? _a : 1);
7757
+ // The glyph plus the chip's padding of a third either side, enlarged for a photo.
7758
+ const diameter = (size + 2 * (size / 3)) * PHOTO_SIZE_SCALE;
7759
+ // Enough pixels to stay sharp at the hover size.
7760
+ const pixels = Math.ceil(diameter * PHOTO_HOVER_SCALE * (window.devicePixelRatio || 1));
7761
+ const canvas = document.createElement("canvas");
7762
+ canvas.className = "entity-label-photo";
7763
+ 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);";
7764
+ canvas.style.transition = ["width", "height", "opacity"].map(property => `${property} ${PHOTO_TRANSITION}ms ease-out`).join(",");
7765
+ icon.classList.add("entity-label-icon-photo");
7766
+ icon.style.display = options.iconScale === 0 ? "none" : "inline-flex";
7767
+ icon.appendChild(canvas);
7768
+ // Resting size, set once the photo's shape is known.
7769
+ let width = 0;
7770
+ let height = 0;
7771
+ let grown = false;
7772
+ const grow = (on) => {
7773
+ if (on === grown || !width) {
7774
+ return;
7775
+ }
7776
+ grown = on;
7777
+ const scale = on ? PHOTO_HOVER_SCALE : 1;
7778
+ canvas.style.width = `${width * scale}px`;
7779
+ canvas.style.height = `${height * scale}px`;
7780
+ this.renderFor(PHOTO_TRANSITION);
7781
+ };
7782
+ // Draws the photo in, or drops the icon when it would not load so the label stays as its text.
7783
+ const show = (photo) => {
7784
+ var _a;
7785
+ if (!photo.source) {
7786
+ icon.dataset.failed = "true";
7787
+ icon.style.display = "none";
7788
+ return false;
7789
+ }
7790
+ canvas.width = photo.source.width;
7791
+ canvas.height = photo.source.height;
7792
+ (_a = canvas.getContext("2d")) === null || _a === void 0 ? void 0 : _a.drawImage(photo.source, 0, 0);
7793
+ const longest = Math.max(photo.width, photo.height);
7794
+ const circle = PhotoShape(photo.width, photo.height) === "circle";
7795
+ width = circle ? diameter : diameter * photo.width / longest;
7796
+ height = circle ? diameter : diameter * photo.height / longest;
7797
+ canvas.style.width = `${width}px`;
7798
+ canvas.style.height = `${height}px`;
7799
+ canvas.style.borderRadius = circle ? "50%" : `${diameter * PHOTO_CORNER}px`;
7800
+ canvas.style.opacity = "1";
7801
+ placePhoto === null || placePhoto === void 0 ? void 0 : placePhoto(circle ? "circle" : "rect", grow);
7802
+ return true;
7803
+ };
7804
+ const photos = Photos.Get(this.viewer);
7805
+ const ready = photos.Peek(options.iconUrl, pixels);
7806
+ if (ready) {
7807
+ show(ready);
7808
+ return { icon };
7809
+ }
7810
+ canvas.style.width = "0px";
7811
+ canvas.style.height = "0px";
7812
+ canvas.style.opacity = "0";
7813
+ let arrived;
7814
+ const arriving = new Promise(resolve => arrived = resolve);
7815
+ let started = false;
7816
+ const start = () => {
7817
+ if (started) {
7818
+ return;
7819
+ }
7820
+ started = true;
7821
+ photos.Load(options.iconUrl, pixels).then(photo => {
7822
+ if (!this.disposed && this.counter === counter && show(photo)) {
7823
+ this.renderFor(PHOTO_TRANSITION);
7824
+ }
7825
+ arrived();
7826
+ });
7827
+ };
7828
+ return { icon, arriving, start };
7829
+ }
7830
+ /*
7831
+ * Keeps rendering while a CSS transition changes a label's size, since placement measures it every frame.
7832
+ */
7833
+ renderFor(duration) {
7834
+ const end = Date.now() + duration;
7835
+ const step = () => {
7836
+ if (this.disposed || !this.viewer || this.viewer.isDestroyed()) {
7837
+ return;
7838
+ }
7839
+ this.viewer.scene.requestRender();
7840
+ if (Date.now() < end) {
7841
+ requestAnimationFrame(step);
7842
+ }
7843
+ };
7844
+ step();
7845
+ }
7480
7846
  /**
7481
7847
  * Removes label from the viewer and unsubscribes from events.
7482
7848
  */
@@ -7635,34 +8001,40 @@ var EntityLabelGroup;
7635
8001
  ].some(Array.isArray);
7636
8002
  }
7637
8003
  EntityLabelGroup.NeedsEntityData = NeedsEntityData;
7638
- // How many entities to read at once, so a long hand-picked list does not arrive as a burst.
7639
- const HYDRATE_AT_ONCE = 8;
8004
+ // How many entities one read asks for, so a long hand-picked list does not arrive as one request.
8005
+ const HYDRATE_AT_ONCE = 100;
7640
8006
  /*
7641
- * Reads the entities behind items that name one without carrying it.
8007
+ * Reads the entities behind items that name one without carrying it, attachments included,
8008
+ * so a label's default image needs no second request.
7642
8009
  */
7643
8010
  async function Hydrate(items, api, signal) {
8011
+ var _a, _b;
7644
8012
  if (!api) {
7645
8013
  return items;
7646
8014
  }
7647
8015
  const hydrated = items.slice();
7648
- for (let start = 0; start < hydrated.length; start += HYDRATE_AT_ONCE) {
8016
+ const missing = hydrated.map((item, at) => (item === null || item === void 0 ? void 0 : item.entityId) && !item.entity ? at : -1).filter(at => at >= 0);
8017
+ for (let start = 0; start < missing.length; start += HYDRATE_AT_ONCE) {
7649
8018
  if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
7650
8019
  return hydrated;
7651
8020
  }
7652
- await Promise.all(hydrated.slice(start, start + HYDRATE_AT_ONCE).map(async (item, at) => {
7653
- if (!(item === null || item === void 0 ? void 0 : item.entityId) || item.entity) {
7654
- return;
7655
- }
7656
- try {
7657
- const { entity } = await Entity$1.Get({ api, entityId: item.entityId, migrated: true });
8021
+ const chunk = missing.slice(start, start + HYDRATE_AT_ONCE);
8022
+ try {
8023
+ const { entities } = await Entity$1.GetListByIds({
8024
+ api, entityIds: chunk.map(at => hydrated[at].entityId), expandAttachments: true, migrated: true
8025
+ });
8026
+ 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]; }));
8027
+ for (const at of chunk) {
8028
+ const entity = found.get(hydrated[at].entityId);
7658
8029
  if (entity) {
7659
- hydrated[start + at] = { ...item, entity };
8030
+ // The API leaves Attachment out when there are none; an empty list records that they were read.
8031
+ 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 : [] } } };
7660
8032
  }
7661
8033
  }
7662
- catch {
7663
- // Rip.
7664
- }
7665
- }));
8034
+ }
8035
+ catch {
8036
+ // Entities that cannot be read keep their labels, just without attribute data.
8037
+ }
7666
8038
  }
7667
8039
  return hydrated;
7668
8040
  }
@@ -7749,6 +8121,10 @@ var EntityLabelGroup;
7749
8121
  return null;
7750
8122
  }
7751
8123
  }
8124
+ // How many entities one attachment read asks for.
8125
+ const IMAGES_AT_ONCE = 100;
8126
+ // How long on-screen requests gather before they are read together.
8127
+ const IMAGES_GATHER = 60;
7752
8128
  /*
7753
8129
  * Owns membership independently of manual labelled states, with atomic replacement and stale-load cancellation.
7754
8130
  */
@@ -7764,6 +8140,12 @@ var EntityLabelGroup;
7764
8140
  this.definitions = new Map();
7765
8141
  this.visibility = new Map();
7766
8142
  this.errors = new Map();
8143
+ // Default image per entity, null once read and found to have none. Shared by every group.
8144
+ this.images = new Map();
8145
+ // Members whose labels came on screen and are waiting on, or being read for, their default image.
8146
+ this.imagesQueued = new Set();
8147
+ this.imagesAsking = new Set();
8148
+ this.imagesTimer = null;
7767
8149
  }
7768
8150
  notify() {
7769
8151
  try {
@@ -7909,7 +8291,7 @@ var EntityLabelGroup;
7909
8291
  return false;
7910
8292
  }
7911
8293
  Resolve(entityId, menuItemId) {
7912
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
8294
+ var _a, _b, _c, _d, _e;
7913
8295
  const ids = Array.from(this.members.get(entityId) || []).reverse();
7914
8296
  for (const id of ids) {
7915
8297
  const group = this.active.get(id);
@@ -7919,61 +8301,145 @@ var EntityLabelGroup;
7919
8301
  if (group.definition.menuItemId && group.definition.menuItemId !== menuItemId) {
7920
8302
  continue;
7921
8303
  }
8304
+ const options = this.compose(group, entityId, menuItemId);
7922
8305
  const item = group.items.get(entityId);
7923
- const entity = (_a = item.entity) !== null && _a !== void 0 ? _a : { Bruce: { ID: entityId, Name: item.text } };
7924
- let iconPriority = (_b = group.definition.options) === null || _b === void 0 ? void 0 : _b.iconPriority;
7925
- if (Array.isArray(iconPriority)) {
7926
- try {
7927
- iconPriority = Calculator.GetNumber(iconPriority, entity);
8306
+ 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) ? EntityLabel.GetDefaultImageUrl(item.entity, (_d = this.register.ApiGetters) === null || _d === void 0 ? void 0 : _d.GetBruceApi()) : null);
8307
+ if (image && !EntityLabel.HasIcon(options)) {
8308
+ (_e = options.photo) !== null && _e !== void 0 ? _e : (options.photo = image);
8309
+ }
8310
+ return { groupId: id, options, item };
8311
+ }
8312
+ return null;
8313
+ }
8314
+ /*
8315
+ * One member's label settings, with any image its own text links to but before its default image.
8316
+ */
8317
+ compose(group, entityId, menuItemId) {
8318
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
8319
+ const item = group.items.get(entityId);
8320
+ const entity = (_a = item.entity) !== null && _a !== void 0 ? _a : { Bruce: { ID: entityId, Name: item.text } };
8321
+ let iconPriority = (_b = group.definition.options) === null || _b === void 0 ? void 0 : _b.iconPriority;
8322
+ if (Array.isArray(iconPriority)) {
8323
+ try {
8324
+ iconPriority = Calculator.GetNumber(iconPriority, entity);
8325
+ }
8326
+ catch {
8327
+ iconPriority = null;
8328
+ }
8329
+ }
8330
+ let iconScale = (_c = group.definition.options) === null || _c === void 0 ? void 0 : _c.iconScale;
8331
+ if (Array.isArray(iconScale)) {
8332
+ try {
8333
+ iconScale = Calculator.GetNumber(iconScale, entity);
8334
+ }
8335
+ catch {
8336
+ iconScale = null;
8337
+ }
8338
+ if (!Number.isFinite(iconScale)) {
8339
+ iconScale = 1;
8340
+ }
8341
+ }
8342
+ if (iconScale != null) {
8343
+ iconScale = Math.max(0, iconScale);
8344
+ }
8345
+ const textSetting = (_d = group.definition.options) === null || _d === void 0 ? void 0 : _d.text;
8346
+ let resolvedText = resolveIcon(textSetting, entity);
8347
+ const expression = Array.isArray(textSetting);
8348
+ if (expression && !(resolvedText === null || resolvedText === void 0 ? void 0 : resolvedText.trim())) {
8349
+ resolvedText = undefined;
8350
+ }
8351
+ 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]
8352
+ .find(text => typeof text === "string" && text.trim().length > 0);
8353
+ const options = {
8354
+ ...group.definition.options,
8355
+ text: resolvedText !== null && resolvedText !== void 0 ? resolvedText : fallbackText,
8356
+ iconScale: iconScale,
8357
+ icon: resolveIcon((_g = group.definition.options) === null || _g === void 0 ? void 0 : _g.icon, entity),
8358
+ iconUrl: resolveIcon((_h = group.definition.options) === null || _h === void 0 ? void 0 : _h.iconUrl, entity),
8359
+ iconClass: resolveIcon((_j = group.definition.options) === null || _j === void 0 ? void 0 : _j.iconClass, entity),
8360
+ iconColor: resolveIcon((_k = group.definition.options) === null || _k === void 0 ? void 0 : _k.iconColor, entity),
8361
+ iconBackground: (_l = group.definition.options) === null || _l === void 0 ? void 0 : _l.iconBackground,
8362
+ iconPriority: Number.isFinite(iconPriority) ? iconPriority : item.iconPriority,
8363
+ iconStatus: (_o = resolveIcon((_m = group.definition.options) === null || _m === void 0 ? void 0 : _m.iconStatus, entity)) !== null && _o !== void 0 ? _o : item.iconStatus
8364
+ };
8365
+ if (options.attributes && item.entity && !(expression && resolvedText != null)) {
8366
+ options.text = [
8367
+ (_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,
8368
+ ...options.attributes.map(path => Entity$1.GetValue({ entity: item.entity, path })).filter(value => value != null).map(String)
8369
+ ].join("\n");
8370
+ }
8371
+ if (!EntityLabel.HasIcon(options) && typeof options.text === "string") {
8372
+ const lines = options.text.split("\n");
8373
+ const at = lines.findIndex(line => EntityLabel.IsImageUrl(line));
8374
+ if (at >= 0) {
8375
+ const [url] = lines.splice(at, 1);
8376
+ options.text = lines.join("\n");
8377
+ options.photo = url.trim();
8378
+ }
8379
+ }
8380
+ return options;
8381
+ }
8382
+ /*
8383
+ * Queues a member's default image once its label is on screen, so members out of view never cost a read.
8384
+ * Requests arriving close together are read as one.
8385
+ */
8386
+ RequestImage(entityId) {
8387
+ var _a, _b, _c, _d, _e, _f;
8388
+ const api = (_a = this.register.ApiGetters) === null || _a === void 0 ? void 0 : _a.GetBruceApi();
8389
+ if (!api || !entityId || this.images.has(entityId) || this.imagesAsking.has(entityId)) {
8390
+ return;
8391
+ }
8392
+ // Members read with their attachments already know their image, or that they have none.
8393
+ for (const id of this.members.get(entityId) || []) {
8394
+ 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)) {
8395
+ return;
8396
+ }
8397
+ }
8398
+ if (!this.members.has(entityId)) {
8399
+ return;
8400
+ }
8401
+ this.imagesAsking.add(entityId);
8402
+ this.imagesQueued.add(entityId);
8403
+ (_f = this.imagesTimer) !== null && _f !== void 0 ? _f : (this.imagesTimer = setTimeout(() => {
8404
+ this.imagesTimer = null;
8405
+ this.readImages(api).catch(error => console.error("Label images could not be loaded", error));
8406
+ }, IMAGES_GATHER));
8407
+ }
8408
+ async readImages(api) {
8409
+ var _a, _b;
8410
+ const ids = Array.from(this.imagesQueued);
8411
+ this.imagesQueued.clear();
8412
+ for (let start = 0; start < ids.length; start += IMAGES_AT_ONCE) {
8413
+ const chunk = ids.slice(start, start + IMAGES_AT_ONCE);
8414
+ let entities;
8415
+ try {
8416
+ entities = (_a = (await Entity$1.GetListByIds({ api, entityIds: chunk, expandAttachments: true, migrated: true })).entities) !== null && _a !== void 0 ? _a : [];
8417
+ }
8418
+ catch {
8419
+ // Left unread, so the next label to come on screen for these members asks again.
8420
+ entities = null;
8421
+ }
8422
+ if (this.register.Disposed) {
8423
+ return;
8424
+ }
8425
+ 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, EntityLabel.GetDefaultImageUrl(entity, api)]; }));
8426
+ const gained = [];
8427
+ for (const id of chunk) {
8428
+ if (!this.imagesAsking.delete(id) || !entities) {
8429
+ continue;
7928
8430
  }
7929
- catch {
7930
- iconPriority = null;
8431
+ const url = (_b = found.get(id)) !== null && _b !== void 0 ? _b : null;
8432
+ this.images.set(id, url);
8433
+ if (url) {
8434
+ gained.push(id);
7931
8435
  }
7932
8436
  }
7933
- let iconScale = (_c = group.definition.options) === null || _c === void 0 ? void 0 : _c.iconScale;
7934
- if (Array.isArray(iconScale)) {
7935
- try {
7936
- iconScale = Calculator.GetNumber(iconScale, entity);
7937
- }
7938
- catch {
7939
- iconScale = null;
7940
- }
7941
- if (!Number.isFinite(iconScale)) {
7942
- iconScale = 1;
7943
- }
7944
- }
7945
- if (iconScale != null) {
7946
- iconScale = Math.max(0, iconScale);
7947
- }
7948
- const textSetting = (_d = group.definition.options) === null || _d === void 0 ? void 0 : _d.text;
7949
- let resolvedText = resolveIcon(textSetting, entity);
7950
- const expression = Array.isArray(textSetting);
7951
- if (expression && !(resolvedText === null || resolvedText === void 0 ? void 0 : resolvedText.trim())) {
7952
- resolvedText = undefined;
7953
- }
7954
- 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]
7955
- .find(text => typeof text === "string" && text.trim().length > 0);
7956
- const options = {
7957
- ...group.definition.options,
7958
- text: resolvedText !== null && resolvedText !== void 0 ? resolvedText : fallbackText,
7959
- iconScale: iconScale,
7960
- icon: resolveIcon((_g = group.definition.options) === null || _g === void 0 ? void 0 : _g.icon, entity),
7961
- iconUrl: resolveIcon((_h = group.definition.options) === null || _h === void 0 ? void 0 : _h.iconUrl, entity),
7962
- iconClass: resolveIcon((_j = group.definition.options) === null || _j === void 0 ? void 0 : _j.iconClass, entity),
7963
- iconColor: resolveIcon((_k = group.definition.options) === null || _k === void 0 ? void 0 : _k.iconColor, entity),
7964
- iconBackground: (_l = group.definition.options) === null || _l === void 0 ? void 0 : _l.iconBackground,
7965
- iconPriority: Number.isFinite(iconPriority) ? iconPriority : item.iconPriority,
7966
- iconStatus: (_o = resolveIcon((_m = group.definition.options) === null || _m === void 0 ? void 0 : _m.iconStatus, entity)) !== null && _o !== void 0 ? _o : item.iconStatus
7967
- };
7968
- if (options.attributes && item.entity && !(expression && resolvedText != null)) {
7969
- options.text = [
7970
- (_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,
7971
- ...options.attributes.map(path => Entity$1.GetValue({ entity: item.entity, path })).filter(value => value != null).map(String)
7972
- ].join("\n");
8437
+ // Reconfigured in place rather than through refresh, which would drop a standalone label and redraw it.
8438
+ if (gained.length) {
8439
+ this.register.ForceUpdate({ entityIds: gained });
8440
+ this.register.Viewer.scene.requestRender();
7973
8441
  }
7974
- return { groupId: id, options, item };
7975
8442
  }
7976
- return null;
7977
8443
  }
7978
8444
  RefreshEntity(entityId) {
7979
8445
  var _a, _b, _c, _d, _e, _f, _g;
@@ -8031,6 +8497,11 @@ var EntityLabelGroup;
8031
8497
  }
8032
8498
  this.unindex(id);
8033
8499
  this.active.delete(id);
8500
+ for (const entityId of group.items.keys()) {
8501
+ if (!this.members.has(entityId)) {
8502
+ this.images.delete(entityId);
8503
+ }
8504
+ }
8034
8505
  this.refresh(new Set(group.items.keys()));
8035
8506
  this.notify();
8036
8507
  }
@@ -8039,6 +8510,7 @@ var EntityLabelGroup;
8039
8510
  controller.abort();
8040
8511
  }
8041
8512
  this.pending.clear();
8513
+ this.clearImages();
8042
8514
  const affected = new Set(this.members.keys());
8043
8515
  this.active.clear();
8044
8516
  this.members.clear();
@@ -8055,6 +8527,7 @@ var EntityLabelGroup;
8055
8527
  for (const label of this.standalone.values()) {
8056
8528
  label.Dispose();
8057
8529
  }
8530
+ this.clearImages();
8058
8531
  this.pending.clear();
8059
8532
  this.standalone.clear();
8060
8533
  this.standaloneItems.clear();
@@ -8064,6 +8537,13 @@ var EntityLabelGroup;
8064
8537
  this.visibility.clear();
8065
8538
  this.errors.clear();
8066
8539
  }
8540
+ clearImages() {
8541
+ clearTimeout(this.imagesTimer);
8542
+ this.imagesTimer = null;
8543
+ this.imagesQueued.clear();
8544
+ this.imagesAsking.clear();
8545
+ this.images.clear();
8546
+ }
8067
8547
  unindex(groupId) {
8068
8548
  var _a;
8069
8549
  for (const id of ((_a = this.active.get(groupId)) === null || _a === void 0 ? void 0 : _a.items.keys()) || []) {
@@ -12561,6 +13041,13 @@ var VisualsRegister;
12561
13041
  var _a;
12562
13042
  return (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.Resolve(entityId, menuItemId);
12563
13043
  }
13044
+ /*
13045
+ * Called by a group label once it is on screen, so its entity's default image is read only when it can be seen.
13046
+ */
13047
+ RequestLabelImage(entityId) {
13048
+ var _a;
13049
+ (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.RequestImage(entityId);
13050
+ }
12564
13051
  RefreshLabelGroupEntity(entityId) {
12565
13052
  var _a, _b;
12566
13053
  (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.RefreshEntity(entityId);
@@ -27322,6 +27809,159 @@ var TilesetI3sRenderManager;
27322
27809
  TilesetI3sRenderManager.Manager = Manager;
27323
27810
  })(TilesetI3sRenderManager || (TilesetI3sRenderManager = {}));
27324
27811
 
27812
+ /**
27813
+ * Remaps a (assumed grayscale, server-rendered black-to-white) image's pixels into a gradient between minColor/maxColor,
27814
+ * preserving source alpha blended with the mask's own alpha.
27815
+ * @param imageData
27816
+ * @param minColor
27817
+ * @param maxColor
27818
+ */
27819
+ function ApplyGrayscaleColorMask(imageData, minColor, maxColor) {
27820
+ const data = imageData.data;
27821
+ for (let i = 0; i < data.length; i += 4) {
27822
+ // The texture is rendered black-to-white server-side, so any channel carries the value.
27823
+ const t = data[i] / 255;
27824
+ data[i] = Math.round(minColor.red + (maxColor.red - minColor.red) * t);
27825
+ data[i + 1] = Math.round(minColor.green + (maxColor.green - minColor.green) * t);
27826
+ data[i + 2] = Math.round(minColor.blue + (maxColor.blue - minColor.blue) * t);
27827
+ // Preserve source transparency (eg unrasterized pixels) blended with the mask's own alpha.
27828
+ const srcAlpha = data[i + 3] / 255;
27829
+ const maskAlpha = minColor.alpha + (maxColor.alpha - minColor.alpha) * t;
27830
+ data[i + 3] = Math.round(srcAlpha * maskAlpha * 255);
27831
+ }
27832
+ }
27833
+ /**
27834
+ * The colour a ramp gives a normalised value.
27835
+ * @param sorted stops in ascending position, normalised into the texture's value range
27836
+ */
27837
+ function SampleRamp(sorted, t) {
27838
+ let upper = 0;
27839
+ while (upper < sorted.length && sorted[upper].position < t) {
27840
+ upper++;
27841
+ }
27842
+ if (upper === 0) {
27843
+ return sorted[0].color;
27844
+ }
27845
+ if (upper >= sorted.length) {
27846
+ return sorted[sorted.length - 1].color;
27847
+ }
27848
+ const a = sorted[upper - 1];
27849
+ const b = sorted[upper];
27850
+ const span = b.position - a.position;
27851
+ // Equal positions are a hard step rather than a division by zero, which is how a band
27852
+ // gets a crisp edge instead of a gradient into its neighbour.
27853
+ const f = span > 0 ? (t - a.position) / span : 1;
27854
+ return {
27855
+ red: a.color.red + (b.color.red - a.color.red) * f,
27856
+ green: a.color.green + (b.color.green - a.color.green) * f,
27857
+ blue: a.color.blue + (b.color.blue - a.color.blue) * f,
27858
+ alpha: a.color.alpha + (b.color.alpha - a.color.alpha) * f
27859
+ };
27860
+ }
27861
+ /**
27862
+ * A ramp baked into RGBA bytes, for a shader that cannot walk stops per fragment.
27863
+ * @param steps entries in the lookup, each covering an equal slice of the value range
27864
+ */
27865
+ function RampLookup(stops, low, high, steps = 256) {
27866
+ const sorted = stops && stops.length > 0
27867
+ ? stops.slice().sort((a, b) => a.position - b.position)
27868
+ : null;
27869
+ const out = new Uint8Array(steps * 4);
27870
+ for (let i = 0; i < steps; i++) {
27871
+ const t = steps > 1 ? i / (steps - 1) : 0;
27872
+ const color = sorted
27873
+ ? SampleRamp(sorted, t)
27874
+ : {
27875
+ red: low.red + (high.red - low.red) * t,
27876
+ green: low.green + (high.green - low.green) * t,
27877
+ blue: low.blue + (high.blue - low.blue) * t,
27878
+ alpha: low.alpha + (high.alpha - low.alpha) * t
27879
+ };
27880
+ out[i * 4] = Math.round(color.red);
27881
+ out[i * 4 + 1] = Math.round(color.green);
27882
+ out[i * 4 + 2] = Math.round(color.blue);
27883
+ out[i * 4 + 3] = Math.round(Math.min(1, Math.max(0, color.alpha)) * 255);
27884
+ }
27885
+ return out;
27886
+ }
27887
+ function ApplyGradientStops(imageData, stops) {
27888
+ if (!stops || stops.length === 0) {
27889
+ return;
27890
+ }
27891
+ const sorted = stops.slice().sort((a, b) => a.position - b.position);
27892
+ const data = imageData.data;
27893
+ for (let i = 0; i < data.length; i += 4) {
27894
+ const t = data[i] / 255;
27895
+ const color = SampleRamp(sorted, t);
27896
+ data[i] = Math.round(color.red);
27897
+ data[i + 1] = Math.round(color.green);
27898
+ data[i + 2] = Math.round(color.blue);
27899
+ const srcAlpha = data[i + 3] / 255;
27900
+ data[i + 3] = Math.round(srcAlpha * color.alpha * 255);
27901
+ }
27902
+ }
27903
+ /**
27904
+ * Draws the texture's own cell grid into the tinted pixels.
27905
+ *
27906
+ * Drawn in texture space rather than screen space so it stays with the cells it describes and needs
27907
+ * no per-frame work as the camera moves.
27908
+ */
27909
+ function ApplyCellBorders(imageData, cellTexels, widthTexels, color) {
27910
+ if (!(cellTexels > 0) || !(widthTexels > 0) || !color || color.alpha <= 0) {
27911
+ return;
27912
+ }
27913
+ const { width, height, data } = imageData;
27914
+ const edge = Math.max(1, Math.round(widthTexels));
27915
+ for (let y = 0; y < height; y++) {
27916
+ const onRow = (y % cellTexels) < edge;
27917
+ for (let x = 0; x < width; x++) {
27918
+ if (!onRow && (x % cellTexels) >= edge) {
27919
+ continue;
27920
+ }
27921
+ const i = (y * width + x) * 4;
27922
+ // Only where something is already drawn, so a border cannot invent coverage the data
27923
+ // does not have.
27924
+ if (data[i + 3] === 0) {
27925
+ continue;
27926
+ }
27927
+ const a = color.alpha;
27928
+ data[i] = Math.round(data[i] * (1 - a) + color.red * a);
27929
+ data[i + 1] = Math.round(data[i + 1] * (1 - a) + color.green * a);
27930
+ data[i + 2] = Math.round(data[i + 2] * (1 - a) + color.blue * a);
27931
+ }
27932
+ }
27933
+ }
27934
+ function loadImage(src) {
27935
+ return new Promise((res, rej) => {
27936
+ const image = new Image();
27937
+ image.onload = () => res(image);
27938
+ image.onerror = (e) => rej(e);
27939
+ image.src = src;
27940
+ });
27941
+ }
27942
+ /*
27943
+ * Loads an image whose pixels will be read back or uploaded to WebGL, which needs a CORS-clean copy.
27944
+ */
27945
+ async function loadImageForCanvas(url) {
27946
+ let response;
27947
+ try {
27948
+ response = await fetch(url);
27949
+ }
27950
+ catch {
27951
+ response = await fetch(url, { cache: "reload" });
27952
+ }
27953
+ if (!response.ok) {
27954
+ throw new Error(`Image request failed with ${response.status}: ${url}`);
27955
+ }
27956
+ const objectUrl = URL.createObjectURL(await response.blob());
27957
+ try {
27958
+ return await loadImage(objectUrl);
27959
+ }
27960
+ finally {
27961
+ URL.revokeObjectURL(objectUrl);
27962
+ }
27963
+ }
27964
+
27325
27965
  var GoogleSearchRenderManager;
27326
27966
  (function (GoogleSearchRenderManager) {
27327
27967
  class Manager {
@@ -27628,45 +28268,39 @@ async function createBillboardImage(url, bgColor, size) {
27628
28268
  return cacheData;
27629
28269
  }
27630
28270
  const IMAGE_PADDING = 8;
27631
- const prom = new Promise(async (res, rej) => {
27632
- try {
27633
- const response = await fetch(url);
27634
- const blob = await response.blob();
27635
- const image = new Image();
27636
- image.onload = () => {
27637
- const canvas = document.createElement("canvas");
27638
- canvas.width = size;
27639
- canvas.height = size;
27640
- const context = canvas.getContext("2d");
27641
- // Calculate padding and available space.
27642
- const availableSize = size - 2 * IMAGE_PADDING;
27643
- // Calculate resized dimensions preserving aspect ratio.
27644
- const { newWidth, newHeight } = calculateBillboardSpace(image.width, image.height, availableSize, availableSize);
27645
- // Draw the circular background.
27646
- context.beginPath();
27647
- context.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI, false);
27648
- context.fillStyle = bgColor;
27649
- context.fill();
27650
- // Draw the image centered within the circle.
27651
- const offsetX = (size - newWidth) / 2;
27652
- const offsetY = (size - newHeight) / 2;
27653
- context.drawImage(image, offsetX, offsetY, newWidth, newHeight);
27654
- // Draw a 2px darkened border around the circle.
27655
- context.beginPath();
27656
- context.arc(size / 2, size / 2, size / 2 - 1, 0, 2 * Math.PI, false);
27657
- context.lineWidth = 1;
27658
- context.strokeStyle = "rgba(0, 0, 0, 0.3)";
27659
- context.stroke();
27660
- res(canvas);
27661
- };
27662
- image.onerror = (e) => rej(e);
27663
- image.src = URL.createObjectURL(blob);
27664
- }
27665
- catch (e) {
27666
- rej(e);
27667
- }
28271
+ const prom = loadImageForCanvas(url).then(image => {
28272
+ const canvas = document.createElement("canvas");
28273
+ canvas.width = size;
28274
+ canvas.height = size;
28275
+ const context = canvas.getContext("2d");
28276
+ // Calculate padding and available space.
28277
+ const availableSize = size - 2 * IMAGE_PADDING;
28278
+ // Calculate resized dimensions preserving aspect ratio.
28279
+ const { newWidth, newHeight } = calculateBillboardSpace(image.width, image.height, availableSize, availableSize);
28280
+ // Draw the circular background.
28281
+ context.beginPath();
28282
+ context.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI, false);
28283
+ context.fillStyle = bgColor;
28284
+ context.fill();
28285
+ // Draw the image centered within the circle.
28286
+ const offsetX = (size - newWidth) / 2;
28287
+ const offsetY = (size - newHeight) / 2;
28288
+ context.drawImage(image, offsetX, offsetY, newWidth, newHeight);
28289
+ // Draw a 2px darkened border around the circle.
28290
+ context.beginPath();
28291
+ context.arc(size / 2, size / 2, size / 2 - 1, 0, 2 * Math.PI, false);
28292
+ context.lineWidth = 1;
28293
+ context.strokeStyle = "rgba(0, 0, 0, 0.3)";
28294
+ context.stroke();
28295
+ return canvas;
27668
28296
  });
27669
28297
  _billboardCache.Set(key, prom);
28298
+ // A failure is not kept, so the icon is tried again rather than staying missing for the session.
28299
+ prom.catch(() => {
28300
+ if (_billboardCache.Get(key) === prom) {
28301
+ _billboardCache.Delete(key);
28302
+ }
28303
+ });
27670
28304
  return prom;
27671
28305
  }
27672
28306
 
@@ -43547,33 +44181,23 @@ const createImageBillboard = (url) => {
43547
44181
  if (cacheData) {
43548
44182
  return cacheData;
43549
44183
  }
43550
- const prom = new Promise(async (res, rej) => {
43551
- try {
43552
- const response = await fetch(url);
43553
- const blob = await response.blob();
43554
- const canvas = document.createElement("canvas");
43555
- const image = new Image();
43556
- image.onload = () => {
43557
- canvas.width = image.width;
43558
- canvas.height = image.height;
43559
- const context = canvas.getContext("2d");
43560
- context.drawImage(image, 0, 0);
43561
- const data = {
43562
- canvasDataUri: canvas.toDataURL("image/png"),
43563
- height: image.height
43564
- };
43565
- res(data);
43566
- };
43567
- image.onerror = (e) => {
43568
- rej(e);
43569
- };
43570
- image.src = URL.createObjectURL(blob);
43571
- }
43572
- catch (e) {
43573
- rej(e);
43574
- }
44184
+ const prom = loadImageForCanvas(url).then((image) => {
44185
+ const canvas = document.createElement("canvas");
44186
+ canvas.width = image.width;
44187
+ canvas.height = image.height;
44188
+ canvas.getContext("2d").drawImage(image, 0, 0);
44189
+ return {
44190
+ canvasDataUri: canvas.toDataURL("image/png"),
44191
+ height: image.height
44192
+ };
43575
44193
  });
43576
44194
  _billboardCache$1.Set(cacheKey, prom);
44195
+ // A failure is not kept, so the icon is tried again rather than staying missing for the session.
44196
+ prom.catch(() => {
44197
+ if (_billboardCache$1.Get(cacheKey) === prom) {
44198
+ _billboardCache$1.Delete(cacheKey);
44199
+ }
44200
+ });
43577
44201
  return prom;
43578
44202
  };
43579
44203
  /**
@@ -43581,7 +44205,7 @@ const createImageBillboard = (url) => {
43581
44205
  * @param url eg: https://demoplantprocess.api.nextspace-uat.net/file/4c29bdf8-1f0b-4739-b0ee-421ea07ba4e5.png
43582
44206
  */
43583
44207
  function extractMetadataFromFileUrl(url) {
43584
- var _a;
44208
+ var _a, _b;
43585
44209
  const lowercased = url.toLowerCase();
43586
44210
  // Bzzt, invalid url format for our purposes.
43587
44211
  if (!lowercased.includes(".api.") || !lowercased.includes("/file/")) {
@@ -43602,13 +44226,8 @@ function extractMetadataFromFileUrl(url) {
43602
44226
  const host = parts[0];
43603
44227
  const hostParts = host.split(".");
43604
44228
  const accountId = hostParts[0];
43605
- let fileId = (_a = parts[2]) !== null && _a !== void 0 ? _a : "";
43606
- if (fileId.includes("?")) {
43607
- fileId = fileId.split("?")[0];
43608
- }
43609
- if (fileId.includes(".")) {
43610
- fileId = fileId.split(".")[0];
43611
- }
44229
+ // Follows the "/file/" route rather than a fixed position, since "RV:" and "S:" segments can come before it.
44230
+ const fileId = (_b = (_a = url.match(/\/file\/([^/?#.]+)/)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : "";
43612
44231
  if (!fileId || !accountId) {
43613
44232
  return null;
43614
44233
  }
@@ -43973,7 +44592,8 @@ var EntityRenderEngineModel3d;
43973
44592
  updateShouldShowTrack();
43974
44593
  prepareExistingGraphic(cEntity, shouldShowTrack ? 1 : 0);
43975
44594
  const currentUri = GetCValue(params.viewer, cEntity.model.uri);
43976
- if (currentUri != params.lodUrl) {
44595
+ // Signature-blind, so a model is not reloaded just because the API signed its URL again.
44596
+ if (UrlUtils.RemoveSignature(currentUri) != UrlUtils.RemoveSignature(params.lodUrl)) {
43977
44597
  cEntity.model.uri = new ConstantProperty(params.lodUrl);
43978
44598
  }
43979
44599
  cEntity.model.heightReference = new ConstantProperty(heightRef);
@@ -44170,7 +44790,7 @@ var EntityRenderEngineModel3d;
44170
44790
  });
44171
44791
  cEntity.show = true;
44172
44792
  }
44173
- const fileRadiusKey = `model3d_${params.lodUrl}_${scale * styleScale}_radius`;
44793
+ const fileRadiusKey = `model3d_${UrlUtils.RemoveSignature(params.lodUrl)}_${scale * styleScale}_radius`;
44174
44794
  let heightProm = _fileRadiusCache.Get(fileRadiusKey);
44175
44795
  if (!heightProm) {
44176
44796
  heightProm = new Promise(async (res) => {
@@ -45033,137 +45653,6 @@ function getZIndex(style, entity, tags) {
45033
45653
  return EnsureNumber(zIndex);
45034
45654
  }
45035
45655
 
45036
- /**
45037
- * Remaps a (assumed grayscale, server-rendered black-to-white) image's pixels into a gradient between minColor/maxColor,
45038
- * preserving source alpha blended with the mask's own alpha.
45039
- * @param imageData
45040
- * @param minColor
45041
- * @param maxColor
45042
- */
45043
- function ApplyGrayscaleColorMask(imageData, minColor, maxColor) {
45044
- const data = imageData.data;
45045
- for (let i = 0; i < data.length; i += 4) {
45046
- // The texture is rendered black-to-white server-side, so any channel carries the value.
45047
- const t = data[i] / 255;
45048
- data[i] = Math.round(minColor.red + (maxColor.red - minColor.red) * t);
45049
- data[i + 1] = Math.round(minColor.green + (maxColor.green - minColor.green) * t);
45050
- data[i + 2] = Math.round(minColor.blue + (maxColor.blue - minColor.blue) * t);
45051
- // Preserve source transparency (eg unrasterized pixels) blended with the mask's own alpha.
45052
- const srcAlpha = data[i + 3] / 255;
45053
- const maskAlpha = minColor.alpha + (maxColor.alpha - minColor.alpha) * t;
45054
- data[i + 3] = Math.round(srcAlpha * maskAlpha * 255);
45055
- }
45056
- }
45057
- /**
45058
- * The colour a ramp gives a normalised value.
45059
- * @param sorted stops in ascending position, normalised into the texture's value range
45060
- */
45061
- function SampleRamp(sorted, t) {
45062
- let upper = 0;
45063
- while (upper < sorted.length && sorted[upper].position < t) {
45064
- upper++;
45065
- }
45066
- if (upper === 0) {
45067
- return sorted[0].color;
45068
- }
45069
- if (upper >= sorted.length) {
45070
- return sorted[sorted.length - 1].color;
45071
- }
45072
- const a = sorted[upper - 1];
45073
- const b = sorted[upper];
45074
- const span = b.position - a.position;
45075
- // Equal positions are a hard step rather than a division by zero, which is how a band
45076
- // gets a crisp edge instead of a gradient into its neighbour.
45077
- const f = span > 0 ? (t - a.position) / span : 1;
45078
- return {
45079
- red: a.color.red + (b.color.red - a.color.red) * f,
45080
- green: a.color.green + (b.color.green - a.color.green) * f,
45081
- blue: a.color.blue + (b.color.blue - a.color.blue) * f,
45082
- alpha: a.color.alpha + (b.color.alpha - a.color.alpha) * f
45083
- };
45084
- }
45085
- /**
45086
- * A ramp baked into RGBA bytes, for a shader that cannot walk stops per fragment.
45087
- * @param steps entries in the lookup, each covering an equal slice of the value range
45088
- */
45089
- function RampLookup(stops, low, high, steps = 256) {
45090
- const sorted = stops && stops.length > 0
45091
- ? stops.slice().sort((a, b) => a.position - b.position)
45092
- : null;
45093
- const out = new Uint8Array(steps * 4);
45094
- for (let i = 0; i < steps; i++) {
45095
- const t = steps > 1 ? i / (steps - 1) : 0;
45096
- const color = sorted
45097
- ? SampleRamp(sorted, t)
45098
- : {
45099
- red: low.red + (high.red - low.red) * t,
45100
- green: low.green + (high.green - low.green) * t,
45101
- blue: low.blue + (high.blue - low.blue) * t,
45102
- alpha: low.alpha + (high.alpha - low.alpha) * t
45103
- };
45104
- out[i * 4] = Math.round(color.red);
45105
- out[i * 4 + 1] = Math.round(color.green);
45106
- out[i * 4 + 2] = Math.round(color.blue);
45107
- out[i * 4 + 3] = Math.round(Math.min(1, Math.max(0, color.alpha)) * 255);
45108
- }
45109
- return out;
45110
- }
45111
- function ApplyGradientStops(imageData, stops) {
45112
- if (!stops || stops.length === 0) {
45113
- return;
45114
- }
45115
- const sorted = stops.slice().sort((a, b) => a.position - b.position);
45116
- const data = imageData.data;
45117
- for (let i = 0; i < data.length; i += 4) {
45118
- const t = data[i] / 255;
45119
- const color = SampleRamp(sorted, t);
45120
- data[i] = Math.round(color.red);
45121
- data[i + 1] = Math.round(color.green);
45122
- data[i + 2] = Math.round(color.blue);
45123
- const srcAlpha = data[i + 3] / 255;
45124
- data[i + 3] = Math.round(srcAlpha * color.alpha * 255);
45125
- }
45126
- }
45127
- /**
45128
- * Draws the texture's own cell grid into the tinted pixels.
45129
- *
45130
- * Drawn in texture space rather than screen space so it stays with the cells it describes and needs
45131
- * no per-frame work as the camera moves.
45132
- */
45133
- function ApplyCellBorders(imageData, cellTexels, widthTexels, color) {
45134
- if (!(cellTexels > 0) || !(widthTexels > 0) || !color || color.alpha <= 0) {
45135
- return;
45136
- }
45137
- const { width, height, data } = imageData;
45138
- const edge = Math.max(1, Math.round(widthTexels));
45139
- for (let y = 0; y < height; y++) {
45140
- const onRow = (y % cellTexels) < edge;
45141
- for (let x = 0; x < width; x++) {
45142
- if (!onRow && (x % cellTexels) >= edge) {
45143
- continue;
45144
- }
45145
- const i = (y * width + x) * 4;
45146
- // Only where something is already drawn, so a border cannot invent coverage the data
45147
- // does not have.
45148
- if (data[i + 3] === 0) {
45149
- continue;
45150
- }
45151
- const a = color.alpha;
45152
- data[i] = Math.round(data[i] * (1 - a) + color.red * a);
45153
- data[i + 1] = Math.round(data[i + 1] * (1 - a) + color.green * a);
45154
- data[i + 2] = Math.round(data[i + 2] * (1 - a) + color.blue * a);
45155
- }
45156
- }
45157
- }
45158
- function loadImage(src) {
45159
- return new Promise((res, rej) => {
45160
- const image = new Image();
45161
- image.onload = () => res(image);
45162
- image.onerror = (e) => rej(e);
45163
- image.src = src;
45164
- });
45165
- }
45166
-
45167
45656
  var TextureFrameSeriesAnimator;
45168
45657
  (function (TextureFrameSeriesAnimator) {
45169
45658
  /**
@@ -49709,8 +50198,7 @@ async function resolveTextureValue(params) {
49709
50198
  const cacheKey = "texture-" + url + "-" + (mask ? mask.type + "-" + mask.minColor + "-" + mask.maxColor : "none");
49710
50199
  let prom = _textureCache.Get(cacheKey);
49711
50200
  if (!prom) {
49712
- prom = buildTintedTextureDataUri(url, mask).catch(() => null);
49713
- _textureCache.Set(cacheKey, prom);
50201
+ prom = cacheTexture(cacheKey, buildTintedTextureDataUri(url, mask));
49714
50202
  }
49715
50203
  return { dataUri: await prom, effective: textureTrace.effective };
49716
50204
  }
@@ -49738,11 +50226,23 @@ async function resolveTextureCondition(params) {
49738
50226
  const cacheKey = "texture-" + url + "-none";
49739
50227
  let prom = _textureCache.Get(cacheKey);
49740
50228
  if (!prom) {
49741
- prom = buildTintedTextureDataUri(url).catch(() => null);
49742
- _textureCache.Set(cacheKey, prom);
50229
+ prom = cacheTexture(cacheKey, buildTintedTextureDataUri(url));
49743
50230
  }
49744
50231
  return { dataUri: await prom, effective: null };
49745
50232
  }
50233
+ /*
50234
+ * Keeps a texture build for reuse, but drops a failed one so the texture is tried again next time.
50235
+ */
50236
+ function cacheTexture(cacheKey, build) {
50237
+ const prom = build.catch(() => {
50238
+ if (_textureCache.Get(cacheKey) === prom) {
50239
+ _textureCache.Delete(cacheKey);
50240
+ }
50241
+ return null;
50242
+ });
50243
+ _textureCache.Set(cacheKey, prom);
50244
+ return prom;
50245
+ }
49746
50246
  /**
49747
50247
  * Fetches an image and, if a color mask is provided, remaps its (assumed grayscale) pixel values
49748
50248
  * into a gradient between the mask's minColor/maxColor, preserving source alpha.
@@ -49750,16 +50250,7 @@ async function resolveTextureCondition(params) {
49750
50250
  * @param mask
49751
50251
  */
49752
50252
  async function buildTintedTextureDataUri(url, mask) {
49753
- const response = await fetch(url);
49754
- const blob = await response.blob();
49755
- const objectUrl = URL.createObjectURL(blob);
49756
- let image;
49757
- try {
49758
- image = await loadImage(objectUrl);
49759
- }
49760
- finally {
49761
- URL.revokeObjectURL(objectUrl);
49762
- }
50253
+ const image = await loadImageForCanvas(url);
49763
50254
  const canvas = document.createElement("canvas");
49764
50255
  canvas.width = image.width;
49765
50256
  canvas.height = image.height;
@@ -51317,7 +51808,7 @@ var StyleUtils;
51317
51808
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
51318
51809
  })(StyleUtils || (StyleUtils = {}));
51319
51810
 
51320
- const VERSION = "7.5.5";
51811
+ const VERSION = "7.5.7";
51321
51812
  /**
51322
51813
  * Updates the environment instance used by bruce-cesium to one specified.
51323
51814
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.
@@ -51334,5 +51825,5 @@ const getENVIRONMENT = () => {
51334
51825
  return ENVIRONMENT;
51335
51826
  };
51336
51827
 
51337
- export { VERSION, setENVIRONMENT, getENVIRONMENT, ClusterItems, PointClustering, PointClusterSize, CesiumAnimatedInOut, DisplacedSurfacePrimitive, CesiumAnimatedProperty, isStyleChanged, isOutlineChanged$1 as isOutlineChanged, StyleEffective, EntityRenderEngine, EntityRenderEngineModel3d, EntityRenderEnginePoint, EntityRenderEnginePolygon, EntityRenderEnginePolyline, TextureFrameSeriesAnimator, TextureValueLabels, MenuItemCreator, MenuItemManager, CesiumParabola, EntityLabel, EntityLabelGroup, LiveCursor, SharedGetters, DataSourceStaticKmlManager, DataLabRenderManager, EntitiesIdsRenderManager, EntitiesLoadedRenderManager, EntitiesRenderManager, EntityRenderManager, AssemblyRenderManager, GoogleSearchRenderManager, RelationsRenderManager, RenderManager, TilesetArbRenderManager, TilesetCadRenderManager, TilesetEntitiesRenderManager, TilesetGooglePhotosRenderManager, TilesetOsmRenderManager, TilesetPointcloudRenderManager, TileRenderEngine, createTileset, TilesetRenderEngine, ViewRenderEngine, VisualRegisterCuller, VisualsRegister, XGridsRenderEngine, CesiumEntityStyler, DrawingUtils, EntityUtils, MeasureUtils, StyleUtils, CESIUM_INSPECTOR_KEY, CESIUM_TIMELINE_KEY, CESIUM_TIMELINE_LIVE_KEY, CESIUM_TIMELINE_LIVE_PADDING_KEY, CESIUM_TIMELINE_INTERVAL_KEY, CESIUM_MODEL_SPACE_KEY, DEFAULT_LIVE_PADDING_SECONDS, DEFAULT_BRIGHTNESS_SHIFT, DEFAULT_LIGHT_INTENSITY, LIGHTING_SETTINGS_VERSION, LIGHT_INTENSITY_COMPENSATION, MAX_COMPENSATED_LIGHT_INTENSITY, ViewUtils, CesiumViewMonitor, ViewGroundArea, ViewerEventTracker, ViewerUtils, Draw3dPolygon, Draw3dPolyline, MeasureCreator, Walkthrough, WidgetControlViewBar, WidgetControlViewBarSearch, WidgetLeftPanelTab, WidgetLeftPanelTabBookmarks, WidgetLeftPanelTabDashboard, EMenuItemState, WidgetLeftPanelTabMenuItems, Widget, VIEWER_BOOKMARKS_WIDGET_KEY, WidgetBookmarks, WidgetBranding, ECursor, WidgetCursorBar, WidgetEmbeddedInfoView, WidgetInfoView, VIEWER_LEFT_PANEL_WIDGET_KEY, VIEWER_LEFT_PANEL_CSS_VAR_LEFT, CreateTabIconSvg, WidgetLeftPanel, WidgetNavCompass$$1 as WidgetNavCompass, VIEWER_VIEW_BAR_WIDGET_KEY, WidgetViewBar };
51828
+ export { VERSION, setENVIRONMENT, getENVIRONMENT, ClusterItems, PointClustering, PointClusterSize, CesiumAnimatedInOut, DisplacedSurfacePrimitive, CesiumAnimatedProperty, isStyleChanged, isOutlineChanged$1 as isOutlineChanged, StyleEffective, EntityRenderEngine, EntityRenderEngineModel3d, EntityRenderEnginePoint, extractMetadataFromFileUrl, EntityRenderEnginePolygon, EntityRenderEnginePolyline, TextureFrameSeriesAnimator, TextureValueLabels, MenuItemCreator, MenuItemManager, CesiumParabola, EntityLabel, EntityLabelGroup, LiveCursor, SharedGetters, DataSourceStaticKmlManager, DataLabRenderManager, EntitiesIdsRenderManager, EntitiesLoadedRenderManager, EntitiesRenderManager, EntityRenderManager, AssemblyRenderManager, GoogleSearchRenderManager, RelationsRenderManager, RenderManager, TilesetArbRenderManager, TilesetCadRenderManager, TilesetEntitiesRenderManager, TilesetGooglePhotosRenderManager, TilesetOsmRenderManager, TilesetPointcloudRenderManager, TileRenderEngine, createTileset, TilesetRenderEngine, ViewRenderEngine, VisualRegisterCuller, VisualsRegister, XGridsRenderEngine, CesiumEntityStyler, DrawingUtils, EntityUtils, MeasureUtils, StyleUtils, CESIUM_INSPECTOR_KEY, CESIUM_TIMELINE_KEY, CESIUM_TIMELINE_LIVE_KEY, CESIUM_TIMELINE_LIVE_PADDING_KEY, CESIUM_TIMELINE_INTERVAL_KEY, CESIUM_MODEL_SPACE_KEY, DEFAULT_LIVE_PADDING_SECONDS, DEFAULT_BRIGHTNESS_SHIFT, DEFAULT_LIGHT_INTENSITY, LIGHTING_SETTINGS_VERSION, LIGHT_INTENSITY_COMPENSATION, MAX_COMPENSATED_LIGHT_INTENSITY, ViewUtils, CesiumViewMonitor, ViewGroundArea, ViewerEventTracker, ViewerUtils, Draw3dPolygon, Draw3dPolyline, MeasureCreator, Walkthrough, WidgetControlViewBar, WidgetControlViewBarSearch, WidgetLeftPanelTab, WidgetLeftPanelTabBookmarks, WidgetLeftPanelTabDashboard, EMenuItemState, WidgetLeftPanelTabMenuItems, Widget, VIEWER_BOOKMARKS_WIDGET_KEY, WidgetBookmarks, WidgetBranding, ECursor, WidgetCursorBar, WidgetEmbeddedInfoView, WidgetInfoView, VIEWER_LEFT_PANEL_WIDGET_KEY, VIEWER_LEFT_PANEL_CSS_VAR_LEFT, CreateTabIconSvg, WidgetLeftPanel, WidgetNavCompass$$1 as WidgetNavCompass, VIEWER_VIEW_BAR_WIDGET_KEY, WidgetViewBar };
51338
51829
  //# sourceMappingURL=bruce-cesium.es5.js.map