bruce-cesium 7.4.1 → 7.4.3

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.
@@ -33,7 +33,14 @@ var EntityLabel;
33
33
  (function (EntityLabel) {
34
34
  const ICON_BACKGROUND_BLEND = 0.8;
35
35
  const ICON_BACKGROUND_ALPHA = 0.45;
36
+ // Under this much difference between the glyph's width and height the chip is squared off,
37
+ // so a row of labels reads as one size rather than a set of differently shaped pills.
38
+ const ICON_BACKGROUND_UNIFORM_TOLERANCE = 0.3;
36
39
  const TEXT_MAX_WIDTH = 200;
40
+ // How far an angled leader runs sideways before the label starts.
41
+ const ANGLED_LEADER_RUN = 28;
42
+ // One per tinted icon, since an SVG mask is addressed by id across the whole document.
43
+ let tintCounter = 0;
37
44
  // Key where the label instance is stored within the cesium entity.
38
45
  // Eg: rego[ATTACH_KEY] = new EntityLabel(...);
39
46
  EntityLabel.ATTACH_KEY = "nextspace-navigator-entity-label";
@@ -132,7 +139,7 @@ var EntityLabel;
132
139
  this.nextOrder = 0;
133
140
  }
134
141
  Resolve(frames) {
135
- var _a, _b;
142
+ var _a, _b, _c;
136
143
  const live = new Set(frames.map(frame => frame.id));
137
144
  const moved = (frame) => {
138
145
  var _a;
@@ -159,10 +166,17 @@ var EntityLabel;
159
166
  const placed = new Map();
160
167
  const next = new Map();
161
168
  const candidate = (frame, side) => {
162
- var _a;
163
- return (side === "left" && frame.canFlip ?
164
- { ...frame, x: Math.max(0, Math.min(frame.anchorX - frame.width, ((_a = frame.viewportWidth) !== null && _a !== void 0 ? _a : Infinity) - frame.width)) } :
165
- frame);
169
+ var _a, _b;
170
+ if (!frame.canFlip) {
171
+ return frame;
172
+ }
173
+ if (frame.leaderRun != null) {
174
+ const x = side === "left" ? frame.anchorX - frame.leaderRun - frame.width : frame.anchorX + frame.leaderRun;
175
+ return { ...frame, x: Math.max(0, Math.min(x, ((_a = frame.viewportWidth) !== null && _a !== void 0 ? _a : Infinity) - frame.width)) };
176
+ }
177
+ return (side === "left" ?
178
+ { ...frame, x: Math.max(0, Math.min(frame.anchorX - frame.width, ((_b = frame.viewportWidth) !== null && _b !== void 0 ? _b : Infinity) - frame.width)) }
179
+ : frame);
166
180
  };
167
181
  const score = (frame, padding = 0) => {
168
182
  const box = {
@@ -178,7 +192,7 @@ var EntityLabel;
178
192
  };
179
193
  for (const frame of ordered) {
180
194
  const old = this.previous.get(frame.id);
181
- let side = frame.canFlip ? (_a = old === null || old === void 0 ? void 0 : old.side) !== null && _a !== void 0 ? _a : "right" : "right";
195
+ 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";
182
196
  let chosen = candidate(frame, side);
183
197
  if (!fixed(frame) && frame.canFlip) {
184
198
  const otherSide = side === "left" ? "right" : "left";
@@ -190,7 +204,7 @@ var EntityLabel;
190
204
  }
191
205
  }
192
206
  placed.set(frame.id, { frame: chosen, side });
193
- 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++ });
207
+ 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++ });
194
208
  text.Add(chosen);
195
209
  const stem = leaderBounds(chosen);
196
210
  if (stem) {
@@ -210,7 +224,7 @@ var EntityLabel;
210
224
  * Resolves clustering and stable text placement before depth fading; highlighted labels receive priority.
211
225
  */
212
226
  function Layout(input, memory = new PlacementMemory()) {
213
- var _a, _b, _c, _d, _e, _f, _g, _h;
227
+ var _a, _b, _c, _d, _e, _f, _g;
214
228
  const frames = input.filter(frame => [frame.x, frame.y, frame.width, frame.height, frame.depth].every(Number.isFinite))
215
229
  .sort((a, b) => (b.priority || 0) - (a.priority || 0) || a.depth - b.depth || a.id.localeCompare(b.id));
216
230
  const result = new Map();
@@ -276,17 +290,13 @@ var EntityLabel;
276
290
  }
277
291
  }
