bruce-cesium 7.7.7 → 7.7.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.
Files changed (49) hide show
  1. package/dist/bruce-cesium.es5.js +2292 -69
  2. package/dist/bruce-cesium.es5.js.map +1 -1
  3. package/dist/bruce-cesium.umd.js +2271 -66
  4. package/dist/bruce-cesium.umd.js.map +1 -1
  5. package/dist/lib/bruce-cesium.js +6 -1
  6. package/dist/lib/bruce-cesium.js.map +1 -1
  7. package/dist/lib/rendering/entity-render-engine-model3d.js +2 -2
  8. package/dist/lib/rendering/entity-render-engine-model3d.js.map +1 -1
  9. package/dist/lib/rendering/entity-render-engine-polygon.js +5 -4
  10. package/dist/lib/rendering/entity-render-engine-polygon.js.map +1 -1
  11. package/dist/lib/rendering/entity-render-engine-polyline.js +2 -1
  12. package/dist/lib/rendering/entity-render-engine-polyline.js.map +1 -1
  13. package/dist/lib/rendering/render-managers/common/entity-simulation.js +1 -1
  14. package/dist/lib/rendering/render-managers/common/entity-simulation.js.map +1 -1
  15. package/dist/lib/rendering/tileset-render-engine.js +47 -14
  16. package/dist/lib/rendering/tileset-render-engine.js.map +1 -1
  17. package/dist/lib/rendering/view-render-engine.js +30 -24
  18. package/dist/lib/rendering/view-render-engine.js.map +1 -1
  19. package/dist/lib/rendering/visuals-register.js +93 -8
  20. package/dist/lib/rendering/visuals-register.js.map +1 -1
  21. package/dist/lib/rendering/xgrids-render-engine.js +20 -3
  22. package/dist/lib/rendering/xgrids-render-engine.js.map +1 -1
  23. package/dist/lib/utils/cesium-map-grid.js +807 -0
  24. package/dist/lib/utils/cesium-map-grid.js.map +1 -0
  25. package/dist/lib/utils/device-profile.js +302 -0
  26. package/dist/lib/utils/device-profile.js.map +1 -0
  27. package/dist/lib/utils/gltf-generator.js +307 -0
  28. package/dist/lib/utils/gltf-generator.js.map +1 -0
  29. package/dist/lib/utils/gltf-gizmo.js +136 -0
  30. package/dist/lib/utils/gltf-gizmo.js.map +1 -0
  31. package/dist/lib/utils/view-utils.js +9 -0
  32. package/dist/lib/utils/view-utils.js.map +1 -1
  33. package/dist/lib/viewer/camera-controls.js +544 -0
  34. package/dist/lib/viewer/camera-controls.js.map +1 -0
  35. package/dist/lib/viewer/viewer-utils.js +9 -1
  36. package/dist/lib/viewer/viewer-utils.js.map +1 -1
  37. package/dist/types/bruce-cesium.d.ts +6 -1
  38. package/dist/types/rendering/tileset-render-engine.d.ts +10 -2
  39. package/dist/types/rendering/view-render-engine.d.ts +4 -0
  40. package/dist/types/rendering/visuals-register.d.ts +4 -0
  41. package/dist/types/rendering/xgrids-render-engine.d.ts +2 -0
  42. package/dist/types/utils/cesium-map-grid.d.ts +23 -0
  43. package/dist/types/utils/device-profile.d.ts +84 -0
  44. package/dist/types/utils/gltf-generator.d.ts +104 -0
  45. package/dist/types/utils/gltf-gizmo.d.ts +18 -0
  46. package/dist/types/utils/view-utils.d.ts +4 -0
  47. package/dist/types/viewer/camera-controls.d.ts +19 -0
  48. package/dist/types/viewer/viewer-utils.d.ts +1 -0
  49. package/package.json +3 -2
@@ -907,6 +907,542 @@
907
907
  return true;
908
908
  }
909
909
 
910
+ const CESIUM_CAMERA_CONTROLS_KEY = "_nextspace_camera_controls";
911
+ // How far a wheel notch or its drag equivalent moves the camera, as a share of the distance to what it points at.
912
+ const ZOOM_SHARE = 0.18;
913
+ // The distance a zoom step treats anything closer as, so the camera keeps moving through a surface instead of stalling.
914
+ const MIN_ZOOM_DISTANCE = 4;
915
+ // How far from the origin zooming out may take a model-space camera, beyond which the scene is a dot on a planet.
916
+ const MODEL_SPACE_MAX_RANGE = 250000;
917
+ // Above this height a geospatial scene is a planet, which CesiumJS's own controller navigates better.
918
+ const GEO_HANDOFF_HEIGHT = 15000;
919
+ // The furthest a depth pick is trusted from a camera close to the ground; further up the limit grows with height.
920
+ const PLAUSIBLE_PICK_FLOOR = 1000;
921
+ // Pixels of right drag that make one wheel notch.
922
+ const DRAG_PIXELS_PER_NOTCH = 40;
923
+ // Share of the outstanding wheel movement applied each animation frame.
924
+ const ZOOM_EASE = 0.35;
925
+ // A pan plane seen closer to edge on than this (sine of the angle) is swapped for one facing the camera.
926
+ const GRAZING_SINE = 0.12;
927
+ // Closest the view may get to straight up or down while orbiting, in radians, so the camera never flips over.
928
+ const ORBIT_POLE_MARGIN = Cesium.Math.toRadians(1);
929
+ var EDrag;
930
+ (function (EDrag) {
931
+ EDrag["Pan"] = "pan";
932
+ EDrag["Orbit"] = "orbit";
933
+ EDrag["Zoom"] = "zoom";
934
+ })(EDrag || (EDrag = {}));
935
+ (function (CameraControls) {
936
+ /**
937
+ * Takes over mouse drag and wheel navigation close to the ground; CesiumJS keeps planet scale, pinch and look.
938
+ */
939
+ function Attach(viewer) {
940
+ var _a, _b, _c;
941
+ if (!viewer || viewer.isDestroyed() || viewer[CESIUM_CAMERA_CONTROLS_KEY]) {
942
+ return;
943
+ }
944
+ const scene = viewer.scene;
945
+ const controller = scene === null || scene === void 0 ? void 0 : scene.screenSpaceCameraController;
946
+ if (!controller || !scene.canvas || !scene.preUpdate) {
947
+ return;
948
+ }
949
+ const state = {
950
+ viewer,
951
+ getModelSpaceFrame: null,
952
+ drag: null,
953
+ touches: new Set(),
954
+ pendingZoom: new Cesium.Cartesian3(),
955
+ zoomFrame: null,
956
+ zoomPosition: null,
957
+ lastDistance: 100,
958
+ saved: {
959
+ rotateEventTypes: controller.rotateEventTypes,
960
+ zoomEventTypes: controller.zoomEventTypes,
961
+ tiltEventTypes: controller.tiltEventTypes,
962
+ enableCollisionDetection: controller.enableCollisionDetection,
963
+ backFaceAlpha: (_b = (_a = scene.globe) === null || _a === void 0 ? void 0 : _a.translucency) === null || _b === void 0 ? void 0 : _b.backFaceAlpha,
964
+ showSkirts: (_c = scene.globe) === null || _c === void 0 ? void 0 : _c.showSkirts
965
+ },
966
+ ours: false,
967
+ removals: []
968
+ };
969
+ viewer[CESIUM_CAMERA_CONTROLS_KEY] = state;
970
+ // The camera may go below the ground; zooming slows near a surface, so it takes a deliberate push.
971
+ controller.enableCollisionDetection = false;
972
+ const canvas = scene.canvas;
973
+ const listen = (target, type, handler, options) => {
974
+ target.addEventListener(type, handler, options);
975
+ state.removals.push(() => target.removeEventListener(type, handler, options));
976
+ };
977
+ listen(canvas, "pointerdown", (e) => onPointerDown(state, e));
978
+ listen(window, "pointermove", (e) => onPointerMove(state, e));
979
+ listen(window, "pointerup", (e) => onPointerUp(state, e));
980
+ listen(window, "pointercancel", (e) => onPointerUp(state, e));
981
+ listen(canvas, "wheel", (e) => onWheel(state, e), { passive: true });
982
+ state.removals.push(scene.preUpdate.addEventListener(() => syncController(state)));
983
+ syncController(state);
984
+ scene.requestRender();
985
+ }
986
+ CameraControls.Attach = Attach;
987
+ /**
988
+ * Hands navigation back to CesiumJS's own camera controller, as it was before Attach.
989
+ */
990
+ function Detach(viewer) {
991
+ const state = viewer === null || viewer === void 0 ? void 0 : viewer[CESIUM_CAMERA_CONTROLS_KEY];
992
+ if (!state) {
993
+ return;
994
+ }
995
+ SetModelSpace(viewer, null);
996
+ viewer[CESIUM_CAMERA_CONTROLS_KEY] = null;
997
+ state.removals.forEach((removal) => removal());
998
+ state.removals = [];
999
+ if (state.zoomFrame != null) {
1000
+ cancelAnimationFrame(state.zoomFrame);
1001
+ }
1002
+ if (viewer.isDestroyed()) {
1003
+ return;
1004
+ }
1005
+ restoreCesiumGestures(state);
1006
+ viewer.scene.screenSpaceCameraController.enableCollisionDetection = state.saved.enableCollisionDetection;
1007
+ viewer.scene.requestRender();
1008
+ }
1009
+ CameraControls.Detach = Detach;
1010
+ function IsAttached(viewer) {
1011
+ return Boolean(viewer === null || viewer === void 0 ? void 0 : viewer[CESIUM_CAMERA_CONTROLS_KEY]);
1012
+ }
1013
+ CameraControls.IsAttached = IsAttached;
1014
+ /**
1015
+ * Switches to navigating model-space measured from the frame getFrame returns, or back to geospatial with null.
1016
+ */
1017
+ function SetModelSpace(viewer, getFrame) {
1018
+ const state = viewer === null || viewer === void 0 ? void 0 : viewer[CESIUM_CAMERA_CONTROLS_KEY];
1019
+ if (!state || viewer.isDestroyed()) {
1020
+ return;
1021
+ }
1022
+ const wasModelSpace = Boolean(state.getModelSpaceFrame);
1023
+ state.getModelSpaceFrame = getFrame !== null && getFrame !== void 0 ? getFrame : null;
1024
+ syncController(state);
1025
+ if (wasModelSpace === Boolean(getFrame)) {
1026
+ return;
1027
+ }
1028
+ const globe = viewer.scene.globe;
1029
+ const translucency = globe === null || globe === void 0 ? void 0 : globe.translucency;
1030
+ if (getFrame) {
1031
+ // The ground is see-through from below, so what stands on it stays in view underground.
1032
+ if (translucency && typeof translucency.backFaceAlpha === "number") {
1033
+ translucency.backFaceAlpha = 0.5;
1034
+ }
1035
+ // Skirts hide seams flat model-space ground never has, and below it they are walls.
1036
+ if (typeof state.saved.showSkirts === "boolean") {
1037
+ globe.showSkirts = false;
1038
+ }
1039
+ }
1040
+ else {
1041
+ if (translucency && typeof state.saved.backFaceAlpha === "number") {
1042
+ translucency.backFaceAlpha = state.saved.backFaceAlpha;
1043
+ }
1044
+ if (typeof state.saved.showSkirts === "boolean") {
1045
+ globe.showSkirts = state.saved.showSkirts;
1046
+ }
1047
+ }
1048
+ viewer.scene.requestRender();
1049
+ }
1050
+ CameraControls.SetModelSpace = SetModelSpace;
1051
+ })(exports.CameraControls || (exports.CameraControls = {}));
1052
+ function restoreCesiumGestures(state) {
1053
+ const controller = state.viewer.scene.screenSpaceCameraController;
1054
+ controller.rotateEventTypes = state.saved.rotateEventTypes;
1055
+ controller.zoomEventTypes = state.saved.zoomEventTypes;
1056
+ controller.tiltEventTypes = state.saved.tiltEventTypes;
1057
+ }
1058
+ /*
1059
+ * Decides each frame who has the mouse: these controls near the ground, CesiumJS for planets, orthographic and tracking.
1060
+ */
1061
+ function syncController(state) {
1062
+ const viewer = state.viewer;
1063
+ if (viewer.isDestroyed() || state.drag) {
1064
+ return;
1065
+ }
1066
+ const ours = shouldControl(state);
1067
+ if (ours === state.ours) {
1068
+ return;
1069
+ }
1070
+ state.ours = ours;
1071
+ if (ours) {
1072
+ const controller = viewer.scene.screenSpaceCameraController;
1073
+ controller.rotateEventTypes = [];
1074
+ controller.zoomEventTypes = [Cesium.CameraEventType.PINCH];
1075
+ controller.tiltEventTypes = [Cesium.CameraEventType.PINCH];
1076
+ }
1077
+ else {
1078
+ restoreCesiumGestures(state);
1079
+ }
1080
+ }
1081
+ function shouldControl(state) {
1082
+ const viewer = state.viewer;
1083
+ const camera = viewer.camera;
1084
+ if (viewer.scene.mode !== Cesium.SceneMode.SCENE3D || camera.frustum instanceof Cesium.OrthographicFrustum) {
1085
+ return false;
1086
+ }
1087
+ // A camera following something is CesiumJS's to move.
1088
+ if (!Cesium.Matrix4.equals(camera.transform, Cesium.Matrix4.IDENTITY)) {
1089
+ return false;
1090
+ }
1091
+ if (state.getModelSpaceFrame) {
1092
+ return true;
1093
+ }
1094
+ return heightOf(state, camera.positionWC) < GEO_HANDOFF_HEIGHT;
1095
+ }
1096
+ function ellipsoidOf(state) {
1097
+ var _a, _b;
1098
+ return (_b = (_a = state.viewer.scene.globe) === null || _a === void 0 ? void 0 : _a.ellipsoid) !== null && _b !== void 0 ? _b : Cesium.Ellipsoid.WGS84;
1099
+ }
1100
+ function heightOf(state, position) {
1101
+ const carto = ellipsoidOf(state).cartesianToCartographic(position);
1102
+ return carto ? carto.height : Number.POSITIVE_INFINITY;
1103
+ }
1104
+ function canvasPosition(viewer, e) {
1105
+ const rect = viewer.scene.canvas.getBoundingClientRect();
1106
+ return new Cesium.Cartesian2(e.clientX - rect.left, e.clientY - rect.top);
1107
+ }
1108
+ /*
1109
+ * Up at a point: the model-space frame's up in model-space, else the ellipsoid's surface normal there.
1110
+ */
1111
+ function upAt(state, position) {
1112
+ if (state.getModelSpaceFrame) {
1113
+ const up = Cesium.Matrix4.multiplyByPointAsVector(state.getModelSpaceFrame(), Cesium.Cartesian3.UNIT_Z, new Cesium.Cartesian3());
1114
+ return Cesium.Cartesian3.normalize(up, up);
1115
+ }
1116
+ return ellipsoidOf(state).geodeticSurfaceNormal(position, new Cesium.Cartesian3());
1117
+ }
1118
+ /*
1119
+ * The level plane standing in for the ground when nothing is drawn under the cursor.
1120
+ */
1121
+ function groundPlane(state) {
1122
+ if (state.getModelSpaceFrame) {
1123
+ const frame = state.getModelSpaceFrame();
1124
+ return Cesium.Plane.fromPointNormal(Cesium.Matrix4.getTranslation(frame, new Cesium.Cartesian3()), upAt(state, null));
1125
+ }
1126
+ const camera = state.viewer.camera;
1127
+ const ellipsoid = ellipsoidOf(state);
1128
+ const surface = ellipsoid.scaleToGeodeticSurface(camera.positionWC, new Cesium.Cartesian3());
1129
+ if (!surface) {
1130
+ return null;
1131
+ }
1132
+ return Cesium.Plane.fromPointNormal(surface, ellipsoid.geodeticSurfaceNormal(surface, new Cesium.Cartesian3()));
1133
+ }
1134
+ /*
1135
+ * How far away a depth pick is believable, since below ground the depth buffer can hold the globe's inside.
1136
+ */
1137
+ function plausibleDistance(state) {
1138
+ const camera = state.viewer.camera;
1139
+ if (state.getModelSpaceFrame) {
1140
+ const origin = Cesium.Matrix4.getTranslation(state.getModelSpaceFrame(), new Cesium.Cartesian3());
1141
+ return Math.max(PLAUSIBLE_PICK_FLOOR, Cesium.Cartesian3.distance(camera.positionWC, origin) * 10);
1142
+ }
1143
+ const height = Math.abs(heightOf(state, camera.positionWC) - (Number(state.viewer.scene.globeHeight) || 0));
1144
+ const horizon = Math.sqrt(2 * ellipsoidOf(state).maximumRadius * Math.max(height, 0));
1145
+ return Math.max(PLAUSIBLE_PICK_FLOOR, height * 10, horizon * 1.5);
1146
+ }
1147
+ /*
1148
+ * The point under a screen position: what is drawn there, else the ground plane, else a point at the last distance.
1149
+ */
1150
+ function pickTarget(state, position) {
1151
+ const viewer = state.viewer;
1152
+ const scene = viewer.scene;
1153
+ const camera = viewer.camera;
1154
+ const ray = camera.getPickRay(position);
1155
+ if (!ray) {
1156
+ return null;
1157
+ }
1158
+ let picked;
1159
+ if (scene.pickPositionSupported) {
1160
+ try {
1161
+ picked = scene.pickPosition(position);
1162
+ }
1163
+ catch (e) {
1164
+ picked = null;
1165
+ }
1166
+ }
1167
+ const plausible = plausibleDistance(state);
1168
+ if (isAhead(camera, picked) && Cesium.Cartesian3.distance(picked, camera.positionWC) < plausible) {
1169
+ return picked;
1170
+ }
1171
+ const ground = groundPlane(state);
1172
+ const onGround = ground ? Cesium.IntersectionTests.rayPlane(ray, ground) : null;
1173
+ if (isAhead(camera, onGround) && Cesium.Cartesian3.distance(onGround, camera.positionWC) < plausible) {
1174
+ return onGround;
1175
+ }
1176
+ return Cesium.Ray.getPoint(ray, state.lastDistance);
1177
+ }
1178
+ function isAhead(camera, point) {
1179
+ if (!point || !Number.isFinite(point.x) || !Number.isFinite(point.y) || !Number.isFinite(point.z)) {
1180
+ return false;
1181
+ }
1182
+ const toPoint = Cesium.Cartesian3.subtract(point, camera.positionWC, new Cesium.Cartesian3());
1183
+ return Cesium.Cartesian3.dot(toPoint, camera.directionWC) > 0;
1184
+ }
1185
+ /*
1186
+ * Picks which gesture a button and its modifiers start, matching CesiumJS's own mapping.
1187
+ */
1188
+ function dragKind(e) {
1189
+ if (e.shiftKey) {
1190
+ // Shift drag looks around, which CesiumJS still handles.
1191
+ return null;
1192
+ }
1193
+ if (e.button === 1 || (e.ctrlKey && (e.button === 0 || e.button === 2))) {
1194
+ return EDrag.Orbit;
1195
+ }
1196
+ if (e.button === 0) {
1197
+ return EDrag.Pan;
1198
+ }
1199
+ if (e.button === 2) {
1200
+ return EDrag.Zoom;
1201
+ }
1202
+ return null;
1203
+ }
1204
+ function allows(state, kind) {
1205
+ const controller = state.viewer.scene.screenSpaceCameraController;
1206
+ if (!controller.enableInputs) {
1207
+ return false;
1208
+ }
1209
+ switch (kind) {
1210
+ case EDrag.Pan:
1211
+ return controller.enableRotate;
1212
+ case EDrag.Orbit:
1213
+ return controller.enableTilt;
1214
+ case EDrag.Zoom:
1215
+ return controller.enableZoom;
1216
+ }
1217
+ return false;
1218
+ }
1219
+ function onPointerDown(state, e) {
1220
+ const viewer = state.viewer;
1221
+ if (viewer.isDestroyed() || !state.ours) {
1222
+ return;
1223
+ }
1224
+ if (e.pointerType === "touch") {
1225
+ state.touches.add(e.pointerId);
1226
+ // A second finger makes it a pinch, which CesiumJS handles.
1227
+ if (state.touches.size > 1) {
1228
+ endDrag(state);
1229
+ return;
1230
+ }
1231
+ }
1232
+ const kind = dragKind(e);
1233
+ if (!kind || !allows(state, kind)) {
1234
+ return;
1235
+ }
1236
+ const position = canvasPosition(viewer, e);
1237
+ const target = pickTarget(state, position);
1238
+ if (!target) {
1239
+ return;
1240
+ }
1241
+ const camera = viewer.camera;
1242
+ const distance = Cesium.Cartesian3.distance(camera.positionWC, target);
1243
+ state.lastDistance = Math.max(distance, MIN_ZOOM_DISTANCE);
1244
+ const drag = { kind, pointerId: e.pointerId, last: position, target, up: upAt(state, target), distance };
1245
+ if (kind === EDrag.Pan) {
1246
+ drag.plane = panPlane(camera, target, drag.up);
1247
+ }
1248
+ else if (kind === EDrag.Zoom) {
1249
+ drag.direction = Cesium.Cartesian3.normalize(Cesium.Cartesian3.subtract(target, camera.positionWC, new Cesium.Cartesian3()), new Cesium.Cartesian3());
1250
+ }
1251
+ state.drag = drag;
1252
+ }
1253
+ /*
1254
+ * A level plane through the grabbed point, or one facing the camera when the level one is seen edge on.
1255
+ */
1256
+ function panPlane(camera, target, up) {
1257
+ if (Math.abs(Cesium.Cartesian3.dot(camera.directionWC, up)) > GRAZING_SINE) {
1258
+ return Cesium.Plane.fromPointNormal(target, up);
1259
+ }
1260
+ return Cesium.Plane.fromPointNormal(target, Cesium.Cartesian3.negate(camera.directionWC, new Cesium.Cartesian3()));
1261
+ }
1262
+ function onPointerMove(state, e) {
1263
+ const drag = state.drag;
1264
+ const viewer = state.viewer;
1265
+ if (!drag || drag.pointerId !== e.pointerId || viewer.isDestroyed()) {
1266
+ return;
1267
+ }
1268
+ if (!allows(state, drag.kind)) {
1269
+ endDrag(state);
1270
+ return;
1271
+ }
1272
+ const position = canvasPosition(viewer, e);
1273
+ const dx = position.x - drag.last.x;
1274
+ const dy = position.y - drag.last.y;
1275
+ if (dx === 0 && dy === 0) {
1276
+ return;
1277
+ }
1278
+ switch (drag.kind) {
1279
+ case EDrag.Pan:
1280
+ pan(state, drag, position);
1281
+ break;
1282
+ case EDrag.Orbit:
1283
+ orbit(state, drag, dx, dy);
1284
+ break;
1285
+ case EDrag.Zoom:
1286
+ // Dragging down zooms in, as it does in CesiumJS.
1287
+ moveAlong(state, drag.direction, drag.target, dy / DRAG_PIXELS_PER_NOTCH);
1288
+ break;
1289
+ }
1290
+ drag.last = position;
1291
+ viewer.scene.requestRender();
1292
+ }
1293
+ function onPointerUp(state, e) {
1294
+ var _a;
1295
+ state.touches.delete(e.pointerId);
1296
+ if (((_a = state.drag) === null || _a === void 0 ? void 0 : _a.pointerId) === e.pointerId) {
1297
+ endDrag(state);
1298
+ }
1299
+ }
1300
+ function endDrag(state) {
1301
+ state.drag = null;
1302
+ }
1303
+ /*
1304
+ * Slides the camera so the grabbed point stays under the cursor.
1305
+ */
1306
+ function pan(state, drag, position) {
1307
+ const camera = state.viewer.camera;
1308
+ const ray = camera.getPickRay(position);
1309
+ const hit = ray ? Cesium.IntersectionTests.rayPlane(ray, drag.plane) : null;
1310
+ if (!isAhead(camera, hit)) {
1311
+ return;
1312
+ }
1313
+ const delta = Cesium.Cartesian3.subtract(drag.target, hit, new Cesium.Cartesian3());
1314
+ // Near the horizon a pixel covers kilometres, so one move is capped at a few times the grab distance.
1315
+ const limit = Math.max(drag.distance, MIN_ZOOM_DISTANCE) * 3;
1316
+ const length = Cesium.Cartesian3.magnitude(delta);
1317
+ if (length > limit) {
1318
+ Cesium.Cartesian3.multiplyByScalar(delta, limit / length, delta);
1319
+ }
1320
+ Cesium.Cartesian3.add(camera.position, delta, camera.position);
1321
+ }
1322
+ /*
1323
+ * Turns the camera around the grabbed point: sideways about up there, and up or down about the camera's right.
1324
+ */
1325
+ function orbit(state, drag, dx, dy) {
1326
+ const viewer = state.viewer;
1327
+ const camera = viewer.camera;
1328
+ const canvas = viewer.scene.canvas;
1329
+ const up = drag.up;
1330
+ const heading = -dx / Math.max(canvas.clientWidth, 1) * Math.PI * 2;
1331
+ // Dragging down looks further down, as CesiumJS's own tilt does.
1332
+ let pitch = -dy / Math.max(canvas.clientHeight, 1) * Math.PI;
1333
+ // Keeps the view from tipping past straight up or down, where it would flip over.
1334
+ const elevation = Math.asin(Cesium.Math.clamp(Cesium.Cartesian3.dot(camera.direction, up), -1, 1));
1335
+ const limit = Math.PI / 2 - ORBIT_POLE_MARGIN;
1336
+ pitch = Cesium.Math.clamp(elevation + pitch, -limit, limit) - elevation;
1337
+ rotateAbout(camera, drag.target, up, heading);
1338
+ const right = Cesium.Cartesian3.cross(camera.direction, up, new Cesium.Cartesian3());
1339
+ if (Cesium.Cartesian3.magnitude(right) > Cesium.Math.EPSILON6) {
1340
+ Cesium.Cartesian3.normalize(right, right);
1341
+ rotateAbout(camera, drag.target, right, pitch);
1342
+ }
1343
+ levelCamera(camera, upAt(state, camera.position));
1344
+ }
1345
+ function rotateAbout(camera, pivot, axis, angle) {
1346
+ if (!angle) {
1347
+ return;
1348
+ }
1349
+ const rotation = Cesium.Matrix3.fromQuaternion(Cesium.Quaternion.fromAxisAngle(axis, angle));
1350
+ const offset = Cesium.Cartesian3.subtract(camera.position, pivot, new Cesium.Cartesian3());
1351
+ Cesium.Matrix3.multiplyByVector(rotation, offset, offset);
1352
+ Cesium.Cartesian3.add(pivot, offset, camera.position);
1353
+ Cesium.Matrix3.multiplyByVector(rotation, camera.direction, camera.direction);
1354
+ Cesium.Matrix3.multiplyByVector(rotation, camera.up, camera.up);
1355
+ Cesium.Matrix3.multiplyByVector(rotation, camera.right, camera.right);
1356
+ }
1357
+ /*
1358
+ * Takes any roll out, so the horizon stays level however the camera has turned.
1359
+ */
1360
+ function levelCamera(camera, up) {
1361
+ const direction = Cesium.Cartesian3.normalize(camera.direction, camera.direction);
1362
+ const right = Cesium.Cartesian3.cross(direction, up, new Cesium.Cartesian3());
1363
+ if (Cesium.Cartesian3.magnitude(right) < Cesium.Math.EPSILON6) {
1364
+ return;
1365
+ }
1366
+ Cesium.Cartesian3.normalize(right, camera.right);
1367
+ Cesium.Cartesian3.cross(camera.right, direction, camera.up);
1368
+ Cesium.Cartesian3.normalize(camera.up, camera.up);
1369
+ }
1370
+ function onWheel(state, e) {
1371
+ const viewer = state.viewer;
1372
+ if (viewer.isDestroyed() || !state.ours || !allows(state, EDrag.Zoom)) {
1373
+ return;
1374
+ }
1375
+ let notches = e.deltaMode === 1 ? e.deltaY / 3 : e.deltaMode === 2 ? e.deltaY : e.deltaY / 100;
1376
+ notches = Cesium.Math.clamp(-notches, -3, 3);
1377
+ if (!notches) {
1378
+ return;
1379
+ }
1380
+ const position = canvasPosition(viewer, e);
1381
+ const camera = viewer.camera;
1382
+ const target = pickTarget(state, position);
1383
+ const ray = camera.getPickRay(position);
1384
+ if (!target || !ray) {
1385
+ return;
1386
+ }
1387
+ const step = zoomStep(state, ray.direction, target, notches);
1388
+ Cesium.Cartesian3.add(state.pendingZoom, step, state.pendingZoom);
1389
+ state.zoomPosition = Cesium.Cartesian3.clone(camera.position);
1390
+ if (state.zoomFrame == null) {
1391
+ state.zoomFrame = requestAnimationFrame(() => easeZoom(state));
1392
+ }
1393
+ }
1394
+ /*
1395
+ * Applies part of the outstanding wheel movement each frame, so a notch glides rather than jumps.
1396
+ */
1397
+ function easeZoom(state) {
1398
+ state.zoomFrame = null;
1399
+ const viewer = state.viewer;
1400
+ if (viewer.isDestroyed() || !viewer[CESIUM_CAMERA_CONTROLS_KEY]) {
1401
+ return;
1402
+ }
1403
+ // A tool locking the camera mid glide (walkthrough, a camera lock) owns it from then on.
1404
+ if (!allows(state, EDrag.Zoom) ||
1405
+ !state.zoomPosition ||
1406
+ !Cesium.Cartesian3.equalsEpsilon(viewer.camera.position, state.zoomPosition, 0, 1e-3)) {
1407
+ state.pendingZoom = new Cesium.Cartesian3();
1408
+ return;
1409
+ }
1410
+ const remaining = Cesium.Cartesian3.magnitude(state.pendingZoom);
1411
+ const share = remaining < 0.01 ? 1 : ZOOM_EASE;
1412
+ const step = Cesium.Cartesian3.multiplyByScalar(state.pendingZoom, share, new Cesium.Cartesian3());
1413
+ Cesium.Cartesian3.subtract(state.pendingZoom, step, state.pendingZoom);
1414
+ Cesium.Cartesian3.add(viewer.camera.position, step, viewer.camera.position);
1415
+ state.zoomPosition = Cesium.Cartesian3.clone(viewer.camera.position);
1416
+ viewer.scene.requestRender();
1417
+ if (share < 1) {
1418
+ state.zoomFrame = requestAnimationFrame(() => easeZoom(state));
1419
+ }
1420
+ }
1421
+ function moveAlong(state, direction, target, notches) {
1422
+ const step = zoomStep(state, direction, target, notches);
1423
+ Cesium.Cartesian3.add(state.viewer.camera.position, step, state.viewer.camera.position);
1424
+ }
1425
+ /*
1426
+ * The move for some notches toward a target along a direction, a share of the distance with a floor so it never stalls.
1427
+ */
1428
+ function zoomStep(state, direction, target, notches) {
1429
+ const camera = state.viewer.camera;
1430
+ const distance = Math.max(Cesium.Cartesian3.distance(camera.positionWC, target), MIN_ZOOM_DISTANCE);
1431
+ state.lastDistance = distance;
1432
+ let length = distance * ZOOM_SHARE * notches;
1433
+ // A geospatial camera zooming out hands over to CesiumJS once it is high enough, so only model-space is capped.
1434
+ if (length < 0 && state.getModelSpaceFrame) {
1435
+ const origin = Cesium.Matrix4.getTranslation(state.getModelSpaceFrame(), new Cesium.Cartesian3());
1436
+ const pending = Cesium.Cartesian3.add(camera.positionWC, state.pendingZoom, new Cesium.Cartesian3());
1437
+ const range = Cesium.Cartesian3.distance(pending, origin);
1438
+ length = Math.max(length, range - MODEL_SPACE_MAX_RANGE);
1439
+ if (length >= 0) {
1440
+ return new Cesium.Cartesian3();
1441
+ }
1442
+ }
1443
+ return Cesium.Cartesian3.multiplyByScalar(Cesium.Cartesian3.normalize(direction, new Cesium.Cartesian3()), length, new Cesium.Cartesian3());
1444
+ }
1445
+
910
1446
  const CESIUM_INSPECTOR_KEY = "_nextspace_inspector";
