bruce-cesium 7.4.0 → 7.4.2

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.
@@ -5383,7 +5383,14 @@
5383
5383
  (function (EntityLabel) {
5384
5384
  const ICON_BACKGROUND_BLEND = 0.8;
5385
5385
  const ICON_BACKGROUND_ALPHA = 0.45;
5386
+ // Under this much difference between the glyph's width and height the chip is squared off,
5387
+ // so a row of labels reads as one size rather than a set of differently shaped pills.
5388
+ const ICON_BACKGROUND_UNIFORM_TOLERANCE = 0.3;
5386
5389
  const TEXT_MAX_WIDTH = 200;
5390
+ // How far an angled leader runs sideways before the label starts.
5391
+ const ANGLED_LEADER_RUN = 28;
5392
+ // One per tinted icon, since an SVG mask is addressed by id across the whole document.
5393
+ let tintCounter = 0;
5387
5394
  // Key where the label instance is stored within the cesium entity.
5388
5395
  // Eg: rego[ATTACH_KEY] = new EntityLabel(...);
5389
5396
  EntityLabel.ATTACH_KEY = "nextspace-navigator-entity-label";
@@ -5482,7 +5489,7 @@
5482
5489
  this.nextOrder = 0;
5483
5490
  }
5484
5491
  Resolve(frames) {
5485
- var _a, _b;
5492
+ var _a, _b, _c;
5486
5493
  const live = new Set(frames.map(frame => frame.id));
5487
5494
  const moved = (frame) => {
5488
5495
  var _a;
@@ -5509,10 +5516,17 @@
5509
5516
  const placed = new Map();
5510
5517
  const next = new Map();
5511
5518
  const candidate = (frame, side) => {
5512
- var _a;
5513
- return (side === "left" && frame.canFlip ?
5514
- { ...frame, x: Math.max(0, Math.min(frame.anchorX - frame.width, ((_a = frame.viewportWidth) !== null && _a !== void 0 ? _a : Infinity) - frame.width)) } :
5515
- frame);
5519
+ var _a, _b;
5520
+ if (!frame.canFlip) {
5521
+ return frame;
5522
+ }
5523
+ if (frame.leaderRun != null) {
5524
+ const x = side === "left" ? frame.anchorX - frame.leaderRun - frame.width : frame.anchorX + frame.leaderRun;
5525
+ return { ...frame, x: Math.max(0, Math.min(x, ((_a = frame.viewportWidth) !== null && _a !== void 0 ? _a : Infinity) - frame.width)) };
5526
+ }
5527
+ return (side === "left" ?
5528
+ { ...frame, x: Math.max(0, Math.min(frame.anchorX - frame.width, ((_b = frame.viewportWidth) !== null && _b !== void 0 ? _b : Infinity) - frame.width)) }
5529
+ : frame);
5516
5530
  };
5517
5531
  const score = (frame, padding = 0) => {
5518
5532
  const box = {
@@ -5528,7 +5542,7 @@
5528
5542
  };
5529
5543
  for (const frame of ordered) {
5530
5544
  const old = this.previous.get(frame.id);
5531
- let side = frame.canFlip ? (_a = old === null || old === void 0 ? void 0 : old.side) !== null && _a !== void 0 ? _a : "right" : "right";
5545
+ let side = frame.canFlip ? (_b = (_a = old === null || old === void 0 ? void 0 : old.side) !== null && _a !== void 0 ? _a : frame.leaderSide) !== null && _b !== void 0 ? _b : "right" : "right";
5532
5546
  let chosen = candidate(frame, side);
5533
5547
  if (!fixed(frame) && frame.canFlip) {
5534
5548
  const otherSide = side === "left" ? "right" : "left";
@@ -5540,7 +5554,7 @@
5540
5554
  }
5541
5555
  }
5542
5556
  placed.set(frame.id, { frame: chosen, side });
5543
- next.set(frame.id, { frame: fixed(frame) ? old.frame : { ...frame }, side, order: (_b = old === null || old === void 0 ? void 0 : old.order) !== null && _b !== void 0 ? _b : this.nextOrder++ });
5557
+ next.set(frame.id, { frame: fixed(frame) ? old.frame : { ...frame }, side, order: (_c = old === null || old === void 0 ? void 0 : old.order) !== null && _c !== void 0 ? _c : this.nextOrder++ });
5544
5558
  text.Add(chosen);
5545
5559
  const stem = leaderBounds(chosen);
5546
5560
  if (stem) {
@@ -5560,7 +5574,7 @@
5560
5574
  * Resolves clustering and stable text placement before depth fading; highlighted labels receive priority.
5561
5575
  */
5562
5576
  function Layout(input, memory = new PlacementMemory()) {
5563
- var _a, _b, _c, _d, _e, _f, _g, _h;
5577
+ var _a, _b, _c, _d, _e, _f, _g;
5564
5578
  const frames = input.filter(frame => [frame.x, frame.y, frame.width, frame.height, frame.depth].every(Number.isFinite))
5565
5579
  .sort((a, b) => (b.priority || 0) - (a.priority || 0) || a.depth - b.depth || a.id.localeCompare(b.id));
5566
5580
  const result = new Map();
@@ -5626,17 +5640,13 @@
5626
5640
  }
5627
5641
  }
5628
5642
  if (values.size > 1) {
5629
- const description = stableMembers.find(member => { var _a, _b; return member.textWrapped && ((_b = (member.text || ((_a = member.options) === null || _a === void 0 ? void 0 : _a.text))) === null || _b === void 0 ? void 0 : _b.trim()); });
5630
- if (description) {
5631
- representative.text = ((_f = description.text) !== null && _f !== void 0 ? _f : description.options.text).trim();
5632
- representative.summaryMore = true;
5633
- }
5634
- else {
5635
- const rows = Array.from(values).slice(0, 2);
5636
- if (values.size > 2)
5637
- rows.push(`+${values.size - 2} more…`);
5638
- representative.text = rows.join("\n");
5639
- }
5643
+ // A value long enough to wrap fills the box on its own, so only one is listed.
5644
+ const wrapped = stableMembers.some(member => { var _a, _b; return member.textWrapped && ((_b = (member.text || ((_a = member.options) === null || _a === void 0 ? void 0 : _a.text))) === null || _b === void 0 ? void 0 : _b.trim()); });
5645
+ const limit = wrapped ? 1 : 2;
5646
+ const rows = Array.from(values).slice(0, limit);
5647
+ if (values.size > limit)
5648
+ rows.push(`+${values.size - limit} more…`);
5649
+ representative.text = rows.join("\n");
5640
5650
  }
5641
5651
  }
5642
5652
  for (const frame of cluster.members) {
@@ -5660,7 +5670,7 @@
5660
5670
  const box = { ...frame, x: frame.x + .01, y: frame.y + .01, width: Math.max(0, frame.width - .02), height: Math.max(0, frame.height - .02) };
5661
5671
  const overlap = foreground.HasOverlap(box) || foregroundLeaders.HasOverlap(box) || Boolean(stem && foreground.HasOverlap(stem));
5662
5672
  if (overlap) {
5663
- placement.opacity *= Math.max(0, Math.min(1, (_h = (_g = frame.options) === null || _g === void 0 ? void 0 : _g.overlapOpacity) !== null && _h !== void 0 ? _h : 0.25));
5673
+ placement.opacity *= Math.max(0, Math.min(1, (_g = (_f = frame.options) === null || _f === void 0 ? void 0 : _f.overlapOpacity) !== null && _g !== void 0 ? _g : 0.25));
5664
5674
  }
5665
5675
  placement.zIndex = zIndex--;
5666
5676
  foreground.Add(frame);
@@ -5982,6 +5992,8 @@
5982
5992
  this.loadTimeout = null;
5983
5993
  this.loadedName = false;
5984
5994
  this.line = null;
5995
+ // The sideways half of an angled leader; the rise is drawn by line.
5996
+ this.leaderRun = null;
5985
5997
  this.anchor = null;
5986
5998
  // Null = not attempted to load.
5987
5999
  this.additionalTexts = null;
@@ -6378,6 +6390,7 @@
6378
6390
  }
6379
6391
  };
6380
6392
  const normalLeader = (_k = this.options.leader) !== null && _k !== void 0 ? _k : presentation === "text";
6393
+ const angled = this.options.leaderStyle === "angled";
6381
6394
  let leader = normalLeader;
6382
6395
  const content = document.createElement("span");
6383
6396
  const originalText = (_l = this.options.text) !== null && _l !== void 0 ? _l : [text, ...additionalTexts].join("\n");
@@ -6386,6 +6399,7 @@
6386
6399
  let sourceNaturalText;
6387
6400
  let iconElement;
6388
6401
  let iconContainer;
6402
+ let fitIconBackground;
6389
6403
  content.className = "entity-label-content";
6390
6404
  content.style.cssText = "display:flex;align-items:center;gap:6px;";
6391
6405
  if (presentation === "icon-text-above") {
@@ -6408,34 +6422,49 @@
6408
6422
  image.alt = this.options.icon || "";
6409
6423
  const scale = (_o = this.options.iconScale) !== null && _o !== void 0 ? _o : 1;
6410
6424
  const size = this.options.iconSize;
6411
- const mask = iconColor && iconBackground ? document.createElement("span") : icon;
6412
- if (mask !== icon) {
6413
- icon.appendChild(mask);
6414
- }
6415
- const target = iconColor ? mask : image;
6416
- if (iconColor) {
6417
- mask.style.display = "inline-block";
6418
- mask.style.backgroundColor = iconColor;
6419
- for (const prefix of ["", "-webkit-"]) {
6420
- mask.style.setProperty(`${prefix}mask-image`, `url(${JSON.stringify(this.options.iconUrl)})`);
6421
- mask.style.setProperty(`${prefix}mask-size`, "contain");
6422
- mask.style.setProperty(`${prefix}mask-repeat`, "no-repeat");
6423
- mask.style.setProperty(`${prefix}mask-position`, "center");
6424
- }
6425
+ const svgNs = "http://www.w3.org/2000/svg";
6426
+ const tint = iconColor ? document.createElementNS(svgNs, "svg") : null;
6427
+ const painted = tint ? document.createElementNS(svgNs, "image") : null;
6428
+ const inked = tint ? document.createElementNS(svgNs, "rect") : null;
6429
+ if (tint) {
6430
+ const maskId = `entity-label-tint-${++tintCounter}`;
6431
+ const mask = document.createElementNS(svgNs, "mask");
6432
+ mask.setAttribute("id", maskId);
6433
+ mask.setAttribute("maskUnits", "userSpaceOnUse");
6434
+ painted.setAttribute("href", this.options.iconUrl);
6435
+ painted.setAttribute("preserveAspectRatio", "xMidYMid meet");
6436
+ mask.appendChild(painted);
6437
+ inked.setAttribute("fill", iconColor);
6438
+ inked.setAttribute("mask", `url(#${maskId})`);
6439
+ tint.append(mask, inked);
6440
+ tint.style.display = "block";
6441
+ icon.style.display = "inline-flex";
6442
+ icon.appendChild(tint);
6425
6443
  }
6426
6444
  const resize = () => {
6427
6445
  if (this.disposed || this.counter !== counter) {
6428
6446
  return;
6429
6447
  }
6430
6448
  const intrinsic = this.options.iconScale != null && size == null;
6431
- target.style.width = `${(intrinsic ? image.naturalWidth || 24 : size || 24) * scale}px`;
6432
- target.style.height = `${(intrinsic ? image.naturalHeight || 24 : size || 24) * scale}px`;
6449
+ const width = (intrinsic ? image.naturalWidth || 24 : size || 24) * scale;
6450
+ const height = (intrinsic ? image.naturalHeight || 24 : size || 24) * scale;
6451
+ if (tint) {
6452
+ tint.setAttribute("viewBox", `0 0 ${width} ${height}`);
6453
+ for (const node of [tint, painted, inked]) {
6454
+ node.setAttribute("width", String(width));
6455
+ node.setAttribute("height", String(height));
6456
+ }
6457
+ }
6458
+ else {
6459
+ image.style.width = `${width}px`;
6460
+ image.style.height = `${height}px`;
6461
+ }
6433
6462
  this.viewer.scene.requestRender();
6434
6463
  };
6435
6464
  image.onload = resize;
6436
6465
  resize();
6437
6466
  image.src = this.options.iconUrl;
6438
- if (!iconColor) {
6467
+ if (!tint) {
6439
6468
  icon.appendChild(image);
6440
6469
  }
6441
6470
  }
@@ -6460,6 +6489,27 @@
6460
6489
  icon.style.minWidth = `${size}px`;
6461
6490
  icon.style.minHeight = `${size}px`;
6462
6491
  icon.style.boxSizing = "content-box";
6492
+ let fitted = false;
6493
+ fitIconBackground = () => {
6494
+ if (fitted) {
6495
+ return;
6496
+ }
6497
+ const padding = size / 3;
6498
+ const width = icon.offsetWidth - padding * 2;
6499
+ const height = icon.offsetHeight - padding * 2;
6500
+ if (width <= 0 || height <= 0) {
6501
+ return;
6502
+ }
6503
+ fitted = true;
6504
+ const longest = Math.max(width, height);
6505
+ const difference = (longest - Math.min(width, height)) / longest;
6506
+ if (difference > ICON_BACKGROUND_UNIFORM_TOLERANCE) {
6507
+ return;
6508
+ }
6509
+ icon.style.minWidth = `${longest}px`;
6510
+ icon.style.minHeight = `${longest}px`;
6511
+ this.viewer.scene.requestRender();
6512
+ };
6463
6513
  }
6464
6514
  if (this.options.iconScale === 0) {
6465
6515
  icon.style.display = "none";
@@ -6498,8 +6548,10 @@
6498
6548
  const countElement = document.createElement("span");
6499
6549
  countElement.className = "entity-label-count";
6500
6550
  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;";
6551
+ // Text above the icon would have the badge land in it, so the free corner is the lower one.
6552
+ const badgeBelow = presentation === "icon-text-above";
6501
6553
  if (iconContainer) {
6502
- countElement.style.cssText += "position:absolute;left:100%;top:0;transform:translate(-50%,-50%);";
6554
+ countElement.style.cssText += `position:absolute;left:100%;top:${badgeBelow ? "100%" : "0"};transform:translate(-50%,-50%);`;
6503
6555
  iconContainer.appendChild(countElement);
6504
6556
  }
6505
6557
  if (!layers.has(this.viewer)) {
@@ -6508,6 +6560,7 @@
6508
6560
  const layer = layers.get(this.viewer);
6509
6561
  let ele = null;
6510
6562
  let lineEle = null;
6563
+ let runEle = null;
6511
6564
  if (this.label) {
6512
6565
  ele = this.label;
6513
6566
  }
@@ -6544,6 +6597,7 @@
6544
6597
  }
6545
6598
  if (this.line) {
6546
6599
  lineEle = this.line;
6600
+ runEle = this.leaderRun;
6547
6601
  }
6548
6602
  else {
6549
6603
  lineEle = document.createElement("div");
@@ -6587,6 +6641,14 @@
6587
6641
  layer.element.appendChild(lineEleNode);
6588
6642
  layer.element.appendChild(lineEle);
6589
6643
  }
6644
+ // Built here rather than beside the stem, since the style can change on a label that is reused.
6645
+ if (angled && !runEle) {
6646
+ runEle = lineEle.cloneNode(false);
6647
+ runEle.className = "entity-label-leader-run";
6648
+ runEle.style.display = "none";
6649
+ this.leaderRun = runEle;
6650
+ layer.element.appendChild(runEle);
6651
+ }
6590
6652
  let MAX_DISTANCE = Math.max(15000, sizeInM * 35);
6591
6653
  if (visual && visual instanceof Cesium.Entity) {
6592
6654
  // Consistent sized things.
@@ -6606,6 +6668,25 @@
6606
6668
  y: upwards ? endY - height : endY, endY
6607
6669
  };
6608
6670
  };
6671
+ /*
6672
+ * An angled leader parks the label beside the anchor instead of over it,
6673
+ * so the stem rises to the label's middle and then runs out to whichever side has the room.
6674
+ */
6675
+ const fitAngledLeader = (anchorX, anchorY, desired, width, height, radius = 0) => {
6676
+ const canvas = this.viewer.canvas;
6677
+ const above = Math.max(0, anchorY - height / 2);
6678
+ const below = Math.max(0, canvas.clientHeight - anchorY - height / 2);
6679
+ const upwards = above >= Math.max(radius ? radius + 4 : 0, Math.min(8, desired)) || above >= below;
6680
+ const length = Math.min(desired, upwards ? above : below);
6681
+ const endY = anchorY + (upwards ? -length : length);
6682
+ const run = Math.max(ANGLED_LEADER_RUN, radius + 4);
6683
+ const rightwards = anchorX + run + width <= canvas.clientWidth || anchorX - run - width < 0;
6684
+ return {
6685
+ x: Math.max(0, Math.min(rightwards ? anchorX + run : anchorX - run - width, canvas.clientWidth - width)),
6686
+ y: Math.max(0, Math.min(endY - height / 2, canvas.clientHeight - height)), endY,
6687
+ run, side: (rightwards ? "right" : "left")
6688
+ };
6689
+ };
6609
6690
  let cursorMotion = this.cursorMotion;
6610
6691
  const project = () => {
6611
6692
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
@@ -6743,7 +6824,14 @@
6743
6824
  y -= height / 2;
6744
6825
  }
6745
6826
  // Membership uses the unclamped source footprint, never the summary's changing dimensions.
6746
- if (leader)
6827
+ let leaderRun;
6828
+ let leaderSide;
6829
+ if (leader && angled) {
6830
+ const fit = fitAngledLeader(anchorX, anchorY, leaderLength, width, height);
6831
+ ({ x, y } = fit);
6832
+ ({ run: leaderRun, side: leaderSide } = fit);
6833
+ }
6834
+ else if (leader)
6747
6835
  ({ x, y } = fitLeader(anchorX, anchorY, leaderLength, width, height));
6748
6836
  const bounds = clusterBounds || { x, y, width, height };
6749
6837
  if (!leader && (bounds.x + bounds.width < 0 || bounds.y + bounds.height < 0 || bounds.x > this.viewer.canvas.clientWidth || bounds.y > this.viewer.canvas.clientHeight)) {
@@ -6758,20 +6846,21 @@
6758
6846
  extendedGeometry: visual instanceof Cesium.Entity && Boolean(visual.polygon || visual.polyline || visual.corridor),
6759
6847
  viewportWidth: this.viewer.canvas.clientWidth,
6760
6848
  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,
6761
- entityId: (_t = this.rego) === null || _t === void 0 ? void 0 : _t.entityId, group: this.groupId, x, y, width, height, anchorX, anchorY, depth, leaderLength, distance,
6849
+ 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,
6762
6850
  // Priority shows the hovered member individually without dragging its cluster along.
6763
6851
  opacity, priority: hover || cursorAmount > 0 || (state === null || state === void 0 ? void 0 : state.highlighted) ? 1 : 0, options: this.options, text: originalText, clusterBounds
6764
6852
  };
6765
6853
  };
6766
6854
  let displacement = { x: 0, y: 0 };
6767
- let displayedSummaryMore = false;
6768
6855
  let nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
6769
6856
  const apply = (placement) => {
6770
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
6857
+ 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;
6771
6858
  const show = (placement === null || placement === void 0 ? void 0 : placement.visible) && placement.opacity > 0;
6772
6859
  clickEntityIds = show ? placement.entityIds : [];
6773
6860
  ele.style.visibility = show ? "visible" : "hidden";
6774
6861
  lineEle.style.display = show && leader ? "block" : "none";
6862
+ if (runEle)
6863
+ runEle.style.display = show && leader ? "block" : "none";
6775
6864
  this.anchor.style.display = show && leader ? "block" : "none";
6776
6865
  if (!show) {
6777
6866
  displacement = { x: 0, y: 0 };
@@ -6782,17 +6871,8 @@
6782
6871
  return;
6783
6872
  }
6784
6873
  const displayText = (_a = placement.text) !== null && _a !== void 0 ? _a : originalText;
6785
- const summaryMore = Boolean(placement.summaryMore);
6786
- if (textElement && (textElement.textContent !== displayText + (summaryMore ? "\n…" : "") || displayedSummaryMore !== summaryMore)) {
6874
+ if (textElement && textElement.textContent !== displayText) {
6787
6875
  textElement.textContent = displayText;
6788
- if (summaryMore) {
6789
- const more = document.createElement("span");
6790
- more.className = "entity-label-more";
6791
- more.style.opacity = "0.65";
6792
- more.textContent = "\n…";
6793
- textElement.appendChild(more);
6794
- }
6795
- displayedSummaryMore = summaryMore;
6796
6876
  // Measure the summary's footprint next frame without replacing each member's source text.
6797
6877
  this.viewer.scene.requestRender();
6798
6878
  }
@@ -6810,6 +6890,7 @@
6810
6890
  // The badge participates in layout, so its new width needs another measurement.
6811
6891
  this.viewer.scene.requestRender();
6812
6892
  }
6893
+ fitIconBackground === null || fitIconBackground === void 0 ? void 0 : fitIconBackground();
6813
6894
  if (iconContainer) {
6814
6895
  const badgeWidth = countElement.offsetWidth;
6815
6896
  const iconWidth = iconElement.offsetWidth;
@@ -6817,7 +6898,9 @@
6817
6898
  const clearance = badgeWidth ? (narrow ? badgeWidth / 2 + 2 : Math.max(0, badgeWidth / 2 - iconWidth / 4)) : 0;
6818
6899
  countElement.style.left = `calc(100% + ${clearance}px)`;
6819
6900
  // Reserve the badge overhang so adjacent text and viewport edges remain clear.
6820
- iconContainer.style.marginTop = `${countElement.offsetHeight / 2}px`;
6901
+ const overhang = countElement.offsetHeight / 2;
6902
+ iconContainer.style.marginTop = badgeBelow ? "0px" : `${overhang}px`;
6903
+ iconContainer.style.marginBottom = badgeBelow ? `${overhang}px` : "0px";
6821
6904
  iconContainer.style.marginRight = `${badgeWidth / 2 + clearance}px`;
6822
6905
  }
6823
6906
  // Summary and badge changes must fit on their first visible frame, including after resize.
@@ -6860,19 +6943,26 @@
6860
6943
  sharedRing.animation = ringAnimation;
6861
6944
  }
6862
6945
  const radius = ringAnimation.inner > 0 ? ringAnimation.outer : 0;
6863
- const stem = (leader ? fitLeader(frame.anchorX, frame.anchorY, Math.max(frame.leaderLength, radius ? radius + 4 : 0), ele.offsetWidth / coordinates.scaleX, ele.offsetHeight / coordinates.scaleY, radius) : undefined);
6946
+ const stem = (leader ? (angled ? fitAngledLeader : fitLeader)(frame.anchorX, frame.anchorY, Math.max(frame.leaderLength, radius ? radius + 4 : 0), ele.offsetWidth / coordinates.scaleX, ele.offsetHeight / coordinates.scaleY, radius) : undefined);
6864
6947
  const position = { x: (stem || frame).x, y: (stem || frame).y };
6865
6948
  const width = ele.offsetWidth / coordinates.scaleX;
6866
6949
  const height = ele.offsetHeight / coordinates.scaleY;
6867
- if (leader && placement.textSide === "left") {
6950
+ if (leader && angled) {
6951
+ // The declutter owns the side; the fit only says which one had the room to begin with.
6952
+ const run = Math.max(ANGLED_LEADER_RUN, radius + 4);
6953
+ const side = (_l = (_k = placement.textSide) !== null && _k !== void 0 ? _k : frame.leaderSide) !== null && _l !== void 0 ? _l : "right";
6954
+ const beside = side === "left" ? frame.anchorX - run - width : frame.anchorX + run;
6955
+ position.x = Math.max(0, Math.min(beside, this.viewer.canvas.clientWidth - width));
6956
+ }
6957
+ else if (leader && placement.textSide === "left") {
6868
6958
  position.x = Math.max(0, Math.min(frame.anchorX - width, this.viewer.canvas.clientWidth - width));
6869
6959
  }
6870
- const gap = (_l = (_k = this.options.pointerAvoidance) === null || _k === void 0 ? void 0 : _k.gap) !== null && _l !== void 0 ? _l : 12;
6960
+ const gap = (_o = (_m = this.options.pointerAvoidance) === null || _m === void 0 ? void 0 : _m.gap) !== null && _o !== void 0 ? _o : 12;
6871
6961
  const containsPointer = (x, y, margin) => {
6872
6962
  return pointer && pointer.x >= x - margin && pointer.x <= x + width + margin && pointer.y >= y - margin && pointer.y <= y + height + margin;
6873
6963
  };
6874
6964
  const passive = !selectOnClick || !this.visualRegister;
6875
- if (passive && ((_m = this.options.pointerAvoidance) === null || _m === void 0 ? void 0 : _m.enabled) !== false && pointer &&
6965
+ if (passive && ((_p = this.options.pointerAvoidance) === null || _p === void 0 ? void 0 : _p.enabled) !== false && pointer &&
6876
6966
  (containsPointer(position.x, position.y, gap) || containsPointer(position.x + displacement.x, position.y + displacement.y, gap))) {
6877
6967
  if (containsPointer(position.x + displacement.x, position.y + displacement.y, 0)) {
6878
6968
  // Choose against the original box and retain the offset until the pointer leaves both boxes.
@@ -6890,7 +6980,7 @@
6890
6980
  else {
6891
6981
  displacement = { x: 0, y: 0 };
6892
6982
  }
6893
- const nudgeDuration = (_p = (_o = this.options.pointerAvoidance) === null || _o === void 0 ? void 0 : _o.transitionDuration) !== null && _p !== void 0 ? _p : 200;
6983
+ const nudgeDuration = (_r = (_q = this.options.pointerAvoidance) === null || _q === void 0 ? void 0 : _q.transitionDuration) !== null && _r !== void 0 ? _r : 200;
6894
6984
  const animateNudge = () => {
6895
6985
  const progress = now < nudge.start ? 0 : nudgeDuration > 0 ? Math.min(1, (now - nudge.start) / nudgeDuration) : 1;
6896
6986
  const eased = progress * progress * (3 - 2 * progress);
@@ -6906,7 +6996,7 @@
6906
6996
  fromY: nudge.y,
6907
6997
  targetX: displacement.x,
6908
6998
  targetY: displacement.y,
6909
- start: now + ((_r = (_q = this.options.pointerAvoidance) === null || _q === void 0 ? void 0 : _q.delay) !== null && _r !== void 0 ? _r : 180)
6999
+ start: now + ((_t = (_s = this.options.pointerAvoidance) === null || _s === void 0 ? void 0 : _s.delay) !== null && _t !== void 0 ? _t : 180)
6910
7000
  };
6911
7001
  }
6912
7002
  if (animateNudge() < 1 && (nudge.fromX !== nudge.targetX || nudge.fromY !== nudge.targetY)) {
@@ -6918,7 +7008,7 @@
6918
7008
  ele.style.top = `${coordinates.y + position.y * coordinates.scaleY}px`;
6919
7009
  const clickTarget = content.querySelector('[role="button"]');
6920
7010
  if (clickTarget) {
6921
- clickTarget.setAttribute("aria-label", placement.members.length ? `Select ${placement.entityIds.length} entities` : `Select ${(_t = (_s = this.options.text) !== null && _s !== void 0 ? _s : text) !== null && _t !== void 0 ? _t : (_u = this.rego) === null || _u === void 0 ? void 0 : _u.entityId}`);
7011
+ clickTarget.setAttribute("aria-label", placement.members.length ? `Select ${placement.entityIds.length} entities` : `Select ${(_v = (_u = this.options.text) !== null && _u !== void 0 ? _u : text) !== null && _v !== void 0 ? _v : (_w = this.rego) === null || _w === void 0 ? void 0 : _w.entityId}`);
6922
7012
  }
6923
7013
  if (leader) {
6924
7014
  const nodeRadius = ringAnimation.outer;
@@ -6941,8 +7031,13 @@
6941
7031
  anchor.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount));
6942
7032
  anchor.style.zIndex = String(placement.zIndex);
6943
7033
  // The stem starts at the rim, never across the transparent centre, even after passive displacement.
6944
- const endX = Math.max(position.x, Math.min(frame.anchorX, position.x + width));
6945
- const endY = frame.anchorY < position.y ? position.y : frame.anchorY > position.y + height ? position.y + height : frame.anchorY;
7034
+ const straightX = Math.max(position.x, Math.min(frame.anchorX, position.x + width));
7035
+ const straightY = frame.anchorY < position.y ? position.y : frame.anchorY > position.y + height ? position.y + height : frame.anchorY;
7036
+ const beside = angled && (frame.anchorX < position.x || frame.anchorX > position.x + width);
7037
+ const elbowY = position.y + height / 2;
7038
+ const endX = beside ? frame.anchorX : straightX;
7039
+ const endY = beside ? elbowY : straightY;
7040
+ const runToX = frame.anchorX < position.x ? position.x : position.x + width;
6946
7041
  const dx = endX - frame.anchorX, dy = endY - frame.anchorY;
6947
7042
  const length = Math.hypot(dx, dy);
6948
7043
  const leaderAngle = Math.atan2(dy, dx);
@@ -6953,13 +7048,13 @@
6953
7048
  // Keep the stem inside the 126-degree arc, away from antialiased endpoints.
6954
7049
  const arcFraction = 0.35;
6955
7050
  const rotationLimit = Math.PI * arcFraction - Math.PI / 30;
6956
- const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_v = ringAnimation.arcOffset) !== null && _v !== void 0 ? _v : 0);
7051
+ const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_x = ringAnimation.arcOffset) !== null && _x !== void 0 ? _x : 0);
6957
7052
  const targetOpen = cursorMotion.target || (hovered && (ring === null || ring === void 0 ? void 0 : ring.enabled) !== false && targetRadius > 0) ? 1 : 0;
6958
- const elapsed = Math.min(32, Math.max(0, now - ((_w = ringAnimation.arcTime) !== null && _w !== void 0 ? _w : now)));
7053
+ const elapsed = Math.min(32, Math.max(0, now - ((_y = ringAnimation.arcTime) !== null && _y !== void 0 ? _y : now)));
6959
7054
  const blend = duration > 0 ? 1 - Math.exp(-elapsed / (duration / 3)) : 1;
6960
7055
  const approach = (value, target) => Math.abs(value - target) < 0.001 ? target : value + (target - value) * blend;
6961
- ringAnimation.arcOffset = approach((_x = ringAnimation.arcOffset) !== null && _x !== void 0 ? _x : 0, targetOffset);
6962
- ringAnimation.arcOpen = approach((_y = ringAnimation.arcOpen) !== null && _y !== void 0 ? _y : 0, targetOpen);
7056
+ ringAnimation.arcOffset = approach((_z = ringAnimation.arcOffset) !== null && _z !== void 0 ? _z : 0, targetOffset);
7057
+ ringAnimation.arcOpen = approach((_0 = ringAnimation.arcOpen) !== null && _0 !== void 0 ? _0 : 0, targetOpen);
6963
7058
  ringAnimation.arcTime = now;
6964
7059
  if (ringAnimation.arcOffset !== targetOffset || ringAnimation.arcOpen !== targetOpen) {
6965
7060
  this.viewer.scene.requestRender();
@@ -6971,17 +7066,27 @@
6971
7066
  const cut = Math.min(radius, length);
6972
7067
  const startX = frame.anchorX + (length ? dx / length * cut : 0);
6973
7068
  const startY = frame.anchorY + (length ? dy / length * cut : 0);
6974
- const screenDx = (endX - startX) * coordinates.scaleX;
6975
- const screenDy = (endY - startY) * coordinates.scaleY;
6976
- lineEle.style.width = `${2 * coordinates.scaleX}px`;
6977
- lineEle.style.left = `${coordinates.x + (startX - 1) * coordinates.scaleX}px`;
6978
- lineEle.style.top = `${coordinates.y + startY * coordinates.scaleY}px`;
6979
- lineEle.style.height = `${Math.hypot(screenDx, screenDy)}px`;
6980
- lineEle.style.transform = `rotate(${Math.atan2(screenDy, screenDx) - Math.PI / 2}rad)`;
6981
- lineEle.style.boxShadow = radius ? "none" : "0 0 5px rgba(0, 0, 0, 0.3)";
6982
- lineEle.style.transformOrigin = "top center";
6983
- lineEle.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount));
6984
- lineEle.style.zIndex = String(placement.zIndex);
7069
+ const draw = (element, fromX, fromY, toX, toY) => {
7070
+ const screenDx = (toX - fromX) * coordinates.scaleX;
7071
+ const screenDy = (toY - fromY) * coordinates.scaleY;
7072
+ element.style.width = `${2 * coordinates.scaleX}px`;
7073
+ element.style.left = `${coordinates.x + (fromX - 1) * coordinates.scaleX}px`;
7074
+ element.style.top = `${coordinates.y + fromY * coordinates.scaleY}px`;
7075
+ element.style.height = `${Math.hypot(screenDx, screenDy)}px`;
7076
+ element.style.transform = `rotate(${Math.atan2(screenDy, screenDx) - Math.PI / 2}rad)`;
7077
+ element.style.boxShadow = radius ? "none" : "0 0 5px rgba(0, 0, 0, 0.3)";
7078
+ element.style.transformOrigin = "top center";
7079
+ element.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount));
7080
+ element.style.zIndex = String(placement.zIndex);
7081
+ };
7082
+ draw(lineEle, startX, startY, endX, endY);
7083
+ if (runEle) {
7084
+ // Overlap the corner by the stem's own width so the elbow has no notch in it.
7085
+ const overlap = frame.anchorX < runToX ? -1 : 1;
7086
+ runEle.style.display = beside ? "block" : "none";
7087
+ if (beside)
7088
+ draw(runEle, endX + overlap, endY, runToX, endY);
7089
+ }
6985
7090
  }
