bruce-cesium 7.3.9 → 7.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/bruce-cesium.es5.js +2655 -603
  2. package/dist/bruce-cesium.es5.js.map +1 -1
  3. package/dist/bruce-cesium.umd.js +2654 -600
  4. package/dist/bruce-cesium.umd.js.map +1 -1
  5. package/dist/lib/bruce-cesium.js +8 -2
  6. package/dist/lib/bruce-cesium.js.map +1 -1
  7. package/dist/lib/rendering/render-managers/common/entity-label-group.js +538 -0
  8. package/dist/lib/rendering/render-managers/common/entity-label-group.js.map +1 -0
  9. package/dist/lib/rendering/render-managers/common/entity-label.js +1213 -155
  10. package/dist/lib/rendering/render-managers/common/entity-label.js.map +1 -1
  11. package/dist/lib/rendering/render-managers/common/point-clustering-core.js +85 -0
  12. package/dist/lib/rendering/render-managers/common/point-clustering-core.js.map +1 -0
  13. package/dist/lib/rendering/render-managers/common/point-clustering.js +566 -419
  14. package/dist/lib/rendering/render-managers/common/point-clustering.js.map +1 -1
  15. package/dist/lib/rendering/render-managers/entities/entities-ids-render-manager.js +1 -1
  16. package/dist/lib/rendering/render-managers/entities/entities-ids-render-manager.js.map +1 -1
  17. package/dist/lib/rendering/render-managers/entities/entities-render-manager.js +2 -2
  18. package/dist/lib/rendering/render-managers/entities/entities-render-manager.js.map +1 -1
  19. package/dist/lib/rendering/visuals-register.js +273 -24
  20. package/dist/lib/rendering/visuals-register.js.map +1 -1
  21. package/dist/types/bruce-cesium.d.ts +4 -1
  22. package/dist/types/rendering/render-managers/common/entity-label-group.d.ts +109 -0
  23. package/dist/types/rendering/render-managers/common/entity-label.d.ts +138 -1
  24. package/dist/types/rendering/render-managers/common/point-clustering-core.d.ts +17 -0
  25. package/dist/types/rendering/render-managers/common/point-clustering.d.ts +37 -72
  26. package/dist/types/rendering/visuals-register.d.ts +43 -2
  27. package/package.json +1 -1
@@ -8,6 +8,7 @@ const js_utils_2 = require("../../../internal/js-utils");
8
8
  const drawing_utils_1 = require("../../../utils/drawing-utils");
9
9
  const measure_utils_1 = require("../../../utils/measure-utils");
10
10
  const bruce_models_1 = require("bruce-models");
11
+ const point_clustering_core_1 = require("./point-clustering-core");
11
12
  /**
12
13
  * Returns cesium property's value.
13
14
  * This will check if it's one that changes over time, or just a fixed value.
@@ -30,10 +31,565 @@ function getValue(viewer, obj) {
30
31
  */
31
32
  var EntityLabel;