911
1447
  const CESIUM_TIMELINE_KEY = "_nextspace_timeline";
912
1448
  const CESIUM_TIMELINE_LIVE_KEY = "_nextspace_timeline_live";
@@ -1383,6 +1919,13 @@
1383
1919
  return overriddenImageBasedLighting || null;
1384
1920
  }
1385
1921
  ViewUtils.GetDefaultImageBasedLighting = GetDefaultImageBasedLighting;
1922
+ /**
1923
+ * Shadow mode for the Entity graphics this library draws, following the scene's shadow switch.
1924
+ */
1925
+ function GetEntityShadowMode(viewer) {
1926
+ return (viewer === null || viewer === void 0 ? void 0 : viewer.shadows) ? Cesium.ShadowMode.ENABLED : Cesium.ShadowMode.DISABLED;
1927
+ }
1928
+ ViewUtils.GetEntityShadowMode = GetEntityShadowMode;
1386
1929
  function EngineShadesModelsDarker() {
1387
1930
  return typeof Cesium.DynamicEnvironmentMapManager !== "undefined";
1388
1931
  }
@@ -1454,6 +1997,7 @@
1454
1997
  viewer[CESIUM_MODEL_SPACE_KEY] = modelSpace;
1455
1998
  // Reload globe since we display it differently between the two modes.
1456
1999
  SetGlobeDetails({ viewer });
2000
+ exports.CameraControls.SetModelSpace(viewer, modelSpace ? () => GetModelSpaceFrame(viewer) : null);
1457
2001
  }
1458
2002
  ViewUtils.SetModelSpace = SetModelSpace;
