bruce-cesium 7.5.6 → 7.5.8

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.
@@ -1,6 +1,6 @@
1
1
  import * as Cesium from 'cesium';
2
- import { Cartographic, ColorMaterialProperty, Entity, Color, ConstantProperty, CallbackProperty, Primitive, Cesium3DTileFeature, HeightReference, ColorBlendMode, HeadingPitchRoll, Math as Math$1, Transforms, ShadowMode, Cartesian3, ClassificationType, Model, ConstantPositionProperty, DistanceDisplayCondition, JulianDate, Quaternion, Matrix4, HorizontalOrigin, VerticalOrigin, ArcType, CornerType, ImageMaterialProperty, PolygonHierarchy, PolylineGraphics, Rectangle, ShaderSource, Ellipsoid, Geometry, GeometryAttribute, ComponentDatatype, PrimitiveType, BoundingSphere, GeometryPipeline, Cartesian2, Cartesian4, sampleTerrain, Texture, PixelFormat, Sampler, TextureWrap, TextureMinificationFilter, TextureMagnificationFilter, VertexArray, BufferUsage, ShaderProgram, RenderState, BlendingState, DrawCommand, Pass, SceneTransforms, FeatureDetection, SceneMode, NearFarScalar, Matrix3, KmlDataSource, GeoJsonDataSource, Cesium3DTileStyle, HeadingPitchRange, Cesium3DTileColorBlendMode, Ion, IonResource, EllipsoidTerrainProvider, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, Cesium3DTileset, Intersect, OrthographicFrustum, EasingFunction, CustomDataSource, PolygonPipeline, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, EllipsoidGeodesic, sampleTerrainMostDetailed, defined, CesiumInspector, ClockRange, DynamicEnvironmentMapManager, GeometryInstance, ScreenSpaceEventHandler, ScreenSpaceEventType, Fullscreen, CzmlDataSource } from 'cesium';
3
- import { Cartes, LRUCache, ProjectViewTile, ENVIRONMENT, Calculator, ClientFile, Entity as Entity$1, EntityLod, EntityTag, EntityType, ObjectUtils, Style, Api, Carto, Geometry as Geometry$1, Bounds, ClientFileValueMap, Color as Color$1, MenuItem, ProjectView, ProjectViewBookmark, Tileset, ZoomControl, BruceEvent, EntityCoords, DataLab, DelayQueue, AccountConcept, EntityRelation, RecordChangeFeed, BruceApi, EntitySource, ProgramKey, ProjectViewLegacyTile, Camera, Account, AccountLimits, Session, UrlUtils, ProjectViewBookmarkGroup, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, EntityRelationType, EntityHistoricData } from 'bruce-models';
2
+ import { Cartographic, ColorMaterialProperty, Entity, Color, ConstantProperty, CallbackProperty, Primitive, Cesium3DTileFeature, ShaderSource, Cartesian3, Math as Math$1, Ellipsoid, Transforms, Matrix4, Geometry, GeometryAttribute, ComponentDatatype, PrimitiveType, BoundingSphere, GeometryPipeline, Cartesian2, Cartesian4, sampleTerrain, Texture, PixelFormat, Sampler, TextureWrap, TextureMinificationFilter, TextureMagnificationFilter, VertexArray, BufferUsage, ShaderProgram, RenderState, BlendingState, DrawCommand, Pass, ConstantPositionProperty, HeightReference, DistanceDisplayCondition, JulianDate, Quaternion, HeadingPitchRoll, ColorBlendMode, ShadowMode, ClassificationType, Model, ArcType, CornerType, ImageMaterialProperty, PolygonHierarchy, PolylineGraphics, Rectangle, HorizontalOrigin, VerticalOrigin, SceneTransforms, FeatureDetection, SceneMode, Matrix3, NearFarScalar, KmlDataSource, GeoJsonDataSource, Cesium3DTileStyle, HeadingPitchRange, Cesium3DTileColorBlendMode, Ion, IonResource, Cesium3DTileset, OrthographicFrustum, EasingFunction, Intersect, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, EllipsoidGeodesic, EllipsoidTerrainProvider, sampleTerrainMostDetailed, defined, GeometryInstance, PolygonPipeline, CustomDataSource, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, CesiumInspector, ClockRange, DynamicEnvironmentMapManager, ScreenSpaceEventHandler, ScreenSpaceEventType, Fullscreen, CzmlDataSource } from 'cesium';
3
+ import { Cartes, LRUCache, ProjectViewTile, ENVIRONMENT, Calculator, ClientFile, Entity as Entity$1, EntityLod, EntityTag, EntityType, ObjectUtils, Style, UrlUtils, Carto, Geometry as Geometry$1, ClientFileValueMap, Color as Color$1, Bounds, Api, MenuItem, ProjectView, ProjectViewBookmark, Tileset, ZoomControl, BruceEvent, EntityCoords, DataLab, DelayQueue, AccountConcept, RecordChangeFeed, BruceApi, EntityRelation, Camera, ProgramKey, EntitySource, ProjectViewLegacyTile, Account, AccountLimits, Session, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, ProjectViewBookmarkGroup, EntityRelationType, EntityHistoricData } from 'bruce-models';
4
4
 
5
5
  /**
6
6
  * Ensures a number is returned from a given value.
@@ -5546,6 +5546,24 @@ var EntityLabel;
5546
5546
  const PHOTO_CACHE_LIMIT = 500;
5547
5547
  // Key where the viewer keeps its downsized photos.
5548
5548
  const PHOTO_CACHE_KEY = "_nextspaceLabelPhotos";
5549
+ // Clearance around a model's box, as a share of its half-extent on each axis.
5550
+ const MODEL_BOX_PADDING = 0.02;
5551
+ // Share of each edge drawn out from its corners while the box is idle. Hovering draws the edges whole.
5552
+ const MODEL_BOX_BRACKET = 0.2;
5553
+ // Under this many pixels across, a box reads as a smudge and the ring takes over.
5554
+ const MODEL_BOX_MIN_SIZE = 24;
5555
+ // Stem length above the box outline when the options do not set one.
5556
+ const MODEL_BOX_STEM = 32;
5557
+ // Gap in pixels a waiting box needs from a drawn one before it shows, so neighbours never trade places.
5558
+ const MODEL_BOX_CLEARANCE = 12;
5559
+ // Longest a model's label stays hidden waiting to learn whether it has a box, before showing its ring anyway.
5560
+ const MODEL_BOX_WAIT = 1500;
5561
+ // Half the length of edge drawn either side of where a stem lands, when brackets would leave it bare.
5562
+ const MODEL_BOX_LANDING = 10;
5563
+ // Past this many touching boxes one box around them all stands in for tracing their union.
5564
+ const MODEL_BOX_UNION_LIMIT = 24;
5565
+ // 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.
5566
+ 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]];
5549
5567
  // One per tinted icon, since an SVG mask is addressed by id across the whole document.
5550
5568
  let tintCounter = 0;
5551
5569
  // Key where the label instance is stored within the cesium entity.
@@ -5553,8 +5571,6 @@ var EntityLabel;
5553
5571
  EntityLabel.ATTACH_KEY = "nextspace-navigator-entity-label";
5554
5572
  // Image types a label draws, read from the file extension. Anything else stays text.
5555
5573
  const IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp"];
5556
- // Shortest side a thumbnail needs before it is preferred over the full file.
5557
- const IMAGE_THUMBNAIL_MIN = 64;
5558
5574
  // Size a found image is drawn at when the group names none, so a photo never lands at its natural size.
5559
5575
  const IMAGE_SIZE = 24;
5560
5576
  function isImageFile(name) {
@@ -5583,12 +5599,24 @@ var EntityLabel;
5583
5599
  return (url.protocol === "http:" || url.protocol === "https:") && isImageFile(url.pathname);
5584
5600
  }
5585
5601
  EntityLabel.IsImageUrl = IsImageUrl;
5602
+ /*
5603
+ * Device pixels across a photo needs to stay sharp at a chip size, at rest or grown on hover.
5604
+ */
5605
+ function photoPixels(size, hover) {
5606
+ // The glyph plus the chip's padding of a third either side, enlarged for a photo.
5607
+ const diameter = (size + 2 * (size / 3)) * PHOTO_SIZE_SCALE;
5608
+ // Picking an image can run with no window, where a screen pixel is a CSS pixel.
5609
+ const ratio = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1;
5610
+ return Math.ceil(diameter * (hover ? PHOTO_HOVER_SCALE : 1) * ratio);
5611
+ }
5586
5612
  /*
5587
5613
  * The entity's default image as InfoView picks it: its first photo attachment by display order that is an image file.
5588
- * Reads attachments expanded onto the entity, and prefers the smallest thumbnail that still covers an icon.
5614
+ *
5615
+ * Reads attachments expanded onto the entity. Prefers the smallest thumbnail sharp at the hover size, then the
5616
+ * largest still sharp at rest, so a low resolution thumbnail is never stretched while the full file is unread.
5589
5617
  */
5590
5618
  function GetDefaultImageUrl(entity, api) {
5591
- var _a, _b, _c;
5619
+ var _a, _b, _c, _d;
5592
5620
  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 : [];
5593
5621
  const photo = attachments
5594
5622
  .filter(x => (x === null || x === void 0 ? void 0 : x["EntityAttachmentType.ID"]) == EntityAttachmentType.EType.Photo && x.ClientFile)
@@ -5597,9 +5625,12 @@ var EntityLabel;
5597
5625
  if (!photo) {
5598
5626
  return null;
5599
5627
  }
5600
- const thumbnail = ((_c = photo.ClientFile.Thumbnails) !== null && _c !== void 0 ? _c : [])
5601
- .filter(x => Math.min(x.Width, x.Height) >= IMAGE_THUMBNAIL_MIN && (x.URL || x["ClientFile.ID"]))
5602
- .sort((a, b) => a.Width * a.Height - b.Width * b.Height)[0];
5628
+ // How far across a thumbnail is shown: a circle crops to its shortest side, a rectangle keeps its longest.
5629
+ const across = (x) => PhotoShape(x.Width, x.Height) === "circle" ? Math.min(x.Width, x.Height) : Math.max(x.Width, x.Height);
5630
+ const thumbnails = ((_c = photo.ClientFile.Thumbnails) !== null && _c !== void 0 ? _c : [])
5631
+ .filter(x => x.Width > 0 && x.Height > 0 && (x.URL || x["ClientFile.ID"]))
5632
+ .sort((a, b) => a.Width * a.Height - b.Width * b.Height);
5633
+ 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));
5603
5634
  if (thumbnail) {
5604
5635
  return thumbnail.URL || ClientFile.GetUrl({ fileId: thumbnail["ClientFile.ID"], api, viaCdn: true });
5605
5636
  }
@@ -6111,6 +6142,7 @@ var EntityLabel;
6111
6142
  // Participants whose failure has been logged, so a label that throws every frame logs once.
6112
6143
  this.failed = new WeakSet();
6113
6144
  this.placementMemory = new PlacementMemory();
6145
+ this.modelBoxes = new ModelBoxes();
6114
6146
  this.forwardingWheel = false;
6115
6147
  this.pendingDrag = null;
6116
6148
  // Null outside a 3D view.
@@ -6349,6 +6381,12 @@ var EntityLabel;
6349
6381
  x: (pointer.x - rect.left) * this.viewer.canvas.clientWidth / rect.width,
6350
6382
  y: (pointer.y - rect.top) * this.viewer.canvas.clientHeight / rect.height
6351
6383
  } : undefined;
6384
+ try {
6385
+ this.modelBoxes.Resolve(this.viewer, this.pointer);
6386
+ }
6387
+ catch (e) {
6388
+ console.error("EntityLabel: model boxes failed to resolve.", e);
6389
+ }
6352
6390
  // This runs inside Cesium's frame, where one throwing label would stop rendering for the whole scene.
6353
6391
  const projected = new Map();
6354
6392
  for (const participant of this.participants) {
@@ -6370,6 +6408,12 @@ var EntityLabel;
6370
6408
  this.logFailure(participant, e);
6371
6409
  }
6372
6410
  }
6411
+ try {
6412
+ this.modelBoxes.Draw(this.viewer, this.element, this.coordinates);
6413
+ }
6414
+ catch (e) {
6415
+ console.error("EntityLabel: model boxes failed to draw.", e);
6416
+ }
6373
6417
  }
6374
6418
  logFailure(participant, error) {
6375
6419
  if (this.failed.has(participant)) {
@@ -6380,6 +6424,728 @@ var EntityLabel;
6380
6424
  }
6381
6425
  }
