bruce-cesium 7.5.7 → 7.5.9

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 (25) hide show
  1. package/dist/bruce-cesium.es5.js +1918 -330
  2. package/dist/bruce-cesium.es5.js.map +1 -1
  3. package/dist/bruce-cesium.umd.js +1916 -328
  4. package/dist/bruce-cesium.umd.js.map +1 -1
  5. package/dist/lib/bruce-cesium.js +1 -1
  6. package/dist/lib/rendering/displaced-surface-primitive.js +187 -36
  7. package/dist/lib/rendering/displaced-surface-primitive.js.map +1 -1
  8. package/dist/lib/rendering/entity-render-engine-polygon.js +20 -9
  9. package/dist/lib/rendering/entity-render-engine-polygon.js.map +1 -1
  10. package/dist/lib/rendering/render-managers/common/entity-label.js +975 -57
  11. package/dist/lib/rendering/render-managers/common/entity-label.js.map +1 -1
  12. package/dist/lib/rendering/texture-frame-series-animator.js +220 -20
  13. package/dist/lib/rendering/texture-frame-series-animator.js.map +1 -1
  14. package/dist/lib/rendering/texture-value-labels.js +478 -194
  15. package/dist/lib/rendering/texture-value-labels.js.map +1 -1
  16. package/dist/lib/rendering/visuals-register.js +34 -9
  17. package/dist/lib/rendering/visuals-register.js.map +1 -1
  18. package/dist/types/bruce-cesium.d.ts +1 -1
  19. package/dist/types/rendering/displaced-surface-primitive.d.ts +32 -0
  20. package/dist/types/rendering/entity-render-engine-polygon.d.ts +1 -0
  21. package/dist/types/rendering/render-managers/common/entity-label.d.ts +134 -20
  22. package/dist/types/rendering/texture-frame-series-animator.d.ts +42 -0
  23. package/dist/types/rendering/texture-value-labels.d.ts +59 -11
  24. package/dist/types/rendering/visuals-register.d.ts +7 -0
  25. package/package.json +2 -2
@@ -5530,6 +5530,24 @@
5530
5530
  const PHOTO_CACHE_LIMIT = 500;
5531
5531
  // Key where the viewer keeps its downsized photos.
5532
5532
  const PHOTO_CACHE_KEY = "_nextspaceLabelPhotos";
5533
+ // Clearance around a model's box, as a share of its half-extent on each axis.
5534
+ const MODEL_BOX_PADDING = 0.02;
5535
+ // Share of each edge drawn out from its corners while the box is idle. Hovering draws the edges whole.
5536
+ const MODEL_BOX_BRACKET = 0.2;
5537
+ // Under this many pixels across, a box reads as a smudge and the ring takes over.
5538
+ const MODEL_BOX_MIN_SIZE = 24;
5539
+ // Stem length above the box outline when the options do not set one.
5540
+ const MODEL_BOX_STEM = 32;
5541
+ // Gap in pixels a waiting box needs from a drawn one before it shows, so neighbours never trade places.
5542
+ const MODEL_BOX_CLEARANCE = 12;
5543
+ // Longest a model's label stays hidden waiting to learn whether it has a box, before showing its ring anyway.
5544
+ const MODEL_BOX_WAIT = 1500;
5545
+ // Half the length of edge drawn either side of where a stem lands, when brackets would leave it bare.
5546
+ const MODEL_BOX_LANDING = 10;
5547
+ // Past this many touching boxes one box around them all stands in for tracing their union.
5548
+ const MODEL_BOX_UNION_LIMIT = 24;
5549
+ // Corner pairs of each box edge. Corner i sits on the +X side when bit 0 is set, +Y for bit 1, +Z for bit 2.
5550
+ const MODEL_BOX_EDGES = [[0, 1], [2, 3], [4, 5], [6, 7], [0, 2], [1, 3], [4, 6], [5, 7], [0, 4], [1, 5], [2, 6], [3, 7]];
5533
5551
  // One per tinted icon, since an SVG mask is addressed by id across the whole document.
5534
5552
  let tintCounter = 0;
5535
5553
  // Key where the label instance is stored within the cesium entity.
@@ -5537,8 +5555,6 @@
5537
5555
  EntityLabel.ATTACH_KEY = "nextspace-navigator-entity-label";
5538
5556
  // Image types a label draws, read from the file extension. Anything else stays text.
5539
5557
  const IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp"];
5540
- // Shortest side a thumbnail needs before it is preferred over the full file.
5541
- const IMAGE_THUMBNAIL_MIN = 64;
5542
5558
  // Size a found image is drawn at when the group names none, so a photo never lands at its natural size.
5543
5559
  const IMAGE_SIZE = 24;
5544
5560
  function isImageFile(name) {
@@ -5567,12 +5583,24 @@
5567
5583
  return (url.protocol === "http:" || url.protocol === "https:") && isImageFile(url.pathname);
5568
5584
  }
5569
5585
  EntityLabel.IsImageUrl = IsImageUrl;
5586
+ /*
5587
+ * Device pixels across a photo needs to stay sharp at a chip size, at rest or grown on hover.
5588
+ */
5589
+ function photoPixels(size, hover) {
5590
+ // The glyph plus the chip's padding of a third either side, enlarged for a photo.
5591
+ const diameter = (size + 2 * (size / 3)) * PHOTO_SIZE_SCALE;
5592
+ // Picking an image can run with no window, where a screen pixel is a CSS pixel.
5593
+ const ratio = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1;
5594
+ return Math.ceil(diameter * (hover ? PHOTO_HOVER_SCALE : 1) * ratio);
5595
+ }
5570
5596
  /*
5571
5597
  * The entity's default image as InfoView picks it: its first photo attachment by display order that is an image file.
5572
- * Reads attachments expanded onto the entity, and prefers the smallest thumbnail that still covers an icon.
5598
+ *
5599
+ * Reads attachments expanded onto the entity. Prefers the smallest thumbnail sharp at the hover size, then the
5600
+ * largest still sharp at rest, so a low resolution thumbnail is never stretched while the full file is unread.
5573
5601
  */
5574
5602
  function GetDefaultImageUrl(entity, api) {
5575
- var _a, _b, _c;
5603
+ var _a, _b, _c, _d;
5576
5604
  const attachments = (_b = (_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.Attachment) !== null && _b !== void 0 ? _b : [];
5577
5605
  const photo = attachments
5578
5606
  .filter(x => (x === null || x === void 0 ? void 0 : x["EntityAttachmentType.ID"]) == BModels.EntityAttachmentType.EType.Photo && x.ClientFile)
@@ -5581,9 +5609,12 @@
5581
5609
  if (!photo) {
5582
5610
  return null;
5583
5611
  }
5584
- const thumbnail = ((_c = photo.ClientFile.Thumbnails) !== null && _c !== void 0 ? _c : [])
5585
- .filter(x => Math.min(x.Width, x.Height) >= IMAGE_THUMBNAIL_MIN && (x.URL || x["ClientFile.ID"]))
5586
- .sort((a, b) => a.Width * a.Height - b.Width * b.Height)[0];
5612
+ // How far across a thumbnail is shown: a circle crops to its shortest side, a rectangle keeps its longest.
5613
+ const across = (x) => PhotoShape(x.Width, x.Height) === "circle" ? Math.min(x.Width, x.Height) : Math.max(x.Width, x.Height);
5614
+ const thumbnails = ((_c = photo.ClientFile.Thumbnails) !== null && _c !== void 0 ? _c : [])
5615
+ .filter(x => x.Width > 0 && x.Height > 0 && (x.URL || x["ClientFile.ID"]))
5616
+ .sort((a, b) => a.Width * a.Height - b.Width * b.Height);
5617
+ const thumbnail = (_d = thumbnails.find(x => across(x) >= photoPixels(IMAGE_SIZE, true))) !== null && _d !== void 0 ? _d : thumbnails.reverse().find(x => across(x) >= photoPixels(IMAGE_SIZE, false));
5587
5618
  if (thumbnail) {
5588
5619
  return thumbnail.URL || BModels.ClientFile.GetUrl({ fileId: thumbnail["ClientFile.ID"], api, viaCdn: true });
5589
5620
  }
@@ -6095,6 +6126,7 @@
6095
6126
  // Participants whose failure has been logged, so a label that throws every frame logs once.
6096
6127
  this.failed = new WeakSet();
6097
6128
  this.placementMemory = new PlacementMemory();
6129
+ this.modelBoxes = new ModelBoxes();
6098
6130
  this.forwardingWheel = false;
6099
6131
  this.pendingDrag = null;
6100
6132
  // Null outside a 3D view.
@@ -6333,6 +6365,12 @@
6333
6365
  x: (pointer.x - rect.left) * this.viewer.canvas.clientWidth / rect.width,
6334
6366
  y: (pointer.y - rect.top) * this.viewer.canvas.clientHeight / rect.height
6335
6367
  } : undefined;
6368
+ try {
6369
+ this.modelBoxes.Resolve(this.viewer, this.pointer);
6370
+ }
6371
+ catch (e) {
6372
+ console.error("EntityLabel: model boxes failed to resolve.", e);
6373
+ }
6336
6374
  // This runs inside Cesium's frame, where one throwing label would stop rendering for the whole scene.
6337
6375
  const projected = new Map();
6338
6376
  for (const participant of this.participants) {
@@ -6354,6 +6392,12 @@
6354
6392
  this.logFailure(participant, e);
6355
6393
  }
6356
6394
  }
6395
+ try {
6396
+ this.modelBoxes.Draw(this.viewer, this.element, this.coordinates);
6397
+ }
6398
+ catch (e) {
6399
+ console.error("EntityLabel: model boxes failed to draw.", e);
6400
+ }
6357
6401
  }
6358
6402
  logFailure(participant, error) {
6359
6403
  if (this.failed.has(participant)) {
@@ -6364,6 +6408,728 @@
6364
6408
  }
6365
6409
  }
6366
6410
  let nextLabelId = 0;