1459
2003
  function GetModelSpace(viewer) {
@@ -6166,7 +6710,7 @@
6166
6710
  const byTime = new Map();
6167
6711
  for (const set of this.sets.values()) {
6168
6712
  for (const [ms, record] of set.records.get(entityId) || []) {
6169
- if (attrKey && record.attrKey && record.attrKey !== attrKey) {
6713
+ if (attrKey && record.attrKey && !BModels.PathUtils.Equals(record.attrKey, attrKey)) {
6170
6714
  continue;
6171
6715
  }
6172
6716
  const held = byTime.get(ms);
@@ -11965,6 +12509,28 @@
11965
12509
  return highest;
11966
12510
  }
11967
12511
  const MAX_SHOW_DEPTH = 10;
12512
+ // Entity graphics whose shadows follow the scene's shadow switch.
12513
+ const SHADOWED_GRAPHICS = ["model", "polygon", "polyline", "corridor"];
12514
+ /*
12515
+ * Points a registered Entity graphic, and the graphics drawn beside it, at the given shadow mode.
12516
+ */
12517
+ function updateCEntityShadows(viewer, visual, mode, depth = 0) {
12518
+ if (!visual || depth > MAX_SHOW_DEPTH) {
12519
+ return;
12520
+ }
12521
+ for (const key of SHADOWED_GRAPHICS) {
12522
+ const graphic = visual[key];
12523
+ // Only graphics drawn with a mode; one left at Cesium's default was never meant to shadow.
12524
+ if ((graphic === null || graphic === void 0 ? void 0 : graphic.shadows) != null) {
12525
+ SetCPropIfChanged({ viewer, graphic, property: "shadows", value: mode });
12526
+ }
12527
+ }
12528
+ if (visual._siblingGraphics) {
12529
+ for (let i = 0; i < visual._siblingGraphics.length; i++) {
12530
+ updateCEntityShadows(viewer, visual._siblingGraphics[i], mode, depth + 1);
12531
+ }
12532
+ }
12533
+ }
11968
12534
  function updateCEntityShow(viewer, visual, rego, show, ignoreParent, depth = 0) {
11969
12535
  if (depth > MAX_SHOW_DEPTH) {
11970
12536
  console.warn("updateCEntityShow(): Max show depth reached. EntityId = " + rego.entityId);
@@ -12297,7 +12863,7 @@
12297
12863
  }
12298
12864
  get OnLabelGroupsChanged() { return this.labelGroupsChanged; }
12299
12865
  constructor(params) {
12300
- var _a, _b;
12866
+ var _a, _b, _c, _d, _e, _f, _g;
12301
12867
  this.id = BModels.ObjectUtils.UId();
12302
12868
  this.disposed = false;
12303
12869
  this.rego = {};
@@ -12347,6 +12913,7 @@
12347
12913
  this.sourceOrderDirty = false;
12348
12914
  // Stops following what is simulated, so labels can read it again when it changes.
12349
12915
  this.simulationRemoval = null;
12916
+ this.shadowsRemoval = null;
12350
12917
  this.labelGroupsChanged = new BModels.BruceEvent();
12351
12918
  this.hoverRingState = {};
12352
12919
  this.retiringHoverLabels = new Map();
@@ -12382,6 +12949,27 @@
12382
12949
  register: this
12383
12950
  });
12384
12951
  this.simulationRemoval = (_b = (_a = exports.EntitySimulation.Get(this.viewer)) === null || _a === void 0 ? void 0 : _a.OnChanged.Subscribe((change) => this.reloadLabels(change.entityIds))) !== null && _b !== void 0 ? _b : null;
12952
+ this.entityShadows = Boolean((_c = this.viewer) === null || _c === void 0 ? void 0 : _c.shadows);
12953
+ this.shadowsRemoval = (_g = (_f = (_e = (_d = this.viewer) === null || _d === void 0 ? void 0 : _d.scene) === null || _e === void 0 ? void 0 : _e.preUpdate) === null || _f === void 0 ? void 0 : _f.addEventListener(() => this.syncEntityShadows())) !== null && _g !== void 0 ? _g : null;
12954
+ }
12955
+ /*
12956
+ * Follows the scene's shadow switch on every registered Entity, whichever app or bookmark flipped it.
12957
+ */
12958
+ syncEntityShadows() {
12959
+ const viewer = this.viewer;
12960
+ const enabled = Boolean(viewer === null || viewer === void 0 ? void 0 : viewer.shadows);
12961
+ if (enabled === this.entityShadows || this.disposed || !viewer || viewer.isDestroyed()) {
12962
+ return;
12963
+ }
12964
+ this.entityShadows = enabled;
12965
+ const mode = enabled ? Cesium.ShadowMode.ENABLED : Cesium.ShadowMode.DISABLED;
12966
+ for (const regos of Object.values(this.rego)) {
12967
+ for (const rego of regos) {
12968
+ if (rego.visual instanceof Cesium.Entity) {
12969
+ updateCEntityShadows(viewer, rego.visual, mode);
12970
+ }
12971
+ }
12972
+ }
12385
12973
  }
12386
12974
  /*
12387
12975
  * Tells labels to read their Entity again, since they keep what they read when drawn.
@@ -13310,9 +13898,11 @@
13310
13898
  (_b = this.viewer.scene) === null || _b === void 0 ? void 0 : _b.requestRender();
13311
13899
  }
13312
13900
  Dispose() {
13313
- var _a, _b, _c, _d, _e, _f;
13901
+ var _a, _b, _c, _d, _e, _f, _g;
13314
13902
  (_a = this.simulationRemoval) === null || _a === void 0 ? void 0 : _a.call(this);
13315
13903
  this.simulationRemoval = null;
13904
+ (_b = this.shadowsRemoval) === null || _b === void 0 ? void 0 : _b.call(this);
13905
+ this.shadowsRemoval = null;
13316
13906
  exports.CesiumEntityStyler.SetDimming(this.viewer, this.id, false);
13317
13907
  this.ClearLabelHover();
13318
13908
  clearTimeout(this.hoverRingReset);
@@ -13322,14 +13912,14 @@
13322
13912
  exports.EntityLabel.Detatch({ label });
13323
13913
  }
13324
13914
  this.retiringHoverLabels.clear();
13325
- (_b = this.labelGroups) === null || _b === void 0 ? void 0 : _b.Dispose();
13915
+ (_c = this.labelGroups) === null || _c === void 0 ? void 0 : _c.Dispose();
13326
13916
  this.labelGroupsChanged.Clear();
13327
13917
  for (const regos of Object.values(this.rego)) {
13328
13918
  for (const rego of regos)
13329
13919
  exports.EntityLabel.Detatch({ rego, force: true });
13330
13920
  }
13331
13921
  exports.EntityLabel.SetContainer(this.viewer);
13332
- (_c = this.cameraCullerDispose) === null || _c === void 0 ? void 0 : _c.call(this, {
13922
+ (_d = this.cameraCullerDispose) === null || _d === void 0 ? void 0 : _d.call(this, {
13333
13923
  uncull: false
13334
13924
  });
13335
13925
  this.cameraCullerDispose = null;
@@ -13346,7 +13936,7 @@
13346
13936
  this.disposed = true;
13347
13937
  for (const record of Array.from(this.menuItemSources.values())) {
13348
13938
  try {
13349
- (_e = (_d = this.viewer) === null || _d === void 0 ? void 0 : _d.dataSources) === null || _e === void 0 ? void 0 : _e.remove(record.source, true);
13939
+ (_f = (_e = this.viewer) === null || _e === void 0 ? void 0 : _e.dataSources) === null || _f === void 0 ? void 0 : _f.remove(record.source, true);
13350
13940
  }
13351
13941
  catch (e) {
13352
13942
  console.error(e);
@@ -13355,7 +13945,7 @@
13355
13945
  this.menuItemSources.clear();
13356
13946
  // Hand every tileset's show flag back to its owner, holding whatever the owner last asked for.
13357
13947
  for (const rego of Array.from(this.tilesets.values())) {
13358
- (_f = rego.gate) === null || _f === void 0 ? void 0 : _f.Release();
13948
+ (_g = rego.gate) === null || _g === void 0 ? void 0 : _g.Release();
13359
13949
  }
13360
13950
  this.tilesets.clear();
13361
13951
  }
@@ -14079,8 +14669,11 @@
14079
14669
  }
14080
14670
  }
14081
14671
  // Add the new visual to the register.
14672
+ // Tileset managers add the same rego again for each tile holding more of the Entity.
14082
14673
  const entityRegos = (_a = this.rego[entityId]) !== null && _a !== void 0 ? _a : [];
14083
- entityRegos.push(rego);
14674
+ if (!entityRegos.includes(rego)) {
14675
+ entityRegos.push(rego);
14676
+ }
14084
14677
  this.rego[entityId] = entityRegos;
14085
14678
  this.indexMenuItemEntity(rego.menuItemId, entityId);
14086
14679
  // Mark the visual as part of this register so selection works.
@@ -14514,6 +15107,28 @@
14514
15107
  }
14515
15108
  }
14516
15109
  }
15110
+ /*
15111
+ * Hands a tile feature rego to a sibling feature from another tile when its own tile unloads.
15112
+ * An Entity split across tiles keeps drawing from those, so it must stay registered for them.
15113
+ */
15114
+ promoteTileSibling(rego, removing) {
15115
+ var _a;
15116
+ const departing = rego.visual;
15117
+ if (!(departing instanceof Cesium.Cesium3DTileFeature) || !((_a = departing._siblingGraphics) === null || _a === void 0 ? void 0 : _a.length)) {
15118
+ return false;
15119
+ }
15120
+ const remaining = departing._siblingGraphics.filter(s => !removing.has(s));
15121
+ if (!remaining.length) {
15122
+ return false;
15123
+ }
15124
+ departing._siblingGraphics = undefined;
15125
+ unmarkEntity(departing, true);
15126
+ const promoted = remaining[0];
15127
+ promoted._siblingGraphics = remaining.slice(1);
15128
+ rego.visual = promoted;
15129
+ markEntity(this, rego, promoted, false);
15130
+ return true;
15131
+ }
14517
15132
  /**
14518
15133
  * Removes regos matching given visuals.
14519
15134
  * This is used when we cannot unpack properties from the visual any longer to determine its ID.
@@ -14524,6 +15139,7 @@
14524
15139
  var _a, _b, _c;
14525
15140
  const { doRemove = true, requestRender = true, source, doUpdate, menuItemId } = params;
14526
15141
  const removedEntityIds = new Set();
15142
+ const removing = new Set(params.visuals);
14527
15143
  for (const visual of params.visuals) {
14528
15144
  const visExt = visual;
14529
15145
  const rego = (visExt === null || visExt === void 0 ? void 0 : visExt._register) === this ? visExt._rego : null;
@@ -14535,7 +15151,20 @@
14535
15151
  if (!entityRegos) {
14536
15152
  continue;
14537
15153
  }
14538
- if (rego.visual === visual) {
15154
+ if (rego.visual === visual && this.promoteTileSibling(rego, removing)) {
15155
+ if (doRemove != false) {
15156
+ removeEntity(this.viewer, visual);
15157
+ }
15158
+ // The label was placed against the departing feature, the refresh attaches it again.
15159
+ exports.EntityLabel.Detatch({
15160
+ rego, force: true
15161
+ });
15162
+ this.queueUpdate({
15163
+ entityId: entityId,
15164
+ refresh: true
15165
+ });
15166
+ }
15167
+ else if (rego.visual === visual) {
14539
15168
  exports.EntityLabel.Detatch({
14540
15169
  rego, force: true
14541
15170
  });
@@ -16187,6 +16816,300 @@
16187
16816
  }
16188
16817
  }
16189
16818
 
16819
+ // viewer[VIEWER_WATCH_KEY] is true once the viewer's canvas reports context loss to DeviceProfile.
16820
+ const VIEWER_WATCH_KEY = "_bruceDeviceProfileWatch";
16821
+ // localStorage[DEMOTION_KEY] holds an IDemotion written after this browser lost a WebGL context.
16822
+ const DEMOTION_KEY = "bruce-cesium-device-demotion";
16823
+ // A lost context demotes for this long, so one driver reset does not cap a strong device forever.
16824
+ // Measured on the wall clock, not the viewer's: it spans sessions, and a scene clock can be historic.
16825
+ const DEMOTION_MS = 14 * 24 * 60 * 60 * 1000;
16826
+ // Each loss demotes one more tier, up to this many.
16827
+ const DEMOTION_MAX = 2;
16828
+ /*
16829
+ * Reads the demotion left by earlier context losses, ignoring an expired or unreadable one.
16830
+ */
16831
+ function readDemotion(now) {
16832
+ try {
16833
+ const value = JSON.parse(window.localStorage.getItem(DEMOTION_KEY));
16834
+ if (value &&
16835
+ Number.isFinite(value.count) &&
16836
+ Number.isFinite(value.at) &&
16837
+ now - value.at < DEMOTION_MS) {
16838
+ return value;
16839
+ }
16840
+ }
16841
+ catch (e) {
16842
+ // Storage is blocked (private mode, sandboxed frame) or holds junk; treat as no demotion.
16843
+ }
16844
+ return null;
16845
+ }
16846
+ /*
16847
+ * Stores or clears the demotion, carrying on without it where storage is blocked.
16848
+ */
16849
+ function writeDemotion(value) {
16850
+ try {
16851
+ if (value) {
16852
+ window.localStorage.setItem(DEMOTION_KEY, JSON.stringify(value));
16853
+ }
16854
+ else {
16855
+ window.localStorage.removeItem(DEMOTION_KEY);
16856
+ }
16857
+ }
16858
+ catch (e) {
16859
+ // Without storage the demotion lasts only for this page.
16860
+ }
16861
+ }
16862
+ /*
16863
+ * Reads the GPU name, preferring the unmasked one, which browsers that hide it simply do not offer.
16864
+ */
16865
+ function readRenderer(gl) {
16866
+ try {
16867
+ const ext = gl.getExtension("WEBGL_debug_renderer_info");
16868
+ const name = ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER);
16869
+ return typeof name === "string" && name ? name : undefined;
16870
+ }
16871
+ catch (e) {
16872
+ return undefined;
16873
+ }
16874
+ }
16875
+ (function (DeviceProfile) {
16876
+ let ETier;
16877
+ (function (ETier) {
16878
+ ETier["Low"] = "LOW";
16879
+ ETier["Mid"] = "MID";
16880
+ ETier["High"] = "HIGH";
16881
+ })(ETier = DeviceProfile.ETier || (DeviceProfile.ETier = {}));
16882
+ DeviceProfile.BUDGETS = {
16883
+ [ETier.Low]: { tilesetMb: 384, splats: 1000000 },
16884
+ [ETier.Mid]: { tilesetMb: 1024, splats: 3000000 },
16885
+ [ETier.High]: { tilesetMb: 2048, splats: 6000000 }
16886
+ };
16887
+ const ORDER = [ETier.Low, ETier.Mid, ETier.High];
16888
+ /**
16889
+ * Raised after the override or a demotion changes the tier, so live budgets can be redistributed.
16890
+ */
16891
+ DeviceProfile.Changed = new Cesium.Event();
16892
+ let override = null;
16893
+ let detected = null;
16894
+ let logged = false;
16895
+ /*
16896
+ * Sorts a GPU name into a class. Order matters: Intel Arc is discrete, SwiftShader names any vendor.
16897
+ */
16898
+ function ClassifyGpu(renderer) {
16899
+ const name = (renderer || "").toLowerCase();
16900
+ if (!name) {
16901
+ return "unknown";
16902
+ }
16903
+ if (/swiftshader|llvmpipe|softpipe|lavapipe|software|basic render/.test(name)) {
16904
+ return "software";
16905
+ }
16906
+ if (/mali|adreno|powervr|apple gpu|videocore|immortalis|xclipse|tegra/.test(name)) {
16907
+ return "mobile";
16908
+ }
16909
+ if (/apple m\d+ (pro|max|ultra)/.test(name)) {
16910
+ return "discrete";
16911
+ }
16912
+ if (/nvidia|geforce|quadro|rtx|radeon (rx|pro|r9|vii)|firepro|arc(\(tm\))? a\d/.test(name)) {
16913
+ return "discrete";
16914
+ }
16915
+ if (/intel|apple m\d|radeon(\(tm\))? graphics|vega \d/.test(name)) {
16916
+ return "integrated";
16917
+ }
16918
+ return "unknown";
16919
+ }
16920
+ DeviceProfile.ClassifyGpu = ClassifyGpu;
16921
+ /**
16922
+ * Guesses a tier from the hardware alone. Unknown hardware stays mid; only clear evidence moves it.
16923
+ */
16924
+ function Classify(env) {
16925
+ const ua = (env.userAgent || "").toLowerCase();
16926
+ const mobile = /android|iphone|ipad|ipod|mobile/.test(ua) || (/macintosh/.test(ua) && env.maxTouchPoints > 1);
16927
+ const gpuClass = ClassifyGpu(env.gpuRenderer);
16928
+ const memory = env.deviceMemory;
16929
+ const reasons = [];
16930
+ let tier = ETier.Mid;
16931
+ if (gpuClass === "software") {
16932
+ reasons.push("software renderer");
16933
+ tier = ETier.Low;
16934
+ }
16935
+ if (mobile) {
16936
+ reasons.push("mobile device");
16937
+ tier = ETier.Low;
16938
+ }
16939
+ if (Number.isFinite(memory) && memory < 4) {
16940
+ reasons.push(`${memory} GB device memory`);
16941
+ tier = ETier.Low;
16942
+ }
16943
+ if (Number.isFinite(env.maxTextureSize) && env.maxTextureSize < 8192) {
16944
+ reasons.push(`max texture ${env.maxTextureSize}`);
16945
+ tier = ETier.Low;
16946
+ }
16947
+ if (tier === ETier.Mid && gpuClass === "discrete") {
16948
+ if (!Number.isFinite(memory) || memory >= 8) {
16949
+ reasons.push("discrete GPU");
16950
+ tier = ETier.High;
16951
+ }
16952
+ else {
16953
+ reasons.push(`discrete GPU held to mid by ${memory} GB device memory`);
16954
+ }
16955
+ }
16956
+ if (!reasons.length) {
16957
+ reasons.push(`${gpuClass} GPU`);
16958
+ }
16959
+ return { tier, gpuClass, mobile, reasons };
16960
+ }
16961
+ DeviceProfile.Classify = Classify;
16962
+ /*
16963
+ * Reads the browser environment, from the viewer's GL context when given rather than a spare one.
16964
+ */
16965
+ function readEnvironment(viewer) {
16966
+ var _a, _b, _c;
16967
+ const nav = typeof navigator !== "undefined" ? navigator : {};
16968
+ const env = {
16969
+ userAgent: nav.userAgent,
16970
+ maxTouchPoints: nav.maxTouchPoints,
16971
+ deviceMemory: nav.deviceMemory,
16972
+ hardwareConcurrency: nav.hardwareConcurrency
16973
+ };
16974
+ let gl = (_b = (_a = viewer === null || viewer === void 0 ? void 0 : viewer.scene) === null || _a === void 0 ? void 0 : _a.context) === null || _b === void 0 ? void 0 : _b._gl;
16975
+ let spare = false;
16976
+ if (!gl && typeof document !== "undefined") {
16977
+ try {
16978
+ gl = document.createElement("canvas").getContext("webgl");
16979
+ spare = Boolean(gl);
16980
+ }
16981
+ catch (e) {
16982
+ gl = null;
16983
+ }
16984
+ }
16985
+ // Detection must never stop a scene loading, so anything odd leaves the GPU unknown.
16986
+ if (gl &&
16987
+ typeof gl.getParameter === "function" &&
16988
+ typeof gl.getExtension === "function") {
16989
+ env.gpuRenderer = readRenderer(gl);
16990
+ try {
16991
+ env.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE) || undefined;
16992
+ if (spare) {
16993
+ // Browsers cap live contexts per page; give this one back now rather than at GC.
16994
+ (_c = gl.getExtension("WEBGL_lose_context")) === null || _c === void 0 ? void 0 : _c.loseContext();
16995
+ }
16996
+ }
16997
+ catch (e) {
16998
+ // Leave it unknown.
16999
+ }
17000
+ }
17001
+ return env;
17002
+ }
17003
+ /**
17004
+ * Returns this device's profile, detecting it once per page. Pass the viewer to read its GL context.
17005
+ */
17006
+ function Get(viewer) {
17007
+ var _a, _b;
17008
+ if (!detected) {
17009
+ const environment = readEnvironment(viewer);
17010
+ detected = { ...Classify(environment), environment };
17011
+ }
17012
+ const demotion = readDemotion(Date.now());
17013
+ const demoted = Math.min((_a = demotion === null || demotion === void 0 ? void 0 : demotion.count) !== null && _a !== void 0 ? _a : 0, DEMOTION_MAX);
17014
+ let tier = ORDER[Math.max(0, ORDER.indexOf(detected.tier) - demoted)];
17015
+ if (override) {
17016
+ tier = override;
17017
+ }
17018
+ const profile = {
17019
+ tier,
17020
+ detectedTier: detected.tier,
17021
+ gpuClass: detected.gpuClass,
17022
+ mobile: detected.mobile,
17023
+ demoted,
17024
+ overridden: Boolean(override),
17025
+ reasons: [...detected.reasons],
17026
+ environment: { ...detected.environment }
17027
+ };
17028
+ if (!logged) {
17029
+ logged = true;
17030
+ console.info(`[DeviceProfile] tier ${profile.tier} (${profile.reasons.join(", ")}` +
17031
+ `${demoted ? `, demoted ${demoted} after lost WebGL context` : ""}` +
17032
+ `${override ? ", overridden" : ""}). GPU: ${(_b = profile.environment.gpuRenderer) !== null && _b !== void 0 ? _b : "unknown"}.`);
17033
+ }
17034
+ return profile;
17035
+ }
17036
+ DeviceProfile.Get = Get;
17037
+ /**
17038
+ * Returns the memory budgets for this device's tier, or for a given tier.
17039
+ */
17040
+ function GetBudget(tier) {
17041
+ return DeviceProfile.BUDGETS[tier !== null && tier !== void 0 ? tier : Get().tier];
17042
+ }
17043
+ DeviceProfile.GetBudget = GetBudget;
17044
+ /**
17045
+ * Forces a tier, for a user who knows better than the guess. Null returns to detection.
17046
+ */
17047
+ function SetOverride(tier) {
17048
+ const next = tier && ORDER.includes(tier) ? tier : null;
17049
+ if (next === override) {
17050
+ return;
17051
+ }
17052
+ override = next;
17053
+ DeviceProfile.Changed.raiseEvent();
17054
+ }
17055
+ DeviceProfile.SetOverride = SetOverride;
17056
+ /**
17057
+ * Returns the forced tier, or null while detection decides.
17058
+ */
17059
+ function GetOverride() {
17060
+ return override;
17061
+ }
17062
+ DeviceProfile.GetOverride = GetOverride;
17063
+ /**
17064
+ * Records that a WebGL context was lost, so this and later sessions start a tier lower.
17065
+ */
17066
+ function RecordContextLost() {
17067
+ var _a, _b;
17068
+ const now = Date.now();
17069
+ const count = Math.min(((_b = (_a = readDemotion(now)) === null || _a === void 0 ? void 0 : _a.count) !== null && _b !== void 0 ? _b : 0) + 1, DEMOTION_MAX);
17070
+ writeDemotion({ count, at: now });
17071
+ console.warn(`[DeviceProfile] WebGL context lost; demoting the memory tier by ${count}.`);
17072
+ DeviceProfile.Changed.raiseEvent();
17073
+ }
17074
+ DeviceProfile.RecordContextLost = RecordContextLost;
17075
+ /**
17076
+ * Forgets earlier context losses, for a host that offers a reset.
17077
+ */
17078
+ function ClearDemotion() {
17079
+ writeDemotion(null);
17080
+ DeviceProfile.Changed.raiseEvent();
17081
+ }
17082
+ DeviceProfile.ClearDemotion = ClearDemotion;
17083
+ /**
17084
+ * Reports the viewer's context losses. Safe to call repeatedly.
17085
+ */
17086
+ function WatchViewer(viewer) {
17087
+ const canvas = viewer === null || viewer === void 0 ? void 0 : viewer.canvas;
17088
+ if (!canvas ||
17089
+ viewer[VIEWER_WATCH_KEY] ||
17090
+ typeof canvas.addEventListener !== "function") {
17091
+ return;
17092
+ }
17093
+ viewer[VIEWER_WATCH_KEY] = true;
17094
+ canvas.addEventListener("webglcontextlost", () => {
17095
+ // Ignore a loss caused by tearing the viewer down.
17096
+ if (!viewer.isDestroyed()) {
17097
+ RecordContextLost();
17098
+ }
17099
+ });
17100
+ }
17101
+ DeviceProfile.WatchViewer = WatchViewer;
17102
+ /**
17103
+ * Forgets the detected hardware and override, for tests.
17104
+ */
17105
+ function Reset() {
17106
+ detected = null;
17107
+ override = null;
17108
+ logged = false;
17109
+ }
17110
+ DeviceProfile.Reset = Reset;
17111
+ })(exports.DeviceProfile || (exports.DeviceProfile = {}));
17112
+
16190
17113
  /**
16191
17114
  * Returns if a given tileset is alive and in the scene.
16192
17115
  * @param viewer
@@ -16205,7 +17128,7 @@
16205
17128
  }
16206
17129
  return viewer.scene.primitives.contains(cTileset);
16207
17130
  }
16208
- const VIEWER_WATCH_KEY = "bruce-viewer-watch";
17131
+ const VIEWER_WATCH_KEY$1 = "bruce-viewer-watch";
16209
17132
  const WATCH_KEY = "bruce-tileset-watch";
16210
17133
  const VIEWER_DATE_RANGE_WATCH_KEY = "bruce-viewer-date-range-watch";
16211
17134
  const DATE_RANGE_WATCH_KEY = "bruce-tileset-date-range-watch";
@@ -16267,6 +17190,22 @@
16267
17190
  }
16268
17191
  renderObject._bruceDateRangeVisible = visible;
16269
17192
  }
17193
+ // A tileset may pass its cache share by this fraction before Cesium coarsens it to fit.
17194
+ const TILESET_OVERFLOW_RATIO = 0.25;
17195
+ /*
17196
+ * Returns the options capping a tileset's tile cache at the given MB, in the form the running Cesium reads.
17197
+ */
17198
+ function getTilesetMemoryProps(mb) {
17199
+ const prototype = Cesium.Cesium3DTileset.prototype;
17200
+ if ("cacheBytes" in prototype) {
17201
+ const props = { cacheBytes: Math.round(mb * 1024 * 1024) };
17202
+ if ("maximumCacheOverflowBytes" in prototype) {
17203
+ props.maximumCacheOverflowBytes = Math.round(mb * TILESET_OVERFLOW_RATIO * 1024 * 1024);
17204
+ }
17205
+ return props;
17206
+ }
17207
+ return { maximumMemoryUsage: mb };
17208
+ }
16270
17209
  /**
16271
17210
  * Creates and returns a tileset instance from a given url.
16272
17211
  * This is made to handle the difference between old and new cesium versions.
@@ -16283,8 +17222,8 @@
16283
17222
  };
16284
17223
  Object.assign(defaultProps, exports.ViewUtils.DefaultLightingProps());
16285
17224
  if (noMemoryLimit != true) {
16286
- // "The maximum amount of memory in MB that can be used by the tileset.".
16287
- defaultProps.maximumMemoryUsage = 512;
17225
+ // The memory watcher narrows this to a share once the tileset joins it.
17226
+ Object.assign(defaultProps, getTilesetMemoryProps(exports.TilesetRenderEngine.GetTilesetMemoryBudget()));
16288
17227
  }
16289
17228
  const C3DT = Cesium.Cesium3DTileset;
16290
17229
  if (C3DT.fromUrl) {
@@ -16835,10 +17774,23 @@
16835
17774
  }
16836
17775
  TilesetRenderEngine.UnwatchDateRange = UnwatchDateRange;
16837
17776
  /**
16838
- * The maximum memory in MB that can be used by all tilesets.
16839
- * This is distributed evenly between all loaded tilesets.
17777
+ * Total MB all tilesets may use, split evenly; null leaves it to the DeviceProfile tier.
17778
+ */
17779
+ TilesetRenderEngine.MAX_TILESET_MEMORY = null;
17780
+ /**
17781
+ * Smallest MB share a tileset gets; small, since Cesium coarsens one that needs more.
16840
17782
  */