6382
6426
  let nextLabelId = 0;
6427
+ /*
6428
+ * The record's oriented box in the same Z-up local space as GeometryPivot, or null when it has none.
6429
+ */
6430
+ function ReadOrientedBox(entity) {
6431
+ var _a, _b;
6432
+ const box = (_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.GeometryOrientedBox;
6433
+ const vector = (value) => (value === null || value === void 0 ? void 0 : value.length) === 3 && value.every((item) => Number.isFinite(+item)) ? value.map(Number) : null;
6434
+ const centre = vector(box === null || box === void 0 ? void 0 : box.Centre), half = vector(box === null || box === void 0 ? void 0 : box.HalfExtents);
6435
+ 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;
6436
+ return centre && half && (axes === null || axes === void 0 ? void 0 : axes.every(Boolean)) ? { centre, axes, half } : null;
6437
+ }
6438
+ EntityLabel.ReadOrientedBox = ReadOrientedBox;
6439
+ /*
6440
+ * The eight padded world corners of an assembly Entity's geometry box, or null when the record cannot give one.
6441
+ *
6442
+ * GeometryPivotExtents are Z-up half-extents about GeometryPivot, and the placement is the one the API uses to put
6443
+ * Bruce.Location at the pivot: ENU(root) * UCS translation * UCS rotation * AssemblyWorldPosition.
6444
+ */
6445
+ function GetModelBox(entity, shape) {
6446
+ var _a, _b, _c;
6447
+ const bruce = entity === null || entity === void 0 ? void 0 : entity.Bruce;
6448
+ 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);
6449
+ const matrix = bruce === null || bruce === void 0 ? void 0 : bruce.AssemblyWorldPosition;
6450
+ const root = bruce === null || bruce === void 0 ? void 0 : bruce.AssemblyRootLocation;
6451
+ if ((half === null || half === void 0 ? void 0 : half.length) !== 3 || !half.every(value => Number.isFinite(value) && value >= 0) || Math.max(...half) < 0.01) {
6452
+ return null;
6453
+ }
6454
+ 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))) {
6455
+ return null;
6456
+ }
6457
+ const pivot = ((_c = bruce.GeometryPivot) === null || _c === void 0 ? void 0 : _c.length) >= 3 ? bruce.GeometryPivot.slice(0, 3).map(Number) : [0, 0, 0];
6458
+ // Without an oriented box the record's own extents are an axis-aligned box about the pivot.
6459
+ const box = shape || { centre: pivot, axes: [[1, 0, 0], [0, 1, 0], [0, 0, 1]], half: half };
6460
+ const transform = bruce.AssemblyWorldTransform;
6461
+ const placement = Transforms.eastNorthUpToFixedFrame(Cartesian3.fromDegrees(+root.longitude, +root.latitude, EnsureNumber(root.altitude)));
6462
+ Matrix4.multiply(placement, Matrix4.fromTranslation(new 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);
6463
+ Matrix4.multiply(placement, Matrix4.fromRotation(Matrix3.fromHeadingPitchRoll(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);
6464
+ Matrix4.multiply(placement, new Matrix4(...matrix.flat().map(Number)), placement);
6465
+ // Pad thin axes by a share of the longest one, so a flat part still gets a visible gap.
6466
+ const longest = Math.max(...box.half);
6467
+ const reach = box.half.map(value => value + Math.max(value, longest * 0.25) * MODEL_BOX_PADDING);
6468
+ const corners = [];
6469
+ for (let i = 0; i < 8; i++) {
6470
+ const signs = [i & 1 ? 1 : -1, i & 2 ? 1 : -1, i & 4 ? 1 : -1];
6471
+ const local = new Cartesian3(...[0, 1, 2].map(k => box.centre[k] +
6472
+ signs[0] * reach[0] * box.axes[0][k] + signs[1] * reach[1] * box.axes[1][k] + signs[2] * reach[2] * box.axes[2][k]));
6473
+ const corner = Matrix4.multiplyByPoint(placement, local, new Cartesian3());
6474
+ if (![corner.x, corner.y, corner.z].every(Number.isFinite)) {
6475
+ return null;
6476
+ }
6477
+ corners.push(corner);
6478
+ }
6479
+ return corners;
6480
+ }
6481
+ EntityLabel.GetModelBox = GetModelBox;
6482
+ const vAdd = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
6483
+ const vSub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
6484
+ const vScale = (a, s) => [a[0] * s, a[1] * s, a[2] * s];
6485
+ const vDot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
6486
+ 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]];
6487
+ const vLength = (a) => Math.hypot(a[0], a[1], a[2]);
6488
+ /*
6489
+ * Reads a solid back from the eight corners GetModelBox gives, relative to the origin.
6490
+ */
6491
+ function ModelSolid(corners, origin) {
6492
+ const local = corners.map((corner) => [corner.x - origin.x, corner.y - origin.y, corner.z - origin.z]);
6493
+ const spans = [1, 2, 4].map(bit => vSub(local[bit], local[0]));
6494
+ const half = spans.map(span => vLength(span) / 2);
6495
+ return {
6496
+ centre: vScale(vAdd(local[0], local[7]), 0.5),
6497
+ axes: spans.map((span, k) => vScale(span, 0.5 / (half[k] || 1))),
6498
+ half
6499
+ };
6500
+ }
6501
+ EntityLabel.ModelSolid = ModelSolid;
6502
+ function solidCorners(solid) {
6503
+ const corners = [];
6504
+ for (let i = 0; i < 8; i++) {
6505
+ corners.push([0, 1, 2].reduce((point, k) => vAdd(point, vScale(solid.axes[k], solid.half[k] * (i & (1 << k) ? 1 : -1))), solid.centre));
6506
+ }
6507
+ return corners;
6508
+ }
6509
+ function solidContains(solid, point, inset = 0) {
6510
+ const offset = vSub(point, solid.centre);
6511
+ return solid.axes.every((axis, k) => Math.abs(vDot(offset, axis)) < solid.half[k] - inset);
6512
+ }
6513
+ /*
6514
+ * Whether two solids overlap, by the separating axis test. Padding makes parts that sit against each other overlap.
6515
+ */
6516
+ function ModelSolidsTouch(a, b) {
6517
+ const between = vSub(b.centre, a.centre);
6518
+ const axes = [...a.axes, ...b.axes];
6519
+ for (const first of a.axes) {
6520
+ for (const second of b.axes) {
6521
+ const axis = vCross(first, second);
6522
+ const length = vLength(axis);
6523
+ if (length > 1e-6)
6524
+ axes.push(vScale(axis, 1 / length));
6525
+ }
6526
+ }
6527
+ const reach = (solid, axis) => solid.axes.reduce((sum, own, k) => sum + solid.half[k] * Math.abs(vDot(own, axis)), 0);
6528
+ return axes.every(axis => Math.abs(vDot(between, axis)) <= reach(a, axis) + reach(b, axis));
6529
+ }
6530
+ EntityLabel.ModelSolidsTouch = ModelSolidsTouch;
6531
+ /*
6532
+ * One box around every solid, turned like the largest of them, for groups too big to trace.
6533
+ */
6534
+ function encloseSolids(solids) {
6535
+ 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);
6536
+ const corners = solids.flatMap(solidCorners);
6537
+ const ranges = largest.axes.map(axis => {
6538
+ const values = corners.map(corner => vDot(corner, axis));
6539
+ return [Math.min(...values), Math.max(...values)];
6540
+ });
6541
+ return {
6542
+ centre: largest.axes.reduce((point, axis, k) => vAdd(point, vScale(axis, (ranges[k][0] + ranges[k][1]) / 2)), [0, 0, 0]),
6543
+ axes: largest.axes,
6544
+ half: ranges.map(([low, high]) => (high - low) / 2)
6545
+ };
6546
+ }
6547
+ /*
6548
+ * Narrows [low, high] to where a + b * t stays within -half..half.
6549
+ */
6550
+ function clipSlab(range, a, b, half) {
6551
+ if (Math.abs(b) < 1e-12) {
6552
+ if (Math.abs(a) > half)
6553
+ range[1] = -Infinity;
6554
+ return;
6555
+ }
6556
+ const first = (-half - a) / b, second = (half - a) / b;
6557
+ range[0] = Math.max(range[0], Math.min(first, second));
6558
+ range[1] = Math.min(range[1], Math.max(first, second));
6559
+ }
6560
+ /*
6561
+ * The edges of the union of the solids: where its surface folds, leaving out seams across flat shared faces.
6562
+ */
6563
+ function ModelBoxOutline(solids) {
6564
+ const span = Math.max(...solids.map(solid => vLength(solid.half)));
6565
+ const thinnest = Math.min(...solids.flatMap(solid => solid.half));
6566
+ const radius = Math.max(1e-6, Math.min(thinnest * 0.05, span * 1e-3));
6567
+ const tolerance = Math.max(1, span) * 1e-6;
6568
+ const candidates = [];
6569
+ for (const solid of solids) {
6570
+ const corners = solidCorners(solid);
6571
+ for (const [a, b] of MODEL_BOX_EDGES)
6572
+ candidates.push([corners[a], corners[b]]);
6573
+ }
6574
+ const faces = (solid) => [0, 1, 2].flatMap(k => [1, -1].map(sign => ({ solid, k, normal: vScale(solid.axes[k], sign) })));
6575
+ for (let i = 0; i < solids.length; i++) {
6576
+ for (let j = i + 1; j < solids.length; j++) {
6577
+ if (!ModelSolidsTouch(solids[i], solids[j])) {
6578
+ continue;
6579
+ }
6580
+ for (const first of faces(solids[i])) {
6581
+ for (const second of faces(solids[j])) {
6582
+ const direction = vCross(first.normal, second.normal);
6583
+ const sine = vLength(direction);
6584
+ // Faces this close to parallel meet along edges the box edges already give.
6585
+ if (sine < 0.05) {
6586
+ continue;
6587
+ }
6588
+ const d1 = vDot(first.normal, first.solid.centre) + first.solid.half[first.k];
6589
+ const d2 = vDot(second.normal, second.solid.centre) + second.solid.half[second.k];
6590
+ const cosine = vDot(first.normal, second.normal);
6591
+ const base = vScale(vAdd(vScale(first.normal, d1 - d2 * cosine), vScale(second.normal, d2 - d1 * cosine)), 1 / (sine * sine));
6592
+ const unit = vScale(direction, 1 / sine);
6593
+ const range = [-Infinity, Infinity];
6594
+ for (const face of [first, second]) {
6595
+ const offset = vSub(base, face.solid.centre);
6596
+ for (let m = 0; m < 3; m++) {
6597
+ if (m !== face.k)
6598
+ clipSlab(range, vDot(offset, face.solid.axes[m]), vDot(unit, face.solid.axes[m]), face.solid.half[m]);
6599
+ }
6600
+ }
6601
+ if (range[1] - range[0] > tolerance) {
6602
+ candidates.push([vAdd(base, vScale(unit, range[0])), vAdd(base, vScale(unit, range[1]))]);
6603
+ }
6604
+ }
6605
+ }
6606
+ }
6607
+ }
6608
+ const inside = (point) => solids.some(solid => solidContains(solid, point));
6609
+ /*
6610
+ * Whether the union folds along a direction at a point, from which way round it the inside lies.
6611
+ */
6612
+ const folds = (point, along) => {
6613
+ const first = vCross(along, Math.abs(along[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0]);
6614
+ const e1 = vScale(first, 1 / vLength(first));
6615
+ const e2 = vCross(along, e1);
6616
+ const samples = [];
6617
+ for (let i = 0; i < 16; i++) {
6618
+ // Offset from the axes so no sample lands exactly on a face through the point.
6619
+ const angle = (i + 0.37) * Math.PI / 8;
6620
+ samples.push(inside(vAdd(point, vAdd(vScale(e1, Math.cos(angle) * radius), vScale(e2, Math.sin(angle) * radius)))));
6621
+ }
6622
+ const filled = samples.filter(Boolean).length;
6623
+ const changes = samples.filter((sample, i) => sample !== samples[(i + 1) % 16]).length;
6624
+ return filled > 0 && filled < 16 && !(filled === 8 && changes === 2);
6625
+ };
6626
+ const lines = [];
6627
+ for (const [start, end] of candidates) {
6628
+ const delta = vSub(end, start);
6629
+ const length = vLength(delta);
6630
+ if (length < tolerance) {
6631
+ continue;
6632
+ }
6633
+ const along = vScale(delta, 1 / length);
6634
+ const cuts = [0, 1];
6635
+ for (const solid of solids) {
6636
+ const offset = vSub(start, solid.centre);
6637
+ solid.axes.forEach((axis, k) => {
6638
+ const rate = vDot(delta, axis);
6639
+ if (Math.abs(rate) < 1e-12)
6640
+ return;
6641
+ for (const bound of [-solid.half[k], solid.half[k]]) {
6642
+ const t = (bound - vDot(offset, axis)) / rate;
6643
+ if (t > 0 && t < 1)
6644
+ cuts.push(t);
6645
+ }
6646
+ });
6647
+ }
6648
+ cuts.sort((a, b) => a - b);
6649
+ for (let i = 0; i + 1 < cuts.length; i++) {
6650
+ if ((cuts[i + 1] - cuts[i]) * length < tolerance || !folds(vAdd(start, vScale(delta, (cuts[i] + cuts[i + 1]) / 2)), along)) {
6651
+ continue;
6652
+ }
6653
+ // Merged by line, so a shared edge draws once and a run cut by other boxes is one stroke.
6654
+ const from = vAdd(start, vScale(delta, cuts[i])), to = vAdd(start, vScale(delta, cuts[i + 1]));
6655
+ let line = lines.find(item => vLength(vCross(item.direction, along)) < 1e-6 && vLength(vCross(vSub(from, item.origin), item.direction)) < tolerance);
6656
+ if (!line) {
6657
+ line = { origin: from, direction: along, spans: [] };
6658
+ lines.push(line);
6659
+ }
6660
+ const a = vDot(vSub(from, line.origin), line.direction), b = vDot(vSub(to, line.origin), line.direction);
6661
+ line.spans.push([Math.min(a, b), Math.max(a, b)]);
6662
+ }
6663
+ }
6664
+ const outline = [];
6665
+ for (const line of lines) {
6666
+ line.spans.sort((a, b) => a[0] - b[0]);
6667
+ let current = line.spans[0];
6668
+ for (const next of [...line.spans.slice(1), null]) {
6669
+ if (next && next[0] <= current[1] + tolerance) {
6670
+ current = [current[0], Math.max(current[1], next[1])];
6671
+ continue;
6672
+ }
6673
+ outline.push([vAdd(line.origin, vScale(line.direction, current[0])), vAdd(line.origin, vScale(line.direction, current[1]))]);
6674
+ current = next;
6675
+ }
6676
+ }
6677
+ return outline;
6678
+ }
6679
+ EntityLabel.ModelBoxOutline = ModelBoxOutline;
6680
+ /*
6681
+ * Whether the sight line from the eye to a point passes through any solid before reaching it.
6682
+ */
6683
+ function solidsHide(solids, eye, point, inset) {
6684
+ const ray = vSub(point, eye);
6685
+ const length = vLength(ray);
6686
+ // Stop just short of the point so the face it lies on does not count against it.
6687
+ const reach = Math.max(0, 1 - inset * 100 / (length || 1));
6688
+ return solids.some(solid => {
6689
+ const offset = vSub(eye, solid.centre);
6690
+ const range = [0, reach];
6691
+ solid.axes.forEach((axis, k) => clipSlab(range, vDot(offset, axis), vDot(ray, axis), solid.half[k] - inset));
6692
+ return range[0] < range[1];
6693
+ });
6694
+ }
6695
+ /*
6696
+ * Window positions of a box's corners, or null when one is behind the camera and the box would draw inside out.
6697
+ */
6698
+ function projectCorners(viewer, corners) {
6699
+ const camera = viewer.camera;
6700
+ const cameraPosition = camera.positionWC || camera.position;
6701
+ const direction = camera.directionWC || camera.direction;
6702
+ const transforms = SceneTransforms;
6703
+ const projectPosition = transforms.wgs84ToWindowCoordinates || transforms.worldToWindowCoordinates;
6704
+ const points = [];
6705
+ const relative = new Cartesian3();
6706
+ for (const corner of corners) {
6707
+ if (Cartesian3.dot(Cartesian3.subtract(corner, cameraPosition, relative), direction) <= 0) {
6708
+ return null;
6709
+ }
6710
+ const point = projectPosition(viewer.scene, corner);
6711
+ if (!Number.isFinite(point === null || point === void 0 ? void 0 : point.x) || !Number.isFinite(point === null || point === void 0 ? void 0 : point.y)) {
6712
+ return null;
6713
+ }
6714
+ points.push({ x: point.x, y: point.y });
6715
+ }
6716
+ return points;
6717
+ }
6718
+ /*
6719
+ * Whether two convex outlines come within a margin of each other, by the separating axis test.
6720
+ */
6721
+ function HullsNear(a, b, margin) {
6722
+ for (const hull of [a, b]) {
6723
+ for (let i = 0; i < hull.length; i++) {
6724
+ const from = hull[i], to = hull[(i + 1) % hull.length];
6725
+ const length = Math.hypot(to.x - from.x, to.y - from.y);
6726
+ if (length < 1e-9) {
6727
+ continue;
6728
+ }
6729
+ const normal = { x: (from.y - to.y) / length, y: (to.x - from.x) / length };
6730
+ const project = (points) => points.map(point => point.x * normal.x + point.y * normal.y);
6731
+ const first = project(a), second = project(b);
6732
+ if (Math.max(Math.min(...second) - Math.max(...first), Math.min(...first) - Math.max(...second)) >= margin) {
6733
+ return false;
6734
+ }
6735
+ }
6736
+ }
6737
+ return true;
6738
+ }
6739
+ EntityLabel.HullsNear = HullsNear;
6740
+ /*
6741
+ * Decides which labels' model boxes are drawn and how, so a multi-selection does not stack box over box.
6742
+ */
6743
+ class ModelBoxes {
6744
+ constructor() {
6745
+ // Whether a box competes for space comes only from inputs (camera distance, hover, the pointer), never from
6746
+ // how its label was laid out, so an animation moving a label cannot feed back into which boxes show.
6747
+ this.entries = new Map();
6748
+ this.groups = null;
6749
+ // The hovered Entity. Its box is drawn on its own and whole, ahead of every other box.
6750
+ this.hovered = null;
6751
+ this.outlines = new Map();
6752
+ this.hulls = new Map();
6753
+ this.frames = new Map();
6754
+ this.drawn = [];
6755
+ // When each label's box started showing; the longer it has shown, the stronger its claim on the space.
6756
+ this.since = new Map();
6757
+ this.requests = new Map();
6758
+ this.looks = new Map();
6759
+ this.memberLooks = new Map();
6760
+ }
6761
+ Set(id, corners, entityId, inputs) {
6762
+ const current = this.entries.get(id);
6763
+ // A label rebuilt over the same box keeps its place and how far it has animated.
6764
+ if (corners && (current === null || current === void 0 ? void 0 : current.corners) === corners && current.entityId === entityId) {
6765
+ Object.assign(current, inputs);
6766
+ return;
6767
+ }
6768
+ if (!corners && !current) {
6769
+ return;
6770
+ }
6771
+ if (corners) {
6772
+ this.entries.set(id, { ...inputs, corners, entityId, active: false, wanted: false });
6773
+ }
6774
+ else
6775
+ this.entries.delete(id);
6776
+ this.since.delete(id);
6777
+ this.groups = null;
6778
+ for (const [key, group] of this.outlines) {
6779
+ if (group.members.includes(id))
6780
+ this.outlines.delete(key);
6781
+ }
6782
+ }
6783
+ /*
6784
+ * The box drawn for a label this frame, or undefined when it has none or lost its place.
6785
+ */
6786
+ Frame(id) {
6787
+ return this.frames.get(id);
6788
+ }
6789
+ /*
6790
+ * The label's own box on screen whether or not it is drawn, so hovering the model can ask for it.
6791
+ */
6792
+ Hull(id) {
6793
+ return this.hulls.get(id);
6794
+ }
6795
+ Request(id, request) {
6796
+ this.requests.set(id, request);
6797
+ }
6798
+ group() {
6799
+ const ids = Array.from(this.entries.keys()).filter(id => this.entries.get(id).wanted).sort();
6800
+ if (!ids.length) {
6801
+ return [];
6802
+ }
6803
+ const origin = this.entries.get(ids[0]).corners[0];
6804
+ const solids = new Map(ids.map(id => [id, ModelSolid(this.entries.get(id).corners, origin)]));
6805
+ const parent = new Map(ids.map(id => [id, id]));
6806
+ const root = (id) => parent.get(id) === id ? id : root(parent.get(id));
6807
+ for (let i = 0; i < ids.length; i++) {
6808
+ const a = solids.get(ids[i]);
6809
+ for (let j = i + 1; j < ids.length; j++) {
6810
+ const b = solids.get(ids[j]);
6811
+ if ((this.entries.get(ids[i]).entityId === this.hovered) !== (this.entries.get(ids[j]).entityId === this.hovered)) {
6812
+ continue;
6813
+ }
6814
+ // Cheap sphere check first; most pairs in a scene are nowhere near each other.
6815
+ if (vLength(vSub(a.centre, b.centre)) > vLength(a.half) + vLength(b.half) || !ModelSolidsTouch(a, b)) {
6816
+ continue;
6817
+ }
6818
+ parent.set(root(ids[j]), root(ids[i]));
6819
+ }
6820
+ }
6821
+ const members = new Map();
6822
+ for (const id of ids) {
6823
+ const key = root(id);
6824
+ if (!members.has(key))
6825
+ members.set(key, []);
6826
+ members.get(key).push(id);
6827
+ }
6828
+ const groups = Array.from(members.values()).map(list => {
6829
+ const key = list.join("|");
6830
+ let group = this.outlines.get(key);
6831
+ if (!group) {
6832
+ const groupOrigin = this.entries.get(list[0]).corners[0];
6833
+ const parts = list.map(id => ModelSolid(this.entries.get(id).corners, groupOrigin));
6834
+ const solidsShown = parts.length > MODEL_BOX_UNION_LIMIT ? [encloseSolids(parts)] : parts;
6835
+ group = { key, members: list, hovered: false, origin: groupOrigin, solids: solidsShown, outline: ModelBoxOutline(solidsShown) };
6836
+ }
6837
+ group.hovered = this.entries.get(list[0]).entityId === this.hovered;
6838
+ return group;
6839
+ });
6840
+ // Kept across hover changes, so pulling a part out and putting it back does not trace the union again.
6841
+ const kept = Array.from(this.outlines.values()).filter(group => !groups.includes(group)).slice(-8);
6842
+ this.outlines = new Map([...kept, ...groups].map(group => [group.key, group]));
6843
+ return groups;
6844
+ }
6845
+ /*
6846
+ * Projects every box for this frame and settles which outlines are drawn, before labels place their stems.
6847
+ */
6848
+ Resolve(viewer, pointer) {
6849
+ var _a, _b, _c, _d;
6850
+ this.frames.clear();
6851
+ this.hulls.clear();
6852
+ this.drawn = [];
6853
+ const points = new Map();
6854
+ for (const [id, entry] of this.entries) {
6855
+ const projected = projectCorners(viewer, entry.corners);
6856
+ if (projected) {
6857
+ points.set(id, projected);
6858
+ this.hulls.set(id, convexHull(projected));
6859
+ }
6860
+ }
6861
+ // The application's hover, which is what its temporary label shows, decides first. Without one the
6862
+ // pointer does: the part it is on holds until it leaves, else the smallest box under it wins.
6863
+ const under = Array.from(this.hulls.entries()).filter(([, hull]) => pointer && hullContains(hull, pointer));
6864
+ 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);
6865
+ if (!hovered && under.length) {
6866
+ 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;
6867
+ }
6868
+ if (hovered !== this.hovered) {
6869
+ this.hovered = hovered;
6870
+ this.groups = null;
6871
+ }
6872
+ for (const [id, entry] of this.entries) {
6873
+ const hull = this.hulls.get(id);
6874
+ entry.active = Boolean(((_c = entry.near) === null || _c === void 0 ? void 0 : _c.call(entry)) || entry.entityId === hovered || (pointer && hull && hullContains(hull, pointer)));
6875
+ // A box keeps its place while it fades out, and leaves the competition once it has.
6876
+ const wanted = entry.active || ((_d = this.memberLooks.get(id)) === null || _d === void 0 ? void 0 : _d.shown) > 0.01;
6877
+ if (wanted !== entry.wanted) {
6878
+ entry.wanted = wanted;
6879
+ this.groups = null;
6880
+ }
6881
+ }
6882
+ if (!this.groups) {
6883
+ this.groups = this.group();
6884
+ }
6885
+ const canvas = viewer.canvas;
6886
+ const candidates = [];
6887
+ for (const group of this.groups) {
6888
+ if (!group.members.every(id => points.has(id))) {
6889
+ continue;
6890
+ }
6891
+ const all = group.members.flatMap(id => points.get(id));
6892
+ const xs = all.map(point => point.x), ys = all.map(point => point.y);
6893
+ const width = Math.max(...xs) - Math.min(...xs), height = Math.max(...ys) - Math.min(...ys);
6894
+ if (Math.max(width, height) < MODEL_BOX_MIN_SIZE || width > canvas.clientWidth * 2 || height > canvas.clientHeight * 2) {
6895
+ continue;
6896
+ }
6897
+ const since = Math.min(...group.members.map(id => { var _a; return (_a = this.since.get(id)) !== null && _a !== void 0 ? _a : Infinity; }));
6898
+ const outline = convexHull(all);
6899
+ 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) });
6900
+ }
6901
+ // Between boxes appearing together the larger on screen goes first, as the one the view is about.
6902
+ // What the user points at now outranks what they selected before it.
6903
+ candidates.sort((a, b) => (Number(b.group.hovered) - Number(a.group.hovered) ||
6904
+ Number(b.selected) - Number(a.selected) ||
6905
+ a.since - b.since ||
6906
+ b.area - a.area ||
6907
+ a.group.key.localeCompare(b.group.key)));
6908
+ const transforms = SceneTransforms;
6909
+ const projectPosition = transforms.wgs84ToWindowCoordinates || transforms.worldToWindowCoordinates;
6910
+ const scratch = new Cartesian3();
6911
+ const now = Date.now();
6912
+ const shown = new Set();
6913
+ for (const candidate of candidates) {
6914
+ // A showing box holds its place until truly overlapped; a waiting one needs clear space.
6915
+ const margin = Number.isFinite(candidate.since) ? 0 : MODEL_BOX_CLEARANCE;
6916
+ if (this.drawn.some(other => HullsNear(candidate.outline, other.outline, margin))) {
6917
+ continue;
6918
+ }
6919
+ const { origin } = candidate.group;
6920
+ const toScreen = (point) => {
6921
+ const projected = projectPosition(viewer.scene, Cartesian3.fromElements(origin.x + point[0], origin.y + point[1], origin.z + point[2], scratch));
6922
+ return { x: projected.x, y: projected.y };
6923
+ };
6924
+ const edges = candidate.group.outline.map(([start, end]) => [toScreen(start), toScreen(end)]);
6925
+ const drawn = { ...candidate, edges, landings: [] };
6926
+ this.drawn.push(drawn);
6927
+ for (const id of candidate.group.members) {
6928
+ shown.add(id);
6929
+ if (!this.since.has(id))
6930
+ this.since.set(id, now);
6931
+ const own = points.get(id);
6932
+ // The silhouette's top is the highest drawn edge at that x, so the stem always meets a line.
6933
+ const x = (Math.min(...own.map(point => point.x)) + Math.max(...own.map(point => point.x))) / 2;
6934
+ let anchor = null, edge = -1;
6935
+ edges.forEach(([a, b], i) => {
6936
+ if (x < Math.min(a.x, b.x) || x > Math.max(a.x, b.x) || Math.abs(b.x - a.x) < 1e-6) {
6937
+ return;
6938
+ }
6939
+ const y = a.y + (b.y - a.y) * (x - a.x) / (b.x - a.x);
6940
+ if (!anchor || y < anchor.y) {
6941
+ anchor = { x, y };
6942
+ edge = i;
6943
+ }
6944
+ });
6945
+ anchor = anchor || { x, y: hullTop(candidate.outline, x) };
6946
+ if (edge >= 0)
6947
+ drawn.landings.push({ edge, point: anchor });
6948
+ this.frames.set(id, { points: own, hull: this.hulls.get(id), outline: candidate.outline, anchor, group: candidate.group.key });
6949
+ }
6950
+ }
6951
+ for (const id of Array.from(this.since.keys())) {
6952
+ if (!shown.has(id))
6953
+ this.since.delete(id);
6954
+ }
6955
+ this.requests.clear();
6956
+ }
6957
+ /*
6958
+ * Draws each settled outline as corner brackets that grow into whole edges, back edges dashed.
6959
+ */
6960
+ Draw(viewer, element, coordinates) {
6961
+ var _a, _b, _c;
6962
+ const now = Date.now();
6963
+ const looks = new Map();
6964
+ const memberLooks = new Map();
6965
+ let animating = false;
6966
+ for (const { group } of this.drawn) {
6967
+ const requests = group.members.map(id => this.requests.get(id)).filter(Boolean);
6968
+ if (!requests.length) {
6969
+ // Its labels are hidden this frame, perhaps off screen; it keeps its look so it does not regrow.
6970
+ const kept = this.looks.get(group.key);
6971
+ if (kept) {
6972
+ looks.set(group.key, kept);
6973
+ group.members.forEach(id => memberLooks.set(id, kept));
6974
+ if (kept.svg)
6975
+ kept.svg.style.display = "none";
6976
+ }
6977
+ continue;
6978
+ }
6979
+ // A group that gained or lost a member carries on from where its members' outline was.
6980
+ const previous = group.members.map(id => this.memberLooks.get(id)).filter(Boolean);
6981
+ const look = (_a = this.looks.get(group.key)) !== null && _a !== void 0 ? _a : {
6982
+ shown: Math.max(0, ...previous.map(item => item.shown)), whole: Math.max(0, ...previous.map(item => item.whole)),
6983
+ 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
6984
+ };
6985
+ const elapsed = Math.min(32, Math.max(0, now - look.time));
6986
+ const duration = Math.min(...requests.map(request => request.duration));
6987
+ const blend = duration > 0 ? 1 - Math.exp(-elapsed / (duration / 3)) : 1;
6988
+ const approach = (value, to) => Math.abs(value - to) < 0.001 ? to : value + (to - value) * blend;
6989
+ const shownTarget = group.members.some(id => { var _a; return (_a = this.entries.get(id)) === null || _a === void 0 ? void 0 : _a.active; }) ? 1 : 0;
6990
+ const wholeTarget = group.hovered || requests.some(request => request.whole) ? 1 : 0;
6991
+ look.shown = approach(look.shown, shownTarget);
6992
+ look.whole = approach(look.whole, wholeTarget);
6993
+ look.grow = approach(look.grow, 1);
6994
+ look.time = now;
6995
+ animating = animating || look.shown !== shownTarget || look.whole !== wholeTarget || look.grow !== 1;
6996
+ looks.set(group.key, look);
6997
+ group.members.forEach(id => memberLooks.set(id, look));
6998
+ if (look.svg && this.looks.get(group.key) !== look) {
6999
+ // Inherited from an earlier group; claim it so that group's cleanup leaves it alone.
7000
+ for (const [key, other] of this.looks) {
7001
+ if (other.svg === look.svg && key !== group.key)
7002
+ other.svg = undefined;
7003
+ }
7004
+ }
7005
+ this.drawGroup(viewer, element, coordinates, this.drawn.find(item => item.group === group), look, requests);
7006
+ }
7007
+ for (const [key, look] of this.looks) {
7008
+ if (!looks.has(key))
7009
+ (_c = look.svg) === null || _c === void 0 ? void 0 : _c.remove();
7010
+ }
7011
+ this.looks = looks;
7012
+ this.memberLooks = memberLooks;
7013
+ if (animating) {
7014
+ viewer.scene.requestRender();
7015
+ }
7016
+ }
7017
+ drawGroup(viewer, element, coordinates, drawn, look, requests) {
7018
+ const group = drawn.group;
7019
+ let svg = look.svg;
7020
+ if (!svg) {
7021
+ const ns = "http://www.w3.org/2000/svg";
7022
+ svg = document.createElementNS(ns, "svg");
7023
+ svg.setAttribute("class", "entity-label-model-box");
7024
+ svg.setAttribute("aria-hidden", "true");
7025
+ 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));";
7026
+ for (const edges of ["back", "front"]) {
7027
+ const path = document.createElementNS(ns, "path");
7028
+ path.setAttribute("fill", "none");
7029
+ path.setAttribute("stroke-linecap", "round");
7030
+ path.dataset.edges = edges;
7031
+ svg.appendChild(path);
7032
+ }
7033
+ look.svg = svg;
7034
+ }
7035
+ if (svg.parentElement !== element) {
7036
+ element.appendChild(svg);
7037
+ }
7038
+ svg.style.display = "";
7039
+ const transforms = SceneTransforms;
7040
+ const projectPosition = transforms.wgs84ToWindowCoordinates || transforms.worldToWindowCoordinates;
7041
+ const origin = group.origin;
7042
+ const scratch = new Cartesian3();
7043
+ // A new box grows out of the ring it replaces.
7044
+ const grown = look.grow * look.grow * (3 - 2 * look.grow);
7045
+ const from = requests[0].from;
7046
+ const at = (point) => `${(from.x + (point.x - from.x) * grown) * coordinates.scaleX} ${(from.y + (point.y - from.y) * grown) * coordinates.scaleY}`;
7047
+ const toScreen = (point) => {
7048
+ const projected = projectPosition(viewer.scene, Cartesian3.fromElements(origin.x + point[0], origin.y + point[1], origin.z + point[2], scratch));
7049
+ return at(projected);
7050
+ };
7051
+ const camera = viewer.camera.positionWC || viewer.camera.position;
7052
+ const eye = [camera.x - origin.x, camera.y - origin.y, camera.z - origin.z];
7053
+ const span = Math.max(...group.solids.map(solid => vLength(solid.half)));
7054
+ const inset = Math.max(1, span) * 1e-6;
7055
+ const fraction = MODEL_BOX_BRACKET + (0.5 - MODEL_BOX_BRACKET) * look.whole;
7056
+ const parts = fraction >= 0.499 ? [[0, 1]] : [[0, fraction], [1 - fraction, 1]];
7057
+ const paths = { front: "", back: "" };
7058
+ for (const [start, end] of group.outline) {
7059
+ const delta = vSub(end, start);
7060
+ // Sample visibility along the edge, since another member can hide part of it.
7061
+ const steps = group.solids.length > 1 ? Math.min(24, Math.max(2, Math.ceil(vLength(delta) / span * 12))) : 1;
7062
+ const visible = [];
7063
+ for (let i = 0; i < steps; i++) {
7064
+ visible.push(!solidsHide(group.solids, eye, vAdd(start, vScale(delta, (i + 0.5) / steps)), inset));
7065
+ }
7066
+ for (const [from, to] of parts) {
7067
+ let runStart = from;
7068
+ for (let i = Math.floor(from * steps); i < steps && i / steps < to; i++) {
7069
+ const runEnd = Math.min(to, (i + 1) / steps);
7070
+ if (runEnd < to && visible[i + 1] === visible[i]) {
7071
+ continue;
7072
+ }
7073
+ paths[visible[i] ? "front" : "back"] += `M${toScreen(vAdd(start, vScale(delta, runStart)))}L${toScreen(vAdd(start, vScale(delta, runEnd)))}`;
7074
+ runStart = runEnd;
7075
+ }
7076
+ }
7077
+ }
7078
+ // Each stem lands on an edge, which brackets may leave bare, so a short piece of it is drawn there.
7079
+ if (fraction < 0.499) {
7080
+ for (const { edge, point } of drawn.landings) {
7081
+ const [a, b] = drawn.edges[edge];
7082
+ const length = Math.hypot(b.x - a.x, b.y - a.y);
7083
+ const reach = Math.min(MODEL_BOX_LANDING, length / 2);
7084
+ const toward = (sign) => {
7085
+ const along = Math.max(0, Math.min(length, Math.hypot(point.x - a.x, point.y - a.y) + sign * reach));
7086
+ return { x: a.x + (b.x - a.x) * along / length, y: a.y + (b.y - a.y) * along / length };
7087
+ };
7088
+ paths.front += `M${at(toward(-1))}L${at(toward(1))}`;
7089
+ }
7090
+ }
7091
+ svg.style.left = `${coordinates.x}px`;
7092
+ svg.style.top = `${coordinates.y}px`;
7093
+ svg.style.opacity = String(Math.max(...requests.map(request => request.opacity)) * look.shown * Math.min(1, look.grow * 2));
7094
+ svg.style.zIndex = String(Math.max(...requests.map(request => request.zIndex)));
7095
+ const color = requests[0].color;
7096
+ for (const path of Array.from(svg.children)) {
7097
+ const front = path.dataset.edges === "front";
7098
+ path.setAttribute("d", paths[front ? "front" : "back"]);
7099
+ path.setAttribute("stroke", color);
7100
+ path.setAttribute("stroke-width", front ? "1.5" : "1");
7101
+ path.setAttribute("stroke-opacity", front ? "1" : "0.35");
7102
+ path.setAttribute("stroke-dasharray", front ? "" : "3 3");
7103
+ }
7104
+ }
7105
+ }
7106
+ EntityLabel.ModelBoxes = ModelBoxes;
7107
+ /*
7108
+ * Andrew's monotone chain, counter-clockwise in screen space.
7109
+ */
7110
+ function convexHull(points) {
7111
+ const sorted = [...points].sort((a, b) => a.x - b.x || a.y - b.y);
7112
+ const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
7113
+ const build = (list) => {
7114
+ const chain = [];
7115
+ for (const point of list) {
7116
+ while (chain.length >= 2 && cross(chain[chain.length - 2], chain[chain.length - 1], point) <= 0)
7117
+ chain.pop();
7118
+ chain.push(point);
7119
+ }
7120
+ chain.pop();
7121
+ return chain;
7122
+ };
7123
+ return [...build(sorted), ...build(sorted.reverse())];
7124
+ }
7125
+ /*
7126
+ * Where the top of a convex outline crosses a vertical line, so a stem leaves the box without jumping between corners.
7127
+ */
7128
+ function hullTop(hull, x) {
7129
+ let top = Infinity;
7130
+ for (let i = 0; i < hull.length; i++) {
7131
+ const a = hull[i];
7132
+ const b = hull[(i + 1) % hull.length];
7133
+ if (x < Math.min(a.x, b.x) || x > Math.max(a.x, b.x)) {
7134
+ continue;
7135
+ }
7136
+ 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));
7137
+ }
7138
+ return top;
7139
+ }
7140
+ function hullArea(hull) {
7141
+ 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;
7142
+ }
7143
+ function hullContains(hull, point) {
7144
+ return hull.length >= 3 && hull.every((a, i) => {
7145
+ const b = hull[(i + 1) % hull.length];
7146
+ return (b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x) >= 0;
7147
+ });
7148
+ }
6383
7149
  class Label {
6384
7150
  get HasText() { return this.options.presentation !== "icon"; }
6385
7151
  SetHoverVisible(visible) {
@@ -6434,6 +7200,11 @@ var EntityLabel;
6434
7200
  this.Transient = false;
6435
7201
  this.hoverFade = { from: 0, to: 1, start: Date.now() };
6436
7202
  this.cursorMotion = { amount: 0, from: 0, target: 0, start: 0, position: undefined };
7203
+ // How far the stem is from the ring onto a model box, and the offset it eases out after changing outline.
7204
+ // The record's box for the Entity this label is on: undefined corners while reading, null when it has none.
7205
+ this.modelBox = null;
7206
+ this.boxMorph = { value: 0, from: 0, target: 0, start: 0, anchor: undefined, group: undefined,
7207
+ shift: { x: 0, y: 0, start: 0 } };
6437
7208
  // Counter to help differentiate between createLabel calls.
6438
7209
  this.counter = 0;
6439
7210
  // Whether this label has asked its group for a default image, which it does once on screen.
@@ -6519,11 +7290,13 @@ var EntityLabel;
6519
7290
  * Recalling will be ignored.
6520
7291
  */
6521
7292
  Dispose() {
7293
+ var _a;
6522
7294
  if (this.disposed) {
6523
7295
  return;
6524
7296
  }
6525
7297
  this.disposed = true;
6526
7298
  this.removeLabel();
7299
+ (_a = layers.get(this.viewer)) === null || _a === void 0 ? void 0 : _a.modelBoxes.Set(this.id, null, null, null);
6527
7300
  clearTimeout(this.loadTimeout);
6528
7301
  this.loadTimeout = null;
6529
7302
  }
@@ -6545,7 +7318,7 @@ var EntityLabel;
6545
7318
  * @param starterPos3d optional position to create label at. Ignored for billboard labels.
6546
7319
  */
6547
7320
  async createLabel(starterPos3d) {
6548
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
7321
+ 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;
6549
7322
  // The old label stays until its replacement is built, so a rebuild never blinks while a location loads.
6550
7323
  let pos3d = starterPos3d ? starterPos3d.clone() : null;
6551
7324
  const counter = ++this.counter;
@@ -6831,6 +7604,9 @@ var EntityLabel;
6831
7604
  if (this.disposed || this.counter != counter) {
6832
7605
  return;
6833
7606
  }
7607
+ // Leave the layer only once rejoined, so a rebuild never empties it and closes it along with its boxes.
7608
+ const leaveLayer = this.renderRemoval;
7609
+ this.renderRemoval = null;
6834
7610
  this.removeLabel();
6835
7611
  // A found photo draws as if configured, without changing the options clustering compares and measures.
6836
7612
  const photo = HasIcon(this.options) ? null : (_j = this.options.photo) !== null && _j !== void 0 ? _j : this.photo;
@@ -7127,9 +7903,65 @@ var EntityLabel;
7127
7903
  radius: Math.max(width, height) / 2
7128
7904
  };
7129
7905
  };
7906
+ // Only CAD tilesets and model Entities are assembly parts with a geometry box; anything else stays a point.
7907
+ const boxable = Boolean(normalLeader && ((_r = this.rego) === null || _r === void 0 ? void 0 : _r.entityId) && ((visual instanceof Cesium3DTileFeature && this.rego.tilesetType === Tileset.EType.Cad) ||
7908
+ (visual instanceof Entity && Boolean(visual.model))));
7909
+ if (!boxable || ((_s = this.modelBox) === null || _s === void 0 ? void 0 : _s.entityId) !== ((_t = this.rego) === null || _t === void 0 ? void 0 : _t.entityId)) {
7910
+ layer.modelBoxes.Set(this.id, null, null, null);
7911
+ this.modelBox = null;
7912
+ }
7913
+ // Read once per Entity, so a label rebuilt for its loaded text keeps its box instead of waiting again.
7914
+ if (boxable && !this.modelBox) {
7915
+ const box = { entityId: this.rego.entityId, corners: undefined, load: null };
7916
+ box.load = this.loadModelBox().catch(() => null).then(corners => box.corners = corners || null);
7917
+ this.modelBox = box;
7918
+ }
7919
+ // A model's label stays hidden until its box is known, so a ring never shows only to turn into a box.
7920
+ 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;
7921
+ 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);
7922
+ const boxPendingUntil = Date.now() + MODEL_BOX_WAIT;
7923
+ // Whether this label has been on screen yet; a box found before then is drawn in place, not eased into.
7924
+ let shownOnce = false;
7925
+ if (boxable) {
7926
+ const found = this._pos3d;
7927
+ const settle = () => {
7928
+ if (boxPending && !this.disposed && this.counter === counter) {
7929
+ boxPending = false;
7930
+ this.viewer.scene.requestRender();
7931
+ }
7932
+ };
7933
+ // Past the wait a slow record no longer holds the label back; a box that comes later eases in.
7934
+ setTimeout(settle, MODEL_BOX_WAIT);
7935
+ this.modelBox.load.then(corners => {
7936
+ if (!corners || this.disposed || this.counter !== counter) {
7937
+ return;
7938
+ }
7939
+ // A record that disagrees with where the model was found describes another placement, such as a moved tileset.
7940
+ const centre = Cartesian3.midpoint(corners[0], corners[7], new Cartesian3());
7941
+ if (Cartesian3.distance(centre, found) > Cartesian3.distance(corners[0], corners[7]) * 0.75 + 2) {
7942
+ return;
7943
+ }
7944
+ const rego = this.rego;
7945
+ // Let go a little further out than it is taken, so a camera resting at the limit does not flip it.
7946
+ let near = false;
7947
+ layer.modelBoxes.Set(this.id, corners, rego.entityId, {
7948
+ 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); },
7949
+ 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)); },
7950
+ near: () => {
7951
+ var _a, _b;
7952
+ const ring = this.options.anchorRing;
7953
+ const camera = this.viewer.camera.positionWC || this.viewer.camera.position;
7954
+ const limit = ((_a = ring === null || ring === void 0 ? void 0 : ring.nearDistance) !== null && _a !== void 0 ? _a : 100) * (near ? 1.1 : 1);
7955
+ near = (ring === null || ring === void 0 ? void 0 : ring.enabled) !== false && Number.isFinite((_b = this._pos3d) === null || _b === void 0 ? void 0 : _b.x) && Cartesian3.distance(camera, this._pos3d) <= limit;
7956
+ return near || ((ring === null || ring === void 0 ? void 0 : ring.enabled) !== false && Boolean(this.cursorMotion.target));
7957
+ }
7958
+ });
7959
+ this.viewer.scene.requestRender();
7960
+ }).finally(settle);
7961
+ }
7130
7962
  let cursorMotion = this.cursorMotion;