278
292
  if (values.size > 1) {
279
- 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()); });
280
- if (description) {
281
- representative.text = ((_f = description.text) !== null && _f !== void 0 ? _f : description.options.text).trim();
282
- representative.summaryMore = true;
283
- }
284
- else {
285
- const rows = Array.from(values).slice(0, 2);
286
- if (values.size > 2)
287
- rows.push(`+${values.size - 2} more…`);
288
- representative.text = rows.join("\n");
289
- }
293
+ // A value long enough to wrap fills the box on its own, so only one is listed.
294
+ 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()); });
295
+ const limit = wrapped ? 1 : 2;
296
+ const rows = Array.from(values).slice(0, limit);
297
+ if (values.size > limit)
298
+ rows.push(`+${values.size - limit} more…`);
299
+ representative.text = rows.join("\n");
290
300
  }
291
301
  }
292
302
  for (const frame of cluster.members) {
@@ -310,7 +320,7 @@ var EntityLabel;
310
320
  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) };
311
321
  const overlap = foreground.HasOverlap(box) || foregroundLeaders.HasOverlap(box) || Boolean(stem && foreground.HasOverlap(stem));
312
322
  if (overlap) {
313
- 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));
323
+ 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));
314
324
  }
315
325
  ;
316
326
  placement.zIndex = zIndex--;
@@ -633,6 +643,8 @@ var EntityLabel;
633
643
  this.loadTimeout = null;
634
644
  this.loadedName = false;
635
645
  this.line = null;
646
+ // The sideways half of an angled leader; the rise is drawn by line.
647
+ this.leaderRun = null;
636
648
  this.anchor = null;
637
649
  // Null = not attempted to load.
638
650
  this.additionalTexts = null;
@@ -1029,6 +1041,7 @@ var EntityLabel;
1029
1041
  }
1030
1042
  };
1031
1043
  const normalLeader = (_k = this.options.leader) !== null && _k !== void 0 ? _k : presentation === "text";
1044
+ const angled = this.options.leaderStyle === "angled";
1032
1045
  let leader = normalLeader;
1033
1046
  const content = document.createElement("span");
1034
1047
  const originalText = (_l = this.options.text) !== null && _l !== void 0 ? _l : [text, ...additionalTexts].join("\n");
@@ -1037,6 +1050,7 @@ var EntityLabel;
1037
1050
  let sourceNaturalText;
1038
1051
  let iconElement;
1039
1052
  let iconContainer;
1053
+ let fitIconBackground;
1040
1054
  content.className = "entity-label-content";
1041
1055
  content.style.cssText = "display:flex;align-items:center;gap:6px;";
