diff --git a/Apps/SampleData/ClampToGround.czml b/Apps/SampleData/ClampToGround.czml index 9d5abaccd264..44734a3de77e 100644 --- a/Apps/SampleData/ClampToGround.czml +++ b/Apps/SampleData/ClampToGround.czml @@ -5,7 +5,7 @@ "clock": { "interval": "2018-07-19T15:18:00Z/2018-07-19T15:18:30Z", "currentTime": "2018-07-19T15:18:00Z", - "multiplier": 5, + "multiplier": 2, "range": "LOOP_STOP", "step": "SYSTEM_CLOCK_MULTIPLIER" } @@ -19,14 +19,22 @@ "interpolationAlgorithm": "LINEAR", "forwardExtrapolationType": "HOLD", "cartesian": [ - "2018-07-19T15:18:00Z", - 1216348.1632364073, - -4736348.958775471, - 4081284.5528982095, + "2018-07-19T15:18:00Z", + 1216327.3893347275, + -4736164.778028102, + 4081507.5209477833, + "2018-07-19T15:18:10Z", + 1216369.543258349, + -4736201.237448179, + 4081447.3732212726, + "2018-07-19T15:18:20Z", + 1216434.7507773656, + -4736241.372142024, + 4081386.1802605274, "2018-07-19T15:18:30Z", - 1216369.1229444197, - -4736377.467107148, - 4081240.888485707 + 1216525.7792628652, + -4736271.927759278, + 4081319.744558958 ] }, "orientation": { @@ -43,26 +51,29 @@ "polyline": { "positions": { "cartesian": [ - 1216348.1632364073, - -4736348.958775471, - 4081284.5528982095, - 1216369.1229444197, - -4736377.467107148, - 4081240.888485707 + 1216327.3893347275, + -4736164.778028102, + 4081507.5209477833, + 1216369.543258349, + -4736201.237448179, + 4081447.3732212726, + 1216434.7507773656, + -4736241.372142024, + 4081386.1802605274, + 1216525.7792628652, + -4736271.927759278, + 4081319.744558958 ] }, "material": { "polylineOutline": { "color": { - "rgba": [255, 255, 0, 255] + "rgba": [100, 149, 237, 140] }, - "outlineColor": { - "rgba": [0, 0, 0, 255] - }, - "outlineWidth": 2 + "outlineWidth": 0 } }, - "width": 10, + "width": 12, "clampToGround": true } } diff --git a/Apps/Sandcastle/gallery/Clamp Entities to Ground.html b/Apps/Sandcastle/gallery/Clamp Entities to Ground.html new file mode 100644 index 000000000000..cdda0b4e48a6 --- /dev/null +++ b/Apps/Sandcastle/gallery/Clamp Entities to Ground.html @@ -0,0 +1,343 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+
+
+
+
+
+ + + diff --git a/Apps/Sandcastle/gallery/Clamp Entities to Ground.jpg b/Apps/Sandcastle/gallery/Clamp Entities to Ground.jpg new file mode 100644 index 000000000000..dfdf6efe707c Binary files /dev/null and b/Apps/Sandcastle/gallery/Clamp Entities to Ground.jpg differ diff --git a/Apps/Sandcastle/gallery/Clamp Model to Ground.html b/Apps/Sandcastle/gallery/Clamp Model to Ground.html new file mode 100644 index 000000000000..1d10dd1121bf --- /dev/null +++ b/Apps/Sandcastle/gallery/Clamp Model to Ground.html @@ -0,0 +1,194 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + diff --git a/Apps/Sandcastle/gallery/Clamp Model to Ground.jpg b/Apps/Sandcastle/gallery/Clamp Model to Ground.jpg new file mode 100644 index 000000000000..174b994d5b5d Binary files /dev/null and b/Apps/Sandcastle/gallery/Clamp Model to Ground.jpg differ diff --git a/Apps/Sandcastle/gallery/Clamp to 3D Tiles.html b/Apps/Sandcastle/gallery/Clamp to 3D Tiles.html deleted file mode 100644 index dadb13795bf0..000000000000 --- a/Apps/Sandcastle/gallery/Clamp to 3D Tiles.html +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - Cesium Demo - - - - - - -
-

Loading...

-
- - - diff --git a/Apps/Sandcastle/gallery/Clamp to 3D Tiles.jpg b/Apps/Sandcastle/gallery/Clamp to 3D Tiles.jpg deleted file mode 100644 index 8164f073a8e6..000000000000 Binary files a/Apps/Sandcastle/gallery/Clamp to 3D Tiles.jpg and /dev/null differ diff --git a/Apps/Sandcastle/gallery/Clamp to Terrain.html b/Apps/Sandcastle/gallery/Clamp to Terrain.html deleted file mode 100644 index f01888c3ee85..000000000000 --- a/Apps/Sandcastle/gallery/Clamp to Terrain.html +++ /dev/null @@ -1,384 +0,0 @@ - - - - - - - - - Cesium Demo - - - - - - -
-

Loading...