6411
+ /*
6412
+ * The record's oriented box in the same Z-up local space as GeometryPivot, or null when it has none.
6413
+ */
6414
+ function ReadOrientedBox(entity) {
6415
+ var _a, _b;
6416
+ const box = (_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.GeometryOrientedBox;
6417
+ const vector = (value) => (value === null || value === void 0 ? void 0 : value.length) === 3 && value.every((item) => Number.isFinite(+item)) ? value.map(Number) : null;
6418
+ const centre = vector(box === null || box === void 0 ? void 0 : box.Centre), half = vector(box === null || box === void 0 ? void 0 : box.HalfExtents);
6419
+ const axes = ((_b = box === null || box === void 0 ? void 0 : box.Axes) === null || _b === void 0 ? void 0 : _b.length) === 3 ? box.Axes.map(vector) : null;
6420
+ return centre && half && (axes === null || axes === void 0 ? void 0 : axes.every(Boolean)) ? { centre, axes, half } : null;
6421
+ }
6422
+ EntityLabel.ReadOrientedBox = ReadOrientedBox;
6423
+ /*
6424
+ * The eight padded world corners of an assembly Entity's geometry box, or null when the record cannot give one.
6425
+ *
6426
+ * GeometryPivotExtents are Z-up half-extents about GeometryPivot, and the placement is the one the API uses to put
6427
+ * Bruce.Location at the pivot: ENU(root) * UCS translation * UCS rotation * AssemblyWorldPosition.
6428
+ */
6429
+ function GetModelBox(entity, shape) {
6430
+ var _a, _b, _c;
6431
+ const bruce = entity === null || entity === void 0 ? void 0 : entity.Bruce;
6432
+ const half = (_b = (_a = bruce === null || bruce === void 0 ? void 0 : bruce.GeometryPivotExtents) === null || _a === void 0 ? void 0 : _a.map) === null || _b === void 0 ? void 0 : _b.call(_a, Number);
6433
+ const matrix = bruce === null || bruce === void 0 ? void 0 : bruce.AssemblyWorldPosition;
6434
+ const root = bruce === null || bruce === void 0 ? void 0 : bruce.AssemblyRootLocation;
6435
+ if ((half === null || half === void 0 ? void 0 : half.length) !== 3 || !half.every(value => Number.isFinite(value) && value >= 0) || Math.max(...half) < 0.01) {
6436
+ return null;
6437
+ }
6438
+ if ((matrix === null || matrix === void 0 ? void 0 : matrix.length) !== 4 || !matrix.every(row => (row === null || row === void 0 ? void 0 : row.length) === 4) || !Number.isFinite(+(root === null || root === void 0 ? void 0 : root.latitude)) || !Number.isFinite(+(root === null || root === void 0 ? void 0 : root.longitude))) {
6439
+ return null;
6440
+ }
6441
+ const pivot = ((_c = bruce.GeometryPivot) === null || _c === void 0 ? void 0 : _c.length) >= 3 ? bruce.GeometryPivot.slice(0, 3).map(Number) : [0, 0, 0];
6442
+ // Without an oriented box the record's own extents are an axis-aligned box about the pivot.
6443
+ const box = shape || { centre: pivot, axes: [[1, 0, 0], [0, 1, 0], [0, 0, 1]], half: half };
6444
+ const transform = bruce.AssemblyWorldTransform;
6445
+ const placement = Cesium.Transforms.eastNorthUpToFixedFrame(Cesium.Cartesian3.fromDegrees(+root.longitude, +root.latitude, EnsureNumber(root.altitude)));
6446
+ Cesium.Matrix4.multiply(placement, Cesium.Matrix4.fromTranslation(new Cesium.Cartesian3(EnsureNumber(transform === null || transform === void 0 ? void 0 : transform.x), EnsureNumber(transform === null || transform === void 0 ? void 0 : transform.y), EnsureNumber(transform === null || transform === void 0 ? void 0 : transform.z))), placement);
6447
+ Cesium.Matrix4.multiply(placement, Cesium.Matrix4.fromRotation(Cesium.Matrix3.fromHeadingPitchRoll(Cesium.HeadingPitchRoll.fromDegrees(EnsureNumber(transform === null || transform === void 0 ? void 0 : transform.heading), EnsureNumber(transform === null || transform === void 0 ? void 0 : transform.pitch), EnsureNumber(transform === null || transform === void 0 ? void 0 : transform.roll)))), placement);
6448
+ Cesium.Matrix4.multiply(placement, new Cesium.Matrix4(...matrix.flat().map(Number)), placement);
6449
+ // Pad thin axes by a share of the longest one, so a flat part still gets a visible gap.
6450
+ const longest = Math.max(...box.half);
6451
+ const reach = box.half.map(value => value + Math.max(value, longest * 0.25) * MODEL_BOX_PADDING);
6452
+ const corners = [];
6453
+ for (let i = 0; i < 8; i++) {
6454
+ const signs = [i & 1 ? 1 : -1, i & 2 ? 1 : -1, i & 4 ? 1 : -1];
6455
+ const local = new Cesium.Cartesian3(...[0, 1, 2].map(k => box.centre[k] +
6456
+ signs[0] * reach[0] * box.axes[0][k] + signs[1] * reach[1] * box.axes[1][k] + signs[2] * reach[2] * box.axes[2][k]));
6457
+ const corner = Cesium.Matrix4.multiplyByPoint(placement, local, new Cesium.Cartesian3());
6458
+ if (![corner.x, corner.y, corner.z].every(Number.isFinite)) {
6459
+ return null;
6460
+ }
6461
+ corners.push(corner);
6462
+ }
6463
+ return corners;
6464
+ }
6465
+ EntityLabel.GetModelBox = GetModelBox;
6466
+ const vAdd = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
6467
+ const vSub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
6468
+ const vScale = (a, s) => [a[0] * s, a[1] * s, a[2] * s];
6469
+ const vDot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
6470
+ const vCross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
6471
+ const vLength = (a) => Math.hypot(a[0], a[1], a[2]);
6472
+ /*
6473
+ * Reads a solid back from the eight corners GetModelBox gives, relative to the origin.
6474
+ */
6475
+ function ModelSolid(corners, origin) {
6476
+ const local = corners.map((corner) => [corner.x - origin.x, corner.y - origin.y, corner.z - origin.z]);
6477
+ const spans = [1, 2, 4].map(bit => vSub(local[bit], local[0]));
6478
+ const half = spans.map(span => vLength(span) / 2);
6479
+ return {
6480
+ centre: vScale(vAdd(local[0], local[7]), 0.5),
6481
+ axes: spans.map((span, k) => vScale(span, 0.5 / (half[k] || 1))),
6482
+ half
6483
+ };
6484
+ }
6485
+ EntityLabel.ModelSolid = ModelSolid;
6486
+ function solidCorners(solid) {
6487
+ const corners = [];
6488
+ for (let i = 0; i < 8; i++) {
6489
+ corners.push([0, 1, 2].reduce((point, k) => vAdd(point, vScale(solid.axes[k], solid.half[k] * (i & (1 << k) ? 1 : -1))), solid.centre));
6490
+ }
6491
+ return corners;
6492
+ }
6493
+ function solidContains(solid, point, inset = 0) {
6494
+ const offset = vSub(point, solid.centre);
6495
+ return solid.axes.every((axis, k) => Math.abs(vDot(offset, axis)) < solid.half[k] - inset);
6496
+ }
6497
+ /*
6498
+ * Whether two solids overlap, by the separating axis test. Padding makes parts that sit against each other overlap.
6499
+ */
6500
+ function ModelSolidsTouch(a, b) {
6501
+ const between = vSub(b.centre, a.centre);
6502
+ const axes = [...a.axes, ...b.axes];
6503
+ for (const first of a.axes) {
6504
+ for (const second of b.axes) {
6505
+ const axis = vCross(first, second);
6506
+ const length = vLength(axis);
6507
+ if (length > 1e-6)
6508
+ axes.push(vScale(axis, 1 / length));
6509
+ }
6510
+ }
6511
+ const reach = (solid, axis) => solid.axes.reduce((sum, own, k) => sum + solid.half[k] * Math.abs(vDot(own, axis)), 0);
6512
+ return axes.every(axis => Math.abs(vDot(between, axis)) <= reach(a, axis) + reach(b, axis));
6513
+ }
6514
+ EntityLabel.ModelSolidsTouch = ModelSolidsTouch;
6515
+ /*
6516
+ * One box around every solid, turned like the largest of them, for groups too big to trace.
6517
+ */
6518
+ function encloseSolids(solids) {
6519
+ const largest = solids.reduce((best, solid) => solid.half[0] * solid.half[1] * solid.half[2] > best.half[0] * best.half[1] * best.half[2] ? solid : best);
6520
+ const corners = solids.flatMap(solidCorners);
6521
+ const ranges = largest.axes.map(axis => {
6522
+ const values = corners.map(corner => vDot(corner, axis));
6523
+ return [Math.min(...values), Math.max(...values)];
6524
+ });
6525
+ return {
6526
+ centre: largest.axes.reduce((point, axis, k) => vAdd(point, vScale(axis, (ranges[k][0] + ranges[k][1]) / 2)), [0, 0, 0]),
6527
+ axes: largest.axes,
6528
+ half: ranges.map(([low, high]) => (high - low) / 2)
6529
+ };
6530
+ }
6531
+ /*
6532
+ * Narrows [low, high] to where a + b * t stays within -half..half.
6533
+ */
6534
+ function clipSlab(range, a, b, half) {
6535
+ if (Math.abs(b) < 1e-12) {
6536
+ if (Math.abs(a) > half)
6537
+ range[1] = -Infinity;
6538
+ return;
6539
+ }
6540
+ const first = (-half - a) / b, second = (half - a) / b;
6541
+ range[0] = Math.max(range[0], Math.min(first, second));
6542
+ range[1] = Math.min(range[1], Math.max(first, second));
6543
+ }
6544
+ /*
6545
+ * The edges of the union of the solids: where its surface folds, leaving out seams across flat shared faces.
6546
+ */
6547
+ function ModelBoxOutline(solids) {
6548
+ const span = Math.max(...solids.map(solid => vLength(solid.half)));
6549
+ const thinnest = Math.min(...solids.flatMap(solid => solid.half));
6550
+ const radius = Math.max(1e-6, Math.min(thinnest * 0.05, span * 1e-3));
6551
+ const tolerance = Math.max(1, span) * 1e-6;
6552
+ const candidates = [];
6553
+ for (const solid of solids) {
6554
+ const corners = solidCorners(solid);
6555
+ for (const [a, b] of MODEL_BOX_EDGES)
6556
+ candidates.push([corners[a], corners[b]]);
6557
+ }
6558
+ const faces = (solid) => [0, 1, 2].flatMap(k => [1, -1].map(sign => ({ solid, k, normal: vScale(solid.axes[k], sign) })));
6559
+ for (let i = 0; i < solids.length; i++) {
6560
+ for (let j = i + 1; j < solids.length; j++) {
6561
+ if (!ModelSolidsTouch(solids[i], solids[j])) {
6562
+ continue;
6563
+ }
6564
+ for (const first of faces(solids[i])) {
6565
+ for (const second of faces(solids[j])) {
6566
+ const direction = vCross(first.normal, second.normal);
6567
+ const sine = vLength(direction);
6568
+ // Faces this close to parallel meet along edges the box edges already give.
6569
+ if (sine < 0.05) {
6570
+ continue;
6571
+ }
6572
+ const d1 = vDot(first.normal, first.solid.centre) + first.solid.half[first.k];
6573
+ const d2 = vDot(second.normal, second.solid.centre) + second.solid.half[second.k];
6574
+ const cosine = vDot(first.normal, second.normal);
6575
+ const base = vScale(vAdd(vScale(first.normal, d1 - d2 * cosine), vScale(second.normal, d2 - d1 * cosine)), 1 / (sine * sine));
6576
+ const unit = vScale(direction, 1 / sine);
6577
+ const range = [-Infinity, Infinity];
6578
+ for (const face of [first, second]) {
6579
+ const offset = vSub(base, face.solid.centre);
6580
+ for (let m = 0; m < 3; m++) {
6581
+ if (m !== face.k)
6582
+ clipSlab(range, vDot(offset, face.solid.axes[m]), vDot(unit, face.solid.axes[m]), face.solid.half[m]);
6583
+ }
6584
+ }
6585
+ if (range[1] - range[0] > tolerance) {
6586
+ candidates.push([vAdd(base, vScale(unit, range[0])), vAdd(base, vScale(unit, range[1]))]);
6587
+ }
6588
+ }
6589
+ }
6590
+ }
6591
+ }
6592
+ const inside = (point) => solids.some(solid => solidContains(solid, point));
6593
+ /*
6594
+ * Whether the union folds along a direction at a point, from which way round it the inside lies.
6595
+ */
6596
+ const folds = (point, along) => {
6597
+ const first = vCross(along, Math.abs(along[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0]);
6598
+ const e1 = vScale(first, 1 / vLength(first));
6599
+ const e2 = vCross(along, e1);
6600
+ const samples = [];
6601
+ for (let i = 0; i < 16; i++) {
6602
+ // Offset from the axes so no sample lands exactly on a face through the point.
6603
+ const angle = (i + 0.37) * Math.PI / 8;
6604
+ samples.push(inside(vAdd(point, vAdd(vScale(e1, Math.cos(angle) * radius), vScale(e2, Math.sin(angle) * radius)))));
6605
+ }
6606
+ const filled = samples.filter(Boolean).length;
6607
+ const changes = samples.filter((sample, i) => sample !== samples[(i + 1) % 16]).length;
6608
+ return filled > 0 && filled < 16 && !(filled === 8 && changes === 2);
6609
+ };
6610
+ const lines = [];
6611
+ for (const [start, end] of candidates) {
6612
+ const delta = vSub(end, start);
6613
+ const length = vLength(delta);
6614
+ if (length < tolerance) {
6615
+ continue;
6616
+ }
6617
+ const along = vScale(delta, 1 / length);
6618
+ const cuts = [0, 1];
6619
+ for (const solid of solids) {
6620
+ const offset = vSub(start, solid.centre);
6621
+ solid.axes.forEach((axis, k) => {
6622
+ const rate = vDot(delta, axis);
6623
+ if (Math.abs(rate) < 1e-12)
6624
+ return;
6625
+ for (const bound of [-solid.half[k], solid.half[k]]) {
6626
+ const t = (bound - vDot(offset, axis)) / rate;
6627
+ if (t > 0 && t < 1)
6628
+ cuts.push(t);
6629
+ }
6630
+ });
6631
+ }
6632
+ cuts.sort((a, b) => a - b);
6633
+ for (let i = 0; i + 1 < cuts.length; i++) {
6634
+ if ((cuts[i + 1] - cuts[i]) * length < tolerance || !folds(vAdd(start, vScale(delta, (cuts[i] + cuts[i + 1]) / 2)), along)) {
6635
+ continue;
6636
+ }
6637
+ // Merged by line, so a shared edge draws once and a run cut by other boxes is one stroke.
6638
+ const from = vAdd(start, vScale(delta, cuts[i])), to = vAdd(start, vScale(delta, cuts[i + 1]));
6639
+ let line = lines.find(item => vLength(vCross(item.direction, along)) < 1e-6 && vLength(vCross(vSub(from, item.origin), item.direction)) < tolerance);
6640
+ if (!line) {
6641
+ line = { origin: from, direction: along, spans: [] };
6642
+ lines.push(line);
6643
+ }
6644
+ const a = vDot(vSub(from, line.origin), line.direction), b = vDot(vSub(to, line.origin), line.direction);
6645
+ line.spans.push([Math.min(a, b), Math.max(a, b)]);
6646
+ }
6647
+ }
6648
+ const outline = [];
6649
+ for (const line of lines) {
6650
+ line.spans.sort((a, b) => a[0] - b[0]);
6651
+ let current = line.spans[0];
6652
+ for (const next of [...line.spans.slice(1), null]) {
6653
+ if (next && next[0] <= current[1] + tolerance) {
6654
+ current = [current[0], Math.max(current[1], next[1])];
6655
+ continue;
6656
+ }
6657
+ outline.push([vAdd(line.origin, vScale(line.direction, current[0])), vAdd(line.origin, vScale(line.direction, current[1]))]);
6658
+ current = next;
6659
+ }
6660
+ }
6661
+ return outline;
6662
+ }
6663
+ EntityLabel.ModelBoxOutline = ModelBoxOutline;
6664
+ /*
6665
+ * Whether the sight line from the eye to a point passes through any solid before reaching it.
6666
+ */
6667
+ function solidsHide(solids, eye, point, inset) {
6668
+ const ray = vSub(point, eye);
6669
+ const length = vLength(ray);
6670
+ // Stop just short of the point so the face it lies on does not count against it.
6671
+ const reach = Math.max(0, 1 - inset * 100 / (length || 1));
6672
+ return solids.some(solid => {
6673
+ const offset = vSub(eye, solid.centre);
6674
+ const range = [0, reach];
6675
+ solid.axes.forEach((axis, k) => clipSlab(range, vDot(offset, axis), vDot(ray, axis), solid.half[k] - inset));
6676
+ return range[0] < range[1];
6677
+ });
6678
+ }
6679
+ /*
6680
+ * Window positions of a box's corners, or null when one is behind the camera and the box would draw inside out.
6681
+ */
6682
+ function projectCorners(viewer, corners) {
6683
+ const camera = viewer.camera;
6684
+ const cameraPosition = camera.positionWC || camera.position;
6685
+ const direction = camera.directionWC || camera.direction;
6686
+ const transforms = Cesium.SceneTransforms;
6687
+ const projectPosition = transforms.wgs84ToWindowCoordinates || transforms.worldToWindowCoordinates;
6688
+ const points = [];
6689
+ const relative = new Cesium.Cartesian3();
6690
+ for (const corner of corners) {
6691
+ if (Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(corner, cameraPosition, relative), direction) <= 0) {
6692
+ return null;
6693
+ }
6694
+ const point = projectPosition(viewer.scene, corner);
6695
+ if (!Number.isFinite(point === null || point === void 0 ? void 0 : point.x) || !Number.isFinite(point === null || point === void 0 ? void 0 : point.y)) {
6696
+ return null;
6697
+ }
6698
+ points.push({ x: point.x, y: point.y });
6699
+ }
6700
+ return points;
6701
+ }
6702
+ /*
6703
+ * Whether two convex outlines come within a margin of each other, by the separating axis test.
6704
+ */
6705
+ function HullsNear(a, b, margin) {
6706
+ for (const hull of [a, b]) {
6707
+ for (let i = 0; i < hull.length; i++) {
6708
+ const from = hull[i], to = hull[(i + 1) % hull.length];
6709
+ const length = Math.hypot(to.x - from.x, to.y - from.y);
6710
+ if (length < 1e-9) {
6711
+ continue;
6712
+ }
6713
+ const normal = { x: (from.y - to.y) / length, y: (to.x - from.x) / length };
6714
+ const project = (points) => points.map(point => point.x * normal.x + point.y * normal.y);
6715
+ const first = project(a), second = project(b);
6716
+ if (Math.max(Math.min(...second) - Math.max(...first), Math.min(...first) - Math.max(...second)) >= margin) {
6717
+ return false;
6718
+ }
6719
+ }
6720
+ }
6721
+ return true;
6722
+ }
6723
+ EntityLabel.HullsNear = HullsNear;
6724
+ /*
6725
+ * Decides which labels' model boxes are drawn and how, so a multi-selection does not stack box over box.
6726
+ */
6727
+ class ModelBoxes {
6728
+ constructor() {
6729
+ // Whether a box competes for space comes only from inputs (camera distance, hover, the pointer), never from
6730
+ // how its label was laid out, so an animation moving a label cannot feed back into which boxes show.
6731
+ this.entries = new Map();
6732
+ this.groups = null;
6733
+ // The hovered Entity. Its box is drawn on its own and whole, ahead of every other box.
6734
+ this.hovered = null;
6735
+ this.outlines = new Map();
6736
+ this.hulls = new Map();
6737
+ this.frames = new Map();
6738
+ this.drawn = [];
6739
+ // When each label's box started showing; the longer it has shown, the stronger its claim on the space.
6740
+ this.since = new Map();
6741
+ this.requests = new Map();
6742
+ this.looks = new Map();
6743
+ this.memberLooks = new Map();
6744
+ }
6745
+ Set(id, corners, entityId, inputs) {
6746
+ const current = this.entries.get(id);
6747
+ // A label rebuilt over the same box keeps its place and how far it has animated.
6748
+ if (corners && (current === null || current === void 0 ? void 0 : current.corners) === corners && current.entityId === entityId) {
6749
+ Object.assign(current, inputs);
6750
+ return;
6751
+ }
6752
+ if (!corners && !current) {
6753
+ return;
6754
+ }
6755
+ if (corners) {
6756
+ this.entries.set(id, { ...inputs, corners, entityId, active: false, wanted: false });
6757
+ }
6758
+ else
6759
+ this.entries.delete(id);
6760
+ this.since.delete(id);
6761
+ this.groups = null;
6762
+ for (const [key, group] of this.outlines) {
6763
+ if (group.members.includes(id))
6764
+ this.outlines.delete(key);
6765
+ }
6766
+ }
6767
+ /*
6768
+ * The box drawn for a label this frame, or undefined when it has none or lost its place.
6769
+ */
6770
+ Frame(id) {
6771
+ return this.frames.get(id);
6772
+ }
6773
+ /*
6774
+ * The label's own box on screen whether or not it is drawn, so hovering the model can ask for it.
6775
+ */
6776
+ Hull(id) {
6777
+ return this.hulls.get(id);
6778
+ }
6779
+ Request(id, request) {
6780
+ this.requests.set(id, request);
6781
+ }
6782
+ group() {
6783
+ const ids = Array.from(this.entries.keys()).filter(id => this.entries.get(id).wanted).sort();
6784
+ if (!ids.length) {
6785
+ return [];
6786
+ }
6787
+ const origin = this.entries.get(ids[0]).corners[0];
6788
+ const solids = new Map(ids.map(id => [id, ModelSolid(this.entries.get(id).corners, origin)]));
6789
+ const parent = new Map(ids.map(id => [id, id]));
6790
+ const root = (id) => parent.get(id) === id ? id : root(parent.get(id));
6791
+ for (let i = 0; i < ids.length; i++) {
6792
+ const a = solids.get(ids[i]);
6793
+ for (let j = i + 1; j < ids.length; j++) {
6794
+ const b = solids.get(ids[j]);
6795
+ if ((this.entries.get(ids[i]).entityId === this.hovered) !== (this.entries.get(ids[j]).entityId === this.hovered)) {
6796
+ continue;
6797
+ }
6798
+ // Cheap sphere check first; most pairs in a scene are nowhere near each other.
6799
+ if (vLength(vSub(a.centre, b.centre)) > vLength(a.half) + vLength(b.half) || !ModelSolidsTouch(a, b)) {
6800
+ continue;
6801
+ }
6802
+ parent.set(root(ids[j]), root(ids[i]));
6803
+ }
6804
+ }
6805
+ const members = new Map();
6806
+ for (const id of ids) {
6807
+ const key = root(id);
6808
+ if (!members.has(key))
6809
+ members.set(key, []);
6810
+ members.get(key).push(id);
6811
+ }
6812
+ const groups = Array.from(members.values()).map(list => {
6813
+ const key = list.join("|");
6814
+ let group = this.outlines.get(key);
6815
+ if (!group) {
6816
+ const groupOrigin = this.entries.get(list[0]).corners[0];
6817
+ const parts = list.map(id => ModelSolid(this.entries.get(id).corners, groupOrigin));
6818
+ const solidsShown = parts.length > MODEL_BOX_UNION_LIMIT ? [encloseSolids(parts)] : parts;
6819
+ group = { key, members: list, hovered: false, origin: groupOrigin, solids: solidsShown, outline: ModelBoxOutline(solidsShown) };
6820
+ }
6821
+ group.hovered = this.entries.get(list[0]).entityId === this.hovered;
6822
+ return group;
6823
+ });
6824
+ // Kept across hover changes, so pulling a part out and putting it back does not trace the union again.
6825
+ const kept = Array.from(this.outlines.values()).filter(group => !groups.includes(group)).slice(-8);
6826
+ this.outlines = new Map([...kept, ...groups].map(group => [group.key, group]));
6827
+ return groups;
6828
+ }
6829
+ /*
6830
+ * Projects every box for this frame and settles which outlines are drawn, before labels place their stems.
6831
+ */
6832
+ Resolve(viewer, pointer) {
6833
+ var _a, _b, _c, _d;
6834
+ this.frames.clear();
6835
+ this.hulls.clear();
6836
+ this.drawn = [];
6837
+ const points = new Map();
6838
+ for (const [id, entry] of this.entries) {
6839
+ const projected = projectCorners(viewer, entry.corners);
6840
+ if (projected) {
6841
+ points.set(id, projected);
6842
+ this.hulls.set(id, convexHull(projected));
6843
+ }
6844
+ }
6845
+ // The application's hover, which is what its temporary label shows, decides first. Without one the
6846
+ // pointer does: the part it is on holds until it leaves, else the smallest box under it wins.
6847
+ const under = Array.from(this.hulls.entries()).filter(([, hull]) => pointer && hullContains(hull, pointer));
6848
+ let hovered = (_b = (_a = Array.from(this.entries.values()).find(entry => { var _a; return (_a = entry.hovered) === null || _a === void 0 ? void 0 : _a.call(entry); })) === null || _a === void 0 ? void 0 : _a.entityId) !== null && _b !== void 0 ? _b : (under.some(([id]) => this.entries.get(id).entityId === this.hovered) ? this.hovered : null);
6849
+ if (!hovered && under.length) {
6850
+ hovered = this.entries.get(under.map(([id, hull]) => ({ id, area: hullArea(hull) })).sort((a, b) => a.area - b.area || a.id.localeCompare(b.id))[0].id).entityId;
6851
+ }
6852
+ if (hovered !== this.hovered) {
6853
+ this.hovered = hovered;
6854
+ this.groups = null;
6855
+ }
6856
+ for (const [id, entry] of this.entries) {
6857
+ const hull = this.hulls.get(id);
6858
+ entry.active = Boolean(((_c = entry.near) === null || _c === void 0 ? void 0 : _c.call(entry)) || entry.entityId === hovered || (pointer && hull && hullContains(hull, pointer)));
6859
+ // A box keeps its place while it fades out, and leaves the competition once it has.
6860
+ const wanted = entry.active || ((_d = this.memberLooks.get(id)) === null || _d === void 0 ? void 0 : _d.shown) > 0.01;
6861
+ if (wanted !== entry.wanted) {
6862
+ entry.wanted = wanted;
6863
+ this.groups = null;
6864
+ }
6865
+ }
6866
+ if (!this.groups) {
6867
+ this.groups = this.group();
6868
+ }
6869
+ const canvas = viewer.canvas;
6870
+ const candidates = [];
6871
+ for (const group of this.groups) {
6872
+ if (!group.members.every(id => points.has(id))) {
6873
+ continue;
6874
+ }
6875
+ const all = group.members.flatMap(id => points.get(id));
6876
+ const xs = all.map(point => point.x), ys = all.map(point => point.y);
6877
+ const width = Math.max(...xs) - Math.min(...xs), height = Math.max(...ys) - Math.min(...ys);
6878
+ if (Math.max(width, height) < MODEL_BOX_MIN_SIZE || width > canvas.clientWidth * 2 || height > canvas.clientHeight * 2) {
6879
+ continue;
6880
+ }
6881
+ const since = Math.min(...group.members.map(id => { var _a; return (_a = this.since.get(id)) !== null && _a !== void 0 ? _a : Infinity; }));
6882
+ const outline = convexHull(all);
6883
+ candidates.push({ group, outline, selected: group.members.some(id => { var _a, _b; return (_b = (_a = this.entries.get(id)).selected) === null || _b === void 0 ? void 0 : _b.call(_a); }), since, area: hullArea(outline) });
6884
+ }
6885
+ // Between boxes appearing together the larger on screen goes first, as the one the view is about.
6886
+ // What the user points at now outranks what they selected before it.
6887
+ candidates.sort((a, b) => (Number(b.group.hovered) - Number(a.group.hovered) ||
6888
+ Number(b.selected) - Number(a.selected) ||
6889
+ a.since - b.since ||
6890
+ b.area - a.area ||
6891
+ a.group.key.localeCompare(b.group.key)));
6892
+ const transforms = Cesium.SceneTransforms;
6893
+ const projectPosition = transforms.wgs84ToWindowCoordinates || transforms.worldToWindowCoordinates;
6894
+ const scratch = new Cesium.Cartesian3();
6895
+ const now = Date.now();
6896
+ const shown = new Set();
6897
+ for (const candidate of candidates) {
6898
+ // A showing box holds its place until truly overlapped; a waiting one needs clear space.
6899
+ const margin = Number.isFinite(candidate.since) ? 0 : MODEL_BOX_CLEARANCE;
6900
+ if (this.drawn.some(other => HullsNear(candidate.outline, other.outline, margin))) {
6901
+ continue;
6902
+ }
6903
+ const { origin } = candidate.group;
6904
+ const toScreen = (point) => {
6905
+ const projected = projectPosition(viewer.scene, Cesium.Cartesian3.fromElements(origin.x + point[0], origin.y + point[1], origin.z + point[2], scratch));
6906
+ return { x: projected.x, y: projected.y };
6907
+ };
6908
+ const edges = candidate.group.outline.map(([start, end]) => [toScreen(start), toScreen(end)]);
6909
+ const drawn = { ...candidate, edges, landings: [] };
6910
+ this.drawn.push(drawn);
6911
+ for (const id of candidate.group.members) {
6912
+ shown.add(id);
6913
+ if (!this.since.has(id))
6914
+ this.since.set(id, now);
6915
+ const own = points.get(id);
6916
+ // The silhouette's top is the highest drawn edge at that x, so the stem always meets a line.
6917
+ const x = (Math.min(...own.map(point => point.x)) + Math.max(...own.map(point => point.x))) / 2;
6918
+ let anchor = null, edge = -1;
6919
+ edges.forEach(([a, b], i) => {
6920
+ if (x < Math.min(a.x, b.x) || x > Math.max(a.x, b.x) || Math.abs(b.x - a.x) < 1e-6) {
6921
+ return;
6922
+ }
6923
+ const y = a.y + (b.y - a.y) * (x - a.x) / (b.x - a.x);
6924
+ if (!anchor || y < anchor.y) {
6925
+ anchor = { x, y };
6926
+ edge = i;
6927
+ }
6928
+ });
6929
+ anchor = anchor || { x, y: hullTop(candidate.outline, x) };
6930
+ if (edge >= 0)
6931
+ drawn.landings.push({ edge, point: anchor });
6932
+ this.frames.set(id, { points: own, hull: this.hulls.get(id), outline: candidate.outline, anchor, group: candidate.group.key });
6933
+ }
6934
+ }
6935
+ for (const id of Array.from(this.since.keys())) {
6936
+ if (!shown.has(id))
6937
+ this.since.delete(id);
6938
+ }
6939
+ this.requests.clear();
6940
+ }
6941
+ /*
6942
+ * Draws each settled outline as corner brackets that grow into whole edges, back edges dashed.
6943
+ */
6944
+ Draw(viewer, element, coordinates) {
6945
+ var _a, _b, _c;
6946
+ const now = Date.now();
6947
+ const looks = new Map();
6948
+ const memberLooks = new Map();
6949
+ let animating = false;
6950
+ for (const { group } of this.drawn) {
6951
+ const requests = group.members.map(id => this.requests.get(id)).filter(Boolean);
6952
+ if (!requests.length) {
6953
+ // Its labels are hidden this frame, perhaps off screen; it keeps its look so it does not regrow.
6954
+ const kept = this.looks.get(group.key);
6955
+ if (kept) {
6956
+ looks.set(group.key, kept);
6957
+ group.members.forEach(id => memberLooks.set(id, kept));
6958
+ if (kept.svg)
6959
+ kept.svg.style.display = "none";
6960
+ }
6961
+ continue;
6962
+ }
6963
+ // A group that gained or lost a member carries on from where its members' outline was.
6964
+ const previous = group.members.map(id => this.memberLooks.get(id)).filter(Boolean);
6965
+ const look = (_a = this.looks.get(group.key)) !== null && _a !== void 0 ? _a : {
6966
+ shown: Math.max(0, ...previous.map(item => item.shown)), whole: Math.max(0, ...previous.map(item => item.whole)),
6967
+ grow: Math.max(0, ...previous.map(item => item.grow)), time: now, svg: (_b = previous.find(item => item.svg)) === null || _b === void 0 ? void 0 : _b.svg
6968
+ };
6969
+ const elapsed = Math.min(32, Math.max(0, now - look.time));
6970
+ const duration = Math.min(...requests.map(request => request.duration));
6971
+ const blend = duration > 0 ? 1 - Math.exp(-elapsed / (duration / 3)) : 1;
6972
+ const approach = (value, to) => Math.abs(value - to) < 0.001 ? to : value + (to - value) * blend;
6973
+ const shownTarget = group.members.some(id => { var _a; return (_a = this.entries.get(id)) === null || _a === void 0 ? void 0 : _a.active; }) ? 1 : 0;
6974
+ const wholeTarget = group.hovered || requests.some(request => request.whole) ? 1 : 0;
6975
+ look.shown = approach(look.shown, shownTarget);
6976
+ look.whole = approach(look.whole, wholeTarget);
6977
+ look.grow = approach(look.grow, 1);
6978
+ look.time = now;
6979
+ animating = animating || look.shown !== shownTarget || look.whole !== wholeTarget || look.grow !== 1;
6980
+ looks.set(group.key, look);
6981
+ group.members.forEach(id => memberLooks.set(id, look));
6982
+ if (look.svg && this.looks.get(group.key) !== look) {
6983
+ // Inherited from an earlier group; claim it so that group's cleanup leaves it alone.
6984
+ for (const [key, other] of this.looks) {
6985
+ if (other.svg === look.svg && key !== group.key)
6986
+ other.svg = undefined;
6987
+ }
6988
+ }
6989
+ this.drawGroup(viewer, element, coordinates, this.drawn.find(item => item.group === group), look, requests);
6990
+ }
6991
+ for (const [key, look] of this.looks) {
6992
+ if (!looks.has(key))
6993
+ (_c = look.svg) === null || _c === void 0 ? void 0 : _c.remove();
6994
+ }
6995
+ this.looks = looks;
6996
+ this.memberLooks = memberLooks;
6997
+ if (animating) {
6998
+ viewer.scene.requestRender();
6999
+ }
7000
+ }
7001
+ drawGroup(viewer, element, coordinates, drawn, look, requests) {
7002
+ const group = drawn.group;
7003
+ let svg = look.svg;
7004
+ if (!svg) {
7005
+ const ns = "http://www.w3.org/2000/svg";
7006
+ svg = document.createElementNS(ns, "svg");
7007
+ svg.setAttribute("class", "entity-label-model-box");
7008
+ svg.setAttribute("aria-hidden", "true");
7009
+ svg.style.cssText = "position:absolute;width:1px;height:1px;overflow:visible;pointer-events:none;filter:drop-shadow(0 0 2px rgba(0, 0, 0, 0.4));";
7010
+ for (const edges of ["back", "front"]) {
7011
+ const path = document.createElementNS(ns, "path");
7012
+ path.setAttribute("fill", "none");
7013
+ path.setAttribute("stroke-linecap", "round");
7014
+ path.dataset.edges = edges;
7015
+ svg.appendChild(path);
7016
+ }
7017
+ look.svg = svg;
7018
+ }
7019
+ if (svg.parentElement !== element) {
7020
+ element.appendChild(svg);
7021
+ }
7022
+ svg.style.display = "";
7023
+ const transforms = Cesium.SceneTransforms;
7024
+ const projectPosition = transforms.wgs84ToWindowCoordinates || transforms.worldToWindowCoordinates;
7025
+ const origin = group.origin;
7026
+ const scratch = new Cesium.Cartesian3();
7027
+ // A new box grows out of the ring it replaces.
7028
+ const grown = look.grow * look.grow * (3 - 2 * look.grow);
7029
+ const from = requests[0].from;
7030
+ const at = (point) => `${(from.x + (point.x - from.x) * grown) * coordinates.scaleX} ${(from.y + (point.y - from.y) * grown) * coordinates.scaleY}`;
7031
+ const toScreen = (point) => {
7032
+ const projected = projectPosition(viewer.scene, Cesium.Cartesian3.fromElements(origin.x + point[0], origin.y + point[1], origin.z + point[2], scratch));
7033
+ return at(projected);
7034
+ };
7035
+ const camera = viewer.camera.positionWC || viewer.camera.position;
7036
+ const eye = [camera.x - origin.x, camera.y - origin.y, camera.z - origin.z];
7037
+ const span = Math.max(...group.solids.map(solid => vLength(solid.half)));
7038
+ const inset = Math.max(1, span) * 1e-6;
7039
+ const fraction = MODEL_BOX_BRACKET + (0.5 - MODEL_BOX_BRACKET) * look.whole;
7040
+ const parts = fraction >= 0.499 ? [[0, 1]] : [[0, fraction], [1 - fraction, 1]];
7041
+ const paths = { front: "", back: "" };
7042
+ for (const [start, end] of group.outline) {
7043
+ const delta = vSub(end, start);
7044
+ // Sample visibility along the edge, since another member can hide part of it.
7045
+ const steps = group.solids.length > 1 ? Math.min(24, Math.max(2, Math.ceil(vLength(delta) / span * 12))) : 1;
7046
+ const visible = [];
7047
+ for (let i = 0; i < steps; i++) {
7048
+ visible.push(!solidsHide(group.solids, eye, vAdd(start, vScale(delta, (i + 0.5) / steps)), inset));
7049
+ }
7050
+ for (const [from, to] of parts) {
7051
+ let runStart = from;
7052
+ for (let i = Math.floor(from * steps); i < steps && i / steps < to; i++) {
7053
+ const runEnd = Math.min(to, (i + 1) / steps);
7054
+ if (runEnd < to && visible[i + 1] === visible[i]) {
7055
+ continue;
7056
+ }
7057
+ paths[visible[i] ? "front" : "back"] += `M${toScreen(vAdd(start, vScale(delta, runStart)))}L${toScreen(vAdd(start, vScale(delta, runEnd)))}`;
7058
+ runStart = runEnd;
7059
+ }
7060
+ }
7061
+ }
7062
+ // Each stem lands on an edge, which brackets may leave bare, so a short piece of it is drawn there.
7063
+ if (fraction < 0.499) {
7064
+ for (const { edge, point } of drawn.landings) {
7065
+ const [a, b] = drawn.edges[edge];
7066
+ const length = Math.hypot(b.x - a.x, b.y - a.y);
7067
+ const reach = Math.min(MODEL_BOX_LANDING, length / 2);
7068
+ const toward = (sign) => {
7069
+ const along = Math.max(0, Math.min(length, Math.hypot(point.x - a.x, point.y - a.y) + sign * reach));
7070
+ return { x: a.x + (b.x - a.x) * along / length, y: a.y + (b.y - a.y) * along / length };
7071
+ };
7072
+ paths.front += `M${at(toward(-1))}L${at(toward(1))}`;
7073
+ }
7074
+ }
7075
+ svg.style.left = `${coordinates.x}px`;
7076
+ svg.style.top = `${coordinates.y}px`;
7077
+ svg.style.opacity = String(Math.max(...requests.map(request => request.opacity)) * look.shown * Math.min(1, look.grow * 2));
7078
+ svg.style.zIndex = String(Math.max(...requests.map(request => request.zIndex)));
7079
+ const color = requests[0].color;
7080
+ for (const path of Array.from(svg.children)) {
7081
+ const front = path.dataset.edges === "front";
7082
+ path.setAttribute("d", paths[front ? "front" : "back"]);
7083
+ path.setAttribute("stroke", color);
7084
+ path.setAttribute("stroke-width", front ? "1.5" : "1");
7085
+ path.setAttribute("stroke-opacity", front ? "1" : "0.35");
7086
+ path.setAttribute("stroke-dasharray", front ? "" : "3 3");
7087
+ }
7088
+ }
7089
+ }
7090
+ EntityLabel.ModelBoxes = ModelBoxes;
7091
+ /*
7092
+ * Andrew's monotone chain, counter-clockwise in screen space.
7093
+ */
7094
+ function convexHull(points) {
7095
+ const sorted = [...points].sort((a, b) => a.x - b.x || a.y - b.y);
7096
+ const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
7097
+ const build = (list) => {
7098
+ const chain = [];
7099
+ for (const point of list) {
7100
+ while (chain.length >= 2 && cross(chain[chain.length - 2], chain[chain.length - 1], point) <= 0)
7101
+ chain.pop();
7102
+ chain.push(point);
7103
+ }
7104
+ chain.pop();
7105
+ return chain;
7106
+ };
7107
+ return [...build(sorted), ...build(sorted.reverse())];
7108
+ }
7109
+ /*
7110
+ * Where the top of a convex outline crosses a vertical line, so a stem leaves the box without jumping between corners.
7111
+ */
7112
+ function hullTop(hull, x) {
7113
+ let top = Infinity;
7114
+ for (let i = 0; i < hull.length; i++) {
7115
+ const a = hull[i];
7116
+ const b = hull[(i + 1) % hull.length];
7117
+ if (x < Math.min(a.x, b.x) || x > Math.max(a.x, b.x)) {
7118
+ continue;
7119
+ }
7120
+ top = Math.min(top, a.x === b.x ? Math.min(a.y, b.y) : a.y + (b.y - a.y) * (x - a.x) / (b.x - a.x));
7121
+ }
7122
+ return top;
7123
+ }
7124
+ function hullArea(hull) {
7125
+ return Math.abs(hull.reduce((sum, point, i) => sum + point.x * hull[(i + 1) % hull.length].y - hull[(i + 1) % hull.length].x * point.y, 0)) / 2;
7126
+ }
7127
+ function hullContains(hull, point) {
7128
+ return hull.length >= 3 && hull.every((a, i) => {
7129
+ const b = hull[(i + 1) % hull.length];
7130
+ return (b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x) >= 0;
7131
+ });
7132
+ }
6367
7133
  class Label {
6368
7134
  get HasText() { return this.options.presentation !== "icon"; }
6369
7135
  SetHoverVisible(visible) {
@@ -6418,6 +7184,11 @@
6418
7184
  this.Transient = false;
6419
7185
  this.hoverFade = { from: 0, to: 1, start: Date.now() };
6420
7186
  this.cursorMotion = { amount: 0, from: 0, target: 0, start: 0, position: undefined };
7187
+ // How far the stem is from the ring onto a model box, and the offset it eases out after changing outline.
7188
+ // The record's box for the Entity this label is on: undefined corners while reading, null when it has none.
7189
+ this.modelBox = null;
7190
+ this.boxMorph = { value: 0, from: 0, target: 0, start: 0, anchor: undefined, group: undefined,
7191
+ shift: { x: 0, y: 0, start: 0 } };
6421
7192
  // Counter to help differentiate between createLabel calls.
6422
7193
  this.counter = 0;
6423
7194
  // Whether this label has asked its group for a default image, which it does once on screen.
@@ -6503,11 +7274,13 @@
6503
7274
  * Recalling will be ignored.
6504
7275
  */
6505
7276
  Dispose() {
7277
+ var _a;
6506
7278
  if (this.disposed) {
6507
7279
  return;
6508
7280
  }
6509
7281
  this.disposed = true;
6510
7282
  this.removeLabel();
7283
+ (_a = layers.get(this.viewer)) === null || _a === void 0 ? void 0 : _a.modelBoxes.Set(this.id, null, null, null);
6511
7284
  clearTimeout(this.loadTimeout);
6512
7285
  this.loadTimeout = null;
6513
7286
  }
@@ -6522,14 +7295,15 @@
6522
7295
  this.groupId = groupId;
6523
7296
  this.additionalTexts = this.options.text != null ? [] : null;
6524
7297
  this.photo = null;
6525
- this.createLabel();
7298
+ // A label given its position keeps it, or the rebuild goes looking for the entity's own.
7299
+ this.createLabel(this.prePositioned ? this._pos3d : undefined);
6526
7300
  }
6527
7301
  /**
6528
7302
  * Creates cesium entity for the label if one does not exist.
6529
7303
  * @param starterPos3d optional position to create label at. Ignored for billboard labels.
6530
7304
  */
6531
7305
  async createLabel(starterPos3d) {
6532
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
7306
+ 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;
6533
7307
  // The old label stays until its replacement is built, so a rebuild never blinks while a location loads.
6534
7308
  let pos3d = starterPos3d ? starterPos3d.clone() : null;
6535
7309
  const counter = ++this.counter;
@@ -6815,6 +7589,9 @@
6815
7589
  if (this.disposed || this.counter != counter) {
6816
7590
  return;
6817
7591
  }
7592
+ // Leave the layer only once rejoined, so a rebuild never empties it and closes it along with its boxes.
7593
+ const leaveLayer = this.renderRemoval;
7594
+ this.renderRemoval = null;
6818
7595
  this.removeLabel();
6819
7596
  // A found photo draws as if configured, without changing the options clustering compares and measures.
6820
7597
  const photo = HasIcon(this.options) ? null : (_j = this.options.photo) !== null && _j !== void 0 ? _j : this.photo;
@@ -7111,9 +7888,65 @@
7111
7888
  radius: Math.max(width, height) / 2
7112
7889
  };
7113
7890
  };
7891
+ // Only CAD tilesets and model Entities are assembly parts with a geometry box; anything else stays a point.
7892
+ const boxable = Boolean(normalLeader && ((_r = this.rego) === null || _r === void 0 ? void 0 : _r.entityId) && ((visual instanceof Cesium.Cesium3DTileFeature && this.rego.tilesetType === BModels.Tileset.EType.Cad) ||
7893
+ (visual instanceof Cesium.Entity && Boolean(visual.model))));
7894
+ if (!boxable || ((_s = this.modelBox) === null || _s === void 0 ? void 0 : _s.entityId) !== ((_t = this.rego) === null || _t === void 0 ? void 0 : _t.entityId)) {
7895
+ layer.modelBoxes.Set(this.id, null, null, null);
7896
+ this.modelBox = null;
7897
+ }
7898
+ // Read once per Entity, so a label rebuilt for its loaded text keeps its box instead of waiting again.
7899
+ if (boxable && !this.modelBox) {
7900
+ const box = { entityId: this.rego.entityId, corners: undefined, load: null };
7901
+ box.load = this.loadModelBox().catch(() => null).then(corners => box.corners = corners || null);
7902
+ this.modelBox = box;
7903
+ }
7904
+ // A model's label stays hidden until its box is known, so a ring never shows only to turn into a box.
7905
+ const hydrated = (_y = (_x = (_u = this.visualRegister) === null || _u === void 0 ? void 0 : _u.GetLabelGroupFor((_v = this.rego) === null || _v === void 0 ? void 0 : _v.entityId, (_w = this.rego) === null || _w === void 0 ? void 0 : _w.menuItemId)) === null || _x === void 0 ? void 0 : _x.item) === null || _y === void 0 ? void 0 : _y.entity;
7906
+ let boxPending = boxable && this.modelBox.corners === undefined && Boolean(((_z = hydrated === null || hydrated === void 0 ? void 0 : hydrated.Bruce) === null || _z === void 0 ? void 0 : _z.GeometryPivotExtents) || this.api);
7907
+ const boxPendingUntil = Date.now() + MODEL_BOX_WAIT;
7908
+ // Whether this label has been on screen yet; a box found before then is drawn in place, not eased into.
7909
+ let shownOnce = false;
7910
+ if (boxable) {
7911
+ const found = this._pos3d;
7912
+ const settle = () => {
7913
+ if (boxPending && !this.disposed && this.counter === counter) {
7914
+ boxPending = false;
7915
+ this.viewer.scene.requestRender();
7916
+ }
7917
+ };
7918
+ // Past the wait a slow record no longer holds the label back; a box that comes later eases in.
7919
+ setTimeout(settle, MODEL_BOX_WAIT);
7920
+ this.modelBox.load.then(corners => {
7921
+ if (!corners || this.disposed || this.counter !== counter) {
7922
+ return;
7923
+ }
7924
+ // A record that disagrees with where the model was found describes another placement, such as a moved tileset.
7925
+ const centre = Cesium.Cartesian3.midpoint(corners[0], corners[7], new Cesium.Cartesian3());
7926
+ if (Cesium.Cartesian3.distance(centre, found) > Cesium.Cartesian3.distance(corners[0], corners[7]) * 0.75 + 2) {
7927
+ return;
7928
+ }
7929
+ const rego = this.rego;
7930
+ // Let go a little further out than it is taken, so a camera resting at the limit does not flip it.
7931
+ let near = false;
7932
+ layer.modelBoxes.Set(this.id, corners, rego.entityId, {
7933
+ selected: () => { var _a, _b; return Boolean((_b = (_a = this.visualRegister) === null || _a === void 0 ? void 0 : _a.GetState({ entityId: rego.entityId, menuItemId: rego.menuItemId })) === null || _b === void 0 ? void 0 : _b.selected); },
7934
+ hovered: () => { var _a, _b; return Boolean((_b = (_a = this.visualRegister) === null || _a === void 0 ? void 0 : _a.IsLabelHovered) === null || _b === void 0 ? void 0 : _b.call(_a, rego.entityId, rego.menuItemId)); },
7935
+ near: () => {
7936
+ var _a, _b;
7937
+ const ring = this.options.anchorRing;
7938
+ const camera = this.viewer.camera.positionWC || this.viewer.camera.position;
7939
+ const limit = ((_a = ring === null || ring === void 0 ? void 0 : ring.nearDistance) !== null && _a !== void 0 ? _a : 100) * (near ? 1.1 : 1);
7940
+ near = (ring === null || ring === void 0 ? void 0 : ring.enabled) !== false && Number.isFinite((_b = this._pos3d) === null || _b === void 0 ? void 0 : _b.x) && Cesium.Cartesian3.distance(camera, this._pos3d) <= limit;
7941
+ return near || ((ring === null || ring === void 0 ? void 0 : ring.enabled) !== false && Boolean(this.cursorMotion.target));
7942
+ }
7943
+ });
7944
+ this.viewer.scene.requestRender();
7945
+ }).finally(settle);
7946
+ }
7114
7947
  let cursorMotion = this.cursorMotion;
7115
7948
  const project = () => {
7116
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
7949
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
7117
7950
  if (this.disposed || !this.viewer || this.viewer.isDestroyed() || ((_a = this.visible) === null || _a === void 0 ? void 0 : _a.call(this)) === false ||
7118
7951
  (presentation === "icon" && this.options.iconScale === 0)) {
7119
7952
  return null;
@@ -7151,18 +7984,29 @@
7151
7984
  if (!Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.x) || !Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.y)) {
7152
7985
  return null;
7153
7986
  }
7987
+ const boxed = layer.modelBoxes.Frame(this.id);
7988
+ const waiting = boxPending && Date.now() < boxPendingUntil;
7989
+ const boxBlend = this.morphToBox(boxed, shownOnce ? (_f = (_e = this.options.anchorRing) === null || _e === void 0 ? void 0 : _e.transitionDuration) !== null && _f !== void 0 ? _f : 200 : 0);
7990
+ // Eased between the ring and where the stem meets the box, so gaining or losing a box never jumps.
7991
+ const boxTarget = this.boxMorph.anchor;
7992
+ const ringPoint = { x: anchor.x + pixelOffset.x, y: anchor.y + pixelOffset.y };
7993
+ const boxAnchor = boxBlend > 0 && boxTarget ? {
7994
+ x: ringPoint.x + (boxTarget.x - ringPoint.x) * boxBlend,
7995
+ y: ringPoint.y + (boxTarget.y - ringPoint.y) * boxBlend
7996
+ } : undefined;
7997
+ const start = boxAnchor || anchor;
7154
7998
  const onScreen = anchor.x >= 0 && anchor.y >= 0 && anchor.x < this.viewer.canvas.clientWidth && anchor.y < this.viewer.canvas.clientHeight;
7155
- if (leader && !onScreen)
7999
+ if (leader && (start.x < 0 || start.y < 0 || start.x >= this.viewer.canvas.clientWidth || start.y >= this.viewer.canvas.clientHeight))
7156
8000
  return null;
7157
8001
  if (onScreen && !this.imageAsked && this.groupId && !HasIcon(this.options) && !this.options.photo) {
7158
8002
  this.imageAsked = true;
7159
- (_e = this.visualRegister) === null || _e === void 0 ? void 0 : _e.RequestLabelImage((_f = this.rego) === null || _f === void 0 ? void 0 : _f.entityId);
8003
+ (_g = this.visualRegister) === null || _g === void 0 ? void 0 : _g.RequestLabelImage((_h = this.rego) === null || _h === void 0 ? void 0 : _h.entityId);
7160
8004
  }
7161
8005
  const transientPivot = (this.Transient && !this.Persistent ?
7162
- (this.hoverFade.to === 0 ? this.retainedHoverPosition : (_g = this.visualRegister) === null || _g === void 0 ? void 0 : _g.GetLabelHoverPosition((_h = this.rego) === null || _h === void 0 ? void 0 : _h.entityId, (_j = this.rego) === null || _j === void 0 ? void 0 : _j.menuItemId)) : undefined);
8006
+ (this.hoverFade.to === 0 ? this.retainedHoverPosition : (_j = this.visualRegister) === null || _j === void 0 ? void 0 : _j.GetLabelHoverPosition((_k = this.rego) === null || _k === void 0 ? void 0 : _k.entityId, (_l = this.rego) === null || _l === void 0 ? void 0 : _l.menuItemId)) : undefined);
7163
8007
  const hover = (presentation !== "icon" ?
7164
8008
  (this.Transient && !this.Persistent && this.hoverFade.to === 0 ? this.retainedHoverPosition :
7165
- (_k = this.visualRegister) === null || _k === void 0 ? void 0 : _k.GetLabelHoverPosition((_l = this.rego) === null || _l === void 0 ? void 0 : _l.entityId, (_m = this.rego) === null || _m === void 0 ? void 0 : _m.menuItemId)) : undefined);
8009
+ (_m = this.visualRegister) === null || _m === void 0 ? void 0 : _m.GetLabelHoverPosition((_o = this.rego) === null || _o === void 0 ? void 0 : _o.entityId, (_p = this.rego) === null || _p === void 0 ? void 0 : _p.menuItemId)) : undefined);
7166
8010
  const reused = !this.Transient || this.Persistent;
