bruce-cesium 7.5.7 → 7.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bruce-cesium.es5.js +1918 -330
- package/dist/bruce-cesium.es5.js.map +1 -1
- package/dist/bruce-cesium.umd.js +1916 -328
- package/dist/bruce-cesium.umd.js.map +1 -1
- package/dist/lib/bruce-cesium.js +1 -1
- package/dist/lib/rendering/displaced-surface-primitive.js +187 -36
- package/dist/lib/rendering/displaced-surface-primitive.js.map +1 -1
- package/dist/lib/rendering/entity-render-engine-polygon.js +20 -9
- package/dist/lib/rendering/entity-render-engine-polygon.js.map +1 -1
- package/dist/lib/rendering/render-managers/common/entity-label.js +975 -57
- package/dist/lib/rendering/render-managers/common/entity-label.js.map +1 -1
- package/dist/lib/rendering/texture-frame-series-animator.js +220 -20
- package/dist/lib/rendering/texture-frame-series-animator.js.map +1 -1
- package/dist/lib/rendering/texture-value-labels.js +478 -194
- package/dist/lib/rendering/texture-value-labels.js.map +1 -1
- package/dist/lib/rendering/visuals-register.js +34 -9
- package/dist/lib/rendering/visuals-register.js.map +1 -1
- package/dist/types/bruce-cesium.d.ts +1 -1
- package/dist/types/rendering/displaced-surface-primitive.d.ts +32 -0
- package/dist/types/rendering/entity-render-engine-polygon.d.ts +1 -0
- package/dist/types/rendering/render-managers/common/entity-label.d.ts +134 -20
- package/dist/types/rendering/texture-frame-series-animator.d.ts +42 -0
- package/dist/types/rendering/texture-value-labels.d.ts +59 -11
- package/dist/types/rendering/visuals-register.d.ts +7 -0
- package/package.json +2 -2
|
@@ -62,6 +62,24 @@ var EntityLabel;
|
|
|
62
62
|
const PHOTO_CACHE_LIMIT = 500;
|
|
63
63
|
// Key where the viewer keeps its downsized photos.
|
|
64
64
|
const PHOTO_CACHE_KEY = "_nextspaceLabelPhotos";
|
|
65
|
+
// Clearance around a model's box, as a share of its half-extent on each axis.
|
|
66
|
+
const MODEL_BOX_PADDING = 0.02;
|
|
67
|
+
// Share of each edge drawn out from its corners while the box is idle. Hovering draws the edges whole.
|
|
68
|
+
const MODEL_BOX_BRACKET = 0.2;
|
|
69
|
+
// Under this many pixels across, a box reads as a smudge and the ring takes over.
|
|
70
|
+
const MODEL_BOX_MIN_SIZE = 24;
|
|
71
|
+
// Stem length above the box outline when the options do not set one.
|
|
72
|
+
const MODEL_BOX_STEM = 32;
|
|
73
|
+
// Gap in pixels a waiting box needs from a drawn one before it shows, so neighbours never trade places.
|
|
74
|
+
const MODEL_BOX_CLEARANCE = 12;
|
|
75
|
+
// Longest a model's label stays hidden waiting to learn whether it has a box, before showing its ring anyway.
|
|
76
|
+
const MODEL_BOX_WAIT = 1500;
|
|
77
|
+
// Half the length of edge drawn either side of where a stem lands, when brackets would leave it bare.
|
|
78
|
+
const MODEL_BOX_LANDING = 10;
|
|
79
|
+
// Past this many touching boxes one box around them all stands in for tracing their union.
|
|
80
|
+
const MODEL_BOX_UNION_LIMIT = 24;
|
|
81
|
+
// 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.
|
|
82
|
+
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]];
|
|
65
83
|
// One per tinted icon, since an SVG mask is addressed by id across the whole document.
|
|
66
84
|
let tintCounter = 0;
|
|
67
85
|
// Key where the label instance is stored within the cesium entity.
|
|
@@ -69,8 +87,6 @@ var EntityLabel;
|
|
|
69
87
|
EntityLabel.ATTACH_KEY = "nextspace-navigator-entity-label";
|
|
70
88
|
// Image types a label draws, read from the file extension. Anything else stays text.
|
|
71
89
|
const IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp"];
|
|
72
|
-
// Shortest side a thumbnail needs before it is preferred over the full file.
|
|
73
|
-
const IMAGE_THUMBNAIL_MIN = 64;
|
|
74
90
|
// Size a found image is drawn at when the group names none, so a photo never lands at its natural size.
|
|
75
91
|
const IMAGE_SIZE = 24;
|
|
76
92
|
function isImageFile(name) {
|
|
@@ -99,12 +115,24 @@ var EntityLabel;
|
|
|
99
115
|
return (url.protocol === "http:" || url.protocol === "https:") && isImageFile(url.pathname);
|
|
100
116
|
}
|
|
101
117
|
EntityLabel.IsImageUrl = IsImageUrl;
|
|
118
|
+
/*
|
|
119
|
+
* Device pixels across a photo needs to stay sharp at a chip size, at rest or grown on hover.
|
|
120
|
+
*/
|
|
121
|
+
function photoPixels(size, hover) {
|
|
122
|
+
// The glyph plus the chip's padding of a third either side, enlarged for a photo.
|
|
123
|
+
const diameter = (size + 2 * (size / 3)) * PHOTO_SIZE_SCALE;
|
|
124
|
+
// Picking an image can run with no window, where a screen pixel is a CSS pixel.
|
|
125
|
+
const ratio = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1;
|
|
126
|
+
return Math.ceil(diameter * (hover ? PHOTO_HOVER_SCALE : 1) * ratio);
|
|
127
|
+
}
|
|
102
128
|
/*
|
|
103
129
|
* The entity's default image as InfoView picks it: its first photo attachment by display order that is an image file.
|
|
104
|
-
*
|
|
130
|
+
*
|
|
131
|
+
* Reads attachments expanded onto the entity. Prefers the smallest thumbnail sharp at the hover size, then the
|
|
132
|
+
* largest still sharp at rest, so a low resolution thumbnail is never stretched while the full file is unread.
|
|
105
133
|
*/
|
|
106
134
|
function GetDefaultImageUrl(entity, api) {
|
|
107
|
-
var _a, _b, _c;
|
|
135
|
+
var _a, _b, _c, _d;
|
|
108
136
|
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 : [];
|
|
109
137
|
const photo = attachments
|
|
110
138
|
.filter(x => (x === null || x === void 0 ? void 0 : x["EntityAttachmentType.ID"]) == bruce_models_1.EntityAttachmentType.EType.Photo && x.ClientFile)
|
|
@@ -113,9 +141,12 @@ var EntityLabel;
|
|
|
113
141
|
if (!photo) {
|
|
114
142
|
return null;
|
|
115
143
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
144
|
+
// How far across a thumbnail is shown: a circle crops to its shortest side, a rectangle keeps its longest.
|
|
145
|
+
const across = (x) => PhotoShape(x.Width, x.Height) === "circle" ? Math.min(x.Width, x.Height) : Math.max(x.Width, x.Height);
|
|
146
|
+
const thumbnails = ((_c = photo.ClientFile.Thumbnails) !== null && _c !== void 0 ? _c : [])
|
|
147
|
+
.filter(x => x.Width > 0 && x.Height > 0 && (x.URL || x["ClientFile.ID"]))
|
|
148
|
+
.sort((a, b) => a.Width * a.Height - b.Width * b.Height);
|
|
149
|
+
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));
|
|
119
150
|
if (thumbnail) {
|
|
120
151
|
return thumbnail.URL || bruce_models_1.ClientFile.GetUrl({ fileId: thumbnail["ClientFile.ID"], api, viaCdn: true });
|
|
121
152
|
}
|
|
@@ -628,6 +659,7 @@ var EntityLabel;
|
|
|
628
659
|
// Participants whose failure has been logged, so a label that throws every frame logs once.
|
|
629
660
|
this.failed = new WeakSet();
|
|
630
661
|
this.placementMemory = new PlacementMemory();
|
|
662
|
+
this.modelBoxes = new ModelBoxes();
|
|
631
663
|
this.forwardingWheel = false;
|
|
632
664
|
this.pendingDrag = null;
|
|
633
665
|
// Null outside a 3D view.
|
|
@@ -866,6 +898,12 @@ var EntityLabel;
|
|
|
866
898
|
x: (pointer.x - rect.left) * this.viewer.canvas.clientWidth / rect.width,
|
|
867
899
|
y: (pointer.y - rect.top) * this.viewer.canvas.clientHeight / rect.height
|
|
868
900
|
} : undefined;
|
|
901
|
+
try {
|
|
902
|
+
this.modelBoxes.Resolve(this.viewer, this.pointer);
|
|
903
|
+
}
|
|
904
|
+
catch (e) {
|
|
905
|
+
console.error("EntityLabel: model boxes failed to resolve.", e);
|
|
906
|
+
}
|
|
869
907
|
// This runs inside Cesium's frame, where one throwing label would stop rendering for the whole scene.
|
|
870
908
|
const projected = new Map();
|
|
871
909
|
for (const participant of this.participants) {
|
|
@@ -887,6 +925,12 @@ var EntityLabel;
|
|
|
887
925
|
this.logFailure(participant, e);
|
|
888
926
|
}
|
|
889
927
|
}
|
|
928
|
+
try {
|
|
929
|
+
this.modelBoxes.Draw(this.viewer, this.element, this.coordinates);
|
|
930
|
+
}
|
|
931
|
+
catch (e) {
|
|
932
|
+
console.error("EntityLabel: model boxes failed to draw.", e);
|
|
933
|
+
}
|
|
890
934
|
}
|
|
891
935
|
logFailure(participant, error) {
|
|
892
936
|
if (this.failed.has(participant)) {
|
|
@@ -897,6 +941,728 @@ var EntityLabel;
|
|
|
897
941
|
}
|
|
898
942
|
}
|
|
899
943
|
let nextLabelId = 0;
|
|
944
|
+
/*
|
|
945
|
+
* The record's oriented box in the same Z-up local space as GeometryPivot, or null when it has none.
|
|
946
|
+
*/
|
|
947
|
+
function ReadOrientedBox(entity) {
|
|
948
|
+
var _a, _b;
|
|
949
|
+
const box = (_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.GeometryOrientedBox;
|
|
950
|
+
const vector = (value) => (value === null || value === void 0 ? void 0 : value.length) === 3 && value.every((item) => Number.isFinite(+item)) ? value.map(Number) : null;
|
|
951
|
+
const centre = vector(box === null || box === void 0 ? void 0 : box.Centre), half = vector(box === null || box === void 0 ? void 0 : box.HalfExtents);
|
|
952
|
+
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;
|
|
953
|
+
return centre && half && (axes === null || axes === void 0 ? void 0 : axes.every(Boolean)) ? { centre, axes, half } : null;
|
|
954
|
+
}
|
|
955
|
+
EntityLabel.ReadOrientedBox = ReadOrientedBox;
|
|
956
|
+
/*
|
|
957
|
+
* The eight padded world corners of an assembly Entity's geometry box, or null when the record cannot give one.
|
|
958
|
+
*
|
|
959
|
+
* GeometryPivotExtents are Z-up half-extents about GeometryPivot, and the placement is the one the API uses to put
|
|
960
|
+
* Bruce.Location at the pivot: ENU(root) * UCS translation * UCS rotation * AssemblyWorldPosition.
|
|
961
|
+
*/
|
|
962
|
+
function GetModelBox(entity, shape) {
|
|
963
|
+
var _a, _b, _c;
|
|
964
|
+
const bruce = entity === null || entity === void 0 ? void 0 : entity.Bruce;
|
|
965
|
+
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);
|
|
966
|
+
const matrix = bruce === null || bruce === void 0 ? void 0 : bruce.AssemblyWorldPosition;
|
|
967
|
+
const root = bruce === null || bruce === void 0 ? void 0 : bruce.AssemblyRootLocation;
|
|
968
|
+
if ((half === null || half === void 0 ? void 0 : half.length) !== 3 || !half.every(value => Number.isFinite(value) && value >= 0) || Math.max(...half) < 0.01) {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
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))) {
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
const pivot = ((_c = bruce.GeometryPivot) === null || _c === void 0 ? void 0 : _c.length) >= 3 ? bruce.GeometryPivot.slice(0, 3).map(Number) : [0, 0, 0];
|
|
975
|
+
// Without an oriented box the record's own extents are an axis-aligned box about the pivot.
|
|
976
|
+
const box = shape || { centre: pivot, axes: [[1, 0, 0], [0, 1, 0], [0, 0, 1]], half: half };
|
|
977
|
+
const transform = bruce.AssemblyWorldTransform;
|
|
978
|
+
const placement = Cesium.Transforms.eastNorthUpToFixedFrame(Cesium.Cartesian3.fromDegrees(+root.longitude, +root.latitude, (0, js_utils_2.EnsureNumber)(root.altitude)));
|
|
979
|
+
Cesium.Matrix4.multiply(placement, Cesium.Matrix4.fromTranslation(new Cesium.Cartesian3((0, js_utils_2.EnsureNumber)(transform === null || transform === void 0 ? void 0 : transform.x), (0, js_utils_2.EnsureNumber)(transform === null || transform === void 0 ? void 0 : transform.y), (0, js_utils_2.EnsureNumber)(transform === null || transform === void 0 ? void 0 : transform.z))), placement);
|
|
980
|
+
Cesium.Matrix4.multiply(placement, Cesium.Matrix4.fromRotation(Cesium.Matrix3.fromHeadingPitchRoll(Cesium.HeadingPitchRoll.fromDegrees((0, js_utils_2.EnsureNumber)(transform === null || transform === void 0 ? void 0 : transform.heading), (0, js_utils_2.EnsureNumber)(transform === null || transform === void 0 ? void 0 : transform.pitch), (0, js_utils_2.EnsureNumber)(transform === null || transform === void 0 ? void 0 : transform.roll)))), placement);
|
|
981
|
+
Cesium.Matrix4.multiply(placement, new Cesium.Matrix4(...matrix.flat().map(Number)), placement);
|
|
982
|
+
// Pad thin axes by a share of the longest one, so a flat part still gets a visible gap.
|
|
983
|
+
const longest = Math.max(...box.half);
|
|
984
|
+
const reach = box.half.map(value => value + Math.max(value, longest * 0.25) * MODEL_BOX_PADDING);
|
|
985
|
+
const corners = [];
|
|
986
|
+
for (let i = 0; i < 8; i++) {
|
|
987
|
+
const signs = [i & 1 ? 1 : -1, i & 2 ? 1 : -1, i & 4 ? 1 : -1];
|
|
988
|
+
const local = new Cesium.Cartesian3(...[0, 1, 2].map(k => box.centre[k] +
|
|
989
|
+
signs[0] * reach[0] * box.axes[0][k] + signs[1] * reach[1] * box.axes[1][k] + signs[2] * reach[2] * box.axes[2][k]));
|
|
990
|
+
const corner = Cesium.Matrix4.multiplyByPoint(placement, local, new Cesium.Cartesian3());
|
|
991
|
+
if (![corner.x, corner.y, corner.z].every(Number.isFinite)) {
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
corners.push(corner);
|
|
995
|
+
}
|
|
996
|
+
return corners;
|
|
997
|
+
}
|
|
998
|
+
EntityLabel.GetModelBox = GetModelBox;
|
|
999
|
+
const vAdd = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
1000
|
+
const vSub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
1001
|
+
const vScale = (a, s) => [a[0] * s, a[1] * s, a[2] * s];
|
|
1002
|
+
const vDot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
1003
|
+
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]];
|
|
1004
|
+
const vLength = (a) => Math.hypot(a[0], a[1], a[2]);
|
|
1005
|
+
/*
|
|
1006
|
+
* Reads a solid back from the eight corners GetModelBox gives, relative to the origin.
|
|
1007
|
+
*/
|
|
1008
|
+
function ModelSolid(corners, origin) {
|
|
1009
|
+
const local = corners.map((corner) => [corner.x - origin.x, corner.y - origin.y, corner.z - origin.z]);
|
|
1010
|
+
const spans = [1, 2, 4].map(bit => vSub(local[bit], local[0]));
|
|
1011
|
+
const half = spans.map(span => vLength(span) / 2);
|
|
1012
|
+
return {
|
|
1013
|
+
centre: vScale(vAdd(local[0], local[7]), 0.5),
|
|
1014
|
+
axes: spans.map((span, k) => vScale(span, 0.5 / (half[k] || 1))),
|
|
1015
|
+
half
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
EntityLabel.ModelSolid = ModelSolid;
|
|
1019
|
+
function solidCorners(solid) {
|
|
1020
|
+
const corners = [];
|
|
1021
|
+
for (let i = 0; i < 8; i++) {
|
|
1022
|
+
corners.push([0, 1, 2].reduce((point, k) => vAdd(point, vScale(solid.axes[k], solid.half[k] * (i & (1 << k) ? 1 : -1))), solid.centre));
|
|
1023
|
+
}
|
|
1024
|
+
return corners;
|
|
1025
|
+
}
|
|
1026
|
+
function solidContains(solid, point, inset = 0) {
|
|
1027
|
+
const offset = vSub(point, solid.centre);
|
|
1028
|
+
return solid.axes.every((axis, k) => Math.abs(vDot(offset, axis)) < solid.half[k] - inset);
|
|
1029
|
+
}
|
|
1030
|
+
/*
|
|
1031
|
+
* Whether two solids overlap, by the separating axis test. Padding makes parts that sit against each other overlap.
|
|
1032
|
+
*/
|
|
1033
|
+
function ModelSolidsTouch(a, b) {
|
|
1034
|
+
const between = vSub(b.centre, a.centre);
|
|
1035
|
+
const axes = [...a.axes, ...b.axes];
|
|
1036
|
+
for (const first of a.axes) {
|
|
1037
|
+
for (const second of b.axes) {
|
|
1038
|
+
const axis = vCross(first, second);
|
|
1039
|
+
const length = vLength(axis);
|
|
1040
|
+
if (length > 1e-6)
|
|
1041
|
+
axes.push(vScale(axis, 1 / length));
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
const reach = (solid, axis) => solid.axes.reduce((sum, own, k) => sum + solid.half[k] * Math.abs(vDot(own, axis)), 0);
|
|
1045
|
+
return axes.every(axis => Math.abs(vDot(between, axis)) <= reach(a, axis) + reach(b, axis));
|
|
1046
|
+
}
|
|
1047
|
+
EntityLabel.ModelSolidsTouch = ModelSolidsTouch;
|
|
1048
|
+
/*
|
|
1049
|
+
* One box around every solid, turned like the largest of them, for groups too big to trace.
|
|
1050
|
+
*/
|
|
1051
|
+
function encloseSolids(solids) {
|
|
1052
|
+
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);
|
|
1053
|
+
const corners = solids.flatMap(solidCorners);
|
|
1054
|
+
const ranges = largest.axes.map(axis => {
|
|
1055
|
+
const values = corners.map(corner => vDot(corner, axis));
|
|
1056
|
+
return [Math.min(...values), Math.max(...values)];
|
|
1057
|
+
});
|
|
1058
|
+
return {
|
|
1059
|
+
centre: largest.axes.reduce((point, axis, k) => vAdd(point, vScale(axis, (ranges[k][0] + ranges[k][1]) / 2)), [0, 0, 0]),
|
|
1060
|
+
axes: largest.axes,
|
|
1061
|
+
half: ranges.map(([low, high]) => (high - low) / 2)
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
/*
|
|
1065
|
+
* Narrows [low, high] to where a + b * t stays within -half..half.
|
|
1066
|
+
*/
|
|
1067
|
+
function clipSlab(range, a, b, half) {
|
|
1068
|
+
if (Math.abs(b) < 1e-12) {
|
|
1069
|
+
if (Math.abs(a) > half)
|
|
1070
|
+
range[1] = -Infinity;
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const first = (-half - a) / b, second = (half - a) / b;
|
|
1074
|
+
range[0] = Math.max(range[0], Math.min(first, second));
|
|
1075
|
+
range[1] = Math.min(range[1], Math.max(first, second));
|
|
1076
|
+
}
|
|
1077
|
+
/*
|
|
1078
|
+
* The edges of the union of the solids: where its surface folds, leaving out seams across flat shared faces.
|
|
1079
|
+
*/
|
|
1080
|
+
function ModelBoxOutline(solids) {
|
|
1081
|
+
const span = Math.max(...solids.map(solid => vLength(solid.half)));
|
|
1082
|
+
const thinnest = Math.min(...solids.flatMap(solid => solid.half));
|
|
1083
|
+
const radius = Math.max(1e-6, Math.min(thinnest * 0.05, span * 1e-3));
|
|
1084
|
+
const tolerance = Math.max(1, span) * 1e-6;
|
|
1085
|
+
const candidates = [];
|
|
1086
|
+
for (const solid of solids) {
|
|
1087
|
+
const corners = solidCorners(solid);
|
|
1088
|
+
for (const [a, b] of MODEL_BOX_EDGES)
|
|
1089
|
+
candidates.push([corners[a], corners[b]]);
|
|
1090
|
+
}
|
|
1091
|
+
const faces = (solid) => [0, 1, 2].flatMap(k => [1, -1].map(sign => ({ solid, k, normal: vScale(solid.axes[k], sign) })));
|
|
1092
|
+
for (let i = 0; i < solids.length; i++) {
|
|
1093
|
+
for (let j = i + 1; j < solids.length; j++) {
|
|
1094
|
+
if (!ModelSolidsTouch(solids[i], solids[j])) {
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
for (const first of faces(solids[i])) {
|
|
1098
|
+
for (const second of faces(solids[j])) {
|
|
1099
|
+
const direction = vCross(first.normal, second.normal);
|
|
1100
|
+
const sine = vLength(direction);
|
|
1101
|
+
// Faces this close to parallel meet along edges the box edges already give.
|
|
1102
|
+
if (sine < 0.05) {
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
const d1 = vDot(first.normal, first.solid.centre) + first.solid.half[first.k];
|
|
1106
|
+
const d2 = vDot(second.normal, second.solid.centre) + second.solid.half[second.k];
|
|
1107
|
+
const cosine = vDot(first.normal, second.normal);
|
|
1108
|
+
const base = vScale(vAdd(vScale(first.normal, d1 - d2 * cosine), vScale(second.normal, d2 - d1 * cosine)), 1 / (sine * sine));
|
|
1109
|
+
const unit = vScale(direction, 1 / sine);
|
|
1110
|
+
const range = [-Infinity, Infinity];
|
|
1111
|
+
for (const face of [first, second]) {
|
|
1112
|
+
const offset = vSub(base, face.solid.centre);
|
|
1113
|
+
for (let m = 0; m < 3; m++) {
|
|
1114
|
+
if (m !== face.k)
|
|
1115
|
+
clipSlab(range, vDot(offset, face.solid.axes[m]), vDot(unit, face.solid.axes[m]), face.solid.half[m]);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
if (range[1] - range[0] > tolerance) {
|
|
1119
|
+
candidates.push([vAdd(base, vScale(unit, range[0])), vAdd(base, vScale(unit, range[1]))]);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
const inside = (point) => solids.some(solid => solidContains(solid, point));
|
|
1126
|
+
/*
|
|
1127
|
+
* Whether the union folds along a direction at a point, from which way round it the inside lies.
|
|
1128
|
+
*/
|
|
1129
|
+
const folds = (point, along) => {
|
|
1130
|
+
const first = vCross(along, Math.abs(along[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0]);
|
|
1131
|
+
const e1 = vScale(first, 1 / vLength(first));
|
|
1132
|
+
const e2 = vCross(along, e1);
|
|
1133
|
+
const samples = [];
|
|
1134
|
+
for (let i = 0; i < 16; i++) {
|
|
1135
|
+
// Offset from the axes so no sample lands exactly on a face through the point.
|
|
1136
|
+
const angle = (i + 0.37) * Math.PI / 8;
|
|
1137
|
+
samples.push(inside(vAdd(point, vAdd(vScale(e1, Math.cos(angle) * radius), vScale(e2, Math.sin(angle) * radius)))));
|
|
1138
|
+
}
|
|
1139
|
+
const filled = samples.filter(Boolean).length;
|
|
1140
|
+
const changes = samples.filter((sample, i) => sample !== samples[(i + 1) % 16]).length;
|
|
1141
|
+
return filled > 0 && filled < 16 && !(filled === 8 && changes === 2);
|
|
1142
|
+
};
|
|
1143
|
+
const lines = [];
|
|
1144
|
+
for (const [start, end] of candidates) {
|
|
1145
|
+
const delta = vSub(end, start);
|
|
1146
|
+
const length = vLength(delta);
|
|
1147
|
+
if (length < tolerance) {
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
const along = vScale(delta, 1 / length);
|
|
1151
|
+
const cuts = [0, 1];
|
|
1152
|
+
for (const solid of solids) {
|
|
1153
|
+
const offset = vSub(start, solid.centre);
|
|
1154
|
+
solid.axes.forEach((axis, k) => {
|
|
1155
|
+
const rate = vDot(delta, axis);
|
|
1156
|
+
if (Math.abs(rate) < 1e-12)
|
|
1157
|
+
return;
|
|
1158
|
+
for (const bound of [-solid.half[k], solid.half[k]]) {
|
|
1159
|
+
const t = (bound - vDot(offset, axis)) / rate;
|
|
1160
|
+
if (t > 0 && t < 1)
|
|
1161
|
+
cuts.push(t);
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
cuts.sort((a, b) => a - b);
|
|
1166
|
+
for (let i = 0; i + 1 < cuts.length; i++) {
|
|
1167
|
+
if ((cuts[i + 1] - cuts[i]) * length < tolerance || !folds(vAdd(start, vScale(delta, (cuts[i] + cuts[i + 1]) / 2)), along)) {
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
// Merged by line, so a shared edge draws once and a run cut by other boxes is one stroke.
|
|
1171
|
+
const from = vAdd(start, vScale(delta, cuts[i])), to = vAdd(start, vScale(delta, cuts[i + 1]));
|
|
1172
|
+
let line = lines.find(item => vLength(vCross(item.direction, along)) < 1e-6 && vLength(vCross(vSub(from, item.origin), item.direction)) < tolerance);
|
|
1173
|
+
if (!line) {
|
|
1174
|
+
line = { origin: from, direction: along, spans: [] };
|
|
1175
|
+
lines.push(line);
|
|
1176
|
+
}
|
|
1177
|
+
const a = vDot(vSub(from, line.origin), line.direction), b = vDot(vSub(to, line.origin), line.direction);
|
|
1178
|
+
line.spans.push([Math.min(a, b), Math.max(a, b)]);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
const outline = [];
|
|
1182
|
+
for (const line of lines) {
|
|
1183
|
+
line.spans.sort((a, b) => a[0] - b[0]);
|
|
1184
|
+
let current = line.spans[0];
|
|
1185
|
+
for (const next of [...line.spans.slice(1), null]) {
|
|
1186
|
+
if (next && next[0] <= current[1] + tolerance) {
|
|
1187
|
+
current = [current[0], Math.max(current[1], next[1])];
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
outline.push([vAdd(line.origin, vScale(line.direction, current[0])), vAdd(line.origin, vScale(line.direction, current[1]))]);
|
|
1191
|
+
current = next;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return outline;
|
|
1195
|
+
}
|
|
1196
|
+
EntityLabel.ModelBoxOutline = ModelBoxOutline;
|
|
1197
|
+
/*
|
|
1198
|
+
* Whether the sight line from the eye to a point passes through any solid before reaching it.
|
|
1199
|
+
*/
|
|
1200
|
+
function solidsHide(solids, eye, point, inset) {
|
|
1201
|
+
const ray = vSub(point, eye);
|
|
1202
|
+
const length = vLength(ray);
|
|
1203
|
+
// Stop just short of the point so the face it lies on does not count against it.
|
|
1204
|
+
const reach = Math.max(0, 1 - inset * 100 / (length || 1));
|
|
1205
|
+
return solids.some(solid => {
|
|
1206
|
+
const offset = vSub(eye, solid.centre);
|
|
1207
|
+
const range = [0, reach];
|
|
1208
|
+
solid.axes.forEach((axis, k) => clipSlab(range, vDot(offset, axis), vDot(ray, axis), solid.half[k] - inset));
|
|
1209
|
+
return range[0] < range[1];
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
/*
|
|
1213
|
+
* Window positions of a box's corners, or null when one is behind the camera and the box would draw inside out.
|
|
1214
|
+
*/
|
|
1215
|
+
function projectCorners(viewer, corners) {
|
|
1216
|
+
const camera = viewer.camera;
|
|
1217
|
+
const cameraPosition = camera.positionWC || camera.position;
|
|
1218
|
+
const direction = camera.directionWC || camera.direction;
|
|
1219
|
+
const transforms = Cesium.SceneTransforms;
|
|
1220
|
+
const projectPosition = transforms.wgs84ToWindowCoordinates || transforms.worldToWindowCoordinates;
|
|
1221
|
+
const points = [];
|
|
1222
|
+
const relative = new Cesium.Cartesian3();
|
|
1223
|
+
for (const corner of corners) {
|
|
1224
|
+
if (Cesium.Cartesian3.dot(Cesium.Cartesian3.subtract(corner, cameraPosition, relative), direction) <= 0) {
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
const point = projectPosition(viewer.scene, corner);
|
|
1228
|
+
if (!Number.isFinite(point === null || point === void 0 ? void 0 : point.x) || !Number.isFinite(point === null || point === void 0 ? void 0 : point.y)) {
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
1231
|
+
points.push({ x: point.x, y: point.y });
|
|
1232
|
+
}
|
|
1233
|
+
return points;
|
|
1234
|
+
}
|
|
1235
|
+
/*
|
|
1236
|
+
* Whether two convex outlines come within a margin of each other, by the separating axis test.
|
|
1237
|
+
*/
|
|
1238
|
+
function HullsNear(a, b, margin) {
|
|
1239
|
+
for (const hull of [a, b]) {
|
|
1240
|
+
for (let i = 0; i < hull.length; i++) {
|
|
1241
|
+
const from = hull[i], to = hull[(i + 1) % hull.length];
|
|
1242
|
+
const length = Math.hypot(to.x - from.x, to.y - from.y);
|
|
1243
|
+
if (length < 1e-9) {
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
const normal = { x: (from.y - to.y) / length, y: (to.x - from.x) / length };
|
|
1247
|
+
const project = (points) => points.map(point => point.x * normal.x + point.y * normal.y);
|
|
1248
|
+
const first = project(a), second = project(b);
|
|
1249
|
+
if (Math.max(Math.min(...second) - Math.max(...first), Math.min(...first) - Math.max(...second)) >= margin) {
|
|
1250
|
+
return false;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
EntityLabel.HullsNear = HullsNear;
|
|
1257
|
+
/*
|
|
1258
|
+
* Decides which labels' model boxes are drawn and how, so a multi-selection does not stack box over box.
|
|
1259
|
+
*/
|
|
1260
|
+
class ModelBoxes {
|
|
1261
|
+
constructor() {
|
|
1262
|
+
// Whether a box competes for space comes only from inputs (camera distance, hover, the pointer), never from
|
|
1263
|
+
// how its label was laid out, so an animation moving a label cannot feed back into which boxes show.
|
|
1264
|
+
this.entries = new Map();
|
|
1265
|
+
this.groups = null;
|
|
1266
|
+
// The hovered Entity. Its box is drawn on its own and whole, ahead of every other box.
|
|
1267
|
+
this.hovered = null;
|
|
1268
|
+
this.outlines = new Map();
|
|
1269
|
+
this.hulls = new Map();
|
|
1270
|
+
this.frames = new Map();
|
|
1271
|
+
this.drawn = [];
|
|
1272
|
+
// When each label's box started showing; the longer it has shown, the stronger its claim on the space.
|
|
1273
|
+
this.since = new Map();
|
|
1274
|
+
this.requests = new Map();
|
|
1275
|
+
this.looks = new Map();
|
|
1276
|
+
this.memberLooks = new Map();
|
|
1277
|
+
}
|
|
1278
|
+
Set(id, corners, entityId, inputs) {
|
|
1279
|
+
const current = this.entries.get(id);
|
|
1280
|
+
// A label rebuilt over the same box keeps its place and how far it has animated.
|
|
1281
|
+
if (corners && (current === null || current === void 0 ? void 0 : current.corners) === corners && current.entityId === entityId) {
|
|
1282
|
+
Object.assign(current, inputs);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
if (!corners && !current) {
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (corners) {
|
|
1289
|
+
this.entries.set(id, { ...inputs, corners, entityId, active: false, wanted: false });
|
|
1290
|
+
}
|
|
1291
|
+
else
|
|
1292
|
+
this.entries.delete(id);
|
|
1293
|
+
this.since.delete(id);
|
|
1294
|
+
this.groups = null;
|
|
1295
|
+
for (const [key, group] of this.outlines) {
|
|
1296
|
+
if (group.members.includes(id))
|
|
1297
|
+
this.outlines.delete(key);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
/*
|
|
1301
|
+
* The box drawn for a label this frame, or undefined when it has none or lost its place.
|
|
1302
|
+
*/
|
|
1303
|
+
Frame(id) {
|
|
1304
|
+
return this.frames.get(id);
|
|
1305
|
+
}
|
|
1306
|
+
/*
|
|
1307
|
+
* The label's own box on screen whether or not it is drawn, so hovering the model can ask for it.
|
|
1308
|
+
*/
|
|
1309
|
+
Hull(id) {
|
|
1310
|
+
return this.hulls.get(id);
|
|
1311
|
+
}
|
|
1312
|
+
Request(id, request) {
|
|
1313
|
+
this.requests.set(id, request);
|
|
1314
|
+
}
|
|
1315
|
+
group() {
|
|
1316
|
+
const ids = Array.from(this.entries.keys()).filter(id => this.entries.get(id).wanted).sort();
|
|
1317
|
+
if (!ids.length) {
|
|
1318
|
+
return [];
|
|
1319
|
+
}
|
|
1320
|
+
const origin = this.entries.get(ids[0]).corners[0];
|
|
1321
|
+
const solids = new Map(ids.map(id => [id, ModelSolid(this.entries.get(id).corners, origin)]));
|
|
1322
|
+
const parent = new Map(ids.map(id => [id, id]));
|
|
1323
|
+
const root = (id) => parent.get(id) === id ? id : root(parent.get(id));
|
|
1324
|
+
for (let i = 0; i < ids.length; i++) {
|
|
1325
|
+
const a = solids.get(ids[i]);
|
|
1326
|
+
for (let j = i + 1; j < ids.length; j++) {
|
|
1327
|
+
const b = solids.get(ids[j]);
|
|
1328
|
+
if ((this.entries.get(ids[i]).entityId === this.hovered) !== (this.entries.get(ids[j]).entityId === this.hovered)) {
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
// Cheap sphere check first; most pairs in a scene are nowhere near each other.
|
|
1332
|
+
if (vLength(vSub(a.centre, b.centre)) > vLength(a.half) + vLength(b.half) || !ModelSolidsTouch(a, b)) {
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
parent.set(root(ids[j]), root(ids[i]));
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
const members = new Map();
|
|
1339
|
+
for (const id of ids) {
|
|
1340
|
+
const key = root(id);
|
|
1341
|
+
if (!members.has(key))
|
|
1342
|
+
members.set(key, []);
|
|
1343
|
+
members.get(key).push(id);
|
|
1344
|
+
}
|
|
1345
|
+
const groups = Array.from(members.values()).map(list => {
|
|
1346
|
+
const key = list.join("|");
|
|
1347
|
+
let group = this.outlines.get(key);
|
|
1348
|
+
if (!group) {
|
|
1349
|
+
const groupOrigin = this.entries.get(list[0]).corners[0];
|
|
1350
|
+
const parts = list.map(id => ModelSolid(this.entries.get(id).corners, groupOrigin));
|
|
1351
|
+
const solidsShown = parts.length > MODEL_BOX_UNION_LIMIT ? [encloseSolids(parts)] : parts;
|
|
1352
|
+
group = { key, members: list, hovered: false, origin: groupOrigin, solids: solidsShown, outline: ModelBoxOutline(solidsShown) };
|
|
1353
|
+
}
|
|
1354
|
+
group.hovered = this.entries.get(list[0]).entityId === this.hovered;
|
|
1355
|
+
return group;
|
|
1356
|
+
});
|
|
1357
|
+
// Kept across hover changes, so pulling a part out and putting it back does not trace the union again.
|
|
1358
|
+
const kept = Array.from(this.outlines.values()).filter(group => !groups.includes(group)).slice(-8);
|
|
1359
|
+
this.outlines = new Map([...kept, ...groups].map(group => [group.key, group]));
|
|
1360
|
+
return groups;
|
|
1361
|
+
}
|
|
1362
|
+
/*
|
|
1363
|
+
* Projects every box for this frame and settles which outlines are drawn, before labels place their stems.
|
|
1364
|
+
*/
|
|
1365
|
+
Resolve(viewer, pointer) {
|
|
1366
|
+
var _a, _b, _c, _d;
|
|
1367
|
+
this.frames.clear();
|
|
1368
|
+
this.hulls.clear();
|
|
1369
|
+
this.drawn = [];
|
|
1370
|
+
const points = new Map();
|
|
1371
|
+
for (const [id, entry] of this.entries) {
|
|
1372
|
+
const projected = projectCorners(viewer, entry.corners);
|
|
1373
|
+
if (projected) {
|
|
1374
|
+
points.set(id, projected);
|
|
1375
|
+
this.hulls.set(id, convexHull(projected));
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
// The application's hover, which is what its temporary label shows, decides first. Without one the
|
|
1379
|
+
// pointer does: the part it is on holds until it leaves, else the smallest box under it wins.
|
|
1380
|
+
const under = Array.from(this.hulls.entries()).filter(([, hull]) => pointer && hullContains(hull, pointer));
|
|
1381
|
+
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);
|
|
1382
|
+
if (!hovered && under.length) {
|
|
1383
|
+
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;
|
|
1384
|
+
}
|
|
1385
|
+
if (hovered !== this.hovered) {
|
|
1386
|
+
this.hovered = hovered;
|
|
1387
|
+
this.groups = null;
|
|
1388
|
+
}
|
|
1389
|
+
for (const [id, entry] of this.entries) {
|
|
1390
|
+
const hull = this.hulls.get(id);
|
|
1391
|
+
entry.active = Boolean(((_c = entry.near) === null || _c === void 0 ? void 0 : _c.call(entry)) || entry.entityId === hovered || (pointer && hull && hullContains(hull, pointer)));
|
|
1392
|
+
// A box keeps its place while it fades out, and leaves the competition once it has.
|
|
1393
|
+
const wanted = entry.active || ((_d = this.memberLooks.get(id)) === null || _d === void 0 ? void 0 : _d.shown) > 0.01;
|
|
1394
|
+
if (wanted !== entry.wanted) {
|
|
1395
|
+
entry.wanted = wanted;
|
|
1396
|
+
this.groups = null;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
if (!this.groups) {
|
|
1400
|
+
this.groups = this.group();
|
|
1401
|
+
}
|
|
1402
|
+
const canvas = viewer.canvas;
|
|
1403
|
+
const candidates = [];
|
|
1404
|
+
for (const group of this.groups) {
|
|
1405
|
+
if (!group.members.every(id => points.has(id))) {
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
const all = group.members.flatMap(id => points.get(id));
|
|
1409
|
+
const xs = all.map(point => point.x), ys = all.map(point => point.y);
|
|
1410
|
+
const width = Math.max(...xs) - Math.min(...xs), height = Math.max(...ys) - Math.min(...ys);
|
|
1411
|
+
if (Math.max(width, height) < MODEL_BOX_MIN_SIZE || width > canvas.clientWidth * 2 || height > canvas.clientHeight * 2) {
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
const since = Math.min(...group.members.map(id => { var _a; return (_a = this.since.get(id)) !== null && _a !== void 0 ? _a : Infinity; }));
|
|
1415
|
+
const outline = convexHull(all);
|
|
1416
|
+
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) });
|
|
1417
|
+
}
|
|
1418
|
+
// Between boxes appearing together the larger on screen goes first, as the one the view is about.
|
|
1419
|
+
// What the user points at now outranks what they selected before it.
|
|
1420
|
+
candidates.sort((a, b) => (Number(b.group.hovered) - Number(a.group.hovered) ||
|
|
1421
|
+
Number(b.selected) - Number(a.selected) ||
|
|
1422
|
+
a.since - b.since ||
|
|
1423
|
+
b.area - a.area ||
|
|
1424
|
+
a.group.key.localeCompare(b.group.key)));
|
|
1425
|
+
const transforms = Cesium.SceneTransforms;
|
|
1426
|
+
const projectPosition = transforms.wgs84ToWindowCoordinates || transforms.worldToWindowCoordinates;
|
|
1427
|
+
const scratch = new Cesium.Cartesian3();
|
|
1428
|
+
const now = Date.now();
|
|
1429
|
+
const shown = new Set();
|
|
1430
|
+
for (const candidate of candidates) {
|
|
1431
|
+
// A showing box holds its place until truly overlapped; a waiting one needs clear space.
|
|
1432
|
+
const margin = Number.isFinite(candidate.since) ? 0 : MODEL_BOX_CLEARANCE;
|
|
1433
|
+
if (this.drawn.some(other => HullsNear(candidate.outline, other.outline, margin))) {
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
const { origin } = candidate.group;
|
|
1437
|
+
const toScreen = (point) => {
|
|
1438
|
+
const projected = projectPosition(viewer.scene, Cesium.Cartesian3.fromElements(origin.x + point[0], origin.y + point[1], origin.z + point[2], scratch));
|
|
1439
|
+
return { x: projected.x, y: projected.y };
|
|
1440
|
+
};
|
|
1441
|
+
const edges = candidate.group.outline.map(([start, end]) => [toScreen(start), toScreen(end)]);
|
|
1442
|
+
const drawn = { ...candidate, edges, landings: [] };
|
|
1443
|
+
this.drawn.push(drawn);
|
|
1444
|
+
for (const id of candidate.group.members) {
|
|
1445
|
+
shown.add(id);
|
|
1446
|
+
if (!this.since.has(id))
|
|
1447
|
+
this.since.set(id, now);
|
|
1448
|
+
const own = points.get(id);
|
|
1449
|
+
// The silhouette's top is the highest drawn edge at that x, so the stem always meets a line.
|
|
1450
|
+
const x = (Math.min(...own.map(point => point.x)) + Math.max(...own.map(point => point.x))) / 2;
|
|
1451
|
+
let anchor = null, edge = -1;
|
|
1452
|
+
edges.forEach(([a, b], i) => {
|
|
1453
|
+
if (x < Math.min(a.x, b.x) || x > Math.max(a.x, b.x) || Math.abs(b.x - a.x) < 1e-6) {
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
const y = a.y + (b.y - a.y) * (x - a.x) / (b.x - a.x);
|
|
1457
|
+
if (!anchor || y < anchor.y) {
|
|
1458
|
+
anchor = { x, y };
|
|
1459
|
+
edge = i;
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
anchor = anchor || { x, y: hullTop(candidate.outline, x) };
|
|
1463
|
+
if (edge >= 0)
|
|
1464
|
+
drawn.landings.push({ edge, point: anchor });
|
|
1465
|
+
this.frames.set(id, { points: own, hull: this.hulls.get(id), outline: candidate.outline, anchor, group: candidate.group.key });
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
for (const id of Array.from(this.since.keys())) {
|
|
1469
|
+
if (!shown.has(id))
|
|
1470
|
+
this.since.delete(id);
|
|
1471
|
+
}
|
|
1472
|
+
this.requests.clear();
|
|
1473
|
+
}
|
|
1474
|
+
/*
|
|
1475
|
+
* Draws each settled outline as corner brackets that grow into whole edges, back edges dashed.
|
|
1476
|
+
*/
|
|
1477
|
+
Draw(viewer, element, coordinates) {
|
|
1478
|
+
var _a, _b, _c;
|
|
1479
|
+
const now = Date.now();
|
|
1480
|
+
const looks = new Map();
|
|
1481
|
+
const memberLooks = new Map();
|
|
1482
|
+
let animating = false;
|
|
1483
|
+
for (const { group } of this.drawn) {
|
|
1484
|
+
const requests = group.members.map(id => this.requests.get(id)).filter(Boolean);
|
|
1485
|
+
if (!requests.length) {
|
|
1486
|
+
// Its labels are hidden this frame, perhaps off screen; it keeps its look so it does not regrow.
|
|
1487
|
+
const kept = this.looks.get(group.key);
|
|
1488
|
+
if (kept) {
|
|
1489
|
+
looks.set(group.key, kept);
|
|
1490
|
+
group.members.forEach(id => memberLooks.set(id, kept));
|
|
1491
|
+
if (kept.svg)
|
|
1492
|
+
kept.svg.style.display = "none";
|
|
1493
|
+
}
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
// A group that gained or lost a member carries on from where its members' outline was.
|
|
1497
|
+
const previous = group.members.map(id => this.memberLooks.get(id)).filter(Boolean);
|
|
1498
|
+
const look = (_a = this.looks.get(group.key)) !== null && _a !== void 0 ? _a : {
|
|
1499
|
+
shown: Math.max(0, ...previous.map(item => item.shown)), whole: Math.max(0, ...previous.map(item => item.whole)),
|
|
1500
|
+
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
|
|
1501
|
+
};
|
|
1502
|
+
const elapsed = Math.min(32, Math.max(0, now - look.time));
|
|
1503
|
+
const duration = Math.min(...requests.map(request => request.duration));
|
|
1504
|
+
const blend = duration > 0 ? 1 - Math.exp(-elapsed / (duration / 3)) : 1;
|
|
1505
|
+
const approach = (value, to) => Math.abs(value - to) < 0.001 ? to : value + (to - value) * blend;
|
|
1506
|
+
const shownTarget = group.members.some(id => { var _a; return (_a = this.entries.get(id)) === null || _a === void 0 ? void 0 : _a.active; }) ? 1 : 0;
|
|
1507
|
+
const wholeTarget = group.hovered || requests.some(request => request.whole) ? 1 : 0;
|
|
1508
|
+
look.shown = approach(look.shown, shownTarget);
|
|
1509
|
+
look.whole = approach(look.whole, wholeTarget);
|
|
1510
|
+
look.grow = approach(look.grow, 1);
|
|
1511
|
+
look.time = now;
|
|
1512
|
+
animating = animating || look.shown !== shownTarget || look.whole !== wholeTarget || look.grow !== 1;
|
|
1513
|
+
looks.set(group.key, look);
|
|
1514
|
+
group.members.forEach(id => memberLooks.set(id, look));
|
|
1515
|
+
if (look.svg && this.looks.get(group.key) !== look) {
|
|
1516
|
+
// Inherited from an earlier group; claim it so that group's cleanup leaves it alone.
|
|
1517
|
+
for (const [key, other] of this.looks) {
|
|
1518
|
+
if (other.svg === look.svg && key !== group.key)
|
|
1519
|
+
other.svg = undefined;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
this.drawGroup(viewer, element, coordinates, this.drawn.find(item => item.group === group), look, requests);
|
|
1523
|
+
}
|
|
1524
|
+
for (const [key, look] of this.looks) {
|
|
1525
|
+
if (!looks.has(key))
|
|
1526
|
+
(_c = look.svg) === null || _c === void 0 ? void 0 : _c.remove();
|
|
1527
|
+
}
|
|
1528
|
+
this.looks = looks;
|
|
1529
|
+
this.memberLooks = memberLooks;
|
|
1530
|
+
if (animating) {
|
|
1531
|
+
viewer.scene.requestRender();
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
drawGroup(viewer, element, coordinates, drawn, look, requests) {
|
|
1535
|
+
const group = drawn.group;
|
|
1536
|
+
let svg = look.svg;
|
|
1537
|
+
if (!svg) {
|
|
1538
|
+
const ns = "http://www.w3.org/2000/svg";
|
|
1539
|
+
svg = document.createElementNS(ns, "svg");
|
|
1540
|
+
svg.setAttribute("class", "entity-label-model-box");
|
|
1541
|
+
svg.setAttribute("aria-hidden", "true");
|
|
1542
|
+
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));";
|
|
1543
|
+
for (const edges of ["back", "front"]) {
|
|
1544
|
+
const path = document.createElementNS(ns, "path");
|
|
1545
|
+
path.setAttribute("fill", "none");
|
|
1546
|
+
path.setAttribute("stroke-linecap", "round");
|
|
1547
|
+
path.dataset.edges = edges;
|
|
1548
|
+
svg.appendChild(path);
|
|
1549
|
+
}
|
|
1550
|
+
look.svg = svg;
|
|
1551
|
+
}
|
|
1552
|
+
if (svg.parentElement !== element) {
|
|
1553
|
+
element.appendChild(svg);
|
|
1554
|
+
}
|
|
1555
|
+
svg.style.display = "";
|
|
1556
|
+
const transforms = Cesium.SceneTransforms;
|
|
1557
|
+
const projectPosition = transforms.wgs84ToWindowCoordinates || transforms.worldToWindowCoordinates;
|
|
1558
|
+
const origin = group.origin;
|
|
1559
|
+
const scratch = new Cesium.Cartesian3();
|
|
1560
|
+
// A new box grows out of the ring it replaces.
|
|
1561
|
+
const grown = look.grow * look.grow * (3 - 2 * look.grow);
|
|
1562
|
+
const from = requests[0].from;
|
|
1563
|
+
const at = (point) => `${(from.x + (point.x - from.x) * grown) * coordinates.scaleX} ${(from.y + (point.y - from.y) * grown) * coordinates.scaleY}`;
|
|
1564
|
+
const toScreen = (point) => {
|
|
1565
|
+
const projected = projectPosition(viewer.scene, Cesium.Cartesian3.fromElements(origin.x + point[0], origin.y + point[1], origin.z + point[2], scratch));
|
|
1566
|
+
return at(projected);
|
|
1567
|
+
};
|
|
1568
|
+
const camera = viewer.camera.positionWC || viewer.camera.position;
|
|
1569
|
+
const eye = [camera.x - origin.x, camera.y - origin.y, camera.z - origin.z];
|
|
1570
|
+
const span = Math.max(...group.solids.map(solid => vLength(solid.half)));
|
|
1571
|
+
const inset = Math.max(1, span) * 1e-6;
|
|
1572
|
+
const fraction = MODEL_BOX_BRACKET + (0.5 - MODEL_BOX_BRACKET) * look.whole;
|
|
1573
|
+
const parts = fraction >= 0.499 ? [[0, 1]] : [[0, fraction], [1 - fraction, 1]];
|
|
1574
|
+
const paths = { front: "", back: "" };
|
|
1575
|
+
for (const [start, end] of group.outline) {
|
|
1576
|
+
const delta = vSub(end, start);
|
|
1577
|
+
// Sample visibility along the edge, since another member can hide part of it.
|
|
1578
|
+
const steps = group.solids.length > 1 ? Math.min(24, Math.max(2, Math.ceil(vLength(delta) / span * 12))) : 1;
|
|
1579
|
+
const visible = [];
|
|
1580
|
+
for (let i = 0; i < steps; i++) {
|
|
1581
|
+
visible.push(!solidsHide(group.solids, eye, vAdd(start, vScale(delta, (i + 0.5) / steps)), inset));
|
|
1582
|
+
}
|
|
1583
|
+
for (const [from, to] of parts) {
|
|
1584
|
+
let runStart = from;
|
|
1585
|
+
for (let i = Math.floor(from * steps); i < steps && i / steps < to; i++) {
|
|
1586
|
+
const runEnd = Math.min(to, (i + 1) / steps);
|
|
1587
|
+
if (runEnd < to && visible[i + 1] === visible[i]) {
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
paths[visible[i] ? "front" : "back"] += `M${toScreen(vAdd(start, vScale(delta, runStart)))}L${toScreen(vAdd(start, vScale(delta, runEnd)))}`;
|
|
1591
|
+
runStart = runEnd;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
// Each stem lands on an edge, which brackets may leave bare, so a short piece of it is drawn there.
|
|
1596
|
+
if (fraction < 0.499) {
|
|
1597
|
+
for (const { edge, point } of drawn.landings) {
|
|
1598
|
+
const [a, b] = drawn.edges[edge];
|
|
1599
|
+
const length = Math.hypot(b.x - a.x, b.y - a.y);
|
|
1600
|
+
const reach = Math.min(MODEL_BOX_LANDING, length / 2);
|
|
1601
|
+
const toward = (sign) => {
|
|
1602
|
+
const along = Math.max(0, Math.min(length, Math.hypot(point.x - a.x, point.y - a.y) + sign * reach));
|
|
1603
|
+
return { x: a.x + (b.x - a.x) * along / length, y: a.y + (b.y - a.y) * along / length };
|
|
1604
|
+
};
|
|
1605
|
+
paths.front += `M${at(toward(-1))}L${at(toward(1))}`;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
svg.style.left = `${coordinates.x}px`;
|
|
1609
|
+
svg.style.top = `${coordinates.y}px`;
|
|
1610
|
+
svg.style.opacity = String(Math.max(...requests.map(request => request.opacity)) * look.shown * Math.min(1, look.grow * 2));
|
|
1611
|
+
svg.style.zIndex = String(Math.max(...requests.map(request => request.zIndex)));
|
|
1612
|
+
const color = requests[0].color;
|
|
1613
|
+
for (const path of Array.from(svg.children)) {
|
|
1614
|
+
const front = path.dataset.edges === "front";
|
|
1615
|
+
path.setAttribute("d", paths[front ? "front" : "back"]);
|
|
1616
|
+
path.setAttribute("stroke", color);
|
|
1617
|
+
path.setAttribute("stroke-width", front ? "1.5" : "1");
|
|
1618
|
+
path.setAttribute("stroke-opacity", front ? "1" : "0.35");
|
|
1619
|
+
path.setAttribute("stroke-dasharray", front ? "" : "3 3");
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
EntityLabel.ModelBoxes = ModelBoxes;
|
|
1624
|
+
/*
|
|
1625
|
+
* Andrew's monotone chain, counter-clockwise in screen space.
|
|
1626
|
+
*/
|
|
1627
|
+
function convexHull(points) {
|
|
1628
|
+
const sorted = [...points].sort((a, b) => a.x - b.x || a.y - b.y);
|
|
1629
|
+
const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
|
1630
|
+
const build = (list) => {
|
|
1631
|
+
const chain = [];
|
|
1632
|
+
for (const point of list) {
|
|
1633
|
+
while (chain.length >= 2 && cross(chain[chain.length - 2], chain[chain.length - 1], point) <= 0)
|
|
1634
|
+
chain.pop();
|
|
1635
|
+
chain.push(point);
|
|
1636
|
+
}
|
|
1637
|
+
chain.pop();
|
|
1638
|
+
return chain;
|
|
1639
|
+
};
|
|
1640
|
+
return [...build(sorted), ...build(sorted.reverse())];
|
|
1641
|
+
}
|
|
1642
|
+
/*
|
|
1643
|
+
* Where the top of a convex outline crosses a vertical line, so a stem leaves the box without jumping between corners.
|
|
1644
|
+
*/
|
|
1645
|
+
function hullTop(hull, x) {
|
|
1646
|
+
let top = Infinity;
|
|
1647
|
+
for (let i = 0; i < hull.length; i++) {
|
|
1648
|
+
const a = hull[i];
|
|
1649
|
+
const b = hull[(i + 1) % hull.length];
|
|
1650
|
+
if (x < Math.min(a.x, b.x) || x > Math.max(a.x, b.x)) {
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
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));
|
|
1654
|
+
}
|
|
1655
|
+
return top;
|
|
1656
|
+
}
|
|
1657
|
+
function hullArea(hull) {
|
|
1658
|
+
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;
|
|
1659
|
+
}
|
|
1660
|
+
function hullContains(hull, point) {
|
|
1661
|
+
return hull.length >= 3 && hull.every((a, i) => {
|
|
1662
|
+
const b = hull[(i + 1) % hull.length];
|
|
1663
|
+
return (b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x) >= 0;
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
900
1666
|
class Label {
|
|
901
1667
|
get HasText() { return this.options.presentation !== "icon"; }
|
|
902
1668
|
SetHoverVisible(visible) {
|
|
@@ -951,6 +1717,11 @@ var EntityLabel;
|
|
|
951
1717
|
this.Transient = false;
|
|
952
1718
|
this.hoverFade = { from: 0, to: 1, start: Date.now() };
|
|
953
1719
|
this.cursorMotion = { amount: 0, from: 0, target: 0, start: 0, position: undefined };
|
|
1720
|
+
// How far the stem is from the ring onto a model box, and the offset it eases out after changing outline.
|
|
1721
|
+
// The record's box for the Entity this label is on: undefined corners while reading, null when it has none.
|
|
1722
|
+
this.modelBox = null;
|
|
1723
|
+
this.boxMorph = { value: 0, from: 0, target: 0, start: 0, anchor: undefined, group: undefined,
|
|
1724
|
+
shift: { x: 0, y: 0, start: 0 } };
|
|
954
1725
|
// Counter to help differentiate between createLabel calls.
|
|
955
1726
|
this.counter = 0;
|
|
956
1727
|
// Whether this label has asked its group for a default image, which it does once on screen.
|
|
@@ -1036,11 +1807,13 @@ var EntityLabel;
|
|
|
1036
1807
|
* Recalling will be ignored.
|
|
1037
1808
|
*/
|
|
1038
1809
|
Dispose() {
|
|
1810
|
+
var _a;
|
|
1039
1811
|
if (this.disposed) {
|
|
1040
1812
|
return;
|
|
1041
1813
|
}
|
|
1042
1814
|
this.disposed = true;
|
|
1043
1815
|
this.removeLabel();
|
|
1816
|
+
(_a = layers.get(this.viewer)) === null || _a === void 0 ? void 0 : _a.modelBoxes.Set(this.id, null, null, null);
|
|
1044
1817
|
clearTimeout(this.loadTimeout);
|
|
1045
1818
|
this.loadTimeout = null;
|
|
1046
1819
|
}
|
|
@@ -1055,14 +1828,15 @@ var EntityLabel;
|
|
|
1055
1828
|
this.groupId = groupId;
|
|
1056
1829
|
this.additionalTexts = this.options.text != null ? [] : null;
|
|
1057
1830
|
this.photo = null;
|
|
1058
|
-
|
|
1831
|
+
// A label given its position keeps it, or the rebuild goes looking for the entity's own.
|
|
1832
|
+
this.createLabel(this.prePositioned ? this._pos3d : undefined);
|
|
1059
1833
|
}
|
|
1060
1834
|
/**
|
|
1061
1835
|
* Creates cesium entity for the label if one does not exist.
|
|
1062
1836
|
* @param starterPos3d optional position to create label at. Ignored for billboard labels.
|
|
1063
1837
|
*/
|
|
1064
1838
|
async createLabel(starterPos3d) {
|
|
1065
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
1839
|
+
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;
|
|
1066
1840
|
// The old label stays until its replacement is built, so a rebuild never blinks while a location loads.
|
|
1067
1841
|
let pos3d = starterPos3d ? starterPos3d.clone() : null;
|
|
1068
1842
|
const counter = ++this.counter;
|
|
@@ -1348,6 +2122,9 @@ var EntityLabel;
|
|
|
1348
2122
|
if (this.disposed || this.counter != counter) {
|
|
1349
2123
|
return;
|
|
1350
2124
|
}
|
|
2125
|
+
// Leave the layer only once rejoined, so a rebuild never empties it and closes it along with its boxes.
|
|
2126
|
+
const leaveLayer = this.renderRemoval;
|
|
2127
|
+
this.renderRemoval = null;
|
|
1351
2128
|
this.removeLabel();
|
|
1352
2129
|
// A found photo draws as if configured, without changing the options clustering compares and measures.
|
|
1353
2130
|
const photo = HasIcon(this.options) ? null : (_j = this.options.photo) !== null && _j !== void 0 ? _j : this.photo;
|
|
@@ -1644,9 +2421,65 @@ var EntityLabel;
|
|
|
1644
2421
|
radius: Math.max(width, height) / 2
|
|
1645
2422
|
};
|
|
1646
2423
|
};
|
|
2424
|
+
// Only CAD tilesets and model Entities are assembly parts with a geometry box; anything else stays a point.
|
|
2425
|
+
const boxable = Boolean(normalLeader && ((_r = this.rego) === null || _r === void 0 ? void 0 : _r.entityId) && ((visual instanceof Cesium.Cesium3DTileFeature && this.rego.tilesetType === bruce_models_1.Tileset.EType.Cad) ||
|
|
2426
|
+
(visual instanceof Cesium.Entity && Boolean(visual.model))));
|
|
2427
|
+
if (!boxable || ((_s = this.modelBox) === null || _s === void 0 ? void 0 : _s.entityId) !== ((_t = this.rego) === null || _t === void 0 ? void 0 : _t.entityId)) {
|
|
2428
|
+
layer.modelBoxes.Set(this.id, null, null, null);
|
|
2429
|
+
this.modelBox = null;
|
|
2430
|
+
}
|
|
2431
|
+
// Read once per Entity, so a label rebuilt for its loaded text keeps its box instead of waiting again.
|
|
2432
|
+
if (boxable && !this.modelBox) {
|
|
2433
|
+
const box = { entityId: this.rego.entityId, corners: undefined, load: null };
|
|
2434
|
+
box.load = this.loadModelBox().catch(() => null).then(corners => box.corners = corners || null);
|
|
2435
|
+
this.modelBox = box;
|
|
2436
|
+
}
|
|
2437
|
+
// A model's label stays hidden until its box is known, so a ring never shows only to turn into a box.
|
|
2438
|
+
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;
|
|
2439
|
+
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);
|
|
2440
|
+
const boxPendingUntil = Date.now() + MODEL_BOX_WAIT;
|
|
2441
|
+
// Whether this label has been on screen yet; a box found before then is drawn in place, not eased into.
|
|
2442
|
+
let shownOnce = false;
|
|
2443
|
+
if (boxable) {
|
|
2444
|
+
const found = this._pos3d;
|
|
2445
|
+
const settle = () => {
|
|
2446
|
+
if (boxPending && !this.disposed && this.counter === counter) {
|
|
2447
|
+
boxPending = false;
|
|
2448
|
+
this.viewer.scene.requestRender();
|
|
2449
|
+
}
|
|
2450
|
+
};
|
|
2451
|
+
// Past the wait a slow record no longer holds the label back; a box that comes later eases in.
|
|
2452
|
+
setTimeout(settle, MODEL_BOX_WAIT);
|
|
2453
|
+
this.modelBox.load.then(corners => {
|
|
2454
|
+
if (!corners || this.disposed || this.counter !== counter) {
|
|
2455
|
+
return;
|
|
2456
|
+
}
|
|
2457
|
+
// A record that disagrees with where the model was found describes another placement, such as a moved tileset.
|
|
2458
|
+
const centre = Cesium.Cartesian3.midpoint(corners[0], corners[7], new Cesium.Cartesian3());
|
|
2459
|
+
if (Cesium.Cartesian3.distance(centre, found) > Cesium.Cartesian3.distance(corners[0], corners[7]) * 0.75 + 2) {
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
2462
|
+
const rego = this.rego;
|
|
2463
|
+
// Let go a little further out than it is taken, so a camera resting at the limit does not flip it.
|
|
2464
|
+
let near = false;
|
|
2465
|
+
layer.modelBoxes.Set(this.id, corners, rego.entityId, {
|
|
2466
|
+
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); },
|
|
2467
|
+
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)); },
|
|
2468
|
+
near: () => {
|
|
2469
|
+
var _a, _b;
|
|
2470
|
+
const ring = this.options.anchorRing;
|
|
2471
|
+
const camera = this.viewer.camera.positionWC || this.viewer.camera.position;
|
|
2472
|
+
const limit = ((_a = ring === null || ring === void 0 ? void 0 : ring.nearDistance) !== null && _a !== void 0 ? _a : 100) * (near ? 1.1 : 1);
|
|
2473
|
+
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;
|
|
2474
|
+
return near || ((ring === null || ring === void 0 ? void 0 : ring.enabled) !== false && Boolean(this.cursorMotion.target));
|
|
2475
|
+
}
|
|
2476
|
+
});
|
|
2477
|
+
this.viewer.scene.requestRender();
|
|
2478
|
+
}).finally(settle);
|
|
2479
|
+
}
|
|
1647
2480
|
let cursorMotion = this.cursorMotion;
|
|
1648
2481
|
const project = () => {
|
|
1649
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
|
|
2482
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
|
|
1650
2483
|
if (this.disposed || !this.viewer || this.viewer.isDestroyed() || ((_a = this.visible) === null || _a === void 0 ? void 0 : _a.call(this)) === false ||
|
|
1651
2484
|
(presentation === "icon" && this.options.iconScale === 0)) {
|
|
1652
2485
|
return null;
|
|
@@ -1684,18 +2517,29 @@ var EntityLabel;
|
|
|
1684
2517
|
if (!Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.x) || !Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.y)) {
|
|
1685
2518
|
return null;
|
|
1686
2519
|
}
|
|
2520
|
+
const boxed = layer.modelBoxes.Frame(this.id);
|
|
2521
|
+
const waiting = boxPending && Date.now() < boxPendingUntil;
|
|
2522
|
+
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);
|
|
2523
|
+
// Eased between the ring and where the stem meets the box, so gaining or losing a box never jumps.
|
|
2524
|
+
const boxTarget = this.boxMorph.anchor;
|
|
2525
|
+
const ringPoint = { x: anchor.x + pixelOffset.x, y: anchor.y + pixelOffset.y };
|
|
2526
|
+
const boxAnchor = boxBlend > 0 && boxTarget ? {
|
|
2527
|
+
x: ringPoint.x + (boxTarget.x - ringPoint.x) * boxBlend,
|
|
2528
|
+
y: ringPoint.y + (boxTarget.y - ringPoint.y) * boxBlend
|
|
2529
|
+
} : undefined;
|
|
2530
|
+
const start = boxAnchor || anchor;
|
|
1687
2531
|
const onScreen = anchor.x >= 0 && anchor.y >= 0 && anchor.x < this.viewer.canvas.clientWidth && anchor.y < this.viewer.canvas.clientHeight;
|
|
1688
|
-
if (leader &&
|
|
2532
|
+
if (leader && (start.x < 0 || start.y < 0 || start.x >= this.viewer.canvas.clientWidth || start.y >= this.viewer.canvas.clientHeight))
|
|
1689
2533
|
return null;
|
|
1690
2534
|
if (onScreen && !this.imageAsked && this.groupId && !HasIcon(this.options) && !this.options.photo) {
|
|
1691
2535
|
this.imageAsked = true;
|
|
1692
|
-
(
|
|
2536
|
+
(_g = this.visualRegister) === null || _g === void 0 ? void 0 : _g.RequestLabelImage((_h = this.rego) === null || _h === void 0 ? void 0 : _h.entityId);
|
|
1693
2537
|
}
|
|
1694
2538
|
const transientPivot = (this.Transient && !this.Persistent ?
|
|
1695
|
-
(this.hoverFade.to === 0 ? this.retainedHoverPosition : (
|
|
2539
|
+
(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);
|
|
1696
2540
|
const hover = (presentation !== "icon" ?
|
|
1697
2541
|
(this.Transient && !this.Persistent && this.hoverFade.to === 0 ? this.retainedHoverPosition :
|
|
1698
|
-
(
|
|
2542
|
+
(_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);
|
|
1699
2543
|
const reused = !this.Transient || this.Persistent;
|
|
1700
2544
|
const now = Date.now();
|
|
1701
2545
|
const animateCursor = () => {
|
|
@@ -1722,12 +2566,14 @@ var EntityLabel;
|
|
|
1722
2566
|
const cursorAmount = cursorMotion.amount;
|
|
1723
2567
|
leader = normalLeader || cursorAmount > 0;
|
|
1724
2568
|
// An icon keeps its callout centred on the glyph rather than following the cursor across it.
|
|
1725
|
-
const icon = measureIcon();
|
|
2569
|
+
const icon = boxAnchor ? null : measureIcon();
|
|
1726
2570
|
const iconRing = icon ? Math.max(ICON_RING_MIN, icon.radius + ICON_RING_PADDING) : 0;
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
2571
|
+
// A boxed model is pinned the same way, with its stem leaving the top of the box outline.
|
|
2572
|
+
const pinned = Boolean(icon || boxAnchor);
|
|
2573
|
+
const pivot = pinned ? undefined : transientPivot;
|
|
2574
|
+
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;
|
|
2575
|
+
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;
|
|
2576
|
+
if (cursorAmount > 0 && !pinned) {
|
|
1731
2577
|
// A leader-less label starts centred on its anchor; preserve that box as it lifts away.
|
|
1732
2578
|
if (!normalLeader) {
|
|
1733
2579
|
x -= ele.offsetWidth / layer.coordinates.scaleX / 2;
|
|
@@ -1762,6 +2608,9 @@ var EntityLabel;
|
|
|
1762
2608
|
if (visual instanceof Cesium.Entity && (visual.polygon || visual.polyline))
|
|
1763
2609
|
leaderLength /= 3;
|
|
1764
2610
|
}
|
|
2611
|
+
if (boxAnchor && this.options.leaderLength == null) {
|
|
2612
|
+
leaderLength += (MODEL_BOX_STEM - leaderLength) * boxBlend;
|
|
2613
|
+
}
|
|
1765
2614
|
if (cursorAmount > 0) {
|
|
1766
2615
|
leaderLength += (96 - leaderLength) * cursorAmount;
|
|
1767
2616
|
}
|
|
@@ -1770,7 +2619,7 @@ var EntityLabel;
|
|
|
1770
2619
|
leaderLength += iconRing;
|
|
1771
2620
|
y = anchorY - leaderLength;
|
|
1772
2621
|
}
|
|
1773
|
-
else if (hover && !reused && !
|
|
2622
|
+
else if (hover && !reused && !pinned) {
|
|
1774
2623
|
x = hover.x + pixelOffset.x;
|
|
1775
2624
|
y = hover.y + pixelOffset.y - 24;
|
|
1776
2625
|
}
|
|
@@ -1779,7 +2628,7 @@ var EntityLabel;
|
|
|
1779
2628
|
const width = ele.offsetWidth / coordinates.scaleX;
|
|
1780
2629
|
const height = ele.offsetHeight / coordinates.scaleY;
|
|
1781
2630
|
let clusterBounds;
|
|
1782
|
-
if ((
|
|
2631
|
+
if ((_s = this.options.cluster) === null || _s === void 0 ? void 0 : _s.enabled) {
|
|
1783
2632
|
const textWidth = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetWidth) || 0;
|
|
1784
2633
|
const textHeight = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetHeight) || 0;
|
|
1785
2634
|
// A photo is never part of the footprint, so its arrival cannot change which cluster a label joins.
|
|
@@ -1813,7 +2662,7 @@ var EntityLabel;
|
|
|
1813
2662
|
if (!leader && (bounds.x + bounds.width < 0 || bounds.y + bounds.height < 0 || bounds.x > this.viewer.canvas.clientWidth || bounds.y > this.viewer.canvas.clientHeight)) {
|
|
1814
2663
|
return null;
|
|
1815
2664
|
}
|
|
1816
|
-
const state = (
|
|
2665
|
+
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 });
|
|
1817
2666
|
const opacity = this.prePositioned || hoverOnly || MAX_DISTANCE === Infinity ? 1 : 1 - distance / MAX_DISTANCE;
|
|
1818
2667
|
return {
|
|
1819
2668
|
canFlip: leader && presentation !== "icon", hasLeader: leader,
|
|
@@ -1821,24 +2670,25 @@ var EntityLabel;
|
|
|
1821
2670
|
transient: this.Transient || Boolean(hover) || cursorAmount > 0,
|
|
1822
2671
|
extendedGeometry: visual instanceof Cesium.Entity && Boolean(visual.polygon || visual.polyline || visual.corridor),
|
|
1823
2672
|
viewportWidth: this.viewer.canvas.clientWidth,
|
|
1824
|
-
id: this.id, entityKey: ((
|
|
1825
|
-
entityId: (
|
|
1826
|
-
iconRadius: icon === null || icon === void 0 ? void 0 : icon.radius,
|
|
2673
|
+
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,
|
|
2674
|
+
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,
|
|
2675
|
+
iconRadius: icon === null || icon === void 0 ? void 0 : icon.radius, modelBox: boxed || undefined, boxBlend,
|
|
1827
2676
|
// Priority shows the hovered member individually without dragging its cluster along.
|
|
1828
|
-
opacity, priority: hover || cursorAmount > 0 || (state === null || state === void 0 ? void 0 : state.highlighted) ? 1 : 0, options: this.options, text: originalText, clusterBounds
|
|
2677
|
+
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
|
|
1829
2678
|
};
|
|
1830
2679
|
};
|
|
1831
2680
|
let displacement = { x: 0, y: 0 };
|
|
1832
2681
|
let nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
|
|
1833
2682
|
const apply = (placement) => {
|
|
1834
|
-
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;
|
|
2683
|
+
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;
|
|
1835
2684
|
const show = (placement === null || placement === void 0 ? void 0 : placement.visible) && placement.opacity > 0;
|
|
2685
|
+
shownOnce = shownOnce || Boolean(show);
|
|
1836
2686
|
clickEntityIds = show ? placement.entityIds : [];
|
|
1837
2687
|
ele.style.visibility = show ? "visible" : "hidden";
|
|
1838
2688
|
lineEle.style.display = show && leader ? "block" : "none";
|
|
1839
2689
|
if (runEle)
|
|
1840
2690
|
runEle.style.display = show && leader ? "block" : "none";
|
|
1841
|
-
this.anchor.style.display = show && leader ? "block" : "none";
|
|
2691
|
+
this.anchor.style.display = show && leader && !(((_a = placement.frame) === null || _a === void 0 ? void 0 : _a.boxBlend) >= 1) ? "block" : "none";
|
|
1842
2692
|
if (!show) {
|
|
1843
2693
|
displacement = { x: 0, y: 0 };
|
|
1844
2694
|
nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
|
|
@@ -1850,7 +2700,7 @@ var EntityLabel;
|
|
|
1850
2700
|
return;
|
|
1851
2701
|
}
|
|
1852
2702
|
// A label standing for a cluster shows its summary only; its photo is put away and never fetched.
|
|
1853
|
-
const solo = !((
|
|
2703
|
+
const solo = !((_b = placement.members) === null || _b === void 0 ? void 0 : _b.length);
|
|
1854
2704
|
if (look.iconPhoto && iconElement && iconElement.dataset.failed !== "true") {
|
|
1855
2705
|
const display = solo ? "inline-flex" : "none";
|
|
1856
2706
|
if (iconElement.style.display !== display) {
|
|
@@ -1868,7 +2718,7 @@ var EntityLabel;
|
|
|
1868
2718
|
}
|
|
1869
2719
|
}, PHOTO_SHOW_DELAY);
|
|
1870
2720
|
}
|
|
1871
|
-
const displayText = (
|
|
2721
|
+
const displayText = (_c = placement.text) !== null && _c !== void 0 ? _c : originalText;
|
|
1872
2722
|
if (textElement && textElement.textContent !== displayText) {
|
|
1873
2723
|
textElement.textContent = displayText;
|
|
1874
2724
|
// Measure the summary's footprint next frame without replacing each member's source text.
|
|
@@ -1892,9 +2742,9 @@ var EntityLabel;
|
|
|
1892
2742
|
const stack = JSON.stringify(placement.icons || []);
|
|
1893
2743
|
if (iconStack && stack !== displayedStack) {
|
|
1894
2744
|
displayedStack = stack;
|
|
1895
|
-
const looks = ((
|
|
2745
|
+
const looks = ((_d = placement.icons) === null || _d === void 0 ? void 0 : _d.length) ? [iconLook(this.options), ...placement.icons] : [];
|
|
1896
2746
|
// One small size for all, so no icon in the arrangement reads as more important than another.
|
|
1897
|
-
const size = (this.options.iconSize || 24) * ((
|
|
2747
|
+
const size = (this.options.iconSize || 24) * ((_e = this.options.iconScale) !== null && _e !== void 0 ? _e : 1) * ICON_STACK_SCALE;
|
|
1898
2748
|
const built = looks.map(look => this.buildIcon({ ...look, iconSize: size }, counter));
|
|
1899
2749
|
// A stack is on show already, so its photos load straight away.
|
|
1900
2750
|
built.forEach(({ start }) => start === null || start === void 0 ? void 0 : start());
|
|
@@ -1929,9 +2779,11 @@ var EntityLabel;
|
|
|
1929
2779
|
// Summary and badge changes must fit on their first visible frame, including after resize.
|
|
1930
2780
|
const ring = this.options.anchorRing;
|
|
1931
2781
|
const pointer = (layer.pointer || (cursorMotion.target ? cursorMotion.position : undefined) || (this.Transient && !this.Persistent ?
|
|
1932
|
-
(this.hoverFade.to === 0 ? this.retainedHoverPosition : (
|
|
2782
|
+
(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));
|
|
1933
2783
|
const iconRing = frame.iconRadius ? Math.max(ICON_RING_MIN, frame.iconRadius + ICON_RING_PADDING) : 0;
|
|
1934
|
-
const
|
|
2784
|
+
const boxed = leader ? frame.modelBox : undefined;
|
|
2785
|
+
const hovered = pointer && (boxed ? hullContains(boxed.hull, pointer) :
|
|
2786
|
+
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));
|
|
1935
2787
|
if (look.iconPhoto && growPhoto) {
|
|
1936
2788
|
// A rectangle leans on the leader's side of the label.
|
|
1937
2789
|
if (photoShape === "rect") {
|
|
@@ -1942,15 +2794,15 @@ var EntityLabel;
|
|
|
1942
2794
|
}
|
|
1943
2795
|
// Grown in layout rather than scaled, so the border keeps its width, and the label, whose bottom
|
|
1944
2796
|
// sits on the leader, grows up and sideways without changing the leader's length.
|
|
1945
|
-
growPhoto(Boolean(hovered) && !((
|
|
2797
|
+
growPhoto(Boolean(hovered) && !((_k = placement.members) === null || _k === void 0 ? void 0 : _k.length));
|
|
1946
2798
|
}
|
|
1947
|
-
const normalRadius = (!leader || (ring === null || ring === void 0 ? void 0 : ring.enabled) === false ? 0 : Math.max(frame.distance <= ((
|
|
2799
|
+
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));
|
|
1948
2800
|
const targetRadius = normalRadius + (15.4 - normalRadius) * cursorMotion.amount;
|
|
1949
2801
|
// Around a glyph the ring keeps the glyph's size and only fades its stroke in, so it never covers the icon.
|
|
1950
2802
|
const targetOuter = iconRing || targetRadius || 2.5;
|
|
1951
2803
|
const targetInner = iconRing ? (targetRadius > 0 ? iconRing - 2 : iconRing) : Math.max(0, targetRadius - 2);
|
|
1952
2804
|
const now = Date.now();
|
|
1953
|
-
const duration = (
|
|
2805
|
+
const duration = (_p = ring === null || ring === void 0 ? void 0 : ring.transitionDuration) !== null && _p !== void 0 ? _p : 200;
|
|
1954
2806
|
const sharedRing = this.Transient && !this.Persistent && this.hoverFade.to === 1 ? this.hoverRingState : undefined;
|
|
1955
2807
|
let ringAnimation = sharedRing ? sharedRing.animation : this.ringAnimation;
|
|
1956
2808
|
if (!ringAnimation) {
|
|
@@ -1985,7 +2837,8 @@ var EntityLabel;
|
|
|
1985
2837
|
if (sharedRing) {
|
|
1986
2838
|
sharedRing.animation = ringAnimation;
|
|
1987
2839
|
}
|
|
1988
|
-
const
|
|
2840
|
+
const ringShare = 1 - (frame.boxBlend || 0);
|
|
2841
|
+
const radius = ringShare > 0 && ringAnimation.inner > 0 ? ringAnimation.outer * ringShare : 0;
|
|
1989
2842
|
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);
|
|
1990
2843
|
const position = { x: (stem || frame).x, y: (stem || frame).y };
|
|
1991
2844
|
const width = ele.offsetWidth / coordinates.scaleX;
|
|
@@ -1993,19 +2846,19 @@ var EntityLabel;
|
|
|
1993
2846
|
if (leader && angled) {
|
|
1994
2847
|
// The declutter owns the side; the fit only says which one had the room to begin with.
|
|
1995
2848
|
const run = Math.max(ANGLED_LEADER_RUN, radius + 4);
|
|
1996
|
-
const side = (
|
|
2849
|
+
const side = (_r = (_q = placement.textSide) !== null && _q !== void 0 ? _q : frame.leaderSide) !== null && _r !== void 0 ? _r : "right";
|
|
1997
2850
|
const beside = side === "left" ? frame.anchorX - run - width : frame.anchorX + run;
|
|
1998
2851
|
position.x = Math.max(0, Math.min(beside, this.viewer.canvas.clientWidth - width));
|
|
1999
2852
|
}
|
|
2000
2853
|
else if (leader && placement.textSide === "left") {
|
|
2001
2854
|
position.x = Math.max(0, Math.min(frame.anchorX - width, this.viewer.canvas.clientWidth - width));
|
|
2002
2855
|
}
|
|
2003
|
-
const gap = (
|
|
2856
|
+
const gap = (_t = (_s = this.options.pointerAvoidance) === null || _s === void 0 ? void 0 : _s.gap) !== null && _t !== void 0 ? _t : 12;
|
|
2004
2857
|
const containsPointer = (x, y, margin) => {
|
|
2005
2858
|
return pointer && pointer.x >= x - margin && pointer.x <= x + width + margin && pointer.y >= y - margin && pointer.y <= y + height + margin;
|
|
2006
2859
|
};
|
|
2007
2860
|
const passive = !selectOnClick || !this.visualRegister;
|
|
2008
|
-
if (passive && ((
|
|
2861
|
+
if (passive && ((_u = this.options.pointerAvoidance) === null || _u === void 0 ? void 0 : _u.enabled) !== false && pointer &&
|
|
2009
2862
|
(containsPointer(position.x, position.y, gap) || containsPointer(position.x + displacement.x, position.y + displacement.y, gap))) {
|
|
2010
2863
|
if (containsPointer(position.x + displacement.x, position.y + displacement.y, 0)) {
|
|
2011
2864
|
// Choose against the original box and retain the offset until the pointer leaves both boxes.
|
|
@@ -2023,7 +2876,7 @@ var EntityLabel;
|
|
|
2023
2876
|
else {
|
|
2024
2877
|
displacement = { x: 0, y: 0 };
|
|
2025
2878
|
}
|
|
2026
|
-
const nudgeDuration = (
|
|
2879
|
+
const nudgeDuration = (_w = (_v = this.options.pointerAvoidance) === null || _v === void 0 ? void 0 : _v.transitionDuration) !== null && _w !== void 0 ? _w : 200;
|
|
2027
2880
|
const animateNudge = () => {
|
|
2028
2881
|
const progress = now < nudge.start ? 0 : nudgeDuration > 0 ? Math.min(1, (now - nudge.start) / nudgeDuration) : 1;
|
|
2029
2882
|
const eased = progress * progress * (3 - 2 * progress);
|
|
@@ -2039,7 +2892,7 @@ var EntityLabel;
|
|
|
2039
2892
|
fromY: nudge.y,
|
|
2040
2893
|
targetX: displacement.x,
|
|
2041
2894
|
targetY: displacement.y,
|
|
2042
|
-
start: now + ((
|
|
2895
|
+
start: now + ((_y = (_x = this.options.pointerAvoidance) === null || _x === void 0 ? void 0 : _x.delay) !== null && _y !== void 0 ? _y : 180)
|
|
2043
2896
|
};
|
|
2044
2897
|
}
|
|
2045
2898
|
if (animateNudge() < 1 && (nudge.fromX !== nudge.targetX || nudge.fromY !== nudge.targetY)) {
|
|
@@ -2050,10 +2903,10 @@ var EntityLabel;
|
|
|
2050
2903
|
ele.style.left = `${coordinates.x + position.x * coordinates.scaleX}px`;
|
|
2051
2904
|
ele.style.top = `${coordinates.y + position.y * coordinates.scaleY}px`;
|
|
2052
2905
|
for (const clickTarget of Array.from(content.querySelectorAll('[role="button"]'))) {
|
|
2053
|
-
clickTarget.setAttribute("aria-label", placement.members.length ? `Select ${placement.entityIds.length} entities` : `Select ${(
|
|
2906
|
+
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}`);
|
|
2054
2907
|
}
|
|
2055
2908
|
if (leader) {
|
|
2056
|
-
const nodeRadius = ringAnimation.outer;
|
|
2909
|
+
const nodeRadius = ringAnimation.outer * (1 - (frame.boxBlend || 0));
|
|
2057
2910
|
const anchor = this.anchor;
|
|
2058
2911
|
anchor.style.left = `${coordinates.x + (frame.anchorX - nodeRadius) * coordinates.scaleX}px`;
|
|
2059
2912
|
anchor.style.top = `${coordinates.y + (frame.anchorY - nodeRadius) * coordinates.scaleY}px`;
|
|
@@ -2066,11 +2919,12 @@ var EntityLabel;
|
|
|
2066
2919
|
const circle = svg.firstElementChild;
|
|
2067
2920
|
circle.setAttribute("cx", String(nodeRadius));
|
|
2068
2921
|
circle.setAttribute("cy", String(nodeRadius));
|
|
2069
|
-
|
|
2922
|
+
const innerRadius = ringAnimation.inner * (1 - (frame.boxBlend || 0));
|
|
2923
|
+
circle.setAttribute("r", String((nodeRadius + innerRadius) / 2));
|
|
2070
2924
|
circle.setAttribute("stroke", this.options.leaderColor || "rgba(255, 255, 255, 0.8)");
|
|
2071
|
-
circle.setAttribute("stroke-width", String(nodeRadius -
|
|
2925
|
+
circle.setAttribute("stroke-width", String(nodeRadius - innerRadius));
|
|
2072
2926
|
anchor.style.boxShadow = radius ? "none" : "0 0 5px rgba(0, 0, 0, 0.3)";
|
|
2073
|
-
anchor.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount));
|
|
2927
|
+
anchor.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount) * (1 - (frame.boxBlend || 0)));
|
|
2074
2928
|
anchor.style.zIndex = String(placement.zIndex);
|
|
2075
2929
|
// The stem starts at the rim, never across the transparent centre, even after passive displacement.
|
|
2076
2930
|
const straightX = Math.max(position.x, Math.min(frame.anchorX, position.x + width));
|
|
@@ -2090,13 +2944,13 @@ var EntityLabel;
|
|
|
2090
2944
|
// Keep the stem inside the 126-degree arc, away from antialiased endpoints.
|
|
2091
2945
|
const arcFraction = 0.35;
|
|
2092
2946
|
const rotationLimit = Math.PI * arcFraction - Math.PI / 30;
|
|
2093
|
-
const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((
|
|
2947
|
+
const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_2 = ringAnimation.arcOffset) !== null && _2 !== void 0 ? _2 : 0);
|
|
2094
2948
|
const targetOpen = cursorMotion.target || (hovered && (ring === null || ring === void 0 ? void 0 : ring.enabled) !== false && targetRadius > 0) ? 1 : 0;
|
|
2095
|
-
const elapsed = Math.min(32, Math.max(0, now - ((
|
|
2949
|
+
const elapsed = Math.min(32, Math.max(0, now - ((_3 = ringAnimation.arcTime) !== null && _3 !== void 0 ? _3 : now)));
|
|
2096
2950
|
const blend = duration > 0 ? 1 - Math.exp(-elapsed / (duration / 3)) : 1;
|
|
2097
2951
|
const approach = (value, target) => Math.abs(value - target) < 0.001 ? target : value + (target - value) * blend;
|
|
2098
|
-
ringAnimation.arcOffset = approach((
|
|
2099
|
-
ringAnimation.arcOpen = approach((
|
|
2952
|
+
ringAnimation.arcOffset = approach((_4 = ringAnimation.arcOffset) !== null && _4 !== void 0 ? _4 : 0, targetOffset);
|
|
2953
|
+
ringAnimation.arcOpen = approach((_5 = ringAnimation.arcOpen) !== null && _5 !== void 0 ? _5 : 0, targetOpen);
|
|
2100
2954
|
ringAnimation.arcTime = now;
|
|
2101
2955
|
if (ringAnimation.arcOffset !== targetOffset || ringAnimation.arcOpen !== targetOpen) {
|
|
2102
2956
|
this.viewer.scene.requestRender();
|
|
@@ -2122,6 +2976,12 @@ var EntityLabel;
|
|
|
2122
2976
|
element.style.zIndex = String(placement.zIndex);
|
|
2123
2977
|
};
|
|
2124
2978
|
draw(lineEle, startX, startY, endX, endY);
|
|
2979
|
+
if (layer.modelBoxes.Hull(this.id)) {
|
|
2980
|
+
layer.modelBoxes.Request(this.id, {
|
|
2981
|
+
whole: Boolean(cursorMotion.target), from: { x: frame.anchorX, y: frame.anchorY },
|
|
2982
|
+
opacity, zIndex: placement.zIndex, color: this.options.leaderColor || "rgba(255, 255, 255, 0.8)", duration
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2125
2985
|
if (runEle) {
|
|
2126
2986
|
// Overlap the corner by the stem's own width so the elbow has no notch in it.
|
|
2127
2987
|
const overlap = frame.anchorX < runToX ? -1 : 1;
|
|
@@ -2134,6 +2994,7 @@ var EntityLabel;
|
|
|
2134
2994
|
ele.style.display = "flex";
|
|
2135
2995
|
ele.style.visibility = "hidden";
|
|
2136
2996
|
this.renderRemoval = layer.Add({ project, apply });
|
|
2997
|
+
leaveLayer === null || leaveLayer === void 0 ? void 0 : leaveLayer();
|
|
2137
2998
|
// If terrain changes then we'll re-call createLabel as the position may have changed.
|
|
2138
2999
|
let terrTimeout = null;
|
|
2139
3000
|
this.terrChangedRemoval = this.viewer.scene.terrainProviderChanged.addEventListener(() => {
|
|
@@ -2279,7 +3140,7 @@ var EntityLabel;
|
|
|
2279
3140
|
// The glyph plus the chip's padding of a third either side, enlarged for a photo.
|
|
2280
3141
|
const diameter = (size + 2 * (size / 3)) * PHOTO_SIZE_SCALE;
|
|
2281
3142
|
// Enough pixels to stay sharp at the hover size.
|
|
2282
|
-
const pixels =
|
|
3143
|
+
const pixels = photoPixels(size, true);
|
|
2283
3144
|
const canvas = document.createElement("canvas");
|
|
2284
3145
|
canvas.className = "entity-label-photo";
|
|
2285
3146
|
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);";
|
|
@@ -2290,13 +3151,15 @@ var EntityLabel;
|
|
|
2290
3151
|
// Resting size, set once the photo's shape is known.
|
|
2291
3152
|
let width = 0;
|
|
2292
3153
|
let height = 0;
|
|
3154
|
+
// How far hovering grows it, which a small image limits.
|
|
3155
|
+
let hoverScale = PHOTO_HOVER_SCALE;
|
|
2293
3156
|
let grown = false;
|
|
2294
3157
|
const grow = (on) => {
|
|
2295
3158
|
if (on === grown || !width) {
|
|
2296
3159
|
return;
|
|
2297
3160
|
}
|
|
2298
3161
|
grown = on;
|
|
2299
|
-
const scale = on ?
|
|
3162
|
+
const scale = on ? hoverScale : 1;
|
|
2300
3163
|
canvas.style.width = `${width * scale}px`;
|
|
2301
3164
|
canvas.style.height = `${height * scale}px`;
|
|
2302
3165
|
this.renderFor(PHOTO_TRANSITION);
|
|
@@ -2314,11 +3177,15 @@ var EntityLabel;
|
|
|
2314
3177
|
(_a = canvas.getContext("2d")) === null || _a === void 0 ? void 0 : _a.drawImage(photo.source, 0, 0);
|
|
2315
3178
|
const longest = Math.max(photo.width, photo.height);
|
|
2316
3179
|
const circle = PhotoShape(photo.width, photo.height) === "circle";
|
|
2317
|
-
|
|
2318
|
-
|
|
3180
|
+
// Shown no larger than the image has pixels for, one to a CSS pixel, and never smaller than the icon.
|
|
3181
|
+
const native = circle ? Math.min(photo.width, photo.height) : longest;
|
|
3182
|
+
const across = Math.min(diameter, Math.max(size, native));
|
|
3183
|
+
hoverScale = Math.max(1, Math.min(PHOTO_HOVER_SCALE, native / across));
|
|
3184
|
+
width = circle ? across : across * photo.width / longest;
|
|
3185
|
+
height = circle ? across : across * photo.height / longest;
|
|
2319
3186
|
canvas.style.width = `${width}px`;
|
|
2320
3187
|
canvas.style.height = `${height}px`;
|
|
2321
|
-
canvas.style.borderRadius = circle ? "50%" : `${
|
|
3188
|
+
canvas.style.borderRadius = circle ? "50%" : `${across * PHOTO_CORNER}px`;
|
|
2322
3189
|
canvas.style.opacity = "1";
|
|
2323
3190
|
placePhoto === null || placePhoto === void 0 ? void 0 : placePhoto(circle ? "circle" : "rect", grow);
|
|
2324
3191
|
return true;
|
|
@@ -2365,6 +3232,57 @@ var EntityLabel;
|
|
|
2365
3232
|
};
|
|
2366
3233
|
step();
|
|
2367
3234
|
}
|
|
3235
|
+
/*
|
|
3236
|
+
* Reads the record's geometry box, preferring an entity a label group already hydrated.
|
|
3237
|
+
*/
|
|
3238
|
+
async loadModelBox() {
|
|
3239
|
+
var _a, _b, _c, _d;
|
|
3240
|
+
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;
|
|
3241
|
+
let entity = ((_d = known === null || known === void 0 ? void 0 : known.Bruce) === null || _d === void 0 ? void 0 : _d.GeometryPivotExtents) ? known : null;
|
|
3242
|
+
if (!entity && this.api) {
|
|
3243
|
+
try {
|
|
3244
|
+
entity = (await bruce_models_1.Entity.Get({ api: this.api, entityId: this.rego.entityId, expandLocation: true, migrated: true })).entity;
|
|
3245
|
+
}
|
|
3246
|
+
// Expanding location fails for entities outside an assembly, which have no box to give.
|
|
3247
|
+
catch {
|
|
3248
|
+
return null;
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
// A file uploaded before the API stored oriented boxes has only the record's axis-aligned extents.
|
|
3252
|
+
return GetModelBox(entity, ReadOrientedBox(entity));
|
|
3253
|
+
}
|
|
3254
|
+
/*
|
|
3255
|
+
* Eases the stem between the ring and the model box, returning how far onto the box it is this frame.
|
|
3256
|
+
*/
|
|
3257
|
+
morphToBox(boxed, duration) {
|
|
3258
|
+
const morph = this.boxMorph;
|
|
3259
|
+
const now = Date.now();
|
|
3260
|
+
const ease = (progress) => progress * progress * (3 - 2 * progress);
|
|
3261
|
+
const progress = (start) => duration > 0 ? Math.min(1, Math.max(0, (now - start) / duration)) : 1;
|
|
3262
|
+
const hadBox = morph.target === 1;
|
|
3263
|
+
const target = boxed ? 1 : 0;
|
|
3264
|
+
if (morph.target !== target) {
|
|
3265
|
+
Object.assign(morph, { from: morph.value, target, start: now });
|
|
3266
|
+
}
|
|
3267
|
+
morph.value = morph.from + (morph.target - morph.from) * ease(progress(morph.start));
|
|
3268
|
+
// Settled, so a later frame with a longer duration does not replay the ease.
|
|
3269
|
+
if (morph.value === morph.target) {
|
|
3270
|
+
morph.from = morph.target;
|
|
3271
|
+
}
|
|
3272
|
+
if (boxed) {
|
|
3273
|
+
// Moving to another outline, such as a hovered part pulled out of its group, eases the stem across.
|
|
3274
|
+
if (hadBox && morph.anchor && morph.group !== boxed.group) {
|
|
3275
|
+
morph.shift = { x: morph.anchor.x - boxed.anchor.x, y: morph.anchor.y - boxed.anchor.y, start: now };
|
|
3276
|
+
}
|
|
3277
|
+
morph.group = boxed.group;
|
|
3278
|
+
const remaining = 1 - ease(progress(morph.shift.start));
|
|
3279
|
+
morph.anchor = { x: boxed.anchor.x + morph.shift.x * remaining, y: boxed.anchor.y + morph.shift.y * remaining };
|
|
3280
|
+
}
|
|
3281
|
+
if (morph.value !== morph.target || (boxed && progress(morph.shift.start) < 1)) {
|
|
3282
|
+
this.viewer.scene.requestRender();
|
|
3283
|
+
}
|
|
3284
|
+
return morph.value;
|
|
3285
|
+
}
|
|
2368
3286
|
/**
|
|
2369
3287
|
* Removes label from the viewer and unsubscribes from events.
|
|
2370
3288
|
*/
|