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