7167
8011
  const now = Date.now();
7168
8012
  const animateCursor = () => {
@@ -7186,12 +8030,14 @@
7186
8030
  const cursorAmount = cursorMotion.amount;
7187
8031
  leader = normalLeader || cursorAmount > 0;
7188
8032
  // An icon keeps its callout centred on the glyph rather than following the cursor across it.
7189
- const icon = measureIcon();
8033
+ const icon = boxAnchor ? null : measureIcon();
7190
8034
  const iconRing = icon ? Math.max(ICON_RING_MIN, icon.radius + ICON_RING_PADDING) : 0;
7191
- const pivot = icon ? undefined : transientPivot;
7192
- let x = ((_o = pivot === null || pivot === void 0 ? void 0 : pivot.x) !== null && _o !== void 0 ? _o : anchor.x) + (icon || pixelOffset).x;
7193
- let y = ((_p = pivot === null || pivot === void 0 ? void 0 : pivot.y) !== null && _p !== void 0 ? _p : anchor.y) + (icon || pixelOffset).y;
7194
- if (cursorAmount > 0 && !icon) {
8035
+ // A boxed model is pinned the same way, with its stem leaving the top of the box outline.
8036
+ const pinned = Boolean(icon || boxAnchor);
8037
+ const pivot = pinned ? undefined : transientPivot;
8038
+ let x = boxAnchor ? boxAnchor.x : ((_q = pivot === null || pivot === void 0 ? void 0 : pivot.x) !== null && _q !== void 0 ? _q : anchor.x) + (icon || pixelOffset).x;
8039
+ let y = boxAnchor ? boxAnchor.y : ((_r = pivot === null || pivot === void 0 ? void 0 : pivot.y) !== null && _r !== void 0 ? _r : anchor.y) + (icon || pixelOffset).y;
8040
+ if (cursorAmount > 0 && !pinned) {
7195
8041
  // A leader-less label starts centred on its anchor; preserve that box as it lifts away.
7196
8042
  if (!normalLeader) {
7197
8043
  x -= ele.offsetWidth / layer.coordinates.scaleX / 2;
@@ -7226,6 +8072,9 @@
7226
8072
  if (visual instanceof Cesium.Entity && (visual.polygon || visual.polyline))
7227
8073
  leaderLength /= 3;
7228
8074
  }
8075
+ if (boxAnchor && this.options.leaderLength == null) {
8076
+ leaderLength += (MODEL_BOX_STEM - leaderLength) * boxBlend;
8077
+ }
7229
8078
  if (cursorAmount > 0) {
7230
8079
  leaderLength += (96 - leaderLength) * cursorAmount;
7231
8080
  }
@@ -7233,7 +8082,7 @@
7233
8082
  leaderLength += iconRing;
7234
8083
  y = anchorY - leaderLength;
7235
8084
  }
7236
- else if (hover && !reused && !icon) {
8085
+ else if (hover && !reused && !pinned) {
7237
8086
  x = hover.x + pixelOffset.x;
7238
8087
  y = hover.y + pixelOffset.y - 24;
7239
8088
  }
@@ -7242,7 +8091,7 @@
7242
8091
  const width = ele.offsetWidth / coordinates.scaleX;
7243
8092
  const height = ele.offsetHeight / coordinates.scaleY;
7244
8093
  let clusterBounds;
7245
- if ((_q = this.options.cluster) === null || _q === void 0 ? void 0 : _q.enabled) {
8094
+ if ((_s = this.options.cluster) === null || _s === void 0 ? void 0 : _s.enabled) {
7246
8095
  const textWidth = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetWidth) || 0;
7247
8096
  const textHeight = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetHeight) || 0;
7248
8097
  // A photo is never part of the footprint, so its arrival cannot change which cluster a label joins.
@@ -7276,7 +8125,7 @@
7276
8125
  if (!leader && (bounds.x + bounds.width < 0 || bounds.y + bounds.height < 0 || bounds.x > this.viewer.canvas.clientWidth || bounds.y > this.viewer.canvas.clientHeight)) {
7277
8126
  return null;
7278
8127
  }
7279
- const state = (_r = this.visualRegister) === null || _r === void 0 ? void 0 : _r.GetState({ entityId: (_s = this.rego) === null || _s === void 0 ? void 0 : _s.entityId, menuItemId: (_t = this.rego) === null || _t === void 0 ? void 0 : _t.menuItemId });
8128
+ const state = (_t = this.visualRegister) === null || _t === void 0 ? void 0 : _t.GetState({ entityId: (_u = this.rego) === null || _u === void 0 ? void 0 : _u.entityId, menuItemId: (_v = this.rego) === null || _v === void 0 ? void 0 : _v.menuItemId });
7280
8129
  const opacity = this.prePositioned || hoverOnly || MAX_DISTANCE === Infinity ? 1 : 1 - distance / MAX_DISTANCE;
7281
8130
  return {
7282
8131
  canFlip: leader && presentation !== "icon", hasLeader: leader,
@@ -7284,24 +8133,25 @@
7284
8133
  transient: this.Transient || Boolean(hover) || cursorAmount > 0,
7285
8134
  extendedGeometry: visual instanceof Cesium.Entity && Boolean(visual.polygon || visual.polyline || visual.corridor),
7286
8135
  viewportWidth: this.viewer.canvas.clientWidth,
7287
- id: this.id, entityKey: ((_u = this.rego) === null || _u === void 0 ? void 0 : _u.entityId) ? JSON.stringify([this.rego.entityId, this.rego.menuItemId || "", this.Transient && !this.Persistent ? "hover" : ""]) : undefined,
7288
- entityId: (_v = this.rego) === null || _v === void 0 ? void 0 : _v.entityId, group: this.groupId, x, y, width, height, anchorX, anchorY, depth, leaderLength, leaderRun, leaderSide, distance,
7289
- iconRadius: icon === null || icon === void 0 ? void 0 : icon.radius,
8136
+ id: this.id, entityKey: ((_w = this.rego) === null || _w === void 0 ? void 0 : _w.entityId) ? JSON.stringify([this.rego.entityId, this.rego.menuItemId || "", this.Transient && !this.Persistent ? "hover" : ""]) : undefined,
8137
+ entityId: (_x = this.rego) === null || _x === void 0 ? void 0 : _x.entityId, group: this.groupId, x, y, width, height, anchorX, anchorY, depth, leaderLength, leaderRun, leaderSide, distance,
8138
+ iconRadius: icon === null || icon === void 0 ? void 0 : icon.radius, modelBox: boxed || undefined, boxBlend,
7290
8139
  // Priority shows the hovered member individually without dragging its cluster along.
7291
- opacity, priority: hover || cursorAmount > 0 || (state === null || state === void 0 ? void 0 : state.highlighted) ? 1 : 0, options: this.options, text: originalText, clusterBounds
8140
+ opacity: waiting ? 0 : opacity, priority: hover || cursorAmount > 0 || (state === null || state === void 0 ? void 0 : state.highlighted) ? 1 : 0, options: this.options, text: originalText, clusterBounds
7292
8141
  };
7293
8142
  };
7294
8143
  let displacement = { x: 0, y: 0 };
7295
8144
  let nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
7296
8145
  const apply = (placement) => {
7297
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4;
8146
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5;
7298
8147
  const show = (placement === null || placement === void 0 ? void 0 : placement.visible) && placement.opacity > 0;
8148
+ shownOnce = shownOnce || Boolean(show);
7299
8149
  clickEntityIds = show ? placement.entityIds : [];
7300
8150
  ele.style.visibility = show ? "visible" : "hidden";
7301
8151
  lineEle.style.display = show && leader ? "block" : "none";
7302
8152
  if (runEle)
7303
8153
  runEle.style.display = show && leader ? "block" : "none";
7304
- this.anchor.style.display = show && leader ? "block" : "none";
8154
+ this.anchor.style.display = show && leader && !(((_a = placement.frame) === null || _a === void 0 ? void 0 : _a.boxBlend) >= 1) ? "block" : "none";
7305
8155
  if (!show) {
7306
8156
  displacement = { x: 0, y: 0 };
7307
8157
  nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
@@ -7313,7 +8163,7 @@
7313
8163
  return;
7314
8164
  }
7315
8165
  // A label standing for a cluster shows its summary only; its photo is put away and never fetched.
7316
- const solo = !((_a = placement.members) === null || _a === void 0 ? void 0 : _a.length);
8166
+ const solo = !((_b = placement.members) === null || _b === void 0 ? void 0 : _b.length);
7317
8167
  if (look.iconPhoto && iconElement && iconElement.dataset.failed !== "true") {
7318
8168
  const display = solo ? "inline-flex" : "none";
7319
8169
  if (iconElement.style.display !== display) {
@@ -7331,7 +8181,7 @@
7331
8181
  }
7332
8182
  }, PHOTO_SHOW_DELAY);
7333
8183
  }
7334
- const displayText = (_b = placement.text) !== null && _b !== void 0 ? _b : originalText;
8184
+ const displayText = (_c = placement.text) !== null && _c !== void 0 ? _c : originalText;
7335
8185
  if (textElement && textElement.textContent !== displayText) {
7336
8186
  textElement.textContent = displayText;
7337
8187
  // Measure the summary's footprint next frame without replacing each member's source text.
@@ -7355,9 +8205,9 @@
7355
8205
  const stack = JSON.stringify(placement.icons || []);
7356
8206
  if (iconStack && stack !== displayedStack) {
7357
8207
  displayedStack = stack;
7358
- const looks = ((_c = placement.icons) === null || _c === void 0 ? void 0 : _c.length) ? [iconLook(this.options), ...placement.icons] : [];
8208
+ const looks = ((_d = placement.icons) === null || _d === void 0 ? void 0 : _d.length) ? [iconLook(this.options), ...placement.icons] : [];
7359
8209
  // One small size for all, so no icon in the arrangement reads as more important than another.
7360
- const size = (this.options.iconSize || 24) * ((_d = this.options.iconScale) !== null && _d !== void 0 ? _d : 1) * ICON_STACK_SCALE;
8210
+ const size = (this.options.iconSize || 24) * ((_e = this.options.iconScale) !== null && _e !== void 0 ? _e : 1) * ICON_STACK_SCALE;
7361
8211
  const built = looks.map(look => this.buildIcon({ ...look, iconSize: size }, counter));
7362
8212
  // A stack is on show already, so its photos load straight away.
7363
8213
  built.forEach(({ start }) => start === null || start === void 0 ? void 0 : start());
@@ -7392,9 +8242,11 @@
7392
8242
  // Summary and badge changes must fit on their first visible frame, including after resize.
7393
8243
  const ring = this.options.anchorRing;
7394
8244
  const pointer = (layer.pointer || (cursorMotion.target ? cursorMotion.position : undefined) || (this.Transient && !this.Persistent ?
7395
- (this.hoverFade.to === 0 ? this.retainedHoverPosition : (_e = this.visualRegister) === null || _e === void 0 ? void 0 : _e.GetLabelHoverPosition((_f = this.rego) === null || _f === void 0 ? void 0 : _f.entityId, (_g = this.rego) === null || _g === void 0 ? void 0 : _g.menuItemId)) : undefined));
8245
+ (this.hoverFade.to === 0 ? this.retainedHoverPosition : (_f = this.visualRegister) === null || _f === void 0 ? void 0 : _f.GetLabelHoverPosition((_g = this.rego) === null || _g === void 0 ? void 0 : _g.entityId, (_h = this.rego) === null || _h === void 0 ? void 0 : _h.menuItemId)) : undefined));
7396
8246
  const iconRing = frame.iconRadius ? Math.max(ICON_RING_MIN, frame.iconRadius + ICON_RING_PADDING) : 0;
7397
- const hovered = pointer && Math.hypot(pointer.x - frame.anchorX, pointer.y - frame.anchorY) <= Math.max((_h = ring === null || ring === void 0 ? void 0 : ring.hoverDistance) !== null && _h !== void 0 ? _h : 24, iconRing);
8247
+ const boxed = leader ? frame.modelBox : undefined;
8248
+ const hovered = pointer && (boxed ? hullContains(boxed.hull, pointer) :
8249
+ Math.hypot(pointer.x - frame.anchorX, pointer.y - frame.anchorY) <= Math.max((_j = ring === null || ring === void 0 ? void 0 : ring.hoverDistance) !== null && _j !== void 0 ? _j : 24, iconRing));
7398
8250
  if (look.iconPhoto && growPhoto) {
7399
8251
  // A rectangle leans on the leader's side of the label.
7400
8252
  if (photoShape === "rect") {
@@ -7405,15 +8257,15 @@
7405
8257
  }
7406
8258
  // Grown in layout rather than scaled, so the border keeps its width, and the label, whose bottom
7407
8259
  // sits on the leader, grows up and sideways without changing the leader's length.
7408
- growPhoto(Boolean(hovered) && !((_j = placement.members) === null || _j === void 0 ? void 0 : _j.length));
8260
+ growPhoto(Boolean(hovered) && !((_k = placement.members) === null || _k === void 0 ? void 0 : _k.length));
7409
8261
  }
7410
- const normalRadius = (!leader || (ring === null || ring === void 0 ? void 0 : ring.enabled) === false ? 0 : Math.max(frame.distance <= ((_k = ring === null || ring === void 0 ? void 0 : ring.nearDistance) !== null && _k !== void 0 ? _k : 100) ? ((_l = ring === null || ring === void 0 ? void 0 : ring.nearRadius) !== null && _l !== void 0 ? _l : 10) : 0, hovered ? ((_m = ring === null || ring === void 0 ? void 0 : ring.hoverRadius) !== null && _m !== void 0 ? _m : (this.Transient && !this.Persistent ? 15.4 : 22)) : 0));
8262
+ const normalRadius = (!leader || (ring === null || ring === void 0 ? void 0 : ring.enabled) === false ? 0 : Math.max(frame.distance <= ((_l = ring === null || ring === void 0 ? void 0 : ring.nearDistance) !== null && _l !== void 0 ? _l : 100) ? ((_m = ring === null || ring === void 0 ? void 0 : ring.nearRadius) !== null && _m !== void 0 ? _m : 10) : 0, hovered ? ((_o = ring === null || ring === void 0 ? void 0 : ring.hoverRadius) !== null && _o !== void 0 ? _o : (this.Transient && !this.Persistent ? 15.4 : 22)) : 0));
7411
8263
  const targetRadius = normalRadius + (15.4 - normalRadius) * cursorMotion.amount;
7412
8264
  // Around a glyph the ring keeps the glyph's size and only fades its stroke in, so it never covers the icon.
7413
8265
  const targetOuter = iconRing || targetRadius || 2.5;
7414
8266
  const targetInner = iconRing ? (targetRadius > 0 ? iconRing - 2 : iconRing) : Math.max(0, targetRadius - 2);
7415
8267
  const now = Date.now();
7416
- const duration = (_o = ring === null || ring === void 0 ? void 0 : ring.transitionDuration) !== null && _o !== void 0 ? _o : 200;
8268
+ const duration = (_p = ring === null || ring === void 0 ? void 0 : ring.transitionDuration) !== null && _p !== void 0 ? _p : 200;
7417
8269
  const sharedRing = this.Transient && !this.Persistent && this.hoverFade.to === 1 ? this.hoverRingState : undefined;
7418
8270
  let ringAnimation = sharedRing ? sharedRing.animation : this.ringAnimation;
7419
8271
  if (!ringAnimation) {
@@ -7447,7 +8299,8 @@
7447
8299
  if (sharedRing) {
7448
8300
  sharedRing.animation = ringAnimation;
7449
8301
  }
7450
- const radius = ringAnimation.inner > 0 ? ringAnimation.outer : 0;
8302
+ const ringShare = 1 - (frame.boxBlend || 0);
8303
+ const radius = ringShare > 0 && ringAnimation.inner > 0 ? ringAnimation.outer * ringShare : 0;
7451
8304
  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);
7452
8305
  const position = { x: (stem || frame).x, y: (stem || frame).y };
7453
8306
  const width = ele.offsetWidth / coordinates.scaleX;
@@ -7455,19 +8308,19 @@
7455
8308
  if (leader && angled) {
7456
8309
  // The declutter owns the side; the fit only says which one had the room to begin with.
7457
8310
  const run = Math.max(ANGLED_LEADER_RUN, radius + 4);
7458
- const side = (_q = (_p = placement.textSide) !== null && _p !== void 0 ? _p : frame.leaderSide) !== null && _q !== void 0 ? _q : "right";
8311
+ const side = (_r = (_q = placement.textSide) !== null && _q !== void 0 ? _q : frame.leaderSide) !== null && _r !== void 0 ? _r : "right";
7459
8312
  const beside = side === "left" ? frame.anchorX - run - width : frame.anchorX + run;
7460
8313
  position.x = Math.max(0, Math.min(beside, this.viewer.canvas.clientWidth - width));
7461
8314
  }
7462
8315
  else if (leader && placement.textSide === "left") {
7463
8316
  position.x = Math.max(0, Math.min(frame.anchorX - width, this.viewer.canvas.clientWidth - width));
7464
8317
  }
7465
- const gap = (_s = (_r = this.options.pointerAvoidance) === null || _r === void 0 ? void 0 : _r.gap) !== null && _s !== void 0 ? _s : 12;
8318
+ const gap = (_t = (_s = this.options.pointerAvoidance) === null || _s === void 0 ? void 0 : _s.gap) !== null && _t !== void 0 ? _t : 12;
7466
8319
  const containsPointer = (x, y, margin) => {
7467
8320
  return pointer && pointer.x >= x - margin && pointer.x <= x + width + margin && pointer.y >= y - margin && pointer.y <= y + height + margin;
7468
8321
  };
7469
8322
  const passive = !selectOnClick || !this.visualRegister;
7470
- if (passive && ((_t = this.options.pointerAvoidance) === null || _t === void 0 ? void 0 : _t.enabled) !== false && pointer &&
8323
+ if (passive && ((_u = this.options.pointerAvoidance) === null || _u === void 0 ? void 0 : _u.enabled) !== false && pointer &&
7471
8324
  (containsPointer(position.x, position.y, gap) || containsPointer(position.x + displacement.x, position.y + displacement.y, gap))) {
7472
8325
  if (containsPointer(position.x + displacement.x, position.y + displacement.y, 0)) {
7473
8326
  // Choose against the original box and retain the offset until the pointer leaves both boxes.
@@ -7485,7 +8338,7 @@
7485
8338
  else {
7486
8339
  displacement = { x: 0, y: 0 };
7487
8340
  }
7488
- const nudgeDuration = (_v = (_u = this.options.pointerAvoidance) === null || _u === void 0 ? void 0 : _u.transitionDuration) !== null && _v !== void 0 ? _v : 200;
8341
+ const nudgeDuration = (_w = (_v = this.options.pointerAvoidance) === null || _v === void 0 ? void 0 : _v.transitionDuration) !== null && _w !== void 0 ? _w : 200;
7489
8342
  const animateNudge = () => {
7490
8343
  const progress = now < nudge.start ? 0 : nudgeDuration > 0 ? Math.min(1, (now - nudge.start) / nudgeDuration) : 1;
7491
8344
  const eased = progress * progress * (3 - 2 * progress);
@@ -7501,7 +8354,7 @@
7501
8354
  fromY: nudge.y,
7502
8355
  targetX: displacement.x,
7503
8356
  targetY: displacement.y,
7504
- start: now + ((_x = (_w = this.options.pointerAvoidance) === null || _w === void 0 ? void 0 : _w.delay) !== null && _x !== void 0 ? _x : 180)
8357
+ start: now + ((_y = (_x = this.options.pointerAvoidance) === null || _x === void 0 ? void 0 : _x.delay) !== null && _y !== void 0 ? _y : 180)
7505
8358
  };
7506
8359
  }
7507
8360
  if (animateNudge() < 1 && (nudge.fromX !== nudge.targetX || nudge.fromY !== nudge.targetY)) {
@@ -7512,10 +8365,10 @@
7512
8365
  ele.style.left = `${coordinates.x + position.x * coordinates.scaleX}px`;
7513
8366
  ele.style.top = `${coordinates.y + position.y * coordinates.scaleY}px`;
7514
8367
  for (const clickTarget of Array.from(content.querySelectorAll('[role="button"]'))) {
7515
- clickTarget.setAttribute("aria-label", placement.members.length ? `Select ${placement.entityIds.length} entities` : `Select ${(_z = (_y = this.options.text) !== null && _y !== void 0 ? _y : text) !== null && _z !== void 0 ? _z : (_0 = this.rego) === null || _0 === void 0 ? void 0 : _0.entityId}`);
8368
+ clickTarget.setAttribute("aria-label", placement.members.length ? `Select ${placement.entityIds.length} entities` : `Select ${(_0 = (_z = this.options.text) !== null && _z !== void 0 ? _z : text) !== null && _0 !== void 0 ? _0 : (_1 = this.rego) === null || _1 === void 0 ? void 0 : _1.entityId}`);
7516
8369
  }
7517
8370
  if (leader) {
7518
- const nodeRadius = ringAnimation.outer;
8371
+ const nodeRadius = ringAnimation.outer * (1 - (frame.boxBlend || 0));
7519
8372
  const anchor = this.anchor;
7520
8373
  anchor.style.left = `${coordinates.x + (frame.anchorX - nodeRadius) * coordinates.scaleX}px`;
7521
8374
  anchor.style.top = `${coordinates.y + (frame.anchorY - nodeRadius) * coordinates.scaleY}px`;
@@ -7528,11 +8381,12 @@
7528
8381
  const circle = svg.firstElementChild;
7529
8382
  circle.setAttribute("cx", String(nodeRadius));
7530
8383
  circle.setAttribute("cy", String(nodeRadius));
7531
- circle.setAttribute("r", String((nodeRadius + ringAnimation.inner) / 2));
8384
+ const innerRadius = ringAnimation.inner * (1 - (frame.boxBlend || 0));
8385
+ circle.setAttribute("r", String((nodeRadius + innerRadius) / 2));
7532
8386
  circle.setAttribute("stroke", this.options.leaderColor || "rgba(255, 255, 255, 0.8)");
7533
- circle.setAttribute("stroke-width", String(nodeRadius - ringAnimation.inner));
8387
+ circle.setAttribute("stroke-width", String(nodeRadius - innerRadius));
7534
8388
  anchor.style.boxShadow = radius ? "none" : "0 0 5px rgba(0, 0, 0, 0.3)";
7535
- anchor.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount));
8389
+ anchor.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount) * (1 - (frame.boxBlend || 0)));
7536
8390
  anchor.style.zIndex = String(placement.zIndex);
7537
8391
  // The stem starts at the rim, never across the transparent centre, even after passive displacement.
7538
8392
  const straightX = Math.max(position.x, Math.min(frame.anchorX, position.x + width));
@@ -7552,13 +8406,13 @@
7552
8406
  // Keep the stem inside the 126-degree arc, away from antialiased endpoints.
7553
8407
  const arcFraction = 0.35;
7554
8408
  const rotationLimit = Math.PI * arcFraction - Math.PI / 30;
7555
- const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_1 = ringAnimation.arcOffset) !== null && _1 !== void 0 ? _1 : 0);
8409
+ const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_2 = ringAnimation.arcOffset) !== null && _2 !== void 0 ? _2 : 0);
7556
8410
  const targetOpen = cursorMotion.target || (hovered && (ring === null || ring === void 0 ? void 0 : ring.enabled) !== false && targetRadius > 0) ? 1 : 0;