32
33
  (function (EntityLabel) {
34
+ const ICON_BACKGROUND_BLEND = 0.8;
35
+ const ICON_BACKGROUND_ALPHA = 0.45;
36
+ const TEXT_MAX_WIDTH = 200;
33
37
  // Key where the label instance is stored within the cesium entity.
34
38
  // Eg: rego[ATTACH_KEY] = new EntityLabel(...);
35
39
  EntityLabel.ATTACH_KEY = "nextspace-navigator-entity-label";
40
+ /*
41
+ * Indexes screen rectangles so sparse scenes do not compare every label with every other label.
42
+ */
43
+ class ScreenIndex {
44
+ constructor() {
45
+ this.cells = new Map();
46
+ this.large = [];
47
+ this.all = [];
48
+ }
49
+ keys(frame, padding = 0) {
50
+ const x0 = Math.floor((frame.x - padding) / 64);
51
+ const y0 = Math.floor((frame.y - padding) / 64);
52
+ const x1 = Math.floor((frame.x + frame.width + padding) / 64);
53
+ const y1 = Math.floor((frame.y + frame.height + padding) / 64);
54
+ if ((x1 - x0 + 1) * (y1 - y0 + 1) > 4096) {
55
+ return null;
56
+ }
57
+ const keys = [];
58
+ for (let x = x0; x <= x1; x++) {
59
+ for (let y = y0; y <= y1; y++) {
60
+ keys.push(`${x},${y}`);
61
+ }
62
+ }
63
+ return keys;
64
+ }
65
+ Add(frame) {
66
+ this.all.push(frame);
67
+ const keys = this.keys(frame);
68
+ if (!keys) {
69
+ this.large.push(frame);
70
+ return;
71
+ }
72
+ for (const key of keys) {
73
+ if (!this.cells.has(key)) {
74
+ this.cells.set(key, []);
75
+ }
76
+ this.cells.get(key).push(frame);
77
+ }
78
+ }
79
+ Query(frame, padding = 0) {
80
+ const keys = this.keys(frame, padding);
81
+ const found = new Set(keys ? this.large : this.all);
82
+ for (const key of keys || []) {
83
+ for (const item of this.cells.get(key) || []) {
84
+ found.add(item);
85
+ }
86
+ }
87
+ return (Array.from(found).filter(other => frame.x <= other.x + other.width + padding && frame.x + frame.width + padding >= other.x &&
88
+ frame.y <= other.y + other.height + padding && frame.y + frame.height + padding >= other.y));
89
+ }
90
+ HasOverlap(frame) {
91
+ var _a;
92
+ const intersects = (other) => (frame.x <= other.x + other.width && frame.x + frame.width >= other.x &&
93
+ frame.y <= other.y + other.height && frame.y + frame.height >= other.y);
94
+ const keys = this.keys(frame);
95
+ if ((keys ? this.large : this.all).some(intersects)) {
96
+ return true;
97
+ }
98
+ for (const key of keys || []) {
99
+ if ((_a = this.cells.get(key)) === null || _a === void 0 ? void 0 : _a.some(intersects)) {
100
+ return true;
101
+ }
102
+ }
103
+ return false;
104
+ }
105
+ }
106
+ /*
107
+ * Includes the stem in collision checks without treating two crossing stems as text occlusion.
108
+ */
109
+ function leaderBounds(frame) {
110
+ if (!frame.hasLeader || !Number.isFinite(frame.anchorX) || !Number.isFinite(frame.anchorY)) {
111
+ return undefined;
112
+ }
113
+ const endX = Math.max(frame.x, Math.min(frame.anchorX, frame.x + frame.width));
114
+ const endY = Math.max(frame.y, Math.min(frame.anchorY, frame.y + frame.height));
115
+ if (Math.hypot(frame.anchorX - endX, frame.anchorY - endY) < .01) {
116
+ return undefined;
117
+ }
118
+ return {
119
+ ...frame,
120
+ x: Math.min(frame.anchorX, endX) - 1,
121
+ y: Math.min(frame.anchorY, endY) + .01,
122
+ width: Math.abs(frame.anchorX - endX) + 2,
123
+ height: Math.max(0, Math.abs(frame.anchorY - endY) - .02)
124
+ };
125
+ }
126
+ /*
127
+ * Remembers sides, never rendered positions: its own output cannot trigger another layout pass.
128
+ */
129
+ class PlacementMemory {
130
+ constructor() {
131
+ this.previous = new Map();
132
+ this.nextOrder = 0;
133
+ }
134
+ Resolve(frames) {
135
+ var _a, _b;
136
+ const live = new Set(frames.map(frame => frame.id));
137
+ const moved = (frame) => {
138
+ var _a;
139
+ const old = (_a = this.previous.get(frame.id)) === null || _a === void 0 ? void 0 : _a.frame;
140
+ return (!old ||
141
+ old.transient !== frame.transient || old.canFlip !== frame.canFlip ||
142
+ Math.hypot(frame.x - old.x, frame.y - old.y) >= 24 ||
143
+ Math.abs(frame.width - old.width) >= 8 || Math.abs(frame.height - old.height) >= 8);
144
+ };
145
+ const persistent = frames.filter(frame => !frame.transient);
146
+ const changes = (persistent.filter(moved).length +
147
+ Array.from(this.previous.values()).filter(old => !old.frame.transient && !live.has(old.frame.id)).length);
148
+ // A hover or one arrival cannot rearrange incumbents; a substantial batch/camera change can.
149
+ const restructure = this.previous.size > 0 && changes >= Math.max(2, Math.ceil(persistent.length * 0.3));
150
+ const fixed = (frame) => !restructure && !frame.transient && this.previous.has(frame.id) && !moved(frame);
151
+ const ordered = ([...frames].sort((a, b) => {
152
+ var _a, _b, _c, _d;
153
+ return Number(fixed(b)) - Number(fixed(a)) ||
154
+ Number(Boolean(a.transient)) - Number(Boolean(b.transient)) ||
155
+ ((_b = (_a = this.previous.get(a.id)) === null || _a === void 0 ? void 0 : _a.order) !== null && _b !== void 0 ? _b : Infinity) - ((_d = (_c = this.previous.get(b.id)) === null || _c === void 0 ? void 0 : _c.order) !== null && _d !== void 0 ? _d : Infinity) ||
156
+ a.depth - b.depth || a.id.localeCompare(b.id);
157
+ }));
158
+ const text = new ScreenIndex(), leaders = new ScreenIndex();
159
+ const placed = new Map();
160
+ const next = new Map();
161
+ 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);
166
+ };
167
+ const score = (frame, padding = 0) => {
168
+ const box = {
169
+ ...frame,
170
+ x: frame.x - padding + .01,
171
+ y: frame.y - padding + .01,
172
+ width: Math.max(0, frame.width + padding * 2 - .02),
173
+ height: Math.max(0, frame.height + padding * 2 - .02)
174
+ };
175
+ const stem = leaderBounds(frame);
176
+ return (Number(text.HasOverlap(box)) * 4 + Number(leaders.HasOverlap(box)) +
177
+ Number(Boolean(stem && text.HasOverlap(stem))));
178
+ };
179
+ for (const frame of ordered) {
180
+ 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";
182
+ let chosen = candidate(frame, side);
183
+ if (!fixed(frame) && frame.canFlip) {
184
+ const otherSide = side === "left" ? "right" : "left";
185
+ const other = candidate(frame, otherSide);
186
+ // An established side only changes when the alternative has a little breathing room.
187
+ if (score(other, old ? 4 : 0) < score(chosen)) {
188
+ side = otherSide;
189
+ chosen = other;
190
+ }
191
+ }
192
+ 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++ });
194
+ text.Add(chosen);
195
+ const stem = leaderBounds(chosen);
196
+ if (stem) {
197
+ leaders.Add(stem);
198
+ }
199
+ }
200
+ this.previous = next;
201
+ return placed;
202
+ }
203
+ }
204
+ EntityLabel.PlacementMemory = PlacementMemory;
205
+ /*
206
+ * Prefer declared semantics; only infers from recognisable glyphs and filename tokens, never image pixels.
207
+ */
208
+ EntityLabel.IconPriority = point_clustering_core_1.GetClusterIconPriority;
209
+ /*
210
+ * Resolves clustering and stable text placement before depth fading; highlighted labels receive priority.
211
+ */
212
+ function Layout(input, memory = new PlacementMemory()) {
213
+ var _a, _b, _c, _d, _e, _f, _g, _h;
214
+ const frames = input.filter(frame => [frame.x, frame.y, frame.width, frame.height, frame.depth].every(Number.isFinite))
215
+ .sort((a, b) => (b.priority || 0) - (a.priority || 0) || a.depth - b.depth || a.id.localeCompare(b.id));
216
+ const result = new Map();
217
+ const entityKeys = new Set();
218
+ const groups = new Map();
219
+ for (const frame of frames) {
220
+ const duplicate = frame.entityKey && entityKeys.has(frame.entityKey);
221
+ result.set(frame.id, { frame, visible: !duplicate, opacity: frame.opacity, zIndex: 0, members: [], entityIds: frame.entityId ? [frame.entityId] : [] });
222
+ if (duplicate) {
223
+ continue;
224
+ }
225
+ if (frame.entityKey)
226
+ entityKeys.add(frame.entityKey);
227
+ if (!((_b = (_a = frame.options) === null || _a === void 0 ? void 0 : _a.cluster) === null || _b === void 0 ? void 0 : _b.enabled) || frame.priority > 0) {
228
+ continue;
229
+ }
230
+ const key = JSON.stringify([frame.group || "", frame.options.presentation || "text"]);
231
+ if (!groups.has(key)) {
232
+ groups.set(key, []);
233
+ }
234
+ groups.get(key).push(frame);
235
+ }
236
+ for (const group of groups.values()) {
237
+ // Summary and badge dimensions are output, never the next frame's membership input.
238
+ const candidates = group.map(frame => frame.extendedGeometry && frame.canFlip &&
239
+ frame.options.cluster.pixelRange == null ? { ...frame, x: frame.anchorX, y: frame.anchorY, width: 1, height: 1 } :
240
+ frame.clusterBounds ? { ...frame, ...frame.clusterBounds } : frame);
241
+ const index = new ScreenIndex();
242
+ for (const frame of candidates) {
243
+ index.Add(frame);
244
+ }
245
+ const clusters = (0, point_clustering_core_1.ClusterItems)({
246
+ items: candidates,
247
+ id: frame => frame.id,
248
+ minSize: group[0].options.cluster.minSize,
249
+ neighbours: frame => {
250
+ var _a;
251
+ return (index.Query(frame, (_a = frame.options.cluster.pixelRange) !== null && _a !== void 0 ? _a : (frame.options.presentation === "icon" ? 24 : 8)));
252
+ }
253
+ });
254
+ for (const cluster of clusters) {
255
+ let chosen = cluster.representative;
256
+ if ((chosen.options.presentation || "text") !== "text" && chosen.options.cluster.representative !== "nearest") {
257
+ let priority = EntityLabel.IconPriority(chosen.options);
258
+ for (const member of cluster.members) {
259
+ const candidate = EntityLabel.IconPriority(member.options);
260
+ if (candidate > priority) {
261
+ chosen = member;
262
+ priority = candidate;
263
+ }
264
+ }
265
+ }
266
+ const representative = result.get(chosen.id);
267
+ representative.members = cluster.members.map(frame => frame.id);
268
+ representative.entityIds = Array.from(new Set(cluster.members.map(frame => frame.entityId).filter(Boolean)));
269
+ if (chosen.options.presentation !== "icon") {
270
+ const values = new Set();
271
+ const stableMembers = [...cluster.members].sort((a, b) => (a.entityKey || a.entityId || a.id).localeCompare(b.entityKey || b.entityId || b.id));
272
+ for (const member of stableMembers) {
273
+ const value = ((_e = (_c = member.text) !== null && _c !== void 0 ? _c : (_d = member.options) === null || _d === void 0 ? void 0 : _d.text) !== null && _e !== void 0 ? _e : "").replace(/\s+/g, " ").trim();
274
+ if (value) {
275
+ values.add(value);
276
+ }
277
+ }
278
+ 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
+ }
290
+ }
291
+ }
292
+ for (const frame of cluster.members) {
293
+ if (frame !== chosen)
294
+ result.get(frame.id).visible = false;
295
+ }
296
+ }
297
+ }
298
+ const placed = memory.Resolve(frames.filter(frame => result.get(frame.id).visible));
299
+ const foreground = new ScreenIndex(), foregroundLeaders = new ScreenIndex();
300
+ let zIndex = frames.length + 1;
301
+ for (const original of frames) {
302
+ const placement = result.get(original.id);
303
+ if (!placement.visible) {
304
+ continue;
305
+ }
306
+ const chosen = placed.get(original.id);
307
+ const frame = placement.frame = chosen.frame;
308
+ placement.textSide = chosen.side;
309
+ const stem = leaderBounds(frame);
310
+ 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
+ const overlap = foreground.HasOverlap(box) || foregroundLeaders.HasOverlap(box) || Boolean(stem && foreground.HasOverlap(stem));
312
+ 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));
314
+ }
315
+ ;
316
+ placement.zIndex = zIndex--;
317
+ foreground.Add(frame);
318
+ if (stem) {
319
+ foregroundLeaders.Add(stem);
320
+ }
321
+ }
322
+ return Array.from(result.values());
323
+ }
324
+ EntityLabel.Layout = Layout;
325
+ const layers = new WeakMap();
326
+ const containers = new WeakMap();
327
+ /*
328
+ * Moves the shared overlay without recreating labels or resolving their sources.
329
+ */
330
+ function SetContainer(viewer, container) {
331
+ const layer = layers.get(viewer);
332
+ if (container && (container === (layer === null || layer === void 0 ? void 0 : layer.element) || (layer === null || layer === void 0 ? void 0 : layer.element.contains(container)))) {
333
+ throw new Error("Label container cannot be inside the label overlay");
334
+ }
335
+ if (container) {
336
+ containers.set(viewer, container);
337
+ }
338
+ else
339
+ containers.delete(viewer);
340
+ layer === null || layer === void 0 ? void 0 : layer.Move();
341
+ }
342
+ EntityLabel.SetContainer = SetContainer;
343
+ /*
344
+ * Owns one scene callback per viewer and separates projection/measurement from DOM writes.
345
+ */
346
+ class Layer {
347
+ constructor(viewer) {
348
+ this.viewer = viewer;
349
+ this.element = document.createElement("div");
350
+ this.coordinates = { x: 0, y: 0, scaleX: 1, scaleY: 1 };
351
+ this.participants = new Set();
352
+ this.placementMemory = new PlacementMemory();
353
+ this.forwardingWheel = false;
354
+ this.pendingDrag = null;
355
+ this.TrackPointer = (event) => {
356
+ var _a, _b;
357
+ if (event.pointerType && event.pointerType !== "mouse" && event.pointerType !== "pen")
358
+ return;
359
+ const rect = (_b = (_a = this.viewer.canvas).getBoundingClientRect) === null || _b === void 0 ? void 0 : _b.call(_a);
360
+ if (!rect || event.clientX < rect.left || event.clientX >= rect.right || event.clientY < rect.top || event.clientY >= rect.bottom) {
361
+ if (this.pointerClient)
362
+ this.ClearPointer();
363
+ return;
364
+ }
365
+ this.pointerClient = { x: event.clientX, y: event.clientY };
366
+ this.viewer.scene.requestRender();
367
+ };
368
+ this.PointerOut = (event) => { if (!event.relatedTarget)
369
+ this.ClearPointer(); };
370
+ this.ClearPointer = () => {
371
+ this.pointerClient = undefined;
372
+ this.pointer = undefined;
373
+ if (!this.viewer.isDestroyed()) {
374
+ this.viewer.scene.requestRender();
375
+ }
376
+ };
377
+ this.PreventNativeDrag = (event) => {
378
+ event.preventDefault();
379
+ };
380
+ this.BeginDrag = (event) => {
381
+ var _a, _b, _c;
382
+ this.EndPendingDrag();
383
+ const detection = Cesium.FeatureDetection;
384
+ const target = (_b = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest) === null || _b === void 0 ? void 0 : _b.call(_a, '.entity-label [role="button"]');
385
+ const canvas = this.viewer.canvas;
386
+ if (event.defaultPrevented || event.button !== 0 || event.pointerType !== "mouse" ||
387
+ ((_c = detection.supportsPointerEvents) === null || _c === void 0 ? void 0 : _c.call(detection)) === false || !canvas.setPointerCapture || this.viewer.isDestroyed() ||
388
+ !target || !this.element.contains(target) ||
389
+ this.element.ownerDocument.defaultView.getComputedStyle(target).visibility !== "visible") {
390
+ return;
391
+ }
392
+ const bounds = canvas.getBoundingClientRect();
393
+ if (event.clientX < bounds.left || event.clientX >= bounds.right || event.clientY < bounds.top || event.clientY >= bounds.bottom) {
394
+ return;
395
+ }
396
+ this.pendingDrag = event;
397
+ const doc = this.element.ownerDocument;
398
+ doc.addEventListener("pointermove", this.ContinueDrag, true);
399
+ doc.addEventListener("pointerup", this.FinishPendingDrag, true);
400
+ doc.addEventListener("pointercancel", this.FinishPendingDrag, true);
401
+ doc.defaultView.addEventListener("blur", this.EndPendingDrag);
402
+ };
403
+ this.FinishPendingDrag = (event) => {
404
+ var _a;
405
+ if (event.pointerId === ((_a = this.pendingDrag) === null || _a === void 0 ? void 0 : _a.pointerId)) {
406
+ this.EndPendingDrag();
407
+ }
408
+ };
409
+ this.EndPendingDrag = () => {
410
+ this.pendingDrag = null;
411
+ const doc = this.element.ownerDocument;
412
+ doc.removeEventListener("pointermove", this.ContinueDrag, true);
413
+ doc.removeEventListener("pointerup", this.FinishPendingDrag, true);
414
+ doc.removeEventListener("pointercancel", this.FinishPendingDrag, true);
415
+ doc.defaultView.removeEventListener("blur", this.EndPendingDrag);
416
+ };
417
+ this.ContinueDrag = (event) => {
418
+ const start = this.pendingDrag;
419
+ if (!start || event.pointerId !== start.pointerId) {
420
+ return;
421
+ }
422
+ if (!(event.buttons & 1) || this.viewer.isDestroyed() || !this.element.contains(start.target)) {
423
+ this.EndPendingDrag();
424
+ return;
425
+ }
426
+ // Keep ordinary clicks local; only a deliberate drag starts Cesium's camera gesture.
427
+ if (Math.hypot(event.clientX - start.clientX, event.clientY - start.clientY) < 6) {
428
+ return;
429
+ }
430
+ this.EndPendingDrag();
431
+ const canvas = this.viewer.canvas;
432
+ try {
433
+ canvas.setPointerCapture(start.pointerId);
434
+ }
435
+ catch {
436
+ // A cancelled pointer or unsupported capture still cannot start native image dragging.
437
+ return;
438
+ }
439
+ const win = this.element.ownerDocument.defaultView;
440
+ const copy = (source) => ({
441
+ bubbles: true, cancelable: true, pointerId: source.pointerId, pointerType: source.pointerType,
442
+ isPrimary: source.isPrimary, button: source.button, buttons: source.buttons,
443
+ clientX: source.clientX, clientY: source.clientY, screenX: source.screenX, screenY: source.screenY,
444
+ ctrlKey: source.ctrlKey, shiftKey: source.shiftKey, altKey: source.altKey, metaKey: source.metaKey
445
+ });
446
+ // Capture routes subsequent real move/up events to Cesium, including releases outside the label.
447
+ canvas.dispatchEvent(new win.PointerEvent("pointerdown", copy(start)));
448
+ canvas.dispatchEvent(new win.PointerEvent("pointermove", copy(event)));
449
+ event.preventDefault();
450
+ event.stopPropagation();
451
+ this.viewer.scene.requestRender();
452
+ };
453
+ this.ForwardWheel = (event) => {
454
+ var _a, _b, _c;
455
+ if (this.forwardingWheel || event.defaultPrevented || this.viewer.isDestroyed()) {
456
+ return;
457
+ }
458
+ const target = (_b = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest) === null || _b === void 0 ? void 0 : _b.call(_a, '.entity-label [role="button"]');
459
+ const canvas = this.viewer.canvas;
460
+ const win = this.element.ownerDocument.defaultView;
461
+ if (!target || !this.element.contains(target) || win.getComputedStyle(target).visibility !== "visible") {
462
+ return;
463
+ }
464
+ const bounds = canvas.getBoundingClientRect();
465
+ if (event.clientX < bounds.left || event.clientX >= bounds.right || event.clientY < bounds.top || event.clientY >= bounds.bottom) {
466
+ return;
467
+ }
468
+ const position = {
469
+ bubbles: true, cancelable: true, clientX: event.clientX, clientY: event.clientY,
470
+ screenX: event.screenX, screenY: event.screenY, buttons: event.buttons,
471
+ ctrlKey: event.ctrlKey, shiftKey: event.shiftKey, altKey: event.altKey, metaKey: event.metaKey,
472
+ pointerId: 1, pointerType: "mouse", isPrimary: true
473
+ };
474
+ this.forwardingWheel = true;
475
+ try {
476
+ // Cesium's wheel zoom uses its last mouse position, not the wheel event's coordinates.
477
+ const detection = Cesium.FeatureDetection;
478
+ // Older Cesium versions opt Firefox out even when the browser exposes PointerEvent.
479
+ canvas.dispatchEvent(((_c = detection.supportsPointerEvents) === null || _c === void 0 ? void 0 : _c.call(detection)) !== false && typeof win.PointerEvent === "function" ?
480
+ new win.PointerEvent("pointermove", position) : new win.MouseEvent("mousemove", position));
481
+ canvas.dispatchEvent(new win.WheelEvent("wheel", {
482
+ ...position, deltaX: event.deltaX, deltaY: event.deltaY, deltaZ: event.deltaZ, deltaMode: event.deltaMode
483
+ }));
484
+ this.viewer.scene.requestRender();
485
+ event.preventDefault();
486
+ event.stopPropagation();
487
+ }
488
+ finally {
489
+ this.forwardingWheel = false;
490
+ }
491
+ };
492
+ this.element.className = "entity-label-layer";
493
+ // Depth ordering belongs inside the map overlay, never in the application's UI stack.
494
+ this.element.style.cssText = "position:absolute;inset:0;z-index:0;isolation:isolate;pointer-events:none;";
495
+ this.Move();
496
+ this.element.addEventListener("wheel", this.ForwardWheel, { passive: false });
497
+ this.element.addEventListener("pointerdown", this.BeginDrag, true);
498
+ this.element.addEventListener("dragstart", this.PreventNativeDrag);
499
+ this.element.ownerDocument.addEventListener("pointermove", this.TrackPointer, true);
500
+ this.element.ownerDocument.addEventListener("pointerout", this.PointerOut, true);
501
+ this.element.ownerDocument.defaultView.addEventListener("blur", this.ClearPointer);
502
+ this.remove = viewer.scene.postUpdate.addEventListener(() => this.Update());
503
+ }
504
+ Move() {
505
+ (containers.get(this.viewer) || this.viewer.container).appendChild(this.element);
506
+ this.Update();
507
+ this.viewer.scene.requestRender();
508
+ }
509
+ MeasureCoordinates() {
510
+ var _a;
511
+ const canvas = this.viewer.canvas;
512
+ const canvasRect = (_a = canvas.getBoundingClientRect) === null || _a === void 0 ? void 0 : _a.call(canvas);
513
+ const rect = this.element.getBoundingClientRect();
514
+ const scaleX = rect.width / this.element.offsetWidth || 1;
515
+ const scaleY = rect.height / this.element.offsetHeight || 1;
516
+ return {
517
+ x: (((canvasRect === null || canvasRect === void 0 ? void 0 : canvasRect.left) || 0) - rect.left) / scaleX,
518
+ y: (((canvasRect === null || canvasRect === void 0 ? void 0 : canvasRect.top) || 0) - rect.top) / scaleY,
519
+ scaleX: ((canvasRect === null || canvasRect === void 0 ? void 0 : canvasRect.width) / canvas.clientWidth || 1) / scaleX,
520
+ scaleY: ((canvasRect === null || canvasRect === void 0 ? void 0 : canvasRect.height) / canvas.clientHeight || 1) / scaleY
521
+ };
522
+ }
523
+ Add(participant) {
524
+ this.participants.add(participant);
525
+ this.viewer.scene.requestRender();
526
+ return () => {
527
+ this.participants.delete(participant);
528
+ if (!this.participants.size) {
529
+ this.remove();
530
+ this.element.removeEventListener("wheel", this.ForwardWheel);
531
+ this.element.removeEventListener("pointerdown", this.BeginDrag, true);
532
+ this.element.removeEventListener("dragstart", this.PreventNativeDrag);
533
+ this.element.ownerDocument.removeEventListener("pointermove", this.TrackPointer, true);
534
+ this.element.ownerDocument.removeEventListener("pointerout", this.PointerOut, true);
535
+ this.element.ownerDocument.defaultView.removeEventListener("blur", this.ClearPointer);
536
+ this.EndPendingDrag();
537
+ this.element.remove();
538
+ layers.delete(this.viewer);
539
+ }
540
+ };
541
+ }
542
+ Update() {
543
+ var _a, _b;
544
+ this.coordinates = this.MeasureCoordinates();
545
+ const rect = (_b = (_a = this.viewer.canvas).getBoundingClientRect) === null || _b === void 0 ? void 0 : _b.call(_a);
546
+ const pointer = this.pointerClient;
547
+ this.pointer = pointer && (rect === null || rect === void 0 ? void 0 : rect.width) && (rect === null || rect === void 0 ? void 0 : rect.height) && pointer.x >= rect.left && pointer.x < rect.right && pointer.y >= rect.top && pointer.y < rect.bottom ?
548
+ {
549
+ x: (pointer.x - rect.left) * this.viewer.canvas.clientWidth / rect.width,
550
+ y: (pointer.y - rect.top) * this.viewer.canvas.clientHeight / rect.height
551
+ } : undefined;
552
+ const projected = new Map();
553
+ for (const participant of this.participants) {
554
+ projected.set(participant, participant.project());
555
+ }
556
+ const placements = new Map(Layout(Array.from(projected.values()).filter(Boolean), this.placementMemory).map(item => [item.frame.id, item]));
557
+ for (const [participant, frame] of projected) {
558
+ participant.apply(frame ? placements.get(frame.id) : undefined);
559
+ }
560
+ }
561
+ }
562
+ let nextLabelId = 0;
36
563
  class Label {
564
+ get HasText() { return this.options.presentation !== "icon"; }
565
+ SetHoverVisible(visible) {
566
+ var _a, _b, _c;
567
+ if (!this.Transient || this.Persistent) {
568
+ return;
569
+ }
570
+ if (!visible) {
571
+ this.retainedHoverPosition = (_a = this.visualRegister) === null || _a === void 0 ? void 0 : _a.GetLabelHoverPosition((_b = this.rego) === null || _b === void 0 ? void 0 : _b.entityId, (_c = this.rego) === null || _c === void 0 ? void 0 : _c.menuItemId);
572
+ // Retiring labels must not change the ring now owned by the next hover target.
573
+ if (this.ringAnimation) {
574
+ this.ringAnimation = { ...this.ringAnimation };
575
+ }
576
+ }
577
+ if (this.hoverFade.to === Number(visible)) {
578
+ return;
579
+ }
580
+ this.hoverFade = { from: this.HoverOpacity(), to: Number(visible), start: Date.now() };
581
+ this.viewer.scene.requestRender();
582
+ }
583
+ HoverOpacity() {
584
+ if (!this.Transient || this.Persistent) {
585
+ return 1;
586
+ }
587
+ const progress = Math.min(1, Math.max(0, (Date.now() - this.hoverFade.start) / 120));
588
+ if (progress < 1) {
589
+ this.viewer.scene.requestRender();
590
+ }
591
+ return this.hoverFade.from + (this.hoverFade.to - this.hoverFade.from) * progress * progress * (3 - 2 * progress);
592
+ }
37
593
  get Rego() {
38
594
  return this.rego;
39
595
  }
@@ -46,10 +602,18 @@ var EntityLabel;
46
602
  set pos3d(val) {
47
603
  this._pos3d = val;
48
604
  this.prePositioned = !isNaN(val === null || val === void 0 ? void 0 : val.x);
49
- this.createLabel(val);
605
+ if (this.label && this.prePositioned)
606
+ this.viewer.scene.requestRender();
607
+ else
608
+ this.createLabel(val);
50
609
  }
51
610
  constructor(params) {
52
- var _a;
611
+ var _a, _b, _c, _d;
612
+ this.id = `label-${++nextLabelId}`;
613
+ this.Persistent = false;
614
+ this.Transient = false;
615
+ this.hoverFade = { from: 0, to: 1, start: Date.now() };
616
+ this.cursorMotion = { amount: 0, from: 0, target: 0, start: 0, position: undefined };
53
617
  // Counter to help differentiate between createLabel calls.
54
618
  this.counter = 0;
55
619
  // The label element itself.
@@ -69,15 +633,24 @@ var EntityLabel;
69
633
  this.loadTimeout = null;
70
634
  this.loadedName = false;
71
635
  this.line = null;
636
+ this.anchor = null;
72
637
  // Null = not attempted to load.
73
638
  this.additionalTexts = null;
74
639
  this.rego = params.rego;
75
640
  this.viewer = params.viewer;
76
641
  this.api = params.api;
77
642
  this.visualRegister = params.visualRegister;
78
- this.prePositioned = !isNaN((_a = params.pos3d) === null || _a === void 0 ? void 0 : _a.x);
643
+ this.options = params.options || {};
644
+ this.groupId = params.groupId;
645
+ this.Persistent = (_a = params.persistent) !== null && _a !== void 0 ? _a : Boolean(params.groupId);
646
+ this.Transient = (_b = params.transient) !== null && _b !== void 0 ? _b : false;
647
+ this.hoverRingState = params.hoverRingState;
648
+ this.visible = params.visible;
649
+ if (((_c = params.options) === null || _c === void 0 ? void 0 : _c.text) != null)
650
+ this.additionalTexts = [];
651
+ this.prePositioned = !isNaN((_d = params.pos3d) === null || _d === void 0 ? void 0 : _d.x);
79
652
  this.createLabel(params.pos3d);
80
- if (!params.api && !bruce_models_1.ENVIRONMENT.IS_SELF_MANAGED) {
653
+ if (!params.api && !bruce_models_1.ENVIRONMENT.IS_SELF_MANAGED && this.options.text == null) {
81
654
  this.api = bruce_models_1.ENVIRONMENT.Api().GetBruceApi();
82
655
  }
83
656
  }
@@ -127,12 +700,24 @@ var EntityLabel;
127
700
  clearTimeout(this.loadTimeout);
128
701
  this.loadTimeout = null;
129
702
  }
703
+ /*
704
+ * Rebuilds only when configuration changes; state refreshes must not recreate DOM every frame.
705
+ */
706
+ Configure(options, groupId) {
707
+ if (this.groupId === groupId && JSON.stringify(this.options) === JSON.stringify(options || {})) {
708
+ return;
709
+ }
710
+ this.options = options || {};
711
+ this.groupId = groupId;
712
+ this.additionalTexts = this.options.text != null ? [] : null;
713
+ this.createLabel();
714
+ }
130
715
  /**
131
716
  * Creates cesium entity for the label if one does not exist.
132
717
  * @param starterPos3d optional position to create label at. Ignored for billboard labels.
133
718
  */
134
719
  async createLabel(starterPos3d) {
135
- var _a, _b, _c, _d, _e, _f, _g, _h;
720
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
136
721
  this.removeLabel();
137
722
  let pos3d = starterPos3d ? starterPos3d.clone() : null;
138
723
  const counter = ++this.counter;
@@ -341,7 +926,7 @@ var EntityLabel;
341
926
  }
342
927
  clearTimeout(this.loadTimeout);
343
928
  this.loadTimeout = setTimeout(async () => {
344
- var _a, _b, _c, _d;
929
+ var _a, _b, _c, _d, _e;
345
930
  if (this.disposed || this.counter != counter) {
346
931
  return;
347
932
  }
@@ -349,10 +934,10 @@ var EntityLabel;
349
934
  if (this.additionalTexts == null) {
350
935
  const additionalTexts = [];
351
936
  // Rego might not have a known or default Entity Type. Eg: OSM building.
352
- if (this.rego.entityTypeId) {
937
+ if (this.rego.entityTypeId || this.options.attributes) {
353
938
  let entityType = null;
354
939
  try {
355
- const data = await bruce_models_1.EntityType.Get({
940
+ const data = this.options.attributes ? null : await bruce_models_1.EntityType.Get({
356
941
  api: this.api,
357
942
  entityTypeId: this.rego.entityTypeId
358
943
  });
@@ -367,14 +952,14 @@ var EntityLabel;
367
952
  }
368
953
  // Type could be deleted, however we'll just catch that to show a label instead of nothing.
369
954
  // This is not unusual for Tilesets where the Type that is baked might be outdated.
370
- if (entityType) {
371
- const attributes = (_b = (_a = entityType.VisualSettings) === null || _a === void 0 ? void 0 : _a.label) === null || _b === void 0 ? void 0 : _b.attributes;
955
+ if (entityType || this.options.attributes) {
956
+ const attributes = (_a = this.options.attributes) !== null && _a !== void 0 ? _a : (_c = (_b = entityType.VisualSettings) === null || _b === void 0 ? void 0 : _b.label) === null || _c === void 0 ? void 0 : _c.attributes;
372
957
  if (attributes) {
373
958
  try {
374
959
  const { entity } = await bruce_models_1.Entity.Get({
375
960
  api: this.api,
376
961
  entityId: this.rego.entityId,
377
- scenario: ((_c = this.rego.outline) === null || _c === void 0 ? void 0 : _c.length) ? (_d = this.rego.outline.find(x => !!x.Scenario)) === null || _d === void 0 ? void 0 : _d.Scenario : null,
962
+ 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,
378
963
  schemaId: this.rego.schema,
379
964
  migrated: true
380
965
  });
@@ -383,7 +968,7 @@ var EntityLabel;
383
968
  entity: entity,
384
969
  path: attributes[i]
385
970
  });
386
- if (value) {
971
+ if (value != null) {
387
972
  additionalTexts.push(value);
388
973
  }
389
974
  }
@@ -412,12 +997,166 @@ var EntityLabel;
412
997
  if (this.disposed || this.counter != counter) {
413
998
  return;
414
999
  }
415
- const labelContent = `
416
- <div>
417
- <div style="margin-bottom:4px">${text}</div>
418
- ${additionalTexts.map(attr => `<div>${attr}</div>`).join('')}
419
- </div>
420
- `;
1000
+ const presentation = this.options.presentation || "text";
1001
+ let clickEntityIds = [];
1002
+ const selectOnClick = (_j = this.options.selectOnClick) !== null && _j !== void 0 ? _j : Boolean(this.groupId);
1003
+ const select = (event) => {
1004
+ event.preventDefault();
1005
+ event.stopPropagation();
1006
+ if (!selectOnClick || this.disposed || this.counter !== counter || !clickEntityIds.length || !this.visualRegister) {
1007
+ return;
1008
+ }
1009
+ this.visualRegister.ClearSelected({ requestRender: false });
1010
+ this.visualRegister.SetSelected({ entityIds: clickEntityIds, selected: true });
1011
+ };
1012
+ const makeClickable = (target) => {
1013
+ if (!selectOnClick || !this.visualRegister) {
1014
+ return;
1015
+ }
1016
+ target.style.pointerEvents = "auto";
1017
+ target.style.cursor = "pointer";
1018
+ target.style.userSelect = "none";
1019
+ target.setAttribute("role", "button");
1020
+ target.tabIndex = 0;
1021
+ target.addEventListener("click", select);
1022
+ target.addEventListener("keydown", event => {
1023
+ if (event.key === "Enter" || event.key === " ")
1024
+ select(event);
1025
+ });
1026
+ // Prevent viewer picking and camera gestures from interpreting label interactions.
1027
+ for (const type of ["pointerdown", "pointerup", "mousedown", "mouseup", "dblclick"]) {
1028
+ target.addEventListener(type, event => event.stopPropagation());
1029
+ }
1030
+ };
1031
+ const normalLeader = (_k = this.options.leader) !== null && _k !== void 0 ? _k : presentation === "text";
1032
+ let leader = normalLeader;
1033
+ const content = document.createElement("span");
1034
+ const originalText = (_l = this.options.text) !== null && _l !== void 0 ? _l : [text, ...additionalTexts].join("\n");
1035
+ let textElement;
1036
+ let sourceText;
1037
+ let sourceNaturalText;
1038
+ let iconElement;
1039
+ let iconContainer;
1040
+ content.className = "entity-label-content";
1041
+ content.style.cssText = "display:flex;align-items:center;gap:6px;";
1042
+ if (presentation === "icon-text-above") {
1043
+ content.style.flexDirection = "column-reverse";
1044
+ }
1045
+ if (presentation !== "text") {
1046
+ const icon = document.createElement("span");
1047
+ iconElement = icon;
1048
+ icon.className = "entity-label-icon";
1049
+ icon.style.cssText = `font-size:${(this.options.iconSize || 24) * ((_m = this.options.iconScale) !== null && _m !== void 0 ? _m : 1)}px;line-height:1;`;
1050
+ icon.style.textShadow = "0 1px 2px rgba(0, 0, 0, 0.25)";
1051
+ const iconColor = typeof this.options.iconColor === "string" ? this.options.iconColor : undefined;
1052
+ const iconBackground = this.options.iconBackground && this.options.iconScale !== 0;
1053
+ if (iconColor) {
1054
+ icon.style.color = iconColor;
1055
+ }
1056
+ if (this.options.iconUrl) {
1057
+ const image = document.createElement("img");
1058
+ image.draggable = false;
1059
+ image.alt = this.options.icon || "";
1060
+ const scale = (_o = this.options.iconScale) !== null && _o !== void 0 ? _o : 1;
1061
+ 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
+ }
1077
+ const resize = () => {
1078
+ if (this.disposed || this.counter !== counter) {
1079
+ return;
1080
+ }
1081
+ 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`;
1084
+ this.viewer.scene.requestRender();
1085
+ };
1086
+ image.onload = resize;
1087
+ resize();
1088
+ image.src = this.options.iconUrl;
1089
+ if (!iconColor) {
1090
+ icon.appendChild(image);
1091
+ }
1092
+ }
1093
+ else if (typeof this.options.iconClass === "string" && this.options.iconClass) {
1094
+ icon.className = `entity-label-icon ${this.options.iconClass}`;
1095
+ }
1096
+ else {
1097
+ icon.textContent = this.options.icon || "●";
1098
+ }
1099
+ if (iconBackground) {
1100
+ const color = Cesium.Color.fromCssColorString(iconColor || "white") || Cesium.Color.WHITE;
1101
+ const luminance = 0.2126 * color.red + 0.7152 * color.green + 0.0722 * color.blue;
1102
+ const background = (luminance > 0.5 ? color.darken(ICON_BACKGROUND_BLEND, new Cesium.Color()) :
1103
+ color.brighten(ICON_BACKGROUND_BLEND, new Cesium.Color()));
1104
+ const size = (this.options.iconSize || 24) * ((_p = this.options.iconScale) !== null && _p !== void 0 ? _p : 1);
1105
+ icon.style.backgroundColor = background.withAlpha(ICON_BACKGROUND_ALPHA).toCssColorString();
1106
+ icon.style.borderRadius = "50%";
1107
+ icon.style.padding = `${size / 3}px`;
1108
+ icon.style.display = "inline-flex";
1109
+ icon.style.alignItems = "center";
1110
+ icon.style.justifyContent = "center";
1111
+ icon.style.minWidth = `${size}px`;
1112
+ icon.style.minHeight = `${size}px`;
1113
+ icon.style.boxSizing = "content-box";
1114
+ }
1115
+ if (this.options.iconScale === 0) {
1116
+ icon.style.display = "none";
1117
+ }
1118
+ makeClickable(icon);
1119
+ // Keep badges outside the glyph so font classes and image masks cannot affect them.
1120
+ iconContainer = document.createElement("span");
1121
+ iconContainer.style.cssText = "position:relative;display:inline-flex;flex:none;";
1122
+ iconContainer.appendChild(icon);
1123
+ content.appendChild(iconContainer);
1124
+ }
1125
+ if (presentation !== "icon") {
1126
+ textElement = document.createElement("span");
1127
+ textElement.className = "entity-label-text";
1128
+ const textStyle = `white-space:pre-wrap;overflow-wrap:anywhere;max-width:${TEXT_MAX_WIDTH}px;width:max-content;flex-shrink:0;`;
1129
+ textElement.style.cssText = textStyle;
1130
+ textElement.textContent = originalText;
1131
+ if (presentation === "text")
1132
+ makeClickable(textElement);
1133
+ content.appendChild(textElement);
1134
+ if ((_q = this.options.cluster) === null || _q === void 0 ? void 0 : _q.enabled) {
1135
+ sourceText = document.createElement("span");
1136
+ sourceText.className = "entity-label-source-text";
1137
+ sourceText.setAttribute("aria-hidden", "true");
1138
+ sourceText.style.cssText = textStyle + "position:absolute;visibility:hidden;pointer-events:none;";
1139
+ sourceText.textContent = originalText;
1140
+ content.appendChild(sourceText);
1141
+ // Measure the original unwrapped text, never a rendered cluster summary.
1142
+ sourceNaturalText = document.createElement("span");
1143
+ sourceNaturalText.setAttribute("aria-hidden", "true");
1144
+ sourceNaturalText.style.cssText = "position:absolute;visibility:hidden;pointer-events:none;white-space:pre;width:max-content;";
1145
+ sourceNaturalText.textContent = originalText;
1146
+ content.appendChild(sourceNaturalText);
1147
+ }
1148
+ }
1149
+ const countElement = document.createElement("span");
1150
+ countElement.className = "entity-label-count";
1151
+ 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;";
1152
+ if (iconContainer) {
1153
+ countElement.style.cssText += "position:absolute;left:100%;top:0;transform:translate(-50%,-50%);";
1154
+ iconContainer.appendChild(countElement);
1155
+ }
1156
+ if (!layers.has(this.viewer)) {
1157
+ layers.set(this.viewer, new Layer(this.viewer));
1158
+ }
1159
+ const layer = layers.get(this.viewer);
421
1160
  let ele = null;
422
1161
  let lineEle = null;
423
1162
  if (this.label) {
@@ -425,11 +1164,22 @@ var EntityLabel;
425
1164
  }
426
1165
  else {
427
1166
  ele = document.createElement("div");
428
- ele.innerHTML = labelContent;
1167
+ ele.appendChild(content);
1168
+ if (!iconContainer) {
1169
+ ele.appendChild(countElement);
1170
+ }
1171
+ ele.className = "entity-label";
1172
+ ele.dataset.entityId = ((_r = this.rego) === null || _r === void 0 ? void 0 : _r.entityId) || "";
1173
+ ele.dataset.presentation = presentation;
1174
+ if (this.groupId) {
1175
+ ele.dataset.groupId = this.groupId;
1176
+ }
429
1177
  ele.setAttribute("style", `
430
1178
  position: absolute;
431
1179
  z-index: 0;
432
1180
  display: none;
1181
+ align-items: center;
1182
+ gap: 6px;
433
1183
  pointer-events: none;
434
1184
  padding: 6px 8px;
435
1185
  border-radius: 6px;
@@ -438,9 +1188,10 @@ var EntityLabel;
438
1188
  border-radius: 9px;
439
1189
  color: #ffffff;
440
1190
  text-shadow: 0 0 5px rgba(0, 0, 0, 0.8);
441
- }`);
1191
+ `);
442
1192
  this.label = ele;
443
- this.viewer.container.appendChild(ele);
1193
+ ele.style.width = "max-content";
1194
+ layer.element.appendChild(ele);
444
1195
  }
445
1196
  if (this.line) {
446
1197
  lineEle = this.line;
@@ -457,7 +1208,10 @@ var EntityLabel;
457
1208
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
458
1209
  `);
459
1210
  this.line = lineEle;
460
- // We'll draw a ball at the bottom of the line to make it look like it's connecting to the entity.
1211
+ lineEle.className = "entity-label-leader";
1212
+ if (this.options.leaderColor)
1213
+ lineEle.style.background = this.options.leaderColor;
1214
+ // A separate anchor keeps the stem out of the hollow centre when proximity opens the ring.
461
1215
  const lineEleNode = document.createElement("div");
462
1216
  lineEleNode.setAttribute("style", `
463
1217
  position: absolute;
@@ -470,35 +1224,20 @@ var EntityLabel;
470
1224
  top: -2px;
471
1225
  left: -2px;
472
1226
  `);
473
- lineEle.appendChild(lineEleNode);
474
- this.viewer.container.appendChild(lineEle);
1227
+ lineEleNode.className = "entity-label-anchor";
1228
+ // SVG preserves fractional strokes in scaled containers where CSS borders round to pixels.
1229
+ const ringSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1230
+ ringSvg.style.cssText = "position:absolute;inset:0;width:100%;height:100%;overflow:visible;";
1231
+ ringSvg.setAttribute("aria-hidden", "true");
1232
+ ringSvg.setAttribute("preserveAspectRatio", "none");
1233
+ const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
1234
+ circle.setAttribute("fill", "none");
1235
+ ringSvg.appendChild(circle);
1236
+ lineEleNode.appendChild(ringSvg);
1237
+ this.anchor = lineEleNode;
1238
+ layer.element.appendChild(lineEleNode);
1239
+ layer.element.appendChild(lineEle);
475
1240
  }
476
- let _lastDistance = null;
477
- let _lastCameraPos = null;
478
- const getDistance = () => {
479
- var _a;
480
- if (this.disposed) {
481
- return null;
482
- }
483
- const cameraPos = (_a = this.viewer.camera) === null || _a === void 0 ? void 0 : _a.position;
484
- if (isNaN(cameraPos === null || cameraPos === void 0 ? void 0 : cameraPos.x)) {
485
- return null;
486
- }
487
- // If camera hasn't moved much then we can use the last distance.
488
- const TOLERANCE = 0.1;
489
- if (_lastCameraPos) {
490
- const totalMovement = Math.abs(_lastCameraPos.x - cameraPos.x) +
491
- Math.abs(_lastCameraPos.y - cameraPos.y) +
492
- Math.abs(_lastCameraPos.z - cameraPos.z);
493
- if (totalMovement < TOLERANCE) {
494
- return _lastDistance;
495
- }
496
- }
497
- const distance = Cesium.Cartesian3.distance(cameraPos, this._pos3d);
498
- _lastDistance = distance;
499
- _lastCameraPos = cameraPos.clone();
500
- return distance;
501
- };
502
1241
  let MAX_DISTANCE = Math.max(15000, sizeInM * 35);
503
1242
  if (visual && visual instanceof Cesium.Entity) {
504
1243
  // Consistent sized things.
@@ -506,123 +1245,404 @@ var EntityLabel;
506
1245
  MAX_DISTANCE = Infinity;
507
1246
  }
508
1247
  }
509
- const updateLabel = () => {
510
- var _a;
511
- if (this.disposed || isNaN((_a = this._pos3d) === null || _a === void 0 ? void 0 : _a.x)) {
512
- return;
1248
+ const fitLeader = (anchorX, anchorY, desired, width, height, radius = 0) => {
1249
+ const above = Math.max(0, anchorY - height);
1250
+ const below = Math.max(0, this.viewer.canvas.clientHeight - anchorY - height);
1251
+ // Keep a short visible stem before switching sides; never aim it at the cursor or a box corner.
1252
+ const upwards = above >= Math.max(radius ? radius + 4 : 0, Math.min(8, desired)) || above >= below;
1253
+ const length = Math.min(desired, upwards ? above : below);
1254
+ const endY = anchorY + (upwards ? -length : length);
1255
+ return {
1256
+ x: Math.max(0, Math.min(anchorX, this.viewer.canvas.clientWidth - width)),
1257
+ y: upwards ? endY - height : endY, endY
1258
+ };
1259
+ };
1260
+ let cursorMotion = this.cursorMotion;
1261
+ const project = () => {
1262
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
1263
+ if (this.disposed || !this.viewer || this.viewer.isDestroyed() || ((_a = this.visible) === null || _a === void 0 ? void 0 : _a.call(this)) === false ||
1264
+ (presentation === "icon" && this.options.iconScale === 0)) {
1265
+ return null;
513
1266
  }
514
- if (!this.viewer || this.viewer.isDestroyed()) {
515
- this.Dispose();
516
- return;
1267
+ if (this.Transient && !this.Persistent && this.hoverFade.to === 0 &&
1268
+ (this.hoverFade.from === 0 || Date.now() - this.hoverFade.start >= 120)) {
1269
+ return null;
517
1270
  }
518
- const distance = getDistance();
519
- if (isNaN(distance) || (!this.prePositioned && distance >= MAX_DISTANCE)) {
520
- ele.style.display = "none";
521
- lineEle.style.display = "none";
522
- return;
1271
+ // Moving point/model positions must be sampled at the current clock time.
1272
+ if (!this.prePositioned && visual instanceof Cesium.Entity && visual.position) {
1273
+ const position = getValue(this.viewer, visual.position);
1274
+ if (position)
1275
+ this._pos3d = drawing_utils_1.DrawingUtils.EnsurePosHeight({
1276
+ desiredHeightRef: Cesium.HeightReference.NONE, heightRef, pos3d: position, viewer: this.viewer
1277
+ });
523
1278
  }
524
- const CESIUM = Cesium;
525
- const _3dTo2D = Cesium.SceneTransforms.hasOwnProperty("wgs84ToWindowCoordinates") ? CESIUM.SceneTransforms.wgs84ToWindowCoordinates : CESIUM.SceneTransforms.worldToWindowCoordinates;
526
- const lineStartPos2d = _3dTo2D(this.viewer.scene, this._pos3d);
527
- // Invalid point.
528
- if (isNaN(lineStartPos2d === null || lineStartPos2d === void 0 ? void 0 : lineStartPos2d.x)) {
529
- ele.style.transform = "";
530
- ele.style.opacity = "0";
531
- lineEle.style.display = "none";
532
- return;
1279
+ if (!Number.isFinite((_b = this._pos3d) === null || _b === void 0 ? void 0 : _b.x)) {
1280
+ return null;
1281
+ }
1282
+ const camera = this.viewer.camera;
1283
+ const cameraPosition = camera.positionWC || camera.position;
1284
+ const relative = Cesium.Cartesian3.subtract(this._pos3d, cameraPosition, new Cesium.Cartesian3());
1285
+ const depth = Cesium.Cartesian3.dot(relative, camera.directionWC || camera.direction);
1286
+ const distance = Cesium.Cartesian3.magnitude(relative);
1287
+ if (!Number.isFinite(distance) || depth <= 0 || distance < ((_c = this.options.near) !== null && _c !== void 0 ? _c : 0) ||
1288
+ distance > ((_d = this.options.far) !== null && _d !== void 0 ? _d : Infinity) || (!this.prePositioned && distance >= MAX_DISTANCE)) {
1289
+ return null;
1290
+ }
1291
+ const transforms = Cesium.SceneTransforms;
1292
+ const projectPosition = transforms.wgs84ToWindowCoordinates || transforms.worldToWindowCoordinates;
1293
+ const anchor = projectPosition(this.viewer.scene, this._pos3d);
1294
+ if (!Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.x) || !Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.y)) {
1295
+ return null;
1296
+ }
1297
+ if (leader && (anchor.x < 0 || anchor.y < 0 || anchor.x >= this.viewer.canvas.clientWidth || anchor.y >= this.viewer.canvas.clientHeight))
1298
+ return null;
1299
+ const transientPivot = (this.Transient && !this.Persistent ?
1300
+ (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);
1301
+ const hover = (presentation !== "icon" ?
1302
+ (this.Transient && !this.Persistent && this.hoverFade.to === 0 ? this.retainedHoverPosition :
1303
+ (_h = this.visualRegister) === null || _h === void 0 ? void 0 : _h.GetLabelHoverPosition((_j = this.rego) === null || _j === void 0 ? void 0 : _j.entityId, (_k = this.rego) === null || _k === void 0 ? void 0 : _k.menuItemId)) : undefined);
1304
+ const reused = !this.Transient || this.Persistent;
1305
+ const now = Date.now();
1306
+ const animateCursor = () => {
1307
+ const progress = Math.min(1, Math.max(0, (now - cursorMotion.start) / 200));
1308
+ const eased = progress * progress * (3 - 2 * progress);
1309
+ cursorMotion.amount = cursorMotion.from + (cursorMotion.target - cursorMotion.from) * eased;
1310
+ return progress;
1311
+ };
1312
+ animateCursor();
1313
+ const cursorTarget = reused && hover ? 1 : 0;
1314
+ if (cursorMotion.target !== cursorTarget) {
1315
+ cursorMotion = { ...cursorMotion, from: cursorMotion.amount, target: cursorTarget, start: now };
1316
+ }
1317
+ ;
1318
+ if (hover) {
1319
+ cursorMotion.position = { x: hover.x, y: hover.y };
533
1320
  }
534
- // Offset start by the pixel offset.
535
- lineStartPos2d.x += pixelOffset.x;
536
- lineStartPos2d.y += pixelOffset.y;
537
- ele.style.display = "block";
538
- lineEle.style.display = "block";
539
- // Calculate opacity to apply based on distance between camera and pos3d.
540
- // We'll make the label less transparent when further away.
541
- const opacity = this.prePositioned || MAX_DISTANCE == Infinity ? 1 : 1 - (distance / MAX_DISTANCE);
542
- if (opacity && opacity > 0.15) {
543
- this.label.style.opacity = `${opacity}`;
544
- // Calc where the line should end.
545
- // We get the pos3d and add 'n' to it, then turn it back into 2d.
546
- let lineEndPos3d = this._pos3d.clone();
547
- const lineEndPoint = Cesium.Cartographic.fromCartesian(lineEndPos3d);
548
- let addHeight = null;
549
- // If we're pre-positioned that means the user is hovering over the entity.
550
- // We will keep the height low to avoid looking goofy.
551
- if (this.prePositioned) {
552
- addHeight = distance / 24;
1321
+ ;
1322
+ if (animateCursor() < 1 && cursorMotion.from !== cursorMotion.target) {
1323
+ this.viewer.scene.requestRender();
1324
+ }
1325
+ ;
1326
+ this.cursorMotion = cursorMotion;
1327
+ const cursorAmount = cursorMotion.amount;
1328
+ leader = normalLeader || cursorAmount > 0;
1329
+ let x = ((_l = transientPivot === null || transientPivot === void 0 ? void 0 : transientPivot.x) !== null && _l !== void 0 ? _l : anchor.x) + pixelOffset.x;
1330
+ let y = ((_m = transientPivot === null || transientPivot === void 0 ? void 0 : transientPivot.y) !== null && _m !== void 0 ? _m : anchor.y) + pixelOffset.y;
1331
+ if (cursorAmount > 0) {
1332
+ // A leader-less label starts centred on its anchor; preserve that box as it lifts away.
1333
+ if (!normalLeader) {
1334
+ x -= ele.offsetWidth / layer.coordinates.scaleX / 2;
1335
+ y += ele.offsetHeight / layer.coordinates.scaleY / 2;
1336
+ }
1337
+ x += (cursorMotion.position.x + pixelOffset.x - x) * cursorAmount;
1338
+ y += (cursorMotion.position.y + pixelOffset.y - y) * cursorAmount;
1339
+ }
1340
+ const anchorX = x;
1341
+ const anchorY = y;
1342
+ let leaderLength;
1343
+ if (leader) {
1344
+ if (!normalLeader) {
1345
+ leaderLength = 0;
1346
+ }
1347
+ else if (this.options.leaderLength != null) {
1348
+ leaderLength = Math.max(0, this.options.leaderLength);
1349
+ }
1350
+ else if (transientPivot) {
1351
+ // Cursor callouts must not jump when the next hover target has a different extent.
1352
+ leaderLength = 96;
553
1353
  }
554
1354
  else {
555
- // If known then calculate based on size.
556
- // If we're close then we'll favour the distance over the size.
557
- if (sizeInM && sizeInM > 0) {
558
- addHeight = Math.min(distance / 5, sizeInM * 1.2);
559
- }
560
- // If unknown then calculate based on camera height or distance.
561
- // Radius is unknown for Tileset things. We could bake the radius in possibly as a feature property.
562
- // We have to make it work for both labels against tall skyscrapers, and up-close assembly models.
563
- else {
564
- addHeight = Math.min(distance, this.viewer.camera.positionCartographic.height) / 7;
565
- }
1355
+ const point = Cesium.Cartographic.fromCartesian(this._pos3d);
1356
+ const height = (this.prePositioned ? distance / 24 : sizeInM > 0 ?
1357
+ Math.min(distance / 5, sizeInM * 1.2) : Math.min(distance, camera.positionCartographic.height) / 7);
1358
+ point.height += Math.max(0.001, height);
1359
+ const end = projectPosition(this.viewer.scene, Cesium.Cartographic.toCartesian(point));
1360
+ // A tall world-space leader can cross the camera plane while its anchor remains visible.
1361
+ leaderLength = Number.isFinite(end === null || end === void 0 ? void 0 : end.x) && Number.isFinite(end === null || end === void 0 ? void 0 : end.y) ? Math.hypot(end.x - anchor.x, end.y - anchor.y) : 24;
1362
+ // Scale after projection so large areas and routes get shorter leaders at every zoom.
1363
+ if (visual instanceof Cesium.Entity && (visual.polygon || visual.polyline))
1364
+ leaderLength /= 3;
566
1365
  }
567
- // The ratio is roughly 1m = 11px on the screen.
568
- // So if we want to add additional labels then we'll need to adjust the height.
569
- if (additionalTexts === null || additionalTexts === void 0 ? void 0 : additionalTexts.length) {
570
- // Each additional text is about 16px tall.
571
- // So we get the total px height and divide by 11 to get the meters.
572
- addHeight += ((additionalTexts.length * 16) / 11);
1366
+ if (cursorAmount > 0) {
1367
+ leaderLength += (96 - leaderLength) * cursorAmount;
573
1368
  }
574
- // Safety check.
575
- if (!addHeight || addHeight <= 0) {
576
- addHeight = 0.001;
1369
+ ;
1370
+ y = anchorY - leaderLength;
1371
+ }
1372
+ else if (hover && !reused) {
1373
+ x = hover.x + pixelOffset.x;
1374
+ y = hover.y + pixelOffset.y - 24;
1375
+ }
1376
+ // Hidden participants remain measurable; visibility never removes their layout boxes.
1377
+ const coordinates = layer.coordinates;
1378
+ const width = ele.offsetWidth / coordinates.scaleX;
1379
+ const height = ele.offsetHeight / coordinates.scaleY;
1380
+ let clusterBounds;
1381
+ if ((_o = this.options.cluster) === null || _o === void 0 ? void 0 : _o.enabled) {
1382
+ const textWidth = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetWidth) || 0;
1383
+ const textHeight = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetHeight) || 0;
1384
+ const iconWidth = (iconElement === null || iconElement === void 0 ? void 0 : iconElement.offsetWidth) || 0;
1385
+ const iconHeight = (iconElement === null || iconElement === void 0 ? void 0 : iconElement.offsetHeight) || 0;
1386
+ const gap = sourceText && iconWidth ? 6 : 0;
1387
+ const above = presentation === "icon-text-above";
1388
+ // Match the fixed content gap and label padding, excluding the count badge.
1389
+ const baseWidth = ((above ? Math.max(textWidth, iconWidth) : textWidth + iconWidth + gap) + 16) / coordinates.scaleX;
1390
+ const baseHeight = ((above ? textHeight + iconHeight + gap : Math.max(textHeight, iconHeight)) + 12) / coordinates.scaleY;
1391
+ clusterBounds = {
1392
+ x: leader ? x : x - baseWidth / 2,
1393
+ y: leader ? y - baseHeight : y - baseHeight / 2, width: baseWidth, height: baseHeight
1394
+ };
1395
+ }
1396
+ if (!leader) {
1397
+ x -= width / 2;
1398
+ y -= height / 2;
1399
+ }
1400
+ // Membership uses the unclamped source footprint, never the summary's changing dimensions.
1401
+ if (leader)
1402
+ ({ x, y } = fitLeader(anchorX, anchorY, leaderLength, width, height));
1403
+ const bounds = clusterBounds || { x, y, width, height };
1404
+ if (!leader && (bounds.x + bounds.width < 0 || bounds.y + bounds.height < 0 || bounds.x > this.viewer.canvas.clientWidth || bounds.y > this.viewer.canvas.clientHeight)) {
1405
+ return null;
1406
+ }
1407
+ const state = (_p = this.visualRegister) === null || _p === void 0 ? void 0 : _p.GetState({ entityId: (_q = this.rego) === null || _q === void 0 ? void 0 : _q.entityId, menuItemId: (_r = this.rego) === null || _r === void 0 ? void 0 : _r.menuItemId });
1408
+ const opacity = this.prePositioned || MAX_DISTANCE === Infinity ? 1 : 1 - distance / MAX_DISTANCE;
1409
+ return {
1410
+ canFlip: leader && presentation !== "icon", hasLeader: leader,
1411
+ textWrapped: ((sourceNaturalText === null || sourceNaturalText === void 0 ? void 0 : sourceNaturalText.offsetWidth) || 0) > TEXT_MAX_WIDTH,
1412
+ transient: this.Transient || Boolean(hover) || cursorAmount > 0,
1413
+ extendedGeometry: visual instanceof Cesium.Entity && Boolean(visual.polygon || visual.polyline || visual.corridor),
1414
+ viewportWidth: this.viewer.canvas.clientWidth,
1415
+ 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,
1417
+ // Priority shows the hovered member individually without dragging its cluster along.
1418
+ opacity, priority: hover || cursorAmount > 0 || (state === null || state === void 0 ? void 0 : state.highlighted) ? 1 : 0, options: this.options, text: originalText, clusterBounds
1419
+ };
1420
+ };
1421
+ let displacement = { x: 0, y: 0 };
1422
+ let displayedSummaryMore = false;
1423
+ let nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
1424
+ 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;
1426
+ const show = (placement === null || placement === void 0 ? void 0 : placement.visible) && placement.opacity > 0;
1427
+ clickEntityIds = show ? placement.entityIds : [];
1428
+ ele.style.visibility = show ? "visible" : "hidden";
1429
+ lineEle.style.display = show && leader ? "block" : "none";
1430
+ this.anchor.style.display = show && leader ? "block" : "none";
1431
+ if (!show) {
1432
+ displacement = { x: 0, y: 0 };
1433
+ nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
1434
+ if (!this.Transient) {
1435
+ this.ringAnimation = undefined;
577
1436
  }
578
- lineEndPoint.height += addHeight;
579
- lineEndPos3d = Cesium.Cartographic.toCartesian(lineEndPoint);
580
- const CESIUM = Cesium;
581
- const _3dTo2D = Cesium.SceneTransforms.hasOwnProperty("wgs84ToWindowCoordinates") ? CESIUM.SceneTransforms.wgs84ToWindowCoordinates : CESIUM.SceneTransforms.worldToWindowCoordinates;
582
- let lineEndPos2d = _3dTo2D(this.viewer.scene, lineEndPos3d);
583
- // Invalid point.
584
- if (isNaN(lineEndPos2d === null || lineEndPos2d === void 0 ? void 0 : lineEndPos2d.x)) {
585
- ele.style.transform = "";
586
- ele.style.opacity = "0";
587
- lineEle.style.display = "none";
588
- return;
1437
+ return;
1438
+ }
1439
+ 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)) {
1442
+ 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);
589
1449
  }
590
- // Offset end by the pixel offset.
591
- lineEndPos2d.x += pixelOffset.x;
592
- lineEndPos2d.y += pixelOffset.y;
593
- // If out of the screen then we can hide it.
594
- const startOutOfScreen = (lineStartPos2d.x < 0 || lineStartPos2d.x > this.viewer.canvas.clientWidth ||
595
- lineStartPos2d.y < 0 || lineStartPos2d.y > this.viewer.canvas.clientHeight);
596
- const endOutOfScreen = (lineEndPos2d.x < 0 || lineEndPos2d.x > this.viewer.canvas.clientWidth ||
597
- lineEndPos2d.y < 0 || lineEndPos2d.y > this.viewer.canvas.clientHeight);
598
- if (startOutOfScreen && endOutOfScreen) {
599
- ele.style.transform = "";
600
- ele.style.opacity = "0";
601
- lineEle.style.display = "none";
602
- return;
1450
+ displayedSummaryMore = summaryMore;
1451
+ // Measure the summary's footprint next frame without replacing each member's source text.
1452
+ this.viewer.scene.requestRender();
1453
+ }
1454
+ const frame = placement.frame;
1455
+ const coordinates = layer.coordinates;
1456
+ const opacity = placement.opacity * this.HoverOpacity();
1457
+ ele.style.opacity = String(opacity);
1458
+ ele.style.zIndex = String(placement.zIndex);
1459
+ ele.dataset.clusterCount = String(placement.members.length || 1);
1460
+ countElement.style.display = placement.members.length ? "block" : "none";
1461
+ countElement.style.order = placement.textSide === "left" ? "-1" : "0";
1462
+ const count = placement.members.length ? String(placement.members.length) : "";
1463
+ if (countElement.textContent !== count) {
1464
+ countElement.textContent = count;
1465
+ // The badge participates in layout, so its new width needs another measurement.
1466
+ this.viewer.scene.requestRender();
1467
+ }
1468
+ if (iconContainer) {
1469
+ const badgeWidth = countElement.offsetWidth;
1470
+ const iconWidth = iconElement.offsetWidth;
1471
+ const narrow = iconWidth < iconElement.offsetHeight * 0.6;
1472
+ const clearance = badgeWidth ? (narrow ? badgeWidth / 2 + 2 : Math.max(0, badgeWidth / 2 - iconWidth / 4)) : 0;
1473
+ countElement.style.left = `calc(100% + ${clearance}px)`;
1474
+ // Reserve the badge overhang so adjacent text and viewport edges remain clear.
1475
+ iconContainer.style.marginTop = `${countElement.offsetHeight / 2}px`;
1476
+ iconContainer.style.marginRight = `${badgeWidth / 2 + clearance}px`;
1477
+ }
1478
+ // Summary and badge changes must fit on their first visible frame, including after resize.
1479
+ const ring = this.options.anchorRing;
1480
+ const pointer = (layer.pointer || (cursorMotion.target ? cursorMotion.position : undefined) || (this.Transient && !this.Persistent ?
1481
+ (this.hoverFade.to === 0 ? this.retainedHoverPosition : (_b = this.visualRegister) === null || _b === void 0 ? void 0 : _b.GetLabelHoverPosition((_c = this.rego) === null || _c === void 0 ? void 0 : _c.entityId, (_d = this.rego) === null || _d === void 0 ? void 0 : _d.menuItemId)) : undefined));
1482
+ const hovered = pointer && Math.hypot(pointer.x - frame.anchorX, pointer.y - frame.anchorY) <= ((_e = ring === null || ring === void 0 ? void 0 : ring.hoverDistance) !== null && _e !== void 0 ? _e : 24);
1483
+ const normalRadius = (!leader || (ring === null || ring === void 0 ? void 0 : ring.enabled) === false ? 0 : Math.max(frame.distance <= ((_f = ring === null || ring === void 0 ? void 0 : ring.nearDistance) !== null && _f !== void 0 ? _f : 100) ? ((_g = ring === null || ring === void 0 ? void 0 : ring.nearRadius) !== null && _g !== void 0 ? _g : 10) : 0, hovered ? ((_h = ring === null || ring === void 0 ? void 0 : ring.hoverRadius) !== null && _h !== void 0 ? _h : (this.Transient && !this.Persistent ? 15.4 : 22)) : 0));
1484
+ const targetRadius = normalRadius + (15.4 - normalRadius) * cursorMotion.amount;
1485
+ const now = Date.now();
1486
+ const duration = (_j = ring === null || ring === void 0 ? void 0 : ring.transitionDuration) !== null && _j !== void 0 ? _j : 200;
1487
+ const sharedRing = this.Transient && !this.Persistent && this.hoverFade.to === 1 ? this.hoverRingState : undefined;
1488
+ let ringAnimation = sharedRing ? sharedRing.animation : this.ringAnimation;
1489
+ if (!ringAnimation) {
1490
+ ringAnimation = { target: 0, start: now, outer: 2.5, inner: 0, fromOuter: 2.5, fromInner: 0 };
1491
+ }
1492
+ ;
1493
+ const animateRing = () => {
1494
+ const progress = duration > 0 ? Math.min(1, Math.max(0, (now - ringAnimation.start) / duration)) : 1;
1495
+ const eased = progress * progress * (3 - 2 * progress);
1496
+ ringAnimation.outer = ringAnimation.fromOuter + ((ringAnimation.target || 2.5) - ringAnimation.fromOuter) * eased;
1497
+ ringAnimation.inner = ringAnimation.fromInner + (Math.max(0, ringAnimation.target - 2) - ringAnimation.fromInner) * eased;
1498
+ return progress;
1499
+ };
1500
+ animateRing();
1501
+ if (ringAnimation.target !== targetRadius) {
1502
+ ringAnimation = {
1503
+ ...ringAnimation,
1504
+ target: targetRadius,
1505
+ start: now,
1506
+ fromOuter: ringAnimation.outer,
1507
+ fromInner: ringAnimation.inner
1508
+ };
1509
+ }
1510
+ // Animate the hole and stem together; no CSS transition may lag behind a moving world anchor.
1511
+ if (animateRing() < 1 && (ringAnimation.fromOuter !== (targetRadius || 2.5) || ringAnimation.fromInner !== Math.max(0, targetRadius - 2))) {
1512
+ this.viewer.scene.requestRender();
1513
+ }
1514
+ this.ringAnimation = ringAnimation;
1515
+ if (sharedRing) {
1516
+ sharedRing.animation = ringAnimation;
1517
+ }
1518
+ 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);
1520
+ const position = { x: (stem || frame).x, y: (stem || frame).y };
1521
+ const width = ele.offsetWidth / coordinates.scaleX;
1522
+ const height = ele.offsetHeight / coordinates.scaleY;
1523
+ if (leader && placement.textSide === "left") {
1524
+ position.x = Math.max(0, Math.min(frame.anchorX - width, this.viewer.canvas.clientWidth - width));
1525
+ }
1526
+ const gap = (_l = (_k = this.options.pointerAvoidance) === null || _k === void 0 ? void 0 : _k.gap) !== null && _l !== void 0 ? _l : 12;
1527
+ const containsPointer = (x, y, margin) => {
1528
+ return pointer && pointer.x >= x - margin && pointer.x <= x + width + margin && pointer.y >= y - margin && pointer.y <= y + height + margin;
1529
+ };
1530
+ const passive = !selectOnClick || !this.visualRegister;
1531
+ if (passive && ((_m = this.options.pointerAvoidance) === null || _m === void 0 ? void 0 : _m.enabled) !== false && pointer &&
1532
+ (containsPointer(position.x, position.y, gap) || containsPointer(position.x + displacement.x, position.y + displacement.y, gap))) {
1533
+ if (containsPointer(position.x + displacement.x, position.y + displacement.y, 0)) {
1534
+ // Choose against the original box and retain the offset until the pointer leaves both boxes.
1535
+ const candidates = [
1536
+ { x: 0, y: pointer.y - gap - height - position.y },
1537
+ { x: 0, y: pointer.y + gap - position.y },
1538
+ { x: pointer.x - gap - width - position.x, y: 0 },
1539
+ { x: pointer.x + gap - position.x, y: 0 }
1540
+ ].filter(offset => position.x + offset.x >= 0 && position.y + offset.y >= 0 &&
1541
+ position.x + offset.x + width <= this.viewer.canvas.clientWidth && position.y + offset.y + height <= this.viewer.canvas.clientHeight)
1542
+ .sort((a, b) => Math.hypot(a.x, a.y) - Math.hypot(b.x, b.y));
1543
+ displacement = candidates[0] || { x: 0, y: 0 };
603
1544
  }
604
- // Position the label above the line end point.
605
- this.label.style.left = `${lineEndPos2d.x}px`;
606
- this.label.style.top = `${lineEndPos2d.y}px`;
607
- // Update line position and size.
608
- const dx = lineEndPos2d.x - lineStartPos2d.x;
609
- const dy = lineEndPos2d.y - lineStartPos2d.y;
610
- const length = Math.sqrt(dx * dx + dy * dy);
611
- const angle = Math.atan2(dy, dx) - Math.PI / 2;
612
- this.line.style.left = `${lineStartPos2d.x}px`;
613
- this.line.style.top = `${lineStartPos2d.y}px`;
614
- this.line.style.height = `${length}px`;
615
- this.line.style.transform = `rotate(${angle}rad)`;
616
- this.line.style.transformOrigin = 'top center';
617
1545
  }
618
1546
  else {
619
- ele.style.transform = "";
620
- ele.style.opacity = "0";
621
- lineEle.style.display = "none";
1547
+ displacement = { x: 0, y: 0 };
1548
+ }
1549
+ const nudgeDuration = (_p = (_o = this.options.pointerAvoidance) === null || _o === void 0 ? void 0 : _o.transitionDuration) !== null && _p !== void 0 ? _p : 200;
1550
+ const animateNudge = () => {
1551
+ const progress = now < nudge.start ? 0 : nudgeDuration > 0 ? Math.min(1, (now - nudge.start) / nudgeDuration) : 1;
1552
+ const eased = progress * progress * (3 - 2 * progress);
1553
+ nudge.x = nudge.fromX + (nudge.targetX - nudge.fromX) * eased;
1554
+ nudge.y = nudge.fromY + (nudge.targetY - nudge.fromY) * eased;
1555
+ return progress;
1556
+ };
1557
+ animateNudge();
1558
+ if (nudge.targetX !== displacement.x || nudge.targetY !== displacement.y) {
1559
+ nudge = {
1560
+ ...nudge,
1561
+ fromX: nudge.x,
1562
+ fromY: nudge.y,
1563
+ targetX: displacement.x,
1564
+ 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)
1566
+ };
1567
+ }
1568
+ if (animateNudge() < 1 && (nudge.fromX !== nudge.targetX || nudge.fromY !== nudge.targetY)) {
1569
+ this.viewer.scene.requestRender();
1570
+ }
1571
+ position.x += nudge.x;
1572
+ position.y += nudge.y;
1573
+ ele.style.left = `${coordinates.x + position.x * coordinates.scaleX}px`;
1574
+ ele.style.top = `${coordinates.y + position.y * coordinates.scaleY}px`;
1575
+ const clickTarget = content.querySelector('[role="button"]');
1576
+ 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}`);
1578
+ }
1579
+ if (leader) {
1580
+ const nodeRadius = ringAnimation.outer;
1581
+ const anchor = this.anchor;
1582
+ anchor.style.left = `${coordinates.x + (frame.anchorX - nodeRadius) * coordinates.scaleX}px`;
1583
+ anchor.style.top = `${coordinates.y + (frame.anchorY - nodeRadius) * coordinates.scaleY}px`;
1584
+ anchor.style.width = `${nodeRadius * 2 * coordinates.scaleX}px`;
1585
+ anchor.style.height = `${nodeRadius * 2 * coordinates.scaleY}px`;
1586
+ anchor.style.boxSizing = "border-box";
1587
+ anchor.style.background = "transparent";
1588
+ const svg = anchor.firstElementChild;
1589
+ svg.setAttribute("viewBox", `0 0 ${nodeRadius * 2} ${nodeRadius * 2}`);
1590
+ const circle = svg.firstElementChild;
1591
+ circle.setAttribute("cx", String(nodeRadius));
1592
+ circle.setAttribute("cy", String(nodeRadius));
1593
+ circle.setAttribute("r", String((nodeRadius + ringAnimation.inner) / 2));
1594
+ circle.setAttribute("stroke", this.options.leaderColor || "rgba(255, 255, 255, 0.8)");
1595
+ circle.setAttribute("stroke-width", String(nodeRadius - ringAnimation.inner));
1596
+ anchor.style.boxShadow = radius ? "none" : "0 0 5px rgba(0, 0, 0, 0.3)";
1597
+ anchor.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount));
1598
+ anchor.style.zIndex = String(placement.zIndex);
1599
+ // 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;
1602
+ const dx = endX - frame.anchorX, dy = endY - frame.anchorY;
1603
+ const length = Math.hypot(dx, dy);
1604
+ const leaderAngle = Math.atan2(dy, dx);
1605
+ const pointerDx = pointer ? pointer.x - frame.anchorX : 0;
1606
+ const pointerDy = pointer ? pointer.y - frame.anchorY : 0;
1607
+ const desiredAngle = Math.hypot(pointerDx, pointerDy) > 0.5 ? Math.atan2(pointerDy, pointerDx) + Math.PI - leaderAngle : 0;
1608
+ const wrappedAngle = Math.atan2(Math.sin(desiredAngle), Math.cos(desiredAngle));
1609
+ // Keep the stem inside the 126-degree arc, away from antialiased endpoints.
1610
+ const arcFraction = 0.35;
1611
+ 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);
1613
+ 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)));
1615
+ const blend = duration > 0 ? 1 - Math.exp(-elapsed / (duration / 3)) : 1;
1616
+ 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);
1619
+ ringAnimation.arcTime = now;
1620
+ if (ringAnimation.arcOffset !== targetOffset || ringAnimation.arcOpen !== targetOpen) {
1621
+ this.viewer.scene.requestRender();
1622
+ }
1623
+ const circumference = Math.PI * (nodeRadius + ringAnimation.inner);
1624
+ const filledFraction = 1 - ringAnimation.arcOpen * (1 - arcFraction);
1625
+ circle.setAttribute("stroke-dasharray", `${circumference * filledFraction} ${circumference * (1 - filledFraction)}`);
1626
+ circle.setAttribute("transform", `rotate(${(leaderAngle + ringAnimation.arcOffset - Math.PI * filledFraction) * 180 / Math.PI} ${nodeRadius} ${nodeRadius})`);
1627
+ const cut = Math.min(radius, length);
1628
+ const startX = frame.anchorX + (length ? dx / length * cut : 0);
1629
+ 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);
622
1641
  }