1042
1056
  if (presentation === "icon-text-above") {
@@ -1059,34 +1073,49 @@ var EntityLabel;
1059
1073
  image.alt = this.options.icon || "";
1060
1074
  const scale = (_o = this.options.iconScale) !== null && _o !== void 0 ? _o : 1;
1061
1075
  const size = this.options.iconSize;
1062
- const mask = iconColor && iconBackground ? document.createElement("span") : icon;
1063
- if (mask !== icon) {
1064
- icon.appendChild(mask);
1065
- }
1066
- const target = iconColor ? mask : image;
1067
- if (iconColor) {
1068
- mask.style.display = "inline-block";
1069
- mask.style.backgroundColor = iconColor;
1070
- for (const prefix of ["", "-webkit-"]) {
1071
- mask.style.setProperty(`${prefix}mask-image`, `url(${JSON.stringify(this.options.iconUrl)})`);
1072
- mask.style.setProperty(`${prefix}mask-size`, "contain");
1073
- mask.style.setProperty(`${prefix}mask-repeat`, "no-repeat");
1074
- mask.style.setProperty(`${prefix}mask-position`, "center");
1075
- }
1076
+ const svgNs = "http://www.w3.org/2000/svg";
1077
+ const tint = iconColor ? document.createElementNS(svgNs, "svg") : null;
1078
+ const painted = tint ? document.createElementNS(svgNs, "image") : null;
1079
+ const inked = tint ? document.createElementNS(svgNs, "rect") : null;
1080
+ if (tint) {
1081
+ const maskId = `entity-label-tint-${++tintCounter}`;
1082
+ const mask = document.createElementNS(svgNs, "mask");
1083
+ mask.setAttribute("id", maskId);
1084
+ mask.setAttribute("maskUnits", "userSpaceOnUse");
1085
+ painted.setAttribute("href", this.options.iconUrl);
1086
+ painted.setAttribute("preserveAspectRatio", "xMidYMid meet");
1087
+ mask.appendChild(painted);
1088
+ inked.setAttribute("fill", iconColor);
1089
+ inked.setAttribute("mask", `url(#${maskId})`);
1090
+ tint.append(mask, inked);
1091
+ tint.style.display = "block";
1092
+ icon.style.display = "inline-flex";
1093
+ icon.appendChild(tint);
1076
1094
  }
1077
1095
  const resize = () => {
1078
1096
  if (this.disposed || this.counter !== counter) {
1079
1097
  return;
1080
1098
  }
1081
1099
  const intrinsic = this.options.iconScale != null && size == null;
1082
- target.style.width = `${(intrinsic ? image.naturalWidth || 24 : size || 24) * scale}px`;
1083
- target.style.height = `${(intrinsic ? image.naturalHeight || 24 : size || 24) * scale}px`;
1100
+ const width = (intrinsic ? image.naturalWidth || 24 : size || 24) * scale;
1101
+ const height = (intrinsic ? image.naturalHeight || 24 : size || 24) * scale;
1102
+ if (tint) {
1103
+ tint.setAttribute("viewBox", `0 0 ${width} ${height}`);
1104
+ for (const node of [tint, painted, inked]) {
1105
+ node.setAttribute("width", String(width));
1106
+ node.setAttribute("height", String(height));
1107
+ }
1108
+ }
1109
+ else {
1110
+ image.style.width = `${width}px`;
1111
+ image.style.height = `${height}px`;
1112
+ }
1084
1113
  this.viewer.scene.requestRender();
1085
1114
  };
1086
1115
  image.onload = resize;
1087
1116
  resize();
1088
1117
  image.src = this.options.iconUrl;
1089
- if (!iconColor) {
1118
+ if (!tint) {
1090
1119
  icon.appendChild(image);
1091
1120
  }
1092
1121
  }
@@ -1111,6 +1140,27 @@ var EntityLabel;
1111
1140
  icon.style.minWidth = `${size}px`;
1112
1141
  icon.style.minHeight = `${size}px`;
1113
1142
  icon.style.boxSizing = "content-box";
1143
+ let fitted = false;
1144
+ fitIconBackground = () => {
1145
+ if (fitted) {
1146
+ return;
1147
+ }
1148
+ const padding = size / 3;
1149
+ const width = icon.offsetWidth - padding * 2;
1150
+ const height = icon.offsetHeight - padding * 2;
1151
+ if (width <= 0 || height <= 0) {
1152
+ return;
1153
+ }
1154
+ fitted = true;
1155
+ const longest = Math.max(width, height);
1156
+ const difference = (longest - Math.min(width, height)) / longest;
1157
+ if (difference > ICON_BACKGROUND_UNIFORM_TOLERANCE) {
1158
+ return;
1159
+ }
1160
+ icon.style.minWidth = `${longest}px`;
1161
+ icon.style.minHeight = `${longest}px`;
1162
+ this.viewer.scene.requestRender();
1163
+ };
1114
1164
  }
1115
1165
  if (this.options.iconScale === 0) {
1116
1166
  icon.style.display = "none";
@@ -1149,8 +1199,10 @@ var EntityLabel;
1149
1199
  const countElement = document.createElement("span");
1150
1200
  countElement.className = "entity-label-count";
1151
1201
  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;";
1202
+ // Text above the icon would have the badge land in it, so the free corner is the lower one.
1203
+ const badgeBelow = presentation === "icon-text-above";
1152
1204
  if (iconContainer) {
1153
- countElement.style.cssText += "position:absolute;left:100%;top:0;transform:translate(-50%,-50%);";
1205
+ countElement.style.cssText += `position:absolute;left:100%;top:${badgeBelow ? "100%" : "0"};transform:translate(-50%,-50%);`;
1154
1206
  iconContainer.appendChild(countElement);
1155
1207
  }
1156
1208
  if (!layers.has(this.viewer)) {
@@ -1159,6 +1211,7 @@ var EntityLabel;
1159
1211
  const layer = layers.get(this.viewer);
1160
1212
  let ele = null;
1161
1213
  let lineEle = null;
1214
+ let runEle = null;
1162
1215
  if (this.label) {
1163
1216
  ele = this.label;
1164
1217
  }
@@ -1195,6 +1248,7 @@ var EntityLabel;
1195
1248
  }
1196
1249
  if (this.line) {
1197
1250
  lineEle = this.line;
1251
+ runEle = this.leaderRun;
1198
1252
  }
1199
1253
  else {
1200
1254
  lineEle = document.createElement("div");
@@ -1238,6 +1292,14 @@ var EntityLabel;
1238
1292
  layer.element.appendChild(lineEleNode);
1239
1293
  layer.element.appendChild(lineEle);
1240
1294
  }
1295
+ // Built here rather than beside the stem, since the style can change on a label that is reused.
1296
+ if (angled && !runEle) {
1297
+ runEle = lineEle.cloneNode(false);
1298
+ runEle.className = "entity-label-leader-run";
1299
+ runEle.style.display = "none";
1300
+ this.leaderRun = runEle;
1301
+ layer.element.appendChild(runEle);
1302
+ }
1241
1303
  let MAX_DISTANCE = Math.max(15000, sizeInM * 35);
1242
1304
  if (visual && visual instanceof Cesium.Entity) {
1243
1305
  // Consistent sized things.
@@ -1257,6 +1319,25 @@ var EntityLabel;
1257
1319
  y: upwards ? endY - height : endY, endY
1258
1320
  };
1259
1321
  };
1322
+ /*
1323
+ * An angled leader parks the label beside the anchor instead of over it,
1324
+ * so the stem rises to the label's middle and then runs out to whichever side has the room.
1325
+ */
1326
+ const fitAngledLeader = (anchorX, anchorY, desired, width, height, radius = 0) => {
1327
+ const canvas = this.viewer.canvas;
1328
+ const above = Math.max(0, anchorY - height / 2);
1329
+ const below = Math.max(0, canvas.clientHeight - anchorY - height / 2);
1330
+ const upwards = above >= Math.max(radius ? radius + 4 : 0, Math.min(8, desired)) || above >= below;
1331
+ const length = Math.min(desired, upwards ? above : below);
1332
+ const endY = anchorY + (upwards ? -length : length);
1333
+ const run = Math.max(ANGLED_LEADER_RUN, radius + 4);
1334
+ const rightwards = anchorX + run + width <= canvas.clientWidth || anchorX - run - width < 0;
1335
+ return {
1336
+ x: Math.max(0, Math.min(rightwards ? anchorX + run : anchorX - run - width, canvas.clientWidth - width)),
1337
+ y: Math.max(0, Math.min(endY - height / 2, canvas.clientHeight - height)), endY,
1338
+ run, side: (rightwards ? "right" : "left")
1339
+ };
1340
+ };
1260
1341
  let cursorMotion = this.cursorMotion;
1261
1342
  const project = () => {
1262
1343
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
@@ -1398,7 +1479,14 @@ var EntityLabel;
1398
1479
  y -= height / 2;
1399
1480
  }
1400
1481
  // Membership uses the unclamped source footprint, never the summary's changing dimensions.
1401
- if (leader)
1482
+ let leaderRun;
1483
+ let leaderSide;
1484
+ if (leader && angled) {
1485
+ const fit = fitAngledLeader(anchorX, anchorY, leaderLength, width, height);
1486
+ ({ x, y } = fit);
1487
+ ({ run: leaderRun, side: leaderSide } = fit);
1488
+ }
1489
+ else if (leader)
1402
1490
  ({ x, y } = fitLeader(anchorX, anchorY, leaderLength, width, height));
1403
1491
  const bounds = clusterBounds || { x, y, width, height };
1404
1492
  if (!leader && (bounds.x + bounds.width < 0 || bounds.y + bounds.height < 0 || bounds.x > this.viewer.canvas.clientWidth || bounds.y > this.viewer.canvas.clientHeight)) {
@@ -1413,20 +1501,21 @@ var EntityLabel;
1413
1501
  extendedGeometry: visual instanceof Cesium.Entity && Boolean(visual.polygon || visual.polyline || visual.corridor),
1414
1502
  viewportWidth: this.viewer.canvas.clientWidth,
1415
1503
  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,
1416
- entityId: (_t = this.rego) === null || _t === void 0 ? void 0 : _t.entityId, group: this.groupId, x, y, width, height, anchorX, anchorY, depth, leaderLength, distance,
1504
+ 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,
1417
1505
  // Priority shows the hovered member individually without dragging its cluster along.
1418
1506
  opacity, priority: hover || cursorAmount > 0 || (state === null || state === void 0 ? void 0 : state.highlighted) ? 1 : 0, options: this.options, text: originalText, clusterBounds
1419
1507
  };
1420
1508
  };