-
-
-
-
-
-
- - - diff --git a/Apps/Sandcastle/gallery/Clamp to Terrain.jpg b/Apps/Sandcastle/gallery/Clamp to Terrain.jpg deleted file mode 100644 index 9841e9883479..000000000000 Binary files a/Apps/Sandcastle/gallery/Clamp to Terrain.jpg and /dev/null differ diff --git a/Apps/Sandcastle/gallery/development/Clamp to Ground Modes.html b/Apps/Sandcastle/gallery/development/Clamp to Ground Modes.html new file mode 100644 index 000000000000..db8662736f2b --- /dev/null +++ b/Apps/Sandcastle/gallery/development/Clamp to Ground Modes.html @@ -0,0 +1,177 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+
+
+
+
+
+ + + diff --git a/CHANGES.md b/CHANGES.md index 2a35b7b448b6..db922b50013f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,12 +9,15 @@ - By default, the screen space camera controller will no longer go inside or under instances of `Cesium3DTileset`. [#11581](https://github.com/CesiumGS/cesium/pull/11581) - This behavior can be disabled by setting `Cesium3DTileset.disableCollision` to true. - This feature is enabled by default only for WebGL 2 and above, but can be enabled for WebGL 1 by setting the `enablePick` option to true when creating the `Cesium3DTileset`. +- Clamping to ground, `HeightReference.CLAMP_TO_GROUND`, and `HeightReference.RELATIVE_TO_GROUND` now take into account 3D Tilesets. These opions will clamp to either 3D Tilesets or Terrain, whichever has a greater height. [#11604](https://github.com/CesiumGS/cesium/pull/11604) + - To restore previous behavior where an entity is clamped only to terrain or relative only to terrain, set `heightReference` to `HeightReference.CLAMP_TO_TERRAIN` or `HeightReference.RELATIVE_TO_TERRAIN` respectively. - Remove the need for node internal packages `http`, `https`, `url` and `zlib` in the `Resource` class. This means they do not need to be marked external by build tools anymore. [#11773](https://github.com/CesiumGS/cesium/pull/11773) - This slightly changed the contents of the `RequestErrorEvent` error that is thrown in node environments when a request fails. The `response` property is now a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) object instead of an [`http.IncomingMessage`](https://nodejs.org/docs/latest-v20.x/api/http.html#class-httpincomingmessage) - `PolygonGeometry.computeRectangle` has been removed. Use `PolygonGeometry.computeRectangleFromPositions` instead. ##### Additions :tada: +- Added `HeightReference.CLAMP_TO_TERRAIN`, `HeightReference.RELATIVE_TO_TERRAIN`, `HeightReference.CLAMP_TO_3D_TILE`, and `HeightReference.RELATIVE_TO_3D_TILE` to position relatve to terrain or 3D tilesets exclusively.[#11604](https://github.com/CesiumGS/cesium/pull/11604) - Added `Cesium3DTileset.getHeight` to sample height values of the loaded tiles. If using WebGL 1, the `enablePick` option must be set to true to use this function. [#11581](https://github.com/CesiumGS/cesium/pull/11581) - Added `Cesium3DTileset.disableCollision` to allow the camera from to go inside or below a 3D tileset, for instance, to be used with 3D Tiles interiors. [#11581](https://github.com/CesiumGS/cesium/pull/11581) - The `Cesium3DTileset.dynamicScreenSpaceError` optimization is now enabled by default, as this improves performance for street-level horizon views. Furthermore, the default settings of this feature were tuned for improved performance. `Cesium3DTileset.dynamicScreenSpaceErrorDensity` was changed from 0.00278 to 0.0002. `Cesium3DTileset.dynamicScreenSpaceErrorFactor` was changed from 4 to 24. [#11718](https://github.com/CesiumGS/cesium/pull/11718) diff --git a/Specs/createGlobe.js b/Specs/createGlobe.js index 200419f42880..fdd17e3d2e77 100644 --- a/Specs/createGlobe.js +++ b/Specs/createGlobe.js @@ -4,8 +4,8 @@ function createGlobe(ellipsoid) { ellipsoid = defaultValue(ellipsoid, Ellipsoid.WGS84); const globe = { - callback: undefined, - removedCallback: false, + _callback: undefined, + _removedCallback: false, ellipsoid: ellipsoid, beginFrame: function () {}, endFrame: function () {}, @@ -22,10 +22,10 @@ function createGlobe(ellipsoid) { }; globe._surface.updateHeight = function (position, callback) { - globe.callback = callback; + globe._callback = callback; return function () { - globe.removedCallback = true; - globe.callback = undefined; + globe._removedCallback = true; + globe._callback = undefined; }; }; diff --git a/packages/engine/Source/DataSources/Entity.js b/packages/engine/Source/DataSources/Entity.js index 934ba64f49c9..c7d20e8c0a3f 100644 --- a/packages/engine/Source/DataSources/Entity.js +++ b/packages/engine/Source/DataSources/Entity.js @@ -13,7 +13,9 @@ import Quaternion from "../Core/Quaternion.js"; import Transforms from "../Core/Transforms.js"; import GroundPolylinePrimitive from "../Scene/GroundPolylinePrimitive.js"; import GroundPrimitive from "../Scene/GroundPrimitive.js"; -import HeightReference from "../Scene/HeightReference.js"; +import HeightReference, { + isHeightReferenceClamp, +} from "../Scene/HeightReference.js"; import BillboardGraphics from "./BillboardGraphics.js"; import BoxGraphics from "./BoxGraphics.js"; import ConstantPositionProperty from "./ConstantPositionProperty.js"; @@ -707,7 +709,7 @@ Entity.prototype.computeModelMatrixForHeightReference = function ( } const carto = ellipsoid.cartesianToCartographic(position, cartoScratch); - if (heightReference === HeightReference.CLAMP_TO_GROUND) { + if (isHeightReferenceClamp(heightReference)) { carto.height = heightOffset; } else { carto.height += heightOffset; diff --git a/packages/engine/Source/DataSources/GroundGeometryUpdater.js b/packages/engine/Source/DataSources/GroundGeometryUpdater.js index 5373bb70d37b..438b483dd0ae 100644 --- a/packages/engine/Source/DataSources/GroundGeometryUpdater.js +++ b/packages/engine/Source/DataSources/GroundGeometryUpdater.js @@ -5,7 +5,9 @@ import DeveloperError from "../Core/DeveloperError.js"; import GeometryOffsetAttribute from "../Core/GeometryOffsetAttribute.js"; import oneTimeWarning from "../Core/oneTimeWarning.js"; import GroundPrimitive from "../Scene/GroundPrimitive.js"; -import HeightReference from "../Scene/HeightReference.js"; +import HeightReference, { + isHeightReferenceClamp, +} from "../Scene/HeightReference.js"; import CallbackProperty from "./CallbackProperty.js"; import ConstantProperty from "./ConstantProperty.js"; import GeometryUpdater from "./GeometryUpdater.js"; @@ -164,9 +166,10 @@ GroundGeometryUpdater.getGeometryHeight = function (height, heightReference) { return; } - if (heightReference !== HeightReference.CLAMP_TO_GROUND) { + if (!isHeightReferenceClamp(heightReference)) { return height; } + return 0.0; }; @@ -186,7 +189,7 @@ GroundGeometryUpdater.getGeometryExtrudedHeight = function ( } return; } - if (extrudedHeightReference !== HeightReference.CLAMP_TO_GROUND) { + if (!isHeightReferenceClamp(extrudedHeightReference)) { return extrudedHeight; } diff --git a/packages/engine/Source/DataSources/ModelVisualizer.js b/packages/engine/Source/DataSources/ModelVisualizer.js index 5665c9dc5a7b..1e2b7a0a2c6c 100644 --- a/packages/engine/Source/DataSources/ModelVisualizer.js +++ b/packages/engine/Source/DataSources/ModelVisualizer.js @@ -4,19 +4,22 @@ import Cartesian2 from "../Core/Cartesian2.js"; import Cartesian3 from "../Core/Cartesian3.js"; import Check from "../Core/Check.js"; import Color from "../Core/Color.js"; +import defaultValue from "../Core/defaultValue.js"; import defined from "../Core/defined.js"; import destroyObject from "../Core/destroyObject.js"; import DeveloperError from "../Core/DeveloperError.js"; +import Ellipsoid from "../Core/Ellipsoid.js"; import Matrix4 from "../Core/Matrix4.js"; import Resource from "../Core/Resource.js"; import ColorBlendMode from "../Scene/ColorBlendMode.js"; -import HeightReference from "../Scene/HeightReference.js"; +import HeightReference, { + isHeightReferenceClamp, +} from "../Scene/HeightReference.js"; import Model from "../Scene/Model/Model.js"; import ModelAnimationLoop from "../Scene/ModelAnimationLoop.js"; import ShadowMode from "../Scene/ShadowMode.js"; import BoundingSphereState from "./BoundingSphereState.js"; import Property from "./Property.js"; -import sampleTerrainMostDetailed from "../Core/sampleTerrainMostDetailed.js"; import Cartographic from "../Core/Cartographic.js"; const defaultScale = 1.0; @@ -172,9 +175,6 @@ ModelVisualizer.prototype.update = function (time) { articulationsScratch: {}, loadFailed: false, modelUpdated: false, - awaitingSampleTerrain: false, - clampedBoundingSphere: undefined, - sampleTerrainFailed: false, }; modelHash[entity.id] = modelData; @@ -399,9 +399,6 @@ ModelVisualizer.prototype.destroy = function () { return destroyObject(this); }; -// Used for testing. -ModelVisualizer._sampleTerrainMostDetailed = sampleTerrainMostDetailed; - const scratchPosition = new Cartesian3(); const scratchCartographic = new Cartographic(); /** @@ -445,106 +442,30 @@ ModelVisualizer.prototype.getBoundingSphere = function (entity, result) { const scene = this._scene; const globe = scene.globe; + const ellipsoid = defaultValue(globe?.ellipsoid, Ellipsoid.WGS84); - // cannot access a terrain provider if there is no globe; formally set to undefined - const terrainProvider = defined(globe) ? globe.terrainProvider : undefined; const hasHeightReference = model.heightReference !== HeightReference.NONE; - if (defined(globe) && hasHeightReference) { - const ellipsoid = globe.ellipsoid; + if (hasHeightReference) { const modelMatrix = model.modelMatrix; scratchPosition.x = modelMatrix[12]; scratchPosition.y = modelMatrix[13]; scratchPosition.z = modelMatrix[14]; - const cartoPosition = ellipsoid.cartesianToCartographic(scratchPosition); - - // For a terrain provider that does not have availability, like the EllipsoidTerrainProvider, - // we can directly assign the bounding sphere's center from model matrix's translation. - if (!defined(terrainProvider.availability)) { - // Regardless of what the original model's position is set to, for CLAMP_TO_GROUND, we reset it to 0 - // when computing the position to zoom/fly to. - if (model.heightReference === HeightReference.CLAMP_TO_GROUND) { - cartoPosition.height = 0; - } - - const scratchPosition = ellipsoid.cartographicToCartesian(cartoPosition); - BoundingSphere.clone(model.boundingSphere, result); - result.center = scratchPosition; - - return BoundingSphereState.DONE; - } - - // Otherwise, in the case of terrain providers with availability, - // since the model's bounding sphere may be clamped to a lower LOD tile if - // the camera is initially far away, we use sampleTerrainMostDetailed to estimate - // where the bounding sphere should be and set that as the target bounding sphere - // for the camera. - let clampedBoundingSphere = this._modelHash[entity.id] - .clampedBoundingSphere; - - // Check if the sample terrain function has failed. - const sampleTerrainFailed = this._modelHash[entity.id].sampleTerrainFailed; - if (sampleTerrainFailed) { - this._modelHash[entity.id].sampleTerrainFailed = false; - return BoundingSphereState.FAILED; - } + const cartoPosition = ellipsoid.cartesianToCartographic( + scratchPosition, + scratchCartographic + ); - if (!defined(clampedBoundingSphere)) { - clampedBoundingSphere = new BoundingSphere(); - - // Since this function is called per-frame, we set a flag when sampleTerrainMostDetailed - // is called and check for it to avoid calling it again. - const awaitingSampleTerrain = this._modelHash[entity.id] - .awaitingSampleTerrain; - if (!awaitingSampleTerrain) { - Cartographic.clone(cartoPosition, scratchCartographic); - this._modelHash[entity.id].awaitingSampleTerrain = true; - ModelVisualizer._sampleTerrainMostDetailed(terrainProvider, [ - scratchCartographic, - ]) - .then((result) => { - if (this.isDestroyed()) { - return; - } - - this._modelHash[entity.id].awaitingSampleTerrain = false; - - const updatedCartographic = result[0]; - if (model.heightReference === HeightReference.RELATIVE_TO_GROUND) { - updatedCartographic.height += cartoPosition.height; - } - ellipsoid.cartographicToCartesian( - updatedCartographic, - scratchPosition - ); - - // Update the bounding sphere with the updated position. - BoundingSphere.clone(model.boundingSphere, clampedBoundingSphere); - clampedBoundingSphere.center = scratchPosition; - - this._modelHash[ - entity.id - ].clampedBoundingSphere = BoundingSphere.clone( - clampedBoundingSphere - ); - }) - .catch((e) => { - if (this.isDestroyed()) { - return; - } - - this._modelHash[entity.id].sampleTerrainFailed = true; - this._modelHash[entity.id].awaitingSampleTerrain = false; - }); + const height = scene.getHeight(cartoPosition, model.heightReference); + if (defined(height)) { + if (isHeightReferenceClamp(model.heightReference)) { + cartoPosition.height = height; + } else { + cartoPosition.height += height; } - - // We will return the state as pending until the clamped bounding sphere is defined, - // which happens when the sampleTerrainMostDetailed promise returns. - return BoundingSphereState.PENDING; } - BoundingSphere.clone(clampedBoundingSphere, result); - // Reset the clamped bounding sphere. - this._modelHash[entity.id].clampedBoundingSphere = undefined; + BoundingSphere.clone(model.boundingSphere, result); + result.center = ellipsoid.cartographicToCartesian(cartoPosition); return BoundingSphereState.DONE; } diff --git a/packages/engine/Source/DataSources/TerrainOffsetProperty.js b/packages/engine/Source/DataSources/TerrainOffsetProperty.js index 17eab9c26406..662d108eed26 100644 --- a/packages/engine/Source/DataSources/TerrainOffsetProperty.js +++ b/packages/engine/Source/DataSources/TerrainOffsetProperty.js @@ -6,12 +6,12 @@ import destroyObject from "../Core/destroyObject.js"; import Event from "../Core/Event.js"; import Iso8601 from "../Core/Iso8601.js"; import CesiumMath from "../Core/Math.js"; -import HeightReference from "../Scene/HeightReference.js"; -import SceneMode from "../Scene/SceneMode.js"; +import HeightReference, { + isHeightReferenceRelative, +} from "../Scene/HeightReference.js"; import Property from "./Property.js"; const scratchPosition = new Cartesian3(); -const scratchCarto = new Cartographic(); /** * @private @@ -118,40 +118,32 @@ TerrainOffsetProperty.prototype._updateClamping = function () { const globe = scene.globe; const position = this._position; - if (!defined(globe) || Cartesian3.equals(position, Cartesian3.ZERO)) { + if (Cartesian3.equals(position, Cartesian3.ZERO)) { this._terrainHeight = 0; return; } const ellipsoid = globe.ellipsoid; - const surface = globe._surface; - - const that = this; const cartographicPosition = ellipsoid.cartesianToCartographic( position, this._cartographicPosition ); - const height = globe.getHeight(cartographicPosition); + + const height = scene.getHeight(cartographicPosition, this._heightReference); if (defined(height)) { this._terrainHeight = height; } else { this._terrainHeight = 0; } - function updateFunction(clampedPosition) { - if (scene.mode === SceneMode.SCENE3D) { - const carto = ellipsoid.cartesianToCartographic( - clampedPosition, - scratchCarto - ); - that._terrainHeight = carto.height; - } else { - that._terrainHeight = clampedPosition.x; - } - that.definitionChanged.raiseEvent(); - } - this._removeCallbackFunc = surface.updateHeight( + const updateFunction = (clampedPosition) => { + this._terrainHeight = clampedPosition.height; + this.definitionChanged.raiseEvent(); + }; + + this._removeCallbackFunc = scene.updateHeight( cartographicPosition, - updateFunction + updateFunction, + this._heightReference ); }; @@ -174,7 +166,7 @@ TerrainOffsetProperty.prototype.getValue = function (time, result) { if ( heightReference === HeightReference.NONE && - extrudedHeightReference !== HeightReference.RELATIVE_TO_GROUND + !isHeightReferenceRelative(extrudedHeightReference) ) { this._position = Cartesian3.clone(Cartesian3.ZERO, this._position); return Cartesian3.clone(Cartesian3.ZERO, result); diff --git a/packages/engine/Source/Scene/Billboard.js b/packages/engine/Source/Scene/Billboard.js index b10b42d40aa8..878bd41dc25e 100644 --- a/packages/engine/Source/Scene/Billboard.js +++ b/packages/engine/Source/Scene/Billboard.js @@ -10,10 +10,13 @@ import defaultValue from "../Core/defaultValue.js"; import defined from "../Core/defined.js"; import DeveloperError from "../Core/DeveloperError.js"; import DistanceDisplayCondition from "../Core/DistanceDisplayCondition.js"; +import Ellipsoid from "../Core/Ellipsoid.js"; import Matrix4 from "../Core/Matrix4.js"; import NearFarScalar from "../Core/NearFarScalar.js"; import Resource from "../Core/Resource.js"; -import HeightReference from "./HeightReference.js"; +import HeightReference, { + isHeightReferenceRelative, +} from "./HeightReference.js"; import HorizontalOrigin from "./HorizontalOrigin.js"; import SceneMode from "./SceneMode.js"; import SceneTransforms from "./SceneTransforms.js"; @@ -1056,15 +1059,13 @@ Billboard.prototype._updateClamping = function () { }; const scratchCartographic = new Cartographic(); -const scratchPosition = new Cartesian3(); - Billboard._updateClamping = function (collection, owner) { const scene = collection._scene; - if (!defined(scene) || !defined(scene.globe)) { + if (!defined(scene)) { //>>includeStart('debug', pragmas.debug); if (owner._heightReference !== HeightReference.NONE) { throw new DeveloperError( - "Height reference is not supported without a scene and globe." + "Height reference is not supported without a scene." ); } //>>includeEnd('debug'); @@ -1072,8 +1073,7 @@ Billboard._updateClamping = function (collection, owner) { } const globe = scene.globe; - const ellipsoid = globe.ellipsoid; - const surface = globe._surface; + const ellipsoid = defaultValue(globe?.ellipsoid, Ellipsoid.WGS84); const mode = scene.frameState.mode; @@ -1096,45 +1096,48 @@ Billboard._updateClamping = function (collection, owner) { return; } + if (defined(owner._removeCallbackFunc)) { + owner._removeCallbackFunc(); + } + const position = ellipsoid.cartesianToCartographic(owner._position); if (!defined(position)) { owner._actualClampedPosition = undefined; return; } - if (defined(owner._removeCallbackFunc)) { - owner._removeCallbackFunc(); - } - function updateFunction(clampedPosition) { - if (owner._heightReference === HeightReference.RELATIVE_TO_GROUND) { + owner._clampedPosition = ellipsoid.cartographicToCartesian( + clampedPosition, + owner._clampedPosition + ); + + if (isHeightReferenceRelative(owner._heightReference)) { if (owner._mode === SceneMode.SCENE3D) { - const clampedCart = ellipsoid.cartesianToCartographic( + clampedPosition.height += position.height; + ellipsoid.cartographicToCartesian( clampedPosition, - scratchCartographic + owner._clampedPosition ); - clampedCart.height += position.height; - ellipsoid.cartographicToCartesian(clampedCart, clampedPosition); } else { - clampedPosition.x += position.height; + owner._clampedPosition.x += position.height; } } - owner._clampedPosition = Cartesian3.clone( - clampedPosition, - owner._clampedPosition - ); } - owner._removeCallbackFunc = surface.updateHeight(position, updateFunction); + + owner._removeCallbackFunc = scene.updateHeight( + position, + updateFunction, + owner._heightReference + ); Cartographic.clone(position, scratchCartographic); - const height = globe.getHeight(position); + const height = scene.getHeight(position, owner._heightReference); if (defined(height)) { scratchCartographic.height = height; } - ellipsoid.cartographicToCartesian(scratchCartographic, scratchPosition); - - updateFunction(scratchPosition); + updateFunction(scratchCartographic); }; Billboard.prototype._loadImage = function () { diff --git a/packages/engine/Source/Scene/BillboardCollection.js b/packages/engine/Source/Scene/BillboardCollection.js index c0116eaabc6b..9bd1d3be1f9b 100644 --- a/packages/engine/Source/Scene/BillboardCollection.js +++ b/packages/engine/Source/Scene/BillboardCollection.js @@ -28,7 +28,7 @@ import BillboardCollectionVS from "../Shaders/BillboardCollectionVS.js"; import Billboard from "./Billboard.js"; import BlendingState from "./BlendingState.js"; import BlendOption from "./BlendOption.js"; -import HeightReference from "./HeightReference.js"; +import HeightReference, { isHeightReferenceClamp } from "./HeightReference.js"; import HorizontalOrigin from "./HorizontalOrigin.js"; import SceneMode from "./SceneMode.js"; import SDFSettings from "./SDFSettings.js"; @@ -1375,7 +1375,7 @@ function writeCompressedAttribute3( let disableDepthTestDistance = billboard.disableDepthTestDistance; const clampToGround = - billboard.heightReference === HeightReference.CLAMP_TO_GROUND && + isHeightReferenceClamp(billboard.heightReference) && frameState.context.depthTexture; if (!defined(disableDepthTestDistance)) { disableDepthTestDistance = clampToGround ? 5000.0 : 0.0; @@ -1448,7 +1448,7 @@ function writeTextureCoordinateBoundsOrLabelTranslate( vafWriters, billboard ) { - if (billboard.heightReference === HeightReference.CLAMP_TO_GROUND) { + if (isHeightReferenceClamp(billboard.heightReference)) { const scene = billboardCollection._scene; const context = frameState.context; const globeTranslucent = frameState.globeTranslucencyState.translucent; diff --git a/packages/engine/Source/Scene/Cesium3DTile.js b/packages/engine/Source/Scene/Cesium3DTile.js index 7edd883817d8..8dd01a1f77a3 100644 --- a/packages/engine/Source/Scene/Cesium3DTile.js +++ b/packages/engine/Source/Scene/Cesium3DTile.js @@ -474,6 +474,7 @@ function Cesium3DTile(tileset, baseResource, header, parent) { this._touchedFrame = 0; this._visitedFrame = 0; this._selectedFrame = 0; + this._wasSelectedLastFrame = false; this._requestedFrame = 0; this._ancestorWithContent = undefined; this._ancestorWithContentAvailable = undefined; diff --git a/packages/engine/Source/Scene/Cesium3DTileset.js b/packages/engine/Source/Scene/Cesium3DTileset.js index 330527ead252..df733b9d8d84 100644 --- a/packages/engine/Source/Scene/Cesium3DTileset.js +++ b/packages/engine/Source/Scene/Cesium3DTileset.js @@ -1,4 +1,5 @@ import ApproximateTerrainHeights from "../Core/ApproximateTerrainHeights.js"; +import BoundingSphere from "../Core/BoundingSphere.js"; import Cartesian2 from "../Core/Cartesian2.js"; import Cartesian3 from "../Core/Cartesian3.js"; import Cartographic from "../Core/Cartographic.js"; @@ -266,6 +267,8 @@ function Cesium3DTileset(options) { ? Matrix4.clone(options.modelMatrix) : Matrix4.clone(Matrix4.IDENTITY); + this._addHeightCallbacks = []; + this._statistics = new Cesium3DTilesetStatistics(); this._statisticsLast = new Cesium3DTilesetStatistics(); this._statisticsPerPass = new Array(Cesium3DTilePass.NUMBER_OF_PASSES); @@ -2632,6 +2635,55 @@ function filterProcessingQueue(tileset) { tiles.length -= removeCount; } +const scratchUpdateHeightCartographic = new Cartographic(); +const scratchUpdateHeightCartographic2 = new Cartographic(); +const scratchUpdateHeightCartesian = new Cartesian3(); +function processUpdateHeight(tileset, tile, frameState) { + const heightCallbackData = tileset._addHeightCallbacks; + const boundingSphere = tile.boundingSphere; + + for (const callbackData of heightCallbackData) { + // No need to upadate if the tile was already visible last frame + if (callbackData.invoked || tile._wasSelectedLastFrame) { + continue; + } + + const ellipsoid = callbackData.ellipsoid; + const positionCartographic = Cartographic.clone( + callbackData.positionCartographic, + scratchUpdateHeightCartographic + ); + const centerCartographic = Cartographic.fromCartesian( + boundingSphere.center, + ellipsoid, + scratchUpdateHeightCartographic2 + ); + + // This can be undefined when the bounding sphere is at the origin + if (defined(centerCartographic)) { + positionCartographic.height = centerCartographic.height; + } + + const position = Cartographic.toCartesian( + positionCartographic, + ellipsoid, + scratchUpdateHeightCartesian + ); + if ( + Cartesian3.distance(position, boundingSphere.center) <= + boundingSphere.radius + ) { + frameState.afterRender.push(() => { + // Callback can be removed before it actually invoked at the end of the frame + if (defined(callbackData.callback)) { + callbackData.callback(positionCartographic); + } + callbackData.invoked = false; + }); + } + } +} + /** * Process tiles in the PROCESSING state so they will eventually move to the READY state. * @private @@ -2899,6 +2951,7 @@ function updateTiles(tileset, frameState, passOptions) { if (isRender) { tileVisible.raiseEvent(tile); } + processUpdateHeight(tileset, tile, frameState); tile.update(tileset, frameState, passOptions); statistics.incrementSelectionCounts(tile.content); ++statistics.selected; @@ -3475,6 +3528,7 @@ Cesium3DTileset.prototype.getHeight = function (cartographic, scene) { cartographic, ray.direction ); + Cartesian3.normalize(ray.direction, ray.direction); ray.direction = Cartesian3.normalize(position, ray.direction); ray.direction = Cartesian3.negate(position, ray.direction); @@ -3495,6 +3549,51 @@ Cesium3DTileset.prototype.getHeight = function (cartographic, scene) { )?.height; }; +/** + * Calls the callback when a new tile is rendered that contains the given cartographic. The only parameter + * is the cartographic position on the tile. + * + * @private + * + * @param {Scene} scene The scene where visualization is taking place. + * @param {Cartographic} cartographic The cartographic position. + * @param {Function} callback The function to be called when a new tile is loaded. + * @param {Ellipsoid} [ellipsoid=Ellipsoid.WGS84] The ellipsoid to use. + * @returns {Function} The function to remove this callback from the quadtree. + */ +Cesium3DTileset.prototype.updateHeight = function ( + cartographic, + callback, + ellipsoid +) { + ellipsoid = defaultValue(ellipsoid, Ellipsoid.WGS84); + + const object = { + positionCartographic: cartographic, + ellipsoid: ellipsoid, + callback: callback, + invoked: false, + }; + + const removeCallback = () => { + const addedCallbacks = this._addHeightCallbacks; + const length = addedCallbacks.length; + for (let i = 0; i < length; ++i) { + if (addedCallbacks[i] === object) { + addedCallbacks.splice(i, 1); + break; + } + } + + if (object.callback) { + object.callback = undefined; + } + }; + + this._addHeightCallbacks.push(object); + return removeCallback; +}; + const scratchSphereIntersection = new Interval(); const scratchPickIntersection = new Cartesian3(); @@ -3515,42 +3614,50 @@ Cesium3DTileset.prototype.pick = function (ray, frameState, result) { const selectedTiles = this._selectedTiles; const selectedLength = selectedTiles.length; + const candidates = []; - let intersection; - let minDistance = Number.POSITIVE_INFINITY; for (let i = 0; i < selectedLength; ++i) { const tile = selectedTiles[i]; const boundsIntersection = IntersectionTests.raySphere( ray, - tile.boundingSphere, + tile.contentBoundingVolume.boundingSphere, scratchSphereIntersection ); - if (!defined(boundsIntersection)) { + if (!defined(boundsIntersection) || !defined(tile.content)) { continue; } - const candidate = tile.content?.pick( + candidates.push(tile); + } + + const length = candidates.length; + candidates.sort((a, b) => { + const aDist = BoundingSphere.distanceSquaredTo( + a.contentBoundingVolume.boundingSphere, + ray.origin + ); + const bDist = BoundingSphere.distanceSquaredTo( + b.contentBoundingVolume.boundingSphere, + ray.origin + ); + + return aDist - bDist; + }); + + let intersection; + for (let i = 0; i < length; ++i) { + const tile = candidates[i]; + const candidate = tile.content.pick( ray, frameState, scratchPickIntersection ); - if (!defined(candidate)) { - continue; - } - - const distance = Cartesian3.distance(ray.origin, candidate); - if (distance < minDistance) { + if (defined(candidate)) { intersection = Cartesian3.clone(candidate, result); - minDistance = distance; + return intersection; } } - - if (!defined(intersection)) { - return undefined; - } - - return intersection; }; /** diff --git a/packages/engine/Source/Scene/Cesium3DTilesetTraversal.js b/packages/engine/Source/Scene/Cesium3DTilesetTraversal.js index 12561ba541eb..57dea371ce94 100644 --- a/packages/engine/Source/Scene/Cesium3DTilesetTraversal.js +++ b/packages/engine/Source/Scene/Cesium3DTilesetTraversal.js @@ -79,6 +79,8 @@ Cesium3DTilesetTraversal.selectTile = function (tile, frameState) { return; } + tile._wasSelectedLastFrame = true; + const { content, tileset } = tile; if (content.featurePropertiesDirty) { // A feature's property in this tile changed, the tile needs to be re-styled. @@ -88,7 +90,9 @@ Cesium3DTilesetTraversal.selectTile = function (tile, frameState) { } else if (tile._selectedFrame < frameState.frameNumber - 1) { // Tile is newly selected; it is selected this frame, but was not selected last frame. tileset._selectedTilesToStyle.push(tile); + tile._wasSelectedLastFrame = false; } + tile._selectedFrame = frameState.frameNumber; tileset._selectedTiles.push(tile); }; diff --git a/packages/engine/Source/Scene/HeightReference.js b/packages/engine/Source/Scene/HeightReference.js index 809fe4c04943..e81a671690de 100644 --- a/packages/engine/Source/Scene/HeightReference.js +++ b/packages/engine/Source/Scene/HeightReference.js @@ -12,17 +12,74 @@ const HeightReference = { NONE: 0, /** - * The position is clamped to the terrain. + * The position is clamped to the terrain and 3D Tiles. * @type {number} * @constant */ CLAMP_TO_GROUND: 1, /** - * The position height is the height above the terrain. + * The position height is the height above the terrain and 3D Tiles. * @type {number} * @constant */ RELATIVE_TO_GROUND: 2, + + /** + * The position is clamped to terain. + * @type {number} + * @constant + */ + CLAMP_TO_TERRAIN: 3, + + /** + * The position height is the height above terrain. + * @type {number} + * @constant + */ + RELATIVE_TO_TERRAIN: 4, + + /** + * The position is clamped to 3D Tiles. + * @type {number} + * @constant + */ + CLAMP_TO_3D_TILE: 5, + + /** + * The position height is the height above 3D Tiles. + * @type {number} + * @constant + */ + RELATIVE_TO_3D_TILE: 6, }; + export default Object.freeze(HeightReference); + +/** + * Returns true if the height should be clamped to the surface + * @param {HeightReference} heightReference + * @returns true if the height should be clamped to the surface + * @private + */ +export function isHeightReferenceClamp(heightReference) { + return ( + heightReference === HeightReference.CLAMP_TO_GROUND || + heightReference === HeightReference.CLAMP_TO_3D_TILE || + heightReference === HeightReference.CLAMP_TO_TERRAIN + ); +} + +/** + * Returns true if the height should be offset relative to the surface + * @param {HeightReference} heightReference + * @returns true if the height should be offset relative to the surface + * @private + */ +export function isHeightReferenceRelative(heightReference) { + return ( + heightReference === HeightReference.RELATIVE_TO_GROUND || + heightReference === HeightReference.RELATIVE_TO_3D_TILE || + heightReference === HeightReference.RELATIVE_TO_TERRAIN + ); +} diff --git a/packages/engine/Source/Scene/LabelCollection.js b/packages/engine/Source/Scene/LabelCollection.js index 314db367fba3..627ba87862cd 100644 --- a/packages/engine/Source/Scene/LabelCollection.js +++ b/packages/engine/Source/Scene/LabelCollection.js @@ -10,7 +10,7 @@ import writeTextToCanvas from "../Core/writeTextToCanvas.js"; import bitmapSDF from "bitmap-sdf"; import BillboardCollection from "./BillboardCollection.js"; import BlendOption from "./BlendOption.js"; -import HeightReference from "./HeightReference.js"; +import { isHeightReferenceClamp } from "./HeightReference.js"; import HorizontalOrigin from "./HorizontalOrigin.js"; import Label from "./Label.js"; import LabelStyle from "./LabelStyle.js"; @@ -510,7 +510,7 @@ function repositionAllGlyphs(label) { ); } - if (label.heightReference === HeightReference.CLAMP_TO_GROUND) { + if (isHeightReferenceClamp(label.heightReference)) { for (glyphIndex = 0; glyphIndex < glyphLength; ++glyphIndex) { glyph = glyphs[glyphIndex]; const billboard = glyph.billboard; diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js index 33378231ccda..548e25f3478c 100644 --- a/packages/engine/Source/Scene/Model/Model.js +++ b/packages/engine/Source/Scene/Model/Model.js @@ -9,6 +9,7 @@ import defaultValue from "../../Core/defaultValue.js"; import DeveloperError from "../../Core/DeveloperError.js"; import destroyObject from "../../Core/destroyObject.js"; import DistanceDisplayCondition from "../../Core/DistanceDisplayCondition.js"; +import Ellipsoid from "../../Core/Ellipsoid.js"; import Event from "../../Core/Event.js"; import Matrix3 from "../../Core/Matrix3.js"; import Matrix4 from "../../Core/Matrix4.js"; @@ -18,7 +19,9 @@ import Pass from "../../Renderer/Pass.js"; import ClippingPlaneCollection from "../ClippingPlaneCollection.js"; import ColorBlendMode from "../ColorBlendMode.js"; import GltfLoader from "../GltfLoader.js"; -import HeightReference from "../HeightReference.js"; +import HeightReference, { + isHeightReferenceRelative, +} from "../HeightReference.js"; import ImageBasedLighting from "../ImageBasedLighting.js"; import PointCloudShading from "../PointCloudShading.js"; import SceneMode from "../SceneMode.js"; @@ -342,10 +345,9 @@ function Model(options) { const scene = options.scene; if (defined(scene) && defined(scene.terrainProviderChanged)) { this._terrainProviderChangedCallback = scene.terrainProviderChanged.addEventListener( - function () { + () => { this._heightDirty = true; - }, - this + } ); } this._scene = scene; @@ -2054,15 +2056,11 @@ function updateClamping(model) { } const scene = model._scene; - if ( - !defined(scene) || - !defined(scene.globe) || - model.heightReference === HeightReference.NONE - ) { + if (!defined(scene) || model.heightReference === HeightReference.NONE) { //>>includeStart('debug', pragmas.debug); if (model.heightReference !== HeightReference.NONE) { throw new DeveloperError( - "Height reference is not supported without a scene and globe." + "Height reference is not supported without a scene." ); } //>>includeEnd('debug'); @@ -2071,7 +2069,7 @@ function updateClamping(model) { } const globe = scene.globe; - const ellipsoid = globe.ellipsoid; + const ellipsoid = defaultValue(globe?.ellipsoid, Ellipsoid.WGS84); // Compute cartographic position so we don't recompute every update const modelMatrix = model.modelMatrix; @@ -2085,14 +2083,14 @@ function updateClamping(model) { } // Install callback to handle updating of terrain tiles - const surface = globe._surface; - model._removeUpdateHeightCallback = surface.updateHeight( + model._removeUpdateHeightCallback = scene.updateHeight( cartoPosition, - getUpdateHeightCallback(model, ellipsoid, cartoPosition) + getUpdateHeightCallback(model, ellipsoid, cartoPosition), + model.heightReference ); // Set the correct height now - const height = globe.getHeight(cartoPosition); + const height = scene.getHeight(cartoPosition, model.heightReference); if (defined(height)) { // Get callback with cartoPosition being the non-clamped position const callback = getUpdateHeightCallback(model, ellipsoid, cartoPosition); @@ -2100,8 +2098,7 @@ function updateClamping(model) { // Compute the clamped cartesian and call updateHeight callback Cartographic.clone(cartoPosition, scratchCartographic); scratchCartographic.height = height; - ellipsoid.cartographicToCartesian(scratchCartographic, scratchPosition); - callback(scratchPosition); + callback(scratchCartographic); } model._heightDirty = false; @@ -2358,24 +2355,25 @@ function scaleInPixels(positionWC, radius, frameState) { ); } -function getUpdateHeightCallback(model, ellipsoid, cartoPosition) { +const scratchUpdateHeightCartesian = new Cartesian3(); +function getUpdateHeightCallback(model, ellipsoid, originalPostition) { return function (clampedPosition) { - if (model.heightReference === HeightReference.RELATIVE_TO_GROUND) { - const clampedCart = ellipsoid.cartesianToCartographic( - clampedPosition, - scratchCartographic - ); - clampedCart.height += cartoPosition.height; - ellipsoid.cartographicToCartesian(clampedCart, clampedPosition); + if (isHeightReferenceRelative(model.heightReference)) { + clampedPosition.height += originalPostition.height; } + ellipsoid.cartographicToCartesian( + clampedPosition, + scratchUpdateHeightCartesian + ); + const clampedModelMatrix = model._clampedModelMatrix; // Modify clamped model matrix to use new height Matrix4.clone(model.modelMatrix, clampedModelMatrix); - clampedModelMatrix[12] = clampedPosition.x; - clampedModelMatrix[13] = clampedPosition.y; - clampedModelMatrix[14] = clampedPosition.z; + clampedModelMatrix[12] = scratchUpdateHeightCartesian.x; + clampedModelMatrix[13] = scratchUpdateHeightCartesian.y; + clampedModelMatrix[14] = scratchUpdateHeightCartesian.z; model._heightDirty = true; }; diff --git a/packages/engine/Source/Scene/QuadtreePrimitive.js b/packages/engine/Source/Scene/QuadtreePrimitive.js index 5b93f2cfad25..66ed2109ca30 100644 --- a/packages/engine/Source/Scene/QuadtreePrimitive.js +++ b/packages/engine/Source/Scene/QuadtreePrimitive.js @@ -274,7 +274,7 @@ QuadtreePrimitive.prototype.forEachRenderedTile = function (tileFunction) { * is the cartesian position on the tile. * * @param {Cartographic} cartographic The cartographic position. - * @param {Function} callback The function to be called when a new tile is loaded containing cartographic. + * @param {Function} callback The function to be called when a new tile is loaded containing the updated cartographic. * @returns {Function} The function to remove this callback from the quadtree. */ QuadtreePrimitive.prototype.updateHeight = function (cartographic, callback) { diff --git a/packages/engine/Source/Scene/Scene.js b/packages/engine/Source/Scene/Scene.js index 1d56d15d9333..17ed1cefaa5b 100644 --- a/packages/engine/Source/Scene/Scene.js +++ b/packages/engine/Source/Scene/Scene.js @@ -18,6 +18,7 @@ import Event from "../Core/Event.js"; import GeographicProjection from "../Core/GeographicProjection.js"; import GeometryInstance from "../Core/GeometryInstance.js"; import GeometryPipeline from "../Core/GeometryPipeline.js"; +import HeightReference from "./HeightReference.js"; import Intersect from "../Core/Intersect.js"; import JulianDate from "../Core/JulianDate.js"; import CesiumMath from "../Core/Math.js"; @@ -3614,25 +3615,57 @@ function callAfterRenderFunctions(scene) { } function getGlobeHeight(scene) { - const globe = scene._globe; + if (scene.mode === SceneMode.MORPHING) { + return; + } + const camera = scene.camera; const cartographic = camera.positionCartographic; + return scene.getHeight(cartographic); +} + +/** + * Gets the height of the loaded surface at the cartographic position. + * @param {Cartographic} cartographic The cartographic position. + * @param {HeightReference} [heightReference=CLAMP_TO_GROUND] Based on the height reference value, determines whether to ignore heights from 3D Tiles or terrain. + * @private + */ +Scene.prototype.getHeight = function (cartographic, heightReference) { + if (!defined(cartographic)) { + return undefined; + } + + const ignore3dTiles = + heightReference === HeightReference.CLAMP_TO_TERRAIN || + heightReference === HeightReference.RELATIVE_TO_TERRAIN; + + const ignoreTerrain = + heightReference === HeightReference.CLAMP_TO_3D_TILE || + heightReference === HeightReference.RELATIVE_TO_3D_TILE; let maxHeight = Number.NEGATIVE_INFINITY; - const length = scene.primitives.length; - for (let i = 0; i < length; ++i) { - const primitive = scene.primitives.get(i); - if (!primitive.isCesium3DTileset || primitive.disableCollision) { - continue; - } - const result = primitive.getHeight(cartographic, scene); - if (defined(result) && result > maxHeight) { - maxHeight = result; + if (!ignore3dTiles) { + const length = this.primitives.length; + for (let i = 0; i < length; ++i) { + const primitive = this.primitives.get(i); + if ( + !primitive.isCesium3DTileset || + !primitive.show || + primitive.disableCollision + ) { + continue; + } + + const result = primitive.getHeight(cartographic, this); + if (defined(result) && result > maxHeight) { + maxHeight = result; + } } } - if (defined(globe) && globe.show && defined(cartographic)) { + const globe = this._globe; + if (!ignoreTerrain && defined(globe) && globe.show) { const result = globe.getHeight(cartographic); if (result > maxHeight) { maxHeight = result; @@ -3644,7 +3677,108 @@ function getGlobeHeight(scene) { } return undefined; -} +}; + +const updateHeightScratchCartographic = new Cartographic(); +/** + * Calls the callback when a new tile is rendered that contains the given cartographic. The only parameter + * is the cartesian position on the tile. + * + * @private + * + * @param {Cartographic} cartographic The cartographic position. + * @param {Function} callback The function to be called when a new tile is loaded containing the updated cartographic. + * @param {HeightReference} [heightReference=CLAMP_TO_GROUND] Based on the height reference value, determines whether to ignore heights from 3D Tiles or terrain. + * @returns {Function} The function to remove this callback from the quadtree. + */ +Scene.prototype.updateHeight = function ( + cartographic, + callback, + heightReference +) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.func("callback", callback); + //>>includeEnd('debug'); + + const callbackWrapper = () => { + Cartographic.clone(cartographic, updateHeightScratchCartographic); + + const height = this.getHeight(cartographic, heightReference); + if (defined(height)) { + updateHeightScratchCartographic.height = height; + callback(updateHeightScratchCartographic); + } + }; + + const ignore3dTiles = + heightReference === HeightReference.CLAMP_TO_TERRAIN || + heightReference === HeightReference.RELATIVE_TO_TERRAIN; + + const ignoreTerrain = + heightReference === HeightReference.CLAMP_TO_3D_TILE || + heightReference === HeightReference.RELATIVE_TO_3D_TILE; + + let terrainRemoveCallback; + if (!ignoreTerrain && defined(this.globe)) { + terrainRemoveCallback = this.globe._surface.updateHeight( + cartographic, + callbackWrapper + ); + } + + let tilesetRemoveCallbacks = {}; + const ellipsoid = this.globe?.ellipsoid; + const createPrimitiveEventListener = (primitive) => { + if ( + ignore3dTiles || + !primitive.isCesium3DTileset || + primitive.disableCollision + ) { + return; + } + + const tilesetRemoveCallback = primitive.updateHeight( + cartographic, + callbackWrapper, + ellipsoid + ); + tilesetRemoveCallbacks[primitive.id] = tilesetRemoveCallback; + }; + + if (!ignore3dTiles) { + const length = this.primitives.length; + for (let i = 0; i < length; ++i) { + const primitive = this.primitives.get(i); + createPrimitiveEventListener(primitive); + } + } + + const removeAddedListener = this.primitives.primitiveAdded.addEventListener( + createPrimitiveEventListener + ); + const removeRemovedListener = this.primitives.primitiveRemoved.addEventListener( + (primitive) => { + if (!primitive.isCesium3DTileset) { + return; + } + + tilesetRemoveCallbacks[primitive.id](); + delete tilesetRemoveCallbacks[primitive.id]; + } + ); + + const removeCallback = () => { + terrainRemoveCallback = terrainRemoveCallback && terrainRemoveCallback(); + Object.values(tilesetRemoveCallbacks).forEach((tilesetRemoveCallback) => + tilesetRemoveCallback() + ); + tilesetRemoveCallbacks = {}; + removeAddedListener(); + removeRemovedListener(); + }; + + return removeCallback; +}; function isCameraUnderground(scene) { const camera = scene.camera; diff --git a/packages/engine/Source/Scene/TileBoundingS2Cell.js b/packages/engine/Source/Scene/TileBoundingS2Cell.js index 872529473f43..307fa36c8e39 100644 --- a/packages/engine/Source/Scene/TileBoundingS2Cell.js +++ b/packages/engine/Source/Scene/TileBoundingS2Cell.js @@ -330,7 +330,7 @@ Object.defineProperties(TileBoundingS2Cell.prototype, { /** * The underlying bounding volume. * - * @memberof TileOrientedBoundingBox.prototype + * @memberof TileBoundingS2Cell.prototype * * @type {object} * @readonly @@ -343,7 +343,7 @@ Object.defineProperties(TileBoundingS2Cell.prototype, { /** * The underlying bounding sphere. * - * @memberof TileOrientedBoundingBox.prototype + * @memberof TileBoundingS2Cell.prototype * * @type {BoundingSphere} * @readonly diff --git a/packages/engine/Specs/DataSources/ModelVisualizerSpec.js b/packages/engine/Specs/DataSources/ModelVisualizerSpec.js index 4c12504069d0..7232087d6cd0 100644 --- a/packages/engine/Specs/DataSources/ModelVisualizerSpec.js +++ b/packages/engine/Specs/DataSources/ModelVisualizerSpec.js @@ -13,6 +13,7 @@ import { Resource, Transforms, BoundingSphereState, + Cesium3DTileset, ConstantPositionProperty, ConstantProperty, EntityCollection, @@ -24,7 +25,6 @@ import { CustomShader, Globe, Cartographic, - createWorldTerrainAsync, } from "../../index.js"; import createScene from "../../../../Specs/createScene.js"; import pollToPromise from "../../../../Specs/pollToPromise.js"; @@ -53,6 +53,7 @@ describe( afterEach(function () { visualizer = visualizer && visualizer.destroy(); entityCollection.removeAll(); + scene.primitives.removeAll(); }); afterAll(function () { @@ -404,16 +405,18 @@ describe( expect(result).toEqual(expected); }); - it("computes bounding sphere with height reference clamp to ground", async function () { + it("computes bounding sphere with height reference clamp to terrain", async function () { // Setup a position for the model. const position = Cartesian3.fromDegrees(149.515332, -34.984799); - const positionCartographic = Cartographic.fromCartesian(position); + + const tileset = new Cesium3DTileset(); + scene.primitives.add(tileset); // Initialize the Entity and the ModelGraphics. const time = JulianDate.now(); const testObject = entityCollection.getOrCreateEntity("test"); const model = new ModelGraphics({ - heightReference: HeightReference.CLAMP_TO_GROUND, + heightReference: HeightReference.CLAMP_TO_TERRAIN, }); testObject.model = model; testObject.position = new ConstantProperty(position); @@ -426,61 +429,96 @@ describe( let state = visualizer.getBoundingSphere(testObject, result); expect(state).toBe(BoundingSphereState.PENDING); - // Assign a tiled terrain provider to the globe. - const globe = scene.globe; - globe.terrainProvider = await createWorldTerrainAsync(); + spyOn(scene.globe, "getHeight").and.returnValue(10.0); + spyOn(tileset, "getHeight").and.returnValue(20.0); + + // Repeatedly request the bounding sphere until it's ready. + await pollToPromise(function () { + scene.renderForSpecs(); + visualizer.update(time); + state = visualizer.getBoundingSphere(testObject, result); + return state !== BoundingSphereState.PENDING; + }); + + expect(state).toBe(BoundingSphereState.DONE); + + // Ensure that flags and results computed for this model are reset. + const modelData = visualizer._modelHash[testObject.id]; + expect(modelData.clampedBoundingSphere).toBeUndefined(); - const updatedCartographics = await ModelVisualizer._sampleTerrainMostDetailed( - globe.terrainProvider, - [positionCartographic] + const expectedCenter = Cartographic.fromCartesian(position); + expectedCenter.height = 10.0; + expect(result.center).toEqualEpsilon( + Cartographic.toCartesian(expectedCenter), + CesiumMath.EPSILON8 ); - const sampledResultCartographic = updatedCartographics[0]; - const sampledResult = globe.ellipsoid.cartographicToCartesian( - sampledResultCartographic + }); + + it("computes bounding sphere with height reference relative to terrain", async function () { + // Setup a position for the model. + const heightOffset = 1000.0; + const position = Cartesian3.fromDegrees( + 149.515332, + -34.984799, + heightOffset ); - const sampleTerrainSpy = spyOn( - ModelVisualizer, - "_sampleTerrainMostDetailed" - ).and.callThrough(); + const tileset = new Cesium3DTileset(); + scene.primitives.add(tileset); + + // Initialize the Entity and the ModelGraphics. + const time = JulianDate.now(); + const testObject = entityCollection.getOrCreateEntity("test"); + const model = new ModelGraphics({ + heightReference: HeightReference.RELATIVE_TO_TERRAIN, + }); + testObject.model = model; + testObject.position = new ConstantProperty(position); + model.uri = new ConstantProperty(boxUrl); + + visualizer.update(time); + + // Request the bounding sphere once. + const result = new BoundingSphere(); + let state = visualizer.getBoundingSphere(testObject, result); + expect(state).toBe(BoundingSphereState.PENDING); + + spyOn(scene.globe, "getHeight").and.returnValue(10.0); + spyOn(tileset, "getHeight").and.returnValue(20.0); // Repeatedly request the bounding sphere until it's ready. await pollToPromise(function () { - scene.render(); + scene.renderForSpecs(); visualizer.update(time); state = visualizer.getBoundingSphere(testObject, result); return state !== BoundingSphereState.PENDING; }); - expect(state).toBe(BoundingSphereState.DONE); // Ensure that flags and results computed for this model are reset. const modelData = visualizer._modelHash[testObject.id]; - expect(modelData.awaitingSampleTerrain).toBe(false); expect(modelData.clampedBoundingSphere).toBeUndefined(); - // Ensure that we only sample the terrain once from the visualizer. - expect(sampleTerrainSpy).toHaveBeenCalledTimes(1); - - // Calculate the distance of the bounding sphere returned from the position returned from sample terrain. - // Since sampleTerrainMostDetailed isn't always precise, we account for some error. - const distance = Cartesian3.distance(result.center, sampledResult); - const errorMargin = 100.0; - expect(distance).toBeLessThan(errorMargin); + const expectedCenter = Cartographic.fromCartesian(position); + expectedCenter.height = heightOffset + 10.0; + expect(result.center).toEqualEpsilon( + Cartographic.toCartesian(expectedCenter), + CesiumMath.EPSILON8 + ); }); - it("computes bounding sphere with height reference clamp to ground on terrain provider without availability", function () { + it("computes bounding sphere with height reference clamp to 3D Tiles", async function () { // Setup a position for the model. - const longitude = CesiumMath.toRadians(149.515332); - const latitude = CesiumMath.toRadians(-34.984799); - const height = 1000; - const position = Cartesian3.fromRadians(longitude, latitude, height); + const position = Cartesian3.fromDegrees(149.515332, -34.984799); + + const tileset = new Cesium3DTileset(); + scene.primitives.add(tileset); // Initialize the Entity and the ModelGraphics. const time = JulianDate.now(); const testObject = entityCollection.getOrCreateEntity("test"); const model = new ModelGraphics({ - heightReference: HeightReference.CLAMP_TO_GROUND, + heightReference: HeightReference.CLAMP_TO_3D_TILE, }); testObject.model = model; testObject.position = new ConstantProperty(position); @@ -493,36 +531,32 @@ describe( let state = visualizer.getBoundingSphere(testObject, result); expect(state).toBe(BoundingSphereState.PENDING); - // Ensure that the terrain provider does not have availability. - const globe = scene.globe; - const terrainProvider = globe.terrainProvider; - expect(terrainProvider.availability).toBe(undefined); + spyOn(scene.globe, "getHeight").and.returnValue(20.0); + spyOn(tileset, "getHeight").and.returnValue(10.0); // Repeatedly request the bounding sphere until it's ready. - return pollToPromise(function () { - scene.render(); + await pollToPromise(function () { + scene.renderForSpecs(); visualizer.update(time); state = visualizer.getBoundingSphere(testObject, result); return state !== BoundingSphereState.PENDING; - }).then(() => { - expect(state).toBe(BoundingSphereState.DONE); - // Ensure that the clamped position has height set to 0. - const cartographic = globe.ellipsoid.cartesianToCartographic( - result.center - ); - expect(cartographic.height).toEqualEpsilon(0, CesiumMath.EPSILON6); - expect(cartographic.latitude).toEqualEpsilon( - latitude, - CesiumMath.EPSILON6 - ); - expect(cartographic.longitude).toEqualEpsilon( - longitude, - CesiumMath.EPSILON6 - ); }); + + expect(state).toBe(BoundingSphereState.DONE); + + // Ensure that flags and results computed for this model are reset. + const modelData = visualizer._modelHash[testObject.id]; + expect(modelData.clampedBoundingSphere).toBeUndefined(); + + const expectedCenter = Cartographic.fromCartesian(position); + expectedCenter.height = 10.0; + expect(result.center).toEqualEpsilon( + Cartographic.toCartesian(expectedCenter), + CesiumMath.EPSILON8 + ); }); - it("computes bounding sphere with height reference relative to ground", async function () { + it("computes bounding sphere with height reference relative to 3D Tiles", async function () { // Setup a position for the model. const heightOffset = 1000.0; const position = Cartesian3.fromDegrees( @@ -530,19 +564,15 @@ describe( -34.984799, heightOffset ); - const positionCartographic = Cartographic.fromCartesian(position); - // Setup a spy so we can track how often sampleTerrain is called. - const sampleTerrainSpy = spyOn( - ModelVisualizer, - "_sampleTerrainMostDetailed" - ).and.callThrough(); + const tileset = new Cesium3DTileset(); + scene.primitives.add(tileset); // Initialize the Entity and the ModelGraphics. const time = JulianDate.now(); const testObject = entityCollection.getOrCreateEntity("test"); const model = new ModelGraphics({ - heightReference: HeightReference.RELATIVE_TO_GROUND, + heightReference: HeightReference.RELATIVE_TO_3D_TILE, }); testObject.model = model; testObject.position = new ConstantProperty(position); @@ -555,51 +585,90 @@ describe( let state = visualizer.getBoundingSphere(testObject, result); expect(state).toBe(BoundingSphereState.PENDING); - // Assign a tiled terrain provider to the globe. - const globe = scene.globe; - globe.terrainProvider = await createWorldTerrainAsync(); + spyOn(scene.globe, "getHeight").and.returnValue(20.0); + spyOn(tileset, "getHeight").and.returnValue(10.0); - const updatedCartographics = await ModelVisualizer._sampleTerrainMostDetailed( - globe.terrainProvider, - [positionCartographic] - ); - const sampledResultCartographic = updatedCartographics[0]; - const sampledResult = globe.ellipsoid.cartographicToCartesian( - sampledResultCartographic + // Repeatedly request the bounding sphere until it's ready. + await pollToPromise(function () { + scene.renderForSpecs(); + visualizer.update(time); + state = visualizer.getBoundingSphere(testObject, result); + return state !== BoundingSphereState.PENDING; + }); + expect(state).toBe(BoundingSphereState.DONE); + + // Ensure that flags and results computed for this model are reset. + const modelData = visualizer._modelHash[testObject.id]; + expect(modelData.clampedBoundingSphere).toBeUndefined(); + + const expectedCenter = Cartographic.fromCartesian(position); + expectedCenter.height = heightOffset + 10.0; + expect(result.center).toEqualEpsilon( + Cartographic.toCartesian(expectedCenter), + CesiumMath.EPSILON8 ); + }); + + it("computes bounding sphere with height reference clamp to ground", async function () { + // Setup a position for the model. + const position = Cartesian3.fromDegrees(149.515332, -34.984799); + + const tileset = new Cesium3DTileset(); + scene.primitives.add(tileset); + + // Initialize the Entity and the ModelGraphics. + const time = JulianDate.now(); + const testObject = entityCollection.getOrCreateEntity("test"); + const model = new ModelGraphics({ + heightReference: HeightReference.CLAMP_TO_GROUND, + }); + testObject.model = model; + testObject.position = new ConstantProperty(position); + model.uri = new ConstantProperty(boxUrl); + + visualizer.update(time); + + // Request the bounding sphere once. + const result = new BoundingSphere(); + let state = visualizer.getBoundingSphere(testObject, result); + expect(state).toBe(BoundingSphereState.PENDING); + + spyOn(scene.globe, "getHeight").and.returnValue(10.0); + spyOn(tileset, "getHeight").and.returnValue(20.0); // Repeatedly request the bounding sphere until it's ready. await pollToPromise(function () { - scene.render(); + scene.renderForSpecs(); visualizer.update(time); state = visualizer.getBoundingSphere(testObject, result); return state !== BoundingSphereState.PENDING; }); + expect(state).toBe(BoundingSphereState.DONE); // Ensure that flags and results computed for this model are reset. const modelData = visualizer._modelHash[testObject.id]; - expect(modelData.awaitingSampleTerrain).toBe(false); expect(modelData.clampedBoundingSphere).toBeUndefined(); - // Ensure that we only sample the terrain once from the visualizer. - // We check for 2 calls here because we call it once in the test. - expect(sampleTerrainSpy).toHaveBeenCalledTimes(2); - - // Calculate the distance of the bounding sphere returned from the position returned from sample terrain. - // Since sampleTerrainMostDetailed isn't always precise, we account for some error. - const distance = - Cartesian3.distance(result.center, sampledResult) - heightOffset; - const errorMargin = 100.0; - expect(distance).toBeLessThan(errorMargin); + const expectedCenter = Cartographic.fromCartesian(position); + expectedCenter.height = 20.0; + expect(result.center).toEqualEpsilon( + Cartographic.toCartesian(expectedCenter), + CesiumMath.EPSILON8 + ); }); - it("computes bounding sphere with height reference relative to ground on terrain provider without availability", function () { + it("computes bounding sphere with height reference relative to ground", async function () { // Setup a position for the model. - const longitude = CesiumMath.toRadians(149.515332); - const latitude = CesiumMath.toRadians(-34.984799); - const height = 1000; - const position = Cartesian3.fromRadians(longitude, latitude, height); + const heightOffset = 1000.0; + const position = Cartesian3.fromDegrees( + 149.515332, + -34.984799, + heightOffset + ); + + const tileset = new Cesium3DTileset(); + scene.primitives.add(tileset); // Initialize the Entity and the ModelGraphics. const time = JulianDate.now(); @@ -618,31 +687,28 @@ describe( let state = visualizer.getBoundingSphere(testObject, result); expect(state).toBe(BoundingSphereState.PENDING); - // Ensure that the terrain provider does not have availability. - const globe = scene.globe; - const terrainProvider = globe.terrainProvider; - expect(terrainProvider.availability).toBe(undefined); + spyOn(scene.globe, "getHeight").and.returnValue(10.0); + spyOn(tileset, "getHeight").and.returnValue(20.0); // Repeatedly request the bounding sphere until it's ready. - return pollToPromise(function () { - scene.render(); + await pollToPromise(function () { + scene.renderForSpecs(); visualizer.update(time); state = visualizer.getBoundingSphere(testObject, result); return state !== BoundingSphereState.PENDING; - }).then(() => { - const cartographic = globe.ellipsoid.cartesianToCartographic( - result.center - ); - expect(cartographic.height).toEqualEpsilon(height, CesiumMath.EPSILON6); - expect(cartographic.latitude).toEqualEpsilon( - latitude, - CesiumMath.EPSILON6 - ); - expect(cartographic.longitude).toEqualEpsilon( - longitude, - CesiumMath.EPSILON6 - ); }); + expect(state).toBe(BoundingSphereState.DONE); + + // Ensure that flags and results computed for this model are reset. + const modelData = visualizer._modelHash[testObject.id]; + expect(modelData.clampedBoundingSphere).toBeUndefined(); + + const expectedCenter = Cartographic.fromCartesian(position); + expectedCenter.height = heightOffset + 20.0; + expect(result.center).toEqualEpsilon( + Cartographic.toCartesian(expectedCenter), + CesiumMath.EPSILON8 + ); }); it("computes bounding sphere where globe is undefined", async function () { @@ -715,61 +781,6 @@ describe( expect(state).toBe(BoundingSphereState.FAILED); }); - it("fails bounding sphere when sampleTerrainMostDetailed fails", async function () { - // Setup a position for the model. - const heightOffset = 1000.0; - const position = Cartesian3.fromDegrees( - 149.515332, - -34.984799, - heightOffset - ); - - // Setup a spy so we can track how often sampleTerrain is called. - const sampleTerrainSpy = spyOn( - ModelVisualizer, - "_sampleTerrainMostDetailed" - ).and.callFake(() => { - return Promise.reject(404); - }); - - // Initialize the Entity and the ModelGraphics. - const time = JulianDate.now(); - const testObject = entityCollection.getOrCreateEntity("test"); - const model = new ModelGraphics({ - heightReference: HeightReference.RELATIVE_TO_GROUND, - }); - testObject.model = model; - testObject.position = new ConstantProperty(position); - model.uri = new ConstantProperty(boxUrl); - - visualizer.update(time); - - // Assign a tiled terrain provider to the globe. - const globe = scene.globe; - globe.terrainProvider = await createWorldTerrainAsync(); - - // Request the bounding sphere once. - const result = new BoundingSphere(); - let state; - - // Repeatedly request the bounding sphere until it's ready. - return pollToPromise(function () { - scene.render(); - visualizer.update(time); - state = visualizer.getBoundingSphere(testObject, result); - return state !== BoundingSphereState.PENDING; - }).then(() => { - expect(state).toBe(BoundingSphereState.FAILED); - - // Ensure that flags and results computed for this model are reset. - const modelData = visualizer._modelHash[testObject.id]; - expect(modelData.sampleTerrainFailed).toBe(false); - - // Ensure that we only sample the terrain once from the visualizer. - expect(sampleTerrainSpy).toHaveBeenCalledTimes(1); - }); - }); - it("compute bounding sphere throws without entity", function () { const result = new BoundingSphere(); expect(function () { diff --git a/packages/engine/Specs/Scene/BillboardCollectionSpec.js b/packages/engine/Specs/Scene/BillboardCollectionSpec.js index 529ff9049530..58b341e8fc7f 100644 --- a/packages/engine/Specs/Scene/BillboardCollectionSpec.js +++ b/packages/engine/Specs/Scene/BillboardCollectionSpec.js @@ -3,10 +3,12 @@ import { BoundingSphere, Cartesian2, Cartesian3, + Cartographic, CesiumTerrainProvider, Color, createGuid, DistanceDisplayCondition, + Globe, NearFarScalar, OrthographicOffCenterFrustum, PerspectiveFrustum, @@ -23,7 +25,6 @@ import { import { Math as CesiumMath } from "../../index.js"; -import createGlobe from "../../../../Specs/createGlobe.js"; import createScene from "../../../../Specs/createScene.js"; import pollToPromise from "../../../../Specs/pollToPromise.js"; @@ -2394,7 +2395,7 @@ describe( describe("height referenced billboards", function () { let billboardsWithHeight; beforeEach(function () { - scene.globe = createGlobe(); + scene.globe = new Globe(); billboardsWithHeight = new BillboardCollection({ scene: scene, }); @@ -2417,63 +2418,127 @@ describe( }); it("creating with a height reference creates a height update callback", function () { + spyOn(scene, "updateHeight"); + + const position = Cartesian3.fromDegrees(-72.0, 40.0); billboardsWithHeight.add({ heightReference: HeightReference.CLAMP_TO_GROUND, - position: Cartesian3.fromDegrees(-72.0, 40.0), + position: position, }); - expect(scene.globe.callback).toBeDefined(); + + expect(scene.updateHeight).toHaveBeenCalledWith( + Cartographic.fromCartesian(position), + jasmine.any(Function), + HeightReference.CLAMP_TO_GROUND + ); }); it("set height reference property creates a height update callback", function () { + spyOn(scene, "updateHeight"); + + const position = Cartesian3.fromDegrees(-72.0, 40.0); const b = billboardsWithHeight.add({ - position: Cartesian3.fromDegrees(-72.0, 40.0), + position: position, }); b.heightReference = HeightReference.CLAMP_TO_GROUND; - expect(scene.globe.callback).toBeDefined(); + + expect(scene.updateHeight).toHaveBeenCalledWith( + Cartographic.fromCartesian(position), + jasmine.any(Function), + HeightReference.CLAMP_TO_GROUND + ); }); it("updates the callback when the height reference changes", function () { + spyOn(scene, "updateHeight"); + + const position = Cartesian3.fromDegrees(-72.0, 40.0); const b = billboardsWithHeight.add({ heightReference: HeightReference.CLAMP_TO_GROUND, - position: Cartesian3.fromDegrees(-72.0, 40.0), + position: position, }); - expect(scene.globe.callback).toBeDefined(); + + expect(scene.updateHeight).toHaveBeenCalledWith( + Cartographic.fromCartesian(position), + jasmine.any(Function), + HeightReference.CLAMP_TO_GROUND + ); b.heightReference = HeightReference.RELATIVE_TO_GROUND; - expect(scene.globe.removedCallback).toEqual(true); - expect(scene.globe.callback).toBeDefined(); - scene.globe.removedCallback = false; + expect(scene.updateHeight).toHaveBeenCalledWith( + Cartographic.fromCartesian(position), + jasmine.any(Function), + HeightReference.RELATIVE_TO_GROUND + ); + }); + + it("removes the callback when the height reference changes", function () { + const removeCallback = jasmine.createSpy(); + spyOn(scene, "updateHeight").and.returnValue(removeCallback); + + const position = Cartesian3.fromDegrees(-72.0, 40.0); + const b = billboardsWithHeight.add({ + heightReference: HeightReference.CLAMP_TO_GROUND, + position: position, + }); + b.heightReference = HeightReference.NONE; - expect(scene.globe.removedCallback).toEqual(true); - expect(scene.globe.callback).toBeUndefined(); + expect(removeCallback).toHaveBeenCalled(); }); it("changing the position updates the callback", function () { + const removeCallback = jasmine.createSpy(); + spyOn(scene, "updateHeight").and.returnValue(removeCallback); + + let position = Cartesian3.fromDegrees(-72.0, 40.0); const b = billboardsWithHeight.add({ heightReference: HeightReference.CLAMP_TO_GROUND, - position: Cartesian3.fromDegrees(-72.0, 40.0), + position: position, }); - expect(scene.globe.callback).toBeDefined(); - b.position = Cartesian3.fromDegrees(-73.0, 40.0); - expect(scene.globe.removedCallback).toEqual(true); - expect(scene.globe.callback).toBeDefined(); + expect(scene.updateHeight).toHaveBeenCalledWith( + Cartographic.fromCartesian(position), + jasmine.any(Function), + HeightReference.CLAMP_TO_GROUND + ); + + position = b.position = Cartesian3.fromDegrees(-73.0, 40.0); + + expect(removeCallback).toHaveBeenCalled(); + expect(scene.updateHeight).toHaveBeenCalledWith( + Cartographic.fromCartesian(position), + jasmine.any(Function), + HeightReference.CLAMP_TO_GROUND + ); }); it("callback updates the position", function () { + let invokeCallback; + spyOn(scene, "updateHeight").and.callFake( + (cartographic, updateCallback) => { + invokeCallback = (height) => { + cartographic.height = height; + updateCallback(cartographic); + }; + } + ); + + const position = Cartesian3.fromDegrees(-72.0, 40.0); const b = billboardsWithHeight.add({ heightReference: HeightReference.CLAMP_TO_GROUND, - position: Cartesian3.fromDegrees(-72.0, 40.0), + position: position, }); - expect(scene.globe.callback).toBeDefined(); + + expect(scene.updateHeight).toHaveBeenCalled(); let cartographic = scene.globe.ellipsoid.cartesianToCartographic( b._clampedPosition ); expect(cartographic.height).toEqual(0.0); - scene.globe.callback(Cartesian3.fromDegrees(-72.0, 40.0, 100.0)); + invokeCallback(100.0); + cartographic = scene.globe.ellipsoid.cartesianToCartographic( b._clampedPosition ); @@ -2484,14 +2549,16 @@ describe( expect(b._clampedPosition).toBeUndefined(); }); - it("disableDepthTest after another function", function () { + it("removes callback after disableDepthTest", function () { + const removeCallback = jasmine.createSpy(); + spyOn(scene, "updateHeight").and.returnValue(removeCallback); + const b = billboardsWithHeight.add({ heightReference: HeightReference.CLAMP_TO_GROUND, position: Cartesian3.fromDegrees(-122, 46.0), disableDepthTestDistance: Number.POSITIVE_INFINITY, }); scene.renderForSpecs(); - expect(scene.globe.callback).toBeDefined(); expect(b._clampedPosition).toBeDefined(); //After changing disableDepthTestDistance and heightReference, the callback should be undefined @@ -2499,18 +2566,24 @@ describe( b.heightReference = HeightReference.NONE; scene.renderForSpecs(); - expect(scene.globe.callback).toBeUndefined(); expect(b._clampedPosition).toBeUndefined(); + expect(removeCallback).toHaveBeenCalled(); }); - it("changing the terrain provider", async function () { - const b = billboardsWithHeight.add({ + it("updates the callback when the terrain provider is changed", async function () { + const removeCallback = jasmine.createSpy(); + spyOn(scene, "updateHeight").and.returnValue(removeCallback); + + const position = Cartesian3.fromDegrees(-72.0, 40.0); + billboardsWithHeight.add({ heightReference: HeightReference.CLAMP_TO_GROUND, - position: Cartesian3.fromDegrees(-72.0, 40.0), + position: position, }); - expect(scene.globe.callback).toBeDefined(); - - spyOn(b, "_updateClamping").and.callThrough(); + expect(scene.updateHeight).toHaveBeenCalledWith( + Cartographic.fromCartesian(position), + jasmine.any(Function), + HeightReference.CLAMP_TO_GROUND + ); const terrainProvider = await CesiumTerrainProvider.fromUrl( "made/up/url", @@ -2521,7 +2594,12 @@ describe( scene.terrainProvider = terrainProvider; - expect(b._updateClamping).toHaveBeenCalled(); + expect(scene.updateHeight).toHaveBeenCalledWith( + Cartographic.fromCartesian(position), + jasmine.any(Function), + HeightReference.CLAMP_TO_GROUND + ); + expect(removeCallback).toHaveBeenCalled(); }); it("height reference without a scene rejects", function () { @@ -2543,15 +2621,17 @@ describe( }).toThrowDeveloperError(); }); - it("height reference without a globe rejects", function () { + it("height reference without a globe works", function () { scene.globe = undefined; expect(function () { - return billboardsWithHeight.add({ + billboardsWithHeight.add({ heightReference: HeightReference.CLAMP_TO_GROUND, position: Cartesian3.fromDegrees(-72.0, 40.0), }); - }).toThrowDeveloperError(); + + scene.renderForSpecs(); + }).not.toThrowError(); }); it("changing height reference without a globe throws DeveloperError", function () { @@ -2563,7 +2643,8 @@ describe( expect(function () { b.heightReference = HeightReference.CLAMP_TO_GROUND; - }).toThrowDeveloperError(); + scene.renderForSpecs(); + }).not.toThrowDeveloperError(); }); }); }, diff --git a/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js b/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js index 20849c2cd004..2fddd5b7a6cb 100644 --- a/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js +++ b/packages/engine/Specs/Scene/Cesium3DTilesetSpec.js @@ -2577,9 +2577,9 @@ describe( ); const expected = new Cartesian3( - 1215013.8353220497, - -4736316.763939952, - 4081608.4319443353 + 1215013.1035421563, + -4736313.911345786, + 4081605.96109977 ); expect(tileset.pick(ray, scene.frameState)).toEqualEpsilon( expected, diff --git a/packages/engine/Specs/Scene/LabelCollectionSpec.js b/packages/engine/Specs/Scene/LabelCollectionSpec.js index a4af501bbd44..70d96c24404e 100644 --- a/packages/engine/Specs/Scene/LabelCollectionSpec.js +++ b/packages/engine/Specs/Scene/LabelCollectionSpec.js @@ -3,6 +3,7 @@ import { BoundingSphere, Cartesian2, Cartesian3, + Cartographic, Color, defined, DistanceDisplayCondition, @@ -19,16 +20,12 @@ import { } from "../../index.js"; import { Math as CesiumMath } from "../../index.js"; - -import createGlobe from "../../../../Specs/createGlobe.js"; import createScene from "../../../../Specs/createScene.js"; import pollToPromise from "../../../../Specs/pollToPromise.js"; describe( "Scene/LabelCollection", function () { - // TODO: rendering tests for pixel offset, eye offset, horizontal origin, vertical origin, font, style, outlineColor, outlineWidth, and fillColor properties - let scene; let camera; let labels; @@ -2565,8 +2562,7 @@ describe( describe("height referenced labels", function () { beforeEach(function () { - scene.globe = createGlobe(); - + scene.globe = new Globe(); labelsWithHeight = new LabelCollection({ scene: scene, }); @@ -2574,7 +2570,6 @@ describe( }); it("explicitly constructs a label with height reference", function () { - scene.globe = createGlobe(); const l = labelsWithHeight.add({ text: "test", heightReference: HeightReference.CLAMP_TO_GROUND, @@ -2584,7 +2579,6 @@ describe( }); it("set label height reference property", function () { - scene.globe = createGlobe(); const l = labelsWithHeight.add({ text: "test", }); @@ -2594,68 +2588,127 @@ describe( }); it("creating with a height reference creates a height update callback", function () { - scene.globe = createGlobe(); + spyOn(scene, "updateHeight"); + + const position = Cartesian3.fromDegrees(-72.0, 40.0); labelsWithHeight.add({ heightReference: HeightReference.CLAMP_TO_GROUND, - position: Cartesian3.fromDegrees(-72.0, 40.0), + position: position, }); - expect(scene.globe.callback).toBeDefined(); + + expect(scene.updateHeight).toHaveBeenCalledWith( + Cartographic.fromCartesian(position), + jasmine.any(Function), + HeightReference.CLAMP_TO_GROUND + ); }); it("set height reference property creates a height update callback", function () { - scene.globe = createGlobe(); + spyOn(scene, "updateHeight"); + + const position = Cartesian3.fromDegrees(-72.0, 40.0); const l = labelsWithHeight.add({ - position: Cartesian3.fromDegrees(-72.0, 40.0), + position: position, }); l.heightReference = HeightReference.CLAMP_TO_GROUND; - expect(scene.globe.callback).toBeDefined(); + + expect(scene.updateHeight).toHaveBeenCalledWith( + Cartographic.fromCartesian(position), + jasmine.any(Function), + HeightReference.CLAMP_TO_GROUND + ); }); it("updates the callback when the height reference changes", function () { - scene.globe = createGlobe(); + spyOn(scene, "updateHeight"); + + const position = Cartesian3.fromDegrees(-72.0, 40.0); const l = labelsWithHeight.add({ heightReference: HeightReference.CLAMP_TO_GROUND, - position: Cartesian3.fromDegrees(-72.0, 40.0), + position: position, }); - expect(scene.globe.callback).toBeDefined(); + + expect(scene.updateHeight).toHaveBeenCalledWith( + Cartographic.fromCartesian(position), + jasmine.any(Function), + HeightReference.CLAMP_TO_GROUND + ); l.heightReference = HeightReference.RELATIVE_TO_GROUND; - expect(scene.globe.removedCallback).toEqual(true); - expect(scene.globe.callback).toBeDefined(); - scene.globe.removedCallback = false; + expect(scene.updateHeight).toHaveBeenCalledWith( + Cartographic.fromCartesian(position), + jasmine.any(Function), + HeightReference.RELATIVE_TO_GROUND + ); + }); + + it("removes the callback when the height reference changes", function () { + const removeCallback = jasmine.createSpy(); + spyOn(scene, "updateHeight").and.returnValue(removeCallback); + + const position = Cartesian3.fromDegrees(-72.0, 40.0); + const l = labelsWithHeight.add({ + heightReference: HeightReference.CLAMP_TO_GROUND, + position: position, + }); + l.heightReference = HeightReference.NONE; - expect(scene.globe.removedCallback).toEqual(true); - expect(scene.globe.callback).toBeUndefined(); + + expect(removeCallback).toHaveBeenCalled(); }); it("changing the position updates the callback", function () { - scene.globe = createGlobe(); + const removeCallback = jasmine.createSpy(); + spyOn(scene, "updateHeight").and.returnValue(removeCallback); + + let position = Cartesian3.fromDegrees(-72.0, 40.0); const l = labelsWithHeight.add({ heightReference: HeightReference.CLAMP_TO_GROUND, - position: Cartesian3.fromDegrees(-72.0, 40.0), + position: position, }); - expect(scene.globe.callback).toBeDefined(); - l.position = Cartesian3.fromDegrees(-73.0, 40.0); - expect(scene.globe.removedCallback).toEqual(true); - expect(scene.globe.callback).toBeDefined(); + expect(scene.updateHeight).toHaveBeenCalledWith( + Cartographic.fromCartesian(position), + jasmine.any(Function), + HeightReference.CLAMP_TO_GROUND + ); + + position = l.position = Cartesian3.fromDegrees(-73.0, 40.0); + + expect(removeCallback).toHaveBeenCalled(); + expect(scene.updateHeight).toHaveBeenCalledWith( + Cartographic.fromCartesian(position), + jasmine.any(Function), + HeightReference.CLAMP_TO_GROUND + ); }); it("callback updates the position", function () { - scene.globe = createGlobe(); + let invokeCallback; + spyOn(scene, "updateHeight").and.callFake( + (cartographic, updateCallback) => { + invokeCallback = (height) => { + cartographic.height = height; + updateCallback(cartographic); + }; + } + ); + + const position = Cartesian3.fromDegrees(-72.0, 40.0); const l = labelsWithHeight.add({ heightReference: HeightReference.CLAMP_TO_GROUND, - position: Cartesian3.fromDegrees(-72.0, 40.0), + position: position, }); - expect(scene.globe.callback).toBeDefined(); + expect(scene.updateHeight).toHaveBeenCalled(); let cartographic = scene.globe.ellipsoid.cartesianToCartographic( l._clampedPosition ); expect(cartographic.height).toEqual(0.0); - scene.globe.callback(Cartesian3.fromDegrees(-72.0, 40.0, 100.0)); + invokeCallback(100.0); + cartographic = scene.globe.ellipsoid.cartesianToCartographic( l._clampedPosition ); @@ -2663,7 +2716,6 @@ describe( }); it("resets the clamped position when HeightReference.NONE", function () { - scene.globe = createGlobe(); spyOn(scene.camera, "update"); const l = labelsWithHeight.add({ heightReference: HeightReference.CLAMP_TO_GROUND, @@ -2681,7 +2733,6 @@ describe( }); it("clears the billboard height reference callback when the label is removed", function () { - scene.globe = createGlobe(); spyOn(scene.camera, "update"); const l = labelsWithHeight.add({ heightReference: HeightReference.CLAMP_TO_GROUND, diff --git a/packages/engine/Specs/Scene/Model/ModelSpec.js b/packages/engine/Specs/Scene/Model/ModelSpec.js index 983c037b155e..b86bbd1df89e 100644 --- a/packages/engine/Specs/Scene/Model/ModelSpec.js +++ b/packages/engine/Specs/Scene/Model/ModelSpec.js @@ -19,8 +19,8 @@ import { DynamicAtmosphereLightingType, DracoLoader, Ellipsoid, - Event, FeatureDetection, + Globe, Fog, HeadingPitchRange, HeadingPitchRoll, @@ -2252,304 +2252,258 @@ describe( }); describe("height reference", function () { - let sceneWithMockGlobe; - - function createMockGlobe() { - const globe = { - callback: undefined, - removedCallback: false, - ellipsoid: Ellipsoid.WGS84, - update: function () {}, - render: function () {}, - getHeight: function () { - return 0.0; - }, - _surface: { - tileProvider: {}, - _tileLoadQueueHigh: [], - _tileLoadQueueMedium: [], - _tileLoadQueueLow: [], - _debug: { - tilesWaitingForChildren: 0, - }, - }, - imageryLayersUpdatedEvent: new Event(), - destroy: function () {}, - beginFrame: function () {}, - endFrame: function () {}, - terrainProviderChanged: new Event(), - }; - - Object.defineProperties(globe, { - terrainProvider: { - set: function (value) { - this.terrainProviderChanged.raiseEvent(value); - }, - }, - }); - - globe._surface.updateHeight = function (position, callback) { - globe.callback = callback; - return function () { - globe.removedCallback = true; - globe.callback = undefined; - }; - }; - - return globe; - } - - beforeAll(function () { - sceneWithMockGlobe = createScene(); - }); - - beforeEach(function () { - sceneWithMockGlobe.globe = createMockGlobe(); - }); - - afterEach(function () { - sceneWithMockGlobe.primitives.removeAll(); + beforeEach(() => { + scene.globe = new Globe(); }); - afterAll(function () { - sceneWithMockGlobe.destroyForSpecs(); + afterEach(() => { + scene.globe = undefined; }); - it("initializes with height reference", function () { - return loadAndZoomToModelAsync( + it("initializes with height reference", async function () { + const position = Cartesian3.fromDegrees(-72.0, 40.0); + const model = await loadAndZoomToModelAsync( { gltf: boxTexturedGltfUrl, heightReference: HeightReference.CLAMP_TO_GROUND, - scene: sceneWithMockGlobe, + modelMatrix: Transforms.eastNorthUpToFixedFrame(position), + scene: scene, }, - sceneWithMockGlobe - ).then(function (model) { - expect(model.heightReference).toEqual( - HeightReference.CLAMP_TO_GROUND - ); - expect(model._scene).toBe(sceneWithMockGlobe); - expect(model._clampedModelMatrix).toBeDefined(); - }); + scene + ); + expect(model.heightReference).toEqual(HeightReference.CLAMP_TO_GROUND); + expect(model._scene).toBe(scene); + expect(model._clampedModelMatrix).toBeDefined(); }); - it("changing height reference works", function () { - return loadAndZoomToModelAsync( + it("changing height reference works", async function () { + const position = Cartesian3.fromDegrees(-72.0, 40.0); + const model = await loadAndZoomToModelAsync( { gltf: boxTexturedGltfUrl, heightReference: HeightReference.NONE, - scene: sceneWithMockGlobe, + modelMatrix: Transforms.eastNorthUpToFixedFrame(position), + scene: scene, }, - sceneWithMockGlobe - ).then(function (model) { - expect(model.heightReference).toEqual(HeightReference.NONE); - expect(model._clampedModelMatrix).toBeUndefined(); + scene + ); + expect(model.heightReference).toEqual(HeightReference.NONE); + expect(model._clampedModelMatrix).toBeUndefined(); - model.heightReference = HeightReference.CLAMP_TO_GROUND; - expect(model._heightDirty).toBe(true); + model.heightReference = HeightReference.CLAMP_TO_GROUND; + expect(model._heightDirty).toBe(true); - sceneWithMockGlobe.renderForSpecs(); - expect(model._heightDirty).toBe(false); - expect(model.heightReference).toEqual( - HeightReference.CLAMP_TO_GROUND - ); - expect(model._clampedModelMatrix).toBeDefined(); - }); + scene.renderForSpecs(); + expect(model.heightReference).toEqual(HeightReference.CLAMP_TO_GROUND); + expect(model._clampedModelMatrix).toBeDefined(); }); - it("creates height update callback when initializing with height reference", function () { - return loadAndZoomToModelAsync( + it("creates height update callback when initializing with height reference", async function () { + spyOn(scene, "updateHeight"); + const position = Cartesian3.fromDegrees(-72.0, 40.0); + + const model = await loadAndZoomToModelAsync( { gltf: boxTexturedGltfUrl, - modelMatrix: Transforms.eastNorthUpToFixedFrame( - Cartesian3.fromDegrees(-72.0, 40.0) - ), + modelMatrix: Transforms.eastNorthUpToFixedFrame(position), heightReference: HeightReference.CLAMP_TO_GROUND, - scene: sceneWithMockGlobe, + scene: scene, }, - sceneWithMockGlobe - ).then(function (model) { - expect(model.heightReference).toEqual( - HeightReference.CLAMP_TO_GROUND - ); - expect(sceneWithMockGlobe.globe.callback).toBeDefined(); - }); + scene + ); + + expect(model.heightReference).toEqual(HeightReference.CLAMP_TO_GROUND); + expect(scene.updateHeight).toHaveBeenCalledWith( + Ellipsoid.WGS84.cartesianToCartographic(position), + jasmine.any(Function), + HeightReference.CLAMP_TO_GROUND + ); }); - it("creates height update callback after setting height reference", function () { - return loadAndZoomToModelAsync( + it("creates height update callback after setting height reference", async function () { + const removeCallback = jasmine.createSpy(); + spyOn(scene, "updateHeight").and.returnValue(removeCallback); + const position = Cartesian3.fromDegrees(-72.0, 40.0); + + const model = await loadAndZoomToModelAsync( { gltf: boxTexturedGltfUrl, - modelMatrix: Transforms.eastNorthUpToFixedFrame( - Cartesian3.fromDegrees(-72.0, 40.0) - ), + modelMatrix: Transforms.eastNorthUpToFixedFrame(position), heightReference: HeightReference.NONE, - scene: sceneWithMockGlobe, + scene: scene, }, - sceneWithMockGlobe - ).then(function (model) { - expect(model.heightReference).toEqual(HeightReference.NONE); - expect(sceneWithMockGlobe.globe.callback).toBeUndefined(); + scene + ); - model.heightReference = HeightReference.CLAMP_TO_GROUND; - expect(model.heightReference).toEqual( - HeightReference.CLAMP_TO_GROUND - ); - sceneWithMockGlobe.renderForSpecs(); - expect(sceneWithMockGlobe.globe.callback).toBeDefined(); - }); + model.heightReference = HeightReference.CLAMP_TO_GROUND; + expect(model.heightReference).toEqual(HeightReference.CLAMP_TO_GROUND); + + scene.renderForSpecs(); + expect(scene.updateHeight).toHaveBeenCalledWith( + Ellipsoid.WGS84.cartesianToCartographic(position), + jasmine.any(Function), + HeightReference.CLAMP_TO_GROUND + ); }); - it("updates height reference callback when the height reference changes", function () { - return loadAndZoomToModelAsync( + it("removes height update callback after changing height reference", async function () { + const removeCallback = jasmine.createSpy(); + spyOn(scene, "updateHeight").and.returnValue(removeCallback); + const position = Cartesian3.fromDegrees(-72.0, 40.0); + + const model = await loadAndZoomToModelAsync( { gltf: boxTexturedGltfUrl, - modelMatrix: Transforms.eastNorthUpToFixedFrame( - Cartesian3.fromDegrees(-72.0, 40.0) - ), + modelMatrix: Transforms.eastNorthUpToFixedFrame(position), heightReference: HeightReference.CLAMP_TO_GROUND, - scene: sceneWithMockGlobe, + scene: scene, }, - sceneWithMockGlobe - ).then(function (model) { - expect(sceneWithMockGlobe.globe.callback).toBeDefined(); + scene + ); - model.heightReference = HeightReference.RELATIVE_TO_GROUND; - sceneWithMockGlobe.renderForSpecs(); - expect(sceneWithMockGlobe.globe.removedCallback).toEqual(true); - expect(sceneWithMockGlobe.globe.callback).toBeDefined(); + model.heightReference = HeightReference.NONE; + expect(model.heightReference).toEqual(HeightReference.NONE); - sceneWithMockGlobe.globe.removedCallback = false; - model.heightReference = HeightReference.NONE; - sceneWithMockGlobe.renderForSpecs(); - expect(sceneWithMockGlobe.globe.removedCallback).toEqual(true); - expect(sceneWithMockGlobe.globe.callback).toBeUndefined(); - }); + scene.renderForSpecs(); + expect(removeCallback).toHaveBeenCalled(); }); - it("updates height reference callback when the model matrix changes", function () { - const modelMatrix = Transforms.eastNorthUpToFixedFrame( - Cartesian3.fromDegrees(-72.0, 40.0) - ); - return loadAndZoomToModelAsync( + it("updates height reference callback when the height reference changes", async function () { + const removeCallback = jasmine.createSpy(); + spyOn(scene, "updateHeight").and.returnValue(removeCallback); + const position = Cartesian3.fromDegrees(-72.0, 40.0); + + const model = await loadAndZoomToModelAsync( { gltf: boxTexturedGltfUrl, - modelMatrix: Matrix4.clone(modelMatrix), + modelMatrix: Transforms.eastNorthUpToFixedFrame(position), heightReference: HeightReference.CLAMP_TO_GROUND, - scene: sceneWithMockGlobe, + scene: scene, }, - sceneWithMockGlobe - ).then(function (model) { - expect(sceneWithMockGlobe.globe.callback).toBeDefined(); - - // Modify the model matrix in place - const position = Cartesian3.fromDegrees(-73.0, 40.0); - model.modelMatrix[12] = position.x; - model.modelMatrix[13] = position.y; - model.modelMatrix[14] = position.z; - - sceneWithMockGlobe.renderForSpecs(); - expect(sceneWithMockGlobe.globe.removedCallback).toEqual(true); - expect(sceneWithMockGlobe.globe.callback).toBeDefined(); + scene + ); + expect(scene.updateHeight).toHaveBeenCalledWith( + Ellipsoid.WGS84.cartesianToCartographic(position), + jasmine.any(Function), + HeightReference.CLAMP_TO_GROUND + ); - // Replace the model matrix entirely - model.modelMatrix = modelMatrix; + model.heightReference = HeightReference.RELATIVE_TO_GROUND; + scene.renderForSpecs(); - sceneWithMockGlobe.renderForSpecs(); - expect(sceneWithMockGlobe.globe.removedCallback).toEqual(true); - expect(sceneWithMockGlobe.globe.callback).toBeDefined(); - }); + expect(removeCallback).toHaveBeenCalled(); + expect(scene.updateHeight).toHaveBeenCalledWith( + Ellipsoid.WGS84.cartesianToCartographic(position), + jasmine.any(Function), + HeightReference.RELATIVE_TO_GROUND + ); }); - it("height reference callback updates the position", function () { - return loadAndZoomToModelAsync( + it("updates height reference callback when the model matrix changes", async function () { + const removeCallback = jasmine.createSpy(); + spyOn(scene, "updateHeight").and.returnValue(removeCallback); + + let position = Cartesian3.fromDegrees(-72.0, 40.0); + const modelMatrix = Transforms.eastNorthUpToFixedFrame(position); + const model = await loadAndZoomToModelAsync( { gltf: boxTexturedGltfUrl, - modelMatrix: Transforms.eastNorthUpToFixedFrame( - Cartesian3.fromDegrees(-72.0, 40.0) - ), + modelMatrix: Matrix4.clone(modelMatrix), heightReference: HeightReference.CLAMP_TO_GROUND, - scene: sceneWithMockGlobe, + scene: scene, }, - sceneWithMockGlobe - ).then(function (model) { - expect(sceneWithMockGlobe.globe.callback).toBeDefined(); + scene + ); + expect(scene.updateHeight).toHaveBeenCalledWith( + Ellipsoid.WGS84.cartesianToCartographic(position), + jasmine.any(Function), + HeightReference.CLAMP_TO_GROUND + ); - sceneWithMockGlobe.globe.callback( - Cartesian3.fromDegrees(-72.0, 40.0, 100.0) - ); - const matrix = model._clampedModelMatrix; - const position = new Cartesian3(matrix[12], matrix[13], matrix[14]); - const ellipsoid = sceneWithMockGlobe.globe.ellipsoid; - const cartographic = ellipsoid.cartesianToCartographic(position); - expect(cartographic.height).toEqualEpsilon( - 100.0, - CesiumMath.EPSILON9 - ); - }); + // Modify the model matrix in place + position = Cartesian3.fromDegrees(-73.0, 40.0); + model.modelMatrix[12] = position.x; + model.modelMatrix[13] = position.y; + model.modelMatrix[14] = position.z; + + scene.renderForSpecs(); + expect(removeCallback).toHaveBeenCalled(); + expect(scene.updateHeight).toHaveBeenCalledWith( + Ellipsoid.WGS84.cartesianToCartographic(position), + jasmine.any(Function), + HeightReference.CLAMP_TO_GROUND + ); }); - it("height reference accounts for change in terrain provider", function () { - return loadAndZoomToModelAsync( + it("updates height reference callback when the model matrix is set", async function () { + const removeCallback = jasmine.createSpy(); + spyOn(scene, "updateHeight").and.returnValue(removeCallback); + + let position = Cartesian3.fromDegrees(-72.0, 40.0); + const modelMatrix = Transforms.eastNorthUpToFixedFrame(position); + const model = await loadAndZoomToModelAsync( { gltf: boxTexturedGltfUrl, - modelMatrix: Transforms.eastNorthUpToFixedFrame( - Cartesian3.fromDegrees(-72.0, 40.0) - ), + modelMatrix: Matrix4.clone(modelMatrix), heightReference: HeightReference.CLAMP_TO_GROUND, - scene: sceneWithMockGlobe, + scene: scene, }, - sceneWithMockGlobe - ).then(function (model) { - expect(model._heightDirty).toBe(false); - const terrainProvider = new CesiumTerrainProvider({ - url: "made/up/url", - requestVertexNormals: true, - }); - sceneWithMockGlobe.terrainProvider = terrainProvider; + scene + ); + expect(scene.updateHeight).toHaveBeenCalledWith( + Ellipsoid.WGS84.cartesianToCartographic(position), + jasmine.any(Function), + HeightReference.CLAMP_TO_GROUND + ); - expect(model._heightDirty).toBe(true); - sceneWithMockGlobe.terrainProvider = undefined; - }); + position = Cartesian3.fromDegrees(-73.0, 40.0); + modelMatrix[12] = position.x; + modelMatrix[13] = position.y; + modelMatrix[14] = position.z; + model.modelMatrix = modelMatrix; + + scene.renderForSpecs(); + expect(removeCallback).toHaveBeenCalled(); + expect(scene.updateHeight).toHaveBeenCalledWith( + Ellipsoid.WGS84.cartesianToCartographic(position), + jasmine.any(Function), + HeightReference.CLAMP_TO_GROUND + ); }); - it("throws when initializing height reference with no scene", function () { - return loadAndZoomToModelAsync( + it("height reference callback updates the position", async function () { + let invokeCallback; + spyOn(scene, "updateHeight").and.callFake( + (cartographic, updateCallback) => { + invokeCallback = (height) => { + cartographic.height = height; + updateCallback(cartographic); + }; + } + ); + + const model = await loadAndZoomToModelAsync( { gltf: boxTexturedGltfUrl, modelMatrix: Transforms.eastNorthUpToFixedFrame( Cartesian3.fromDegrees(-72.0, 40.0) ), heightReference: HeightReference.CLAMP_TO_GROUND, - scene: undefined, + scene: scene, }, - sceneWithMockGlobe - ).catch(function (error) { - expect(error.message).toEqual( - "Height reference is not supported without a scene and globe." - ); - }); - }); + scene + ); - it("throws when changing height reference with no scene", function () { - return loadAndZoomToModelAsync( - { - gltf: boxTexturedGltfUrl, - modelMatrix: Transforms.eastNorthUpToFixedFrame( - Cartesian3.fromDegrees(-72.0, 40.0) - ), - heightReference: HeightReference.NONE, - }, - sceneWithMockGlobe - ).then(function (model) { - expect(function () { - model.heightReference = HeightReference.CLAMP_TO_GROUND; - sceneWithMockGlobe.renderForSpecs(); - }).toThrowDeveloperError(); - }); + invokeCallback(100.0); + + const matrix = model._clampedModelMatrix; + const position = new Cartesian3(matrix[12], matrix[13], matrix[14]); + const cartographic = Ellipsoid.WGS84.cartesianToCartographic(position); + expect(cartographic.height).toEqualEpsilon(100.0, CesiumMath.EPSILON9); }); - it("throws when initializing height reference with no globe", function () { - return loadAndZoomToModelAsync( + it("height reference accounts for change in terrain provider", async function () { + const model = await loadAndZoomToModelAsync( { gltf: boxTexturedGltfUrl, modelMatrix: Transforms.eastNorthUpToFixedFrame( @@ -2559,49 +2513,89 @@ describe( scene: scene, }, scene - ).catch(function (error) { - expect(error.message).toEqual( - "Height reference is not supported without a scene and globe." - ); + ); + expect(model._heightDirty).toBe(false); + const terrainProvider = new CesiumTerrainProvider({ + url: "made/up/url", + requestVertexNormals: true, }); + scene.terrainProvider = terrainProvider; + + expect(model._heightDirty).toBe(true); + scene.terrainProvider = undefined; }); - it("throws when changing height reference with no globe", function () { - return loadAndZoomToModelAsync( + it("throws when initializing height reference with no scene", async function () { + await expectAsync( + loadAndZoomToModelAsync( + { + gltf: boxTexturedGltfUrl, + modelMatrix: Transforms.eastNorthUpToFixedFrame( + Cartesian3.fromDegrees(-72.0, 40.0) + ), + heightReference: HeightReference.CLAMP_TO_GROUND, + scene: undefined, + }, + scene + ) + ).toBeRejectedWithDeveloperError( + "Height reference is not supported without a scene." + ); + }); + + it("throws when changing height reference with no scene", async function () { + const model = await loadAndZoomToModelAsync( { gltf: boxTexturedGltfUrl, modelMatrix: Transforms.eastNorthUpToFixedFrame( Cartesian3.fromDegrees(-72.0, 40.0) ), - scene: scene, + heightReference: HeightReference.NONE, }, scene - ).then(function (model) { - expect(function () { - model.heightReference = HeightReference.CLAMP_TO_GROUND; - scene.renderForSpecs(); - }).toThrowDeveloperError(); - }); + ); + + expect(function () { + model.heightReference = HeightReference.CLAMP_TO_GROUND; + scene.renderForSpecs(); + }).toThrowDeveloperError(); }); - it("destroys height reference callback", function () { - return loadAndZoomToModelAsync( + it("works when initializing height reference with no globe", function () { + return expectAsync( + loadAndZoomToModelAsync( + { + gltf: boxTexturedGltfUrl, + modelMatrix: Transforms.eastNorthUpToFixedFrame( + Cartesian3.fromDegrees(-72.0, 40.0) + ), + heightReference: HeightReference.CLAMP_TO_GROUND, + scene: scene, + }, + scene + ) + ).toBeResolved(); + }); + + it("destroys height reference callback", async function () { + const removeCallback = jasmine.createSpy(); + spyOn(scene, "updateHeight").and.returnValue(removeCallback); + + const model = await loadAndZoomToModelAsync( { gltf: boxTexturedGlbUrl, modelMatrix: Transforms.eastNorthUpToFixedFrame( Cartesian3.fromDegrees(-72.0, 40.0) ), heightReference: HeightReference.CLAMP_TO_GROUND, - scene: sceneWithMockGlobe, + scene: scene, }, - sceneWithMockGlobe - ).then(function (model) { - expect(sceneWithMockGlobe.globe.callback).toBeDefined(); + scene + ); - sceneWithMockGlobe.primitives.remove(model); - expect(model.isDestroyed()).toBe(true); - expect(sceneWithMockGlobe.globe.callback).toBeUndefined(); - }); + scene.primitives.remove(model); + expect(model.isDestroyed()).toBe(true); + expect(removeCallback).toHaveBeenCalled(); }); });