bruce-cesium 7.5.7 → 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.
- package/dist/bruce-cesium.es5.js +983 -59
- package/dist/bruce-cesium.es5.js.map +1 -1
- package/dist/bruce-cesium.umd.js +981 -57
- package/dist/bruce-cesium.umd.js.map +1 -1
- package/dist/lib/bruce-cesium.js +1 -1
- package/dist/lib/rendering/render-managers/common/entity-label.js +973 -56
- package/dist/lib/rendering/render-managers/common/entity-label.js.map +1 -1
- package/dist/lib/rendering/visuals-register.js +7 -0
- package/dist/lib/rendering/visuals-register.js.map +1 -1
- package/dist/types/bruce-cesium.d.ts +1 -1
- package/dist/types/rendering/render-managers/common/entity-label.d.ts +134 -20
- package/dist/types/rendering/visuals-register.d.ts +1 -0
- package/package.json +2 -2
package/dist/bruce-cesium.umd.js
CHANGED
|
@@ -5530,6 +5530,24 @@
|
|
|
5530
5530
|
const PHOTO_CACHE_LIMIT = 500;
|
|
5531
5531
|
// Key where the viewer keeps its downsized photos.
|
|
5532
5532
|
const PHOTO_CACHE_KEY = "_nextspaceLabelPhotos";
|
|
5533
|
+
// Clearance around a model's box, as a share of its half-extent on each axis.
|
|
5534
|
+
const MODEL_BOX_PADDING = 0.02;
|
|
5535
|
+
// Share of each edge drawn out from its corners while the box is idle. Hovering draws the edges whole.
|
|
5536
|
+
const MODEL_BOX_BRACKET = 0.2;
|
|
5537
|
+
// Under this many pixels across, a box reads as a smudge and the ring takes over.
|
|
5538
|
+
const MODEL_BOX_MIN_SIZE = 24;
|
|
5539
|
+
// Stem length above the box outline when the options do not set one.
|
|
5540
|
+
const MODEL_BOX_STEM = 32;
|
|
5541
|
+
// Gap in pixels a waiting box needs from a drawn one before it shows, so neighbours never trade places.
|
|
5542
|
+
const MODEL_BOX_CLEARANCE = 12;
|
|
5543
|
+
// Longest a model's label stays hidden waiting to learn whether it has a box, before showing its ring anyway.
|
|
5544
|
+
const MODEL_BOX_WAIT = 1500;
|
|
5545
|
+
// Half the length of edge drawn either side of where a stem lands, when brackets would leave it bare.
|
|
5546
|
+
const MODEL_BOX_LANDING = 10;
|
|
5547
|
+
// Past this many touching boxes one box around them all stands in for tracing their union.
|
|
5548
|
+
const MODEL_BOX_UNION_LIMIT = 24;
|
|
5549
|
+
// Corner pairs of each box edge. Corner i sits on the +X side when bit 0 is set, +Y for bit 1, +Z for bit 2.
|
|
5550
|
+
const MODEL_BOX_EDGES = [[0, 1], [2, 3], [4, 5], [6, 7], [0, 2], [1, 3], [4, 6], [5, 7], [0, 4], [1, 5], [2, 6], [3, 7]];
|
|
5533
5551
|
// One per tinted icon, since an SVG mask is addressed by id across the whole document.
|
|
5534
5552
|
let tintCounter = 0;
|
|
5535
5553
|
// Key where the label instance is stored within the cesium entity.
|
|
@@ -5537,8 +5555,6 @@
|
|
|
5537
5555
|
EntityLabel.ATTACH_KEY = "nextspace-navigator-entity-label";
|
|
5538
5556
|
// Image types a label draws, read from the file extension. Anything else stays text.
|
|
5539
5557
|
const IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp"];
|
|
5540
|
-
// Shortest side a thumbnail needs before it is preferred over the full file.
|
|
5541
|
-
const IMAGE_THUMBNAIL_MIN = 64;
|
|
5542
5558
|
// Size a found image is drawn at when the group names none, so a photo never lands at its natural size.
|
|
5543
5559
|
const IMAGE_SIZE = 24;
|
|
5544
5560
|
function isImageFile(name) {
|
|
@@ -5567,12 +5583,24 @@
|
|
|
5567
5583
|
return (url.protocol === "http:" || url.protocol === "https:") && isImageFile(url.pathname);
|
|
5568
5584
|
}
|
|
5569
5585
|
EntityLabel.IsImageUrl = IsImageUrl;
|
|
5586
|
+
/*
|
|
5587
|
+
* Device pixels across a photo needs to stay sharp at a chip size, at rest or grown on hover.
|
|
5588
|
+
*/
|
|
5589
|
+
function photoPixels(size, hover) {
|
|
5590
|
+
// The glyph plus the chip's padding of a third either side, enlarged for a photo.
|
|
5591
|
+
const diameter = (size + 2 * (size / 3)) * PHOTO_SIZE_SCALE;
|
|
5592
|
+
// Picking an image can run with no window, where a screen pixel is a CSS pixel.
|
|
5593
|
+
const ratio = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1;
|
|
5594
|
+
return Math.ceil(diameter * (hover ? PHOTO_HOVER_SCALE : 1) * ratio);
|
|
5595
|
+
}
|
|
5570
5596
|
/*
|
|
5571
5597
|
* The entity's default image as InfoView picks it: its first photo attachment by display order that is an image file.
|
|
5572
|
-
*
|
|
5598
|
+
*
|
|
5599
|
+
* Reads attachments expanded onto the entity. Prefers the smallest thumbnail sharp at the hover size, then the
|
|
5600
|
+
* largest still sharp at rest, so a low resolution thumbnail is never stretched while the full file is unread.
|
|
5573
5601
|
*/
|
|
5574
5602
|
function GetDefaultImageUrl(entity, api) {
|
|
5575
|
-
var _a, _b, _c;
|
|
5603
|
+
var _a, _b, _c, _d;
|
|
5576
5604
|
const attachments = (_b = (_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.Attachment) !== null && _b !== void 0 ? _b : [];
|
|
5577
5605
|
const photo = attachments
|
|
5578
5606
|
.filter(x => (x === null || x === void 0 ? void 0 : x["EntityAttachmentType.ID"]) == BModels.EntityAttachmentType.EType.Photo && x.ClientFile)
|
|
@@ -5581,9 +5609,12 @@
|
|
|
5581
5609
|
if (!photo) {
|
|
5582
5610
|
return null;
|
|
5583
5611
|
}
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5612
|
+
// How far across a thumbnail is shown: a circle crops to its shortest side, a rectangle keeps its longest.
|
|
5613
|
+
const across = (x) => PhotoShape(x.Width, x.Height) === "circle" ? Math.min(x.Width, x.Height) : Math.max(x.Width, x.Height);
|
|
5614
|
+
const thumbnails = ((_c = photo.ClientFile.Thumbnails) !== null && _c !== void 0 ? _c : [])
|
|
5615
|
+
.filter(x => x.Width > 0 && x.Height > 0 && (x.URL || x["ClientFile.ID"]))
|
|
5616
|
+
.sort((a, b) => a.Width * a.Height - b.Width * b.Height);
|
|
5617
|
+
const thumbnail = (_d = thumbnails.find(x => across(x) >= photoPixels(IMAGE_SIZE, true))) !== null && _d !== void 0 ? _d : thumbnails.reverse().find(x => across(x) >= photoPixels(IMAGE_SIZE, false));
|
|
5587
5618
|
if (thumbnail) {
|
|
5588
5619
|
return thumbnail.URL || BModels.ClientFile.GetUrl({ fileId: thumbnail["ClientFile.ID"], api, viaCdn: true });
|
|
5589
5620
|
}
|
|
@@ -6095,6 +6126,7 @@
|
|
|
6095
6126
|
// Participants whose failure has been logged, so a label that throws every frame logs once.
|
|
6096
6127
|
this.failed = new WeakSet();
|
|
6097
6128
|
this.placementMemory = new PlacementMemory();
|
|
6129
|
+
this.modelBoxes = new ModelBoxes();
|
|
6098
6130
|
this.forwardingWheel = false;
|
|
6099
6131
|
this.pendingDrag = null;
|
|
6100
6132
|
// Null outside a 3D view.
|
|
@@ -6333,6 +6365,12 @@
|
|
|
6333
6365
|
x: (pointer.x - rect.left) * this.viewer.canvas.clientWidth / rect.width,
|
|
6334
6366
|
y: (pointer.y - rect.top) * this.viewer.canvas.clientHeight / rect.height
|
|
6335
6367
|
} : undefined;
|
|
6368
|
+
try {
|
|
6369
|
+
this.modelBoxes.Resolve(this.viewer, this.pointer);
|
|
6370
|
+
}
|
|
6371
|
+
catch (e) {
|
|
6372
|
+
console.error("EntityLabel: model boxes failed to resolve.", e);
|
|
6373
|
+
}
|
|
6336
6374
|
// This runs inside Cesium's frame, where one throwing label would stop rendering for the whole scene.
|
|
6337
6375
|
const projected = new Map();
|
|
6338
6376
|
for (const participant of this.participants) {
|
|
@@ -6354,6 +6392,12 @@
|
|
|
6354
6392
|
this.logFailure(participant, e);
|
|
6355
6393
|
}
|
|
6356
6394
|
}
|
|
6395
|
+
try {
|
|
6396
|
+
this.modelBoxes.Draw(this.viewer, this.element, this.coordinates);
|
|
6397
|
+
}
|
|
6398
|
+
catch (e) {
|
|
6399
|
+
console.error("EntityLabel: model boxes failed to draw.", e);
|
|
6400
|
+
}
|
|
6357
6401
|
}
|
|
6358
6402
|
logFailure(participant, error) {
|
|
6359
6403
|
if (this.failed.has(participant)) {
|
|
@@ -6364,6 +6408,728 @@
|
|
|
6364
6408
|
}
|
|
6365
6409
|
}
|
|
6366
6410
|
let nextLabelId = 0;
|
|
6411
|
+
/*
|
|
6412
|
+
* The record's oriented box in the same Z-up local space as GeometryPivot, or null when it has none.
|
|
6413
|
+
*/
|
|
6414
|
+
function ReadOrientedBox(entity) {
|
|
6415
|
+
var _a, _b;
|
|
6416
|
+
const box = (_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.GeometryOrientedBox;
|
|
6417
|
+
const vector = (value) => (value === null || value === void 0 ? void 0 : value.length) === 3 && value.every((item) => Number.isFinite(+item)) ? value.map(Number) : null;
|
|
6418
|
+
const centre = vector(box === null || box === void 0 ? void 0 : box.Centre), half = vector(box === null || box === void 0 ? void 0 : box.HalfExtents);
|
|
6419
|
+
const axes = ((_b = box === null || box === void 0 ? void 0 : box.Axes) === null || _b === void 0 ? void 0 : _b.length) === 3 ? box.Axes.map(vector) : null;
|
|
6420
|
+
return centre && half && (axes === null || axes === void 0 ? void 0 : axes.every(Boolean)) ? { centre, axes, half } : null;
|
|
6421
|
+
}
|
|
6422
|
+
EntityLabel.ReadOrientedBox = ReadOrientedBox;
|
|
6423
|
+
/*
|
|
6424
|
+
* The eight padded world corners of an assembly Entity's geometry box, or null when the record cannot give one.
|
|
6425
|
+
*
|
|
6426
|
+
* GeometryPivotExtents are Z-up half-extents about GeometryPivot, and the placement is the one the API uses to put
|
|
6427
|
+
* Bruce.Location at the pivot: ENU(root) * UCS translation * UCS rotation * AssemblyWorldPosition.
|
|
6428
|
+
*/
|
|
6429
|
+
function GetModelBox(entity, shape) {
|
|
6430
|
+
var _a, _b, _c;
|
|
6431
|
+
const bruce = entity === null || entity === void 0 ? void 0 : entity.Bruce;
|
|
6432
|
+
const half = (_b = (_a = bruce === null || bruce === void 0 ? void 0 : bruce.GeometryPivotExtents) === null || _a === void 0 ? void 0 : _a.map) === null || _b === void 0 ? void 0 : _b.call(_a, Number);
|
|
6433
|
+
const matrix = bruce === null || bruce === void 0 ? void 0 : bruce.AssemblyWorldPosition;
|
|
6434
|
+
const root = bruce === null || bruce === void 0 ? void 0 : bruce.AssemblyRootLocation;
|
|
6435
|
+
if ((half === null || half === void 0 ? void 0 : half.length) !== 3 || !half.every(value => Number.isFinite(value) && value >= 0) || Math.max(...half) < 0.01) {
|
|
6436
|
+
return null;
|
|
6437
|
+
}
|
|
6438
|
+
if ((matrix === null || matrix === void 0 ? void 0 : matrix.length) !== 4 || !matrix.every(row => (row === null || row === void 0 ? void 0 : row.length) === 4) || !Number.isFinite(+(root === null || root === void 0 ? void 0 : root.latitude)) || !Number.isFinite(+(root === null || root === void 0 ? void 0 : root.longitude))) {
|
|
6439
|
+
return null;
|
|
6440
|
+
}
|
|
6441
|
+
const pivot = ((_c = bruce.GeometryPivot) === null || _c === void 0 ? void 0 : _c.length) >= 3 ? bruce.GeometryPivot.slice(0, 3).map(Number) : [0, 0, 0];
|
|
6442
|
+
// Without an oriented box the record's own extents are an axis-aligned box about the pivot.
|
|
6443
|
+
const box = shape || { centre: pivot, axes: [[1, 0, 0], [0, 1, 0], [0, 0, 1]], half: half };
|
|
6444
|
+
const transform = bruce.AssemblyWorldTransform;
|
|
6445
|
+
const placement = Cesium.Transforms.eastNorthUpToFixedFrame(Cesium.Cartesian3.fromDegrees(+root.longitude, +root.latitude, EnsureNumber(root.altitude)));
|
|
6446
|
+
Cesium.Matrix4.multiply(placement, Cesium.Matrix4.fromTranslation(new Cesium.Cartesian3(EnsureNumber(transform === null || transform === void 0 ? void 0 : transform.x), EnsureNumber(transform === null || transform === void 0 ? void 0 : transform.y), EnsureNumber(transform === null || transform === void 0 ? void 0 : transform.z))), placement);
|
|
6447
|
+
Cesium.Matrix4.multiply(placement, Cesium.Matrix4.fromRotation(Cesium.Matrix3.fromHeadingPitchRoll(Cesium.HeadingPitchRoll.fromDegrees(EnsureNumber(transform === null || transform === void 0 ? void 0 : transform.heading), EnsureNumber(transform === null || transform === void 0 ? void 0 : transform.pitch), EnsureNumber(transform === null || transform === void 0 ? void 0 : transform.roll)))), placement);
|
|
6448
|
+
Cesium.Matrix4.multiply(placement, new Cesium.Matrix4(...matrix.flat().map(Number)), placement);
|
|
6449
|
+
// Pad thin axes by a share of the longest one, so a flat part still gets a visible gap.
|
|
6450
|
+
const longest = Math.max(...box.half);
|
|
6451
|
+
const reach = box.half.map(value => value + Math.max(value, longest * 0.25) * MODEL_BOX_PADDING);
|
|
6452
|
+
const corners = [];
|
|
6453
|
+
for (let i = 0; i < 8; i++) {
|
|
6454
|
+
const signs = [i & 1 ? 1 : -1, i & 2 ? 1 : -1, i & 4 ? 1 : -1];
|
|
6455
|
+
const local = new Cesium.Cartesian3(...[0, 1, 2].map(k => box.centre[k] +
|
|
6456
|
+
signs[0] * reach[0] * box.axes[0][k] + signs[1] * reach[1] * box.axes[1][k] + signs[2] * reach[2] * box.axes[2][k]));
|
|
6457
|
+
const corner = Cesium.Matrix4.multiplyByPoint(placement, local, new Cesium.Cartesian3());
|
|
6458
|
+
if (![corner.x, corner.y, corner.z].every(Number.isFinite)) {
|
|
6459
|
+
return null;
|
|
6460
|
+
}
|
|
6461
|
+
corners.push(corner);
|
|
6462
|
+
}
|
|
6463
|
+
return corners;
|
|
6464
|
+
}
|
|
6465
|
+
EntityLabel.GetModelBox = GetModelBox;
|
|
6466
|
+
const vAdd = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
6467
|
+
const vSub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
6468
|
+
const vScale = (a, s) => [a[0] * s, a[1] * s, a[2] * s];
|
|
6469
|
+
const vDot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
6470
|
+
const vCross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
6471
|
+
const vLength = (a) => Math.hypot(a[0], a[1], a[2]);
|
|
6472
|
+
/*
|
|
6473
|
+
* Reads a solid back from the eight corners GetModelBox gives, relative to the origin.
|
|
6474
|
+
*/
|
|
6475
|
+
function ModelSolid(corners, origin) {
|
|
6476
|
+
const local = corners.map((corner) => [corner.x - origin.x, corner.y - origin.y, corner.z - origin.z]);
|
|
6477
|
+
const spans = [1, 2, 4].map(bit => vSub(local[bit], local[0]));
|
|
6478
|
+
const half = spans.map(span => vLength(span) / 2);
|
|
6479
|
+
return {
|
|
6480
|
+
centre: vScale(vAdd(local[0], local[7]), 0.5),
|
|
6481
|
+
axes: spans.map((span, k) => vScale(span, 0.5 / (half[k] || 1))),
|
|
6482
|
+
half
|
|
6483
|
+
};
|
|
6484
|
+
}
|
|
6485
|
+
EntityLabel.ModelSolid = ModelSolid;
|
|
6486
|
+
function solidCorners(solid) {
|
|
6487
|
+
const corners = [];
|
|
6488
|
+
for (let i = 0; i < 8; i++) {
|
|
6489
|
+
corners.push([0, 1, 2].reduce((point, k) => vAdd(point, vScale(solid.axes[k], solid.half[k] * (i & (1 << k) ? 1 : -1))), solid.centre));
|
|
6490
|
+
}
|
|
6491
|
+
return corners;
|
|
6492
|
+
}
|
|
6493
|
+
function solidContains(solid, point, inset = 0) {
|
|
6494
|
+
const offset = vSub(point, solid.centre);
|
|
6495
|
+
return solid.axes.every((axis, k) => Math.abs(vDot(offset, axis)) < solid.half[k] - inset);
|
|
6496
|
+
}
|
|
6497
|
+
/*
|
|
6498
|
+
* Whether two solids overlap, by the separating axis test. Padding makes parts that sit against each other overlap.
|
|
6499
|
+
*/
|
|
6500
|
+
function ModelSolidsTouch(a, b) {
|
|
6501
|
+
const between = vSub(b.centre, a.centre);
|
|
6502
|
+
const axes = [...a.axes, ...b.axes];
|
|
6503
|
+
for (const first of a.axes) {
|
|
6504
|
+
for (const second of b.axes) {
|
|
6505
|
+
const axis = vCross(first, second);
|
|
6506
|
+
const length = vLength(axis);
|
|
6507
|
+
if (length > 1e-6)
|
|
6508
|
+
axes.push(vScale(axis, 1 / length));
|
|
6509
|
+
}
|
|
6510
|
+
}
|
|
6511
|
+
const reach = (solid, axis) => solid.axes.reduce((sum, own, k) => sum + solid.half[k] * Math.abs(vDot(own, axis)), 0);
|
|
6512
|
+
return axes.every(axis => Math.abs(vDot(between, axis)) <= reach(a, axis) + reach(b, axis));
|
|
6513
|
+
}
|
|
6514
|
+
EntityLabel.ModelSolidsTouch = ModelSolidsTouch;
|
|
6515
|
+
/*
|
|
6516
|
+
* One box around every solid, turned like the largest of them, for groups too big to trace.
|
|
6517
|
+
*/
|
|
6518
|
+
function encloseSolids(solids) {
|
|
6519
|
+
const largest = solids.reduce((best, solid) => solid.half[0] * solid.half[1] * solid.half[2] > best.half[0] * best.half[1] * best.half[2] ? solid : best);
|
|
6520
|
+
const corners = solids.flatMap(solidCorners);
|
|
6521
|
+
const ranges = largest.axes.map(axis => {
|
|
6522
|
+
const values = corners.map(corner => vDot(corner, axis));
|
|
6523
|
+
return [Math.min(...values), Math.max(...values)];
|
|
6524
|
+
});
|
|
6525
|
+
return {
|
|
6526
|
+
centre: largest.axes.reduce((point, axis, k) => vAdd(point, vScale(axis, (ranges[k][0] + ranges[k][1]) / 2)), [0, 0, 0]),
|
|
6527
|
+
axes: largest.axes,
|
|
6528
|
+
half: ranges.map(([low, high]) => (high - low) / 2)
|
|
6529
|
+
};
|
|
6530
|
+
}
|
|
6531
|
+
/*
|
|
6532
|
+
* Narrows [low, high] to where a + b * t stays within -half..half.
|
|
6533
|
+
*/
|
|
6534
|
+
function clipSlab(range, a, b, half) {
|
|
6535
|
+
if (Math.abs(b) < 1e-12) {
|
|
6536
|
+
if (Math.abs(a) > half)
|
|
6537
|
+
range[1] = -Infinity;
|
|
6538
|
+
return;
|
|
6539
|
+
}
|
|
6540
|
+
const first = (-half - a) / b, second = (half - a) / b;
|
|
6541
|
+
range[0] = Math.max(range[0], Math.min(first, second));
|
|
6542
|
+
range[1] = Math.min(range[1], Math.max(first, second));
|
|
6543
|
+
}
|
|
6544
|
+
/*
|
|
6545
|
+
* The edges of the union of the solids: where its surface folds, leaving out seams across flat shared faces.
|
|
6546
|
+
*/
|
|
6547
|
+
function ModelBoxOutline(solids) {
|
|
6548
|
+
const span = Math.max(...solids.map(solid => vLength(solid.half)));
|
|
6549
|
+
const thinnest = Math.min(...solids.flatMap(solid => solid.half));
|
|
6550
|
+
const radius = Math.max(1e-6, Math.min(thinnest * 0.05, span * 1e-3));
|
|
6551
|
+
const tolerance = Math.max(1, span) * 1e-6;
|
|
6552
|
+
const candidates = [];
|
|
6553
|
+
for (const solid of solids) {
|
|
6554
|
+
const corners = solidCorners(solid);
|
|
6555
|
+
for (const [a, b] of MODEL_BOX_EDGES)
|
|
6556
|
+
candidates.push([corners[a], corners[b]]);
|
|
6557
|
+
}
|
|
6558
|
+
const faces = (solid) => [0, 1, 2].flatMap(k => [1, -1].map(sign => ({ solid, k, normal: vScale(solid.axes[k], sign) })));
|
|
6559
|
+
for (let i = 0; i < solids.length; i++) {
|
|
6560
|
+
for (let j = i + 1; j < solids.length; j++) {
|
|
6561
|
+
if (!ModelSolidsTouch(solids[i], solids[j])) {
|
|
6562
|
+
continue;
|
|
6563
|
+
}
|
|
6564
|
+
for (const first of faces(solids[i])) {
|
|
6565
|
+
for (const second of faces(solids[j])) {
|
|
6566
|
+
const direction = vCross(first.normal, second.normal);
|
|
6567
|
+
const sine = vLength(direction);
|
|
6568
|
+
// Faces this close to parallel meet along edges the box edges already give.
|
|
6569
|
+
if (sine < 0.05) {
|
|
6570
|
+
continue;
|
|
6571
|
+
}
|
|
6572
|
+
const d1 = vDot(first.normal, first.solid.centre) + first.solid.half[first.k];
|
|
6573
|
+
const d2 = vDot(second.normal, second.solid.centre) + second.solid.half[second.k];
|
|
6574
|
+
const cosine = vDot(first.normal, second.normal);
|
|
6575
|
+
const base = vScale(vAdd(vScale(first.normal, d1 - d2 * cosine), vScale(second.normal, d2 - d1 * cosine)), 1 / (sine * sine));
|
|
6576
|
+
const unit = vScale(direction, 1 / sine);
|
|
6577
|
+
const range = [-Infinity, Infinity];
|
|
6578
|
+
for (const face of [first, second]) {
|
|
6579
|
+
const offset = vSub(base, face.solid.centre);
|
|
6580
|
+
for (let m = 0; m < 3; m++) {
|
|
6581
|
+
if (m !== face.k)
|
|
6582
|
+
clipSlab(range, vDot(offset, face.solid.axes[m]), vDot(unit, face.solid.axes[m]), face.solid.half[m]);
|
|
6583
|
+
}
|
|
6584
|
+
}
|
|
6585
|
+
if (range[1] - range[0] > tolerance) {
|
|
6586
|
+
candidates.push([vAdd(base, vScale(unit, range[0])), vAdd(base, vScale(unit, range[1]))]);
|
|
6587
|
+
}
|
|
6588
|
+
}
|
|
6589
|
+
}
|
|
6590
|
+
}
|
|
6591
|
+
}
|
|
6592
|
+
const inside = (point) => solids.some(solid => solidContains(solid, point));
|
|
6593
|
+
/*
|
|
6594
|
+
* Whether the union folds along a direction at a point, from which way round it the inside lies.
|
|
6595
|
+
*/
|
|
6596
|
+
const folds = (point, along) => {
|
|
6597
|
+
const first = vCross(along, Math.abs(along[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0]);
|
|
6598
|
+
const e1 = vScale(first, 1 / vLength(first));
|
|
6599
|
+
const e2 = vCross(along, e1);
|
|
6600
|
+
const samples = [];
|
|
6601
|
+
for (let i = 0; i < 16; i++) {
|
|
6602
|
+
// Offset from the axes so no sample lands exactly on a face through the point.
|
|
6603
|
+
const angle = (i + 0.37) * Math.PI / 8;
|
|
6604
|
+
samples.push(inside(vAdd(point, vAdd(vScale(e1, Math.cos(angle) * radius), vScale(e2, Math.sin(angle) * radius)))));
|
|
6605
|
+
}
|
|
6606
|
+
const filled = samples.filter(Boolean).length;
|
|
6607
|
+
const changes = samples.filter((sample, i) => sample !== samples[(i + 1) % 16]).length;
|
|
6608
|
+
return filled > 0 && filled < 16 && !(filled === 8 && changes === 2);
|
|
6609
|
+
};
|
|
6610
|
+
const lines = [];
|
|
6611
|
+
for (const [start, end] of candidates) {
|
|
6612
|
+
const delta = vSub(end, start);
|
|
6613
|
+
const length = vLength(delta);
|
|
6614
|
+
if (length < tolerance) {
|
|
6615
|
+
continue;
|
|
6616
|
+
}
|
|
6617
|
+
const along = vScale(delta, 1 / length);
|
|
6618
|
+
const cuts = [0, 1];
|
|
6619
|
+
for (const solid of solids) {
|
|
6620
|
+
const offset = vSub(start, solid.centre);
|
|
6621
|
+
solid.axes.forEach((axis, k) => {
|
|
6622
|
+
const rate = vDot(delta, axis);
|
|
6623
|
+
if (Math.abs(rate) < 1e-12)
|
|
6624
|
+
return;
|
|
6625
|
+
for (const bound of [-solid.half[k], solid.half[k]]) {
|
|
6626
|
+
const t = (bound - vDot(offset, axis)) / rate;
|
|
6627
|
+
if (t > 0 && t < 1)
|
|
6628
|
+
cuts.push(t);
|
|
6629
|
+
}
|
|
6630
|
+
});
|
|
6631
|
+
}
|
|
6632
|
+
cuts.sort((a, b) => a - b);
|
|
6633
|
+
for (let i = 0; i + 1 < cuts.length; i++) {
|
|
6634
|
+
if ((cuts[i + 1] - cuts[i]) * length < tolerance || !folds(vAdd(start, vScale(delta, (cuts[i] + cuts[i + 1]) / 2)), along)) {
|
|
6635
|
+
continue;
|
|
6636
|
+
}
|
|
6637
|
+
// Merged by line, so a shared edge draws once and a run cut by other boxes is one stroke.
|
|
6638
|
+
const from = vAdd(start, vScale(delta, cuts[i])), to = vAdd(start, vScale(delta, cuts[i + 1]));
|
|
6639
|
+
let line = lines.find(item => vLength(vCross(item.direction, along)) < 1e-6 && vLength(vCross(vSub(from, item.origin), item.direction)) < tolerance);
|
|
6640
|
+
if (!line) {
|
|
6641
|
+
line = { origin: from, direction: along, spans: [] };
|
|
6642
|
+
lines.push(line);
|
|
6643
|
+
}
|
|
6644
|
+
const a = vDot(vSub(from, line.origin), line.direction), b = vDot(vSub(to, line.origin), line.direction);
|
|
6645
|
+
line.spans.push([Math.min(a, b), Math.max(a, b)]);
|
|
6646
|
+
}
|
|
6647
|
+
}
|
|
6648
|
+
const outline = [];
|
|
6649
|
+
for (const line of lines) {
|
|
6650
|
+
line.spans.sort((a, b) => a[0] - b[0]);
|
|
6651
|
+
let current = line.spans[0];
|
|
6652
|
+
for (const next of [...line.spans.slice(1), null]) {
|
|
6653
|
+
if (next && next[0] <= current[1] + tolerance) {
|
|
6654
|
+
current = [current[0], Math.max(current[1], next[1])];
|
|
6655
|
+
continue;
|
|
6656
|
+
}
|
|
6657
|
+
outline.push([vAdd(line.origin, vScale(line.direction, current[0])), vAdd(line.origin, vScale(line.direction, current[1]))]);
|
|
6658
|
+
current = next;
|
|
6659
|
+
}
|
|
6660
|
+
}
|
|
6661
|
+
return outline;
|
|
6662
|
+
}
|
|
6663
|
+
EntityLabel.ModelBoxOutline = ModelBoxOutline;
|
|
6664
|
+
/*
|
|
6665
|
+
* Whether the sight line from the eye to a point passes through any solid before reaching it.
|
|
6666
|
+
*/
|
|
6667
|
+
function solidsHide(solids, eye, point, inset) {
|
|
6668
|
+
const ray = vSub(point, eye);
|
|
6669
|
+
const length = vLength(ray);
|
|
6670
|
+
// Stop just short of the point so the face it lies on does not count against it.
|
|
6671
|
+
const reach = Math.max(0, 1 - inset * 100 / (length || 1));
|
|
6672
|
+
return solids.some(solid => {
|
|
6673
|
+
const offset = vSub(eye, solid.centre);
|
|
6674
|
+
const range = [0, reach];
|
|
6675
|
+
solid.axes.forEach((axis, k) => clipSlab(range, vDot(offset, axis), vDot(ray, axis), solid.half[k] - inset));
|
|
6676
|
+
return range[0] < range[1];
|
|
6677
|
+
});
|
|
6678
|
+
}
|
|
6679
|
+
/*
|
|
6680
|
+
* Window positions of a box's corners, or null when one is behind the camera and the box would draw inside out.
|
|
6681
|
+
*/
|
|
6682
|
+
function projectCorners(viewer, corners) {
|
|
6683
|
+
const camera = viewer.camera;
|
|
6684
|
+
const cameraPosition = camera.positionWC || camera.position;
|
|
6685
|
+
const direction = camera.directionWC || camera.direction;
|
|
6686
|
+
const transforms = Cesium.SceneTransforms;
|
|
6687
|
+
const projectPosition = transforms.wgs84ToWindowCoordinates || transforms.worldToWindowCoordinates;
|
|
6688
|
+
const points = [];
|
|
6689
|
+
const relative = new Cesium.Cartesian3();
|
|
6690
|
+
for (const corner of corners) {
|
|
6691
|
+
if (Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(corner, cameraPosition, relative), direction) <= 0) {
|
|
6692
|
+
return null;
|
|
6693
|
+
}
|
|
6694
|
+
const point = projectPosition(viewer.scene, corner);
|
|
6695
|
+
if (!Number.isFinite(point === null || point === void 0 ? void 0 : point.x) || !Number.isFinite(point === null || point === void 0 ? void 0 : point.y)) {
|
|
6696
|
+
return null;
|
|
6697
|
+
}
|
|
6698
|
+
points.push({ x: point.x, y: point.y });
|
|
6699
|
+
}
|
|
6700
|
+
return points;
|
|
6701
|
+
}
|
|
6702
|
+
/*
|
|
6703
|
+
* Whether two convex outlines come within a margin of each other, by the separating axis test.
|
|
6704
|
+
*/
|
|
6705
|
+
function HullsNear(a, b, margin) {
|
|
6706
|
+
for (const hull of [a, b]) {
|
|
6707
|
+
for (let i = 0; i < hull.length; i++) {
|
|
6708
|
+
const from = hull[i], to = hull[(i + 1) % hull.length];
|
|
6709
|
+
const length = Math.hypot(to.x - from.x, to.y - from.y);
|
|
6710
|
+
if (length < 1e-9) {
|
|
6711
|
+
continue;
|
|
6712
|
+
}
|
|
6713
|
+
const normal = { x: (from.y - to.y) / length, y: (to.x - from.x) / length };
|
|
6714
|
+
const project = (points) => points.map(point => point.x * normal.x + point.y * normal.y);
|
|
6715
|
+
const first = project(a), second = project(b);
|
|
6716
|
+
if (Math.max(Math.min(...second) - Math.max(...first), Math.min(...first) - Math.max(...second)) >= margin) {
|
|
6717
|
+
return false;
|
|
6718
|
+
}
|
|
6719
|
+
}
|
|
6720
|
+
}
|
|
6721
|
+
return true;
|
|
6722
|
+
}
|
|
6723
|
+
EntityLabel.HullsNear = HullsNear;
|
|
6724
|
+
/*
|
|
6725
|
+
* Decides which labels' model boxes are drawn and how, so a multi-selection does not stack box over box.
|
|
6726
|
+
*/
|
|
6727
|
+
class ModelBoxes {
|
|
6728
|
+
constructor() {
|
|
6729
|
+
// Whether a box competes for space comes only from inputs (camera distance, hover, the pointer), never from
|
|
6730
|
+
// how its label was laid out, so an animation moving a label cannot feed back into which boxes show.
|
|
6731
|
+
this.entries = new Map();
|
|
6732
|
+
this.groups = null;
|
|
6733
|
+
// The hovered Entity. Its box is drawn on its own and whole, ahead of every other box.
|
|
6734
|
+
this.hovered = null;
|
|
6735
|
+
this.outlines = new Map();
|
|
6736
|
+
this.hulls = new Map();
|
|
6737
|
+
this.frames = new Map();
|
|
6738
|
+
this.drawn = [];
|
|
6739
|
+
// When each label's box started showing; the longer it has shown, the stronger its claim on the space.
|
|
6740
|
+
this.since = new Map();
|
|
6741
|
+
this.requests = new Map();
|
|
6742
|
+
this.looks = new Map();
|
|
6743
|
+
this.memberLooks = new Map();
|
|
6744
|
+
}
|
|
6745
|
+
Set(id, corners, entityId, inputs) {
|
|
6746
|
+
const current = this.entries.get(id);
|
|
6747
|
+
// A label rebuilt over the same box keeps its place and how far it has animated.
|
|
6748
|
+
if (corners && (current === null || current === void 0 ? void 0 : current.corners) === corners && current.entityId === entityId) {
|
|
6749
|
+
Object.assign(current, inputs);
|
|
6750
|
+
return;
|
|
6751
|
+
}
|
|
6752
|
+
if (!corners && !current) {
|
|
6753
|
+
return;
|
|
6754
|
+
}
|
|
6755
|
+
if (corners) {
|
|
6756
|
+
this.entries.set(id, { ...inputs, corners, entityId, active: false, wanted: false });
|
|
6757
|
+
}
|
|
6758
|
+
else
|
|
6759
|
+
this.entries.delete(id);
|
|
6760
|
+
this.since.delete(id);
|
|
6761
|
+
this.groups = null;
|
|
6762
|
+
for (const [key, group] of this.outlines) {
|
|
6763
|
+
if (group.members.includes(id))
|
|
6764
|
+
this.outlines.delete(key);
|
|
6765
|
+
}
|
|
6766
|
+
}
|
|
6767
|
+
/*
|
|
6768
|
+
* The box drawn for a label this frame, or undefined when it has none or lost its place.
|
|
6769
|
+
*/
|
|
6770
|
+
Frame(id) {
|
|
6771
|
+
return this.frames.get(id);
|
|
6772
|
+
}
|
|
6773
|
+
/*
|
|
6774
|
+
* The label's own box on screen whether or not it is drawn, so hovering the model can ask for it.
|
|
6775
|
+
*/
|
|
6776
|
+
Hull(id) {
|
|
6777
|
+
return this.hulls.get(id);
|
|
6778
|
+
}
|
|
6779
|
+
Request(id, request) {
|
|
6780
|
+
this.requests.set(id, request);
|
|
6781
|
+
}
|
|
6782
|
+
group() {
|
|
6783
|
+
const ids = Array.from(this.entries.keys()).filter(id => this.entries.get(id).wanted).sort();
|
|
6784
|
+
if (!ids.length) {
|
|
6785
|
+
return [];
|
|
6786
|
+
}
|
|
6787
|
+
const origin = this.entries.get(ids[0]).corners[0];
|
|
6788
|
+
const solids = new Map(ids.map(id => [id, ModelSolid(this.entries.get(id).corners, origin)]));
|
|
6789
|
+
const parent = new Map(ids.map(id => [id, id]));
|
|
6790
|
+
const root = (id) => parent.get(id) === id ? id : root(parent.get(id));
|
|
6791
|
+
for (let i = 0; i < ids.length; i++) {
|
|
6792
|
+
const a = solids.get(ids[i]);
|
|
6793
|
+
for (let j = i + 1; j < ids.length; j++) {
|
|
6794
|
+
const b = solids.get(ids[j]);
|
|
6795
|
+
if ((this.entries.get(ids[i]).entityId === this.hovered) !== (this.entries.get(ids[j]).entityId === this.hovered)) {
|
|
6796
|
+
continue;
|
|
6797
|
+
}
|
|
6798
|
+
// Cheap sphere check first; most pairs in a scene are nowhere near each other.
|
|
6799
|
+
if (vLength(vSub(a.centre, b.centre)) > vLength(a.half) + vLength(b.half) || !ModelSolidsTouch(a, b)) {
|
|
6800
|
+
continue;
|
|
6801
|
+
}
|
|
6802
|
+
parent.set(root(ids[j]), root(ids[i]));
|
|
6803
|
+
}
|
|
6804
|
+
}
|
|
6805
|
+
const members = new Map();
|
|
6806
|
+
for (const id of ids) {
|
|
6807
|
+
const key = root(id);
|
|
6808
|
+
if (!members.has(key))
|
|
6809
|
+
members.set(key, []);
|
|
6810
|
+
members.get(key).push(id);
|
|
6811
|
+
}
|
|
6812
|
+
const groups = Array.from(members.values()).map(list => {
|
|
6813
|
+
const key = list.join("|");
|
|
6814
|
+
let group = this.outlines.get(key);
|
|
6815
|
+
if (!group) {
|
|
6816
|
+
const groupOrigin = this.entries.get(list[0]).corners[0];
|
|
6817
|
+
const parts = list.map(id => ModelSolid(this.entries.get(id).corners, groupOrigin));
|
|
6818
|
+
const solidsShown = parts.length > MODEL_BOX_UNION_LIMIT ? [encloseSolids(parts)] : parts;
|
|
6819
|
+
group = { key, members: list, hovered: false, origin: groupOrigin, solids: solidsShown, outline: ModelBoxOutline(solidsShown) };
|
|
6820
|
+
}
|
|
6821
|
+
group.hovered = this.entries.get(list[0]).entityId === this.hovered;
|
|
6822
|
+
return group;
|
|
6823
|
+
});
|
|
6824
|
+
// Kept across hover changes, so pulling a part out and putting it back does not trace the union again.
|
|
6825
|
+
const kept = Array.from(this.outlines.values()).filter(group => !groups.includes(group)).slice(-8);
|
|
6826
|
+
this.outlines = new Map([...kept, ...groups].map(group => [group.key, group]));
|
|
6827
|
+
return groups;
|
|
6828
|
+
}
|
|
6829
|
+
/*
|
|
6830
|
+
* Projects every box for this frame and settles which outlines are drawn, before labels place their stems.
|
|
6831
|
+
*/
|
|
6832
|
+
Resolve(viewer, pointer) {
|
|
6833
|
+
var _a, _b, _c, _d;
|
|
6834
|
+
this.frames.clear();
|
|
6835
|
+
this.hulls.clear();
|
|
6836
|
+
this.drawn = [];
|
|
6837
|
+
const points = new Map();
|
|
6838
|
+
for (const [id, entry] of this.entries) {
|
|
6839
|
+
const projected = projectCorners(viewer, entry.corners);
|
|
6840
|
+
if (projected) {
|
|
6841
|
+
points.set(id, projected);
|
|
6842
|
+
this.hulls.set(id, convexHull(projected));
|
|
6843
|
+
}
|
|
6844
|
+
}
|
|
6845
|
+
// The application's hover, which is what its temporary label shows, decides first. Without one the
|
|
6846
|
+
// pointer does: the part it is on holds until it leaves, else the smallest box under it wins.
|
|
6847
|
+
const under = Array.from(this.hulls.entries()).filter(([, hull]) => pointer && hullContains(hull, pointer));
|
|
6848
|
+
let hovered = (_b = (_a = Array.from(this.entries.values()).find(entry => { var _a; return (_a = entry.hovered) === null || _a === void 0 ? void 0 : _a.call(entry); })) === null || _a === void 0 ? void 0 : _a.entityId) !== null && _b !== void 0 ? _b : (under.some(([id]) => this.entries.get(id).entityId === this.hovered) ? this.hovered : null);
|
|
6849
|
+
if (!hovered && under.length) {
|
|
6850
|
+
hovered = this.entries.get(under.map(([id, hull]) => ({ id, area: hullArea(hull) })).sort((a, b) => a.area - b.area || a.id.localeCompare(b.id))[0].id).entityId;
|
|
6851
|
+
}
|
|
6852
|
+
if (hovered !== this.hovered) {
|
|
6853
|
+
this.hovered = hovered;
|
|
6854
|
+
this.groups = null;
|
|
6855
|
+
}
|
|
6856
|
+
for (const [id, entry] of this.entries) {
|
|
6857
|
+
const hull = this.hulls.get(id);
|
|
6858
|
+
entry.active = Boolean(((_c = entry.near) === null || _c === void 0 ? void 0 : _c.call(entry)) || entry.entityId === hovered || (pointer && hull && hullContains(hull, pointer)));
|
|
6859
|
+
// A box keeps its place while it fades out, and leaves the competition once it has.
|
|
6860
|
+
const wanted = entry.active || ((_d = this.memberLooks.get(id)) === null || _d === void 0 ? void 0 : _d.shown) > 0.01;
|
|
6861
|
+
if (wanted !== entry.wanted) {
|
|
6862
|
+
entry.wanted = wanted;
|
|
6863
|
+
this.groups = null;
|
|
6864
|
+
}
|
|
6865
|
+
}
|
|
6866
|
+
if (!this.groups) {
|
|
6867
|
+
this.groups = this.group();
|
|
6868
|
+
}
|
|
6869
|
+
const canvas = viewer.canvas;
|
|
6870
|
+
const candidates = [];
|
|
6871
|
+
for (const group of this.groups) {
|
|
6872
|
+
if (!group.members.every(id => points.has(id))) {
|
|
6873
|
+
continue;
|
|
6874
|
+
}
|
|
6875
|
+
const all = group.members.flatMap(id => points.get(id));
|
|
6876
|
+
const xs = all.map(point => point.x), ys = all.map(point => point.y);
|
|
6877
|
+
const width = Math.max(...xs) - Math.min(...xs), height = Math.max(...ys) - Math.min(...ys);
|
|
6878
|
+
if (Math.max(width, height) < MODEL_BOX_MIN_SIZE || width > canvas.clientWidth * 2 || height > canvas.clientHeight * 2) {
|
|
6879
|
+
continue;
|
|
6880
|
+
}
|
|
6881
|
+
const since = Math.min(...group.members.map(id => { var _a; return (_a = this.since.get(id)) !== null && _a !== void 0 ? _a : Infinity; }));
|
|
6882
|
+
const outline = convexHull(all);
|
|
6883
|
+
candidates.push({ group, outline, selected: group.members.some(id => { var _a, _b; return (_b = (_a = this.entries.get(id)).selected) === null || _b === void 0 ? void 0 : _b.call(_a); }), since, area: hullArea(outline) });
|
|
6884
|
+
}
|
|
6885
|
+
// Between boxes appearing together the larger on screen goes first, as the one the view is about.
|
|
6886
|
+
// What the user points at now outranks what they selected before it.
|
|
6887
|
+
candidates.sort((a, b) => (Number(b.group.hovered) - Number(a.group.hovered) ||
|
|
6888
|
+
Number(b.selected) - Number(a.selected) ||
|
|
6889
|
+
a.since - b.since ||
|
|
6890
|
+
b.area - a.area ||
|
|
6891
|
+
a.group.key.localeCompare(b.group.key)));
|
|
6892
|
+
const transforms = Cesium.SceneTransforms;
|
|
6893
|
+
const projectPosition = transforms.wgs84ToWindowCoordinates || transforms.worldToWindowCoordinates;
|
|
6894
|
+
const scratch = new Cesium.Cartesian3();
|
|
6895
|
+
const now = Date.now();
|
|
6896
|
+
const shown = new Set();
|
|
6897
|
+
for (const candidate of candidates) {
|
|
6898
|
+
// A showing box holds its place until truly overlapped; a waiting one needs clear space.
|
|
6899
|
+
const margin = Number.isFinite(candidate.since) ? 0 : MODEL_BOX_CLEARANCE;
|
|
6900
|
+
if (this.drawn.some(other => HullsNear(candidate.outline, other.outline, margin))) {
|
|
6901
|
+
continue;
|
|
6902
|
+
}
|
|
6903
|
+
const { origin } = candidate.group;
|
|
6904
|
+
const toScreen = (point) => {
|
|
6905
|
+
const projected = projectPosition(viewer.scene, Cesium.Cartesian3.fromElements(origin.x + point[0], origin.y + point[1], origin.z + point[2], scratch));
|
|
6906
|
+
return { x: projected.x, y: projected.y };
|
|
6907
|
+
};
|
|
6908
|
+
const edges = candidate.group.outline.map(([start, end]) => [toScreen(start), toScreen(end)]);
|
|
6909
|
+
const drawn = { ...candidate, edges, landings: [] };
|
|
6910
|
+
this.drawn.push(drawn);
|
|
6911
|
+
for (const id of candidate.group.members) {
|
|
6912
|
+
shown.add(id);
|
|
6913
|
+
if (!this.since.has(id))
|
|
6914
|
+
this.since.set(id, now);
|
|
6915
|
+
const own = points.get(id);
|
|
6916
|
+
// The silhouette's top is the highest drawn edge at that x, so the stem always meets a line.
|
|
6917
|
+
const x = (Math.min(...own.map(point => point.x)) + Math.max(...own.map(point => point.x))) / 2;
|
|
6918
|
+
let anchor = null, edge = -1;
|
|
6919
|
+
edges.forEach(([a, b], i) => {
|
|
6920
|
+
if (x < Math.min(a.x, b.x) || x > Math.max(a.x, b.x) || Math.abs(b.x - a.x) < 1e-6) {
|
|
6921
|
+
return;
|
|
6922
|
+
}
|
|
6923
|
+
const y = a.y + (b.y - a.y) * (x - a.x) / (b.x - a.x);
|
|
6924
|
+
if (!anchor || y < anchor.y) {
|
|
6925
|
+
anchor = { x, y };
|
|
6926
|
+
edge = i;
|
|
6927
|
+
}
|
|
6928
|
+
});
|
|
6929
|
+
anchor = anchor || { x, y: hullTop(candidate.outline, x) };
|
|
6930
|
+
if (edge >= 0)
|
|
6931
|
+
drawn.landings.push({ edge, point: anchor });
|
|
6932
|
+
this.frames.set(id, { points: own, hull: this.hulls.get(id), outline: candidate.outline, anchor, group: candidate.group.key });
|
|
6933
|
+
}
|
|
6934
|
+
}
|
|
6935
|
+
for (const id of Array.from(this.since.keys())) {
|
|
6936
|
+
if (!shown.has(id))
|
|
6937
|
+
this.since.delete(id);
|
|
6938
|
+
}
|
|
6939
|
+
this.requests.clear();
|
|
6940
|
+
}
|
|
6941
|
+
/*
|
|
6942
|
+
* Draws each settled outline as corner brackets that grow into whole edges, back edges dashed.
|
|
6943
|
+
*/
|
|
6944
|
+
Draw(viewer, element, coordinates) {
|
|
6945
|
+
var _a, _b, _c;
|
|
6946
|
+
const now = Date.now();
|
|
6947
|
+
const looks = new Map();
|
|
6948
|
+
const memberLooks = new Map();
|
|
6949
|
+
let animating = false;
|
|
6950
|
+
for (const { group } of this.drawn) {
|
|
6951
|
+
const requests = group.members.map(id => this.requests.get(id)).filter(Boolean);
|
|
6952
|
+
if (!requests.length) {
|
|
6953
|
+
// Its labels are hidden this frame, perhaps off screen; it keeps its look so it does not regrow.
|
|
6954
|
+
const kept = this.looks.get(group.key);
|
|
6955
|
+
if (kept) {
|
|
6956
|
+
looks.set(group.key, kept);
|
|
6957
|
+
group.members.forEach(id => memberLooks.set(id, kept));
|
|
6958
|
+
if (kept.svg)
|
|
6959
|
+
kept.svg.style.display = "none";
|
|
6960
|
+
}
|
|
6961
|
+
continue;
|
|
6962
|
+
}
|
|
6963
|
+
// A group that gained or lost a member carries on from where its members' outline was.
|
|
6964
|
+
const previous = group.members.map(id => this.memberLooks.get(id)).filter(Boolean);
|
|
6965
|
+
const look = (_a = this.looks.get(group.key)) !== null && _a !== void 0 ? _a : {
|
|
6966
|
+
shown: Math.max(0, ...previous.map(item => item.shown)), whole: Math.max(0, ...previous.map(item => item.whole)),
|
|
6967
|
+
grow: Math.max(0, ...previous.map(item => item.grow)), time: now, svg: (_b = previous.find(item => item.svg)) === null || _b === void 0 ? void 0 : _b.svg
|
|
6968
|
+
};
|
|
6969
|
+
const elapsed = Math.min(32, Math.max(0, now - look.time));
|
|
6970
|
+
const duration = Math.min(...requests.map(request => request.duration));
|
|
6971
|
+
const blend = duration > 0 ? 1 - Math.exp(-elapsed / (duration / 3)) : 1;
|
|
6972
|
+
const approach = (value, to) => Math.abs(value - to) < 0.001 ? to : value + (to - value) * blend;
|
|
6973
|
+
const shownTarget = group.members.some(id => { var _a; return (_a = this.entries.get(id)) === null || _a === void 0 ? void 0 : _a.active; }) ? 1 : 0;
|
|
6974
|
+
const wholeTarget = group.hovered || requests.some(request => request.whole) ? 1 : 0;
|
|
6975
|
+
look.shown = approach(look.shown, shownTarget);
|
|
6976
|
+
look.whole = approach(look.whole, wholeTarget);
|
|
6977
|
+
look.grow = approach(look.grow, 1);
|
|
6978
|
+
look.time = now;
|
|
6979
|
+
animating = animating || look.shown !== shownTarget || look.whole !== wholeTarget || look.grow !== 1;
|
|
6980
|
+
looks.set(group.key, look);
|
|
6981
|
+
group.members.forEach(id => memberLooks.set(id, look));
|
|
6982
|
+
if (look.svg && this.looks.get(group.key) !== look) {
|
|
6983
|
+
// Inherited from an earlier group; claim it so that group's cleanup leaves it alone.
|
|
6984
|
+
for (const [key, other] of this.looks) {
|
|
6985
|
+
if (other.svg === look.svg && key !== group.key)
|
|
6986
|
+
other.svg = undefined;
|
|
6987
|
+
}
|
|
6988
|
+
}
|
|
6989
|
+
this.drawGroup(viewer, element, coordinates, this.drawn.find(item => item.group === group), look, requests);
|
|
6990
|
+
}
|
|
6991
|
+
for (const [key, look] of this.looks) {
|
|
6992
|
+
if (!looks.has(key))
|
|
6993
|
+
(_c = look.svg) === null || _c === void 0 ? void 0 : _c.remove();
|
|
6994
|
+
}
|
|
6995
|
+
this.looks = looks;
|
|
6996
|
+
this.memberLooks = memberLooks;
|
|
6997
|
+
if (animating) {
|
|
6998
|
+
viewer.scene.requestRender();
|
|
6999
|
+
}
|
|
7000
|
+
}
|
|
7001
|
+
drawGroup(viewer, element, coordinates, drawn, look, requests) {
|
|
7002
|
+
const group = drawn.group;
|
|
7003
|
+
let svg = look.svg;
|
|
7004
|
+
if (!svg) {
|
|
7005
|
+
const ns = "http://www.w3.org/2000/svg";
|
|
7006
|
+
svg = document.createElementNS(ns, "svg");
|
|
7007
|
+
svg.setAttribute("class", "entity-label-model-box");
|
|
7008
|
+
svg.setAttribute("aria-hidden", "true");
|
|
7009
|
+
svg.style.cssText = "position:absolute;width:1px;height:1px;overflow:visible;pointer-events:none;filter:drop-shadow(0 0 2px rgba(0, 0, 0, 0.4));";
|
|
7010
|
+
for (const edges of ["back", "front"]) {
|
|
7011
|
+
const path = document.createElementNS(ns, "path");
|
|
7012
|
+
path.setAttribute("fill", "none");
|
|
7013
|
+
path.setAttribute("stroke-linecap", "round");
|
|
7014
|
+
path.dataset.edges = edges;
|
|
7015
|
+
svg.appendChild(path);
|
|
7016
|
+
}
|
|
7017
|
+
look.svg = svg;
|
|
7018
|
+
}
|
|
7019
|
+
if (svg.parentElement !== element) {
|
|
7020
|
+
element.appendChild(svg);
|
|
7021
|
+
}
|
|
7022
|
+
svg.style.display = "";
|
|
7023
|
+
const transforms = Cesium.SceneTransforms;
|
|
7024
|
+
const projectPosition = transforms.wgs84ToWindowCoordinates || transforms.worldToWindowCoordinates;
|
|
7025
|
+
const origin = group.origin;
|
|
7026
|
+
const scratch = new Cesium.Cartesian3();
|
|
7027
|
+
// A new box grows out of the ring it replaces.
|
|
7028
|
+
const grown = look.grow * look.grow * (3 - 2 * look.grow);
|
|
7029
|
+
const from = requests[0].from;
|
|
7030
|
+
const at = (point) => `${(from.x + (point.x - from.x) * grown) * coordinates.scaleX} ${(from.y + (point.y - from.y) * grown) * coordinates.scaleY}`;
|
|
7031
|
+
const toScreen = (point) => {
|
|
7032
|
+
const projected = projectPosition(viewer.scene, Cesium.Cartesian3.fromElements(origin.x + point[0], origin.y + point[1], origin.z + point[2], scratch));
|
|
7033
|
+
return at(projected);
|
|
7034
|
+
};
|
|
7035
|
+
const camera = viewer.camera.positionWC || viewer.camera.position;
|
|
7036
|
+
const eye = [camera.x - origin.x, camera.y - origin.y, camera.z - origin.z];
|
|
7037
|
+
const span = Math.max(...group.solids.map(solid => vLength(solid.half)));
|
|
7038
|
+
const inset = Math.max(1, span) * 1e-6;
|
|
7039
|
+
const fraction = MODEL_BOX_BRACKET + (0.5 - MODEL_BOX_BRACKET) * look.whole;
|
|
7040
|
+
const parts = fraction >= 0.499 ? [[0, 1]] : [[0, fraction], [1 - fraction, 1]];
|
|
7041
|
+
const paths = { front: "", back: "" };
|
|
7042
|
+
for (const [start, end] of group.outline) {
|
|
7043
|
+
const delta = vSub(end, start);
|
|
7044
|
+
// Sample visibility along the edge, since another member can hide part of it.
|
|
7045
|
+
const steps = group.solids.length > 1 ? Math.min(24, Math.max(2, Math.ceil(vLength(delta) / span * 12))) : 1;
|
|
7046
|
+
const visible = [];
|
|
7047
|
+
for (let i = 0; i < steps; i++) {
|
|
7048
|
+
visible.push(!solidsHide(group.solids, eye, vAdd(start, vScale(delta, (i + 0.5) / steps)), inset));
|
|
7049
|
+
}
|
|
7050
|
+
for (const [from, to] of parts) {
|
|
7051
|
+
let runStart = from;
|
|
7052
|
+
for (let i = Math.floor(from * steps); i < steps && i / steps < to; i++) {
|
|
7053
|
+
const runEnd = Math.min(to, (i + 1) / steps);
|
|
7054
|
+
if (runEnd < to && visible[i + 1] === visible[i]) {
|
|
7055
|
+
continue;
|
|
7056
|
+
}
|
|
7057
|
+
paths[visible[i] ? "front" : "back"] += `M${toScreen(vAdd(start, vScale(delta, runStart)))}L${toScreen(vAdd(start, vScale(delta, runEnd)))}`;
|
|
7058
|
+
runStart = runEnd;
|
|
7059
|
+
}
|
|
7060
|
+
}
|
|
7061
|
+
}
|
|
7062
|
+
// Each stem lands on an edge, which brackets may leave bare, so a short piece of it is drawn there.
|
|
7063
|
+
if (fraction < 0.499) {
|
|
7064
|
+
for (const { edge, point } of drawn.landings) {
|
|
7065
|
+
const [a, b] = drawn.edges[edge];
|
|
7066
|
+
const length = Math.hypot(b.x - a.x, b.y - a.y);
|
|
7067
|
+
const reach = Math.min(MODEL_BOX_LANDING, length / 2);
|
|
7068
|
+
const toward = (sign) => {
|
|
7069
|
+
const along = Math.max(0, Math.min(length, Math.hypot(point.x - a.x, point.y - a.y) + sign * reach));
|
|
7070
|
+
return { x: a.x + (b.x - a.x) * along / length, y: a.y + (b.y - a.y) * along / length };
|
|
7071
|
+
};
|
|
7072
|
+
paths.front += `M${at(toward(-1))}L${at(toward(1))}`;
|
|
7073
|
+
}
|
|
7074
|
+
}
|
|
7075
|
+
svg.style.left = `${coordinates.x}px`;
|
|
7076
|
+
svg.style.top = `${coordinates.y}px`;
|
|
7077
|
+
svg.style.opacity = String(Math.max(...requests.map(request => request.opacity)) * look.shown * Math.min(1, look.grow * 2));
|
|
7078
|
+
svg.style.zIndex = String(Math.max(...requests.map(request => request.zIndex)));
|
|
7079
|
+
const color = requests[0].color;
|
|
7080
|
+
for (const path of Array.from(svg.children)) {
|
|
7081
|
+
const front = path.dataset.edges === "front";
|
|
7082
|
+
path.setAttribute("d", paths[front ? "front" : "back"]);
|
|
7083
|
+
path.setAttribute("stroke", color);
|
|
7084
|
+
path.setAttribute("stroke-width", front ? "1.5" : "1");
|
|
7085
|
+
path.setAttribute("stroke-opacity", front ? "1" : "0.35");
|
|
7086
|
+
path.setAttribute("stroke-dasharray", front ? "" : "3 3");
|
|
7087
|
+
}
|
|
7088
|
+
}
|
|
7089
|
+
}
|
|
7090
|
+
EntityLabel.ModelBoxes = ModelBoxes;
|
|
7091
|
+
/*
|
|
7092
|
+
* Andrew's monotone chain, counter-clockwise in screen space.
|
|
7093
|
+
*/
|
|
7094
|
+
function convexHull(points) {
|
|
7095
|
+
const sorted = [...points].sort((a, b) => a.x - b.x || a.y - b.y);
|
|
7096
|
+
const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
|
7097
|
+
const build = (list) => {
|
|
7098
|
+
const chain = [];
|
|
7099
|
+
for (const point of list) {
|
|
7100
|
+
while (chain.length >= 2 && cross(chain[chain.length - 2], chain[chain.length - 1], point) <= 0)
|
|
7101
|
+
chain.pop();
|
|
7102
|
+
chain.push(point);
|
|
7103
|
+
}
|
|
7104
|
+
chain.pop();
|
|
7105
|
+
return chain;
|
|
7106
|
+
};
|
|
7107
|
+
return [...build(sorted), ...build(sorted.reverse())];
|
|
7108
|
+
}
|
|
7109
|
+
/*
|
|
7110
|
+
* Where the top of a convex outline crosses a vertical line, so a stem leaves the box without jumping between corners.
|
|
7111
|
+
*/
|
|
7112
|
+
function hullTop(hull, x) {
|
|
7113
|
+
let top = Infinity;
|
|
7114
|
+
for (let i = 0; i < hull.length; i++) {
|
|
7115
|
+
const a = hull[i];
|
|
7116
|
+
const b = hull[(i + 1) % hull.length];
|
|
7117
|
+
if (x < Math.min(a.x, b.x) || x > Math.max(a.x, b.x)) {
|
|
7118
|
+
continue;
|
|
7119
|
+
}
|
|
7120
|
+
top = Math.min(top, a.x === b.x ? Math.min(a.y, b.y) : a.y + (b.y - a.y) * (x - a.x) / (b.x - a.x));
|
|
7121
|
+
}
|
|
7122
|
+
return top;
|
|
7123
|
+
}
|
|
7124
|
+
function hullArea(hull) {
|
|
7125
|
+
return Math.abs(hull.reduce((sum, point, i) => sum + point.x * hull[(i + 1) % hull.length].y - hull[(i + 1) % hull.length].x * point.y, 0)) / 2;
|
|
7126
|
+
}
|
|
7127
|
+
function hullContains(hull, point) {
|
|
7128
|
+
return hull.length >= 3 && hull.every((a, i) => {
|
|
7129
|
+
const b = hull[(i + 1) % hull.length];
|
|
7130
|
+
return (b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x) >= 0;
|
|
7131
|
+
});
|
|
7132
|
+
}
|
|
6367
7133
|
class Label {
|
|
6368
7134
|
get HasText() { return this.options.presentation !== "icon"; }
|
|
6369
7135
|
SetHoverVisible(visible) {
|
|
@@ -6418,6 +7184,11 @@
|
|
|
6418
7184
|
this.Transient = false;
|
|
6419
7185
|
this.hoverFade = { from: 0, to: 1, start: Date.now() };
|
|
6420
7186
|
this.cursorMotion = { amount: 0, from: 0, target: 0, start: 0, position: undefined };
|
|
7187
|
+
// How far the stem is from the ring onto a model box, and the offset it eases out after changing outline.
|
|
7188
|
+
// The record's box for the Entity this label is on: undefined corners while reading, null when it has none.
|
|
7189
|
+
this.modelBox = null;
|
|
7190
|
+
this.boxMorph = { value: 0, from: 0, target: 0, start: 0, anchor: undefined, group: undefined,
|
|
7191
|
+
shift: { x: 0, y: 0, start: 0 } };
|
|
6421
7192
|
// Counter to help differentiate between createLabel calls.
|
|
6422
7193
|
this.counter = 0;
|
|
6423
7194
|
// Whether this label has asked its group for a default image, which it does once on screen.
|
|
@@ -6503,11 +7274,13 @@
|
|
|
6503
7274
|
* Recalling will be ignored.
|
|
6504
7275
|
*/
|
|
6505
7276
|
Dispose() {
|
|
7277
|
+
var _a;
|
|
6506
7278
|
if (this.disposed) {
|
|
6507
7279
|
return;
|
|
6508
7280
|
}
|
|
6509
7281
|
this.disposed = true;
|
|
6510
7282
|
this.removeLabel();
|
|
7283
|
+
(_a = layers.get(this.viewer)) === null || _a === void 0 ? void 0 : _a.modelBoxes.Set(this.id, null, null, null);
|
|
6511
7284
|
clearTimeout(this.loadTimeout);
|
|
6512
7285
|
this.loadTimeout = null;
|
|
6513
7286
|
}
|
|
@@ -6529,7 +7302,7 @@
|
|
|
6529
7302
|
* @param starterPos3d optional position to create label at. Ignored for billboard labels.
|
|
6530
7303
|
*/
|
|
6531
7304
|
async createLabel(starterPos3d) {
|
|
6532
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
7305
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
|
|
6533
7306
|
// The old label stays until its replacement is built, so a rebuild never blinks while a location loads.
|
|
6534
7307
|
let pos3d = starterPos3d ? starterPos3d.clone() : null;
|
|
6535
7308
|
const counter = ++this.counter;
|
|
@@ -6815,6 +7588,9 @@
|
|
|
6815
7588
|
if (this.disposed || this.counter != counter) {
|
|
6816
7589
|
return;
|
|
6817
7590
|
}
|
|
7591
|
+
// Leave the layer only once rejoined, so a rebuild never empties it and closes it along with its boxes.
|
|
7592
|
+
const leaveLayer = this.renderRemoval;
|
|
7593
|
+
this.renderRemoval = null;
|
|
6818
7594
|
this.removeLabel();
|
|
6819
7595
|
// A found photo draws as if configured, without changing the options clustering compares and measures.
|
|
6820
7596
|
const photo = HasIcon(this.options) ? null : (_j = this.options.photo) !== null && _j !== void 0 ? _j : this.photo;
|
|
@@ -7111,9 +7887,65 @@
|
|
|
7111
7887
|
radius: Math.max(width, height) / 2
|
|
7112
7888
|
};
|
|
7113
7889
|
};
|
|
7890
|
+
// Only CAD tilesets and model Entities are assembly parts with a geometry box; anything else stays a point.
|
|
7891
|
+
const boxable = Boolean(normalLeader && ((_r = this.rego) === null || _r === void 0 ? void 0 : _r.entityId) && ((visual instanceof Cesium.Cesium3DTileFeature && this.rego.tilesetType === BModels.Tileset.EType.Cad) ||
|
|
7892
|
+
(visual instanceof Cesium.Entity && Boolean(visual.model))));
|
|
7893
|
+
if (!boxable || ((_s = this.modelBox) === null || _s === void 0 ? void 0 : _s.entityId) !== ((_t = this.rego) === null || _t === void 0 ? void 0 : _t.entityId)) {
|
|
7894
|
+
layer.modelBoxes.Set(this.id, null, null, null);
|
|
7895
|
+
this.modelBox = null;
|
|
7896
|
+
}
|
|
7897
|
+
// Read once per Entity, so a label rebuilt for its loaded text keeps its box instead of waiting again.
|
|
7898
|
+
if (boxable && !this.modelBox) {
|
|
7899
|
+
const box = { entityId: this.rego.entityId, corners: undefined, load: null };
|
|
7900
|
+
box.load = this.loadModelBox().catch(() => null).then(corners => box.corners = corners || null);
|
|
7901
|
+
this.modelBox = box;
|
|
7902
|
+
}
|
|
7903
|
+
// A model's label stays hidden until its box is known, so a ring never shows only to turn into a box.
|
|
7904
|
+
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;
|
|
7905
|
+
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);
|
|
7906
|
+
const boxPendingUntil = Date.now() + MODEL_BOX_WAIT;
|
|
7907
|
+
// Whether this label has been on screen yet; a box found before then is drawn in place, not eased into.
|
|
7908
|
+
let shownOnce = false;
|
|
7909
|
+
if (boxable) {
|
|
7910
|
+
const found = this._pos3d;
|
|
7911
|
+
const settle = () => {
|
|
7912
|
+
if (boxPending && !this.disposed && this.counter === counter) {
|
|
7913
|
+
boxPending = false;
|
|
7914
|
+
this.viewer.scene.requestRender();
|
|
7915
|
+
}
|
|
7916
|
+
};
|
|
7917
|
+
// Past the wait a slow record no longer holds the label back; a box that comes later eases in.
|
|
7918
|
+
setTimeout(settle, MODEL_BOX_WAIT);
|
|
7919
|
+
this.modelBox.load.then(corners => {
|
|
7920
|
+
if (!corners || this.disposed || this.counter !== counter) {
|
|
7921
|
+
return;
|
|
7922
|
+
}
|
|
7923
|
+
// A record that disagrees with where the model was found describes another placement, such as a moved tileset.
|
|
7924
|
+
const centre = Cesium.Cartesian3.midpoint(corners[0], corners[7], new Cesium.Cartesian3());
|
|
7925
|
+
if (Cesium.Cartesian3.distance(centre, found) > Cesium.Cartesian3.distance(corners[0], corners[7]) * 0.75 + 2) {
|
|
7926
|
+
return;
|
|
7927
|
+
}
|
|
7928
|
+
const rego = this.rego;
|
|
7929
|
+
// Let go a little further out than it is taken, so a camera resting at the limit does not flip it.
|
|
7930
|
+
let near = false;
|
|
7931
|
+
layer.modelBoxes.Set(this.id, corners, rego.entityId, {
|
|
7932
|
+
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); },
|
|
7933
|
+
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)); },
|
|
7934
|
+
near: () => {
|
|
7935
|
+
var _a, _b;
|
|
7936
|
+
const ring = this.options.anchorRing;
|
|
7937
|
+
const camera = this.viewer.camera.positionWC || this.viewer.camera.position;
|
|
7938
|
+
const limit = ((_a = ring === null || ring === void 0 ? void 0 : ring.nearDistance) !== null && _a !== void 0 ? _a : 100) * (near ? 1.1 : 1);
|
|
7939
|
+
near = (ring === null || ring === void 0 ? void 0 : ring.enabled) !== false && Number.isFinite((_b = this._pos3d) === null || _b === void 0 ? void 0 : _b.x) && Cesium.Cartesian3.distance(camera, this._pos3d) <= limit;
|
|
7940
|
+
return near || ((ring === null || ring === void 0 ? void 0 : ring.enabled) !== false && Boolean(this.cursorMotion.target));
|
|
7941
|
+
}
|
|
7942
|
+
});
|
|
7943
|
+
this.viewer.scene.requestRender();
|
|
7944
|
+
}).finally(settle);
|
|
7945
|
+
}
|
|
7114
7946
|
let cursorMotion = this.cursorMotion;
|
|
7115
7947
|
const project = () => {
|
|
7116
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
|
|
7948
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
|
|
7117
7949
|
if (this.disposed || !this.viewer || this.viewer.isDestroyed() || ((_a = this.visible) === null || _a === void 0 ? void 0 : _a.call(this)) === false ||
|
|
7118
7950
|
(presentation === "icon" && this.options.iconScale === 0)) {
|
|
7119
7951
|
return null;
|
|
@@ -7151,18 +7983,29 @@
|
|
|
7151
7983
|
if (!Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.x) || !Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.y)) {
|
|
7152
7984
|
return null;
|
|
7153
7985
|
}
|
|
7986
|
+
const boxed = layer.modelBoxes.Frame(this.id);
|
|
7987
|
+
const waiting = boxPending && Date.now() < boxPendingUntil;
|
|
7988
|
+
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);
|
|
7989
|
+
// Eased between the ring and where the stem meets the box, so gaining or losing a box never jumps.
|
|
7990
|
+
const boxTarget = this.boxMorph.anchor;
|
|
7991
|
+
const ringPoint = { x: anchor.x + pixelOffset.x, y: anchor.y + pixelOffset.y };
|
|
7992
|
+
const boxAnchor = boxBlend > 0 && boxTarget ? {
|
|
7993
|
+
x: ringPoint.x + (boxTarget.x - ringPoint.x) * boxBlend,
|
|
7994
|
+
y: ringPoint.y + (boxTarget.y - ringPoint.y) * boxBlend
|
|
7995
|
+
} : undefined;
|
|
7996
|
+
const start = boxAnchor || anchor;
|
|
7154
7997
|
const onScreen = anchor.x >= 0 && anchor.y >= 0 && anchor.x < this.viewer.canvas.clientWidth && anchor.y < this.viewer.canvas.clientHeight;
|
|
7155
|
-
if (leader &&
|
|
7998
|
+
if (leader && (start.x < 0 || start.y < 0 || start.x >= this.viewer.canvas.clientWidth || start.y >= this.viewer.canvas.clientHeight))
|
|
7156
7999
|
return null;
|
|
7157
8000
|
if (onScreen && !this.imageAsked && this.groupId && !HasIcon(this.options) && !this.options.photo) {
|
|
7158
8001
|
this.imageAsked = true;
|
|
7159
|
-
(
|
|
8002
|
+
(_g = this.visualRegister) === null || _g === void 0 ? void 0 : _g.RequestLabelImage((_h = this.rego) === null || _h === void 0 ? void 0 : _h.entityId);
|
|
7160
8003
|
}
|
|
7161
8004
|
const transientPivot = (this.Transient && !this.Persistent ?
|
|
7162
|
-
(this.hoverFade.to === 0 ? this.retainedHoverPosition : (
|
|
8005
|
+
(this.hoverFade.to === 0 ? this.retainedHoverPosition : (_j = this.visualRegister) === null || _j === void 0 ? void 0 : _j.GetLabelHoverPosition((_k = this.rego) === null || _k === void 0 ? void 0 : _k.entityId, (_l = this.rego) === null || _l === void 0 ? void 0 : _l.menuItemId)) : undefined);
|
|
7163
8006
|
const hover = (presentation !== "icon" ?
|
|
7164
8007
|
(this.Transient && !this.Persistent && this.hoverFade.to === 0 ? this.retainedHoverPosition :
|
|
7165
|
-
(
|
|
8008
|
+
(_m = this.visualRegister) === null || _m === void 0 ? void 0 : _m.GetLabelHoverPosition((_o = this.rego) === null || _o === void 0 ? void 0 : _o.entityId, (_p = this.rego) === null || _p === void 0 ? void 0 : _p.menuItemId)) : undefined);
|
|
7166
8009
|
const reused = !this.Transient || this.Persistent;
|
|
7167
8010
|
const now = Date.now();
|
|
7168
8011
|
const animateCursor = () => {
|
|
@@ -7186,12 +8029,14 @@
|
|
|
7186
8029
|
const cursorAmount = cursorMotion.amount;
|
|
7187
8030
|
leader = normalLeader || cursorAmount > 0;
|
|
7188
8031
|
// An icon keeps its callout centred on the glyph rather than following the cursor across it.
|
|
7189
|
-
const icon = measureIcon();
|
|
8032
|
+
const icon = boxAnchor ? null : measureIcon();
|
|
7190
8033
|
const iconRing = icon ? Math.max(ICON_RING_MIN, icon.radius + ICON_RING_PADDING) : 0;
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
8034
|
+
// A boxed model is pinned the same way, with its stem leaving the top of the box outline.
|
|
8035
|
+
const pinned = Boolean(icon || boxAnchor);
|
|
8036
|
+
const pivot = pinned ? undefined : transientPivot;
|
|
8037
|
+
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;
|
|
8038
|
+
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;
|
|
8039
|
+
if (cursorAmount > 0 && !pinned) {
|
|
7195
8040
|
// A leader-less label starts centred on its anchor; preserve that box as it lifts away.
|
|
7196
8041
|
if (!normalLeader) {
|
|
7197
8042
|
x -= ele.offsetWidth / layer.coordinates.scaleX / 2;
|
|
@@ -7226,6 +8071,9 @@
|
|
|
7226
8071
|
if (visual instanceof Cesium.Entity && (visual.polygon || visual.polyline))
|
|
7227
8072
|
leaderLength /= 3;
|
|
7228
8073
|
}
|
|
8074
|
+
if (boxAnchor && this.options.leaderLength == null) {
|
|
8075
|
+
leaderLength += (MODEL_BOX_STEM - leaderLength) * boxBlend;
|
|
8076
|
+
}
|
|
7229
8077
|
if (cursorAmount > 0) {
|
|
7230
8078
|
leaderLength += (96 - leaderLength) * cursorAmount;
|
|
7231
8079
|
}
|
|
@@ -7233,7 +8081,7 @@
|
|
|
7233
8081
|
leaderLength += iconRing;
|
|
7234
8082
|
y = anchorY - leaderLength;
|
|
7235
8083
|
}
|
|
7236
|
-
else if (hover && !reused && !
|
|
8084
|
+
else if (hover && !reused && !pinned) {
|
|
7237
8085
|
x = hover.x + pixelOffset.x;
|
|
7238
8086
|
y = hover.y + pixelOffset.y - 24;
|
|
7239
8087
|
}
|
|
@@ -7242,7 +8090,7 @@
|
|
|
7242
8090
|
const width = ele.offsetWidth / coordinates.scaleX;
|
|
7243
8091
|
const height = ele.offsetHeight / coordinates.scaleY;
|
|
7244
8092
|
let clusterBounds;
|
|
7245
|
-
if ((
|
|
8093
|
+
if ((_s = this.options.cluster) === null || _s === void 0 ? void 0 : _s.enabled) {
|
|
7246
8094
|
const textWidth = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetWidth) || 0;
|
|
7247
8095
|
const textHeight = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetHeight) || 0;
|
|
7248
8096
|
// A photo is never part of the footprint, so its arrival cannot change which cluster a label joins.
|
|
@@ -7276,7 +8124,7 @@
|
|
|
7276
8124
|
if (!leader && (bounds.x + bounds.width < 0 || bounds.y + bounds.height < 0 || bounds.x > this.viewer.canvas.clientWidth || bounds.y > this.viewer.canvas.clientHeight)) {
|
|
7277
8125
|
return null;
|
|
7278
8126
|
}
|
|
7279
|
-
const state = (
|
|
8127
|
+
const state = (_t = this.visualRegister) === null || _t === void 0 ? void 0 : _t.GetState({ entityId: (_u = this.rego) === null || _u === void 0 ? void 0 : _u.entityId, menuItemId: (_v = this.rego) === null || _v === void 0 ? void 0 : _v.menuItemId });
|
|
7280
8128
|
const opacity = this.prePositioned || hoverOnly || MAX_DISTANCE === Infinity ? 1 : 1 - distance / MAX_DISTANCE;
|
|
7281
8129
|
return {
|
|
7282
8130
|
canFlip: leader && presentation !== "icon", hasLeader: leader,
|
|
@@ -7284,24 +8132,25 @@
|
|
|
7284
8132
|
transient: this.Transient || Boolean(hover) || cursorAmount > 0,
|
|
7285
8133
|
extendedGeometry: visual instanceof Cesium.Entity && Boolean(visual.polygon || visual.polyline || visual.corridor),
|
|
7286
8134
|
viewportWidth: this.viewer.canvas.clientWidth,
|
|
7287
|
-
id: this.id, entityKey: ((
|
|
7288
|
-
entityId: (
|
|
7289
|
-
iconRadius: icon === null || icon === void 0 ? void 0 : icon.radius,
|
|
8135
|
+
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,
|
|
8136
|
+
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,
|
|
8137
|
+
iconRadius: icon === null || icon === void 0 ? void 0 : icon.radius, modelBox: boxed || undefined, boxBlend,
|
|
7290
8138
|
// Priority shows the hovered member individually without dragging its cluster along.
|
|
7291
|
-
opacity, priority: hover || cursorAmount > 0 || (state === null || state === void 0 ? void 0 : state.highlighted) ? 1 : 0, options: this.options, text: originalText, clusterBounds
|
|
8139
|
+
opacity: waiting ? 0 : opacity, priority: hover || cursorAmount > 0 || (state === null || state === void 0 ? void 0 : state.highlighted) ? 1 : 0, options: this.options, text: originalText, clusterBounds
|
|
7292
8140
|
};
|
|
7293
8141
|
};
|
|
7294
8142
|
let displacement = { x: 0, y: 0 };
|
|
7295
8143
|
let nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
|
|
7296
8144
|
const apply = (placement) => {
|
|
7297
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4;
|
|
8145
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5;
|
|
7298
8146
|
const show = (placement === null || placement === void 0 ? void 0 : placement.visible) && placement.opacity > 0;
|
|
8147
|
+
shownOnce = shownOnce || Boolean(show);
|
|
7299
8148
|
clickEntityIds = show ? placement.entityIds : [];
|
|
7300
8149
|
ele.style.visibility = show ? "visible" : "hidden";
|
|
7301
8150
|
lineEle.style.display = show && leader ? "block" : "none";
|
|
7302
8151
|
if (runEle)
|
|
7303
8152
|
runEle.style.display = show && leader ? "block" : "none";
|
|
7304
|
-
this.anchor.style.display = show && leader ? "block" : "none";
|
|
8153
|
+
this.anchor.style.display = show && leader && !(((_a = placement.frame) === null || _a === void 0 ? void 0 : _a.boxBlend) >= 1) ? "block" : "none";
|
|
7305
8154
|
if (!show) {
|
|
7306
8155
|
displacement = { x: 0, y: 0 };
|
|
7307
8156
|
nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
|
|
@@ -7313,7 +8162,7 @@
|
|
|
7313
8162
|
return;
|
|
7314
8163
|
}
|
|
7315
8164
|
// A label standing for a cluster shows its summary only; its photo is put away and never fetched.
|
|
7316
|
-
const solo = !((
|
|
8165
|
+
const solo = !((_b = placement.members) === null || _b === void 0 ? void 0 : _b.length);
|
|
7317
8166
|
if (look.iconPhoto && iconElement && iconElement.dataset.failed !== "true") {
|
|
7318
8167
|
const display = solo ? "inline-flex" : "none";
|
|
7319
8168
|
if (iconElement.style.display !== display) {
|
|
@@ -7331,7 +8180,7 @@
|
|
|
7331
8180
|
}
|
|
7332
8181
|
}, PHOTO_SHOW_DELAY);
|
|
7333
8182
|
}
|
|
7334
|
-
const displayText = (
|
|
8183
|
+
const displayText = (_c = placement.text) !== null && _c !== void 0 ? _c : originalText;
|
|
7335
8184
|
if (textElement && textElement.textContent !== displayText) {
|
|
7336
8185
|
textElement.textContent = displayText;
|
|
7337
8186
|
// Measure the summary's footprint next frame without replacing each member's source text.
|
|
@@ -7355,9 +8204,9 @@
|
|
|
7355
8204
|
const stack = JSON.stringify(placement.icons || []);
|
|
7356
8205
|
if (iconStack && stack !== displayedStack) {
|
|
7357
8206
|
displayedStack = stack;
|
|
7358
|
-
const looks = ((
|
|
8207
|
+
const looks = ((_d = placement.icons) === null || _d === void 0 ? void 0 : _d.length) ? [iconLook(this.options), ...placement.icons] : [];
|
|
7359
8208
|
// One small size for all, so no icon in the arrangement reads as more important than another.
|
|
7360
|
-
const size = (this.options.iconSize || 24) * ((
|
|
8209
|
+
const size = (this.options.iconSize || 24) * ((_e = this.options.iconScale) !== null && _e !== void 0 ? _e : 1) * ICON_STACK_SCALE;
|
|
7361
8210
|
const built = looks.map(look => this.buildIcon({ ...look, iconSize: size }, counter));
|
|
7362
8211
|
// A stack is on show already, so its photos load straight away.
|
|
7363
8212
|
built.forEach(({ start }) => start === null || start === void 0 ? void 0 : start());
|
|
@@ -7392,9 +8241,11 @@
|
|
|
7392
8241
|
// Summary and badge changes must fit on their first visible frame, including after resize.
|
|
7393
8242
|
const ring = this.options.anchorRing;
|
|
7394
8243
|
const pointer = (layer.pointer || (cursorMotion.target ? cursorMotion.position : undefined) || (this.Transient && !this.Persistent ?
|
|
7395
|
-
(this.hoverFade.to === 0 ? this.retainedHoverPosition : (
|
|
8244
|
+
(this.hoverFade.to === 0 ? this.retainedHoverPosition : (_f = this.visualRegister) === null || _f === void 0 ? void 0 : _f.GetLabelHoverPosition((_g = this.rego) === null || _g === void 0 ? void 0 : _g.entityId, (_h = this.rego) === null || _h === void 0 ? void 0 : _h.menuItemId)) : undefined));
|
|
7396
8245
|
const iconRing = frame.iconRadius ? Math.max(ICON_RING_MIN, frame.iconRadius + ICON_RING_PADDING) : 0;
|
|
7397
|
-
const
|
|
8246
|
+
const boxed = leader ? frame.modelBox : undefined;
|
|
8247
|
+
const hovered = pointer && (boxed ? hullContains(boxed.hull, pointer) :
|
|
8248
|
+
Math.hypot(pointer.x - frame.anchorX, pointer.y - frame.anchorY) <= Math.max((_j = ring === null || ring === void 0 ? void 0 : ring.hoverDistance) !== null && _j !== void 0 ? _j : 24, iconRing));
|
|
7398
8249
|
if (look.iconPhoto && growPhoto) {
|
|
7399
8250
|
// A rectangle leans on the leader's side of the label.
|
|
7400
8251
|
if (photoShape === "rect") {
|
|
@@ -7405,15 +8256,15 @@
|
|
|
7405
8256
|
}
|
|
7406
8257
|
// Grown in layout rather than scaled, so the border keeps its width, and the label, whose bottom
|
|
7407
8258
|
// sits on the leader, grows up and sideways without changing the leader's length.
|
|
7408
|
-
growPhoto(Boolean(hovered) && !((
|
|
8259
|
+
growPhoto(Boolean(hovered) && !((_k = placement.members) === null || _k === void 0 ? void 0 : _k.length));
|
|
7409
8260
|
}
|
|
7410
|
-
const normalRadius = (!leader || (ring === null || ring === void 0 ? void 0 : ring.enabled) === false ? 0 : Math.max(frame.distance <= ((
|
|
8261
|
+
const normalRadius = (!leader || (ring === null || ring === void 0 ? void 0 : ring.enabled) === false ? 0 : Math.max(frame.distance <= ((_l = ring === null || ring === void 0 ? void 0 : ring.nearDistance) !== null && _l !== void 0 ? _l : 100) ? ((_m = ring === null || ring === void 0 ? void 0 : ring.nearRadius) !== null && _m !== void 0 ? _m : 10) : 0, hovered ? ((_o = ring === null || ring === void 0 ? void 0 : ring.hoverRadius) !== null && _o !== void 0 ? _o : (this.Transient && !this.Persistent ? 15.4 : 22)) : 0));
|
|
7411
8262
|
const targetRadius = normalRadius + (15.4 - normalRadius) * cursorMotion.amount;
|
|
7412
8263
|
// Around a glyph the ring keeps the glyph's size and only fades its stroke in, so it never covers the icon.
|
|
7413
8264
|
const targetOuter = iconRing || targetRadius || 2.5;
|
|
7414
8265
|
const targetInner = iconRing ? (targetRadius > 0 ? iconRing - 2 : iconRing) : Math.max(0, targetRadius - 2);
|
|
7415
8266
|
const now = Date.now();
|
|
7416
|
-
const duration = (
|
|
8267
|
+
const duration = (_p = ring === null || ring === void 0 ? void 0 : ring.transitionDuration) !== null && _p !== void 0 ? _p : 200;
|
|
7417
8268
|
const sharedRing = this.Transient && !this.Persistent && this.hoverFade.to === 1 ? this.hoverRingState : undefined;
|
|
7418
8269
|
let ringAnimation = sharedRing ? sharedRing.animation : this.ringAnimation;
|
|
7419
8270
|
if (!ringAnimation) {
|
|
@@ -7447,7 +8298,8 @@
|
|
|
7447
8298
|
if (sharedRing) {
|
|
7448
8299
|
sharedRing.animation = ringAnimation;
|
|
7449
8300
|
}
|
|
7450
|
-
const
|
|
8301
|
+
const ringShare = 1 - (frame.boxBlend || 0);
|
|
8302
|
+
const radius = ringShare > 0 && ringAnimation.inner > 0 ? ringAnimation.outer * ringShare : 0;
|
|
7451
8303
|
const stem = (leader ? (angled ? fitAngledLeader : fitLeader)(frame.anchorX, frame.anchorY, Math.max(frame.leaderLength, radius ? radius + 4 : 0), ele.offsetWidth / coordinates.scaleX, ele.offsetHeight / coordinates.scaleY, radius) : undefined);
|
|
7452
8304
|
const position = { x: (stem || frame).x, y: (stem || frame).y };
|
|
7453
8305
|
const width = ele.offsetWidth / coordinates.scaleX;
|
|
@@ -7455,19 +8307,19 @@
|
|
|
7455
8307
|
if (leader && angled) {
|
|
7456
8308
|
// The declutter owns the side; the fit only says which one had the room to begin with.
|
|
7457
8309
|
const run = Math.max(ANGLED_LEADER_RUN, radius + 4);
|
|
7458
|
-
const side = (
|
|
8310
|
+
const side = (_r = (_q = placement.textSide) !== null && _q !== void 0 ? _q : frame.leaderSide) !== null && _r !== void 0 ? _r : "right";
|
|
7459
8311
|
const beside = side === "left" ? frame.anchorX - run - width : frame.anchorX + run;
|
|
7460
8312
|
position.x = Math.max(0, Math.min(beside, this.viewer.canvas.clientWidth - width));
|
|
7461
8313
|
}
|
|
7462
8314
|
else if (leader && placement.textSide === "left") {
|
|
7463
8315
|
position.x = Math.max(0, Math.min(frame.anchorX - width, this.viewer.canvas.clientWidth - width));
|
|
7464
8316
|
}
|
|
7465
|
-
const gap = (
|
|
8317
|
+
const gap = (_t = (_s = this.options.pointerAvoidance) === null || _s === void 0 ? void 0 : _s.gap) !== null && _t !== void 0 ? _t : 12;
|
|
7466
8318
|
const containsPointer = (x, y, margin) => {
|
|
7467
8319
|
return pointer && pointer.x >= x - margin && pointer.x <= x + width + margin && pointer.y >= y - margin && pointer.y <= y + height + margin;
|
|
7468
8320
|
};
|
|
7469
8321
|
const passive = !selectOnClick || !this.visualRegister;
|
|
7470
|
-
if (passive && ((
|
|
8322
|
+
if (passive && ((_u = this.options.pointerAvoidance) === null || _u === void 0 ? void 0 : _u.enabled) !== false && pointer &&
|
|
7471
8323
|
(containsPointer(position.x, position.y, gap) || containsPointer(position.x + displacement.x, position.y + displacement.y, gap))) {
|
|
7472
8324
|
if (containsPointer(position.x + displacement.x, position.y + displacement.y, 0)) {
|
|
7473
8325
|
// Choose against the original box and retain the offset until the pointer leaves both boxes.
|
|
@@ -7485,7 +8337,7 @@
|
|
|
7485
8337
|
else {
|
|
7486
8338
|
displacement = { x: 0, y: 0 };
|
|
7487
8339
|
}
|
|
7488
|
-
const nudgeDuration = (
|
|
8340
|
+
const nudgeDuration = (_w = (_v = this.options.pointerAvoidance) === null || _v === void 0 ? void 0 : _v.transitionDuration) !== null && _w !== void 0 ? _w : 200;
|
|
7489
8341
|
const animateNudge = () => {
|
|
7490
8342
|
const progress = now < nudge.start ? 0 : nudgeDuration > 0 ? Math.min(1, (now - nudge.start) / nudgeDuration) : 1;
|
|
7491
8343
|
const eased = progress * progress * (3 - 2 * progress);
|
|
@@ -7501,7 +8353,7 @@
|
|
|
7501
8353
|
fromY: nudge.y,
|
|
7502
8354
|
targetX: displacement.x,
|
|
7503
8355
|
targetY: displacement.y,
|
|
7504
|
-
start: now + ((
|
|
8356
|
+
start: now + ((_y = (_x = this.options.pointerAvoidance) === null || _x === void 0 ? void 0 : _x.delay) !== null && _y !== void 0 ? _y : 180)
|
|
7505
8357
|
};
|
|
7506
8358
|
}
|
|
7507
8359
|
if (animateNudge() < 1 && (nudge.fromX !== nudge.targetX || nudge.fromY !== nudge.targetY)) {
|
|
@@ -7512,10 +8364,10 @@
|
|
|
7512
8364
|
ele.style.left = `${coordinates.x + position.x * coordinates.scaleX}px`;
|
|
7513
8365
|
ele.style.top = `${coordinates.y + position.y * coordinates.scaleY}px`;
|
|
7514
8366
|
for (const clickTarget of Array.from(content.querySelectorAll('[role="button"]'))) {
|
|
7515
|
-
clickTarget.setAttribute("aria-label", placement.members.length ? `Select ${placement.entityIds.length} entities` : `Select ${(
|
|
8367
|
+
clickTarget.setAttribute("aria-label", placement.members.length ? `Select ${placement.entityIds.length} entities` : `Select ${(_0 = (_z = this.options.text) !== null && _z !== void 0 ? _z : text) !== null && _0 !== void 0 ? _0 : (_1 = this.rego) === null || _1 === void 0 ? void 0 : _1.entityId}`);
|
|
7516
8368
|
}
|
|
7517
8369
|
if (leader) {
|
|
7518
|
-
const nodeRadius = ringAnimation.outer;
|
|
8370
|
+
const nodeRadius = ringAnimation.outer * (1 - (frame.boxBlend || 0));
|
|
7519
8371
|
const anchor = this.anchor;
|
|
7520
8372
|
anchor.style.left = `${coordinates.x + (frame.anchorX - nodeRadius) * coordinates.scaleX}px`;
|
|
7521
8373
|
anchor.style.top = `${coordinates.y + (frame.anchorY - nodeRadius) * coordinates.scaleY}px`;
|
|
@@ -7528,11 +8380,12 @@
|
|
|
7528
8380
|
const circle = svg.firstElementChild;
|
|
7529
8381
|
circle.setAttribute("cx", String(nodeRadius));
|
|
7530
8382
|
circle.setAttribute("cy", String(nodeRadius));
|
|
7531
|
-
|
|
8383
|
+
const innerRadius = ringAnimation.inner * (1 - (frame.boxBlend || 0));
|
|
8384
|
+
circle.setAttribute("r", String((nodeRadius + innerRadius) / 2));
|
|
7532
8385
|
circle.setAttribute("stroke", this.options.leaderColor || "rgba(255, 255, 255, 0.8)");
|
|
7533
|
-
circle.setAttribute("stroke-width", String(nodeRadius -
|
|
8386
|
+
circle.setAttribute("stroke-width", String(nodeRadius - innerRadius));
|
|
7534
8387
|
anchor.style.boxShadow = radius ? "none" : "0 0 5px rgba(0, 0, 0, 0.3)";
|
|
7535
|
-
anchor.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount));
|
|
8388
|
+
anchor.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount) * (1 - (frame.boxBlend || 0)));
|
|
7536
8389
|
anchor.style.zIndex = String(placement.zIndex);
|
|
7537
8390
|
// The stem starts at the rim, never across the transparent centre, even after passive displacement.
|
|
7538
8391
|
const straightX = Math.max(position.x, Math.min(frame.anchorX, position.x + width));
|
|
@@ -7552,13 +8405,13 @@
|
|
|
7552
8405
|
// Keep the stem inside the 126-degree arc, away from antialiased endpoints.
|
|
7553
8406
|
const arcFraction = 0.35;
|
|
7554
8407
|
const rotationLimit = Math.PI * arcFraction - Math.PI / 30;
|
|
7555
|
-
const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((
|
|
8408
|
+
const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_2 = ringAnimation.arcOffset) !== null && _2 !== void 0 ? _2 : 0);
|
|
7556
8409
|
const targetOpen = cursorMotion.target || (hovered && (ring === null || ring === void 0 ? void 0 : ring.enabled) !== false && targetRadius > 0) ? 1 : 0;
|
|
7557
|
-
const elapsed = Math.min(32, Math.max(0, now - ((
|
|
8410
|
+
const elapsed = Math.min(32, Math.max(0, now - ((_3 = ringAnimation.arcTime) !== null && _3 !== void 0 ? _3 : now)));
|
|
7558
8411
|
const blend = duration > 0 ? 1 - Math.exp(-elapsed / (duration / 3)) : 1;
|
|
7559
8412
|
const approach = (value, target) => Math.abs(value - target) < 0.001 ? target : value + (target - value) * blend;
|
|
7560
|
-
ringAnimation.arcOffset = approach((
|
|
7561
|
-
ringAnimation.arcOpen = approach((
|
|
8413
|
+
ringAnimation.arcOffset = approach((_4 = ringAnimation.arcOffset) !== null && _4 !== void 0 ? _4 : 0, targetOffset);
|
|
8414
|
+
ringAnimation.arcOpen = approach((_5 = ringAnimation.arcOpen) !== null && _5 !== void 0 ? _5 : 0, targetOpen);
|
|
7562
8415
|
ringAnimation.arcTime = now;
|
|
7563
8416
|
if (ringAnimation.arcOffset !== targetOffset || ringAnimation.arcOpen !== targetOpen) {
|
|
7564
8417
|
this.viewer.scene.requestRender();
|
|
@@ -7584,6 +8437,12 @@
|
|
|
7584
8437
|
element.style.zIndex = String(placement.zIndex);
|
|
7585
8438
|
};
|
|
7586
8439
|
draw(lineEle, startX, startY, endX, endY);
|
|
8440
|
+
if (layer.modelBoxes.Hull(this.id)) {
|
|
8441
|
+
layer.modelBoxes.Request(this.id, {
|
|
8442
|
+
whole: Boolean(cursorMotion.target), from: { x: frame.anchorX, y: frame.anchorY },
|
|
8443
|
+
opacity, zIndex: placement.zIndex, color: this.options.leaderColor || "rgba(255, 255, 255, 0.8)", duration
|
|
8444
|
+
});
|
|
8445
|
+
}
|
|
7587
8446
|
if (runEle) {
|
|
7588
8447
|
// Overlap the corner by the stem's own width so the elbow has no notch in it.
|
|
7589
8448
|
const overlap = frame.anchorX < runToX ? -1 : 1;
|
|
@@ -7596,6 +8455,7 @@
|
|
|
7596
8455
|
ele.style.display = "flex";
|
|
7597
8456
|
ele.style.visibility = "hidden";
|
|
7598
8457
|
this.renderRemoval = layer.Add({ project, apply });
|
|
8458
|
+
leaveLayer === null || leaveLayer === void 0 ? void 0 : leaveLayer();
|
|
7599
8459
|
// If terrain changes then we'll re-call createLabel as the position may have changed.
|
|
7600
8460
|
let terrTimeout = null;
|
|
7601
8461
|
this.terrChangedRemoval = this.viewer.scene.terrainProviderChanged.addEventListener(() => {
|
|
@@ -7741,7 +8601,7 @@
|
|
|
7741
8601
|
// The glyph plus the chip's padding of a third either side, enlarged for a photo.
|
|
7742
8602
|
const diameter = (size + 2 * (size / 3)) * PHOTO_SIZE_SCALE;
|
|
7743
8603
|
// Enough pixels to stay sharp at the hover size.
|
|
7744
|
-
const pixels =
|
|
8604
|
+
const pixels = photoPixels(size, true);
|
|
7745
8605
|
const canvas = document.createElement("canvas");
|
|
7746
8606
|
canvas.className = "entity-label-photo";
|
|
7747
8607
|
canvas.style.cssText = "display:block;box-sizing:border-box;border:2px solid rgba(255,255,255,0.9);box-shadow:0 1px 3px rgba(0,0,0,0.35);";
|
|
@@ -7752,13 +8612,15 @@
|
|
|
7752
8612
|
// Resting size, set once the photo's shape is known.
|
|
7753
8613
|
let width = 0;
|
|
7754
8614
|
let height = 0;
|
|
8615
|
+
// How far hovering grows it, which a small image limits.
|
|
8616
|
+
let hoverScale = PHOTO_HOVER_SCALE;
|
|
7755
8617
|
let grown = false;
|
|
7756
8618
|
const grow = (on) => {
|
|
7757
8619
|
if (on === grown || !width) {
|
|
7758
8620
|
return;
|
|
7759
8621
|
}
|
|
7760
8622
|
grown = on;
|
|
7761
|
-
const scale = on ?
|
|
8623
|
+
const scale = on ? hoverScale : 1;
|
|
7762
8624
|
canvas.style.width = `${width * scale}px`;
|
|
7763
8625
|
canvas.style.height = `${height * scale}px`;
|
|
7764
8626
|
this.renderFor(PHOTO_TRANSITION);
|
|
@@ -7776,11 +8638,15 @@
|
|
|
7776
8638
|
(_a = canvas.getContext("2d")) === null || _a === void 0 ? void 0 : _a.drawImage(photo.source, 0, 0);
|
|
7777
8639
|
const longest = Math.max(photo.width, photo.height);
|
|
7778
8640
|
const circle = PhotoShape(photo.width, photo.height) === "circle";
|
|
7779
|
-
|
|
7780
|
-
|
|
8641
|
+
// Shown no larger than the image has pixels for, one to a CSS pixel, and never smaller than the icon.
|
|
8642
|
+
const native = circle ? Math.min(photo.width, photo.height) : longest;
|
|
8643
|
+
const across = Math.min(diameter, Math.max(size, native));
|
|
8644
|
+
hoverScale = Math.max(1, Math.min(PHOTO_HOVER_SCALE, native / across));
|
|
8645
|
+
width = circle ? across : across * photo.width / longest;
|
|
8646
|
+
height = circle ? across : across * photo.height / longest;
|
|
7781
8647
|
canvas.style.width = `${width}px`;
|
|
7782
8648
|
canvas.style.height = `${height}px`;
|
|
7783
|
-
canvas.style.borderRadius = circle ? "50%" : `${
|
|
8649
|
+
canvas.style.borderRadius = circle ? "50%" : `${across * PHOTO_CORNER}px`;
|
|
7784
8650
|
canvas.style.opacity = "1";
|
|
7785
8651
|
placePhoto === null || placePhoto === void 0 ? void 0 : placePhoto(circle ? "circle" : "rect", grow);
|
|
7786
8652
|
return true;
|
|
@@ -7827,6 +8693,57 @@
|
|
|
7827
8693
|
};
|
|
7828
8694
|
step();
|
|
7829
8695
|
}
|
|
8696
|
+
/*
|
|
8697
|
+
* Reads the record's geometry box, preferring an entity a label group already hydrated.
|
|
8698
|
+
*/
|
|
8699
|
+
async loadModelBox() {
|
|
8700
|
+
var _a, _b, _c, _d;
|
|
8701
|
+
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;
|
|
8702
|
+
let entity = ((_d = known === null || known === void 0 ? void 0 : known.Bruce) === null || _d === void 0 ? void 0 : _d.GeometryPivotExtents) ? known : null;
|
|
8703
|
+
if (!entity && this.api) {
|
|
8704
|
+
try {
|
|
8705
|
+
entity = (await BModels.Entity.Get({ api: this.api, entityId: this.rego.entityId, expandLocation: true, migrated: true })).entity;
|
|
8706
|
+
}
|
|
8707
|
+
// Expanding location fails for entities outside an assembly, which have no box to give.
|
|
8708
|
+
catch {
|
|
8709
|
+
return null;
|
|
8710
|
+
}
|
|
8711
|
+
}
|
|
8712
|
+
// A file uploaded before the API stored oriented boxes has only the record's axis-aligned extents.
|
|
8713
|
+
return GetModelBox(entity, ReadOrientedBox(entity));
|
|
8714
|
+
}
|
|
8715
|
+
/*
|
|
8716
|
+
* Eases the stem between the ring and the model box, returning how far onto the box it is this frame.
|
|
8717
|
+
*/
|
|
8718
|
+
morphToBox(boxed, duration) {
|
|
8719
|
+
const morph = this.boxMorph;
|
|
8720
|
+
const now = Date.now();
|
|
8721
|
+
const ease = (progress) => progress * progress * (3 - 2 * progress);
|
|
8722
|
+
const progress = (start) => duration > 0 ? Math.min(1, Math.max(0, (now - start) / duration)) : 1;
|
|
8723
|
+
const hadBox = morph.target === 1;
|
|
8724
|
+
const target = boxed ? 1 : 0;
|
|
8725
|
+
if (morph.target !== target) {
|
|
8726
|
+
Object.assign(morph, { from: morph.value, target, start: now });
|
|
8727
|
+
}
|
|
8728
|
+
morph.value = morph.from + (morph.target - morph.from) * ease(progress(morph.start));
|
|
8729
|
+
// Settled, so a later frame with a longer duration does not replay the ease.
|
|
8730
|
+
if (morph.value === morph.target) {
|
|
8731
|
+
morph.from = morph.target;
|
|
8732
|
+
}
|
|
8733
|
+
if (boxed) {
|
|
8734
|
+
// Moving to another outline, such as a hovered part pulled out of its group, eases the stem across.
|
|
8735
|
+
if (hadBox && morph.anchor && morph.group !== boxed.group) {
|
|
8736
|
+
morph.shift = { x: morph.anchor.x - boxed.anchor.x, y: morph.anchor.y - boxed.anchor.y, start: now };
|
|
8737
|
+
}
|
|
8738
|
+
morph.group = boxed.group;
|
|
8739
|
+
const remaining = 1 - ease(progress(morph.shift.start));
|
|
8740
|
+
morph.anchor = { x: boxed.anchor.x + morph.shift.x * remaining, y: boxed.anchor.y + morph.shift.y * remaining };
|
|
8741
|
+
}
|
|
8742
|
+
if (morph.value !== morph.target || (boxed && progress(morph.shift.start) < 1)) {
|
|
8743
|
+
this.viewer.scene.requestRender();
|
|
8744
|
+
}
|
|
8745
|
+
return morph.value;
|
|
8746
|
+
}
|
|
7830
8747
|
/**
|
|
7831
8748
|
* Removes label from the viewer and unsubscribes from events.
|
|
7832
8749
|
*/
|
|
@@ -12888,6 +13805,13 @@
|
|
|
12888
13805
|
this.SyncLabelHover();
|
|
12889
13806
|
this.viewer.scene.requestRender();
|
|
12890
13807
|
}
|
|
13808
|
+
/*
|
|
13809
|
+
* Whether the label hover is on this Entity, with or without a cursor position.
|
|
13810
|
+
*/
|
|
13811
|
+
IsLabelHovered(entityId, menuItemId) {
|
|
13812
|
+
const hover = this.labelHover;
|
|
13813
|
+
return Boolean(entityId) && (hover === null || hover === void 0 ? void 0 : hover.entityId) === entityId && (hover.menuItemId == null || hover.menuItemId === menuItemId);
|
|
13814
|
+
}
|
|
12891
13815
|
GetLabelHoverPosition(entityId, menuItemId) {
|
|
12892
13816
|
const hover = this.labelHover;
|
|
12893
13817
|
return (hover === null || hover === void 0 ? void 0 : hover.entityId) === entityId && (hover.menuItemId == null || hover.menuItemId === menuItemId) ? hover.position : undefined;
|
|
@@ -51668,7 +52592,7 @@ void main() {
|
|
|
51668
52592
|
StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
|
|
51669
52593
|
})(exports.StyleUtils || (exports.StyleUtils = {}));
|
|
51670
52594
|
|
|
51671
|
-
const VERSION = "7.5.
|
|
52595
|
+
const VERSION = "7.5.8";
|
|
51672
52596
|
/**
|
|
51673
52597
|
* Updates the environment instance used by bruce-cesium to one specified.
|
|
51674
52598
|
* This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.
|