6986
7091
  };
6987
7092
  ele.style.display = "flex";
@@ -7006,7 +7111,7 @@
7006
7111
  * Removes label from the viewer and unsubscribes from events.
7007
7112
  */
7008
7113
  removeLabel() {
7009
- var _a;
7114
+ var _a, _b;
7010
7115
  (_a = this.anchor) === null || _a === void 0 ? void 0 : _a.remove();
7011
7116
  this.anchor = null;
7012
7117
  if (this.label &&
@@ -7018,6 +7123,8 @@
7018
7123
  this.line.parentElement.removeChild(this.line);
7019
7124
  this.line = null;
7020
7125
  }
7126
+ (_b = this.leaderRun) === null || _b === void 0 ? void 0 : _b.remove();
7127
+ this.leaderRun = null;
7021
7128
  if (this.renderRemoval) {
7022
7129
  this.renderRemoval();
7023
7130
  this.renderRemoval = null;
@@ -7140,10 +7247,59 @@
7140
7247
  };
7141
7248
  }
7142
7249
  EntityLabelGroup.FromEntity = FromEntity;
7250
+ /**
7251
+ * Returns if we need to resolve Entity data to create the label data.
7252
+ */
7253
+ function NeedsEntityData(options) {
7254
+ var _a;
7255
+ if (!options) {
7256
+ return false;
7257
+ }
7258
+ if ((_a = options.attributes) === null || _a === void 0 ? void 0 : _a.length) {
7259
+ return true;
7260
+ }
7261
+ return [
7262
+ options.text, options.icon, options.iconUrl, options.iconClass,
7263
+ options.iconColor, options.iconStatus, options.iconPriority, options.iconScale
7264
+ ].some(Array.isArray);
7265
+ }
7266
+ EntityLabelGroup.NeedsEntityData = NeedsEntityData;
7267
+ // How many entities to read at once, so a long hand-picked list does not arrive as a burst.
7268
+ const HYDRATE_AT_ONCE = 8;
7269
+ /*
7270
+ * Reads the entities behind items that name one without carrying it.
7271
+ */
7272
+ async function Hydrate(items, api, signal) {
7273
+ if (!api) {
7274
+ return items;
7275
+ }
7276
+ const hydrated = items.slice();
7277
+ for (let start = 0; start < hydrated.length; start += HYDRATE_AT_ONCE) {
7278
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
7279
+ return hydrated;
7280
+ }
7281
+ await Promise.all(hydrated.slice(start, start + HYDRATE_AT_ONCE).map(async (item, at) => {
7282
+ if (!(item === null || item === void 0 ? void 0 : item.entityId) || item.entity) {
7283
+ return;
7284
+ }
7285
+ try {
7286
+ const { entity } = await BModels.Entity.Get({ api, entityId: item.entityId, migrated: true });
7287
+ if (entity) {
7288
+ hydrated[start + at] = { ...item, entity };
7289
+ }
7290
+ }
7291
+ catch {
7292
+ // Rip.
7293
+ }
7294
+ }));
7295
+ }
7296
+ return hydrated;
7297
+ }
7298
+ EntityLabelGroup.Hydrate = Hydrate;
7143
7299
  /*
7144
7300
  * Uses the existing DataLab grammar, discarding actions and projections that cannot return Entities.
7145
7301
  */