16841
- TilesetRenderEngine.MAX_TILESET_MEMORY = 1024;
17783
+ TilesetRenderEngine.MIN_TILESET_MEMORY = 32;
17784
+ /**
17785
+ * Returns the total MB all tilesets may use, from MAX_TILESET_MEMORY or else the device tier.
17786
+ */
17787
+ function GetTilesetMemoryBudget() {
17788
+ if (Number.isFinite(TilesetRenderEngine.MAX_TILESET_MEMORY) && TilesetRenderEngine.MAX_TILESET_MEMORY > 0) {
17789
+ return TilesetRenderEngine.MAX_TILESET_MEMORY;
17790
+ }
17791
+ return exports.DeviceProfile.GetBudget().tilesetMb;
17792
+ }
17793
+ TilesetRenderEngine.GetTilesetMemoryBudget = GetTilesetMemoryBudget;
16842
17794
  /**
16843
17795
  * Watches tileset date cutoffs and toggles their primitive visibility based on the viewer clock.
16844
17796
  */
@@ -16929,23 +17881,26 @@
16929
17881
  constructor(viewer) {
16930
17882
  this.watched = [];
16931
17883
  this.viewer = viewer;
17884
+ exports.DeviceProfile.Get(viewer);
17885
+ exports.DeviceProfile.WatchViewer(viewer);
17886
+ this.removeChanged = exports.DeviceProfile.Changed.addEventListener(() => this.distributeMemory());
16932
17887
  }
16933
17888
  distributeMemory() {
16934
17889
  this.clean();
16935
- // Total 1gb as default.
16936
- // We'll need to allow user to change this somehow.
16937
- const maxMemory = TilesetRenderEngine.MAX_TILESET_MEMORY;
16938
- // Minimum memory in MB per tileset.
16939
- const minMemory = 80;
16940
- const totalPerTileset = Math.max(this.watched.length ? maxMemory / this.watched.length : maxMemory, minMemory);
17890
+ if (!this.watched.length) {
17891
+ return;
17892
+ }
17893
+ const share = Math.max(GetTilesetMemoryBudget() / this.watched.length, TilesetRenderEngine.MIN_TILESET_MEMORY);
17894
+ const props = getTilesetMemoryProps(share);
16941
17895
  this.watched.forEach(x => {
16942
- // Newer Cesium killed this property.
16943
- // TODO: Check if it's needed then.
16944
- x["maximumMemoryUsage"] = totalPerTileset;
17896
+ Object.assign(x, props);
16945
17897
  });
16946
17898
  }
16947
17899
  destroy() {
17900
+ var _a;
16948
17901
  this.watched = [];
17902
+ (_a = this.removeChanged) === null || _a === void 0 ? void 0 : _a.call(this);
17903
+ this.removeChanged = null;
16949
17904
  }
16950
17905
  /**
16951
17906
  * Remove all dead tilesets.
@@ -16994,10 +17949,10 @@
16994
17949
  if (!viewer || viewer.isDestroyed()) {
16995
17950
  return null;
16996
17951
  }
16997
- if (!viewer[VIEWER_WATCH_KEY]) {
16998
- viewer[VIEWER_WATCH_KEY] = new MemoryWatcher(viewer);
17952
+ if (!viewer[VIEWER_WATCH_KEY$1]) {
17953
+ viewer[VIEWER_WATCH_KEY$1] = new MemoryWatcher(viewer);
16999
17954
  }
17000
- return viewer[VIEWER_WATCH_KEY];
17955
+ return viewer[VIEWER_WATCH_KEY$1];
17001
17956
  }
17002
17957
  TilesetRenderEngine.GetMemoryWatcher = GetMemoryWatcher;
17003
17958
  function GetDateRangeWatcher(viewer) {
@@ -28159,7 +29114,9 @@
28159
29114
  if (loadUrl.indexOf("?") !== -1) {
28160
29115
  loadUrl = loadUrl.split("?")[0];
28161
29116
  }
28162
- let maxSplats = 6000000;
29117
+ const profile = exports.DeviceProfile.Get(viewer);
29118
+ exports.DeviceProfile.WatchViewer(viewer);
29119
+ let maxSplats = exports.DeviceProfile.GetBudget(profile.tier).splats;
28163
29120
  /*
28164
29121
  * Configures optional LOD capabilities once metadata or initial geometry is ready.
28165
29122
  */
@@ -28244,8 +29201,7 @@
28244
29201
  }
28245
29202
  try {
28246
29203
  const sdkBudget = typeof obj.getCurrentConfig === "function" ? (_d = obj.getCurrentConfig()) === null || _d === void 0 ? void 0 : _d.maxLoadSplatCount : undefined;
28247
- // Preserve the SDK's mobile and weak-GPU limits before applying any temporary startup cap.
28248
- maxSplats = Number.isFinite(sdkBudget) && sdkBudget > 0 && sdkBudget <= 2000000 ? sdkBudget : 6000000;
29204
+ maxSplats = GetSplatBudget(sdkBudget, profile);
28249
29205
  if (LCCRender.nextspaceMetadataReady === 1 &&
28250
29206
  typeof obj.setNextspaceMetadataCallback === "function" &&
28251
29207
  typeof obj.setMaxSplats === "function") {
@@ -28286,6 +29242,21 @@
28286
29242
  return obj;
28287
29243
  }
28288
29244
  XGridsRenderEngine.Render = Render;
29245
+ /*
29246
+ * The most splats a scan may load: the tier's, lowered by the SDK's GPU limit unless overridden.
29247
+ */
29248
+ function GetSplatBudget(sdkBudget, profile) {
29249
+ const budget = exports.DeviceProfile.GetBudget(profile.tier).splats;
29250
+ // Above 2M the SDK is reporting its flat default, not a limit it chose for this GPU.
29251
+ if (!profile.overridden &&
29252
+ Number.isFinite(sdkBudget) &&
29253
+ sdkBudget > 0 &&
29254
+ sdkBudget <= 2000000) {
29255
+ return Math.min(sdkBudget, budget);
29256
+ }
29257
+ return budget;
29258
+ }
29259
+ XGridsRenderEngine.GetSplatBudget = GetSplatBudget;
28289
29260
  /*
28290
29261
  * Detaches optional tuning before unloading the scan.
28291
29262
  */
@@ -38712,6 +39683,1228 @@
38712
39683
  })(Terrain = TileRenderEngine.Terrain || (TileRenderEngine.Terrain = {}));
38713
39684
  })(exports.TileRenderEngine || (exports.TileRenderEngine = {}));
38714
39685
 