1421
1509
  let displacement = { x: 0, y: 0 };
1422
- let displayedSummaryMore = false;
1423
1510
  let nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
1424
1511
  const apply = (placement) => {
1425
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
1512
+ 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;
1426
1513
  const show = (placement === null || placement === void 0 ? void 0 : placement.visible) && placement.opacity > 0;
1427
1514
  clickEntityIds = show ? placement.entityIds : [];
1428
1515
  ele.style.visibility = show ? "visible" : "hidden";
1429
1516
  lineEle.style.display = show && leader ? "block" : "none";
1517
+ if (runEle)
1518
+ runEle.style.display = show && leader ? "block" : "none";
1430
1519
  this.anchor.style.display = show && leader ? "block" : "none";
1431
1520
  if (!show) {
1432
1521
  displacement = { x: 0, y: 0 };
@@ -1437,17 +1526,8 @@ var EntityLabel;
1437
1526
  return;
1438
1527
  }
1439
1528
  const displayText = (_a = placement.text) !== null && _a !== void 0 ? _a : originalText;
1440
- const summaryMore = Boolean(placement.summaryMore);
1441
- if (textElement && (textElement.textContent !== displayText + (summaryMore ? "\n…" : "") || displayedSummaryMore !== summaryMore)) {
1529
+ if (textElement && textElement.textContent !== displayText) {
1442
1530
  textElement.textContent = displayText;
1443
- if (summaryMore) {
1444
- const more = document.createElement("span");
1445
- more.className = "entity-label-more";
1446
- more.style.opacity = "0.65";
1447
- more.textContent = "\n…";
1448
- textElement.appendChild(more);
1449
- }
1450
- displayedSummaryMore = summaryMore;
1451
1531
  // Measure the summary's footprint next frame without replacing each member's source text.
1452
1532
  this.viewer.scene.requestRender();
1453
1533
  }
@@ -1465,6 +1545,7 @@ var EntityLabel;
1465
1545
  // The badge participates in layout, so its new width needs another measurement.
1466
1546
  this.viewer.scene.requestRender();
1467
1547
  }
1548
+ fitIconBackground === null || fitIconBackground === void 0 ? void 0 : fitIconBackground();
1468
1549
  if (iconContainer) {
1469
1550
  const badgeWidth = countElement.offsetWidth;
1470
1551
  const iconWidth = iconElement.offsetWidth;
@@ -1472,7 +1553,9 @@ var EntityLabel;
1472
1553
  const clearance = badgeWidth ? (narrow ? badgeWidth / 2 + 2 : Math.max(0, badgeWidth / 2 - iconWidth / 4)) : 0;
1473
1554
  countElement.style.left = `calc(100% + ${clearance}px)`;
1474
1555
  // Reserve the badge overhang so adjacent text and viewport edges remain clear.
1475
- iconContainer.style.marginTop = `${countElement.offsetHeight / 2}px`;
1556
+ const overhang = countElement.offsetHeight / 2;
1557
+ iconContainer.style.marginTop = badgeBelow ? "0px" : `${overhang}px`;
1558
+ iconContainer.style.marginBottom = badgeBelow ? `${overhang}px` : "0px";
1476
1559
  iconContainer.style.marginRight = `${badgeWidth / 2 + clearance}px`;
1477
1560
  }
1478
1561
  // Summary and badge changes must fit on their first visible frame, including after resize.
@@ -1516,19 +1599,26 @@ var EntityLabel;
1516
1599
  sharedRing.animation = ringAnimation;
1517
1600
  }