7146
- async function Loader(source, api, signal) {
7302
+ async function Loader(source, api, signal, hydrate) {
7147
7303
  var _a;
7148
7304
  var _b;
7149
7305
  if (source.type === "provider") {
@@ -7155,7 +7311,13 @@
7155
7311
  if (!Array.isArray(items)) {
7156
7312
  throw new Error("Label JSON must be an array of label items");
7157
7313
  }
7158
- return async (offset, limit) => ({ items: items.slice(offset, offset + limit), hasMore: offset + limit < items.length });
7314
+ return async (offset, limit, signal) => {
7315
+ const page = items.slice(offset, offset + limit);
7316
+ return {
7317
+ items: hydrate ? await Hydrate(page, api, signal) : page,
7318
+ hasMore: offset + limit < items.length
7319
+ };
7320
+ };
7159
7321
  }
7160
7322
  if (!api) {
7161
7323
  throw new Error("DataLab label sources require API getters on the visual register");
@@ -7284,7 +7446,7 @@
7284
7446
  const items = new Map();
7285
7447
  let truncated = false;
7286
7448
  try {
7287
- const load = await Loader(definition.source, (_g = this.register.ApiGetters) === null || _g === void 0 ? void 0 : _g.GetBruceApi(), controller.signal);
7449
+ const load = await Loader(definition.source, (_g = this.register.ApiGetters) === null || _g === void 0 ? void 0 : _g.GetBruceApi(), controller.signal, NeedsEntityData(definition.options));
7288
7450
  let offset = 0;
7289
7451
  for (let pageNumber = 0;; pageNumber++) {
7290
7452
  if (controller.signal.aborted || this.register.Disposed) {
@@ -7490,10 +7652,11 @@
7490
7652
  this.pending.delete(id);
7491
7653
  const group = this.active.get(id);
7492
7654
  if (!group) {
7655
+ // Nothing to unindex. Say so only if there was a definition to forget.
7493
7656
  if (known) {
7494
7657
  this.notify();
7495
- return;
7496
7658
  }
7659
+ return;
7497
7660
  }
7498
7661
  this.unindex(id);
7499
7662
  this.active.delete(id);
@@ -50068,7 +50231,7 @@ void main() {
50068
50231
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
50069
50232
  })(exports.StyleUtils || (exports.StyleUtils = {}));
50070
50233
 
50071
- const VERSION = "7.4.0";
50234
+ const VERSION = "7.4.2";
50072
50235
  /**
50073
50236
  * Updates the environment instance used by bruce-cesium to one specified.
50074
50237
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.