39686
+ // Builds simple meshes into binary glTF in code. Nothing here imports Cesium, so it can move to bruce-models as is.
39687
+ const GLB_MAGIC = 0x46546C67;
39688
+ const GLB_CHUNK_JSON = 0x4E4F534A;
39689
+ const GLB_CHUNK_BIN = 0x004E4942;
39690
+ const GL_ARRAY_BUFFER = 34962;
39691
+ const GL_ELEMENT_ARRAY_BUFFER = 34963;
39692
+ const GL_UNSIGNED_SHORT = 5123;
39693
+ const GL_UNSIGNED_INT = 5125;
39694
+ const GL_FLOAT = 5126;
39695
+ const GL_TRIANGLES = 4;
39696
+ (function (GltfGenerator) {
39697
+ /**
39698
+ * Vertices and triangles collected by the shape functions, one material's worth.
39699
+ */
39700
+ class Mesh {
39701
+ constructor() {
39702
+ this.positions = [];
39703
+ this.normals = [];
39704
+ this.indices = [];
39705
+ }
39706
+ AddVertex(position, normal) {
39707
+ this.positions.push(position[0], position[1], position[2]);
39708
+ this.normals.push(normal[0], normal[1], normal[2]);
39709
+ return this.positions.length / 3 - 1;
39710
+ }
39711
+ /**
39712
+ * Adds a triangle wound so its front faces along normal, whichever order the corners come in.
39713
+ * Per corner normals smooth it; without them every corner takes the face normal.
39714
+ */
39715
+ AddTriangle(a, b, c, normal, corners) {
39716
+ var _a, _b, _c;
39717
+ const ia = this.AddVertex(a, (_a = corners === null || corners === void 0 ? void 0 : corners[0]) !== null && _a !== void 0 ? _a : normal);
39718
+ const ib = this.AddVertex(b, (_b = corners === null || corners === void 0 ? void 0 : corners[1]) !== null && _b !== void 0 ? _b : normal);
39719
+ const ic = this.AddVertex(c, (_c = corners === null || corners === void 0 ? void 0 : corners[2]) !== null && _c !== void 0 ? _c : normal);
39720
+ if (Dot(Cross(Sub(b, a), Sub(c, a)), normal) >= 0) {
39721
+ this.indices.push(ia, ib, ic);
39722
+ }
39723
+ else {
39724
+ this.indices.push(ia, ic, ib);
39725
+ }
39726
+ }
39727
+ /**
39728
+ * Joins three added vertices into a triangle facing along normal, so shapes can share corners.
39729
+ */
39730
+ AddIndexedTriangle(ia, ib, ic, normal) {
39731
+ const at = (i) => [this.positions[i * 3], this.positions[i * 3 + 1], this.positions[i * 3 + 2]];
39732
+ const a = at(ia);
39733
+ if (Dot(Cross(Sub(at(ib), a), Sub(at(ic), a)), normal) >= 0) {
39734
+ this.indices.push(ia, ib, ic);
39735
+ }
39736
+ else {
39737
+ this.indices.push(ia, ic, ib);
39738
+ }
39739
+ }
39740
+ get VertexCount() {
39741
+ return this.positions.length / 3;
39742
+ }
39743
+ }
39744
+ GltfGenerator.Mesh = Mesh;
39745
+ function Add(a, b) {
39746
+ return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
39747
+ }
39748
+ GltfGenerator.Add = Add;
39749
+ function Sub(a, b) {
39750
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
39751
+ }
39752
+ GltfGenerator.Sub = Sub;
39753
+ function Scale(a, s) {
39754
+ return [a[0] * s, a[1] * s, a[2] * s];
39755
+ }
39756
+ GltfGenerator.Scale = Scale;
39757
+ function Dot(a, b) {
39758
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
39759
+ }
39760
+ GltfGenerator.Dot = Dot;
39761
+ function Cross(a, b) {
39762
+ return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
39763
+ }
39764
+ GltfGenerator.Cross = Cross;
39765
+ function Normalize(a) {
39766
+ const length = Math.hypot(a[0], a[1], a[2]);
39767
+ return length ? Scale(a, 1 / length) : [0, 0, 0];
39768
+ }
39769
+ GltfGenerator.Normalize = Normalize;
39770
+ /*
39771
+ * Two unit vectors at right angles to an axis and each other, the first along start where one is given.
39772
+ */
39773
+ function perpendiculars(axis, start) {
39774
+ const n = Normalize(axis);
39775
+ let u = start ? Sub(start, Scale(n, Dot(start, n))) : null;
39776
+ if (!u || Math.hypot(u[0], u[1], u[2]) < 1e-9) {
39777
+ u = Math.abs(n[0]) < 0.9 ? Cross(n, [1, 0, 0]) : Cross(n, [0, 1, 0]);
39778
+ }
39779
+ u = Normalize(u);
39780
+ return [u, Cross(n, u)];
39781
+ }
39782
+ /*
39783
+ * The point on a circle around centre at a given turn, in the plane the two perpendiculars span.
39784
+ */
39785
+ function onCircle(centre, u, v, radius, angle) {
39786
+ return Add(centre, Add(Scale(u, Math.cos(angle) * radius), Scale(v, Math.sin(angle) * radius)));
39787
+ }
39788
+ /**
39789
+ * Adds a flat disc facing along normal.
39790
+ */
39791
+ function AddDisc(mesh, params) {
39792
+ const { centre, radius, sides } = params;
39793
+ const normal = Normalize(params.normal);
39794
+ const [u, v] = perpendiculars(normal, params.start);
39795
+ for (let i = 0; i < sides; i++) {
39796
+ const a = onCircle(centre, u, v, radius, i / sides * Math.PI * 2);
39797
+ const b = onCircle(centre, u, v, radius, (i + 1) / sides * Math.PI * 2);
39798
+ mesh.AddTriangle(centre, a, b, normal);
39799
+ }
39800
+ }
39801
+ GltfGenerator.AddDisc = AddDisc;
39802
+ /**
39803
+ * Adds a smooth sided cylinder between two points, closed at the start by default.
39804
+ */
39805
+ function AddCylinder(mesh, params) {
39806
+ const { start, end, radius, sides } = params;
39807
+ const axis = Normalize(Sub(end, start));
39808
+ const [u, v] = perpendiculars(axis);
39809
+ for (let i = 0; i < sides; i++) {
39810
+ const angleA = i / sides * Math.PI * 2;
39811
+ const angleB = (i + 1) / sides * Math.PI * 2;
39812
+ const outA = onCircle([0, 0, 0], u, v, 1, angleA);
39813
+ const outB = onCircle([0, 0, 0], u, v, 1, angleB);
39814
+ const a0 = onCircle(start, u, v, radius, angleA);
39815
+ const b0 = onCircle(start, u, v, radius, angleB);
39816
+ const a1 = onCircle(end, u, v, radius, angleA);
39817
+ const b1 = onCircle(end, u, v, radius, angleB);
39818
+ const face = Normalize(Add(outA, outB));
39819
+ mesh.AddTriangle(a0, b0, b1, face, [outA, outB, outB]);
39820
+ mesh.AddTriangle(a0, b1, a1, face, [outA, outB, outA]);
39821
+ }
39822
+ if (params.capStart != false) {
39823
+ AddDisc(mesh, { centre: start, normal: Scale(axis, -1), radius, sides });
39824
+ }
39825
+ if (params.capEnd) {
39826
+ AddDisc(mesh, { centre: end, normal: axis, radius, sides });
39827
+ }
39828
+ }
39829
+ GltfGenerator.AddCylinder = AddCylinder;
39830
+ /**
39831
+ * Adds a cone from a base circle to a tip, closed at the base by default.
39832
+ */
39833
+ function AddCone(mesh, params) {
39834
+ const { base, tip, radius, sides } = params;
39835
+ const toTip = Sub(tip, base);
39836
+ const height = Math.hypot(toTip[0], toTip[1], toTip[2]);
39837
+ const axis = Normalize(toTip);
39838
+ const [u, v] = perpendiculars(axis, params.start);
39839
+ // A side normal leans toward the tip by the slope of the side.
39840
+ const sideNormal = (angle) => Normalize(Add(Scale(onCircle([0, 0, 0], u, v, 1, angle), height), Scale(axis, radius)));
39841
+ for (let i = 0; i < sides; i++) {
39842
+ const angleA = i / sides * Math.PI * 2;
39843
+ const angleB = (i + 1) / sides * Math.PI * 2;
39844
+ const a = onCircle(base, u, v, radius, angleA);
39845
+ const b = onCircle(base, u, v, radius, angleB);
39846
+ const face = sideNormal((angleA + angleB) / 2);
39847
+ mesh.AddTriangle(a, b, tip, face, params.faceted ? undefined : [sideNormal(angleA), sideNormal(angleB), face]);
39848
+ }
39849
+ if (params.cap != false) {
39850
+ AddDisc(mesh, { centre: base, normal: Scale(axis, -1), radius, sides, start: params.start });
39851
+ }
39852
+ }
39853
+ GltfGenerator.AddCone = AddCone;
39854
+ /**
39855
+ * Adds a smooth ring of tube around an axis through centre.
39856
+ */
39857
+ function AddTorus(mesh, params) {
39858
+ const { centre, radius, tube, segments, sides } = params;
39859
+ const axis = Normalize(params.axis);
39860
+ const [u, v] = perpendiculars(axis);
39861
+ // A grid of shared corners, wrapping round both ways.
39862
+ const first = mesh.VertexCount;
39863
+ for (let i = 0; i < segments; i++) {
39864
+ const out = onCircle([0, 0, 0], u, v, 1, i / segments * Math.PI * 2);
39865
+ for (let j = 0; j < sides; j++) {
39866
+ const turn = j / sides * Math.PI * 2;
39867
+ const normal = Add(Scale(out, Math.cos(turn)), Scale(axis, Math.sin(turn)));
39868
+ mesh.AddVertex(Add(Add(centre, Scale(out, radius)), Scale(normal, tube)), normal);
39869
+ }
39870
+ }
39871
+ const at = (i, j) => first + (i % segments) * sides + (j % sides);
39872
+ const normalAt = (i) => [mesh.normals[i * 3], mesh.normals[i * 3 + 1], mesh.normals[i * 3 + 2]];
39873
+ for (let i = 0; i < segments; i++) {
39874
+ for (let j = 0; j < sides; j++) {
39875
+ const quad = [at(i, j), at(i + 1, j), at(i + 1, j + 1), at(i, j + 1)];
39876
+ const normal = Normalize(Add(normalAt(quad[0]), normalAt(quad[2])));
39877
+ mesh.AddIndexedTriangle(quad[0], quad[1], quad[2], normal);
39878
+ mesh.AddIndexedTriangle(quad[0], quad[2], quad[3], normal);
39879
+ }
39880
+ }
39881
+ }
39882
+ GltfGenerator.AddTorus = AddTorus;
39883
+ /**
39884
+ * Adds a flat convex outline, given in a plane's own across and up coordinates, facing along normal.
39885
+ */
39886
+ function AddConvexOutline(mesh, params) {
39887
+ const { points, origin, across, up } = params;
39888
+ const normal = Normalize(params.normal);
39889
+ const at = ([x, y]) => Add(origin, Add(Scale(across, x), Scale(up, y)));
39890
+ for (let i = 1; i < points.length - 1; i++) {
39891
+ mesh.AddTriangle(at(points[0]), at(points[i]), at(points[i + 1]), normal);
39892
+ }
39893
+ }
39894
+ GltfGenerator.AddConvexOutline = AddConvexOutline;
39895
+ function align4(length) {
39896
+ return Math.ceil(length / 4) * 4;
39897
+ }
39898
+ function bounds(positions) {
39899
+ const min = [Infinity, Infinity, Infinity];
39900
+ const max = [-Infinity, -Infinity, -Infinity];
39901
+ for (let i = 0; i < positions.length; i++) {
39902
+ const k = i % 3;
39903
+ min[k] = Math.min(min[k], positions[i]);
39904
+ max[k] = Math.max(max[k], positions[i]);
39905
+ }
39906
+ return { min, max };
39907
+ }
39908
+ /**
39909
+ * Packs a mesh into GLB bytes, under a chain of node matrices (outermost first) when given.
39910
+ */
39911
+ function ToGlb(params) {
39912
+ var _a, _b, _c;
39913
+ const { mesh, material } = params;
39914
+ const count = mesh.VertexCount;
39915
+ const wide = count > 0xFFFF;
39916
+ const IndexArray = wide ? Uint32Array : Uint16Array;
39917
+ const indexLength = mesh.indices.length * IndexArray.BYTES_PER_ELEMENT;
39918
+ const indexBytes = align4(indexLength);
39919
+ const vertexBytes = count * 12;
39920
+ const bin = new ArrayBuffer(indexBytes + vertexBytes * 2);
39921
+ new IndexArray(bin, 0, mesh.indices.length).set(mesh.indices);
39922
+ new Float32Array(bin, indexBytes, count * 3).set(mesh.positions);
39923
+ new Float32Array(bin, indexBytes + vertexBytes, count * 3).set(mesh.normals);
39924
+ // The mesh node is last, each matrix node holding the next.
39925
+ const matrices = (_a = params.matrices) !== null && _a !== void 0 ? _a : [];
39926
+ const nodes = matrices.map((matrix, i) => ({ matrix, children: [i + 1] }));
39927
+ nodes.push({ mesh: 0 });
39928
+ const json = {
39929
+ asset: { version: "2.0", generator: "bruce-cesium GltfGenerator" },
39930
+ scene: 0,
39931
+ scenes: [{ nodes: [0] }],
39932
+ nodes,
39933
+ meshes: [{ primitives: [{ attributes: { POSITION: 1, NORMAL: 2 }, indices: 0, material: 0, mode: GL_TRIANGLES }] }],
39934
+ materials: [{
39935
+ pbrMetallicRoughness: {
39936
+ baseColorFactor: material.color,
39937
+ metallicFactor: (_b = material.metallic) !== null && _b !== void 0 ? _b : 0,
39938
+ roughnessFactor: (_c = material.roughness) !== null && _c !== void 0 ? _c : 1
39939
+ },
39940
+ doubleSided: Boolean(material.doubleSided)
39941
+ }],
39942
+ buffers: [{ byteLength: bin.byteLength }],
39943
+ bufferViews: [
39944
+ { buffer: 0, byteOffset: 0, byteLength: indexLength, target: GL_ELEMENT_ARRAY_BUFFER },
39945
+ { buffer: 0, byteOffset: indexBytes, byteLength: vertexBytes, target: GL_ARRAY_BUFFER },
39946
+ { buffer: 0, byteOffset: indexBytes + vertexBytes, byteLength: vertexBytes, target: GL_ARRAY_BUFFER }
39947
+ ],
39948
+ accessors: [
39949
+ { bufferView: 0, componentType: wide ? GL_UNSIGNED_INT : GL_UNSIGNED_SHORT, count: mesh.indices.length, type: "SCALAR" },
39950
+ { bufferView: 1, componentType: GL_FLOAT, count, type: "VEC3", ...bounds(mesh.positions) },
39951
+ { bufferView: 2, componentType: GL_FLOAT, count, type: "VEC3" }
39952
+ ]
39953
+ };
39954
+ let text = JSON.stringify(json);
39955
+ text += " ".repeat(align4(text.length) - text.length);
39956
+ const jsonBytes = new TextEncoder().encode(text);
39957
+ const glb = new Uint8Array(20 + jsonBytes.length + 8 + bin.byteLength);
39958
+ const view = new DataView(glb.buffer);
39959
+ view.setUint32(0, GLB_MAGIC, true);
39960
+ view.setUint32(4, 2, true);
39961
+ view.setUint32(8, glb.length, true);
39962
+ view.setUint32(12, jsonBytes.length, true);
39963
+ view.setUint32(16, GLB_CHUNK_JSON, true);
39964
+ glb.set(jsonBytes, 20);
39965
+ const binStart = 20 + jsonBytes.length;
39966
+ view.setUint32(binStart, bin.byteLength, true);
39967
+ view.setUint32(binStart + 4, GLB_CHUNK_BIN, true);
39968
+ glb.set(new Uint8Array(bin), binStart + 8);
39969
+ return glb;
39970
+ }
39971
+ GltfGenerator.ToGlb = ToGlb;
39972
+ /**
39973
+ * Packs a mesh into a GLB data URI, which loads as a model url with nothing to fetch.
39974
+ */
39975
+ function ToDataUri(params) {
39976
+ const glb = ToGlb(params);
39977
+ let binary = "";
39978
+ for (let i = 0; i < glb.length; i += 0x8000) {
39979
+ binary += String.fromCharCode.apply(null, glb.subarray(i, i + 0x8000));
39980
+ }
39981
+ return "data:model/gltf-binary;base64," + btoa(binary);
39982
+ }
39983
+ GltfGenerator.ToDataUri = ToDataUri;
39984
+ })(exports.GltfGenerator || (exports.GltfGenerator = {}));
39985
+
39986
+ // Sides on an arrow's shaft and cone.
39987
+ const ARROW_SIDES = 16;
39988
+ const SHAFT_RADIUS = 2.4;
39989
+ const SHAFT_LENGTH = 265;
39990
+ const CONE_RADIUS = 10;
39991
+ const CONE_BASE = 264.6;
39992
+ const CONE_TIP = 306.3;
39993
+ const RING_RADIUS = 210;
39994
+ const RING_TUBE = 1;
39995
+ const RING_SEGMENTS = 60;
39996
+ const RING_SIDES = 10;
39997
+ // The ring's two arrowheads sit either side of its 45 degree mark, each pointing back toward it.
39998
+ const RING_MARK_ANGLES = [137.64, -47.64];
39999
+ const RING_MARK_LENGTH = 5.82;
40000
+ const RING_MARK_CENTRE = 209.8;
40001
+ const RING_MARK_RADIUS = 8.5;
40002
+ const RING_MARK_SIDES = 8;
40003
+ // Turns the original models' Y up node into the Z up frame the pieces were drawn in.
40004
+ const ROOT_MATRIX = [1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1];
40005
+ // Points a piece drawn along +Z down another axis, as the original models' mesh nodes did.
40006
+ const TO_X_MATRIX = [0, 0, -1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1];
40007
+ const TO_Y_ARROW_MATRIX = [0, 0, -1, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1];
40008
+ const TO_Y_RING_MATRIX = [1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1];
40009
+ const RED = { color: [1, 0, 0, 1] };
40010
+ const GREEN = { color: [0.2196, 0.5216, 0.1647, 1] };
40011
+ const BLUE = { color: [0.1608, 0.3843, 0.698, 1] };
40012
+ const LETTER_X = {
40013
+ across: [0, 1, 0],
40014
+ facing: [-1, 0, 0],
40015
+ strokes: [
40016
+ [[16.4, 318.6], [16.4, 324.2], [-19.4, 349.8], [-19.4, 343.9]],
40017
+ [[-19.4, 316.9], [-19.4, 322.6], [16.4, 348.8], [16.4, 343.6]]
40018
+ ]
40019
+ };
40020
+ const LETTER_Y = {
40021
+ across: [0, 1, 0],
40022
+ facing: [-1, 0, 0],
40023
+ strokes: [
40024
+ [[-1.9, 320.1], [2.8, 320.1], [2.8, 337.7], [-1.9, 337.7]],
40025
+ [[-1.9, 335.3], [2.8, 335.3], [-10.7, 355.9], [-16.2, 355.9]],
40026
+ [[-1.9, 335.3], [2.8, 335.3], [16.6, 355.9], [10.9, 355.9]]
40027
+ ]
40028
+ };
40029
+ const LETTER_Z = {
40030
+ across: [1, 0, 0],
40031
+ facing: [0, -1, 0],
40032
+ strokes: [
40033
+ [[-14.6, 319], [13.7, 319], [13.7, 323.3], [-14.6, 323.3]],
40034
+ [[-14.6, 323.3], [-9.1, 323.3], [13.1, 350.6], [7.5, 350.6]],
40035
+ [[-12.5, 350.6], [13.1, 350.6], [13.1, 354.8], [-12.5, 354.8]]
40036
+ ]
40037
+ };
40038
+ /*
40039
+ * An axis arrow along +Z: shaft, cone and the axis letter past the tip.
40040
+ */
40041
+ function arrowMesh(letter) {
40042
+ const mesh = new exports.GltfGenerator.Mesh();
40043
+ exports.GltfGenerator.AddCylinder(mesh, { start: [0, 0, 0], end: [0, 0, SHAFT_LENGTH], radius: SHAFT_RADIUS, sides: ARROW_SIDES });
40044
+ exports.GltfGenerator.AddCone(mesh, { base: [0, 0, CONE_BASE], tip: [0, 0, CONE_TIP], radius: CONE_RADIUS, sides: ARROW_SIDES });
40045
+ for (const stroke of letter.strokes) {
40046
+ exports.GltfGenerator.AddConvexOutline(mesh, { points: stroke, origin: [0, 0, 0], across: letter.across, up: [0, 0, 1], normal: letter.facing });
40047
+ }
40048
+ return mesh;
40049
+ }
40050
+ function onRing(degrees, radius) {
40051
+ const angle = degrees * Math.PI / 180;
40052
+ return [Math.cos(angle) * radius, Math.sin(angle) * radius, 0];
40053
+ }
40054
+ /*
40055
+ * A rotation ring about +Z with its two arrowheads.
40056
+ */
40057
+ function ringMesh() {
40058
+ const mesh = new exports.GltfGenerator.Mesh();
40059
+ exports.GltfGenerator.AddTorus(mesh, {
40060
+ centre: [0, 0, 0],
40061
+ axis: [0, 0, 1],
40062
+ radius: RING_RADIUS,
40063
+ tube: RING_TUBE,
40064
+ segments: RING_SEGMENTS,
40065
+ sides: RING_SIDES
40066
+ });
40067
+ for (const degrees of RING_MARK_ANGLES) {
40068
+ const toward = degrees > 45 ? -RING_MARK_LENGTH : RING_MARK_LENGTH;
40069
+ exports.GltfGenerator.AddCone(mesh, {
40070
+ base: onRing(degrees, RING_MARK_CENTRE),
40071
+ tip: onRing(degrees + toward, RING_MARK_CENTRE),
40072
+ radius: RING_MARK_RADIUS,
40073
+ sides: RING_MARK_SIDES,
40074
+ faceted: true,
40075
+ // A rim corner points at the ring's centre, as the original arrowheads' did.
40076
+ start: onRing(degrees, 1)
40077
+ });
40078
+ }
40079
+ return mesh;
40080
+ }
40081
+ (function (GltfGizmo) {
40082
+ let EPiece;
40083
+ (function (EPiece) {
40084
+ EPiece["X"] = "x";
40085
+ EPiece["Y"] = "y";
40086
+ EPiece["Z"] = "z";
40087
+ EPiece["XRotation"] = "x-rot";
40088
+ EPiece["YRotation"] = "y-rot";
40089
+ EPiece["ZRotation"] = "z-rot";
40090
+ })(EPiece = GltfGizmo.EPiece || (GltfGizmo.EPiece = {}));
40091
+ const BUILDERS = {
40092
+ [EPiece.X]: () => exports.GltfGenerator.ToDataUri({ mesh: arrowMesh(LETTER_X), material: RED, matrices: [ROOT_MATRIX, TO_X_MATRIX] }),
40093
+ [EPiece.Y]: () => exports.GltfGenerator.ToDataUri({ mesh: arrowMesh(LETTER_Y), material: GREEN, matrices: [ROOT_MATRIX, TO_Y_ARROW_MATRIX] }),
40094
+ [EPiece.Z]: () => exports.GltfGenerator.ToDataUri({ mesh: arrowMesh(LETTER_Z), material: BLUE, matrices: [ROOT_MATRIX] }),
40095
+ [EPiece.XRotation]: () => exports.GltfGenerator.ToDataUri({ mesh: ringMesh(), material: RED, matrices: [ROOT_MATRIX, TO_X_MATRIX] }),
40096
+ [EPiece.YRotation]: () => exports.GltfGenerator.ToDataUri({ mesh: ringMesh(), material: GREEN, matrices: [ROOT_MATRIX, TO_Y_RING_MATRIX] }),
40097
+ [EPiece.ZRotation]: () => exports.GltfGenerator.ToDataUri({ mesh: ringMesh(), material: BLUE, matrices: [ROOT_MATRIX] })
40098
+ };
40099
+ // Built pieces, so every gizmo shares one string per piece and CesiumJS's model cache sees the same url.
40100
+ const built = {};
40101
+ /**
40102
+ * Returns a piece's model as a data URI, to pass wherever a model url is taken.
40103
+ */
40104
+ function GetUri(piece) {
40105
+ if (!built[piece]) {
40106
+ built[piece] = BUILDERS[piece]();
40107
+ }
40108
+ return built[piece];
40109
+ }
40110
+ GltfGizmo.GetUri = GetUri;
40111
+ })(exports.GltfGizmo || (exports.GltfGizmo = {}));
40112
+
40113
+ // viewer[CLASS_INSTANCE_KEY] will point at an instance of MapGridHandler.
40114
+ const CLASS_INSTANCE_KEY = "nextspace-navigator-grid-handler";
40115
+ // viewer[UPDATE_MARKER_KEY] will point at a number that is incremented every time the grid is updated.
40116
+ // This is used to determine if the grid is still valid or if it needs to be redrawn.
40117
+ const UPDATE_MARKER_KEY = "nextspace-marker-grid";
40118
+ // viewer[LAST_SETTINGS_KEY] will point at the last settings used for the grid.
40119
+ const LAST_SETTINGS_KEY = "nextspace-navigator-last-settings-grid";
40120
+ /*
40121
+ * The point a distance in metres along a heading in degrees from another, over a spherical earth.
40122
+ */
40123
+ function offsetPoint$1(point, distance, heading) {
40124
+ // Radius of earth.
40125
+ const radius = 6371e3;
40126
+ const δ = distance / radius;
40127
+ const θ = Cesium.Math.toRadians(heading);
40128
+ const φ1 = Cesium.Math.toRadians(point.latitude);
40129
+ const λ1 = Cesium.Math.toRadians(point.longitude);
40130
+ const sinφ2 = Math.sin(φ1) * Math.cos(δ) + Math.cos(φ1) * Math.sin(δ) * Math.cos(θ);
40131
+ const φ2 = Math.asin(sinφ2);
40132
+ const y = Math.sin(θ) * Math.sin(δ) * Math.cos(φ1);
40133
+ const x = Math.cos(δ) - Math.sin(φ1) * sinφ2;
40134
+ const λ2 = λ1 + Math.atan2(y, x);
40135
+ return {
40136
+ altitude: point.altitude,
40137
+ latitude: Cesium.Math.toDegrees(φ2),
40138
+ longitude: Cesium.Math.toDegrees(λ2)
40139
+ };
40140
+ }
40141
+ /**
40142
+ * Handler for drawing a grid of lines on the map with a specified spacing.
40143
+ * Initially this grid is for model-space viewing which means we start at 0,0 lat/lon and have it expand from there.
40144
+ * However, it should be a general-use tool for drawing a grid anywhere.
40145
+ *
40146
+ * It reacts to camera changes and draws the grid within view with padding.
40147
+ * This helps avoid performance issues around drawing too many lines.
40148
+ */
40149
+ class MapGridHandler {
40150
+ constructor(viewer, spacingInMeters = 100, modelSpace = false) {
40151
+ this.gridCenterGizmoPieces = [];
40152
+ this.maxCacheSize = 1000;
40153
+ // === Thresholds for grid stability.
40154
+ // As a fraction of current grid extent.
40155
+ this.CAMERA_MOVE_THRESHOLD = 0.5;
40156
+ // 40% change in camera height triggers redraw.
40157
+ this.HEIGHT_CHANGE_THRESHOLD = 0.4;
40158
+ // 40% change in grid extent triggers redraw.
40159
+ this.GRID_EXTENT_CHANGE_THRESHOLD = 0.4;
40160
+ // 5km - distance that triggers origin reset.
40161
+ this.EXTREME_DISTANCE_THRESHOLD = 5000;
40162
+ // Grid limits to prevent drawing too many lines.
40163
+ this.MAX_LINES = 500;
40164
+ // 50% padding around visible area.
40165
+ this.VIEW_PADDING_FACTOR = 0.5;
40166
+ // Minimum pixels between grid lines to show them.
40167
+ this.MIN_GRID_PIXEL_SIZE = 20;
40168
+ // Base ratio - will be adjusted dynamically based on spacing size and camera height.
40169
+ this.BASE_MAX_VISIBILITY_RATIO = 30;
40170
+ // Height to spacing ratio.
40171
+ this.HEIGHT_TO_SPACING_RATIO = 100;
40172
+ // === Label settings
40173
+ // Label every nth intersection.
40174
+ this.LABEL_INTERVAL = 2;
40175
+ // Maximum number of labels to show.
40176
+ this.MAX_LABELS = 100;
40177
+ // Spacing levels to use for adaptive spacing.
40178
+ this.SPACING_LEVELS = [
40179
+ 1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 300, 400, 500, 1000, 2000, 2500, 5000, 10000, 20000, 25000, 50000, 100000
40180
+ ];
40181
+ // Minimum zoom distance to prevent precision issues.
40182
+ this.MINIMUM_ZOOM_DISTANCE = 40;
40183
+ // Base spacing to use for adaptive spacing.
40184
+ this.BASE_SPACING = 1;
40185
+ // Maximum number of attempts to find a suitable spacing.
40186
+ this.MAX_ADAPTIVE_ATTEMPTS = 5;
40187
+ this.viewer = viewer;
40188
+ this.spacingInMeters = spacingInMeters;
40189
+ this.isUpdating = false;
40190
+ this.modelSpace = modelSpace;
40191
+ this.lastCameraPosition = null;
40192
+ this.fixedGridOrigin = null;
40193
+ this.lastGridExtent = 0;
40194
+ this.offsetCache = new Map();
40195
+ this.gridPrimitives = new Cesium.PrimitiveCollection();
40196
+ this.viewer.scene.primitives.add(this.gridPrimitives);
40197
+ this.currentMarker = viewer[UPDATE_MARKER_KEY] || 1;
40198
+ const isMac = typeof navigator !== "undefined" && (navigator.platform.toUpperCase().indexOf("MAC") !== -1 ||
40199
+ navigator.userAgent.toUpperCase().indexOf("MAC") !== -1);
40200
+ if (modelSpace && isMac) {
40201
+ console.log("[MAC-FIX] Applying camera distance limits for Mac");
40202
+ viewer.scene.screenSpaceCameraController.minimumZoomDistance = this.MINIMUM_ZOOM_DISTANCE;
40203
+ }
40204
+ const manager = exports.ViewerUtils.GetManager({
40205
+ viewer: this.viewer,
40206
+ createIfMissing: false
40207
+ });
40208
+ if (manager) {
40209
+ this.cameraChangeRemoval = manager.Monitor.Updated().Subscribe(() => {
40210
+ this.onCameraChanged();
40211
+ });
40212
+ }
40213
+ // Initial grid draw.
40214
+ this.onCameraChanged();
40215
+ }
40216
+ Update(spacingInMeters, modelSpace) {
40217
+ this.currentMarker = this.viewer[UPDATE_MARKER_KEY] || 1;
40218
+ let needsUpdate = false;
40219
+ if (this.spacingInMeters !== spacingInMeters) {
40220
+ this.spacingInMeters = spacingInMeters;
40221
+ needsUpdate = true;
40222
+ }
40223
+ if (modelSpace !== undefined && this.modelSpace !== modelSpace) {
40224
+ this.modelSpace = modelSpace;
40225
+ needsUpdate = true;
40226
+ }
40227
+ if (needsUpdate) {
40228
+ this.clearGrid();
40229
+ // When spacing or model space changes, clear the cached origin to force recalculation
40230
+ // This ensures grid alignment at the new spacing.
40231
+ this.fixedGridOrigin = null;
40232
+ this.offsetCache.clear();
40233
+ this.onCameraChanged();
40234
+ }
40235
+ }
40236
+ Dispose() {
40237
+ this.clearGrid();
40238
+ this.clearGizmoPieces();
40239
+ if (this.cameraChangeRemoval) {
40240
+ this.cameraChangeRemoval();
40241
+ this.cameraChangeRemoval = null;
40242
+ }
40243
+ if (this.gridPrimitives) {
40244
+ this.viewer.scene.primitives.remove(this.gridPrimitives);
40245
+ this.gridPrimitives = null;
40246
+ }
40247
+ if (this.viewer) {
40248
+ this.viewer = null;
40249
+ }
40250
+ this.offsetCache.clear();
40251
+ this.fixedGridOrigin = null;
40252
+ }
40253
+ clearGrid() {
40254
+ if (this.gridPrimitives) {
40255
+ this.gridPrimitives.removeAll();
40256
+ this.gridLines = null;
40257
+ this.gridLabels = null;
40258
+ }
40259
+ }
40260
+ calculateAdaptiveSpacing(cameraHeight, baseSpacing) {
40261
+ // Define spacing levels that look good (powers of 2, 5, and 10)
40262
+ const spacingLevels = this.SPACING_LEVELS;
40263
+ // Ensure base spacing is at least 1m
40264
+ const adjustedBaseSpacing = Math.max(this.BASE_SPACING, baseSpacing);
40265
+ // For very close cameras, use much smaller spacing regardless of base
40266
+ if (cameraHeight < 10) {
40267
+ // Find the smallest reasonable spacing for this height
40268
+ // Rule: spacing should be roughly 1/20th to 1/50th of camera height, but at least 1m
40269
+ const targetSpacing = Math.max(this.BASE_SPACING, cameraHeight / 30);
40270
+ // Find the closest spacing level to our target
40271
+ let bestSpacing = this.BASE_SPACING; // Start with minimum
40272
+ let bestDifference = Math.abs(1 - targetSpacing);
40273
+ for (const spacing of spacingLevels) {
40274
+ if (spacing >= this.BASE_SPACING) { // Only consider valid spacings
40275
+ const difference = Math.abs(spacing - targetSpacing);
40276
+ if (difference < bestDifference) {
40277
+ bestSpacing = spacing;
40278
+ bestDifference = difference;
40279
+ }
40280
+ }
40281
+ }
40282
+ return bestSpacing;
40283
+ }
40284
+ // For medium distances (10m - 200m), scale more gradually
40285
+ if (cameraHeight < 200) {
40286
+ const targetSpacing = cameraHeight / 10; // More conservative scaling// Find closest spacing level, but prefer going up from base spacing
40287
+ let bestSpacing = adjustedBaseSpacing;
40288
+ let bestDifference = Math.abs(adjustedBaseSpacing - targetSpacing);
40289
+ for (const spacing of spacingLevels) {
40290
+ if (spacing >= Math.min(adjustedBaseSpacing, targetSpacing)) {
40291
+ const difference = Math.abs(spacing - targetSpacing);
40292
+ if (difference < bestDifference) {
40293
+ bestSpacing = spacing;
40294
+ bestDifference = difference;
40295
+ }
40296
+ }
40297
+ }
40298
+ return bestSpacing;
40299
+ }
40300
+ // For far distances (100m+), scale up aggressively
40301
+ const targetSpacing = cameraHeight / 20;
40302
+ // Find the spacing level that's closest to target but not smaller than base
40303
+ let bestSpacing = adjustedBaseSpacing;
40304
+ let bestDifference = Math.abs(adjustedBaseSpacing - targetSpacing);
40305
+ for (const spacing of spacingLevels) {
40306
+ if (spacing >= adjustedBaseSpacing) {
40307
+ const difference = Math.abs(spacing - targetSpacing);
40308
+ if (difference < bestDifference) {
40309
+ bestSpacing = spacing;
40310
+ bestDifference = difference;
40311
+ }
40312
+ }
40313
+ }
40314
+ return bestSpacing;
40315
+ }
40316
+ /*
40317
+ * The point model-space treats as 0,0,0: the UCS in UCS placement, otherwise 0,0 lat/lon at sea level.
40318
+ */
40319
+ modelSpaceOrigin() {
40320
+ const origin = exports.ViewUtils.IsModelSpaceUcs(this.viewer) ? exports.ViewUtils.GetModelSpaceOrigin(this.viewer) : null;
40321
+ return origin !== null && origin !== void 0 ? origin : { latitude: 0, longitude: 0, altitude: 0 };
40322
+ }
40323
+ /*
40324
+ * The origin as a scene position, where the axis gizmo sits.
40325
+ */
40326
+ modelSpaceOriginPos3d() {
40327
+ const origin = this.modelSpaceOrigin();
40328
+ return Cesium.Cartesian3.fromDegrees(origin.longitude, origin.latitude, origin.altitude);
40329
+ }
40330
+ drawGizmoPieces() {
40331
+ if (this.gridCenterGizmoPieces.length) {
40332
+ return;
40333
+ }
40334
+ // X.
40335
+ {
40336
+ const pos3d = this.modelSpaceOriginPos3d();
40337
+ const scale = new Cesium.CallbackProperty(() => Cesium.Cartesian3.distance(this.viewer.camera.position, pos3d) * 0.0002, false);
40338
+ const orientation = Cesium.Transforms.headingPitchRollQuaternion(pos3d, new Cesium.HeadingPitchRoll(Cesium.Math.toRadians(90), 0, 0));
40339
+ const entity = this.viewer.entities.add(new Cesium.Entity({
40340
+ model: {
40341
+ uri: exports.GltfGizmo.GetUri(exports.GltfGizmo.EPiece.X),
40342
+ heightReference: Cesium.HeightReference.NONE,
40343
+ shadows: Cesium.ShadowMode.DISABLED,
40344
+ distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, Number.MAX_VALUE),
40345
+ scale
40346
+ },
40347
+ position: pos3d,
40348
+ orientation: new Cesium.ConstantProperty(orientation)
40349
+ }));
40350
+ this.gridCenterGizmoPieces.push(entity);
40351
+ }
40352
+ // Y.
40353
+ {
40354
+ const pos3d = this.modelSpaceOriginPos3d();
40355
+ const scale = new Cesium.CallbackProperty(() => Cesium.Cartesian3.distance(this.viewer.camera.position, pos3d) * 0.0002, false);
40356
+ const orientation = Cesium.Transforms.headingPitchRollQuaternion(pos3d, new Cesium.HeadingPitchRoll(Cesium.Math.toRadians(90), 0, 0));
40357
+ const entity = this.viewer.entities.add(new Cesium.Entity({
40358
+ model: {
40359
+ uri: exports.GltfGizmo.GetUri(exports.GltfGizmo.EPiece.Y),
40360
+ heightReference: Cesium.HeightReference.NONE,
40361
+ shadows: Cesium.ShadowMode.DISABLED,
40362
+ distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, Number.MAX_VALUE),
40363
+ scale
40364
+ },
40365
+ position: pos3d,
40366
+ orientation: new Cesium.ConstantProperty(orientation)
40367
+ }));
40368
+ this.gridCenterGizmoPieces.push(entity);
40369
+ }
40370
+ // Z.
40371
+ {
40372
+ const pos3d = this.modelSpaceOriginPos3d();
40373
+ const scale = new Cesium.CallbackProperty(() => Cesium.Cartesian3.distance(this.viewer.camera.position, pos3d) * 0.0002, false);
40374
+ const orientation = Cesium.Transforms.headingPitchRollQuaternion(pos3d, new Cesium.HeadingPitchRoll(Cesium.Math.toRadians(90), 0, 0));
40375
+ const entity = this.viewer.entities.add(new Cesium.Entity({
40376
+ model: {
40377
+ uri: exports.GltfGizmo.GetUri(exports.GltfGizmo.EPiece.Z),
40378
+ heightReference: Cesium.HeightReference.NONE,
40379
+ shadows: Cesium.ShadowMode.DISABLED,
40380
+ distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, Number.MAX_VALUE),
40381
+ scale
40382
+ },
40383
+ position: pos3d,
40384
+ orientation: new Cesium.ConstantProperty(orientation)
40385
+ }));
40386
+ this.gridCenterGizmoPieces.push(entity);
40387
+ }
40388
+ }
40389
+ clearGizmoPieces() {
40390
+ for (const piece of this.gridCenterGizmoPieces) {
40391
+ if (piece && this.viewer.entities.contains(piece)) {
40392
+ this.viewer.entities.remove(piece);
40393
+ }
40394
+ }
40395
+ this.gridCenterGizmoPieces = [];
40396
+ }
40397
+ onCameraChanged() {
40398
+ if (this.isUpdating) {
40399
+ return;
40400
+ }
40401
+ else if (this.currentMarker !== this.viewer[UPDATE_MARKER_KEY]) {
40402
+ return;
40403
+ }
40404
+ this.isUpdating = true;
40405
+ // Small delay to avoid excessive updates during camera movement.
40406
+ setTimeout(() => {
40407
+ if (this.currentMarker !== this.viewer[UPDATE_MARKER_KEY]) {
40408
+ this.isUpdating = false;
40409
+ return;
40410
+ }
40411
+ try {
40412
+ // The grid goes first, since it notices when the origin has moved and the gizmo has to follow.
40413
+ this.updateGrid();
40414
+ if (this.modelSpace) {
40415
+ this.drawGizmoPieces();
40416
+ }
40417
+ else {
40418
+ this.clearGizmoPieces();
40419
+ }
40420
+ }
40421
+ catch (e) {
40422
+ console.error(e);
40423
+ }
40424
+ finally {
40425
+ this.isUpdating = false;
40426
+ }
40427
+ }, 100);
40428
+ }
40429
+ cachedOffsetPoint(point, distance, heading) {
40430
+ const key = `${point.latitude.toFixed(8)},${point.longitude.toFixed(8)},${distance.toFixed(1)},${heading}`;
40431
+ if (this.offsetCache.has(key)) {
40432
+ return this.offsetCache.get(key);
40433
+ }
40434
+ const result = offsetPoint$1(point, distance, heading);
40435
+ this.offsetCache.set(key, result);
40436
+ if (this.offsetCache.size > this.maxCacheSize) {
40437
+ const keys = Array.from(this.offsetCache.keys());
40438
+ for (let i = 0; i < 100; i++) {
40439
+ this.offsetCache.delete(keys[i]);
40440
+ }
40441
+ }
40442
+ return result;
40443
+ }
40444
+ /**
40445
+ * Calculates a dynamic visibility ratio based on spacing size.
40446
+ * For small grid spacings, we can show the grid from farther away.
40447
+ * For large grid spacings, we need to hide it sooner to prevent performance issues.
40448
+ */
40449
+ getMaxVisibilityRatio() {
40450
+ if (this.spacingInMeters <= 10) {
40451
+ return this.BASE_MAX_VISIBILITY_RATIO;
40452
+ }
40453
+ if (this.spacingInMeters <= 100) {
40454
+ const factor = 1 - ((this.spacingInMeters - 10) / 90);
40455
+ return this.BASE_MAX_VISIBILITY_RATIO * (0.5 + (factor * 0.5));
40456
+ }
40457
+ const largeGridFactor = Math.min(1, 100 / this.spacingInMeters);
40458
+ return this.BASE_MAX_VISIBILITY_RATIO * 0.5 * largeGridFactor;
40459
+ }
40460
+ updateGrid() {
40461
+ const cameraPosition = this.viewer.camera.position;
40462
+ const cameraCartographic = Cesium.Cartographic.fromCartesian(cameraPosition);
40463
+ const cameraHeight = cameraCartographic.height;
40464
+ let adaptiveSpacing = this.calculateAdaptiveSpacing(cameraHeight, this.spacingInMeters);
40465
+ // Calculate visible area.
40466
+ const visibleArea = this.calculateVisibleArea();
40467
+ if (!visibleArea) {
40468
+ return;
40469
+ }
40470
+ let densityCheckPassed = false;
40471
+ let attempts = 0;
40472
+ while (!densityCheckPassed && attempts < this.MAX_ADAPTIVE_ATTEMPTS) {
40473
+ if (this.isGridDensityAcceptable(visibleArea, cameraHeight, adaptiveSpacing)) {
40474
+ densityCheckPassed = true;
40475
+ }
40476
+ else {
40477
+ adaptiveSpacing *= 2;
40478
+ attempts++;
40479
+ }
40480
+ }
40481
+ // The origin follows the UCS of what is shown, so the grid moves with it when that changes.
40482
+ if (this.modelSpace && this.fixedGridOrigin && !BModels.Carto.IsEqual(this.fixedGridOrigin, this.modelSpaceOrigin())) {
40483
+ this.fixedGridOrigin = null;
40484
+ this.lastCameraPosition = null;
40485
+ this.clearGizmoPieces();
40486
+ }
40487
+ // Calculate grid origin if we don't have one yet.
40488
+ if (!this.fixedGridOrigin) {
40489
+ if (this.modelSpace) {
40490
+ this.fixedGridOrigin = this.modelSpaceOrigin();
40491
+ }
40492
+ else {
40493
+ const centerCartographic = Cesium.Cartographic.fromCartesian(visibleArea.centerCartesian);
40494
+ const centerPoint = {
40495
+ latitude: Cesium.Math.toDegrees(centerCartographic.latitude),
40496
+ longitude: Cesium.Math.toDegrees(centerCartographic.longitude),
40497
+ altitude: 0
40498
+ };
40499
+ // Simple attempt at snapping to the grid, should do something smarter.
40500
+ this.fixedGridOrigin = {
40501
+ latitude: Math.round(centerPoint.latitude * 100000) / 100000,
40502
+ longitude: Math.round(centerPoint.longitude * 100000) / 100000,
40503
+ altitude: 0
40504
+ };
40505
+ }
40506
+ }
40507
+ // Calculate appropriate grid extent based on camera height.
40508
+ // For larger grid spacings, we use a tighter extent to prevent performance issues.
40509
+ const scaledExtentFactor = Math.max(0.5, Math.min(3, 3 - Math.log10(Math.max(1, adaptiveSpacing)) * 0.5));
40510
+ const visibilityRadius = Math.min(cameraHeight * scaledExtentFactor, Infinity);
40511
+ // Check if we should redraw the grid.
40512
+ const shouldRedraw = this.shouldRedrawGrid(cameraPosition, cameraHeight, visibilityRadius, adaptiveSpacing);
40513
+ if (!shouldRedraw) {
40514
+ return;
40515
+ }
40516
+ // Remember current settings for next comparison.
40517
+ this.lastCameraHeight = cameraHeight;
40518
+ this.lastCameraPosition = cameraPosition.clone();
40519
+ this.lastGridExtent = visibilityRadius;
40520
+ // Clear existing grid and create a new one.
40521
+ this.clearGrid();
40522
+ this.gridLines = new Cesium.PolylineCollection();
40523
+ this.gridLabels = new Cesium.LabelCollection({
40524
+ scene: this.viewer.scene
40525
+ });
40526
+ this.gridPrimitives.add(this.gridLines);
40527
+ this.gridPrimitives.add(this.gridLabels);
40528
+ // Generate grid lines and labels using our fixed origin.
40529
+ this.generateGridLines(this.fixedGridOrigin, visibilityRadius, adaptiveSpacing);
40530
+ }
40531
+ shouldRedrawGrid(cameraPosition, cameraHeight, gridExtent, currentSpacing) {
40532
+ // check if the spacing has changed
40533
+ if (this.lastUsedSpacing && this.lastUsedSpacing !== currentSpacing) {
40534
+ return true;
40535
+ }
40536
+ // Always draw if we don't have a grid yet.
40537
+ if (!this.gridLines || !this.lastCameraPosition || !this.fixedGridOrigin) {
40538
+ return true;
40539
+ }
40540
+ // Check for significant height change.
40541
+ const heightRatio = cameraHeight / this.lastCameraHeight;
40542
+ if (heightRatio > (1 + this.HEIGHT_CHANGE_THRESHOLD) ||
40543
+ heightRatio < (1 - this.HEIGHT_CHANGE_THRESHOLD)) {
40544
+ return true;
40545
+ }
40546
+ // Check for significant position change relative to current grid extent.
40547
+ const distance = Cesium.Cartesian3.distance(cameraPosition, this.lastCameraPosition);
40548
+ const moveThreshold = gridExtent * this.CAMERA_MOVE_THRESHOLD;
40549
+ if (distance > moveThreshold) {
40550
+ return true;
40551
+ }
40552
+ // Check for significant grid extent change.
40553
+ const extentRatio = gridExtent / this.lastGridExtent;
40554
+ if (extentRatio > (1 + this.GRID_EXTENT_CHANGE_THRESHOLD) ||
40555
+ extentRatio < (1 - this.GRID_EXTENT_CHANGE_THRESHOLD)) {
40556
+ return true;
40557
+ }
40558
+ // No significant changes, keep current grid.
40559
+ return false;
40560
+ }
40561
+ isGridDensityAcceptable(visibleArea, cameraHeight, spacingToCheck = this.spacingInMeters) {
40562
+ const lonRange = visibleArea.eastLon - visibleArea.westLon;
40563
+ const latRange = visibleArea.northLat - visibleArea.southLat;
40564
+ // Convert to approximate meters at center
40565
+ const centerCartographic = Cesium.Cartographic.fromCartesian(visibleArea.centerCartesian);
40566
+ const metersPerDegreeLon = 111319.9 * Math.cos(centerCartographic.latitude);
40567
+ const canvas = this.viewer.scene.canvas;
40568
+ const spacingInDegrees = spacingToCheck / metersPerDegreeLon;
40569
+ const canvasWidth = canvas.clientWidth;
40570
+ const pixelsPerDegree = canvasWidth / lonRange;
40571
+ const pixelsBetweenGridLines = pixelsPerDegree * spacingInDegrees;
40572
+ const minPixelSize = 1;
40573
+ return pixelsBetweenGridLines >= minPixelSize;
40574
+ }
40575
+ calculateVisibleArea() {
40576
+ const scene = this.viewer.scene;
40577
+ const camera = scene.camera;
40578
+ const globe = scene.globe;
40579
+ // Get the corners of the view frustum
40580
+ const canvas = scene.canvas;
40581
+ const corners = [
40582
+ new Cesium.Cartesian2(0, 0),
40583
+ new Cesium.Cartesian2(canvas.clientWidth, 0),
40584
+ new Cesium.Cartesian2(0, canvas.clientHeight),
40585
+ new Cesium.Cartesian2(canvas.clientWidth, canvas.clientHeight),
40586
+ new Cesium.Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2) // Center
40587
+ ];
40588
+ // Project corners to the globe
40589
+ const cartographicCorners = [];
40590
+ for (const corner of corners) {
40591
+ const ray = camera.getPickRay(corner);
40592
+ if (!ray) {
40593
+ continue;
40594
+ }
40595
+ const position = globe.pick(ray, scene);
40596
+ if (!position) {
40597
+ continue;
40598
+ }
40599
+ cartographicCorners.push(Cesium.Cartographic.fromCartesian(position));
40600
+ }
40601
+ if (cartographicCorners.length === 0) {
40602
+ return null;
40603
+ }
40604
+ // Find the bounding rectangle
40605
+ let westLon = Number.POSITIVE_INFINITY;
40606
+ let eastLon = Number.NEGATIVE_INFINITY;
40607
+ let southLat = Number.POSITIVE_INFINITY;
40608
+ let northLat = Number.NEGATIVE_INFINITY;
40609
+ for (const corner of cartographicCorners) {
40610
+ westLon = Math.min(westLon, corner.longitude);
40611
+ eastLon = Math.max(eastLon, corner.longitude);
40612
+ southLat = Math.min(southLat, corner.latitude);
40613
+ northLat = Math.max(northLat, corner.latitude);
40614
+ }
40615
+ // Add some padding around the visible area
40616
+ const lonPadding = (eastLon - westLon) * this.VIEW_PADDING_FACTOR;
40617
+ const latPadding = (northLat - southLat) * this.VIEW_PADDING_FACTOR;
40618
+ westLon -= lonPadding;
40619
+ eastLon += lonPadding;
40620
+ southLat -= latPadding;
40621
+ northLat += latPadding;
40622
+ // Get center position for reference
40623
+ const centerCartographic = new Cesium.Cartographic((westLon + eastLon) / 2, (southLat + northLat) / 2);
40624
+ const centerCartesian = Cesium.Cartesian3.fromRadians(centerCartographic.longitude, centerCartographic.latitude, 0);
40625
+ return {
40626
+ westLon: Cesium.Math.toDegrees(westLon),
40627
+ eastLon: Cesium.Math.toDegrees(eastLon),
40628
+ southLat: Cesium.Math.toDegrees(southLat),
40629
+ northLat: Cesium.Math.toDegrees(northLat),
40630
+ centerCartesian: centerCartesian
40631
+ };
40632
+ }
40633
+ generateGridLines(originPoint, visibilityRadius, actualSpacing = this.spacingInMeters) {
40634
+ // Number of grid cells in each direction (add 1 for edge lines).
40635
+ const gridCellCount = Math.ceil(visibilityRadius / actualSpacing) + 1;
40636
+ const maxCells = Math.min(gridCellCount, Math.floor(this.MAX_LINES / 2));
40637
+ // Calculate interval to skip lines for density control.
40638
+ const spacingFactor = actualSpacing / 100;
40639
+ const heightFactor = this.lastCameraHeight / 500;
40640
+ let lineSkipInterval = Math.floor(spacingFactor * heightFactor);
40641
+ lineSkipInterval = Math.min(Math.max(lineSkipInterval, 1), 10); // Clamp between 1 and 10
40642
+ const intersections = [];
40643
+ // Generate north-south grid lines (longitude lines)
40644
+ for (let i = -maxCells; i <= maxCells; i++) {
40645
+ if (i % lineSkipInterval !== 0) {
40646
+ continue;
40647
+ }
40648
+ const positions = [];
40649
+ // Create a vertical line at this longitude offset.
40650
+ // East heading for longitude.
40651
+ const startPoint = this.cachedOffsetPoint(originPoint, i * actualSpacing, 90);
40652
+ // Draw the line from south to north
40653
+ for (let j = -maxCells; j <= maxCells; j++) {
40654
+ if (j % lineSkipInterval !== 0) {
40655
+ continue;
40656
+ }
40657
+ // North heading for latitude.
40658
+ const linePoint = this.cachedOffsetPoint(startPoint, j * actualSpacing, 0);
40659
+ const position = Cesium.Cartesian3.fromDegrees(linePoint.longitude, linePoint.latitude, linePoint.altitude);
40660
+ positions.push(position);
40661
+ intersections.push({
40662
+ position: position,
40663
+ i: i,
40664
+ j: j
40665
+ });
40666
+ }
40667
+ if (positions.length >= 2) {
40668
+ // Check if this is the primary Y axis (i=0) in model space
40669
+ const isPrimaryYAxis = this.modelSpace && i === 0;
40670
+ this.addGridLine(positions, isPrimaryYAxis ? "y" : null);
40671
+ }
40672
+ }
40673
+ // Generate east-west grid lines (latitude lines).
40674
+ for (let j = -maxCells; j <= maxCells; j++) {
40675
+ if (j % lineSkipInterval !== 0) {
40676
+ continue;
40677
+ }
40678
+ const positions = [];
40679
+ // Create a horizontal line at this latitude offset
40680
+ // North heading for latitude.
40681
+ const startPoint = this.cachedOffsetPoint(originPoint, j * actualSpacing, 0);
40682
+ // Draw the line from west to east.
40683
+ for (let i = -maxCells; i <= maxCells; i++) {
40684
+ if (i % lineSkipInterval !== 0) {
40685
+ continue;
40686
+ }
40687
+ // East heading for longitude.
40688
+ const linePoint = this.cachedOffsetPoint(startPoint, i * actualSpacing, 90);
40689
+ const position = Cesium.Cartesian3.fromDegrees(linePoint.longitude, linePoint.latitude, linePoint.altitude);
40690
+ positions.push(position);
40691
+ }
40692
+ if (positions.length >= 2) {
40693
+ // Check if this is the primary X axis (j=0) in model space
40694
+ const isPrimaryXAxis = this.modelSpace && j === 0;
40695
+ this.addGridLine(positions, isPrimaryXAxis ? "x" : null);
40696
+ }
40697
+ }
40698
+ this.addGridLabels(intersections, actualSpacing);
40699
+ }
40700
+ addGridLabels(intersections, actualSpacing) {
40701
+ if (!this.gridLabels) {
40702
+ return;
40703
+ }
40704
+ // Determine label interval based on number of intersections.
40705
+ // Use higher interval for more intersections to prevent crowding.
40706
+ const totalIntersections = intersections.length;
40707
+ let labelInterval = this.LABEL_INTERVAL;
40708
+ if (totalIntersections > 1000) {
40709
+ labelInterval = 5;
40710
+ }
40711
+ else if (totalIntersections > 500) {
40712
+ labelInterval = 3;
40713
+ }
40714
+ let labelCount = 0;
40715
+ // Filter intersections to only those at the labelInterval.
40716
+ const labelPoints = intersections.filter(point => {
40717
+ // Always label the origin (0,0).
40718
+ if (point.i === 0 && point.j === 0) {
40719
+ return true;
40720
+ }
40721
+ return (point.i % labelInterval === 0 && point.j % labelInterval === 0);
40722
+ });
40723
+ // Sort points by distance from origin.
40724
+ labelPoints.sort((a, b) => {
40725
+ const distA = Math.abs(a.i) + Math.abs(a.j);
40726
+ const distB = Math.abs(b.i) + Math.abs(b.j);
40727
+ return distA - distB;
40728
+ });
40729
+ // Add labels, prioritizing those closest to the origin.
40730
+ for (const point of labelPoints) {
40731
+ if (labelCount >= this.MAX_LABELS) {
40732
+ // Limit reached.
40733
+ break;
40734
+ }
40735
+ let labelText;
40736
+ // Model space coordinates (meters from origin).
40737
+ if (this.modelSpace) {
40738
+ const xMeters = point.i * actualSpacing;
40739
+ const yMeters = point.j * actualSpacing;
40740
+ if (point.i === 0 && point.j === 0) {
40741
+ labelText = "0,0,0";
40742
+ }
40743
+ else {
40744
+ labelText = `${xMeters},${yMeters}`;
40745
+ }
40746
+ }
40747
+ // Geographic coordinates (degrees).
40748
+ else {
40749
+ const cartographic = Cesium.Cartographic.fromCartesian(point.position);
40750
+ const latitude = Cesium.Math.toDegrees(cartographic.latitude);
40751
+ const longitude = Cesium.Math.toDegrees(cartographic.longitude);
40752
+ const latFormatted = latitude.toFixed(5);
40753
+ const lonFormatted = longitude.toFixed(5);
40754
+ labelText = `${latFormatted}, ${lonFormatted}`;
40755
+ }
40756
+ this.gridLabels.add({
40757
+ position: point.position,
40758
+ text: labelText,
40759
+ font: this.modelSpace && point.i === 0 && point.j === 0 ? "20px sans-serif" : "14px sans-serif",
40760
+ fillColor: this.modelSpace && point.i === 0 && point.j === 0 ? Cesium.Color.WHITE : Cesium.Color.WHITE.withAlpha(0.5),
40761
+ style: Cesium.LabelStyle.FILL,
40762
+ horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
40763
+ verticalOrigin: Cesium.VerticalOrigin.TOP,
40764
+ pixelOffset: new Cesium.Cartesian2(0, 10),
40765
+ // Fade out with distance.
40766
+ translucencyByDistance: new Cesium.NearFarScalar(1000, 1.0, 15000, 0.4)
40767
+ });
40768
+ labelCount++;
40769
+ }
40770
+ }
40771
+ addGridLine(positions, primaryAxis = null) {
40772
+ if (!this.gridLines || positions.length < 2) {
40773
+ return;
40774
+ }
40775
+ let lineColor;
40776
+ let lineWidth = 1.0;
40777
+ let lineAlpha = 0.2;
40778
+ // Red for X axis.
40779
+ if (primaryAxis === "x") {
40780
+ lineColor = Cesium.Color.RED;
40781
+ lineAlpha = 0.5;
40782
+ lineWidth = 1.5;
40783
+ }
40784
+ // Green for Y axis.
40785
+ else if (primaryAxis === "y") {
40786
+ lineColor = Cesium.Color.GREEN;
40787
+ lineAlpha = 0.5;
40788
+ lineWidth = 1.5;
40789
+ }
40790
+ // Default gray for regular grid lines.
40791
+ else {
40792
+ lineColor = Cesium.Color.LIGHTGRAY;
40793
+ }
40794
+ this.gridLines.add({
40795
+ positions: positions,
40796
+ width: lineWidth,
40797
+ material: Cesium.Material.fromType("Color", {
40798
+ color: lineColor.withAlpha(lineAlpha)
40799
+ })
40800
+ });
40801
+ }
40802
+ }
40803
+ // Grid square size used when settings name none, in metres.
40804
+ const DEFAULT_SPACING = 50;
40805
+ (function (CesiumMapGrid) {
40806
+ function GetCurrentSpacing(viewer) {
40807
+ if (!viewer || viewer.isDestroyed()) {
40808
+ return 0;
40809
+ }
40810
+ const handler = viewer[CLASS_INSTANCE_KEY];
40811
+ return handler ? handler.getCurrentSpacing() : 0;
40812
+ }
40813
+ CesiumMapGrid.GetCurrentSpacing = GetCurrentSpacing;
40814
+ function Remove(viewer) {
40815
+ if (!viewer || viewer.isDestroyed()) {
40816
+ return;
40817
+ }
40818
+ else if (!viewer[LAST_SETTINGS_KEY]) {
40819
+ return;
40820
+ }
40821
+ if (!viewer[UPDATE_MARKER_KEY]) {
40822
+ viewer[UPDATE_MARKER_KEY] = 1;
40823
+ }
40824
+ else {
40825
+ viewer[UPDATE_MARKER_KEY]++;
40826
+ }
40827
+ // Dispose of the handler.
40828
+ if (viewer[CLASS_INSTANCE_KEY]) {
40829
+ viewer[CLASS_INSTANCE_KEY].Dispose();
40830
+ viewer[CLASS_INSTANCE_KEY] = null;
40831
+ }
40832
+ viewer[LAST_SETTINGS_KEY] = {
40833
+ ...viewer[LAST_SETTINGS_KEY],
40834
+ enabled: false
40835
+ };
40836
+ }
40837
+ CesiumMapGrid.Remove = Remove;
40838
+ function Draw(viewer, spacingInMeters, modelSpace = true) {
40839
+ if (!viewer || viewer.isDestroyed()) {
40840
+ return;
40841
+ }
40842
+ let settings = viewer[LAST_SETTINGS_KEY];
40843
+ if (settings) {
40844
+ // See if the settings are the same as the last time.
40845
+ if (settings.spacing === spacingInMeters &&
40846
+ settings.modelSpace === modelSpace &&
40847
+ settings.enabled) {
40848
+ return;
40849
+ }
40850
+ }
40851
+ if (!viewer[UPDATE_MARKER_KEY]) {
40852
+ viewer[UPDATE_MARKER_KEY] = 1;
40853
+ }
40854
+ else {
40855
+ viewer[UPDATE_MARKER_KEY]++;
40856
+ }
40857
+ settings = {
40858
+ ...settings,
40859
+ enabled: true,
40860
+ spacing: spacingInMeters,
40861
+ modelSpace: modelSpace
40862
+ };
40863
+ viewer[LAST_SETTINGS_KEY] = settings;
40864
+ // Create or update the handler.
40865
+ if (viewer[CLASS_INSTANCE_KEY]) {
40866
+ viewer[CLASS_INSTANCE_KEY].Update(spacingInMeters, modelSpace);
40867
+ }
40868
+ else {
40869
+ viewer[CLASS_INSTANCE_KEY] = new MapGridHandler(viewer, spacingInMeters, modelSpace);
40870
+ }
40871
+ }
40872
+ CesiumMapGrid.Draw = Draw;
40873
+ /**
40874
+ * Draws a bookmark's grid, or removes the grid when it has none; only model-space draws one.
40875
+ */
40876
+ function Apply(viewer, grid, modelSpace) {
40877
+ if ((grid === null || grid === void 0 ? void 0 : grid.enabled) && modelSpace) {
40878
+ Draw(viewer, Number(grid.spacing) || DEFAULT_SPACING, modelSpace);
40879
+ }
40880
+ else {
40881
+ Remove(viewer);
40882
+ }
40883
+ }
40884
+ CesiumMapGrid.Apply = Apply;
40885
+ function GetIsEnabled(viewer) {
40886
+ if (!viewer || viewer.isDestroyed()) {
40887
+ return false;
40888
+ }
40889
+ const settings = viewer[LAST_SETTINGS_KEY];
40890
+ if (settings) {
40891
+ return Boolean(settings.enabled);
40892
+ }
40893
+ return false;
40894
+ }
40895
+ CesiumMapGrid.GetIsEnabled = GetIsEnabled;
40896
+ /**
40897
+ * Returns the last applied grid settings.
40898
+ */
40899
+ function GetSettings(viewer) {
40900
+ if (!viewer || viewer.isDestroyed()) {
40901
+ return null;
40902
+ }
40903
+ return viewer[LAST_SETTINGS_KEY];
40904
+ }
40905
+ CesiumMapGrid.GetSettings = GetSettings;
40906
+ })(exports.CesiumMapGrid || (exports.CesiumMapGrid = {}));
40907
+
38715
40908
  // TODO: Move these to the data model.