7557
- const elapsed = Math.min(32, Math.max(0, now - ((_2 = ringAnimation.arcTime) !== null && _2 !== void 0 ? _2 : now)));
8411
+ const elapsed = Math.min(32, Math.max(0, now - ((_3 = ringAnimation.arcTime) !== null && _3 !== void 0 ? _3 : now)));
7558
8412
  const blend = duration > 0 ? 1 - Math.exp(-elapsed / (duration / 3)) : 1;
7559
8413
  const approach = (value, target) => Math.abs(value - target) < 0.001 ? target : value + (target - value) * blend;
7560
- ringAnimation.arcOffset = approach((_3 = ringAnimation.arcOffset) !== null && _3 !== void 0 ? _3 : 0, targetOffset);
7561
- ringAnimation.arcOpen = approach((_4 = ringAnimation.arcOpen) !== null && _4 !== void 0 ? _4 : 0, targetOpen);
8414
+ ringAnimation.arcOffset = approach((_4 = ringAnimation.arcOffset) !== null && _4 !== void 0 ? _4 : 0, targetOffset);
8415
+ ringAnimation.arcOpen = approach((_5 = ringAnimation.arcOpen) !== null && _5 !== void 0 ? _5 : 0, targetOpen);
7562
8416
  ringAnimation.arcTime = now;
7563
8417
  if (ringAnimation.arcOffset !== targetOffset || ringAnimation.arcOpen !== targetOpen) {
7564
8418
  this.viewer.scene.requestRender();
@@ -7584,6 +8438,12 @@
7584
8438
  element.style.zIndex = String(placement.zIndex);
7585
8439
  };
7586
8440
  draw(lineEle, startX, startY, endX, endY);
8441
+ if (layer.modelBoxes.Hull(this.id)) {
8442
+ layer.modelBoxes.Request(this.id, {
8443
+ whole: Boolean(cursorMotion.target), from: { x: frame.anchorX, y: frame.anchorY },
8444
+ opacity, zIndex: placement.zIndex, color: this.options.leaderColor || "rgba(255, 255, 255, 0.8)", duration
8445
+ });
8446
+ }
7587
8447
  if (runEle) {
7588
8448
  // Overlap the corner by the stem's own width so the elbow has no notch in it.
7589
8449
  const overlap = frame.anchorX < runToX ? -1 : 1;
@@ -7596,6 +8456,7 @@
7596
8456
  ele.style.display = "flex";
7597
8457
  ele.style.visibility = "hidden";
7598
8458
  this.renderRemoval = layer.Add({ project, apply });
8459
+ leaveLayer === null || leaveLayer === void 0 ? void 0 : leaveLayer();
7599
8460
  // If terrain changes then we'll re-call createLabel as the position may have changed.
7600
8461
  let terrTimeout = null;
7601
8462
  this.terrChangedRemoval = this.viewer.scene.terrainProviderChanged.addEventListener(() => {
@@ -7741,7 +8602,7 @@
7741
8602
  // The glyph plus the chip's padding of a third either side, enlarged for a photo.
7742
8603
  const diameter = (size + 2 * (size / 3)) * PHOTO_SIZE_SCALE;
7743
8604
  // Enough pixels to stay sharp at the hover size.
7744
- const pixels = Math.ceil(diameter * PHOTO_HOVER_SCALE * (window.devicePixelRatio || 1));
8605
+ const pixels = photoPixels(size, true);
7745
8606
  const canvas = document.createElement("canvas");
7746
8607
  canvas.className = "entity-label-photo";
7747
8608
  canvas.style.cssText = "display:block;box-sizing:border-box;border:2px solid rgba(255,255,255,0.9);box-shadow:0 1px 3px rgba(0,0,0,0.35);";
@@ -7752,13 +8613,15 @@
7752
8613
  // Resting size, set once the photo's shape is known.
7753
8614
  let width = 0;
7754
8615
  let height = 0;
8616
+ // How far hovering grows it, which a small image limits.
8617
+ let hoverScale = PHOTO_HOVER_SCALE;
7755
8618
  let grown = false;
7756
8619
  const grow = (on) => {
7757
8620
  if (on === grown || !width) {
7758
8621
  return;
7759
8622
  }
7760
8623
  grown = on;
7761
- const scale = on ? PHOTO_HOVER_SCALE : 1;
8624
+ const scale = on ? hoverScale : 1;
7762
8625
  canvas.style.width = `${width * scale}px`;
7763
8626
  canvas.style.height = `${height * scale}px`;
7764
8627
  this.renderFor(PHOTO_TRANSITION);
@@ -7776,11 +8639,15 @@
7776
8639
  (_a = canvas.getContext("2d")) === null || _a === void 0 ? void 0 : _a.drawImage(photo.source, 0, 0);
7777
8640
  const longest = Math.max(photo.width, photo.height);
7778
8641
  const circle = PhotoShape(photo.width, photo.height) === "circle";
7779
- width = circle ? diameter : diameter * photo.width / longest;
7780
- height = circle ? diameter : diameter * photo.height / longest;
8642
+ // Shown no larger than the image has pixels for, one to a CSS pixel, and never smaller than the icon.
8643
+ const native = circle ? Math.min(photo.width, photo.height) : longest;
8644
+ const across = Math.min(diameter, Math.max(size, native));
8645
+ hoverScale = Math.max(1, Math.min(PHOTO_HOVER_SCALE, native / across));
8646
+ width = circle ? across : across * photo.width / longest;
8647
+ height = circle ? across : across * photo.height / longest;
7781
8648
  canvas.style.width = `${width}px`;
7782
8649
  canvas.style.height = `${height}px`;
7783
- canvas.style.borderRadius = circle ? "50%" : `${diameter * PHOTO_CORNER}px`;
8650
+ canvas.style.borderRadius = circle ? "50%" : `${across * PHOTO_CORNER}px`;
7784
8651
  canvas.style.opacity = "1";
7785
8652
  placePhoto === null || placePhoto === void 0 ? void 0 : placePhoto(circle ? "circle" : "rect", grow);
7786
8653
  return true;
@@ -7827,6 +8694,57 @@
7827
8694
  };
7828
8695
  step();
7829
8696
  }
8697
+ /*
8698
+ * Reads the record's geometry box, preferring an entity a label group already hydrated.
8699
+ */
8700
+ async loadModelBox() {
8701
+ var _a, _b, _c, _d;
8702
+ const known = (_c = (_b = (_a = this.visualRegister) === null || _a === void 0 ? void 0 : _a.GetLabelGroupFor(this.rego.entityId, this.rego.menuItemId)) === null || _b === void 0 ? void 0 : _b.item) === null || _c === void 0 ? void 0 : _c.entity;
8703
+ let entity = ((_d = known === null || known === void 0 ? void 0 : known.Bruce) === null || _d === void 0 ? void 0 : _d.GeometryPivotExtents) ? known : null;
8704
+ if (!entity && this.api) {
8705
+ try {
8706
+ entity = (await BModels.Entity.Get({ api: this.api, entityId: this.rego.entityId, expandLocation: true, migrated: true })).entity;
8707
+ }
8708
+ // Expanding location fails for entities outside an assembly, which have no box to give.
8709
+ catch {
8710
+ return null;
8711
+ }
8712
+ }
8713
+ // A file uploaded before the API stored oriented boxes has only the record's axis-aligned extents.
8714
+ return GetModelBox(entity, ReadOrientedBox(entity));
8715
+ }
8716
+ /*
8717
+ * Eases the stem between the ring and the model box, returning how far onto the box it is this frame.
8718
+ */
8719
+ morphToBox(boxed, duration) {
8720
+ const morph = this.boxMorph;
8721
+ const now = Date.now();
8722
+ const ease = (progress) => progress * progress * (3 - 2 * progress);
8723
+ const progress = (start) => duration > 0 ? Math.min(1, Math.max(0, (now - start) / duration)) : 1;
8724
+ const hadBox = morph.target === 1;
8725
+ const target = boxed ? 1 : 0;
8726
+ if (morph.target !== target) {
8727
+ Object.assign(morph, { from: morph.value, target, start: now });
8728
+ }
8729
+ morph.value = morph.from + (morph.target - morph.from) * ease(progress(morph.start));
8730
+ // Settled, so a later frame with a longer duration does not replay the ease.
8731
+ if (morph.value === morph.target) {
8732
+ morph.from = morph.target;
8733
+ }
8734
+ if (boxed) {
8735
+ // Moving to another outline, such as a hovered part pulled out of its group, eases the stem across.
8736
+ if (hadBox && morph.anchor && morph.group !== boxed.group) {
8737
+ morph.shift = { x: morph.anchor.x - boxed.anchor.x, y: morph.anchor.y - boxed.anchor.y, start: now };
8738
+ }
8739
+ morph.group = boxed.group;
8740
+ const remaining = 1 - ease(progress(morph.shift.start));
8741
+ morph.anchor = { x: boxed.anchor.x + morph.shift.x * remaining, y: boxed.anchor.y + morph.shift.y * remaining };
8742
+ }
8743
+ if (morph.value !== morph.target || (boxed && progress(morph.shift.start) < 1)) {
8744
+ this.viewer.scene.requestRender();
8745
+ }
8746
+ return morph.value;
8747
+ }
7830
8748
  /**
7831
8749
  * Removes label from the viewer and unsubscribes from events.
7832
8750
  */
@@ -12872,7 +13790,9 @@
12872
13790
  entityId: params.entityId,
12873
13791
  menuItemId: params.menuItemId,
12874
13792
  position: params.position ? { ...params.position } : undefined,
12875
- createIfMissing: params.createIfMissing !== false
13793
+ createIfMissing: params.createIfMissing !== false,
13794
+ text: params.text,
13795
+ pos3d: params.pos3d ? Cesium.Cartesian3.clone(params.pos3d) : undefined
12876
13796
  };
12877
13797
  const doc = this.viewer.container.ownerDocument;
12878
13798
  // Only a label following the cursor should go when the cursor leaves the canvas.
@@ -12888,12 +13808,26 @@
12888
13808
  this.SyncLabelHover();
12889
13809
  this.viewer.scene.requestRender();
12890
13810
  }