1518
1601
  const radius = ringAnimation.inner > 0 ? ringAnimation.outer : 0;
1519
- 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);
1602
+ 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);
1520
1603
  const position = { x: (stem || frame).x, y: (stem || frame).y };
1521
1604
  const width = ele.offsetWidth / coordinates.scaleX;
1522
1605
  const height = ele.offsetHeight / coordinates.scaleY;
1523
- if (leader && placement.textSide === "left") {
1606
+ if (leader && angled) {
1607
+ // The declutter owns the side; the fit only says which one had the room to begin with.
1608
+ const run = Math.max(ANGLED_LEADER_RUN, radius + 4);
1609
+ const side = (_l = (_k = placement.textSide) !== null && _k !== void 0 ? _k : frame.leaderSide) !== null && _l !== void 0 ? _l : "right";
1610
+ const beside = side === "left" ? frame.anchorX - run - width : frame.anchorX + run;
1611
+ position.x = Math.max(0, Math.min(beside, this.viewer.canvas.clientWidth - width));
1612
+ }
1613
+ else if (leader && placement.textSide === "left") {
1524
1614
  position.x = Math.max(0, Math.min(frame.anchorX - width, this.viewer.canvas.clientWidth - width));
1525
1615
  }
1526
- const gap = (_l = (_k = this.options.pointerAvoidance) === null || _k === void 0 ? void 0 : _k.gap) !== null && _l !== void 0 ? _l : 12;
1616
+ const gap = (_o = (_m = this.options.pointerAvoidance) === null || _m === void 0 ? void 0 : _m.gap) !== null && _o !== void 0 ? _o : 12;
1527
1617
  const containsPointer = (x, y, margin) => {
1528
1618
  return pointer && pointer.x >= x - margin && pointer.x <= x + width + margin && pointer.y >= y - margin && pointer.y <= y + height + margin;
1529
1619
  };
1530
1620
  const passive = !selectOnClick || !this.visualRegister;
1531
- if (passive && ((_m = this.options.pointerAvoidance) === null || _m === void 0 ? void 0 : _m.enabled) !== false && pointer &&
1621
+ if (passive && ((_p = this.options.pointerAvoidance) === null || _p === void 0 ? void 0 : _p.enabled) !== false && pointer &&
1532
1622
  (containsPointer(position.x, position.y, gap) || containsPointer(position.x + displacement.x, position.y + displacement.y, gap))) {
1533
1623
  if (containsPointer(position.x + displacement.x, position.y + displacement.y, 0)) {
1534
1624
  // Choose against the original box and retain the offset until the pointer leaves both boxes.
@@ -1546,7 +1636,7 @@ var EntityLabel;
1546
1636
  else {
1547
1637
  displacement = { x: 0, y: 0 };
1548
1638
  }
1549
- const nudgeDuration = (_p = (_o = this.options.pointerAvoidance) === null || _o === void 0 ? void 0 : _o.transitionDuration) !== null && _p !== void 0 ? _p : 200;
1639
+ const nudgeDuration = (_r = (_q = this.options.pointerAvoidance) === null || _q === void 0 ? void 0 : _q.transitionDuration) !== null && _r !== void 0 ? _r : 200;
1550
1640
  const animateNudge = () => {
1551
1641
  const progress = now < nudge.start ? 0 : nudgeDuration > 0 ? Math.min(1, (now - nudge.start) / nudgeDuration) : 1;
1552
1642
  const eased = progress * progress * (3 - 2 * progress);
@@ -1562,7 +1652,7 @@ var EntityLabel;
1562
1652
  fromY: nudge.y,
1563
1653
  targetX: displacement.x,
1564
1654
  targetY: displacement.y,
1565
- start: now + ((_r = (_q = this.options.pointerAvoidance) === null || _q === void 0 ? void 0 : _q.delay) !== null && _r !== void 0 ? _r : 180)
1655
+ start: now + ((_t = (_s = this.options.pointerAvoidance) === null || _s === void 0 ? void 0 : _s.delay) !== null && _t !== void 0 ? _t : 180)
1566
1656
  };
1567
1657
  }
1568
1658
  if (animateNudge() < 1 && (nudge.fromX !== nudge.targetX || nudge.fromY !== nudge.targetY)) {
@@ -1574,7 +1664,7 @@ var EntityLabel;
1574
1664
  ele.style.top = `${coordinates.y + position.y * coordinates.scaleY}px`;
1575
1665
  const clickTarget = content.querySelector('[role="button"]');
1576
1666
  if (clickTarget) {
1577
- 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}`);
1667
+ 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}`);
1578
1668
  }
1579
1669
  if (leader) {
1580
1670
  const nodeRadius = ringAnimation.outer;
@@ -1597,8 +1687,13 @@ var EntityLabel;
1597
1687
  anchor.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount));
1598
1688
  anchor.style.zIndex = String(placement.zIndex);
1599
1689
  // The stem starts at the rim, never across the transparent centre, even after passive displacement.
1600
- const endX = Math.max(position.x, Math.min(frame.anchorX, position.x + width));
1601
- const endY = frame.anchorY < position.y ? position.y : frame.anchorY > position.y + height ? position.y + height : frame.anchorY;
1690
+ const straightX = Math.max(position.x, Math.min(frame.anchorX, position.x + width));
1691
+ const straightY = frame.anchorY < position.y ? position.y : frame.anchorY > position.y + height ? position.y + height : frame.anchorY;
1692
+ const beside = angled && (frame.anchorX < position.x || frame.anchorX > position.x + width);
1693
+ const elbowY = position.y + height / 2;
1694
+ const endX = beside ? frame.anchorX : straightX;
1695
+ const endY = beside ? elbowY : straightY;
1696
+ const runToX = frame.anchorX < position.x ? position.x : position.x + width;
1602
1697
  const dx = endX - frame.anchorX, dy = endY - frame.anchorY;
1603
1698
  const length = Math.hypot(dx, dy);
1604
1699
  const leaderAngle = Math.atan2(dy, dx);
@@ -1609,13 +1704,13 @@ var EntityLabel;
1609
1704
  // Keep the stem inside the 126-degree arc, away from antialiased endpoints.
1610
1705
  const arcFraction = 0.35;
1611
1706
  const rotationLimit = Math.PI * arcFraction - Math.PI / 30;
1612
- const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_v = ringAnimation.arcOffset) !== null && _v !== void 0 ? _v : 0);
1707
+ const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_x = ringAnimation.arcOffset) !== null && _x !== void 0 ? _x : 0);
1613
1708
  const targetOpen = cursorMotion.target || (hovered && (ring === null || ring === void 0 ? void 0 : ring.enabled) !== false && targetRadius > 0) ? 1 : 0;