7131
7963
  const project = () => {
7132
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
7964
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
7133
7965
  if (this.disposed || !this.viewer || this.viewer.isDestroyed() || ((_a = this.visible) === null || _a === void 0 ? void 0 : _a.call(this)) === false ||
7134
7966
  (presentation === "icon" && this.options.iconScale === 0)) {
7135
7967
  return null;
@@ -7167,18 +7999,29 @@ var EntityLabel;
7167
7999
  if (!Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.x) || !Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.y)) {
7168
8000
  return null;
7169
8001
  }
8002
+ const boxed = layer.modelBoxes.Frame(this.id);
8003
+ const waiting = boxPending && Date.now() < boxPendingUntil;
8004
+ 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);
8005
+ // Eased between the ring and where the stem meets the box, so gaining or losing a box never jumps.
8006
+ const boxTarget = this.boxMorph.anchor;
8007
+ const ringPoint = { x: anchor.x + pixelOffset.x, y: anchor.y + pixelOffset.y };
8008
+ const boxAnchor = boxBlend > 0 && boxTarget ? {
8009
+ x: ringPoint.x + (boxTarget.x - ringPoint.x) * boxBlend,
8010
+ y: ringPoint.y + (boxTarget.y - ringPoint.y) * boxBlend
8011
+ } : undefined;
8012
+ const start = boxAnchor || anchor;
7170
8013
  const onScreen = anchor.x >= 0 && anchor.y >= 0 && anchor.x < this.viewer.canvas.clientWidth && anchor.y < this.viewer.canvas.clientHeight;