13811
+ /*
13812
+ * Which Entity holds the label hover, if any, so a caller can leave another's hover alone.
13813
+ */
13814
+ GetLabelHoverTarget() {
13815
+ const hover = this.labelHover;
13816
+ return hover ? { entityId: hover.entityId, menuItemId: hover.menuItemId } : undefined;
13817
+ }
13818
+ /*
13819
+ * Whether the label hover is on this Entity, with or without a cursor position.
13820
+ */
13821
+ IsLabelHovered(entityId, menuItemId) {
13822
+ const hover = this.labelHover;
13823
+ return Boolean(entityId) && (hover === null || hover === void 0 ? void 0 : hover.entityId) === entityId && (hover.menuItemId == null || hover.menuItemId === menuItemId);
13824
+ }
12891
13825
  GetLabelHoverPosition(entityId, menuItemId) {
12892
13826
  const hover = this.labelHover;
12893
13827
  return (hover === null || hover === void 0 ? void 0 : hover.entityId) === entityId && (hover.menuItemId == null || hover.menuItemId === menuItemId) ? hover.position : undefined;
12894
13828
  }
12895
13829
  SyncLabelHover() {
12896
- var _a, _b, _c;
13830
+ var _a, _b, _c, _d;
12897
13831
  const hover = this.labelHover;
12898
13832
  if (!hover || this.disposed) {
12899
13833
  return;
@@ -12925,12 +13859,24 @@
12925
13859
  this.ReleaseHoverLabel(true);
12926
13860
  return;
12927
13861
  }
13862
+ const hoverOptions = {
13863
+ presentation: "text",
13864
+ text: (_b = hover.text) !== null && _b !== void 0 ? _b : group === null || group === void 0 ? void 0 : group.options.text,
13865
+ selectOnClick: false
13866
+ };
12928
13867
  if (this.hoverLabel && !this.hoverLabel.Disposed) {
13868
+ // Rebuilds only when the text changed, as it does when the pointer crosses a cell.
13869
+ if (hover.pos3d && !Cesium.Cartesian3.equals(hover.pos3d, this.hoverLabel.pos3d)) {
13870
+ this.hoverLabel.pos3d = hover.pos3d;
13871
+ }
13872
+ if (hover.text != null) {
13873
+ this.hoverLabel.Configure(hoverOptions);
13874
+ }
12929
13875
  this.hoverLabel.SetHoverVisible(true);
12930
13876
  return;
12931
13877
  }
12932
13878
  // Query-only icon groups have coordinates but no registered scene visual to attach to.
12933
- if (!rego && group && [group.item.longitude, group.item.latitude, (_b = group.item.altitude) !== null && _b !== void 0 ? _b : 0].every(Number.isFinite)) {
13879
+ if (!rego && group && [group.item.longitude, group.item.latitude, (_c = group.item.altitude) !== null && _c !== void 0 ? _c : 0].every(Number.isFinite)) {
12934
13880
  rego = {
12935
13881
  entityId: hover.entityId,
12936
13882
  menuItemId: hover.menuItemId,
@@ -12952,15 +13898,12 @@
12952
13898
  rego,
12953
13899
  viewer: this.viewer,
12954
13900
  visualRegister: this,
12955
- api: (_c = this.ApiGetters) === null || _c === void 0 ? void 0 : _c.GetBruceApi(),
13901
+ api: (_d = this.ApiGetters) === null || _d === void 0 ? void 0 : _d.GetBruceApi(),
12956
13902
  persistent: false,
12957
13903
  transient: true,
12958
13904
  hoverRingState: this.hoverRingState,
12959
- options: {
12960
- presentation: "text",
12961
- text: group === null || group === void 0 ? void 0 : group.options.text,
12962
- selectOnClick: false
12963
- },
13905
+ pos3d: hover.pos3d,
13906
+ options: hoverOptions,
12964
13907
  visible: () => {
12965
13908
  var _a;
12966
13909
  const current = this.GetState(hover);
@@ -45784,6 +46727,10 @@
45784
46727
  TextureFrameSeriesAnimator.AppearanceSignature = AppearanceSignature;
45785
46728
  const MAX_COMPOSITE_TEXELS = 2048;
45786
46729
  const VALUE_DILATE_PASSES = 2;
46730
+ // Decoded frame bytes kept at once. A 2048 square composite is 16 MB, so a long series cannot all stay.
46731
+ const MAX_CACHED_FRAME_BYTES = 256 * 1024 * 1024;
46732
+ // Fetched bytes kept for decoding again, a small fraction of what they decode to.
46733
+ const MAX_RAW_FRAME_BYTES = 96 * 1024 * 1024;
45787
46734
  /*
45788
46735
  * Rewrites a packed 16 bit value as the 8 bit grey the colour ramps index on.
45789
46736
  *
@@ -45804,19 +46751,33 @@
45804
46751
  /*
45805
46752
  * Bleeds covered values outward into uncovered texels, leaving alpha untouched.
45806
46753
  */
45807
- function dilateValues(value, width, height, passes, packed) {
46754
+ function DilateValues(value, width, height, passes, packed) {
45808
46755
  const texels = width * height;
45809
- const filled = new Uint8Array(texels);
46756
+ // 255 for a texel that had data to begin with, otherwise the 1 based pass that filled it, so a pass
46757
+ // reads only texels filled before it and can work in place.
46758
+ const stamp = new Uint8Array(texels);
45810
46759
  for (let p = 0; p < texels; p++) {
45811
- filled[p] = value[p * 4 + 3] !== 0 ? 1 : 0;
46760
+ stamp[p] = value[p * 4 + 3] !== 0 ? 255 : 0;
45812
46761
  }
46762
+ // Whether a texel or a horizontal neighbour had data, so empty space is skipped in three reads.
46763
+ const across = new Uint8Array(texels);
45813
46764
  for (let pass = 0; pass < passes; pass++) {
45814
- const source = value.slice();
45815
- const wasFilled = filled.slice();
46765
+ const current = pass + 1;
46766
+ const had = (p) => stamp[p] !== 0 && stamp[p] !== current;
46767
+ for (let y = 0; y < height; y++) {
46768
+ const row = y * width;
46769
+ for (let x = 0; x < width; x++) {
46770
+ const p = row + x;
46771
+ across[p] = (had(p) || (x > 0 && had(p - 1)) || (x + 1 < width && had(p + 1))) ? 1 : 0;
46772
+ }
46773
+ }
45816
46774
  for (let y = 0; y < height; y++) {
45817
46775
  for (let x = 0; x < width; x++) {
45818
46776
  const p = y * width + x;
45819
- if (wasFilled[p]) {
46777
+ if (stamp[p] !== 0) {
46778
+ continue;
46779
+ }
46780
+ if (!across[p] && !(y > 0 && across[p - width]) && !(y + 1 < height && across[p + width])) {
45820
46781
  continue;
45821
46782
  }
45822
46783
  let sum = 0;
@@ -45825,13 +46786,13 @@
45825
46786
  for (let dx = -1; dx <= 1; dx++) {
45826
46787
  const nx = x + dx;
45827
46788
  const ny = y + dy;
45828
- if (nx < 0 || ny < 0 || nx >= width || ny >= height || !wasFilled[ny * width + nx]) {
46789
+ if (nx < 0 || ny < 0 || nx >= width || ny >= height || !had(ny * width + nx)) {
45829
46790
  continue;
45830
46791
  }
45831
46792
  const at = (ny * width + nx) * 4;
45832
46793
  // Averaged as a value rather than per byte: averaging a packed low byte
45833
46794
  // on its own wraps at every 256th step and speckles the bled edge.
45834
- sum += packed ? source[at] * 256 + source[at + 1] : source[at];
46795
+ sum += packed ? value[at] * 256 + value[at + 1] : value[at];
45835
46796
  hits++;
45836
46797
  }
45837
46798
  }
@@ -45847,18 +46808,25 @@
45847
46808
  value[p * 4 + 1] = v;
45848
46809
  value[p * 4 + 2] = v;
45849
46810
  }
45850
- filled[p] = 1;
46811
+ stamp[p] = current;
45851
46812
  }
45852
46813
  }
45853
46814
  }
45854
46815
  }
45855
46816
  }
46817
+ TextureFrameSeriesAnimator.DilateValues = DilateValues;
46818
+ /*
46819
+ * Bytes one cached frame holds, counting a buffer shared between colour and value once.
46820
+ */
46821
+ function frameBytes(pixels, value) {
46822
+ return (pixels ? pixels.byteLength : 0) + (value && value !== pixels ? value.byteLength : 0);
46823
+ }
45856
46824
  const DEFAULT_CROSSFADE_MS = 900;
45857
46825
  const DEFAULT_LOW_COLOR = { red: 255, green: 255, blue: 255, alpha: 0.12 };
45858
46826
  const DEFAULT_HIGH_COLOR = { red: 21, green: 96, blue: 196, alpha: 0.92 };
45859
46827
  class Animator {
45860
46828
  constructor(options) {
45861
- var _a, _b, _c;
46829
+ var _a, _b, _c, _d;
45862
46830
  this.removeOnTick = null;
45863
46831
  this.removeCrossfadeTick = null;
45864
46832
  this.disposed = false;
@@ -45889,6 +46857,15 @@
45889
46857
  this.extremesLoad = null;
45890
46858
  this.floorPixels = null;
45891
46859
  this.ceilingPixels = null;
46860
+ // How far through the fade from the previous value frame to the presented one, 1 when settled.
46861
+ this.fadeMix = 1;
46862
+ // Where the current fade started from, above zero when it took over from one still running.
46863
+ this.fadeMixStart = 0;
46864
+ // Version the value canvas was last written at, since in value only mode it is written on demand.
46865
+ this.valueCanvasVersion = -1;
46866
+ this.rawBytes = 0;
46867
+ this.cachedBytes = 0;
46868
+ this.lastFrameBytes = 0;
45892
46869
  if (!options.entity.polygon) {
45893
46870
  throw new Error("TextureFrameSeriesAnimator requires an entity with polygon graphics.");
45894
46871
  }
@@ -45936,6 +46913,9 @@
45936
46913
  this.maskBaseline = (_c = options.maskBaseline) !== null && _c !== void 0 ? _c : this.tiles.some((tile) => Boolean(tile.BaselineMask));
45937
46914
  this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
45938
46915
  this.driveMaterial = options.driveMaterial !== false;
46916
+ this.valueOnly = this.produceValueCanvas && !this.driveMaterial;
46917
+ this.rawCache = new Array(this.frames.length).fill(null);
46918
+ this.cacheBudget = (_d = options.cacheBytes) !== null && _d !== void 0 ? _d : MAX_CACHED_FRAME_BYTES;
45939
46919
  this.originalMaterial = options.entity.polygon.material;
45940
46920
  this.imageProperty = new Cesium.CallbackProperty(() => this.pool[this.poolIdx], false);
45941
46921
  if (this.driveMaterial) {
@@ -46001,7 +46981,40 @@
46001
46981
  * or null unless the instance was constructed with produceValueCanvas.
46002
46982
  */
46003
46983
  GetValueCanvas() {
46004
- return this.valueDims ? this.valueCanvas : null;
46984
+ if (!this.valueDims) {
46985
+ return null;
46986
+ }
46987
+ if (this.valueOnly && this.valueCanvasVersion !== this.presentVersion && this.displayedValuePixels) {
46988
+ this.writeInto(this.valueCanvas, this.displayedValuePixels, this.valueDims);
46989
+ this.valueCanvasVersion = this.presentVersion;
46990
+ }
46991
+ return this.valueCanvas;
46992
+ }
46993
+ /**
46994
+ * The presented frame's value pixels, or null before one. Shares the cached array, so never write to it.
46995
+ */
46996
+ GetValueRaster() {
46997
+ if (!this.valueDims || !this.displayedValuePixels) {
46998
+ return null;
46999
+ }
47000
+ return { data: this.displayedValuePixels, width: this.valueDims.width, height: this.valueDims.height };
47001
+ }
47002
+ /**
47003
+ * The two value frames being faded between and the mix, or null unless the material is not driven.
47004
+ */
47005
+ GetValueFade() {
47006
+ if (!this.valueOnly || !this.valueDims || !this.displayedValuePixels) {
47007
+ return null;
47008
+ }
47009
+ const previous = this.fadeFromValuePixels;
47010
+ const from = previous && previous.length === this.displayedValuePixels.length ? previous : null;
47011
+ return {
47012
+ from,
47013
+ to: this.displayedValuePixels,
47014
+ width: this.valueDims.width,
47015
+ height: this.valueDims.height,
47016
+ mix: from ? this.fadeMix : 1
47017
+ };
46005
47018
  }
46006
47019
  /**
46007
47020
  * Increments every time a frame is presented, so a consumer holding the value canvas can tell
@@ -46068,7 +47081,11 @@
46068
47081
  if (this.currentFrameIndex < 0) {
46069
47082
  return this.frameCache[0] ? -1 : 0;
46070
47083
  }
46071
- for (let d = 0; d < n; d++) {
47084
+ // Full once another frame will not fit, so a prefetch only displaces a frame further from the
47085
+ // playhead than itself and never trades the two ends of the window back and forth.
47086
+ const full = this.cachedBytes + this.lastFrameBytes > this.cacheBudget;
47087
+ const reach = full ? this.farthestCachedDistance() : n;
47088
+ for (let d = 0; d < Math.min(n, reach); d++) {
46072
47089
  const forward = this.currentFrameIndex + d;
46073
47090
  if (forward < n && !this.frameCache[forward]) {
46074
47091
  return forward;
@@ -46080,6 +47097,68 @@
46080
47097
  }
46081
47098
  return -1;
46082
47099
  }
47100
+ farthestCachedDistance() {
47101
+ let farthest = 0;
47102
+ for (let i = 0; i < this.frameCache.length; i++) {
47103
+ if (this.frameCache[i]) {
47104
+ farthest = Math.max(farthest, Math.abs(i - this.currentFrameIndex));
47105
+ }
47106
+ }
47107
+ return farthest;
47108
+ }
47109
+ /*
47110
+ * Drops decoded frames furthest from the playhead until the cache fits its budget again.
47111
+ */
47112
+ evictDecoded(keep) {
47113
+ // A tinted fade reads its target from the cache each tick, where a value fade holds the arrays.
47114
+ const fading = !this.valueOnly && this.fadeStartMs !== null ? this.fadeToIndex : -1;
47115
+ while (this.cachedBytes > this.cacheBudget) {
47116
+ let victim = -1;
47117
+ let distance = -1;
47118
+ for (let i = 0; i < this.frameCache.length; i++) {
47119
+ if (!this.frameCache[i] || i === keep || i === this.currentFrameIndex || i === fading) {
47120
+ continue;
47121
+ }
47122
+ const d = Math.abs(i - this.currentFrameIndex);
47123
+ if (d > distance) {
47124
+ distance = d;
47125
+ victim = i;
47126
+ }
47127
+ }
47128
+ if (victim < 0) {
47129
+ return;
47130
+ }
47131
+ this.cachedBytes -= frameBytes(this.frameCache[victim], this.valueCache[victim]);
47132
+ this.frameCache[victim] = null;
47133
+ this.valueCache[victim] = null;
47134
+ }
47135
+ }
47136
+ /*
47137
+ * Keeps a frame's fetched bytes unless they are large, as a tile major archive's are.
47138
+ */
47139
+ keepRaw(idx, buffer, base) {
47140
+ if (buffer.byteLength > MAX_RAW_FRAME_BYTES / 16) {
47141
+ return;
47142
+ }
47143
+ this.rawCache[idx] = { buffer, base };
47144
+ this.rawBytes += buffer.byteLength;
47145
+ while (this.rawBytes > MAX_RAW_FRAME_BYTES) {
47146
+ let victim = -1;
47147
+ let distance = -1;
47148
+ for (let i = 0; i < this.rawCache.length; i++) {
47149
+ const d = Math.abs(i - this.currentFrameIndex);
47150
+ if (this.rawCache[i] && i !== idx && d > distance) {
47151
+ distance = d;
47152
+ victim = i;
47153
+ }
47154
+ }
47155
+ if (victim < 0) {
47156
+ return;
47157
+ }
47158
+ this.rawBytes -= this.rawCache[victim].buffer.byteLength;
47159
+ this.rawCache[victim] = null;
47160
+ }
47161
+ }
46083
47162
  pumpFetchQueue() {
46084
47163
  if (this.disposed || this.inFlightIndex !== -1) {
46085
47164
  return;
@@ -46123,10 +47202,18 @@
46123
47202
  lo = Math.min(lo, at.ByteOffset);
46124
47203
  hi = Math.max(hi, at.ByteOffset + at.ByteLength);
46125
47204
  }
46126
- const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
46127
- const buffer = await response.arrayBuffer();
46128
- if (this.disposed) {
46129
- return;
47205
+ let buffer;
47206
+ const raw = this.rawCache[idx];
47207
+ if (raw && raw.base === lo) {
47208
+ buffer = raw.buffer;
47209
+ }
47210
+ else {
47211
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
47212
+ buffer = await response.arrayBuffer();
47213
+ if (this.disposed) {
47214
+ return;
47215
+ }
47216
+ this.keepRaw(idx, buffer, lo);
46130
47217
  }
46131
47218
  // Awaited before decoding rather than applied later, so no frame is ever cached unmasked and
46132
47219
  // then corrected once the mask lands.
@@ -46138,9 +47225,15 @@
46138
47225
  if (this.disposed) {
46139
47226
  return;
46140
47227
  }
47228
+ if (this.frameCache[idx]) {
47229
+ this.cachedBytes -= frameBytes(this.frameCache[idx], this.valueCache[idx]);
47230
+ }
46141
47231
  this.frameCache[idx] = decoded.pixels;
46142
47232
  this.frameDims[idx] = { width: decoded.width, height: decoded.height };
46143
47233
  this.valueCache[idx] = decoded.valuePixels || null;
47234
+ this.lastFrameBytes = frameBytes(decoded.pixels, decoded.valuePixels || null);
47235
+ this.cachedBytes += this.lastFrameBytes;
47236
+ this.evictDecoded(idx);
46144
47237
  if (this.shouldPresent(idx, isTarget)) {
46145
47238
  this.beginCrossfadeTo(idx);
46146
47239
  }
@@ -46210,6 +47303,13 @@
46210
47303
  */
46211
47304
  tint(imageData) {
46212
47305
  const canvas = { width: imageData.width, height: imageData.height };
47306
+ if (this.valueOnly) {
47307
+ // One array serves as both, since nothing will ever display the colour.
47308
+ const values = imageData.data;
47309
+ this.applyBaselineMask(values, undefined, canvas.width, canvas.height);
47310
+ DilateValues(values, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
47311
+ return { pixels: values, width: canvas.width, height: canvas.height, valuePixels: values };
47312
+ }
46213
47313
  // Copied before tinting: the ramp is a lerp between two colours, so the value cannot be
46214
47314
  // recovered from the tinted pixels afterwards.
46215
47315
  const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
@@ -46230,7 +47330,7 @@
46230
47330
  }
46231
47331
  this.applyBaselineMask(imageData.data, valuePixels, canvas.width, canvas.height);
46232
47332
  if (valuePixels) {
46233
- dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
47333
+ DilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
46234
47334
  }
46235
47335
  return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
46236
47336
  }
@@ -46488,6 +47588,10 @@
46488
47588
  }
46489
47589
  }
46490
47590
  beginCrossfadeTo(idx) {
47591
+ if (this.valueOnly) {
47592
+ this.beginValueFadeTo(idx);
47593
+ return;
47594
+ }
46491
47595
  const to = this.frameCache[idx];
46492
47596
  this.fadeFromValuePixels = this.displayedValuePixels === null && this.valueCache[idx]
46493
47597
  ? new Uint8ClampedArray(this.valueCache[idx].length)
@@ -46497,12 +47601,53 @@
46497
47601
  this.fadeToDims = this.frameDims[idx];
46498
47602
  this.fadeStartMs = Now();
46499
47603
  }
47604
+ /*
47605
+ * Presents a value frame at once and leaves the fade to the consumer, which blends on the GPU.
47606
+ */
47607
+ beginValueFadeTo(idx) {
47608
+ const to = this.valueCache[idx];
47609
+ if (!to || to === this.displayedValuePixels) {
47610
+ return;
47611
+ }
47612
+ // A new target mid fade keeps whichever frame is mostly on screen and carries on from the current
47613
+ // mix, so frames landing in quick succession do not jump the sheet back.
47614
+ const midFade = this.fadeStartMs !== null && Boolean(this.fadeFromValuePixels);
47615
+ if (midFade && this.fadeMix < 0.5) {
47616
+ this.fadeMixStart = this.fadeMix;
47617
+ }
47618
+ else {
47619
+ this.fadeFromValuePixels = this.displayedValuePixels;
47620
+ this.fadeMixStart = 0;
47621
+ }
47622
+ this.displayedValuePixels = to;
47623
+ this.valueDims = this.frameDims[idx];
47624
+ this.fadeToIndex = idx;
47625
+ this.fadeMix = this.fadeFromValuePixels ? this.fadeMixStart : 1;
47626
+ this.fadeStartMs = this.fadeFromValuePixels ? Now() : null;
47627
+ this.presentVersion++;
47628
+ this.requestRender();
47629
+ }
47630
+ requestRender() {
47631
+ if (this.viewer && this.viewer.scene && this.viewer.scene.requestRenderMode) {
47632
+ this.viewer.scene.requestRender();
47633
+ }
47634
+ }
46500
47635
  tickCrossfade() {
46501
47636
  if (this.disposed || this.fadeStartMs === null) {
46502
47637
  return;
46503
47638
  }
46504
47639
  const t = Math.min(1, (Now() - this.fadeStartMs) / this.crossfadeMs);
46505
47640
  const eased = easeInOutQuad(t);
47641
+ if (this.valueOnly) {
47642
+ this.fadeMix = this.fadeMixStart + (1 - this.fadeMixStart) * eased;
47643
+ if (t >= 1) {
47644
+ this.fadeStartMs = null;
47645
+ this.fadeFromValuePixels = null;
47646
+ this.fadeMix = 1;
47647
+ }
47648
+ this.requestRender();
47649
+ return;
47650
+ }
46506
47651
  const from = this.fadeFromPixels;
46507
47652
  const to = this.frameCache[this.fadeToIndex];
46508
47653
  const n = Math.min(from.length, to.length);
@@ -46542,9 +47687,7 @@
46542
47687
  this.valueDims = dims;
46543
47688
  }
46544
47689
  this.presentVersion++;
46545
- if (this.viewer && this.viewer.scene && this.viewer.scene.requestRenderMode) {
46546
- this.viewer.scene.requestRender();
46547
- }
47690
+ this.requestRender();
46548
47691
  }
46549
47692
  }
46550
47693
  TextureFrameSeriesAnimator.Animator = Animator;
@@ -46563,13 +47706,15 @@
46563
47706
  const MIN_VISIBLE_PIXELS = 60;
46564
47707
  const MAX_STRIDE = 512;
46565
47708
  const STRIDE_HYSTERESIS = 1.4;
46566
- const DEFAULT_MIN_SPACING_PIXELS = 78;
46567
- const DEFAULT_MAX_LABELS = 240;
46568
- const LABEL_RADIUS = 4;
46569
- const LABEL_FONT = "600 11px ui-monospace, SFMono-Regular, Menlo, monospace";
46570
- // Clear space either side of a label box, so neighbours read as separate.
46571
- const LABEL_GAP_PIXELS = 16;
46572
- const LABEL_ANCHOR_COLOR = "rgba(255,255,255,0.85)";
47709
+ // Scene labels carry a leader and are wider than a value, so they want room and not many of them.
47710
+ const DEFAULT_MIN_SPACING_PIXELS = 140;
47711
+ const DEFAULT_MAX_LABELS = 60;
47712
+ // How far past the edge of the view an anchor may sit before its label is dropped.
47713
+ const LABEL_EDGE_MARGIN_PIXELS = 120;
47714
+ // Distance discount for a label already up when the cap forces a choice.
47715
+ const LABEL_KEEP_BIAS = 0.6;
47716
+ // How long a label outlives its cell leaving the wanted set, so one back a frame later does not blink.
47717
+ const LABEL_LINGER_MS = 300;
46573
47718
  // 8 rather than 5: below this a cell reads as texture rather than as a boundary, and it also
46574
47719
  // bounds the worst case, since a full screen of 8 px cells fits inside MAX_GRID_CELLS.
46575
47720
  const DEFAULT_MIN_GRID_CELL_PIXELS = 8;
@@ -46583,7 +47728,8 @@
46583
47728
  const TILE_OUTLINE_ALPHA = 0.45;
46584
47729
  // Cells per grid square at the coarsest. Beyond this a tile is outlined instead.
46585
47730
  const MAX_GRID_STRIDE = 256;
46586
- const LINE_HEIGHT = 13;
47731
+ // Times the pointer's ground hit is moved onto the sheet's own height before reading a cell.
47732
+ const HOVER_REFINE_STEPS = 3;
46587
47733
  /**
46588
47734
  * A clip outline from a projected ring, or null when the ring cannot be drawn.
46589
47735
  * @param projected one entry per ring vertex, null where the vertex has no window coordinate
@@ -46677,11 +47823,25 @@
46677
47823
  this.sourceMap = null;
46678
47824
  this.source = null;
46679
47825
  this.painted = null;
47826
+ this.paintedRamp = null;
46680
47827
  this.floor = null;
46681
47828
  this.ceiling = null;
46682
47829
  this.disposed = false;
46683
- this.drawn = 0;
46684
47830
  this.stride = 0;
47831
+ this.cellLabels = new Map();
47832
+ this.lingerTimer = null;
47833
+ // What the last draw was made from, so a frame that changes none of it draws nothing.
47834
+ this.drawnView = null;
47835
+ this.drawnInputs = [];
47836
+ this.scratchWorld = new Cesium.Cartesian3();
47837
+ this.scratchWindow = new Cesium.Cartesian2();
47838
+ this.scratchClip = new Cesium.Cartesian4();
47839
+ // The camera's view and projection as one matrix, rebuilt per draw, or null outside 3D.
47840
+ this.viewProjection = null;
47841
+ this.viewProjectionScratch = new Cesium.Matrix4();
47842
+ this.hovering = false;
47843
+ this.hoverFrame = 0;
47844
+ this.hoverAt = null;
46685
47845
  this.viewer = options.viewer;
46686
47846
  this.extent = options.extent;
46687
47847
  this.settings = options.settings || {};
@@ -46691,6 +47851,9 @@
46691
47851
  this.sourceMap = exports.TextureFrameSeriesAnimator.SourceMap(options.metadata);
46692
47852
  this.tiles = options.tiles;
46693
47853
  this.clipRing = options.clipRing;
47854
+ this.heightAt = options.heightAt;
47855
+ this.heightKey = options.heightKey;
47856
+ this.hover = options.hover;
46694
47857
  this.canvas = document.createElement("canvas");
46695
47858
  this.canvas.style.position = "absolute";
46696
47859
  this.canvas.style.inset = "0";
@@ -46700,12 +47863,31 @@
46700
47863
  parent.appendChild(this.canvas);
46701
47864
  }
46702
47865
  this.ctx = this.canvas.getContext("2d");
47866
+ this.onPointerMove = (event) => {
47867
+ const bounds = this.viewer.canvas.getBoundingClientRect();
47868
+ this.hoverAt = { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
47869
+ // At most once a frame, since a pointer reports far faster than anything is drawn.
47870
+ if (!this.hoverFrame) {
47871
+ this.hoverFrame = requestAnimationFrame(() => {
47872
+ this.hoverFrame = 0;
47873
+ this.updateHover();
47874
+ });
47875
+ }
47876
+ };
47877
+ this.onPointerLeave = () => {
47878
+ this.hoverAt = null;
47879
+ this.releaseHover();
47880
+ };
47881
+ if (this.hover && this.hover.register) {
47882
+ this.viewer.canvas.addEventListener("pointermove", this.onPointerMove, { passive: true });
47883
+ this.viewer.canvas.addEventListener("pointerleave", this.onPointerLeave);
47884
+ }
46703
47885
  }
46704
47886
  SetExtent(extent) {
46705
47887
  this.extent = extent;
46706
47888
  }
46707
47889
  /**
46708
- * The current frame's decoded values, as the animator's value canvas already provides them.
47890
+ * The current frame's decoded values.
46709
47891
  * @param source value pixels, one texel per cell
46710
47892
  */
46711
47893
  SetSource(source) {
@@ -46713,11 +47895,17 @@
46713
47895
  }
46714
47896
  /**
46715
47897
  * The tinted pixels of the same frame, so the grid can skip cells the ramp painted nothing in.
46716
- * @param painted RGBA of the presented frame, or null to fall back to coverage alone
47898
+ * @param painted RGBA of the presented frame, or null to fall back to the ramp or coverage alone
46717
47899
  */
46718
47900
  SetPainted(painted) {
46719
47901
  this.painted = painted;
46720
47902
  }
47903
+ /**
47904
+ * The surface's 256 step RGBA ramp, for telling whether a cell was painted without a tinted frame.
47905
+ */
47906
+ SetPaintedRamp(ramp) {
47907
+ this.paintedRamp = ramp;
47908
+ }
46721
47909
  /**
46722
47910
  * The archive's per-cell extremes, when it publishes them. Without these a label can still
46723
47911
  * print the current reading, just not the range behind it.
@@ -46727,21 +47915,28 @@
46727
47915
  this.ceiling = ceiling;
46728
47916
  }
46729
47917
  GetDrawnCount() {
46730
- return this.drawn;
47918
+ return this.cellLabels.size;
46731
47919
  }
46732
47920
  Clear() {
46733
47921
  this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
46734
- this.drawn = 0;
47922
+ this.drawnView = null;
46735
47923
  }
46736
47924
  Dispose() {
46737
47925
  this.disposed = true;
47926
+ this.clearCellLabels();
47927
+ this.releaseHover();
47928
+ if (this.hoverFrame) {
47929
+ cancelAnimationFrame(this.hoverFrame);
47930
+ this.hoverFrame = 0;
47931
+ }
47932
+ this.viewer.canvas.removeEventListener("pointermove", this.onPointerMove);
47933
+ this.viewer.canvas.removeEventListener("pointerleave", this.onPointerLeave);
46738
47934
  if (this.canvas.parentElement) {
46739
47935
  this.canvas.parentElement.removeChild(this.canvas);
46740
47936
  }
46741
47937
  }
46742
47938
  /**
46743
- * Redraws every visible label. Cheap enough to call per rendered frame, because the cell
46744
- * count it considers is bounded by the view rather than by the texture.
47939
+ * Redraws the grid and syncs the value labels, when anything they are drawn from has changed.
46745
47940
  */
46746
47941
  Render() {
46747
47942
  var _a, _b, _c;
@@ -46749,67 +47944,296 @@
46749
47944
  return;
46750
47945
  }
46751
47946
  const view = this.viewer.canvas;
47947
+ const inputs = [this.source, this.painted, this.paintedRamp, this.floor, this.ceiling,
47948
+ view.clientWidth, view.clientHeight, this.heightKey ? this.heightKey() : null];
47949
+ const camera = this.viewer.camera.viewMatrix;
47950
+ if (this.drawnView &&
47951
+ Cesium.Matrix4.equals(this.drawnView, camera) &&
47952
+ inputs.every((input, i) => input === this.drawnInputs[i])) {
47953
+ return;
47954
+ }
46752
47955
  if (this.canvas.width !== view.clientWidth || this.canvas.height !== view.clientHeight) {
46753
47956
  this.canvas.width = view.clientWidth;
46754
47957
  this.canvas.height = view.clientHeight;
46755
47958
  }
46756
47959
  this.Clear();
47960
+ this.drawnView = Cesium.Matrix4.clone(camera, this.drawnView || undefined);
47961
+ this.drawnInputs = inputs;
47962
+ const projection = this.viewer.camera.frustum.projectionMatrix;
47963
+ this.viewProjection = this.viewer.scene.mode === Cesium.SceneMode.SCENE3D
47964
+ ? Cesium.Matrix4.multiply(projection, camera, this.viewProjectionScratch)
47965
+ : null;
46757
47966
  const cols = this.source.width;
46758
47967
  const rows = this.source.height;
46759
47968
  const window = this.visibleTexels(cols, rows);
46760
47969
  if (!window) {
47970
+ this.syncCellLabels([]);
46761
47971
  return;
46762
47972
  }
46763
- const midCol = (window.col0 + window.col1) / 2;
46764
- const midRow = (window.row0 + window.row1) / 2;
46765
- const a = this.project(this.lonAt(midCol, cols), this.latAt(midRow, rows));
46766
- const b = this.project(this.lonAt(midCol + 1, cols), this.latAt(midRow + 1, rows));
47973
+ // Measured where the view is looking, since in an oblique view up close the middle of the visible
47974
+ // texels sits out toward the horizon, where cells are a fraction of the size.
47975
+ const focus = this.groundAtScreenCentre();
47976
+ const at = focus ? this.texelAt(focus.lon, focus.lat) : null;
47977
+ const focused = (Boolean(at) &&
47978
+ at.col >= window.col0 &&
47979
+ at.col <= window.col1 &&
47980
+ at.row >= window.row0 &&
47981
+ at.row <= window.row1);
47982
+ const midCol = focused ? at.col : (window.col0 + window.col1) / 2;
47983
+ const midRow = focused ? at.row : (window.row0 + window.row1) / 2;
47984
+ const a = this.projectCell(midCol, midRow, cols, rows);
47985
+ const b = this.projectCell(midCol + 1, midRow + 1, cols, rows);
46767
47986
  if (!a || !b) {
47987
+ // Left as they are, since one frame where the focus will not project is not the data leaving.
46768
47988
  return;
46769
47989
  }
46770
47990
  const cellPixels = Math.min(Math.abs(b.x - a.x), Math.abs(b.y - a.y));
46771
47991
  // Ahead of the label threshold on purpose: a grid still reads at a cell size where a
46772
47992
  // number no longer fits.
46773
47993
  this.drawGrid(window, cols, rows, cellPixels);
46774
- if (cellPixels < ((_a = this.settings.minCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_CELL_PIXELS)) {
46775
- return;
46776
- }
46777
- const visibleWidth = cellPixels * (window.col1 - window.col0 + 1);
46778
- const visibleHeight = cellPixels * (window.row1 - window.row0 + 1);
46779
- if (Math.max(visibleWidth, visibleHeight) < MIN_VISIBLE_PIXELS) {
47994
+ // By area rather than the shorter side, which an oblique view foreshortens to a fraction of the other.
47995
+ const across = this.projectCell(midCol + 1, midRow, cols, rows);
47996
+ const down = this.projectCell(midCol, midRow + 1, cols, rows);
47997
+ const cellSize = across && down
47998
+ ? Math.sqrt(Math.abs((across.x - a.x) * (down.y - a.y) - (across.y - a.y) * (down.x - a.x)))
47999
+ : cellPixels;
48000
+ const visibleWidth = cellSize * (window.col1 - window.col0 + 1);
48001
+ const visibleHeight = cellSize * (window.row1 - window.row0 + 1);
48002
+ if (cellSize < ((_a = this.settings.minCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_CELL_PIXELS) ||
48003
+ Math.max(visibleWidth, visibleHeight) < MIN_VISIBLE_PIXELS) {
48004
+ this.syncCellLabels([]);
46780
48005
  return;
46781
48006
  }
46782
- const parts = this.settings.parts || ["value", "floor", "ceiling"];
46783
- this.ctx.font = LABEL_FONT;
46784
- this.ctx.textAlign = "center";
46785
48007
  const maxLabels = (_b = this.settings.maxLabels) !== null && _b !== void 0 ? _b : DEFAULT_MAX_LABELS;
46786
48008
  this.stride = ChooseLabelStride({
46787
- cellPixels,
48009
+ cellPixels: cellSize,
46788
48010
  visibleCols: window.col1 - window.col0 + 1,
46789
48011
  visibleRows: window.row1 - window.row0 + 1,
46790
48012
  maxLabels,
46791
- // Measured, not assumed: the spacing that keeps labels apart is whatever the widest
46792
- // one is, so changing what a label says cannot quietly start overlapping them.
46793
- minSpacingPixels: Math.max((_c = this.settings.minSpacingPixels) !== null && _c !== void 0 ? _c : DEFAULT_MIN_SPACING_PIXELS, this.widestLabel(parts)),
48013
+ minSpacingPixels: (_c = this.settings.minSpacingPixels) !== null && _c !== void 0 ? _c : DEFAULT_MIN_SPACING_PIXELS,
46794
48014
  held: this.stride
46795
48015
  });
46796
- const stride = this.stride;
48016
+ const parts = this.settings.parts || ["value", "floor", "ceiling"];
48017
+ this.syncCellLabels(this.labelCells(window, cols, rows, this.stride, maxLabels, parts));
48018
+ }
48019
+ /*
48020
+ * One labelled cell per lattice block, keyed by the block's origin so it survives frame and stride changes.
48021
+ */
48022
+ labelCells(window, cols, rows, stride, max, parts) {
48023
+ const width = this.canvas.width;
48024
+ const height = this.canvas.height;
46797
48025
  const firstRow = Math.floor(window.row0 / stride) * stride;
46798
48026
  const firstCol = Math.floor(window.col0 / stride) * stride;
46799
- for (let row = firstRow; row <= window.row1 && this.drawn < maxLabels; row += stride) {
46800
- for (let col = firstCol; col <= window.col1 && this.drawn < maxLabels; col += stride) {
48027
+ const candidates = [];
48028
+ for (let row = firstRow; row <= window.row1; row += stride) {
48029
+ for (let col = firstCol; col <= window.col1; col += stride) {
46801
48030
  const found = this.readNear(col, row, cols, rows, stride);
46802
48031
  if (!found) {
46803
48032
  continue;
46804
48033
  }
46805
- const point = this.project(this.lonAt(found.col, cols), this.latAt(found.row, rows));
46806
- if (!point || point.x < 0 || point.y < 0
46807
- || point.x > this.canvas.width || point.y > this.canvas.height) {
48034
+ const point = this.projectCell(found.col, found.row, cols, rows);
48035
+ // A margin past the edge, so a label does not drop the moment its anchor does.
48036
+ const margin = LABEL_EDGE_MARGIN_PIXELS;
48037
+ if (!point ||
48038
+ point.x < -margin ||
48039
+ point.y < -margin ||
48040
+ point.x > width + margin ||
48041
+ point.y > height + margin) {
46808
48042
  continue;
46809
48043
  }
46810
- this.drawLabel(point, found.reading, parts, this.cellOutline(found.col, found.row, cols, rows));
48044
+ const text = this.labelLines(found.reading, parts).join("\n");
48045
+ if (!text) {
48046
+ continue;
48047
+ }
48048
+ const key = col + "," + row;
48049
+ const distance = Math.hypot(point.x - width / 2, point.y - height / 2);
48050
+ candidates.push({
48051
+ key,
48052
+ lon: this.lonAt(found.col, cols),
48053
+ lat: this.latAt(found.row, rows),
48054
+ text,
48055
+ // Labels already up outrank new ones, so the capped set does not shuffle as the view pans.
48056
+ rank: this.cellLabels.has(key) ? distance * LABEL_KEEP_BIAS : distance
48057
+ });
48058
+ }
48059
+ }
48060
+ if (candidates.length > max) {
48061
+ candidates.sort((x, y) => x.rank - y.rank);
48062
+ candidates.length = max;
48063
+ }
48064
+ return candidates;
48065
+ }
48066
+ /*
48067
+ * Keeps one scene label per wanted cell, reusing those that are still wanted.
48068
+ */
48069
+ syncCellLabels(wanted) {
48070
+ var _a;
48071
+ const keep = new Set(wanted.map((w) => w.key));
48072
+ const now = Date.now();
48073
+ let lingering = false;
48074
+ for (const [key, entry] of this.cellLabels) {
48075
+ if (keep.has(key)) {
48076
+ entry.missingSince = undefined;
48077
+ continue;
48078
+ }
48079
+ entry.missingSince = (_a = entry.missingSince) !== null && _a !== void 0 ? _a : now;
48080
+ if (now - entry.missingSince >= LABEL_LINGER_MS) {
48081
+ entry.label.Dispose();
48082
+ this.cellLabels.delete(key);
48083
+ }
48084
+ else {
48085
+ lingering = true;
48086
+ }
48087
+ }
48088
+ // A still camera draws nothing more, so a lingering label is given its own later look.
48089
+ if (lingering && !this.lingerTimer) {
48090
+ this.lingerTimer = setTimeout(() => {
48091
+ this.lingerTimer = null;
48092
+ if (!this.disposed) {
48093
+ this.drawnView = null;
48094
+ this.viewer.scene.requestRender();
48095
+ }
48096
+ }, LABEL_LINGER_MS);
48097
+ }
48098
+ for (const want of wanted) {
48099
+ const pos3d = Cesium.Cartesian3.fromDegrees(want.lon, want.lat, this.surfaceHeight(want.lon, want.lat));
48100
+ const options = {
48101
+ presentation: "text",
48102
+ text: want.text,
48103
+ leader: true,
48104
+ selectOnClick: false
48105
+ };
48106
+ const existing = this.cellLabels.get(want.key);
48107
+ if (existing) {
48108
+ if (!Cesium.Cartesian3.equalsEpsilon(existing.label.pos3d, pos3d, 0, 0.01)) {
48109
+ existing.label.pos3d = pos3d;
48110
+ }
48111
+ if (existing.text !== want.text) {
48112
+ existing.label.Configure(options);
48113
+ existing.text = want.text;
48114
+ }
48115
+ continue;
48116
+ }
48117
+ const id = (this.hover ? this.hover.entityId : "texture-value") + "#cell:" + want.key;
48118
+ const label = new exports.EntityLabel.Label({
48119
+ rego: {
48120
+ entityId: id,
48121
+ menuItemId: this.hover ? this.hover.menuItemId : undefined,
48122
+ name: want.text,
48123
+ priority: 0,
48124
+ // No graphic, since a point or billboard visual pulls a label back to itself on rebuild.
48125
+ visual: new Cesium.Entity({ position: pos3d })
48126
+ },
48127
+ viewer: this.viewer,
48128
+ pos3d,
48129
+ options,
48130
+ persistent: true,
48131
+ visible: () => !this.disposed
48132
+ });
48133
+ this.cellLabels.set(want.key, { label, text: want.text });
48134
+ }
48135
+ }
48136
+ clearCellLabels() {
48137
+ if (this.lingerTimer) {
48138
+ clearTimeout(this.lingerTimer);
48139
+ this.lingerTimer = null;
48140
+ }
48141
+ for (const entry of this.cellLabels.values()) {
48142
+ entry.label.Dispose();
48143
+ }
48144
+ this.cellLabels.clear();
48145
+ }
48146
+ /*
48147
+ * Shows the cell under the pointer through the register's hover label, unless something else holds it.
48148
+ */
48149
+ updateHover() {
48150
+ const hover = this.hover;
48151
+ if (this.disposed || !hover || !hover.register || !this.source || !this.hoverAt) {
48152
+ return;
48153
+ }
48154
+ const register = hover.register;
48155
+ const target = register.GetLabelHoverTarget();
48156
+ const ours = Boolean(target) && register.IsLabelHovered(hover.entityId, hover.menuItemId);
48157
+ if (target && !ours) {
48158
+ this.hovering = false;
48159
+ return;
48160
+ }
48161
+ const under = this.readingUnder(this.hoverAt.x, this.hoverAt.y);
48162
+ if (!under) {
48163
+ this.releaseHover();
48164
+ return;
48165
+ }
48166
+ register.SetLabelHover({
48167
+ entityId: hover.entityId,
48168
+ menuItemId: hover.menuItemId,
48169
+ position: { x: this.hoverAt.x, y: this.hoverAt.y },
48170
+ text: under.text,
48171
+ pos3d: under.pos3d
48172
+ });
48173
+ this.hovering = true;
48174
+ }
48175
+ releaseHover() {
48176
+ const hover = this.hover;
48177
+ if (!this.hovering || !hover || !hover.register) {
48178
+ return;
48179
+ }
48180
+ this.hovering = false;
48181
+ if (hover.register.IsLabelHovered(hover.entityId, hover.menuItemId)) {
48182
+ hover.register.ClearLabelHover();
48183
+ }
48184
+ }
48185
+ /*
48186
+ * The text for the painted cell under a window position and its centre on the sheet, or null.
48187
+ */
48188
+ readingUnder(x, y) {
48189
+ const at = this.cellUnder(x, y);
48190
+ if (!at) {
48191
+ return null;
48192
+ }
48193
+ const cols = this.source.width;
48194
+ const found = this.readAt(at.col, at.row, cols);
48195
+ if (!found || !this.paintedAt(at.col, at.row, cols)) {
48196
+ return null;
48197
+ }
48198
+ const text = this.labelLines(found, this.settings.parts || ["value", "floor", "ceiling"]).join("\n");
48199
+ if (!text) {
48200
+ return null;
48201
+ }
48202
+ const lon = this.lonAt(at.col, cols);
48203
+ const lat = this.latAt(at.row, this.source.height);
48204
+ return { text, pos3d: Cesium.Cartesian3.fromDegrees(lon, lat, this.surfaceHeight(lon, lat)) };
48205
+ }
48206
+ /*
48207
+ * The cell under a window position, found on the sheet rather than on the ground beneath it.
48208
+ */
48209
+ cellUnder(x, y) {
48210
+ const scene = this.viewer.scene;
48211
+ const camera = this.viewer.camera;
48212
+ const screen = new Cesium.Cartesian2(x, y);
48213
+ const ray = camera.getPickRay(screen);
48214
+ if (!ray) {
48215
+ return null;
48216
+ }
48217
+ let hit = (scene.globe && scene.globe.pick(ray, scene)) || camera.pickEllipsoid(screen);
48218
+ if (!hit) {
48219
+ return null;
48220
+ }
48221
+ const radii = Cesium.Ellipsoid.WGS84.radii;
48222
+ for (let i = 0; i < HOVER_REFINE_STEPS && this.heightAt; i++) {
48223
+ const carto = Cesium.Cartographic.fromCartesian(hit);
48224
+ const height = this.heightAt(Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude));
48225
+ if (height == null) {
48226
+ break;
48227
+ }
48228
+ const inflated = new Cesium.Ellipsoid(radii.x + height, radii.y + height, radii.z + height);
48229
+ const interval = Cesium.IntersectionTests.rayEllipsoid(ray, inflated);
48230
+ if (!interval) {
48231
+ break;
46811
48232
  }
48233
+ hit = Cesium.Ray.getPoint(ray, interval.start);
46812
48234
  }
48235
+ const carto = Cesium.Cartographic.fromCartesian(hit);
48236
+ return this.texelAt(Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude));
46813
48237
  }
46814
48238
  /*
46815
48239
  * Outlines each covered cell, from a projected lattice of its corners.
@@ -46837,44 +48261,25 @@
46837
48261
  if (wide * high > MAX_GRID_CELLS) {
46838
48262
  return;
46839
48263
  }
46840
- const lattice = new Array((wide + 1) * (high + 1));
46841
- for (let r = 0; r <= high; r++) {
46842
- for (let c = 0; c <= wide; c++) {
46843
- // Corners sit half a texel out from the centres lonAt/latAt return.
46844
- const lon = this.lonAt(window.col0 + c - 0.5, cols);
46845
- const lat = this.latAt(window.row0 + r - 0.5, rows);
46846
- lattice[r * (wide + 1) + c] = this.project(lon, lat);
46847
- }
46848
- }
46849
- const path = new Path2D();
46850
- let drew = false;
48264
+ const lattice = new Lattice(wide + 1, high + 1, (c, r) => {
48265
+ // Corners sit half a texel out from the centres lonAt/latAt return.
48266
+ const lon = this.lonAt(window.col0 + c - 0.5, cols);
48267
+ const lat = this.latAt(window.row0 + r - 0.5, rows);
48268
+ return this.projectInto(lon, lat);
48269
+ });
48270
+ const covered = new Uint8Array(wide * high);
46851
48271
  for (let r = 0; r < high; r++) {
46852
48272
  for (let c = 0; c < wide; c++) {
46853
- if (!this.isCovered(window.col0 + c, window.row0 + r, cols)) {
46854
- continue;
46855
- }
46856
- const tl = lattice[r * (wide + 1) + c];
46857
- const tr = lattice[r * (wide + 1) + c + 1];
46858
- const bl = lattice[(r + 1) * (wide + 1) + c];
46859
- const br = lattice[(r + 1) * (wide + 1) + c + 1];
46860
- if (!tl || !tr || !bl || !br) {
46861
- continue;
46862
- }
46863
- path.moveTo(tl.x, tl.y);
46864
- path.lineTo(tr.x, tr.y);
46865
- path.lineTo(br.x, br.y);
46866
- path.lineTo(bl.x, bl.y);
46867
- path.closePath();
46868
- drew = true;
48273
+ covered[r * wide + c] = this.isCovered(window.col0 + c, window.row0 + r, cols) ? 1 : 0;
46869
48274
  }
46870
48275
  }
46871
- if (!drew) {
48276
+ const path = new Path2D();
48277
+ if (!lattice.Trace(path, covered, wide, high)) {
46872
48278
  return;
46873
48279
  }
46874
48280
  this.ctx.strokeStyle = color;
46875
48281
  this.ctx.lineWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
46876
48282
  this.ctx.stroke(path);
46877
- +" alpha" + this.ctx.globalAlpha;
46878
48283
  }
46879
48284
  /*
46880
48285
  * Restricts drawing to the polygon, so nothing is ruled over ground it does not cover.
@@ -46896,33 +48301,11 @@
46896
48301
  path.closePath();
46897
48302
  this.ctx.clip(path);
46898
48303
  }
46899
- /*
46900
- * The cell of whichever tile covers a point, as screen corners.
46901
- */
46902
- tileCellOutline(lon, lat) {
46903
- for (const tile of this.tiles) {
46904
- if (lon < tile.West || lon > tile.East || lat < tile.South || lat > tile.North) {
46905
- continue;
46906
- }
46907
- const c = (Math.min(tile.ResolutionX - 1, Math.max(0, Math.floor((lon - tile.West) / (tile.East - tile.West) * tile.ResolutionX))));
46908
- const r = (Math.min(tile.ResolutionY - 1, Math.max(0, Math.floor((tile.North - lat) / (tile.North - tile.South) * tile.ResolutionY))));
46909
- const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
46910
- const w1 = tile.West + ((c + 1) / tile.ResolutionX) * (tile.East - tile.West);
46911
- const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
46912
- const n1 = tile.North - ((r + 1) / tile.ResolutionY) * (tile.North - tile.South);
46913
- const corners = [
46914
- this.project(w0, n0), this.project(w1, n0),
46915
- this.project(w1, n1), this.project(w0, n1)
46916
- ];
46917
- return corners.every((q) => q) ? corners : null;
46918
- }
46919
- return null;
46920
- }
46921
48304
  /*
46922
48305
  * Outlines each tile's own cells, so cell size still shows where the mesh is refined.
46923
48306
  */
46924
48307
  drawTileGrid(color) {
46925
- var _a, _b, _c;
48308
+ var _a, _b;
46926
48309
  const view = this.viewer.camera.computeViewRectangle(Cesium.Ellipsoid.WGS84);
46927
48310
  if (!view) {
46928
48311
  return;
@@ -46951,7 +48334,7 @@
46951
48334
  coarse.lineTo(tr.x, tr.y);
46952
48335
  coarse.lineTo(br.x, br.y);
46953
48336
  coarse.lineTo(bl.x, bl.y);
46954
- coarse.closePath();
48337
+ coarse.lineTo(tl.x, tl.y);
46955
48338
  drewCoarse = true;
46956
48339
  };
46957
48340
  // Planned for every visible tile BEFORE any is drawn.
@@ -47012,31 +48395,29 @@
47012
48395
  // moves, and so each step up is a subset of the one below it.
47013
48396
  const firstR = Math.floor(r0 / step) * step;
47014
48397
  const firstC = Math.floor(c0 / step) * step;
47015
- for (let r = firstR; r < r1; r += step) {
47016
- const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
47017
- const n1 = tile.North
47018
- - (Math.min(r + step, tile.ResolutionY) / tile.ResolutionY) * (tile.North - tile.South);
47019
- for (let c = firstC; c < c1; c += step) {
47020
- const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
47021
- const w1 = tile.West
47022
- + (Math.min(c + step, tile.ResolutionX) / tile.ResolutionX) * (tile.East - tile.West);
47023
- if (!this.coveredAt((w0 + w1) / 2, (n0 + n1) / 2)) {
47024
- continue;
47025
- }
47026
- const tl = this.project(w0, n0);
47027
- const tr = this.project(w1, n0);
47028
- const br = this.project(w1, n1);
47029
- const bl = this.project(w0, n1);
47030
- if (!tl || !tr || !br || !bl) {
47031
- continue;
47032
- }
47033
- path.moveTo(tl.x, tl.y);
47034
- path.lineTo(tr.x, tr.y);
47035
- path.lineTo(br.x, br.y);
47036
- path.lineTo(bl.x, bl.y);
47037
- path.closePath();
47038
- drew = true;
47039
- }
48398
+ const wide = Math.ceil((c1 - firstC) / step);
48399
+ const high = Math.ceil((r1 - firstR) / step);
48400
+ const lonOf = (c) => tile.West
48401
+ + (Math.min(firstC + c * step, tile.ResolutionX) / tile.ResolutionX) * (tile.East - tile.West);
48402
+ const latOf = (r) => tile.North
48403
+ - (Math.min(firstR + r * step, tile.ResolutionY) / tile.ResolutionY) * (tile.North - tile.South);
48404
+ const covered = new Uint8Array(wide * high);
48405
+ let any = false;
48406
+ for (let r = 0; r < high; r++) {
48407
+ for (let c = 0; c < wide; c++) {
48408
+ if (this.coveredAt((lonOf(c) + lonOf(c + 1)) / 2, (latOf(r) + latOf(r + 1)) / 2)) {
48409
+ covered[r * wide + c] = 1;
48410
+ any = true;
48411
+ }
48412
+ }
48413
+ }
48414
+ if (!any) {
48415
+ continue;
48416
+ }
48417
+ // Projected on demand, since a sparse raster leaves most corners belonging to no cell.
48418
+ const lattice = new Lattice(wide + 1, high + 1, (c, r) => this.projectInto(lonOf(c), latOf(r)));
48419
+ if (lattice.Trace(path, covered, wide, high)) {
48420
+ drew = true;
47040
48421
  }
47041
48422
  }
47042
48423
  const gridWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
@@ -47054,7 +48435,7 @@
47054
48435
  return;
47055
48436
  }
47056
48437
  this.ctx.strokeStyle = color;
47057
- this.ctx.lineWidth = (_c = this.settings.gridWidthPixels) !== null && _c !== void 0 ? _c : DEFAULT_GRID_WIDTH_PIXELS;
48438
+ this.ctx.lineWidth = gridWidth;
47058
48439
  this.ctx.stroke(path);
47059
48440
  }
47060
48441
  /*
@@ -47074,7 +48455,7 @@
47074
48455
  };
47075
48456
  }
47076
48457
  /*
47077
- * Whether the composited raster holds data at a lon/lat.
48458
+ * Whether the composited raster holds painted data at a lon/lat.
47078
48459
  */
47079
48460
  coveredAt(lon, lat) {
47080
48461
  if (!this.source) {
@@ -47087,60 +48468,20 @@
47087
48468
  if (col < 0 || row < 0 || col >= cols || row >= rows) {
47088
48469
  return false;
47089
48470
  }
47090
- const at = (row * cols + col) * 4 + 3;
47091
48471
  // Painted, not merely covered: a dry cell holds a reading the ramp deliberately hides, and
47092
48472
  // ruling a grid over it draws detail where the picture shows none.
47093
- if (this.painted && this.painted.length === cols * rows * 4) {
47094
- return this.painted[at] > 0;
48473
+ if (this.painted || this.paintedRamp) {
48474
+ return this.paintedAt(col, row, cols);
47095
48475
  }
47096
- return this.source.data[at] >= 128;
48476
+ return this.source.data[(row * cols + col) * 4 + 3] >= 128;
47097
48477
  }
47098
48478
  isCovered(col, row, cols) {
47099
48479
  return Boolean(this.source && this.source.data[(row * cols + col) * 4 + 3] >= 128);
47100
48480
  }
47101
48481
  /*
47102
- * Width of the widest label this overlay could draw, in pixels.
48482
+ * What a label says about one reading, a line at a time.
47103
48483
  */
47104
- widestLabel(parts) {
47105
- const sample = [];
47106
- if (parts.indexOf("value") >= 0) {
47107
- sample.push(valueLine("-000.00", LabelUnits(this.metadata)));
47108
- }
47109
- const range = [];
47110
- const showsRange = !exports.TextureFrameSeriesAnimator.IsTransformed(this.metadata);
47111
- if (showsRange && parts.indexOf("floor") >= 0) {
47112
- range.push("min: -000.00");
47113
- }
47114
- if (showsRange && parts.indexOf("ceiling") >= 0) {
47115
- range.push("max: -000.00");
47116
- }
47117
- if (range.length) {
47118
- sample.push(range.join(" - "));
47119
- }
47120
- if (!sample.length) {
47121
- return 0;
47122
- }
47123
- const widest = Math.max(...sample.map((line) => this.ctx.measureText(line).width));
47124
- return widest + LABEL_GAP_PIXELS;
47125
- }
47126
- /*
47127
- * The four screen corners of one cell, or null if any of them will not project.
47128
- */
47129
- cellOutline(col, row, cols, rows) {
47130
- // On a pyramid the composited texel is not a cell. Outlining it would draw a second,
47131
- // uniform lattice over the tile grid, misaligned with it everywhere.
47132
- if (this.tiles && this.tiles.length) {
47133
- return this.tileCellOutline(this.lonAt(col, cols), this.latAt(row, rows));
47134
- }
47135
- const corners = [
47136
- this.project(this.lonAt(col - 0.5, cols), this.latAt(row - 0.5, rows)),
47137
- this.project(this.lonAt(col + 0.5, cols), this.latAt(row - 0.5, rows)),
47138
- this.project(this.lonAt(col + 0.5, cols), this.latAt(row + 0.5, rows)),
47139
- this.project(this.lonAt(col - 0.5, cols), this.latAt(row + 0.5, rows))
47140
- ];
47141
- return corners.every((c) => c) ? corners : null;
47142
- }
47143
- drawLabel(point, reading, parts, outline) {
48484
+ labelLines(reading, parts) {
47144
48485
  var _a;
47145
48486
  const dp = (_a = this.settings.decimals) !== null && _a !== void 0 ? _a : (Math.abs(reading.value) < 10 ? 2 : 1);
47146
48487
  const lines = [];
@@ -47159,32 +48500,7 @@
47159
48500
  if (range.length) {
47160
48501
  lines.push(range.join(" - "));
47161
48502
  }
47162
- if (!lines.length) {
47163
- return;
47164
- }
47165
- if (outline) {
47166
- this.ctx.strokeStyle = LABEL_ANCHOR_COLOR;
47167
- this.ctx.lineWidth = 1.5;
47168
- this.ctx.beginPath();
47169
- this.ctx.moveTo(outline[0].x, outline[0].y);
47170
- for (let i = 1; i < outline.length; i++) {
47171
- this.ctx.lineTo(outline[i].x, outline[i].y);
47172
- }
47173
- this.ctx.closePath();
47174
- this.ctx.stroke();
47175
- }
47176
- const width = Math.max(...lines.map((l) => this.ctx.measureText(l).width)) + 10;
47177
- const height = lines.length * LINE_HEIGHT + 6;
47178
- const top = point.y - height / 2;
47179
- this.ctx.fillStyle = "rgba(16,16,14,0.72)";
47180
- this.ctx.beginPath();
47181
- this.ctx.roundRect(point.x - width / 2, top, width, height, LABEL_RADIUS);
47182
- this.ctx.fill();
47183
- lines.forEach((line, i) => {
47184
- this.ctx.fillStyle = i === 0 ? "#ffffff" : "#a8a89f";
47185
- this.ctx.fillText(line, point.x, top + LINE_HEIGHT * (i + 1) - 1);
47186
- });
47187
- this.drawn++;
48503
+ return lines;
47188
48504
  }
47189
48505
  /*
47190
48506
  * The first cell with a reading at or near a lattice point, searched within its own block.
@@ -47211,11 +48527,24 @@
47211
48527
  return null;
47212
48528
  }
47213
48529
  paintedAt(col, row, cols) {
47214
- if (!this.painted) {
47215
- return true;
48530
+ const at = (row * cols + col) * 4;
48531
+ if (this.painted && this.painted.length === this.source.data.length) {
48532
+ return this.painted[at + 3] > 0;
47216
48533
  }
47217
- const at = (row * cols + col) * 4 + 3;
47218
- return this.painted[at] > 0;
48534
+ if (this.paintedRamp) {
48535
+ const data = this.source.data;
48536
+ if (data[at + 3] === 0) {
48537
+ return false;
48538
+ }
48539
+ // The 8 bit grey the ramp is indexed on, which is what a tinted frame would have used.
48540
+ const grey = exports.TextureFrameSeriesAnimator.IsPackedValue(this.metadata)
48541
+ ? Math.round((data[at] * 256 + data[at + 1]) / 65535 * 255)
48542
+ : data[at];
48543
+ const steps = this.paintedRamp.length / 4;
48544
+ const index = Math.min(steps - 1, Math.round(grey / 255 * (steps - 1)));
48545
+ return this.paintedRamp[index * 4 + 3] > 0;
48546
+ }
48547
+ return true;
47219
48548
  }
47220
48549
  // Decodes one cell, skipping anything the texture marks as having no data.
47221
48550
  readAt(col, row, cols) {
@@ -47231,6 +48560,16 @@
47231
48560
  sourceMap: this.sourceMap
47232
48561
  });
47233
48562
  }
48563
+ /*
48564
+ * The texel holding a lon/lat, or null outside the raster.
48565
+ */
48566
+ texelAt(lon, lat) {
48567
+ const cols = this.source.width;
48568
+ const rows = this.source.height;
48569
+ const col = Math.floor((lon - this.extent.West) / (this.extent.East - this.extent.West) * cols);
48570
+ const row = Math.floor((this.extent.North - lat) / (this.extent.North - this.extent.South) * rows);
48571
+ return col < 0 || row < 0 || col >= cols || row >= rows ? null : { col, row };
48572
+ }
47234
48573
  lonAt(col, cols) {
47235
48574
  return this.extent.West + ((col + 0.5) / cols) * (this.extent.East - this.extent.West);
47236
48575
  }
@@ -47259,15 +48598,102 @@
47259
48598
  row1: Math.min(rows - 1, Math.ceil((this.extent.North - south) / spanLat * rows) + 1)
47260
48599
  };
47261
48600
  }
48601
+ // Metres the sheet sits at over a point, or the ellipsoid when nothing says otherwise.
48602
+ surfaceHeight(lon, lat) {
48603
+ const height = this.heightAt ? this.heightAt(lon, lat) : null;
48604
+ return height == null || !isFinite(height) ? 0 : height;
48605
+ }
48606
+ projectCell(col, row, cols, rows) {
48607
+ return this.project(this.lonAt(col, cols), this.latAt(row, rows));
48608
+ }
48609
+ // Projected at the sheet's own height, since up close the ellipsoid is metres from where it is drawn.
47262
48610
  project(lon, lat) {
47263
- const world = Cesium.Cartesian3.fromDegrees(lon, lat, 0);
48611
+ return this.projectInto(lon, lat) ? { x: this.scratchWindow.x, y: this.scratchWindow.y } : null;
48612
+ }
48613
+ /*
48614
+ * Projects into the shared scratch window position, returning whether it landed anywhere.
48615
+ */
48616
+ projectInto(lon, lat) {
48617
+ const world = Cesium.Cartesian3.fromDegrees(lon, lat, this.surfaceHeight(lon, lat), Cesium.Ellipsoid.WGS84, this.scratchWorld);
48618
+ // One matrix multiply in 3D, where the general path rederives the viewport and frustum per point.
48619
+ if (this.viewProjection) {
48620
+ const clip = Cesium.Matrix4.multiplyByVector(this.viewProjection, Cesium.Cartesian4.fromElements(world.x, world.y, world.z, 1, this.scratchClip), this.scratchClip);
48621
+ if (!(clip.w > 0)) {
48622
+ return null;
48623
+ }
48624
+ const view = this.viewer.canvas;
48625
+ this.scratchWindow.x = (clip.x / clip.w * 0.5 + 0.5) * view.clientWidth;
48626
+ this.scratchWindow.y = (0.5 - clip.y / clip.w * 0.5) * view.clientHeight;
48627
+ return this.scratchWindow;
48628
+ }
47264
48629
  const transforms = Cesium.SceneTransforms;
47265
48630
  // Renamed in newer Cesium, and this library runs on whatever the host app supplies.
47266
48631
  const fn = transforms.worldToWindowCoordinates || transforms.wgs84ToWindowCoordinates;
47267
- return fn ? fn.call(transforms, this.viewer.scene, world) : null;
48632
+ return fn ? fn.call(transforms, this.viewer.scene, world, this.scratchWindow) || null : null;
47268
48633
  }
47269
48634
  }
47270
48635
  TextureValueLabels.Labels = Labels;
48636
+ /*
48637
+ * A grid of window positions projected at most once each, and the cell edges traced through them.
48638
+ */
48639
+ class Lattice {
48640
+ constructor(wide, high, projectAt) {
48641
+ this.wide = wide;
48642
+ this.projectAt = projectAt;
48643
+ this.xs = new Float64Array(wide * high);
48644
+ this.ys = new Float64Array(wide * high);
48645
+ this.state = new Uint8Array(wide * high);
48646
+ }
48647
+ at(c, r) {
48648
+ const i = r * this.wide + c;
48649
+ if (this.state[i] === 0) {
48650
+ const p = this.projectAt(c, r);
48651
+ if (p) {
48652
+ this.xs[i] = p.x;
48653
+ this.ys[i] = p.y;
48654
+ this.state[i] = 1;
48655
+ }
48656
+ else {
48657
+ this.state[i] = 2;
48658
+ }
48659
+ }
48660
+ return this.state[i] === 1 ? i : -1;
48661
+ }
48662
+ edge(path, a, b) {
48663
+ if (a < 0 || b < 0) {
48664
+ return false;
48665
+ }
48666
+ path.moveTo(this.xs[a], this.ys[a]);
48667
+ path.lineTo(this.xs[b], this.ys[b]);
48668
+ return true;
48669
+ }
48670
+ /*
48671
+ * Adds each covered cell's edges to a path, a shared edge once, and says whether any were added.
48672
+ */
48673
+ Trace(path, covered, cellsWide, cellsHigh) {
48674
+ let drew = false;
48675
+ for (let r = 0; r < cellsHigh; r++) {
48676
+ for (let c = 0; c < cellsWide; c++) {
48677
+ if (!covered[r * cellsWide + c]) {
48678
+ continue;
48679
+ }
48680
+ const tl = this.at(c, r);
48681
+ const tr = this.at(c + 1, r);
48682
+ const bl = this.at(c, r + 1);
48683
+ if (!(r > 0 && covered[(r - 1) * cellsWide + c])) {
48684
+ drew = this.edge(path, tl, tr) || drew;
48685
+ }
48686
+ if (!(c > 0 && covered[r * cellsWide + c - 1])) {
48687
+ drew = this.edge(path, tl, bl) || drew;
48688
+ }
48689
+ const br = this.at(c + 1, r + 1);
48690
+ drew = this.edge(path, tr, br) || drew;
48691
+ drew = this.edge(path, bl, br) || drew;
48692
+ }
48693
+ }
48694
+ return drew;
48695
+ }
48696
+ }
47271
48697
  /*
47272
48698
  * The value line. Units go on it alone: repeating them on the range underneath doubles the widest
47273
48699
  * label to say nothing, and the two lines are plainly the same quantity.
@@ -47363,6 +48789,9 @@ in vec2 st;
47363
48789
  in float skirt;
47364
48790
 
47365
48791
  uniform sampler2D u_valueTexture;
48792
+ // The frame being faded away from, and how far the fade has got. The same texture at 1 when settled.
48793
+ uniform sampler2D u_valueTextureFrom;
48794
+ uniform float u_valueFade;
47366
48795
  uniform sampler2D u_groundTexture;
47367
48796
  uniform float u_valuePacked;
47368
48797
  uniform float u_valueMin;
@@ -47483,18 +48912,22 @@ float decodeValue(vec4 texel) {
47483
48912
  }
47484
48913
 
47485
48914
  void main() {
47486
- vec4 texel = texture(u_valueTexture, st);
48915
+ vec4 texelTo = texture(u_valueTexture, st);
48916
+ vec4 texelFrom = texture(u_valueTextureFrom, st);
47487
48917
  v_st = st;
48918
+ // Decoded before mixing, since blending a packed pair byte by byte reads as some other value
48919
+ // halfway through the fade.
48920
+ float raw = mix(decodeValue(texelFrom), decodeValue(texelTo), u_valueFade);
48921
+ float alpha = mix(texelFrom.a, texelTo.a, u_valueFade);
47488
48922
 
47489
48923
  // Inside a region the scene knows is water, a texel with no value still has to paint, or the
47490
48924
  // hole the region exists to close is still a hole.
47491
48925
  // The value it paints is the shallowest the ramp is not transparent at.
47492
48926
  float inMask = u_maskWeight * texture(u_maskTexture, st).a;
47493
- float hasData = step(u_coverageCutoffVertex, texel.a);
47494
- float raw = decodeValue(texel);
48927
+ float hasData = step(u_coverageCutoffVertex, alpha);
47495
48928
  float filled = mix(u_maskFillValue, max(raw, u_maskFillValue), hasData);
47496
48929
  v_value = mix(raw, filled, inMask * u_maskFill);
47497
- v_coverage = max(texel.a, inMask * u_maskFill);
48930
+ v_coverage = max(alpha, inMask * u_maskFill);
47498
48931
 
47499
48932
  // Exaggeration stretches the span above the archive's minimum rather than the height itself. On an
47500
48933
  // elevation the height is measured from the ellipsoid, tens of metres from any of the data, so
@@ -47537,6 +48970,8 @@ void main() {
47537
48970
  `;
47538
48971
  const FRAGMENT_SHADER_GLSL300 = `
47539
48972
  uniform sampler2D u_valueTexture;
48973
+ uniform sampler2D u_valueTextureFrom;
48974
+ uniform float u_valueFade;
47540
48975
  uniform sampler2D u_rampTexture;
47541
48976
  uniform float u_coverageCutoff;
47542
48977
  uniform vec2 u_texelSize;
@@ -47573,12 +49008,15 @@ in vec2 v_waveMetres;
47573
49008
  * Scaled per channel for the same reason the vertex shader's decode is: reassembling the pair reaches
47574
49009
  * 65535, which mediump does not have to represent, and this is the shader that may not get highp.
47575
49010
  */
47576
- float valueAt(vec2 uv) {
47577
- vec4 texel = texture(u_valueTexture, uv);
49011
+ float decodeValue(vec4 texel) {
47578
49012
  float packed16 = texel.r * (255.0 * 256.0 / 65535.0) + texel.g * (255.0 / 65535.0);
47579
49013
  return mix(texel.r, packed16, u_valuePacked);
47580
49014
  }
47581
49015
 
49016
+ float valueAt(vec2 uv) {
49017
+ return mix(decodeValue(texture(u_valueTextureFrom, uv)), decodeValue(texture(u_valueTexture, uv)), u_valueFade);
49018
+ }
49019
+
47582
49020
  //#water
47583
49021
  /*
47584
49022
  * Slope of the ripple field at a point, in metres of rise per metre travelled.
@@ -48025,6 +49463,13 @@ void main() {
48025
49463
  this.rampTexture = null;
48026
49464
  this.texture = null;
48027
49465
  this.textureDirty = true;
49466
+ // Fed from a fading source: the frame being left, and which pixels each texture holds.
49467
+ this.fade = null;
49468
+ this.textureFrom = null;
49469
+ this.uploadedTo = null;
49470
+ this.uploadedFrom = null;
49471
+ // Size of whichever value raster is bound, canvas or pixels.
49472
+ this.sourceDims = null;
48028
49473
  this.shaderProgram = null;
48029
49474
  this.shaderProgramWater = false;
48030
49475
  this.renderState = null;
@@ -48195,6 +49640,65 @@ void main() {
48195
49640
  this.maskGroundTexture = null;
48196
49641
  }
48197
49642
  }
49643
+ /**
49644
+ * Metres above the ellipsoid the sheet sits at over a point, or null unless a fading source is bound.
49645
+ */
49646
+ HeightAt(lon, lat) {
49647
+ const pixels = this.fade ? this.fade.to : null;
49648
+ const dims = this.sourceDims;
49649
+ if (!pixels || !dims) {
49650
+ return null;
49651
+ }
49652
+ const fx = (lon - this.extent.West) / (this.extent.East - this.extent.West);
49653
+ const fy = (this.extent.North - lat) / (this.extent.North - this.extent.South);
49654
+ if (!(fx >= 0 && fx <= 1 && fy >= 0 && fy <= 1)) {
49655
+ return null;
49656
+ }
49657
+ const col = Math.min(dims.width - 1, Math.floor(fx * dims.width));
49658
+ const row = Math.min(dims.height - 1, Math.floor(fy * dims.height));
49659
+ const at = (row * dims.width + col) * 4;
49660
+ const coverage = pixels[at + 3] / 255;
49661
+ const normalised = this.packedValue
49662
+ ? (pixels[at] * 256 + pixels[at + 1]) / 65535
49663
+ : pixels[at] / 255;
49664
+ const metres = this.valueMin + normalised * (this.valueMax - this.valueMin) * this.exaggeration;
49665
+ const following = this.followGround && Boolean(this.groundPixels);
49666
+ const ground = following ? this.groundAt(fx, fy) : 0;
49667
+ return this.baseHeight + ground + metres * (following ? coverage : 1);
49668
+ }
49669
+ /**
49670
+ * Changes whenever HeightAt would answer differently for the same frame.
49671
+ */
49672
+ GetPlacementKey() {
49673
+ return [this.groundPixels ? 1 : 0, this.baseHeight, this.exaggeration, this.followGround ? 1 : 0].join("|");
49674
+ }
49675
+ /**
49676
+ * The ramp the sheet is painted with, as 256 RGBA steps across the value range.
49677
+ */
49678
+ GetRampPixels() {
49679
+ return this.rampPixels;
49680
+ }
49681
+ /*
49682
+ * The packed ground raster read the way sampleGround reads it, bilinear between texel centres.
49683
+ */
49684
+ groundAt(u, v) {
49685
+ const size = GROUND_SAMPLES_PER_SIDE;
49686
+ const pixels = this.groundPixels;
49687
+ const read = (x, y) => {
49688
+ const i = (Math.min(size - 1, Math.max(0, y)) * size + Math.min(size - 1, Math.max(0, x))) * 4;
49689
+ const packed = (pixels[i] << 16) + (pixels[i + 1] << 8) + pixels[i + 2];
49690
+ return GROUND_MIN_METRES + (packed / 16777215) * (GROUND_MAX_METRES - GROUND_MIN_METRES);
49691
+ };
49692
+ const x = u * size - 0.5;
49693
+ const y = v * size - 0.5;
49694
+ const x0 = Math.floor(x);
49695
+ const y0 = Math.floor(y);
49696
+ const fx = x - x0;
49697
+ const fy = y - y0;
49698
+ const top = read(x0, y0) + (read(x0 + 1, y0) - read(x0, y0)) * fx;
49699
+ const bottom = read(x0, y0 + 1) + (read(x0 + 1, y0 + 1) - read(x0, y0 + 1)) * fx;
49700
+ return top + (bottom - top) * fy;
49701
+ }
48198
49702
  /**
48199
49703
  * Feeds the surface from an animated frame series.
48200
49704
  * Height and colour advance with the clock without the caller having to push each frame in.
@@ -48213,23 +49717,39 @@ void main() {
48213
49717
  return;
48214
49718
  }
48215
49719
  const context = frameState.context;
48216
- if (this.valueSource) {
48217
- const canvas = this.valueSource.GetValueCanvas();
48218
- if (!canvas) {
48219
- // No frame decoded yet, so there is nothing to displace against.
48220
- return;
48221
- }
48222
- const version = this.valueSource.GetPresentVersion();
48223
- if (canvas !== this.source || version !== this.lastPresentVersion) {
48224
- this.source = canvas;
48225
- this.lastPresentVersion = version;
48226
- this.textureDirty = true;
49720
+ const fade = this.valueSource && this.valueSource.GetValueFade
49721
+ ? this.valueSource.GetValueFade()
49722
+ : null;
49723
+ if (fade) {
49724
+ this.fade = fade;
49725
+ this.sourceDims = { width: fade.width, height: fade.height };
49726
+ }
49727
+ else {
49728
+ this.fade = null;
49729
+ if (this.valueSource) {
49730
+ const canvas = this.valueSource.GetValueCanvas();
49731
+ if (!canvas) {
49732
+ // No frame decoded yet, so there is nothing to displace against.
49733
+ return;
49734
+ }
49735
+ const version = this.valueSource.GetPresentVersion();
49736
+ if (canvas !== this.source || version !== this.lastPresentVersion) {
49737
+ this.source = canvas;
49738
+ this.lastPresentVersion = version;
49739
+ this.textureDirty = true;
49740
+ }
48227
49741
  }
49742
+ this.sourceDims = this.source ? { width: this.source.width, height: this.source.height } : null;
48228
49743
  }
48229
- if (!this.source || !this.source.width || !this.source.height) {
49744
+ if (!this.sourceDims || !this.sourceDims.width || !this.sourceDims.height) {
48230
49745
  return;
48231
49746
  }
48232
- this.syncTexture(context);
49747
+ if (this.fade) {
49748
+ this.syncFadeTextures(context);
49749
+ }
49750
+ else {
49751
+ this.syncTexture(context);
49752
+ }
48233
49753
  this.syncRampTexture(context);
48234
49754
  this.requestGround();
48235
49755
  this.syncGroundTexture(context);
@@ -48277,6 +49797,10 @@ void main() {
48277
49797
  this.texture.destroy();
48278
49798
  this.texture = null;
48279
49799
  }
49800
+ if (this.textureFrom) {
49801
+ this.textureFrom.destroy();
49802
+ this.textureFrom = null;
49803
+ }
48280
49804
  if (this.rampTexture) {
48281
49805
  this.rampTexture.destroy();
48282
49806
  this.rampTexture = null;
@@ -48544,7 +50068,7 @@ void main() {
48544
50068
  const southWest = Cesium.Cartesian3.fromDegrees(this.extent.West, this.extent.South, 0);
48545
50069
  const southEast = Cesium.Cartesian3.fromDegrees(this.extent.East, this.extent.South, 0);
48546
50070
  const width = Cesium.Cartesian3.distance(southWest, southEast);
48547
- return Math.max(1, width / Math.max(1, this.source ? this.source.width : 1));
50071
+ return Math.max(1, width / Math.max(1, this.sourceDims ? this.sourceDims.width : 1));
48548
50072
  }
48549
50073
  syncRampTexture(context) {
48550
50074
  if (this.rampTexture) {
@@ -48565,6 +50089,67 @@ void main() {
48565
50089
  })
48566
50090
  });
48567
50091
  }
50092
+ valueSampler() {
50093
+ // Nearest for a packed value, because filtering would blend the two bytes of the
50094
+ // packing rather than the values they encode, the same trap sampleGround avoids.
50095
+ const minification = this.packedValue
50096
+ ? Cesium.TextureMinificationFilter.NEAREST
50097
+ : Cesium.TextureMinificationFilter.LINEAR;
50098
+ const magnification = this.packedValue
50099
+ ? Cesium.TextureMagnificationFilter.NEAREST
50100
+ : Cesium.TextureMagnificationFilter.LINEAR;
50101
+ return new Cesium.Sampler({
50102
+ wrapS: Cesium.TextureWrap.CLAMP_TO_EDGE,
50103
+ wrapT: Cesium.TextureWrap.CLAMP_TO_EDGE,
50104
+ minificationFilter: minification,
50105
+ magnificationFilter: magnification
50106
+ });
50107
+ }
50108
+ /*
50109
+ * Uploads value pixels into a texture, reusing it when the size still fits.
50110
+ */
50111
+ uploadPixels(context, texture, pixels, width, height) {
50112
+ const view = new Uint8Array(pixels.buffer, pixels.byteOffset, pixels.byteLength);
50113
+ const source = { width, height, arrayBufferView: view };
50114
+ if (texture && texture.width === width && texture.height === height) {
50115
+ texture.copyFrom({ source });
50116
+ return texture;
50117
+ }
50118
+ if (texture) {
50119
+ texture.destroy();
50120
+ }
50121
+ return new Cesium.Texture({
50122
+ context,
50123
+ width,
50124
+ height,
50125
+ pixelFormat: Cesium.PixelFormat.RGBA,
50126
+ pixelDatatype: Cesium.PixelDatatype.UNSIGNED_BYTE,
50127
+ source,
50128
+ // Row 0 is the archive's north edge, and has to stay at the top, as the canvas path does.
50129
+ flipY: false,
50130
+ sampler: this.valueSampler()
50131
+ });
50132
+ }
50133
+ /*
50134
+ * Keeps a texture each side of the fade, swapping them on a new frame so only that one is uploaded.
50135
+ */
50136
+ syncFadeTextures(context) {
50137
+ const fade = this.fade;
50138
+ if (fade.to !== this.uploadedTo) {
50139
+ if (fade.from && fade.from === this.uploadedTo && this.texture) {
50140
+ const previous = this.textureFrom;
50141
+ this.textureFrom = this.texture;
50142
+ this.uploadedFrom = this.uploadedTo;
50143
+ this.texture = previous;
50144
+ }
50145
+ this.texture = this.uploadPixels(context, this.texture, fade.to, fade.width, fade.height);
50146
+ this.uploadedTo = fade.to;
50147
+ }
50148
+ if (fade.from && fade.from !== this.uploadedFrom) {
50149
+ this.textureFrom = this.uploadPixels(context, this.textureFrom, fade.from, fade.width, fade.height);
50150
+ this.uploadedFrom = fade.from;
50151
+ }
50152
+ }
48568
50153
  syncTexture(context) {
48569
50154
  const sizeChanged = this.texture
48570
50155
  && (this.texture.width !== this.source.width || this.texture.height !== this.source.height);
@@ -48572,27 +50157,15 @@ void main() {
48572
50157
  if (this.texture) {
48573
50158
  this.texture.destroy();
48574
50159
  }
48575
- // Nearest for a packed value, because filtering would blend the two bytes of the
48576
- // packing rather than the values they encode, the same trap sampleGround avoids.
48577
- const minification = this.packedValue
48578
- ? Cesium.TextureMinificationFilter.NEAREST
48579
- : Cesium.TextureMinificationFilter.LINEAR;
48580
- const magnification = this.packedValue
48581
- ? Cesium.TextureMagnificationFilter.NEAREST
48582
- : Cesium.TextureMagnificationFilter.LINEAR;
48583
50160
  this.texture = new Cesium.Texture({
48584
50161
  context,
48585
50162
  source: this.source,
48586
50163
  // Cesium flips canvas rows on upload by default, which would put the archive's
48587
50164
  // row 0 (its north edge) at the south edge and mirror the whole surface.
48588
50165
  flipY: false,
48589
- sampler: new Cesium.Sampler({
48590
- wrapS: Cesium.TextureWrap.CLAMP_TO_EDGE,
48591
- wrapT: Cesium.TextureWrap.CLAMP_TO_EDGE,
48592
- minificationFilter: minification,
48593
- magnificationFilter: magnification
48594
- })
50166
+ sampler: this.valueSampler()
48595
50167
  });
50168
+ this.uploadedTo = null;
48596
50169
  this.textureDirty = false;
48597
50170
  return;
48598
50171
  }
@@ -48655,7 +50228,7 @@ void main() {
48655
50228
  * carry, so the ladder is capped one step above the texel count rather than run to its top.
48656
50229
  */
48657
50230
  tierCeiling() {
48658
- const source = this.source;
50231
+ const source = this.sourceDims;
48659
50232
  if (!source || !source.width || !source.height) {
48660
50233
  return TIERS[TIERS.length - 1];
48661
50234
  }
@@ -48733,6 +50306,10 @@ void main() {
48733
50306
  owner: this,
48734
50307
  uniformMap: {
48735
50308
  u_valueTexture: () => self.texture,
50309
+ u_valueTextureFrom: () => (self.fade && self.fade.from && self.textureFrom
50310
+ ? self.textureFrom
50311
+ : self.texture),
50312
+ u_valueFade: () => (self.fade && self.fade.from ? self.fade.mix : 1),
48736
50313
  u_groundTexture: () => self.groundTexture,
48737
50314
  u_valuePacked: () => (self.packedValue ? 1 : 0),
48738
50315
  u_groundWeight: () => (self.followGround && self.groundPixels ? 1 : 0),
@@ -48770,7 +50347,7 @@ void main() {
48770
50347
  : 0,
48771
50348
  u_coverageCutoff: () => COVERAGE_CUTOFF,
48772
50349
  u_rampTexture: () => self.rampTexture,
48773
- u_texelSize: () => new Cesium.Cartesian2(1 / self.source.width, 1 / self.source.height),
50350
+ u_texelSize: () => new Cesium.Cartesian2(1 / self.sourceDims.width, 1 / self.sourceDims.height),
48774
50351
  u_waterOn: () => (self.water ? 1 : 0),
48775
50352
  u_waterWavelength: () => (self.water && self.water.wavelength)
48776
50353
  || DEFAULT_WATER_WAVELENGTH_METRES,
@@ -49141,6 +50718,10 @@ void main() {
49141
50718
  syncTextureValueLabels({
49142
50719
  cEntity,
49143
50720
  viewer: params.viewer,
50721
+ surface: drawsDisplacedSurface ? cEntity[DISPLACED_SURFACE_KEY] : null,
50722
+ hover: params.visualRegister
50723
+ ? { register: params.visualRegister, entityId: String(entity.Bruce.ID), menuItemId: params.menuItemId }
50724
+ : undefined,
49144
50725
  settings: overlaySettings(style, entity, params.tags),
49145
50726
  extent: labelArchive
49146
50727
  ? (drawsDisplacedSurface ? archiveExtent(labelArchive.metadata, posses) : drapeExtent)
@@ -49447,6 +51028,7 @@ void main() {
49447
51028
  // rendered: null
49448
51029
  rendered: (_c = params.rendered) === null || _c === void 0 ? void 0 : _c.get(entity.Bruce.ID),
49449
51030
  menuItemId: params.menuItemId,
51031
+ visualRegister: params.visualRegister,
49450
51032
  onSeriesDiscovered: params.onSeriesDiscovered
49451
51033
  });
49452
51034
  }
@@ -49884,8 +51466,8 @@ void main() {
49884
51466
  */
49885
51467
  const LABEL_GRID_COLOR = "rgba(255,255,255,0.55)";
49886
51468
  const LABEL_GRID_WIDTH_PIXELS = 1;
49887
- const LABEL_MIN_SPACING_PIXELS = 78;
49888
- const LABEL_MAX_LABELS = 240;
51469
+ const LABEL_MIN_SPACING_PIXELS = 140;
51470
+ const LABEL_MAX_LABELS = 60;
49889
51471
  function overlaySettings(style, entity, tags) {
49890
51472
  if (!wantsLabels(style)) {
49891
51473
  return undefined;
@@ -49905,6 +51487,7 @@ void main() {
49905
51487
  */
49906
51488
  function syncTextureValueLabels(params) {
49907
51489
  const { cEntity, viewer, settings, extent, animator, metadata } = params;
51490
+ const surface = params.surface && !params.surface.isDestroyed() ? params.surface : null;
49908
51491
  const existing = cEntity[TEXTURE_VALUE_LABELS_KEY];
49909
51492
  // Offline has no extent to rebuild the overlay from, so an existing one is left running.
49910
51493
  if (params.offline && existing) {
@@ -49923,8 +51506,13 @@ void main() {
49923
51506
  West: tile.West, East: tile.East, South: tile.South, North: tile.North,
49924
51507
  ResolutionX: tile.ResolutionX, ResolutionY: tile.ResolutionY
49925
51508
  })),
49926
- clipRing: params.clipRing
51509
+ clipRing: params.clipRing,
51510
+ heightAt: surface ? (lon, lat) => surface.HeightAt(lon, lat) : undefined,
51511
+ heightKey: surface ? () => surface.GetPlacementKey() : undefined,
51512
+ hover: params.hover
49927
51513
  });
51514
+ // A surface keeps no tinted frame, so whether a cell was painted is read off its ramp.
51515
+ labels.SetPaintedRamp(surface ? surface.GetRampPixels() : null);
49928
51516
  let lastVersion = -1;
49929
51517
  let appliedFloor = null;
49930
51518
  let appliedCeiling = null;
@@ -49936,15 +51524,15 @@ void main() {
49936
51524
  if (animator.IsDisposed()) {
49937
51525
  return;
49938
51526
  }
49939
- const canvas = animator.GetValueCanvas();
49940
- if (!canvas) {
49941
- return;
49942
- }
49943
51527
  const version = animator.GetPresentVersion();
49944
51528
  if (version !== lastVersion) {
51529
+ // Read from the frame cache, since reading the value canvas back copies a whole frame.
51530
+ const raster = animator.GetValueRaster();
51531
+ if (!raster) {
51532
+ return;
51533
+ }
49945
51534
  lastVersion = version;
49946
- const ctx = canvas.getContext("2d");
49947
- labels.SetSource(ctx.getImageData(0, 0, canvas.width, canvas.height));
51535
+ labels.SetSource(raster);
49948
51536
  labels.SetPainted(animator.GetDisplayedPixels());
49949
51537
  }
49950
51538
  // Tracked by reference rather than latched on the first one to arrive: a tiled archive
@@ -51668,7 +53256,7 @@ void main() {
51668
53256
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
51669
53257
  })(exports.StyleUtils || (exports.StyleUtils = {}));
51670
53258
 
51671
- const VERSION = "7.5.7";
53259
+ const VERSION = "7.5.9";
51672
53260
  /**
51673
53261
  * Updates the environment instance used by bruce-cesium to one specified.
51674
53262
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.