623
1642
  };
624
- this.renderRemoval = this.viewer.scene.postUpdate.addEventListener(updateLabel);
625
- updateLabel();
1643
+ ele.style.display = "flex";
1644
+ ele.style.visibility = "hidden";
1645
+ this.renderRemoval = layer.Add({ project, apply });
626
1646
  // If terrain changes then we'll re-call createLabel as the position may have changed.
627
1647
  let terrTimeout = null;
628
1648
  this.terrChangedRemoval = this.viewer.scene.terrainProviderChanged.addEventListener(() => {
@@ -642,6 +1662,9 @@ var EntityLabel;
642
1662
  * Removes label from the viewer and unsubscribes from events.
643
1663
  */
644
1664
  removeLabel() {
1665
+ var _a;
1666
+ (_a = this.anchor) === null || _a === void 0 ? void 0 : _a.remove();
1667
+ this.anchor = null;
645
1668
  if (this.label &&
646
1669
  this.label.parentElement) {
647
1670
  this.label.parentElement.removeChild(this.label);
@@ -667,16 +1690,44 @@ var EntityLabel;
667
1690
  * If one exists then the action is ignored.
668
1691
  */
669
1692
  function Attach(params) {
670
- const { rego, viewer, pos3d, visualRegister, api } = params;
671
- if (!rego || !viewer || rego[EntityLabel.ATTACH_KEY]) {
1693
+ var _a, _b, _c;
1694
+ const { rego, viewer, pos3d, visualRegister, api, options, groupId } = params;
1695
+ if (!rego || !viewer) {
672
1696
  return;
673
1697
  }
1698
+ if (params.persistent == null && visualRegister) {
1699
+ const group = visualRegister.GetLabelGroupFor(rego.entityId, rego.menuItemId);
1700
+ if (group || visualRegister.GetIsLabelled({ entityId: rego.entityId, menuItemId: rego.menuItemId })) {
1701
+ return Attach({
1702
+ ...params,
1703
+ pos3d: undefined,
1704
+ options: (_a = group === null || group === void 0 ? void 0 : group.options) !== null && _a !== void 0 ? _a : options,
1705
+ groupId: (_b = group === null || group === void 0 ? void 0 : group.groupId) !== null && _b !== void 0 ? _b : groupId,
1706
+ persistent: true
1707
+ });
1708
+ }
1709
+ }
1710
+ if (rego[EntityLabel.ATTACH_KEY]) {
1711
+ // Legacy hover callers omit ownership/options and must not reconfigure a register-owned label.
1712
+ if (rego[EntityLabel.ATTACH_KEY].Persistent && params.persistent !== true && !groupId) {
1713
+ return rego[EntityLabel.ATTACH_KEY];
1714
+ }
1715
+ rego[EntityLabel.ATTACH_KEY].Persistent = (_c = params.persistent) !== null && _c !== void 0 ? _c : Boolean(groupId);
1716
+ rego[EntityLabel.ATTACH_KEY].Configure(options, groupId);
1717
+ return rego[EntityLabel.ATTACH_KEY];
1718
+ }
674
1719
  rego[EntityLabel.ATTACH_KEY] = new Label({
675
1720
  rego,
676
1721
  viewer,
677
1722
  pos3d,
678
1723
  visualRegister,
679
- api
1724
+ api,
1725
+ options,
1726
+ groupId,
1727
+ persistent: params.persistent,
1728
+ transient: params.transient,
1729
+ hoverRingState: params.hoverRingState,
1730
+ visible: params.visible
680
1731
  });
681
1732
  return rego[EntityLabel.ATTACH_KEY];
682
1733
  }
@@ -691,12 +1742,19 @@ var EntityLabel;
691
1742
  if (!rego && label) {
692
1743
  rego = label.Rego;
693
1744
  }
1745
+ if (label && label !== (rego === null || rego === void 0 ? void 0 : rego[EntityLabel.ATTACH_KEY])) {
1746
+ if (!label.Persistent || params.force)
1747
+ label.Dispose();
1748
+ return;
1749
+ }
694
1750
  if (!rego || !rego[EntityLabel.ATTACH_KEY]) {
695
1751
  if (label) {
696
1752
  (_a = label.Dispose) === null || _a === void 0 ? void 0 : _a.call(label);
697
1753
  }
698
1754
  return;
699
1755
  }
1756
+ if (rego[EntityLabel.ATTACH_KEY].Persistent && !params.force)
1757
+ return;
700
1758
  (_c = (_b = rego[EntityLabel.ATTACH_KEY]).Dispose) === null || _c === void 0 ? void 0 : _c.call(_b);
701
1759
  rego[EntityLabel.ATTACH_KEY] = null;
702
1760
  }