7171
- if (leader && !onScreen)
8014
+ if (leader && (start.x < 0 || start.y < 0 || start.x >= this.viewer.canvas.clientWidth || start.y >= this.viewer.canvas.clientHeight))
7172
8015
  return null;
7173
8016
  if (onScreen && !this.imageAsked && this.groupId && !HasIcon(this.options) && !this.options.photo) {
7174
8017
  this.imageAsked = true;
7175
- (_e = this.visualRegister) === null || _e === void 0 ? void 0 : _e.RequestLabelImage((_f = this.rego) === null || _f === void 0 ? void 0 : _f.entityId);
8018
+ (_g = this.visualRegister) === null || _g === void 0 ? void 0 : _g.RequestLabelImage((_h = this.rego) === null || _h === void 0 ? void 0 : _h.entityId);
7176
8019
  }
7177
8020
  const transientPivot = (this.Transient && !this.Persistent ?
7178
- (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);
8021
+ (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);
7179
8022
  const hover = (presentation !== "icon" ?
7180
8023
  (this.Transient && !this.Persistent && this.hoverFade.to === 0 ? this.retainedHoverPosition :
7181
- (_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);
8024
+ (_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);
7182
8025
  const reused = !this.Transient || this.Persistent;
7183
8026
  const now = Date.now();
7184
8027
  const animateCursor = () => {
@@ -7202,12 +8045,14 @@ var EntityLabel;
7202
8045
  const cursorAmount = cursorMotion.amount;
7203
8046
  leader = normalLeader || cursorAmount > 0;
7204
8047
  // An icon keeps its callout centred on the glyph rather than following the cursor across it.
7205
- const icon = measureIcon();
8048
+ const icon = boxAnchor ? null : measureIcon();
7206
8049
  const iconRing = icon ? Math.max(ICON_RING_MIN, icon.radius + ICON_RING_PADDING) : 0;
7207
- const pivot = icon ? undefined : transientPivot;
7208
- let x = ((_o = pivot === null || pivot === void 0 ? void 0 : pivot.x) !== null && _o !== void 0 ? _o : anchor.x) + (icon || pixelOffset).x;
7209
- let y = ((_p = pivot === null || pivot === void 0 ? void 0 : pivot.y) !== null && _p !== void 0 ? _p : anchor.y) + (icon || pixelOffset).y;
7210
- if (cursorAmount > 0 && !icon) {
8050
+ // A boxed model is pinned the same way, with its stem leaving the top of the box outline.
8051
+ const pinned = Boolean(icon || boxAnchor);
8052
+ const pivot = pinned ? undefined : transientPivot;
8053
+ 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;
8054
+ 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;
8055
+ if (cursorAmount > 0 && !pinned) {
7211
8056
  // A leader-less label starts centred on its anchor; preserve that box as it lifts away.
7212
8057
  if (!normalLeader) {
7213
8058
  x -= ele.offsetWidth / layer.coordinates.scaleX / 2;
@@ -7242,6 +8087,9 @@ var EntityLabel;
7242
8087
  if (visual instanceof Entity && (visual.polygon || visual.polyline))
7243
8088
  leaderLength /= 3;
7244
8089
  }
8090
+ if (boxAnchor && this.options.leaderLength == null) {
8091
+ leaderLength += (MODEL_BOX_STEM - leaderLength) * boxBlend;
8092
+ }
7245
8093
  if (cursorAmount > 0) {
7246
8094
  leaderLength += (96 - leaderLength) * cursorAmount;
7247
8095
  }
@@ -7249,7 +8097,7 @@ var EntityLabel;
7249
8097
  leaderLength += iconRing;
7250
8098
  y = anchorY - leaderLength;
7251
8099
  }
7252
- else if (hover && !reused && !icon) {
8100
+ else if (hover && !reused && !pinned) {
7253
8101
  x = hover.x + pixelOffset.x;
7254
8102
  y = hover.y + pixelOffset.y - 24;
7255
8103
  }
@@ -7258,7 +8106,7 @@ var EntityLabel;
7258
8106
  const width = ele.offsetWidth / coordinates.scaleX;
7259
8107
  const height = ele.offsetHeight / coordinates.scaleY;
7260
8108
  let clusterBounds;
7261
- if ((_q = this.options.cluster) === null || _q === void 0 ? void 0 : _q.enabled) {
8109
+ if ((_s = this.options.cluster) === null || _s === void 0 ? void 0 : _s.enabled) {
7262
8110
  const textWidth = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetWidth) || 0;
7263
8111
  const textHeight = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetHeight) || 0;
7264
8112
  // A photo is never part of the footprint, so its arrival cannot change which cluster a label joins.
@@ -7292,7 +8140,7 @@ var EntityLabel;
7292
8140
  if (!leader && (bounds.x + bounds.width < 0 || bounds.y + bounds.height < 0 || bounds.x > this.viewer.canvas.clientWidth || bounds.y > this.viewer.canvas.clientHeight)) {
7293
8141
  return null;
7294
8142
  }
7295
- 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 });
8143
+ 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 });
7296
8144
  const opacity = this.prePositioned || hoverOnly || MAX_DISTANCE === Infinity ? 1 : 1 - distance / MAX_DISTANCE;
7297
8145
  return {
7298
8146
  canFlip: leader && presentation !== "icon", hasLeader: leader,
@@ -7300,24 +8148,25 @@ var EntityLabel;
7300
8148
  transient: this.Transient || Boolean(hover) || cursorAmount > 0,
7301
8149
  extendedGeometry: visual instanceof Entity && Boolean(visual.polygon || visual.polyline || visual.corridor),
7302
8150
  viewportWidth: this.viewer.canvas.clientWidth,
7303
- 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,
7304
- 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,
7305
- iconRadius: icon === null || icon === void 0 ? void 0 : icon.radius,
8151
+ 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,
8152
+ 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,
8153
+ iconRadius: icon === null || icon === void 0 ? void 0 : icon.radius, modelBox: boxed || undefined, boxBlend,
7306
8154
  // Priority shows the hovered member individually without dragging its cluster along.
7307
- opacity, priority: hover || cursorAmount > 0 || (state === null || state === void 0 ? void 0 : state.highlighted) ? 1 : 0, options: this.options, text: originalText, clusterBounds
8155
+ 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
7308
8156
  };
7309
8157
  };
7310
8158
  let displacement = { x: 0, y: 0 };
7311
8159
  let nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
7312
8160
  const apply = (placement) => {
7313
- 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;
8161
+ 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;
7314
8162
  const show = (placement === null || placement === void 0 ? void 0 : placement.visible) && placement.opacity > 0;
8163
+ shownOnce = shownOnce || Boolean(show);
7315
8164
  clickEntityIds = show ? placement.entityIds : [];
7316
8165
  ele.style.visibility = show ? "visible" : "hidden";
7317
8166
  lineEle.style.display = show && leader ? "block" : "none";
7318
8167
  if (runEle)
7319
8168
  runEle.style.display = show && leader ? "block" : "none";
7320
- this.anchor.style.display = show && leader ? "block" : "none";
8169
+ this.anchor.style.display = show && leader && !(((_a = placement.frame) === null || _a === void 0 ? void 0 : _a.boxBlend) >= 1) ? "block" : "none";
7321
8170
  if (!show) {
7322
8171
  displacement = { x: 0, y: 0 };
7323
8172
  nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
@@ -7329,7 +8178,7 @@ var EntityLabel;
7329
8178
  return;
7330
8179
  }
7331
8180
  // A label standing for a cluster shows its summary only; its photo is put away and never fetched.
7332
- const solo = !((_a = placement.members) === null || _a === void 0 ? void 0 : _a.length);
8181
+ const solo = !((_b = placement.members) === null || _b === void 0 ? void 0 : _b.length);
7333
8182
  if (look.iconPhoto && iconElement && iconElement.dataset.failed !== "true") {
7334
8183
  const display = solo ? "inline-flex" : "none";
7335
8184
  if (iconElement.style.display !== display) {
@@ -7347,7 +8196,7 @@ var EntityLabel;
7347
8196
  }
7348
8197
  }, PHOTO_SHOW_DELAY);
7349
8198
  }
