From 78d24406002a4c298b0ad883472708532ee28fbd Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 31 Jul 2025 15:06:24 +0200 Subject: [PATCH 1/7] Handle antimeridian for computing cartographic bounds --- .../Source/Scene/Model/ModelImageryMapping.js | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/engine/Source/Scene/Model/ModelImageryMapping.js b/packages/engine/Source/Scene/Model/ModelImageryMapping.js index 6d053cf6982e..1fe5d379e4d8 100644 --- a/packages/engine/Source/Scene/Model/ModelImageryMapping.js +++ b/packages/engine/Source/Scene/Model/ModelImageryMapping.js @@ -1,4 +1,5 @@ import defined from "../../Core/defined.js"; +import CesiumMath from "../../Core/Math.js"; import Cartesian2 from "../../Core/Cartesian2.js"; import Cartesian3 from "../../Core/Cartesian3.js"; import Matrix4 from "../../Core/Matrix4.js"; @@ -272,6 +273,9 @@ class ModelImageryMapping { * Computes the bounding rectangle of the given cartographic positions, * stores it in the given result, and returns it. * + * This is taken from Rectangle.fromCartographicArray, but + * operates on an iterable instead of an array. + * * If the given result is `undefined`, a new rectangle will be created * and returned. * @@ -287,17 +291,37 @@ class ModelImageryMapping { if (!defined(result)) { result = new Rectangle(); } - // One could store these directly in the result, but that would - // violate the constraint of the PI-related ranges.. let north = Number.NEGATIVE_INFINITY; let south = Number.POSITIVE_INFINITY; let east = Number.NEGATIVE_INFINITY; let west = Number.POSITIVE_INFINITY; + let westOverIDL = Number.POSITIVE_INFINITY; + let eastOverIDL = Number.NEGATIVE_INFINITY; + for (const cartographicPosition of cartographicPositions) { north = Math.max(north, cartographicPosition.latitude); south = Math.min(south, cartographicPosition.latitude); east = Math.max(east, cartographicPosition.longitude); west = Math.min(west, cartographicPosition.longitude); + + const lonAdjusted = + cartographicPosition.longitude >= 0 + ? cartographicPosition.longitude + : cartographicPosition.longitude + CesiumMath.TWO_PI; + westOverIDL = Math.min(westOverIDL, lonAdjusted); + eastOverIDL = Math.max(eastOverIDL, lonAdjusted); + } + + if (east - west > eastOverIDL - westOverIDL) { + west = westOverIDL; + east = eastOverIDL; + + if (east > CesiumMath.PI) { + east = east - CesiumMath.TWO_PI; + } + if (west > CesiumMath.PI) { + west = west - CesiumMath.TWO_PI; + } } result.north = north; result.south = south; From 267fe47dc7dbbe19f4b1fe8a2245eb65ec7eda59 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 31 Jul 2025 15:33:47 +0200 Subject: [PATCH 2/7] Basic wrapping of imagery range --- .../Source/Scene/Model/ImageryCoverage.js | 84 ++++++++++++++++--- 1 file changed, 71 insertions(+), 13 deletions(-) diff --git a/packages/engine/Source/Scene/Model/ImageryCoverage.js b/packages/engine/Source/Scene/Model/ImageryCoverage.js index 776371ba1a0e..e32d4fb98a0c 100644 --- a/packages/engine/Source/Scene/Model/ImageryCoverage.js +++ b/packages/engine/Source/Scene/Model/ImageryCoverage.js @@ -1,4 +1,5 @@ import defined from "../../Core/defined.js"; +import CesiumMath from "../../Core/Math.js"; import Rectangle from "../../Core/Rectangle.js"; import CartesianRectangle from "./CartesianRectangle.js"; @@ -7,7 +8,7 @@ const imageryBoundsScratch = new Rectangle(); const overlappedRectangleScratch = new Rectangle(); const clippedRectangleScratch = new Rectangle(); const nativeInputRectangleScratch = new Rectangle(); -const nativeImageryBoundsScratch = new Rectangle(); +//const nativeImageryBoundsScratch = new Rectangle(); XXX_DRAPING unused const nativeClippedImageryBoundsScratch = new Rectangle(); /** @@ -31,7 +32,8 @@ const nativeClippedImageryBoundsScratch = new Rectangle(); * ImageryLayer.prototype._createTileImagerySkeletons * See https://github.com/CesiumGS/cesium/blob/5eaa2280f495d8f300d9e1f0497118c97aec54c8/packages/engine/Source/Scene/ImageryLayer.js#L700 * An instance of this class roughly corresponds to the TileImagery - * that is created there. + * that is created there. Questions about the implementation here should + * be addressed to the original author. * * @private */ @@ -162,11 +164,15 @@ class ImageryCoverage { inputRectangle, nativeInputRectangle, ); + + // XXX_DRAPING Was unused? + /* const nativeImageryBounds = nativeImageryBoundsScratch; imageryTilingScheme.rectangleToNativeRectangle( imageryBounds, nativeImageryBounds, ); + */ // A function that returns an imagery rectangle, based on (x, y, level), // clipped to the imagery bounds (or undefined if there is no intersection @@ -236,7 +242,12 @@ class ImageryCoverage { * the X/Y coordinates of the imagery that is overlapped by the given * input rectangle, based on the given imagery rectangle. * - * Extracted from _createTileImagerySkeletons. + * This was largely extracted from _createTileImagerySkeletons. + * + * Note that the resulting rectangle will always obey the `minX<=maxX` + * constraint, but `maxX` may be larger than the number of tiles along + * x in the given imagery level. The receiver is responsible for + * wrapping the results into the valid range. * * @param {Rectangle} inputRectangle The input rectangle * @param {Rectangle} imageryBounds The imagery bounds @@ -254,12 +265,22 @@ class ImageryCoverage { inputRectangle, imageryBounds, ); + + // XXX_DRAPING: The wrapping that is happening here has to be + // verified. The "computeOverlappedRectangle" is doing some + // obscure "clamping" under certain conditions, as extracted + // from "_createTileImagerySkeletons", which may cause results + // that do not make sense here. + const nw = Rectangle.northwest(overlappedRectangle); + nw.longitude = CesiumMath.convertLongitudeRange(nw.longitude); const northwestTileCoordinates = imageryTilingScheme.positionToTileXY( - Rectangle.northwest(overlappedRectangle), + nw, imageryLevel, ); + const se = Rectangle.southeast(overlappedRectangle); + se.longitude = CesiumMath.convertLongitudeRange(se.longitude); const southeastTileCoordinates = imageryTilingScheme.positionToTileXY( - Rectangle.southeast(overlappedRectangle), + se, imageryLevel, ); @@ -269,6 +290,7 @@ class ImageryCoverage { result.maxX = southeastTileCoordinates.x; result.maxY = southeastTileCoordinates.y; + /*/ XXX_DRAPING We have to get rid of this... // As extracted from _createTileImagerySkeletons: // If the southeast corner of the rectangle lies very close to the north or west side // of the southeast tile, we don't actually need the southernmost or easternmost @@ -314,6 +336,24 @@ class ImageryCoverage { if (deltaEast < veryCloseX && result.maxX > result.minX) { --result.maxX; } + //*/ + + // For the case that the inputRectangle crosses the antimeridian, + // "west" (the westernmost side) may be 179° and "east" (the + // easternmost side) may be -179°. + // The "imageryTilingScheme.positionToTileXY" call returns coordinates + // that are wrapped into the valid range individually (!). This + // means that the tile x-coordinate for "west" (minX) may be larger + // than the tile x-coordinate for "east" (maxX). In this case, + // wrap the "maxX" around, based on the number of tiles along x. + if (result.minX > result.maxX) { + const numTilesX = + imageryTilingScheme.getNumberOfXTilesAtLevel(imageryLevel); + result.maxX += numTilesX; + } + + // XXX_DRAPING + console.log("ImageryCoverage._computeImageryRange: result ", result); return result; } @@ -383,6 +423,10 @@ class ImageryCoverage { * the texture coordinates that are contained in the given range of * imagery tile coordinates, referring to the given input rectangle. * + * The given imageryRange may contain x-coordinates that are larger than the + * number of tiles along x for the given imagery level. This method will + * wrap the coordinates to be in a valid range. + * * @param {ImageryLayer} imageryLayer The imagery layer * @param {CartesianRectangle} imageryRange The range of imagery tile coordinates * @param {number} imageryLevel The imagery level @@ -402,11 +446,25 @@ class ImageryCoverage { nativeInputRectangle, computeClippedImageryRectangle, ) { + const imageryProvider = imageryLayer.imageryProvider; + const imageryTilingScheme = imageryProvider.tilingScheme; + const numTilesX = + imageryTilingScheme.getNumberOfXTilesAtLevel(imageryLevel); + const imageryCoverages = []; - for (let i = imageryRange.minX; i <= imageryRange.maxX; i++) { + for (let rawX = imageryRange.minX; rawX <= imageryRange.maxX; rawX++) { + let x = rawX; + if (rawX >= numTilesX) { + x = rawX % numTilesX; + /// XXX_DRAPING Debug log + console.log( + `Wrapping ${rawX} to ${x} for ${numTilesX} tiles on level ${imageryLevel}`, + ); + } + const clippedImageryRectangleU = computeClippedImageryRectangle( - i, + x, imageryRange.maxY, imageryLevel, ); @@ -415,10 +473,10 @@ class ImageryCoverage { continue; } - for (let j = imageryRange.minY; j <= imageryRange.maxY; j++) { + for (let y = imageryRange.minY; y <= imageryRange.maxY; y++) { const clippedImageryRectangleV = computeClippedImageryRectangle( - i, - j, + x, + y, imageryLevel, ); @@ -439,10 +497,10 @@ class ImageryCoverage { // getImageryFromCacheAndCreateAllAncestorsAndAddReferences. // There is currently no way to have a single imagery, because // somewhere in TileImagery, the parent is assumed to be present. - const imagery = imageryLayer.getImageryFromCache(i, j, imageryLevel); + const imagery = imageryLayer.getImageryFromCache(x, y, imageryLevel); const imageryCoverage = new ImageryCoverage( - i, - j, + x, + y, imageryLevel, textureCoordinateRectangle, imagery, From 22cadc1af1d6cd5ad6d134bef9f9d9c33cce17f5 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 31 Jul 2025 15:44:41 +0200 Subject: [PATCH 3/7] Proper bounding rectangle from rectangle --- .../Source/Scene/Model/ModelImageryMapping.js | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/packages/engine/Source/Scene/Model/ModelImageryMapping.js b/packages/engine/Source/Scene/Model/ModelImageryMapping.js index 1fe5d379e4d8..810db869af53 100644 --- a/packages/engine/Source/Scene/Model/ModelImageryMapping.js +++ b/packages/engine/Source/Scene/Model/ModelImageryMapping.js @@ -13,6 +13,12 @@ import AttributeType from "../AttributeType.js"; import ModelReader from "./ModelReader.js"; import VertexAttributeSemantic from "../VertexAttributeSemantic.js"; +// Scratch variables for boundingRectangleFromRectangle +const boundingRectangleFromRectangleScratchCartographicSW = new Cartographic(); +const boundingRectangleFromRectangleScratchCartographicNE = new Cartographic(); +const boundingRectangleFromRectangleScratchProjectedSW = new Cartesian3(); +const boundingRectangleFromRectangleScratchProjectedNE = new Cartesian3(); + /** * A class for computing the texture coordinates of imagery that is * supposed to be mapped on a ModelComponents.Primitive. @@ -92,11 +98,24 @@ class ModelImageryMapping { // Convert the bounding `Rectangle`(!) of the cartographic positions // into a `BoundingRectangle`(!) using the given projection const boundingRectangle = new BoundingRectangle(); + /*/ + // XXX_DRAPING: BoundingRectangle.fromRectangle is broken, as + // it does not handle the antimeridian BoundingRectangle.fromRectangle( cartographicBoundingRectangle, projection, boundingRectangle, ); + //*/ + ModelImageryMapping.boundingRectangleFromRectangle( + cartographicBoundingRectangle, + projection, + boundingRectangle, + ); + console.log( + "ModelPrimitiveImagery._createTextureCoordinates: boundingRectangle with antimeridian wrapping", + boundingRectangle, + ); // Compute the projected positions, using the given projection const projectedPositions = ModelImageryMapping.createProjectedPositions( @@ -121,6 +140,51 @@ class ModelImageryMapping { return texCoordsTypedArray; } + /** + * Computes a bounding rectangle from a rectangle. + * + * This is similar to BoundingRectangle.fromRectangle, but + * does not make assumptions about whether the input rectangle is + * crossing the antimeridian or not. + * + * @param {Rectangle} rectangle The valid rectangle used to create a bounding rectangle. + * @param {object} projection The projection used to project the rectangle into 2D. + * @param {BoundingRectangle} [result] The object onto which to store the result. + * @returns {BoundingRectangle} The modified result parameter or a new BoundingRectangle instance if one was not provided. + */ + static boundingRectangleFromRectangle(rectangle, projection, result) { + if (!defined(result)) { + result = new BoundingRectangle(); + } + const sw = Rectangle.southwest( + rectangle, + boundingRectangleFromRectangleScratchCartographicSW, + ); + const ne = Rectangle.northeast( + rectangle, + boundingRectangleFromRectangleScratchCartographicNE, + ); + // XXX_DRAPING This is the only relevant difference to BoundingRectangle.fromRectangle, + // but subsequent processing steps may have to be adjusted to take this into account: + if (sw.longitude > ne.longitude) { + ne.longitude += CesiumMath.TWO_PI; + } + const projectedSW = projection.project( + sw, + boundingRectangleFromRectangleScratchProjectedSW, + ); + const projectedNE = projection.project( + ne, + boundingRectangleFromRectangleScratchProjectedNE, + ); + + result.x = projectedSW.x; + result.y = projectedSW.y; + result.width = projectedNE.x - projectedSW.x; + result.height = projectedNE.y - projectedSW.y; + return result; + } + /** * Creates the `ModelComponents.Attribute` for the texture coordinates * for a primitive From e4958a0ea1dbe7182c15ad0ecbde79f8e18e6931 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 31 Jul 2025 15:51:39 +0200 Subject: [PATCH 4/7] Wrapping that is likely necessary for projected positions --- .../Source/Scene/Model/ModelImageryMapping.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/engine/Source/Scene/Model/ModelImageryMapping.js b/packages/engine/Source/Scene/Model/ModelImageryMapping.js index 810db869af53..1ddd39a8f7da 100644 --- a/packages/engine/Source/Scene/Model/ModelImageryMapping.js +++ b/packages/engine/Source/Scene/Model/ModelImageryMapping.js @@ -117,10 +117,14 @@ class ModelImageryMapping { boundingRectangle, ); + const wrapped = + cartographicBoundingRectangle.west > cartographicBoundingRectangle.east; + // Compute the projected positions, using the given projection const projectedPositions = ModelImageryMapping.createProjectedPositions( cartographicPositions, projection, + wrapped, ); // Relativize the projected positions into the bounding rectangle @@ -459,18 +463,27 @@ class ModelImageryMapping { * @param {Iterable} cartographicPositions The cartographic * positions * @param {MapProjection} projection The projection to use + * @param {boolean} wrapped Whether the coordinates should be wrapped + * at the antimeridian. Negative longitude values will be brought + * into the positive range by adding 2*PI * @returns {Iterable} The projected positions */ - static createProjectedPositions(cartographicPositions, projection) { + static createProjectedPositions(cartographicPositions, projection, wrapped) { //>>includeStart('debug', pragmas.debug); Check.defined("cartographicPositions", cartographicPositions); Check.defined("projection", projection); //>>includeEnd('debug'); + const wrappedCartographic = new Cartographic(); const projectedPosition = new Cartesian3(); const projectedPositions = ModelImageryMapping.map( cartographicPositions, (c) => { + wrappedCartographic.latitude = c.latitude; + wrappedCartographic.longitude = c.longitude; + if (wrapped && wrappedCartographic.longitude < 0) { + wrappedCartographic.longitude += CesiumMath.TWO_PI; + } projection.project(c, projectedPosition); return projectedPosition; }, From 72e027626b964b666023f1c83fdb17e5c2e2cfd5 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 31 Jul 2025 16:02:17 +0200 Subject: [PATCH 5/7] Debug functions for rectangle --- packages/engine/Source/Core/Rectangle.js | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/engine/Source/Core/Rectangle.js b/packages/engine/Source/Core/Rectangle.js index eedaa97df6c5..46e4a589cc7f 100644 --- a/packages/engine/Source/Core/Rectangle.js +++ b/packages/engine/Source/Core/Rectangle.js @@ -86,6 +86,42 @@ Object.defineProperties(Rectangle.prototype, { */ Rectangle.packedLength = 4; +/** + * XXX_DRAPING + * + * Print the message and the rectangle for debugging, assuming that it stores values + * that are given in radians, and converting them to degrees for printing. + * + * Note that a rectangle may contain arbitrary values in arbitrary units (degrees, meters, + * furlongs, who knows). If the printed values do not make sense, try debugPrintDirectly. + * + * @param {string} message The message + * @param {Rectangle} rectangle The rectangle + */ +Rectangle.debugPrintRadiansAsDegrees = function (message, rectangle) { + console.log(message, " (converted from radians to degrees)"); + console.log(" E ", CesiumMath.toDegrees(rectangle.east)); + console.log(" S ", CesiumMath.toDegrees(rectangle.south)); + console.log(" W ", CesiumMath.toDegrees(rectangle.west)); + console.log(" N ", CesiumMath.toDegrees(rectangle.north)); +}; +/** + * XXX_DRAPING + * + * Print the message and the rectangle for debugging, directly, without assuming + * that the input is actually storing values in radians. + * + * @param {string} message The message + * @param {Rectangle} rectangle The rectangle + */ +Rectangle.debugPrintDirectly = function (message, rectangle) { + console.log(message, " (printed directly)"); + console.log(" W ", rectangle.west); + console.log(" S ", rectangle.south); + console.log(" E ", rectangle.east); + console.log(" N ", rectangle.north); +}; + /** * Stores the provided instance into the provided array. * From ee5dcdd85d1ff17d376ca1da4c863418230d812c Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 31 Jul 2025 16:07:02 +0200 Subject: [PATCH 6/7] Add DRAFT texture coordinate computation function --- .../Source/Scene/Model/ImageryCoverage.js | 110 ++++++++++++++- .../Specs/Scene/Model/ImageryCoverageSpec.js | 128 ++++++++++++++++++ 2 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 packages/engine/Specs/Scene/Model/ImageryCoverageSpec.js diff --git a/packages/engine/Source/Scene/Model/ImageryCoverage.js b/packages/engine/Source/Scene/Model/ImageryCoverage.js index e32d4fb98a0c..a3f67bb47186 100644 --- a/packages/engine/Source/Scene/Model/ImageryCoverage.js +++ b/packages/engine/Source/Scene/Model/ImageryCoverage.js @@ -459,7 +459,7 @@ class ImageryCoverage { x = rawX % numTilesX; /// XXX_DRAPING Debug log console.log( - `Wrapping ${rawX} to ${x} for ${numTilesX} tiles on level ${imageryLevel}`, + `ImageryCoverage._computeImageryCoverages: Wrapping ${rawX} to ${x} for ${numTilesX} tiles on level ${imageryLevel}`, ); } @@ -491,6 +491,20 @@ class ImageryCoverage { undefined, ); + // XXX_DRAPING Debug log... + Rectangle.debugPrintDirectly( + "ImageryCoverage._computeImageryCoverages: clippedImageryRectangleV", + clippedImageryRectangleV, + ); + Rectangle.debugPrintDirectly( + "ImageryCoverage._computeImageryCoverages: nativeInputRectangle", + nativeInputRectangle, + ); + console.log( + "ImageryCoverage._computeImageryCoverages: textureCoordinateRectangle", + textureCoordinateRectangle, + ); + // Note: The getImageryFromCache function will create the whole "chain" // of ancestor imageries, up to the root, and increases the reference // counter for each of them, even though it is not called @@ -512,6 +526,10 @@ class ImageryCoverage { } /** + * XXX_DRAPING This function does not work for rectangles that have + * been converted to the "native" representation, because + * Rectangle.computeWidth is broken for these rectangles + * * Compute the coordinates of the first rectangle relative to the * second rectangle. * @@ -541,6 +559,96 @@ class ImageryCoverage { result.maxY = (rectangleA.north - rectangleB.south) * invY; return result; } + + /** + * XXX_DRAPING This function should replace _localizeToCartesianRectangle, + * but operates on Rectangles that are proper Cartographic rectangles, + * which is often not the case for the imagery-related computations that + * have been extracted from _createTileImagerySkeletons + * + * Compute the coordinates of the first rectangle relative to the + * second rectangle. + * + * The result will describe the bounds of the first rectangle + * in coordinates that are relative to the (west, south) and + * (width, height) of the second rectangle, wrapping the + * longitude at the antimeridian. This is suitable for + * describing the texture coordinates of the first + * rectangle within the second one. + * + * The result will be stored in the given result parameter, or + * in a new rectangle if the result was undefined. + * + * @param {Rectangle} rectangleA The first rectangle + * @param {Rectangle} rectangleB The second rectangle + * @param {CartesianRectangle} [result] The result + * @returns {CartesianRectangle} The result + */ + static _localizeCartographicRectanglesToCartesianRectangle( + rectangleA, + rectangleB, + result, + ) { + if (!defined(result)) { + result = new CartesianRectangle(); + } + const invX = 1.0 / rectangleB.width; + const invY = 1.0 / rectangleB.height; + + const wa = CesiumMath.zeroToTwoPi(rectangleA.west); + const ea = CesiumMath.zeroToTwoPi(rectangleA.east); + const wb = CesiumMath.zeroToTwoPi(rectangleB.west); + + const rawMinX = ImageryCoverage.wrappedDifference( + wa, + wb, + CesiumMath.TWO_PI, + ); + const rawMaxX = ImageryCoverage.wrappedDifference( + ea, + wb, + CesiumMath.TWO_PI, + ); + + result.minX = rawMinX * invX; + result.minY = (rectangleA.south - rectangleB.south) * invY; + result.maxX = rawMaxX * invX; + result.maxY = (rectangleA.north - rectangleB.south) * invY; + return result; + } + + /** + * Computes the difference between the given values, wrapped to + * the given wrapping value. + * + * The values will be brought into the range [0, wrap]. The + * result will be the signed (!) difference between both values, + * considering the wrapping of the values. + * + * For example: + * wrappedDifference(0.9, 0.7, 1.0) = 0.2 + * wrappedDifference(0.7, 0.9, 1.0) = -0.2 + * wrappedDifference(1.1, 0.9, 1.0) = 0.2 + * wrappedDifference(0.9, 1.1, 1.0) = -0.2 + * + * @param {number} a The first value + * @param {number} b The second value + * @param {number} wrap The wrapping value + * @returns The wrapped difference + */ + static wrappedDifference(a, b, wrap) { + const wrappedA = (a %= wrap); + const wrappedB = (b %= wrap); + const diff = wrappedA - wrappedB; + const absDiff = Math.abs(diff); + if (absDiff < wrap - absDiff) { + return diff; + } + if (diff < 0) { + return diff + wrap; + } + return diff - wrap; + } } export default ImageryCoverage; diff --git a/packages/engine/Specs/Scene/Model/ImageryCoverageSpec.js b/packages/engine/Specs/Scene/Model/ImageryCoverageSpec.js new file mode 100644 index 000000000000..5dd6139b1332 --- /dev/null +++ b/packages/engine/Specs/Scene/Model/ImageryCoverageSpec.js @@ -0,0 +1,128 @@ +import { + Rectangle, + ImageryCoverage, + CartesianRectangle, + Math as CesiumMath, +} from "../../../index.js"; + +describe("Scene/Model/ImageryCoverage", function () { + it("_localizeCartographicRectanglesToCartesianRectangle returns unit rectangle for equal inputs", async function () { + const ra = Rectangle.fromDegrees(10, 10, 20, 20); + const rb = Rectangle.fromDegrees(10, 10, 20, 20); + const actual = new CartesianRectangle(); + const expected = new CartesianRectangle(0, 0, 1, 1); + ImageryCoverage._localizeCartographicRectanglesToCartesianRectangle( + ra, + rb, + actual, + ); + expect(actual).toEqualEpsilon(expected, CesiumMath.EPSILON8); + }); + + it("_localizeCartographicRectanglesToCartesianRectangle computes offset for overlapping inputs", async function () { + const ra = Rectangle.fromDegrees(15, 15, 25, 25); + const rb = Rectangle.fromDegrees(10, 10, 20, 20); + const actual = new CartesianRectangle(); + const expected = new CartesianRectangle(0.5, 0.5, 1.5, 1.5); + ImageryCoverage._localizeCartographicRectanglesToCartesianRectangle( + ra, + rb, + actual, + ); + expect(actual).toEqualEpsilon(expected, CesiumMath.EPSILON8); + }); + + it("_localizeCartographicRectanglesToCartesianRectangle computes offset for non-overlapping inputs", async function () { + const ra = Rectangle.fromDegrees(30, 30, 50, 50); + const rb = Rectangle.fromDegrees(10, 10, 20, 20); + const actual = new CartesianRectangle(); + const expected = new CartesianRectangle(2, 2, 4, 4); + ImageryCoverage._localizeCartographicRectanglesToCartesianRectangle( + ra, + rb, + actual, + ); + expect(actual).toEqualEpsilon(expected, CesiumMath.EPSILON8); + }); + + it("_localizeCartographicRectanglesToCartesianRectangle works when inner rectangle is left of antimeridian", async function () { + const ra = Rectangle.fromDegrees(160, 10, 170, 50); + const rb = Rectangle.fromDegrees(155, 10, -155, 50); + const actual = new CartesianRectangle(); + const expected = new CartesianRectangle(0.1, 0, 0.3, 1); + ImageryCoverage._localizeCartographicRectanglesToCartesianRectangle( + ra, + rb, + actual, + ); + expect(actual).toEqualEpsilon(expected, CesiumMath.EPSILON8); + }); + + it("_localizeCartographicRectanglesToCartesianRectangle works when inner rectangle is right of antimeridian", async function () { + const ra = Rectangle.fromDegrees(-175, 10, -165, 50); + const rb = Rectangle.fromDegrees(155, 10, -155, 50); + const actual = new CartesianRectangle(); + const expected = new CartesianRectangle(0.6, 0, 0.8, 1); + ImageryCoverage._localizeCartographicRectanglesToCartesianRectangle( + ra, + rb, + actual, + ); + expect(actual).toEqualEpsilon(expected, CesiumMath.EPSILON8); + }); + + it("_localizeCartographicRectanglesToCartesianRectangle works when inner rectangle crosses antimeridian", async function () { + const ra = Rectangle.fromDegrees(175, 10, -175, 50); + const rb = Rectangle.fromDegrees(155, 10, -155, 50); + const actual = new CartesianRectangle(); + const expected = new CartesianRectangle(0.4, 0, 0.6, 1); + + console.log(actual); + + ImageryCoverage._localizeCartographicRectanglesToCartesianRectangle( + ra, + rb, + actual, + ); + expect(actual).toEqualEpsilon(expected, CesiumMath.EPSILON8); + }); + + it("_localizeCartographicRectanglesToCartesianRectangle works when inner is left and outer is right of antimeridian", async function () { + const ra = Rectangle.fromDegrees(-175, 10, -165, 50); + const rb = Rectangle.fromDegrees(165, 10, 175, 50); + const actual = new CartesianRectangle(); + const expected = new CartesianRectangle(2, 0, 3, 1); + ImageryCoverage._localizeCartographicRectanglesToCartesianRectangle( + ra, + rb, + actual, + ); + expect(actual).toEqualEpsilon(expected, CesiumMath.EPSILON8); + }); + + it("_localizeCartographicRectanglesToCartesianRectangle works when inner is right and outer is left or antimeridian", async function () { + const ra = Rectangle.fromDegrees(165, 10, 175, 50); + const rb = Rectangle.fromDegrees(-175, 10, -165, 50); + const actual = new CartesianRectangle(); + const expected = new CartesianRectangle(-2, 0, -1, 1); + ImageryCoverage._localizeCartographicRectanglesToCartesianRectangle( + ra, + rb, + actual, + ); + expect(actual).toEqualEpsilon(expected, CesiumMath.EPSILON8); + }); + + it("_localizeCartographicRectanglesToCartesianRectangle works when inner is left or meridian and outer is right of meridian", async function () { + const ra = Rectangle.fromDegrees(-20, 10, -10, 20); + const rb = Rectangle.fromDegrees(10, 10, 20, 20); + const actual = new CartesianRectangle(); + const expected = new CartesianRectangle(-3, 0, -2, 1); + ImageryCoverage._localizeCartographicRectanglesToCartesianRectangle( + ra, + rb, + actual, + ); + expect(actual).toEqualEpsilon(expected, CesiumMath.EPSILON8); + }); +}); From 327827445da06be1ab780559af7d4d0d573e1958 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 31 Jul 2025 16:07:39 +0200 Subject: [PATCH 7/7] Use fixed level for debugging. Add TODO comment. --- .../engine/Source/Scene/Model/ImageryPipelineStage.js | 3 +++ .../engine/Source/Scene/Model/ModelPrimitiveImagery.js | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/engine/Source/Scene/Model/ImageryPipelineStage.js b/packages/engine/Source/Scene/Model/ImageryPipelineStage.js index 29ee3baf16fd..e6114f9265be 100644 --- a/packages/engine/Source/Scene/Model/ImageryPipelineStage.js +++ b/packages/engine/Source/Scene/Model/ImageryPipelineStage.js @@ -924,6 +924,9 @@ class ImageryPipelineStage { } /** + * XXX_DRAPING: This should essentially do the same as _localizeCartographicRectanglesToCartesianRectangle, + * but whether or not this should (or has to) operate on the so-called "native" rectangles has to be checked. + * * Compute the translation and scale that has to be applied to * the texture coordinates for mapping the given imagery to * the geometry. diff --git a/packages/engine/Source/Scene/Model/ModelPrimitiveImagery.js b/packages/engine/Source/Scene/Model/ModelPrimitiveImagery.js index 6f088609a9d9..4d447b85485d 100644 --- a/packages/engine/Source/Scene/Model/ModelPrimitiveImagery.js +++ b/packages/engine/Source/Scene/Model/ModelPrimitiveImagery.js @@ -707,10 +707,16 @@ class ModelPrimitiveImagery { ); // Clamp the level to a valid range, and an integer value - const imageryLevel = ImageryCoverage._clampImageryLevel( + let imageryLevel = ImageryCoverage._clampImageryLevel( imageryProvider, desiredLevel, ); + // XXX_DRAPING Using fixed imagery level for debugging + imageryLevel = -1; // Comment this out to use the fixed level + if (imageryLevel < 0) { + imageryLevel = 12; + console.log(`XXX_DRAPING: Using fixed imagery level ${imageryLevel}`); + } return imageryLevel; }