1614
- const elapsed = Math.min(32, Math.max(0, now - ((_w = ringAnimation.arcTime) !== null && _w !== void 0 ? _w : now)));
1709
+ const elapsed = Math.min(32, Math.max(0, now - ((_y = ringAnimation.arcTime) !== null && _y !== void 0 ? _y : now)));
1615
1710
  const blend = duration > 0 ? 1 - Math.exp(-elapsed / (duration / 3)) : 1;
1616
1711
  const approach = (value, target) => Math.abs(value - target) < 0.001 ? target : value + (target - value) * blend;
1617
- ringAnimation.arcOffset = approach((_x = ringAnimation.arcOffset) !== null && _x !== void 0 ? _x : 0, targetOffset);
1618
- ringAnimation.arcOpen = approach((_y = ringAnimation.arcOpen) !== null && _y !== void 0 ? _y : 0, targetOpen);
1712
+ ringAnimation.arcOffset = approach((_z = ringAnimation.arcOffset) !== null && _z !== void 0 ? _z : 0, targetOffset);
1713
+ ringAnimation.arcOpen = approach((_0 = ringAnimation.arcOpen) !== null && _0 !== void 0 ? _0 : 0, targetOpen);
1619
1714
  ringAnimation.arcTime = now;
1620
1715
  if (ringAnimation.arcOffset !== targetOffset || ringAnimation.arcOpen !== targetOpen) {
1621
1716
  this.viewer.scene.requestRender();
@@ -1627,17 +1722,27 @@ var EntityLabel;
1627
1722
  const cut = Math.min(radius, length);
1628
1723
  const startX = frame.anchorX + (length ? dx / length * cut : 0);
1629
1724
  const startY = frame.anchorY + (length ? dy / length * cut : 0);
1630
- const screenDx = (endX - startX) * coordinates.scaleX;
1631
- const screenDy = (endY - startY) * coordinates.scaleY;
1632
- lineEle.style.width = `${2 * coordinates.scaleX}px`;
1633
- lineEle.style.left = `${coordinates.x + (startX - 1) * coordinates.scaleX}px`;
1634
- lineEle.style.top = `${coordinates.y + startY * coordinates.scaleY}px`;
1635
- lineEle.style.height = `${Math.hypot(screenDx, screenDy)}px`;
1636
- lineEle.style.transform = `rotate(${Math.atan2(screenDy, screenDx) - Math.PI / 2}rad)`;
1637
- lineEle.style.boxShadow = radius ? "none" : "0 0 5px rgba(0, 0, 0, 0.3)";
1638
- lineEle.style.transformOrigin = "top center";
1639
- lineEle.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount));
1640
- lineEle.style.zIndex = String(placement.zIndex);
1725
+ const draw = (element, fromX, fromY, toX, toY) => {
1726
+ const screenDx = (toX - fromX) * coordinates.scaleX;
1727
+ const screenDy = (toY - fromY) * coordinates.scaleY;
1728
+ element.style.width = `${2 * coordinates.scaleX}px`;
1729
+ element.style.left = `${coordinates.x + (fromX - 1) * coordinates.scaleX}px`;
1730
+ element.style.top = `${coordinates.y + fromY * coordinates.scaleY}px`;
1731
+ element.style.height = `${Math.hypot(screenDx, screenDy)}px`;
1732
+ element.style.transform = `rotate(${Math.atan2(screenDy, screenDx) - Math.PI / 2}rad)`;
1733
+ element.style.boxShadow = radius ? "none" : "0 0 5px rgba(0, 0, 0, 0.3)";
1734
+ element.style.transformOrigin = "top center";
1735
+ element.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount));
1736
+ element.style.zIndex = String(placement.zIndex);
1737
+ };
1738
+ draw(lineEle, startX, startY, endX, endY);
1739
+ if (runEle) {
1740
+ // Overlap the corner by the stem's own width so the elbow has no notch in it.
1741
+ const overlap = frame.anchorX < runToX ? -1 : 1;
1742
+ runEle.style.display = beside ? "block" : "none";
1743
+ if (beside)
1744
+ draw(runEle, endX + overlap, endY, runToX, endY);
1745
+ }
1641
1746
  }
1642
1747
  };
1643
1748
  ele.style.display = "flex";
@@ -1662,7 +1767,7 @@ var EntityLabel;
1662
1767
  * Removes label from the viewer and unsubscribes from events.
1663
1768
  */
1664
1769
  removeLabel() {
1665
- var _a;
1770
+ var _a, _b;
1666
1771
  (_a = this.anchor) === null || _a === void 0 ? void 0 : _a.remove();
1667
1772
  this.anchor = null;
1668
1773
  if (this.label &&
@@ -1674,6 +1779,8 @@ var EntityLabel;
1674
1779
  this.line.parentElement.removeChild(this.line);
1675
1780
  this.line = null;
1676
1781
  }
1782
+ (_b = this.leaderRun) === null || _b === void 0 ? void 0 : _b.remove();
1783
+ this.leaderRun = null;
1677
1784
  if (this.renderRemoval) {
1678
1785
  this.renderRemoval();
1679
1786
  this.renderRemoval = null;