7350
- const displayText = (_b = placement.text) !== null && _b !== void 0 ? _b : originalText;
8199
+ const displayText = (_c = placement.text) !== null && _c !== void 0 ? _c : originalText;
7351
8200
  if (textElement && textElement.textContent !== displayText) {
7352
8201
  textElement.textContent = displayText;
7353
8202
  // Measure the summary's footprint next frame without replacing each member's source text.
@@ -7371,9 +8220,9 @@ var EntityLabel;
7371
8220
  const stack = JSON.stringify(placement.icons || []);
7372
8221
  if (iconStack && stack !== displayedStack) {
7373
8222
  displayedStack = stack;
7374
- const looks = ((_c = placement.icons) === null || _c === void 0 ? void 0 : _c.length) ? [iconLook(this.options), ...placement.icons] : [];
8223
+ const looks = ((_d = placement.icons) === null || _d === void 0 ? void 0 : _d.length) ? [iconLook(this.options), ...placement.icons] : [];
7375
8224
  // One small size for all, so no icon in the arrangement reads as more important than another.
7376
- const size = (this.options.iconSize || 24) * ((_d = this.options.iconScale) !== null && _d !== void 0 ? _d : 1) * ICON_STACK_SCALE;
8225
+ const size = (this.options.iconSize || 24) * ((_e = this.options.iconScale) !== null && _e !== void 0 ? _e : 1) * ICON_STACK_SCALE;
7377
8226
  const built = looks.map(look => this.buildIcon({ ...look, iconSize: size }, counter));
7378
8227
  // A stack is on show already, so its photos load straight away.
7379
8228
  built.forEach(({ start }) => start === null || start === void 0 ? void 0 : start());
@@ -7408,9 +8257,11 @@ var EntityLabel;
7408
8257
  // Summary and badge changes must fit on their first visible frame, including after resize.
7409
8258
  const ring = this.options.anchorRing;
7410
8259
  const pointer = (layer.pointer || (cursorMotion.target ? cursorMotion.position : undefined) || (this.Transient && !this.Persistent ?
7411
- (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));
8260
+ (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));
7412
8261
  const iconRing = frame.iconRadius ? Math.max(ICON_RING_MIN, frame.iconRadius + ICON_RING_PADDING) : 0;
7413
- 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);
8262
+ const boxed = leader ? frame.modelBox : undefined;
8263
+ const hovered = pointer && (boxed ? hullContains(boxed.hull, pointer) :
8264
+ 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));
7414
8265
  if (look.iconPhoto && growPhoto) {
7415
8266
  // A rectangle leans on the leader's side of the label.
7416
8267
  if (photoShape === "rect") {
@@ -7421,15 +8272,15 @@ var EntityLabel;
7421
8272
  }
7422
8273
  // Grown in layout rather than scaled, so the border keeps its width, and the label, whose bottom
7423
8274
  // sits on the leader, grows up and sideways without changing the leader's length.
7424
- growPhoto(Boolean(hovered) && !((_j = placement.members) === null || _j === void 0 ? void 0 : _j.length));
8275
+ growPhoto(Boolean(hovered) && !((_k = placement.members) === null || _k === void 0 ? void 0 : _k.length));
7425
8276
  }