38716
40909
  const MIN_SHADOW_SIZE = 2048;
38717
40910
  const MAX_SHADOW_SIZE = 32768;
@@ -38903,7 +41096,7 @@
38903
41096
  * @param view
38904
41097
  */
38905
41098
  async function renderNavigator(iteration, params, bookmark, view, getters) {
38906
- 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, _6, _7, _8;
41099
+ 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, _6, _7, _8, _9;
38907
41100
  const viewer = params.manager.Viewer;
38908
41101
  const scene = viewer.scene;
38909
41102
  // Ion requests go through the server-side proxy (set up during viewer init or here as fallback).
@@ -39128,15 +41321,7 @@
39128
41321
  scene.globe.translucency.frontFaceAlphaByDistance.farValue = 1;
39129
41322
  }
39130
41323
  scene.globe.translucency.frontFaceAlphaByDistance.nearValue = EnsureNumber(globeAlpha, 1);
39131
- let shadows = bSettings === null || bSettings === void 0 ? void 0 : bSettings.shadows;
39132
- if (shadows == null) {
39133
- shadows = (_u = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _u === void 0 ? void 0 : _u.shadows;
39134
- }
39135
- if (shadows == null) {
39136
- shadows = {
39137
- enabled: false
39138
- };
39139
- }
41324
+ const shadows = exports.ViewRenderEngine.ResolveShadows(bSettings, defaults === null || defaults === void 0 ? void 0 : defaults.settings, contentType);
39140
41325
  viewer.shadows = Boolean(shadows.enabled);
39141
41326
  viewer.shadowMap.enabled = Boolean(shadows.enabled);
39142
41327
  let size = EnsureNumber(shadows.pixelSize);
@@ -39149,9 +41334,11 @@
39149
41334
  viewer.shadowMap.size = size;
39150
41335
  viewer.shadowMap.softShadows = Boolean(shadows.soften);
39151
41336
  viewer.shadowMap.darkness = EnsureNumber(shadows.darkness, 0.3);
41337
+ // Drawn after the model-space origin is resolved above, since the grid starts from it.
41338
+ exports.CesiumMapGrid.Apply(viewer, (_u = bSettings.grid) !== null && _u !== void 0 ? _u : (_v = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _v === void 0 ? void 0 : _v.grid, contentType == BModels.ProjectViewBookmark.EContentType.WEB_3D_MODEL_SPACE);
39152
41339
  let ambientOcclusion = bSettings === null || bSettings === void 0 ? void 0 : bSettings.ambientOcclusion;
39153
41340
  if (ambientOcclusion == null) {
39154
- ambientOcclusion = (_v = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _v === void 0 ? void 0 : _v.ambientOcclusion;
41341
+ ambientOcclusion = (_w = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _w === void 0 ? void 0 : _w.ambientOcclusion;
39155
41342
  }
39156
41343
  if (ambientOcclusion == null) {
39157
41344
  ambientOcclusion = {
@@ -39174,7 +41361,7 @@
39174
41361
  }
39175
41362
  let lighting = bSettings === null || bSettings === void 0 ? void 0 : bSettings.lighting;
39176
41363
  if (lighting == null) {
39177
- lighting = (_w = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _w === void 0 ? void 0 : _w.lighting;
41364
+ lighting = (_x = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _x === void 0 ? void 0 : _x.lighting;
39178
41365
  }
39179
41366
  if (lighting == null) {
39180
41367
  lighting = {
@@ -39192,7 +41379,7 @@
39192
41379
  exports.ViewUtils.ApplyDefaultBrightness(viewer, EnsureNumber(lighting.brightnessShift, DEFAULT_BRIGHTNESS_SHIFT));
39193
41380
  let quality = bSettings === null || bSettings === void 0 ? void 0 : bSettings.quality;
39194
41381
  if (quality == null) {
39195
- quality = (_x = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _x === void 0 ? void 0 : _x.quality;
41382
+ quality = (_y = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _y === void 0 ? void 0 : _y.quality;
39196
41383
  }
39197
41384
  if (quality == null) {
39198
41385
  quality = {
@@ -39203,11 +41390,11 @@
39203
41390
  fxaa.enabled = Boolean(quality.fxaa);
39204
41391
  let dateTime = bSettings === null || bSettings === void 0 ? void 0 : bSettings.dateTime;
39205
41392
  if (dateTime == null) {
39206
- dateTime = (_y = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _y === void 0 ? void 0 : _y.dateTime;
41393
+ dateTime = (_z = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _z === void 0 ? void 0 : _z.dateTime;
39207
41394
  }
39208
41395
  let timeline = bSettings === null || bSettings === void 0 ? void 0 : bSettings.timeline;
39209
41396
  if (timeline == null) {
39210
- timeline = (_z = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _z === void 0 ? void 0 : _z.timeline;
41397
+ timeline = (_0 = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _0 === void 0 ? void 0 : _0.timeline;
39211
41398
  }
39212
41399
  if (timeline == null) {
39213
41400
  timeline = {
@@ -39231,7 +41418,7 @@
39231
41418
  // Newer version that has states per Entity ID + Menu Item ID.
39232
41419
  let states = bSettings === null || bSettings === void 0 ? void 0 : bSettings.states;
39233
41420
  if (states == null) {
39234
- states = (_0 = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _0 === void 0 ? void 0 : _0.states;
41421
+ states = (_1 = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _1 === void 0 ? void 0 : _1.states;
39235
41422
  }
39236
41423
  if (states != null) {
39237
41424
  params.manager.VisualsRegister.OverrideStates(states);
@@ -39240,7 +41427,7 @@
39240
41427
  else {
39241
41428
  let selectedIds = bSettings === null || bSettings === void 0 ? void 0 : bSettings.selectedEntityIds;
39242
41429
  if (selectedIds == null) {
39243
- selectedIds = (_1 = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _1 === void 0 ? void 0 : _1.selectedEntityIds;
41430
+ selectedIds = (_2 = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _2 === void 0 ? void 0 : _2.selectedEntityIds;
39244
41431
  }
39245
41432
  if (selectedIds != null) {
39246
41433
  params.manager.VisualsRegister.ClearSelected();
@@ -39254,7 +41441,7 @@
39254
41441
  params.manager.VisualsRegister.ClearHidden();
39255
41442
  let hiddenIds = bSettings === null || bSettings === void 0 ? void 0 : bSettings.hiddenEntityIds;
39256
41443
  if (hiddenIds == null) {
39257
- hiddenIds = (_2 = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _2 === void 0 ? void 0 : _2.hiddenEntityIds;
41444
+ hiddenIds = (_3 = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _3 === void 0 ? void 0 : _3.hiddenEntityIds;
39258
41445
  }
39259
41446
  if (hiddenIds != null) {
39260
41447
  params.manager.VisualsRegister.SetHidden({
@@ -39266,7 +41453,7 @@
39266
41453
  params.manager.VisualsRegister.ClearIsolated();
39267
41454
  let isolatedIds = bSettings === null || bSettings === void 0 ? void 0 : bSettings.isolatedEntityIds;
39268
41455
  if (isolatedIds == null) {
39269
- isolatedIds = (_3 = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _3 === void 0 ? void 0 : _3.isolatedEntityIds;
41456
+ isolatedIds = (_4 = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _4 === void 0 ? void 0 : _4.isolatedEntityIds;
39270
41457
  }
39271
41458
  if (isolatedIds != null) {
39272
41459
  params.manager.VisualsRegister.SetIsolated({
@@ -39277,7 +41464,7 @@
39277
41464
  }
39278
41465
  let labelledIds = bSettings === null || bSettings === void 0 ? void 0 : bSettings.labelledEntityIds;
39279
41466
  if (labelledIds == null) {
39280
- labelledIds = (_4 = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _4 === void 0 ? void 0 : _4.labelledEntityIds;
41467
+ labelledIds = (_5 = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _5 === void 0 ? void 0 : _5.labelledEntityIds;
39281
41468
  }
39282
41469
  if (!(labelledIds === null || labelledIds === void 0 ? void 0 : labelledIds.length)) {
39283
41470
  params.manager.VisualsRegister.ClearLabelled({
@@ -39301,7 +41488,7 @@
39301
41488
  params.manager.VisualsRegister.ClearOpacity();
39302
41489
  let entityOpacityMap = bSettings === null || bSettings === void 0 ? void 0 : bSettings.entityOpacityMap;
39303
41490
  if (entityOpacityMap == null) {
39304
- entityOpacityMap = (_5 = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _5 === void 0 ? void 0 : _5.entityOpacityMap;
41491
+ entityOpacityMap = (_6 = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _6 === void 0 ? void 0 : _6.entityOpacityMap;
39305
41492
  }
39306
41493
  if (entityOpacityMap != null) {
39307
41494
  for (const entityId in entityOpacityMap) {
@@ -39316,7 +41503,7 @@
39316
41503
  }
39317
41504
  }
39318
41505
  }
39319
- let imagery = (bSettings === null || bSettings === void 0 ? void 0 : bSettings.imagery) != null ? bSettings.imagery : (_6 = defaults.settings) === null || _6 === void 0 ? void 0 : _6.imagery;
41506
+ let imagery = (bSettings === null || bSettings === void 0 ? void 0 : bSettings.imagery) != null ? bSettings.imagery : (_7 = defaults.settings) === null || _7 === void 0 ? void 0 : _7.imagery;
39320
41507
  if (imagery == null) {
39321
41508
  // TODO: Need global default.
39322
41509
  imagery = [
@@ -39386,7 +41573,7 @@
39386
41573
  // Otherwise we cause a flicker of rendered graphics.
39387
41574
  // Maybe TODO to such a function for menu item manager.
39388
41575
  const curEnabled = params.manager.GetEnabledItemIds();
39389
- const newItemIds = (_7 = bSettings === null || bSettings === void 0 ? void 0 : bSettings.menuItemIds) !== null && _7 !== void 0 ? _7 : [];
41576
+ const newItemIds = (_8 = bSettings === null || bSettings === void 0 ? void 0 : bSettings.menuItemIds) !== null && _8 !== void 0 ? _8 : [];
39390
41577
  for (const id of curEnabled) {
39391
41578
  let shouldRemove;
39392
41579
  if (id == RELATION_MENU_ITEM_ID) {
@@ -39460,7 +41647,7 @@
39460
41647
  }
39461
41648
  let gOcclusion = bSettings === null || bSettings === void 0 ? void 0 : bSettings.groundOcclusion;
39462
41649
  if (gOcclusion == null) {
39463
- gOcclusion = (_8 = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _8 === void 0 ? void 0 : _8.groundOcclusion;
41650
+ gOcclusion = (_9 = defaults === null || defaults === void 0 ? void 0 : defaults.settings) === null || _9 === void 0 ? void 0 : _9.groundOcclusion;
39464
41651
  }
39465
41652
  if (gOcclusion == null) {
39466
41653
  // TODO: Need global default.
@@ -39477,6 +41664,17 @@
39477
41664
  BModels.ProjectViewBookmark.EContentType.WEB_3D_MODEL_SPACE,
39478
41665
  BModels.ProjectViewBookmark.EContentType.WEB_3D_SPLIT_PLUGIN
39479
41666
  ];
41667
+ /**
41668
+ * A bookmark's own shadows, else the view's defaults unless it is model-space, which reads brighter without.
41669
+ */
41670
+ function ResolveShadows(bSettings, defaults, contentType) {
41671
+ let shadows = bSettings === null || bSettings === void 0 ? void 0 : bSettings.shadows;
41672
+ if (shadows == null && contentType != BModels.ProjectViewBookmark.EContentType.WEB_3D_MODEL_SPACE) {
41673
+ shadows = defaults === null || defaults === void 0 ? void 0 : defaults.shadows;
41674
+ }
41675
+ return shadows !== null && shadows !== void 0 ? shadows : { enabled: false };
41676
+ }
41677
+ ViewRenderEngine.ResolveShadows = ResolveShadows;
39480
41678
  /**
39481
41679
  * Returns where a model-space bookmark's camera goes, measured from the origin in effect.
39482
41680
  * @param viewer
@@ -44201,7 +46399,7 @@
44201
46399
  const HOLD_LOADER_DELAY_MS = 150;
44202
46400
  const CONTINUOUS_ANIMATION_SPEED = 0.02 / 3;
44203
46401
  const ZOOM_DISTANCE = 0.85;
44204
- const MIN_ZOOM_DISTANCE = 0.3;
46402
+ const MIN_ZOOM_DISTANCE$1 = 0.3;
44205
46403
  const NEAR_NADIR_PITCH = -Math.PI / 2 + 0.05;
44206
46404
  const MIN_ORBIT_OFFSET_METERS = 5;
44207
46405
  // Caps how far a pivot may be from the camera relative to the camera's own altitude.
@@ -44407,7 +46605,7 @@
44407
46605
  const mag = (ZOOM_DISTANCE * 0.5) / Math.max(1, 4 - Math.log10(height));
44408
46606
  const zoom = zoomIn ? 1 - mag : 1 + mag;
44409
46607
  const offset = Cesium.Cartesian3.multiplyByScalar(Cesium.Cartesian3.subtract(p, ground, new Cesium.Cartesian3()), zoom, new Cesium.Cartesian3());
44410
- if (zoomIn && Cesium.Cartesian3.magnitude(offset) <= MIN_ZOOM_DISTANCE) {
46608
+ if (zoomIn && Cesium.Cartesian3.magnitude(offset) <= MIN_ZOOM_DISTANCE$1) {
44411
46609
  return Promise.resolve(null);
44412
46610
  }
44413
46611
  const to = Cesium.Cartesian3.add(ground, offset, new Cesium.Cartesian3());
@@ -44631,7 +46829,7 @@
44631
46829
  const mag = (ZOOM_DISTANCE * 0.5) / Math.max(1, 4 - Math.log10(height));
44632
46830
  const zoom = zoomIn ? 1 - mag * 0.03 : 1 + mag * 0.03;
44633
46831
  const offset = Cesium.Cartesian3.multiplyByScalar(Cesium.Cartesian3.subtract(p, ground, new Cesium.Cartesian3()), zoom, new Cesium.Cartesian3());
44634
- if (zoomIn && Cesium.Cartesian3.magnitude(offset) <= MIN_ZOOM_DISTANCE) {
46832
+ if (zoomIn && Cesium.Cartesian3.magnitude(offset) <= MIN_ZOOM_DISTANCE$1) {
44635
46833
  active = false;
44636
46834
  return;
44637
46835
  }
@@ -45440,7 +47638,7 @@
45440
47638
  * @param viewer
45441
47639
  */
45442
47640
  function InitViewer(params) {
45443
- const { viewer, destroyWidgets, styleContainer, nextspaceWidgets, widgetParams } = params;
47641
+ const { viewer, destroyWidgets, styleContainer, nextspaceWidgets, widgetParams, cameraControls } = params;
45444
47642
  const data = { widgets: {} };
45445
47643
  if (destroyWidgets != false) {
45446
47644
  try {
@@ -45467,6 +47665,12 @@
45467
47665
  // Newer cesium is much dimmer by default so we shift brightness a bit.
45468
47666
  exports.ViewUtils.ApplyDefaultBrightness(viewer);
45469
47667
  backgroundRendering(viewer);
47668
+ if (cameraControls != false) {
47669
+ exports.CameraControls.Attach(viewer);
47670
+ if (exports.ViewUtils.GetModelSpace(viewer)) {
47671
+ exports.CameraControls.SetModelSpace(viewer, () => exports.ViewUtils.GetModelSpaceFrame(viewer));
47672
+ }
47673
+ }
45470
47674
  // Capturing the destroy call to destroy all widgets.
45471
47675
  const olDestroy = viewer.destroy;
45472
47676
  viewer.destroy = () => {
@@ -45475,6 +47679,7 @@
45475
47679
  }
45476
47680
  try {
45477
47681
  ViewerUtils.DisposeNextspaceWidgets(viewer);
47682
+ exports.CameraControls.Detach(viewer);
45478
47683
  }
45479
47684
  // We don't want the actual destroy to fail.
45480
47685
  catch (e) {
@@ -49609,7 +51814,7 @@
49609
51814
  ...exports.ViewUtils.DefaultLightingProps(),
49610
51815
  heightReference: heightRef,
49611
51816
  scale: new Cesium.CallbackProperty(() => scale * styleScale, true),
49612
- shadows: Cesium.ShadowMode.ENABLED,
51817
+ shadows: exports.ViewUtils.GetEntityShadowMode(params.viewer),
49613
51818
  colorBlendAmount: blendAmount,
49614
51819
  colorBlendMode: blendMode,
49615
51820
  color: new Cesium.CallbackProperty(() => {
@@ -49644,7 +51849,7 @@
49644
51849
  cEntity.model.uri = new Cesium.ConstantProperty(params.lodUrl);
49645
51850
  }
49646
51851
  cEntity.model.heightReference = new Cesium.ConstantProperty(heightRef);
49647
- cEntity.model.shadows = new Cesium.ConstantProperty(Cesium.ShadowMode.ENABLED);
51852
+ cEntity.model.shadows = new Cesium.ConstantProperty(exports.ViewUtils.GetEntityShadowMode(params.viewer));
49648
51853
  cEntity.model.colorBlendAmount = new Cesium.ConstantProperty(blendAmount);
49649
51854
  cEntity.model.colorBlendMode = new Cesium.ConstantProperty(blendMode);
49650
51855
  cEntity.model.distanceDisplayCondition = new Cesium.ConstantProperty(exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance));
@@ -50482,7 +52687,7 @@
50482
52687
  zIndex: getZIndex(style, entity, params.tags),
50483
52688
  distanceDisplayCondition: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance, width),
50484
52689
  cornerType: Cesium.CornerType.MITERED,
50485
- shadows: Cesium.ShadowMode.ENABLED,
52690
+ shadows: exports.ViewUtils.GetEntityShadowMode(params.viewer),
50486
52691
  fill: true
50487
52692
  } : null,
50488
52693
  position: exports.EntityUtils.GetPos({
@@ -54937,7 +57142,7 @@ void main() {
54937
57142
  fill: !drawsDisplacedSurface,
54938
57143
  extrudedHeight: extrusion.value,
54939
57144
  extrudedHeightReference: extrusion.exHeightRef,
54940
- shadows: Cesium.ShadowMode.ENABLED,
57145
+ shadows: exports.ViewUtils.GetEntityShadowMode(params.viewer),
54941
57146
  heightReference: heightRef,
54942
57147
  classificationType: classification,
54943
57148
  perPositionHeight: heightRef == Cesium.HeightReference.CLAMP_TO_GROUND ? false : true,
@@ -55017,7 +57222,7 @@ void main() {
55017
57222
  viewer: params.viewer,
55018
57223
  graphic: cEntity.polygon,
55019
57224
  property: "shadows",
55020
- value: Cesium.ShadowMode.ENABLED
57225
+ value: exports.ViewUtils.GetEntityShadowMode(params.viewer)
55021
57226
  });
55022
57227
  SetCPropIfChanged({
55023
57228
  viewer: params.viewer,
@@ -55152,7 +57357,7 @@ void main() {
55152
57357
  cornerType: Cesium.CornerType.MITERED,
55153
57358
  classificationType: classification,
55154
57359
  distanceDisplayCondition: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance, width, false, size),
55155
- shadows: Cesium.ShadowMode.ENABLED
57360
+ shadows: exports.ViewUtils.GetEntityShadowMode(params.viewer)
55156
57361
  } : null,
55157
57362
  show: true
55158
57363
  });
@@ -55275,7 +57480,7 @@ void main() {
55275
57480
  cornerType: Cesium.CornerType.MITERED,
55276
57481
  classificationType: classification,
55277
57482
  distanceDisplayCondition: exports.EntityRenderEngine.GetDisplayCondition(params.minDistance, params.maxDistance, width, false, size),
55278
- shadows: Cesium.ShadowMode.ENABLED,
57483
+ shadows: exports.ViewUtils.GetEntityShadowMode(params.viewer),
55279
57484
  } : null,
55280
57485
  show: true
55281
57486
  });
@@ -57596,7 +59801,7 @@ void main() {
57596
59801
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
57597
59802
  })(exports.StyleUtils || (exports.StyleUtils = {}));
57598
59803
 
57599
- const VERSION = "7.7.7";
59804
+ const VERSION = "7.7.8";
57600
59805
  /**
57601
59806
  * Updates the environment instance used by bruce-cesium to one specified.
57602
59807
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.