true if the polygon was removed; false if the polygon was not found in the collection.
*
* @see ClippingPolygonCollection#add
* @see ClippingPolygonCollection#contains
* @see ClippingPolygonCollection#removeAll
*/
ClippingPolygonCollection.prototype.remove = function (polygon) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("polygon", polygon);
//>>includeEnd('debug');
const polygons = this._polygons;
const index = polygons.findIndex((p) => ClippingPolygon.equals(p, polygon));
if (index === -1) {
return false;
}
polygons.splice(index, 1);
this.polygonRemoved.raiseEvent(polygon, index);
return true;
};
/**
* Computes padded extents for a polygon's bounding rectangle, clamped to valid spherical ranges.
*
* @param {Rectangle} extents The original spherical extents to pad.
* @param {number} padding A multiplier applied to the extents' width and height to determine the padding amount.
* @param {Rectangle} [result] An optional rectangle to store the result in.
* @returns {Rectangle} The padded and clamped rectangle.
*
* @private
*/
function computePaddedExtents(extents, padding, result) {
const height = Math.max(extents.height * padding, 0);
const width = Math.max(extents.width * padding, 0);
const paddedExtents = Rectangle.clone(extents, result);
// Pad
paddedExtents.south -= height;
paddedExtents.west -= width;
paddedExtents.north += height;
paddedExtents.east += width;
// Clamp
paddedExtents.south = Math.max(paddedExtents.south, -Math.PI);
paddedExtents.west = Math.max(paddedExtents.west, -Math.PI);
paddedExtents.north = Math.min(paddedExtents.north, Math.PI);
paddedExtents.east = Math.min(paddedExtents.east, Math.PI);
return paddedExtents;
}
/**
* @typedef {object} ExtentsResult
* @property {Rectangle[]} extentsList The list of merged padded extents, one per group.
* @property {Map* Do not call this function directly. *
* @private * @throws {RuntimeError} ClippingPolygonCollections are only supported for WebGL 2 */ ClippingPolygonCollection.prototype.update = function (frameState) { const context = frameState.context; if (!ClippingPolygonCollection.isSupported(frameState)) { throw new RuntimeError( "ClippingPolygonCollections are only supported for WebGL 2.", ); } if (this.debugShowDistanceTexture && defined(this._signedDistanceTexture)) { if (!defined(this.debugCommand)) { this.debugCommand = createDebugCommand( this._signedDistanceTexture, frameState.context, ); } frameState.commandList.push(this.debugCommand); } // It'd be expensive to validate any individual position has changed. Instead verify if the list of polygon positions has had elements added or removed, which should be good enough for most cases. const totalPositions = this._polygons.reduce( (totalPositions, polygon) => totalPositions + polygon.length, 0, ); if (totalPositions === this.totalPositions) { return; } this._totalPositions = totalPositions; // If there are no clipping polygons, there's nothing to update. if (this.length === 0) { return; } if (defined(this._signedDistanceComputeCommand)) { this._signedDistanceComputeCommand.canceled = true; this._signedDistanceComputeCommand = undefined; } let polygonsTexture = this._polygonsTexture; let extentsTexture = this._extentsTexture; let signedDistanceTexture = this._signedDistanceTexture; if (defined(polygonsTexture)) { const currentPixelCount = polygonsTexture.width * polygonsTexture.height; // Recreate the texture to double current requirement if it isn't big enough or is 4 times larger than it needs to be. // Optimization note: this isn't exactly the classic resizeable array algorithm // * not necessarily checking for resize after each add/remove operation // * random-access deletes instead of just pops // * alloc ops likely more expensive than demonstrable via big-O analysis if ( currentPixelCount < this.pixelsNeededForPolygonPositions || this.pixelsNeededForPolygonPositions < 0.25 * currentPixelCount ) { polygonsTexture.destroy(); polygonsTexture = undefined; this._polygonsTexture = undefined; } } if (!defined(polygonsTexture)) { const requiredResolution = ClippingPolygonCollection.getTextureResolution( polygonsTexture, this.pixelsNeededForPolygonPositions, textureResolutionScratch, ); polygonsTexture = new Texture({ context: context, width: requiredResolution.x, height: requiredResolution.y, pixelFormat: PixelFormat.RG, pixelDatatype: PixelDatatype.FLOAT, sampler: Sampler.NEAREST, flipY: false, }); this._float32View = new Float32Array( requiredResolution.x * requiredResolution.y * 2, ); this._polygonsTexture = polygonsTexture; } if (defined(extentsTexture)) { const currentPixelCount = extentsTexture.width * extentsTexture.height; // Recreate the texture to double current requirement if it isn't big enough or is 4 times larger than it needs to be. // Optimization note: this isn't exactly the classic resizeable array algorithm // * not necessarily checking for resize after each add/remove operation // * random-access deletes instead of just pops // * alloc ops likely more expensive than demonstrable via big-O analysis if ( currentPixelCount < this.pixelsNeededForExtents || this.pixelsNeededForExtents < 0.25 * currentPixelCount ) { extentsTexture.destroy(); extentsTexture = undefined; this._extentsTexture = undefined; } } if (!defined(extentsTexture)) { const requiredResolution = ClippingPolygonCollection.getTextureResolution( extentsTexture, this.pixelsNeededForExtents, textureResolutionScratch, ); extentsTexture = new Texture({ context: context, width: requiredResolution.x, height: requiredResolution.y, pixelFormat: PixelFormat.RGBA, pixelDatatype: PixelDatatype.FLOAT, sampler: Sampler.NEAREST, flipY: false, }); this._extentsFloat32View = new Float32Array( requiredResolution.x * requiredResolution.y * 4, ); this._extentsTexture = extentsTexture; } packPolygonsAsFloats(this); extentsTexture.copyFrom({ source: { width: extentsTexture.width, height: extentsTexture.height, arrayBufferView: this._extentsFloat32View, }, }); polygonsTexture.copyFrom({ source: { width: polygonsTexture.width, height: polygonsTexture.height, arrayBufferView: this._float32View, }, }); if (!defined(signedDistanceTexture)) { const textureDimensions = ClippingPolygonCollection.getClippingDistanceTextureResolution( this, textureResolutionScratch, ); signedDistanceTexture = new Texture({ context: context, width: textureDimensions.x, height: textureDimensions.y, pixelFormat: context.webgl2 ? PixelFormat.RED : PixelFormat.LUMINANCE, pixelDatatype: PixelDatatype.FLOAT, sampler: new Sampler({ wrapS: TextureWrap.CLAMP_TO_EDGE, wrapT: TextureWrap.CLAMP_TO_EDGE, minificationFilter: TextureMinificationFilter.LINEAR, magnificationFilter: TextureMagnificationFilter.LINEAR, }), flipY: false, }); this._signedDistanceTexture = signedDistanceTexture; } this._signedDistanceComputeCommand = createSignedDistanceTextureCommand(this); }; function createDebugCommand(texture, context) { const fs = "uniform highp sampler2D billboard_texture; \n" + "in vec2 v_textureCoordinates; \n" + "float getSignedDistance(vec2 uv, highp sampler2D clippingDistance) { \n" + " float signedDistance = texture(clippingDistance, uv).r; \n" + " return (signedDistance - 0.5) * 2.0; \n" + "} \n" + "void main() \n" + "{ \n" + " float dist = texture(billboard_texture, v_textureCoordinates).r; \n" + " if (dist > 0.5) { \n" + " out_FragColor = vec4(dist, 0.0, 0.0, 1.0); \n" + // outside " } else {\n" + " out_FragColor = vec4(0.0, dist, 0.0, 1.0); \n" + // inside " } \n" + "} \n"; const drawCommand = context.createViewportQuadCommand(fs, { uniformMap: { billboard_texture: function () { return texture; }, }, }); drawCommand.pass = Pass.OVERLAY; return drawCommand; } /** * Called when {@link Viewer} or {@link CesiumWidget} render the scene to * build the resources for clipping polygons. ** Do not call this function directly. *
* @private * @param {FrameState} frameState */ ClippingPolygonCollection.prototype.queueCommands = function (frameState) { if (defined(this._signedDistanceComputeCommand)) { frameState.commandList.push(this._signedDistanceComputeCommand); } }; function createSignedDistanceTextureCommand(collection) { const polygonTexture = collection._polygonsTexture; const extentsTexture = collection._extentsTexture; return new ComputeCommand({ fragmentShaderSource: PolygonSignedDistanceFS, outputTexture: collection._signedDistanceTexture, uniformMap: { u_polygonsLength: function () { return collection.length; }, u_extentsLength: function () { return collection.extentsCount; }, u_extentsTexture: function () { return extentsTexture; }, u_polygonTexture: function () { return polygonTexture; }, }, persists: false, owner: collection, postExecute: () => { collection._signedDistanceComputeCommand = undefined; }, }); } const scratchRectangleTile = new Rectangle(); const scratchRectangleIntersection = new Rectangle(); const scratchRectanglePolygon = new Rectangle(); /** * Determines the type intersection with the polygons of this ClippingPolygonCollection instance and the specified {@link TileBoundingVolume}. * @private * * @param {object} tileBoundingVolume The volume to determine the intersection with the polygons. * @param {Ellipsoid} [ellipsoid=Ellipsoid.default] The ellipsoid on which the bounding volumes are defined. * @returns {Intersect} The intersection type: {@link Intersect.OUTSIDE} if the entire volume is not clipped, {@link Intersect.INSIDE} * if the entire volume should be clipped, and {@link Intersect.INTERSECTING} if the volume intersects the polygons and will partially clipped. */ ClippingPolygonCollection.prototype.computeIntersectionWithBoundingVolume = function (tileBoundingVolume, ellipsoid) { const polygons = this._polygons; const length = polygons.length; let intersection = Intersect.OUTSIDE; if (this.inverse) { intersection = Intersect.INSIDE; } let tileBoundingRectangle = tileBoundingVolume.rectangle; if ( !defined(tileBoundingRectangle) && defined(tileBoundingVolume.boundingVolume?.computeCorners) ) { const points = tileBoundingVolume.boundingVolume.computeCorners(); tileBoundingRectangle = Rectangle.fromCartesianArray( points, ellipsoid, scratchRectangleTile, ); } if (!defined(tileBoundingRectangle)) { tileBoundingRectangle = Rectangle.fromBoundingSphere( tileBoundingVolume.boundingSphere, ellipsoid, scratchRectangleTile, ); } for (let i = 0; i < length; ++i) { const polygon = polygons[i]; const polygonBoundingRectangle = polygon.computeRectangle( scratchRectanglePolygon, ); const result = Rectangle.simpleIntersection( tileBoundingRectangle, polygonBoundingRectangle, scratchRectangleIntersection, ); if (defined(result)) { return Intersect.INTERSECTING; } } return intersection; }; /** * Sets the owner for the input ClippingPolygonCollection if there wasn't another owner. * Destroys the owner's previous ClippingPolygonCollection if setting is successful. * * @param {ClippingPolygonCollection} [clippingPolygonsCollection] A ClippingPolygonCollection (or undefined) being attached to an object * @param {object} owner An Object that should receive the new ClippingPolygonCollection * @param {string} key The Key for the Object to reference the ClippingPolygonCollection * @private */ ClippingPolygonCollection.setOwner = function ( clippingPolygonsCollection, owner, key, ) { // Don't destroy the ClippingPolygonCollection if it is already owned by newOwner if (clippingPolygonsCollection === owner[key]) { return; } // Destroy the existing ClippingPolygonCollection, if any owner[key] = owner[key] && owner[key].destroy(); if (defined(clippingPolygonsCollection)) { //>>includeStart('debug', pragmas.debug); if (defined(clippingPolygonsCollection._owner)) { throw new DeveloperError( "ClippingPolygonCollection should only be assigned to one object", ); } //>>includeEnd('debug'); clippingPolygonsCollection._owner = owner; owner[key] = clippingPolygonsCollection; } }; /** * Function for checking if the context will allow clipping polygons, which require floating point textures. * * @param {Scene|object} scene The scene that will contain clipped objects and clipping textures. * @returns {boolean}true if the context supports clipping polygons.
*/
ClippingPolygonCollection.isSupported = function (scene) {
return scene?.context.webgl2;
};
/**
* Function for getting packed texture resolution.
* If the ClippingPolygonCollection hasn't been updated, returns the resolution that will be
* allocated based on the provided needed pixels.
*
* @param {Texture} texture The texture to be packed.
* @param {number} pixelsNeeded The number of pixels needed based on the current polygon count.
* @param {Cartesian2} result A Cartesian2 for the result.
* @returns {Cartesian2} The required resolution.
* @private
*/
ClippingPolygonCollection.getTextureResolution = function (
texture,
pixelsNeeded,
result,
) {
if (defined(texture)) {
result.x = texture.width;
result.y = texture.height;
return result;
}
const maxSize = ContextLimits.maximumTextureSize;
result.x = Math.min(pixelsNeeded, maxSize);
result.y = Math.ceil(pixelsNeeded / result.x);
// Allocate twice as much space as needed to avoid frequent texture reallocation.
result.y *= 2;
return result;
};
/**
* Function for getting the clipping collection's signed distance texture resolution.
* If the ClippingPolygonCollection hasn't been updated, returns the resolution that will be
* allocated based on the current settings
*
* @param {ClippingPolygonCollection} clippingPolygonCollection The clipping polygon collection
* @param {Cartesian2} result A Cartesian2 for the result.
* @returns {Cartesian2} The required resolution.
* @private
*/
ClippingPolygonCollection.getClippingDistanceTextureResolution = function (
clippingPolygonCollection,
result,
) {
const texture = clippingPolygonCollection.signedDistanceTexture;
if (defined(texture)) {
result.x = texture.width;
result.y = texture.height;
return result;
}
const quality = clippingPolygonCollection.quality;
const baseSize = Math.max(128, Math.ceil(4096 * quality));
result.x = Math.min(ContextLimits.maximumTextureSize, baseSize);
result.y = Math.min(ContextLimits.maximumTextureSize, baseSize);
return result;
};
/**
* Function for getting the clipping collection's extents texture resolution.
* If the ClippingPolygonCollection hasn't been updated, returns the resolution that will be
* allocated based on the current polygon count.
*
* @param {ClippingPolygonCollection} clippingPolygonCollection The clipping polygon collection
* @param {Cartesian2} result A Cartesian2 for the result.
* @returns {Cartesian2} The required resolution.
* @private
*/
ClippingPolygonCollection.getClippingExtentsTextureResolution = function (
clippingPolygonCollection,
result,
) {
const texture = clippingPolygonCollection.extentsTexture;
if (defined(texture)) {
result.x = texture.width;
result.y = texture.height;
return result;
}
return ClippingPolygonCollection.getTextureResolution(
texture,
clippingPolygonCollection.pixelsNeededForExtents,
result,
);
};
/**
* Returns true if this object was destroyed; otherwise, false.
* isDestroyed will result in a {@link DeveloperError} exception.
*
* @returns {boolean} true if this object was destroyed; otherwise, false.
*
* @see ClippingPolygonCollection#destroy
*/
ClippingPolygonCollection.prototype.isDestroyed = function () {
return false;
};
/**
* Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
* release of WebGL resources, instead of relying on the garbage collector to destroy this object.
* isDestroyed will result in a {@link DeveloperError} exception. Therefore,
* assign the return value (undefined) to the object as done in the example.
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
*
* @example
* clippingPolygons = clippingPolygons && clippingPolygons.destroy();
*
* @see ClippingPolygonCollection#isDestroyed
*/
ClippingPolygonCollection.prototype.destroy = function () {
if (defined(this._signedDistanceComputeCommand)) {
this._signedDistanceComputeCommand.canceled = true;
}
this._polygonsTexture =
this._polygonsTexture && this._polygonsTexture.destroy();
this._extentsTexture = this._extentsTexture && this._extentsTexture.destroy();
this._signedDistanceTexture =
this._signedDistanceTexture && this._signedDistanceTexture.destroy();
return destroyObject(this);
};
export default ClippingPolygonCollection;