7426
- 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));
8277
+ 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));
7427
8278
  const targetRadius = normalRadius + (15.4 - normalRadius) * cursorMotion.amount;
7428
8279
  // Around a glyph the ring keeps the glyph's size and only fades its stroke in, so it never covers the icon.
7429
8280
  const targetOuter = iconRing || targetRadius || 2.5;
7430
8281
  const targetInner = iconRing ? (targetRadius > 0 ? iconRing - 2 : iconRing) : Math.max(0, targetRadius - 2);
7431
8282
  const now = Date.now();
7432
- const duration = (_o = ring === null || ring === void 0 ? void 0 : ring.transitionDuration) !== null && _o !== void 0 ? _o : 200;
8283
+ const duration = (_p = ring === null || ring === void 0 ? void 0 : ring.transitionDuration) !== null && _p !== void 0 ? _p : 200;
7433
8284
  const sharedRing = this.Transient && !this.Persistent && this.hoverFade.to === 1 ? this.hoverRingState : undefined;
7434
8285
  let ringAnimation = sharedRing ? sharedRing.animation : this.ringAnimation;
7435
8286
  if (!ringAnimation) {
@@ -7463,7 +8314,8 @@ var EntityLabel;
7463
8314
  if (sharedRing) {
7464
8315
  sharedRing.animation = ringAnimation;
7465
8316
  }
7466
- const radius = ringAnimation.inner > 0 ? ringAnimation.outer : 0;
8317
+ const ringShare = 1 - (frame.boxBlend || 0);
8318
+ const radius = ringShare > 0 && ringAnimation.inner > 0 ? ringAnimation.outer * ringShare : 0;
7467
8319
  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);
7468
8320
  const position = { x: (stem || frame).x, y: (stem || frame).y };
7469
8321
  const width = ele.offsetWidth / coordinates.scaleX;
@@ -7471,19 +8323,19 @@ var EntityLabel;
7471
8323
  if (leader && angled) {
7472
8324
  // The declutter owns the side; the fit only says which one had the room to begin with.
7473
8325
  const run = Math.max(ANGLED_LEADER_RUN, radius + 4);
7474
- const side = (_q = (_p = placement.textSide) !== null && _p !== void 0 ? _p : frame.leaderSide) !== null && _q !== void 0 ? _q : "right";
8326
+ const side = (_r = (_q = placement.textSide) !== null && _q !== void 0 ? _q : frame.leaderSide) !== null && _r !== void 0 ? _r : "right";
7475
8327
  const beside = side === "left" ? frame.anchorX - run - width : frame.anchorX + run;
7476
8328
  position.x = Math.max(0, Math.min(beside, this.viewer.canvas.clientWidth - width));
7477
8329
  }
7478
8330
  else if (leader && placement.textSide === "left") {
7479
8331
  position.x = Math.max(0, Math.min(frame.anchorX - width, this.viewer.canvas.clientWidth - width));
7480
8332
  }
7481
- const gap = (_s = (_r = this.options.pointerAvoidance) === null || _r === void 0 ? void 0 : _r.gap) !== null && _s !== void 0 ? _s : 12;
8333
+ const gap = (_t = (_s = this.options.pointerAvoidance) === null || _s === void 0 ? void 0 : _s.gap) !== null && _t !== void 0 ? _t : 12;
7482
8334
  const containsPointer = (x, y, margin) => {
7483
8335
  return pointer && pointer.x >= x - margin && pointer.x <= x + width + margin && pointer.y >= y - margin && pointer.y <= y + height + margin;
7484
8336
  };
7485
8337
  const passive = !selectOnClick || !this.visualRegister;
7486
- if (passive && ((_t = this.options.pointerAvoidance) === null || _t === void 0 ? void 0 : _t.enabled) !== false && pointer &&
8338
+ if (passive && ((_u = this.options.pointerAvoidance) === null || _u === void 0 ? void 0 : _u.enabled) !== false && pointer &&
7487
8339
  (containsPointer(position.x, position.y, gap) || containsPointer(position.x + displacement.x, position.y + displacement.y, gap))) {
7488
8340
  if (containsPointer(position.x + displacement.x, position.y + displacement.y, 0)) {
7489
8341
  // Choose against the original box and retain the offset until the pointer leaves both boxes.
@@ -7501,7 +8353,7 @@ var EntityLabel;
7501
8353
  else {
7502
8354
  displacement = { x: 0, y: 0 };
7503
8355
  }
7504
- const nudgeDuration = (_v = (_u = this.options.pointerAvoidance) === null || _u === void 0 ? void 0 : _u.transitionDuration) !== null && _v !== void 0 ? _v : 200;
8356
+ const nudgeDuration = (_w = (_v = this.options.pointerAvoidance) === null || _v === void 0 ? void 0 : _v.transitionDuration) !== null && _w !== void 0 ? _w : 200;
7505
8357
  const animateNudge = () => {
7506
8358
  const progress = now < nudge.start ? 0 : nudgeDuration > 0 ? Math.min(1, (now - nudge.start) / nudgeDuration) : 1;
7507
8359
  const eased = progress * progress * (3 - 2 * progress);
@@ -7517,7 +8369,7 @@ var EntityLabel;
7517
8369
  fromY: nudge.y,
7518
8370
  targetX: displacement.x,
7519
8371
  targetY: displacement.y,
7520
- start: now + ((_x = (_w = this.options.pointerAvoidance) === null || _w === void 0 ? void 0 : _w.delay) !== null && _x !== void 0 ? _x : 180)
8372
+ start: now + ((_y = (_x = this.options.pointerAvoidance) === null || _x === void 0 ? void 0 : _x.delay) !== null && _y !== void 0 ? _y : 180)
7521
8373
  };
7522
8374
  }
7523
8375
  if (animateNudge() < 1 && (nudge.fromX !== nudge.targetX || nudge.fromY !== nudge.targetY)) {
@@ -7528,10 +8380,10 @@ var EntityLabel;
7528
8380
  ele.style.left = `${coordinates.x + position.x * coordinates.scaleX}px`;
7529
8381
  ele.style.top = `${coordinates.y + position.y * coordinates.scaleY}px`;
7530
8382
  for (const clickTarget of Array.from(content.querySelectorAll('[role="button"]'))) {
7531
- 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}`);
8383
+ 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}`);
7532
8384
  }
7533
8385
  if (leader) {
7534
- const nodeRadius = ringAnimation.outer;
8386
+ const nodeRadius = ringAnimation.outer * (1 - (frame.boxBlend || 0));
7535
8387
  const anchor = this.anchor;
7536
8388
  anchor.style.left = `${coordinates.x + (frame.anchorX - nodeRadius) * coordinates.scaleX}px`;
7537
8389
  anchor.style.top = `${coordinates.y + (frame.anchorY - nodeRadius) * coordinates.scaleY}px`;
@@ -7544,11 +8396,12 @@ var EntityLabel;
7544
8396
  const circle = svg.firstElementChild;
7545
8397
  circle.setAttribute("cx", String(nodeRadius));
7546
8398
  circle.setAttribute("cy", String(nodeRadius));
7547
- circle.setAttribute("r", String((nodeRadius + ringAnimation.inner) / 2));
8399
+ const innerRadius = ringAnimation.inner * (1 - (frame.boxBlend || 0));
8400
+ circle.setAttribute("r", String((nodeRadius + innerRadius) / 2));
7548
8401
  circle.setAttribute("stroke", this.options.leaderColor || "rgba(255, 255, 255, 0.8)");
7549
- circle.setAttribute("stroke-width", String(nodeRadius - ringAnimation.inner));
8402
+ circle.setAttribute("stroke-width", String(nodeRadius - innerRadius));
7550
8403
  anchor.style.boxShadow = radius ? "none" : "0 0 5px rgba(0, 0, 0, 0.3)";
7551
- anchor.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount));
8404
+ anchor.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount) * (1 - (frame.boxBlend || 0)));
7552
8405
  anchor.style.zIndex = String(placement.zIndex);
7553
8406
  // The stem starts at the rim, never across the transparent centre, even after passive displacement.
7554
8407
  const straightX = Math.max(position.x, Math.min(frame.anchorX, position.x + width));
@@ -7568,13 +8421,13 @@ var EntityLabel;
7568
8421
  // Keep the stem inside the 126-degree arc, away from antialiased endpoints.
7569
8422
  const arcFraction = 0.35;
7570
8423
  const rotationLimit = Math.PI * arcFraction - Math.PI / 30;
7571
- const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_1 = ringAnimation.arcOffset) !== null && _1 !== void 0 ? _1 : 0);
8424
+ const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_2 = ringAnimation.arcOffset) !== null && _2 !== void 0 ? _2 : 0);
7572
8425
  const targetOpen = cursorMotion.target || (hovered && (ring === null || ring === void 0 ? void 0 : ring.enabled) !== false && targetRadius > 0) ? 1 : 0;
7573
- const elapsed = Math.min(32, Math.max(0, now - ((_2 = ringAnimation.arcTime) !== null && _2 !== void 0 ? _2 : now)));
8426
+ const elapsed = Math.min(32, Math.max(0, now - ((_3 = ringAnimation.arcTime) !== null && _3 !== void 0 ? _3 : now)));
7574
8427
  const blend = duration > 0 ? 1 - Math.exp(-elapsed / (duration / 3)) : 1;
7575
8428
  const approach = (value, target) => Math.abs(value - target) < 0.001 ? target : value + (target - value) * blend;
7576
- ringAnimation.arcOffset = approach((_3 = ringAnimation.arcOffset) !== null && _3 !== void 0 ? _3 : 0, targetOffset);
7577
- ringAnimation.arcOpen = approach((_4 = ringAnimation.arcOpen) !== null && _4 !== void 0 ? _4 : 0, targetOpen);
8429
+ ringAnimation.arcOffset = approach((_4 = ringAnimation.arcOffset) !== null && _4 !== void 0 ? _4 : 0, targetOffset);
8430
+ ringAnimation.arcOpen = approach((_5 = ringAnimation.arcOpen) !== null && _5 !== void 0 ? _5 : 0, targetOpen);
7578
8431
  ringAnimation.arcTime = now;
7579
8432
  if (ringAnimation.arcOffset !== targetOffset || ringAnimation.arcOpen !== targetOpen) {
7580
8433
  this.viewer.scene.requestRender();
@@ -7600,6 +8453,12 @@ var EntityLabel;
7600
8453
  element.style.zIndex = String(placement.zIndex);
7601
8454
  };
7602
8455
  draw(lineEle, startX, startY, endX, endY);
8456
+ if (layer.modelBoxes.Hull(this.id)) {
8457
+ layer.modelBoxes.Request(this.id, {
8458
+ whole: Boolean(cursorMotion.target), from: { x: frame.anchorX, y: frame.anchorY },
8459
+ opacity, zIndex: placement.zIndex, color: this.options.leaderColor || "rgba(255, 255, 255, 0.8)", duration
8460
+ });
8461
+ }
7603
8462
  if (runEle) {
7604
8463
  // Overlap the corner by the stem's own width so the elbow has no notch in it.
7605
8464
  const overlap = frame.anchorX < runToX ? -1 : 1;
@@ -7612,6 +8471,7 @@ var EntityLabel;
7612
8471
  ele.style.display = "flex";
7613
8472
  ele.style.visibility = "hidden";
7614
8473
  this.renderRemoval = layer.Add({ project, apply });
8474
+ leaveLayer === null || leaveLayer === void 0 ? void 0 : leaveLayer();
7615
8475
  // If terrain changes then we'll re-call createLabel as the position may have changed.
7616
8476
  let terrTimeout = null;
7617
8477
  this.terrChangedRemoval = this.viewer.scene.terrainProviderChanged.addEventListener(() => {
@@ -7757,7 +8617,7 @@ var EntityLabel;
7757
8617
  // The glyph plus the chip's padding of a third either side, enlarged for a photo.
7758
8618
  const diameter = (size + 2 * (size / 3)) * PHOTO_SIZE_SCALE;
7759
8619
  // Enough pixels to stay sharp at the hover size.
7760
- const pixels = Math.ceil(diameter * PHOTO_HOVER_SCALE * (window.devicePixelRatio || 1));
8620
+ const pixels = photoPixels(size, true);
7761
8621
  const canvas = document.createElement("canvas");
7762
8622
  canvas.className = "entity-label-photo";
7763
8623
  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);";
@@ -7768,13 +8628,15 @@ var EntityLabel;
7768
8628
  // Resting size, set once the photo's shape is known.
7769
8629
  let width = 0;
7770
8630
  let height = 0;
8631
+ // How far hovering grows it, which a small image limits.
8632
+ let hoverScale = PHOTO_HOVER_SCALE;
7771
8633
  let grown = false;
7772
8634
  const grow = (on) => {
7773
8635
  if (on === grown || !width) {
7774
8636
  return;
7775
8637
  }
7776
8638
  grown = on;
7777
- const scale = on ? PHOTO_HOVER_SCALE : 1;
8639
+ const scale = on ? hoverScale : 1;
7778
8640
  canvas.style.width = `${width * scale}px`;
7779
8641
  canvas.style.height = `${height * scale}px`;
7780
8642
  this.renderFor(PHOTO_TRANSITION);
@@ -7792,11 +8654,15 @@ var EntityLabel;
7792
8654
  (_a = canvas.getContext("2d")) === null || _a === void 0 ? void 0 : _a.drawImage(photo.source, 0, 0);
7793
8655
  const longest = Math.max(photo.width, photo.height);
7794
8656
  const circle = PhotoShape(photo.width, photo.height) === "circle";
7795
- width = circle ? diameter : diameter * photo.width / longest;
7796
- height = circle ? diameter : diameter * photo.height / longest;
8657
+ // Shown no larger than the image has pixels for, one to a CSS pixel, and never smaller than the icon.
8658
+ const native = circle ? Math.min(photo.width, photo.height) : longest;
8659
+ const across = Math.min(diameter, Math.max(size, native));
8660
+ hoverScale = Math.max(1, Math.min(PHOTO_HOVER_SCALE, native / across));
8661
+ width = circle ? across : across * photo.width / longest;
8662
+ height = circle ? across : across * photo.height / longest;
7797
8663
  canvas.style.width = `${width}px`;
7798
8664
  canvas.style.height = `${height}px`;
7799
- canvas.style.borderRadius = circle ? "50%" : `${diameter * PHOTO_CORNER}px`;
8665
+ canvas.style.borderRadius = circle ? "50%" : `${across * PHOTO_CORNER}px`;
7800
8666
  canvas.style.opacity = "1";
7801
8667
  placePhoto === null || placePhoto === void 0 ? void 0 : placePhoto(circle ? "circle" : "rect", grow);
7802
8668
  return true;
@@ -7843,6 +8709,57 @@ var EntityLabel;
7843
8709
  };
7844
8710
  step();
7845
8711
  }
8712
+ /*
8713
+ * Reads the record's geometry box, preferring an entity a label group already hydrated.
8714
+ */
8715
+ async loadModelBox() {
8716
+ var _a, _b, _c, _d;
8717
+ 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;
8718
+ let entity = ((_d = known === null || known === void 0 ? void 0 : known.Bruce) === null || _d === void 0 ? void 0 : _d.GeometryPivotExtents) ? known : null;
8719
+ if (!entity && this.api) {
8720
+ try {
8721
+ entity = (await Entity$1.Get({ api: this.api, entityId: this.rego.entityId, expandLocation: true, migrated: true })).entity;
8722
+ }
8723
+ // Expanding location fails for entities outside an assembly, which have no box to give.
8724
+ catch {
8725
+ return null;
8726
+ }
8727
+ }
8728
+ // A file uploaded before the API stored oriented boxes has only the record's axis-aligned extents.
8729
+ return GetModelBox(entity, ReadOrientedBox(entity));
8730
+ }
8731
+ /*
8732
+ * Eases the stem between the ring and the model box, returning how far onto the box it is this frame.
8733
+ */
8734
+ morphToBox(boxed, duration) {
8735
+ const morph = this.boxMorph;
8736
+ const now = Date.now();
8737
+ const ease = (progress) => progress * progress * (3 - 2 * progress);
8738
+ const progress = (start) => duration > 0 ? Math.min(1, Math.max(0, (now - start) / duration)) : 1;
8739
+ const hadBox = morph.target === 1;
8740
+ const target = boxed ? 1 : 0;
8741
+ if (morph.target !== target) {
8742
+ Object.assign(morph, { from: morph.value, target, start: now });
8743
+ }
8744
+ morph.value = morph.from + (morph.target - morph.from) * ease(progress(morph.start));
8745
+ // Settled, so a later frame with a longer duration does not replay the ease.
8746
+ if (morph.value === morph.target) {
8747
+ morph.from = morph.target;
8748
+ }
8749
+ if (boxed) {
8750
+ // Moving to another outline, such as a hovered part pulled out of its group, eases the stem across.
8751
+ if (hadBox && morph.anchor && morph.group !== boxed.group) {
8752
+ morph.shift = { x: morph.anchor.x - boxed.anchor.x, y: morph.anchor.y - boxed.anchor.y, start: now };
8753
+ }
8754
+ morph.group = boxed.group;
8755
+ const remaining = 1 - ease(progress(morph.shift.start));
8756
+ morph.anchor = { x: boxed.anchor.x + morph.shift.x * remaining, y: boxed.anchor.y + morph.shift.y * remaining };
8757
+ }
8758
+ if (morph.value !== morph.target || (boxed && progress(morph.shift.start) < 1)) {
8759
+ this.viewer.scene.requestRender();
8760
+ }
8761
+ return morph.value;
8762
+ }
7846
8763
  /**
7847
8764
  * Removes label from the viewer and unsubscribes from events.
7848
8765
  */
@@ -12910,6 +13827,13 @@ var VisualsRegister;
12910
13827
  this.SyncLabelHover();
12911
13828
  this.viewer.scene.requestRender();
12912
13829
  }
13830
+ /*
13831
+ * Whether the label hover is on this Entity, with or without a cursor position.
13832
+ */
13833
+ IsLabelHovered(entityId, menuItemId) {
13834
+ const hover = this.labelHover;
13835
+ return Boolean(entityId) && (hover === null || hover === void 0 ? void 0 : hover.entityId) === entityId && (hover.menuItemId == null || hover.menuItemId === menuItemId);
13836
+ }
12913
13837
  GetLabelHoverPosition(entityId, menuItemId) {
12914
13838
  const hover = this.labelHover;
12915
13839
  return (hover === null || hover === void 0 ? void 0 : hover.entityId) === entityId && (hover.menuItemId == null || hover.menuItemId === menuItemId) ? hover.position : undefined;
@@ -44205,7 +45129,7 @@ const createImageBillboard = (url) => {
44205
45129
  * @param url eg: https://demoplantprocess.api.nextspace-uat.net/file/4c29bdf8-1f0b-4739-b0ee-421ea07ba4e5.png
44206
45130
  */
44207
45131
  function extractMetadataFromFileUrl(url) {
44208
- var _a;
45132
+ var _a, _b;
44209
45133
  const lowercased = url.toLowerCase();
44210
45134
  // Bzzt, invalid url format for our purposes.
44211
45135
  if (!lowercased.includes(".api.") || !lowercased.includes("/file/")) {
@@ -44226,13 +45150,8 @@ function extractMetadataFromFileUrl(url) {
44226
45150
  const host = parts[0];
44227
45151
  const hostParts = host.split(".");
44228
45152
  const accountId = hostParts[0];
44229
- let fileId = (_a = parts[2]) !== null && _a !== void 0 ? _a : "";
44230
- if (fileId.includes("?")) {
44231
- fileId = fileId.split("?")[0];
44232
- }
44233
- if (fileId.includes(".")) {
44234
- fileId = fileId.split(".")[0];
44235
- }
45153
+ // Follows the "/file/" route rather than a fixed position, since "RV:" and "S:" segments can come before it.
45154
+ const fileId = (_b = (_a = url.match(/\/file\/([^/?#.]+)/)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : "";
44236
45155
  if (!fileId || !accountId) {
44237
45156
  return null;
44238
45157
  }
@@ -44597,7 +45516,8 @@ var EntityRenderEngineModel3d;
44597
45516
  updateShouldShowTrack();
44598
45517
  prepareExistingGraphic(cEntity, shouldShowTrack ? 1 : 0);
44599
45518
  const currentUri = GetCValue(params.viewer, cEntity.model.uri);
44600
- if (currentUri != params.lodUrl) {
45519
+ // Signature-blind, so a model is not reloaded just because the API signed its URL again.
45520
+ if (UrlUtils.RemoveSignature(currentUri) != UrlUtils.RemoveSignature(params.lodUrl)) {
44601
45521
  cEntity.model.uri = new ConstantProperty(params.lodUrl);
44602
45522
  }
44603
45523
  cEntity.model.heightReference = new ConstantProperty(heightRef);
@@ -44794,7 +45714,7 @@ var EntityRenderEngineModel3d;
44794
45714
  });
44795
45715
  cEntity.show = true;
44796
45716
  }
44797
- const fileRadiusKey = `model3d_${params.lodUrl}_${scale * styleScale}_radius`;
45717
+ const fileRadiusKey = `model3d_${UrlUtils.RemoveSignature(params.lodUrl)}_${scale * styleScale}_radius`;
44798
45718
  let heightProm = _fileRadiusCache.Get(fileRadiusKey);
44799
45719
  if (!heightProm) {
44800
45720
  heightProm = new Promise(async (res) => {
@@ -51812,7 +52732,7 @@ var StyleUtils;
51812
52732
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
51813
52733
  })(StyleUtils || (StyleUtils = {}));
51814
52734
 
51815
- const VERSION = "7.5.6";
52735
+ const VERSION = "7.5.8";
51816
52736
  /**
51817
52737
  * Updates the environment instance used by bruce-cesium to one specified.
51818
52738
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.
@@ -51829,5 +52749,5 @@ const getENVIRONMENT = () => {
51829
52749
  return ENVIRONMENT;
51830
52750
  };
51831
52751
 
51832
- export { VERSION, setENVIRONMENT, getENVIRONMENT, ClusterItems, PointClustering, PointClusterSize, CesiumAnimatedInOut, DisplacedSurfacePrimitive, CesiumAnimatedProperty, isStyleChanged, isOutlineChanged$1 as isOutlineChanged, StyleEffective, EntityRenderEngine, EntityRenderEngineModel3d, EntityRenderEnginePoint, EntityRenderEnginePolygon, EntityRenderEnginePolyline, TextureFrameSeriesAnimator, TextureValueLabels, MenuItemCreator, MenuItemManager, CesiumParabola, EntityLabel, EntityLabelGroup, LiveCursor, SharedGetters, DataSourceStaticKmlManager, DataLabRenderManager, EntitiesIdsRenderManager, EntitiesLoadedRenderManager, EntitiesRenderManager, EntityRenderManager, AssemblyRenderManager, GoogleSearchRenderManager, RelationsRenderManager, RenderManager, TilesetArbRenderManager, TilesetCadRenderManager, TilesetEntitiesRenderManager, TilesetGooglePhotosRenderManager, TilesetOsmRenderManager, TilesetPointcloudRenderManager, TileRenderEngine, createTileset, TilesetRenderEngine, ViewRenderEngine, VisualRegisterCuller, VisualsRegister, XGridsRenderEngine, CesiumEntityStyler, DrawingUtils, EntityUtils, MeasureUtils, StyleUtils, CESIUM_INSPECTOR_KEY, CESIUM_TIMELINE_KEY, CESIUM_TIMELINE_LIVE_KEY, CESIUM_TIMELINE_LIVE_PADDING_KEY, CESIUM_TIMELINE_INTERVAL_KEY, CESIUM_MODEL_SPACE_KEY, DEFAULT_LIVE_PADDING_SECONDS, DEFAULT_BRIGHTNESS_SHIFT, DEFAULT_LIGHT_INTENSITY, LIGHTING_SETTINGS_VERSION, LIGHT_INTENSITY_COMPENSATION, MAX_COMPENSATED_LIGHT_INTENSITY, ViewUtils, CesiumViewMonitor, ViewGroundArea, ViewerEventTracker, ViewerUtils, Draw3dPolygon, Draw3dPolyline, MeasureCreator, Walkthrough, WidgetControlViewBar, WidgetControlViewBarSearch, WidgetLeftPanelTab, WidgetLeftPanelTabBookmarks, WidgetLeftPanelTabDashboard, EMenuItemState, WidgetLeftPanelTabMenuItems, Widget, VIEWER_BOOKMARKS_WIDGET_KEY, WidgetBookmarks, WidgetBranding, ECursor, WidgetCursorBar, WidgetEmbeddedInfoView, WidgetInfoView, VIEWER_LEFT_PANEL_WIDGET_KEY, VIEWER_LEFT_PANEL_CSS_VAR_LEFT, CreateTabIconSvg, WidgetLeftPanel, WidgetNavCompass$$1 as WidgetNavCompass, VIEWER_VIEW_BAR_WIDGET_KEY, WidgetViewBar };
52752
+ export { VERSION, setENVIRONMENT, getENVIRONMENT, ClusterItems, PointClustering, PointClusterSize, CesiumAnimatedInOut, DisplacedSurfacePrimitive, CesiumAnimatedProperty, isStyleChanged, isOutlineChanged$1 as isOutlineChanged, StyleEffective, EntityRenderEngine, EntityRenderEngineModel3d, EntityRenderEnginePoint, extractMetadataFromFileUrl, EntityRenderEnginePolygon, EntityRenderEnginePolyline, TextureFrameSeriesAnimator, TextureValueLabels, MenuItemCreator, MenuItemManager, CesiumParabola, EntityLabel, EntityLabelGroup, LiveCursor, SharedGetters, DataSourceStaticKmlManager, DataLabRenderManager, EntitiesIdsRenderManager, EntitiesLoadedRenderManager, EntitiesRenderManager, EntityRenderManager, AssemblyRenderManager, GoogleSearchRenderManager, RelationsRenderManager, RenderManager, TilesetArbRenderManager, TilesetCadRenderManager, TilesetEntitiesRenderManager, TilesetGooglePhotosRenderManager, TilesetOsmRenderManager, TilesetPointcloudRenderManager, TileRenderEngine, createTileset, TilesetRenderEngine, ViewRenderEngine, VisualRegisterCuller, VisualsRegister, XGridsRenderEngine, CesiumEntityStyler, DrawingUtils, EntityUtils, MeasureUtils, StyleUtils, CESIUM_INSPECTOR_KEY, CESIUM_TIMELINE_KEY, CESIUM_TIMELINE_LIVE_KEY, CESIUM_TIMELINE_LIVE_PADDING_KEY, CESIUM_TIMELINE_INTERVAL_KEY, CESIUM_MODEL_SPACE_KEY, DEFAULT_LIVE_PADDING_SECONDS, DEFAULT_BRIGHTNESS_SHIFT, DEFAULT_LIGHT_INTENSITY, LIGHTING_SETTINGS_VERSION, LIGHT_INTENSITY_COMPENSATION, MAX_COMPENSATED_LIGHT_INTENSITY, ViewUtils, CesiumViewMonitor, ViewGroundArea, ViewerEventTracker, ViewerUtils, Draw3dPolygon, Draw3dPolyline, MeasureCreator, Walkthrough, WidgetControlViewBar, WidgetControlViewBarSearch, WidgetLeftPanelTab, WidgetLeftPanelTabBookmarks, WidgetLeftPanelTabDashboard, EMenuItemState, WidgetLeftPanelTabMenuItems, Widget, VIEWER_BOOKMARKS_WIDGET_KEY, WidgetBookmarks, WidgetBranding, ECursor, WidgetCursorBar, WidgetEmbeddedInfoView, WidgetInfoView, VIEWER_LEFT_PANEL_WIDGET_KEY, VIEWER_LEFT_PANEL_CSS_VAR_LEFT, CreateTabIconSvg, WidgetLeftPanel, WidgetNavCompass$$1 as WidgetNavCompass, VIEWER_VIEW_BAR_WIDGET_KEY, WidgetViewBar };
51833
52753
  //# sourceMappingURL=bruce-cesium.es5.js.map