Add existing to tracked

This commit is contained in:
Jay
2026-08-11 09:53:42 -04:00
parent afe07f3055
commit ffd6e3d73c
8531 changed files with 4396230 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
// @ts-check
/**
* The alpha rendering mode of the material.
*
* @enum {string}
* @private
*/
const AlphaMode = {
/**
* The alpha value is ignored and the rendered output is fully opaque.
*
* @type {string}
* @constant
*/
OPAQUE: "OPAQUE",
/**
* The rendered output is either fully opaque or fully transparent depending on the alpha value and the specified alpha cutoff value.
*
* @type {string}
* @constant
*/
MASK: "MASK",
/**
* The rendered output is composited onto the destination with alpha blending.
*
* @type {string}
* @constant
*/
BLEND: "BLEND",
};
Object.freeze(AlphaMode);
export default AlphaMode;
+204
View File
@@ -0,0 +1,204 @@
import clone from "../Core/clone.js";
import combine from "../Core/combine.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import BlendingState from "./BlendingState.js";
import CullFace from "./CullFace.js";
/**
* An appearance defines the full GLSL vertex and fragment shaders and the
* render state used to draw a {@link Primitive}. All appearances implement
* this base <code>Appearance</code> interface.
*
* @alias Appearance
* @constructor
*
* @param {object} [options] Object with the following properties:
* @param {boolean} [options.translucent=true] When <code>true</code>, the geometry is expected to appear translucent so {@link Appearance#renderState} has alpha blending enabled.
* @param {boolean} [options.closed=false] When <code>true</code>, the geometry is expected to be closed so {@link Appearance#renderState} has backface culling enabled.
* @param {Material} [options.material=Material.ColorType] The material used to determine the fragment color.
* @param {string} [options.vertexShaderSource] Optional GLSL vertex shader source to override the default vertex shader.
* @param {string} [options.fragmentShaderSource] Optional GLSL fragment shader source to override the default fragment shader.
* @param {object} [options.renderState] Optional render state to override the default render state.
*
* @see MaterialAppearance
* @see EllipsoidSurfaceAppearance
* @see PerInstanceColorAppearance
* @see DebugAppearance
* @see PolylineColorAppearance
* @see PolylineMaterialAppearance
*
* @demo {@link https://sandcastle.cesium.com/index.html?id=geometry-and-appearances|Geometry and Appearances Demo}
*/
function Appearance(options) {
options = options ?? Frozen.EMPTY_OBJECT;
/**
* The material used to determine the fragment color. Unlike other {@link Appearance}
* properties, this is not read-only, so an appearance's material can change on the fly.
*
* @type Material
*
* @see {@link https://github.com/CesiumGS/cesium/wiki/Fabric|Fabric}
*/
this.material = options.material;
/**
* When <code>true</code>, the geometry is expected to appear translucent.
*
* @type {boolean}
*
* @default true
*/
this.translucent = options.translucent ?? true;
this._vertexShaderSource = options.vertexShaderSource;
this._fragmentShaderSource = options.fragmentShaderSource;
this._renderState = options.renderState;
this._closed = options.closed ?? false;
}
Object.defineProperties(Appearance.prototype, {
/**
* The GLSL source code for the vertex shader.
*
* @memberof Appearance.prototype
*
* @type {string}
* @readonly
*/
vertexShaderSource: {
get: function () {
return this._vertexShaderSource;
},
},
/**
* The GLSL source code for the fragment shader. The full fragment shader
* source is built procedurally taking into account the {@link Appearance#material}.
* Use {@link Appearance#getFragmentShaderSource} to get the full source.
*
* @memberof Appearance.prototype
*
* @type {string}
* @readonly
*/
fragmentShaderSource: {
get: function () {
return this._fragmentShaderSource;
},
},
/**
* The WebGL fixed-function state to use when rendering the geometry.
*
* @memberof Appearance.prototype
*
* @type {object}
* @readonly
*/
renderState: {
get: function () {
return this._renderState;
},
},
/**
* When <code>true</code>, the geometry is expected to be closed.
*
* @memberof Appearance.prototype
*
* @type {boolean}
* @readonly
*
* @default false
*/
closed: {
get: function () {
return this._closed;
},
},
});
/**
* Procedurally creates the full GLSL fragment shader source for this appearance
* taking into account {@link Appearance#fragmentShaderSource} and {@link Appearance#material}.
*
* @returns {string} The full GLSL fragment shader source.
*/
Appearance.prototype.getFragmentShaderSource = function () {
const parts = [];
if (this.flat) {
parts.push("#define FLAT");
}
if (this.faceForward) {
parts.push("#define FACE_FORWARD");
}
if (defined(this.material)) {
parts.push(this.material.shaderSource);
}
parts.push(this.fragmentShaderSource);
return parts.join("\n");
};
/**
* Determines if the geometry is translucent based on {@link Appearance#translucent} and {@link Material#isTranslucent}.
*
* @returns {boolean} <code>true</code> if the appearance is translucent.
*/
Appearance.prototype.isTranslucent = function () {
return (
(defined(this.material) && this.material.isTranslucent()) ||
(!defined(this.material) && this.translucent)
);
};
/**
* Creates a render state. This is not the final render state instance; instead,
* it can contain a subset of render state properties identical to the render state
* created in the context.
*
* @returns {object} The render state.
*/
Appearance.prototype.getRenderState = function () {
const translucent = this.isTranslucent();
const rs = clone(this.renderState, false);
if (translucent) {
rs.depthMask = false;
rs.blending = BlendingState.ALPHA_BLEND;
} else {
rs.depthMask = true;
}
return rs;
};
/**
* @private
*/
Appearance.getDefaultRenderState = function (translucent, closed, existing) {
let rs = {
depthTest: {
enabled: true,
},
};
if (translucent) {
rs.depthMask = false;
rs.blending = BlendingState.ALPHA_BLEND;
}
if (closed) {
rs.cull = {
enabled: true,
face: CullFace.BACK,
};
}
if (defined(existing)) {
rs = combine(existing, rs, true);
}
return rs;
};
export default Appearance;
+17
View File
@@ -0,0 +1,17 @@
// @ts-check
/**
* ArcGisBaseMapType enumerates the ArcGIS image tile layers that are supported by default.
*
* @enum {number}
* @see ArcGisMapServerImageryProvider
*/
const ArcGisBaseMapType = {
SATELLITE: 1,
OCEANS: 2,
HILLSHADE: 3,
};
Object.freeze(ArcGisBaseMapType);
export default ArcGisBaseMapType;
@@ -0,0 +1,870 @@
import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Cartographic from "../Core/Cartographic.js";
import Check from "../Core/Check.js";
import Credit from "../Core/Credit.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import Event from "../Core/Event.js";
import GeographicProjection from "../Core/GeographicProjection.js";
import GeographicTilingScheme from "../Core/GeographicTilingScheme.js";
import CesiumMath from "../Core/Math.js";
import Rectangle from "../Core/Rectangle.js";
import Resource from "../Core/Resource.js";
import RuntimeError from "../Core/RuntimeError.js";
import WebMercatorProjection from "../Core/WebMercatorProjection.js";
import WebMercatorTilingScheme from "../Core/WebMercatorTilingScheme.js";
import ArcGisMapService from "./ArcGisMapService.js";
import DiscardMissingTileImagePolicy from "./DiscardMissingTileImagePolicy.js";
import ImageryLayerFeatureInfo from "./ImageryLayerFeatureInfo.js";
import ImageryProvider from "./ImageryProvider.js";
import ArcGisBaseMapType from "./ArcGisBaseMapType.js";
import DeveloperError from "../Core/DeveloperError.js";
/**
* @typedef {object} ArcGisMapServerImageryProvider.ConstructorOptions
*
* Initialization options for the ArcGisMapServerImageryProvider constructor
*
* @property {TileDiscardPolicy} [tileDiscardPolicy] The policy that determines if a tile
* is invalid and should be discarded. If this value is not specified, a default
* {@link DiscardMissingTileImagePolicy} is used for tiled map servers, and a
* {@link NeverTileDiscardPolicy} is used for non-tiled map servers. In the former case,
* we request tile 0,0 at the maximum tile level and check pixels (0,0), (200,20), (20,200),
* (80,110), and (160, 130). If all of these pixels are transparent, the discard check is
* disabled and no tiles are discarded. If any of them have a non-transparent color, any
* tile that has the same values in these pixel locations is discarded. The end result of
* these defaults should be correct tile discarding for a standard ArcGIS Server. To ensure
* that no tiles are discarded, construct and pass a {@link NeverTileDiscardPolicy} for this
* parameter.
* @property {boolean} [usePreCachedTilesIfAvailable=true] If true, the server's pre-cached
* tiles are used if they are available. Exporting Tiles is only supported with deprecated APIs.
* @property {string} [layers] A comma-separated list of the layers to show, or undefined if all layers should be shown.
* @property {boolean} [enablePickFeatures=true] If true, {@link ArcGisMapServerImageryProvider#pickFeatures} will invoke
* the Identify service on the MapServer and return the features included in the response. If false,
* {@link ArcGisMapServerImageryProvider#pickFeatures} will immediately return undefined (indicating no pickable features)
* without communicating with the server. Set this property to false if you don't want this provider's features to
* be pickable. Can be overridden by setting the {@link ArcGisMapServerImageryProvider#enablePickFeatures} property on the object.
* @property {Rectangle} [rectangle=Rectangle.MAX_VALUE] The rectangle of the layer. This parameter is ignored when accessing
* a tiled layer.
* @property {TilingScheme} [tilingScheme=new GeographicTilingScheme()] The tiling scheme to use to divide the world into tiles.
* This parameter is ignored when accessing a tiled server.
* @property {Ellipsoid} [ellipsoid=Ellipsoid.default] The ellipsoid. If the tilingScheme is specified and used,
* this parameter is ignored and the tiling scheme's ellipsoid is used instead. If neither
* parameter is specified, the default ellipsoid is used.
* @property {Credit|string} [credit] A credit for the data source, which is displayed on the canvas. This parameter is ignored when accessing a tiled server.
* @property {number} [tileWidth=256] The width of each tile in pixels. This parameter is ignored when accessing a tiled server.
* @property {number} [tileHeight=256] The height of each tile in pixels. This parameter is ignored when accessing a tiled server.
* @property {number} [maximumLevel] The maximum tile level to request, or undefined if there is no maximum. This parameter is ignored when accessing
* a tiled server.
*
*
*/
/**
* Used to track creation details while fetching initial metadata
*
* @constructor
* @private
*
* @param {ArcGisMapServerImageryProvider.ConstructorOptions} options An object describing initialization options
*/
function ImageryProviderBuilder(options) {
this.useTiles = options.usePreCachedTilesIfAvailable ?? true;
const ellipsoid = options.ellipsoid;
this.tilingScheme =
options.tilingScheme ??
new GeographicTilingScheme({ ellipsoid: ellipsoid });
this.rectangle = options.rectangle ?? this.tilingScheme.rectangle;
this.ellipsoid = ellipsoid;
let credit = options.credit;
if (typeof credit === "string") {
credit = new Credit(credit);
}
this.credit = credit;
this.tileCredits = undefined;
this.tileDiscardPolicy = options.tileDiscardPolicy;
this.tileWidth = options.tileWidth ?? 256;
this.tileHeight = options.tileHeight ?? 256;
this.maximumLevel = options.maximumLevel;
}
/**
* Complete ArcGisMapServerImageryProvider creation based on builder values.
*
* @private
*
* @param {ArcGisMapServerImageryProvider} provider
*/
ImageryProviderBuilder.prototype.build = function (provider) {
provider._useTiles = this.useTiles;
provider._tilingScheme = this.tilingScheme;
provider._rectangle = this.rectangle;
provider._credit = this.credit;
provider._tileCredits = this.tileCredits;
provider._tileDiscardPolicy = this.tileDiscardPolicy;
provider._tileWidth = this.tileWidth;
provider._tileHeight = this.tileHeight;
provider._maximumLevel = this.maximumLevel;
// Install the default tile discard policy if none has been supplied.
if (this.useTiles && !defined(this.tileDiscardPolicy)) {
provider._tileDiscardPolicy = new DiscardMissingTileImagePolicy({
missingImageUrl: buildImageResource(provider, 0, 0, this.maximumLevel)
.url,
pixelsToCheck: [
new Cartesian2(0, 0),
new Cartesian2(200, 20),
new Cartesian2(20, 200),
new Cartesian2(80, 110),
new Cartesian2(160, 130),
],
disableCheckIfAllPixelsAreTransparent: true,
});
}
};
function metadataSuccess(data, imageryProviderBuilder) {
const tileInfo = data.tileInfo;
if (!defined(tileInfo)) {
imageryProviderBuilder.useTiles = false;
} else {
imageryProviderBuilder.tileWidth = tileInfo.rows;
imageryProviderBuilder.tileHeight = tileInfo.cols;
if (
tileInfo.spatialReference.wkid === 102100 ||
tileInfo.spatialReference.wkid === 102113
) {
imageryProviderBuilder.tilingScheme = new WebMercatorTilingScheme({
ellipsoid: imageryProviderBuilder.ellipsoid,
});
} else if (data.tileInfo.spatialReference.wkid === 4326) {
imageryProviderBuilder.tilingScheme = new GeographicTilingScheme({
ellipsoid: imageryProviderBuilder.ellipsoid,
});
} else {
const message = `Tile spatial reference WKID ${data.tileInfo.spatialReference.wkid} is not supported.`;
throw new RuntimeError(message);
}
imageryProviderBuilder.maximumLevel = data.tileInfo.lods.length - 1;
if (defined(data.fullExtent)) {
if (
defined(data.fullExtent.spatialReference) &&
defined(data.fullExtent.spatialReference.wkid)
) {
if (
data.fullExtent.spatialReference.wkid === 102100 ||
data.fullExtent.spatialReference.wkid === 102113
) {
const projection = new WebMercatorProjection();
const extent = data.fullExtent;
const sw = projection.unproject(
new Cartesian3(
Math.max(
extent.xmin,
-imageryProviderBuilder.tilingScheme.ellipsoid.maximumRadius *
Math.PI,
),
Math.max(
extent.ymin,
-imageryProviderBuilder.tilingScheme.ellipsoid.maximumRadius *
Math.PI,
),
0.0,
),
);
const ne = projection.unproject(
new Cartesian3(
Math.min(
extent.xmax,
imageryProviderBuilder.tilingScheme.ellipsoid.maximumRadius *
Math.PI,
),
Math.min(
extent.ymax,
imageryProviderBuilder.tilingScheme.ellipsoid.maximumRadius *
Math.PI,
),
0.0,
),
);
imageryProviderBuilder.rectangle = new Rectangle(
sw.longitude,
sw.latitude,
ne.longitude,
ne.latitude,
);
} else if (data.fullExtent.spatialReference.wkid === 4326) {
imageryProviderBuilder.rectangle = Rectangle.fromDegrees(
data.fullExtent.xmin,
data.fullExtent.ymin,
data.fullExtent.xmax,
data.fullExtent.ymax,
);
} else {
const extentMessage = `fullExtent.spatialReference WKID ${data.fullExtent.spatialReference.wkid} is not supported.`;
throw new RuntimeError(extentMessage);
}
}
} else {
imageryProviderBuilder.rectangle =
imageryProviderBuilder.tilingScheme.rectangle;
}
imageryProviderBuilder.useTiles = true;
}
if (defined(data.copyrightText) && data.copyrightText.length > 0) {
if (defined(imageryProviderBuilder.credit)) {
imageryProviderBuilder.tileCredits = [new Credit(data.copyrightText)];
} else {
imageryProviderBuilder.credit = new Credit(data.copyrightText);
}
}
}
function metadataFailure(resource, error) {
let message = `An error occurred while accessing ${resource.url}`;
if (defined(error) && defined(error.message)) {
message += `: ${error.message}`;
}
throw new RuntimeError(message);
}
async function requestMetadata(resource, imageryProviderBuilder) {
const jsonResource = resource.getDerivedResource({
queryParameters: {
f: "json",
},
});
try {
const data = await jsonResource.fetchJson();
metadataSuccess(data, imageryProviderBuilder);
} catch (error) {
metadataFailure(resource, error);
}
}
/**
* <div class="notice">
* This object is normally not instantiated directly, use {@link ArcGisMapServerImageryProvider.fromBasemapType} or {@link ArcGisMapServerImageryProvider.fromUrl}.
* </div>
*
* Provides tiled imagery hosted by an ArcGIS MapServer. By default, the server's pre-cached tiles are
* used, if available.
*
* <br/>
*
* An {@link https://developers.arcgis.com/documentation/mapping-apis-and-services/security| ArcGIS Access Token } is required to authenticate requests to an ArcGIS Image Tile service.
* To access secure ArcGIS resources, it's required to create an ArcGIS developer
* account or an ArcGIS online account, then implement an authentication method to obtain an access token.
*
* @alias ArcGisMapServerImageryProvider
* @constructor
*
* @param {ArcGisMapServerImageryProvider.ConstructorOptions} [options] Object describing initialization options
*
* @see ArcGisMapServerImageryProvider.fromBasemapType
* @see ArcGisMapServerImageryProvider.fromUrl
*
* @example
* // Set the default access token for accessing ArcGIS Image Tile service
* Cesium.ArcGisMapService.defaultAccessToken = "<ArcGIS Access Token>";
*
* // Add a base layer from a default ArcGIS basemap
* const viewer = new Cesium.Viewer("cesiumContainer", {
* baseLayer: Cesium.ImageryLayer.fromProviderAsync(
* Cesium.ArcGisMapServerImageryProvider.fromBasemapType(
* Cesium.ArcGisBaseMapType.SATELLITE
* )
* ),
* });
*
* @example
* // Create an imagery provider from the url directly
* const esri = await Cesium.ArcGisMapServerImageryProvider.fromUrl(
* "https://ibasemaps-api.arcgis.com/arcgis/rest/services/World_Imagery/MapServer", {
* token: "<ArcGIS Access Token>"
* });
*
* @see {@link https://developers.arcgis.com/rest/|ArcGIS Server REST API}
* @see {@link https://developers.arcgis.com/documentation/mapping-apis-and-services/security| ArcGIS Access Token }
*/
function ArcGisMapServerImageryProvider(options) {
options = options ?? Frozen.EMPTY_OBJECT;
this._defaultAlpha = undefined;
this._defaultNightAlpha = undefined;
this._defaultDayAlpha = undefined;
this._defaultBrightness = undefined;
this._defaultContrast = undefined;
this._defaultHue = undefined;
this._defaultSaturation = undefined;
this._defaultGamma = undefined;
this._defaultMinificationFilter = undefined;
this._defaultMagnificationFilter = undefined;
this._tileDiscardPolicy = options.tileDiscardPolicy;
this._tileWidth = options.tileWidth ?? 256;
this._tileHeight = options.tileHeight ?? 256;
this._maximumLevel = options.maximumLevel;
this._tilingScheme =
options.tilingScheme ??
new GeographicTilingScheme({ ellipsoid: options.ellipsoid });
this._useTiles = options.usePreCachedTilesIfAvailable ?? true;
this._rectangle = options.rectangle ?? this._tilingScheme.rectangle;
this._layers = options.layers;
this._credit = options.credit;
this._tileCredits = undefined;
/**
* Gets or sets a value indicating whether feature picking is enabled. If true, {@link ArcGisMapServerImageryProvider#pickFeatures} will
* invoke the "identify" operation on the ArcGIS server and return the features included in the response. If false,
* {@link ArcGisMapServerImageryProvider#pickFeatures} will immediately return undefined (indicating no pickable features)
* without communicating with the server.
* @type {boolean}
* @default true
*/
this.enablePickFeatures = options.enablePickFeatures ?? true;
this._errorEvent = new Event();
}
/**
* Creates an {@link ImageryProvider} which provides tiled imagery from an ArcGIS base map.
* @param {ArcGisBaseMapType} style The style of the ArcGIS base map imagery. Valid options are {@link ArcGisBaseMapType.SATELLITE}, {@link ArcGisBaseMapType.OCEANS}, and {@link ArcGisBaseMapType.HILLSHADE}.
* @param {ArcGisMapServerImageryProvider.ConstructorOptions} [options] Object describing initialization options.
* @returns {Promise<ArcGisMapServerImageryProvider>} A promise that resolves to the created ArcGisMapServerImageryProvider.
*
* @example
* // Set the default access token for accessing ArcGIS Image Tile service
* Cesium.ArcGisMapService.defaultAccessToken = "<ArcGIS Access Token>";
*
* // Add a base layer from a default ArcGIS basemap
* const provider = await Cesium.ArcGisMapServerImageryProvider.fromBasemapType(
* Cesium.ArcGisBaseMapType.SATELLITE);
*
* @example
* // Add a base layer from a default ArcGIS Basemap
* const viewer = new Cesium.Viewer("cesiumContainer", {
* baseLayer: Cesium.ImageryLayer.fromProviderAsync(
* Cesium.ArcGisMapServerImageryProvider.fromBasemapType(
* Cesium.ArcGisBaseMapType.HILLSHADE, {
* token: "<ArcGIS Access Token>"
* }
* )
* ),
* });
*/
ArcGisMapServerImageryProvider.fromBasemapType = async function (
style,
options,
) {
//>>includeStart('debug', pragmas.debug);
Check.defined("style", style);
//>>includeEnd('debug');
options = options ?? Frozen.EMPTY_OBJECT;
let accessToken;
let server;
let warningCredit;
switch (style) {
case ArcGisBaseMapType.SATELLITE:
{
accessToken = options.token ?? ArcGisMapService.defaultAccessToken;
server = Resource.createIfNeeded(
ArcGisMapService.defaultWorldImageryServer,
);
server.appendForwardSlash();
const defaultTokenCredit =
ArcGisMapService.getDefaultTokenCredit(accessToken);
if (defined(defaultTokenCredit)) {
warningCredit = Credit.clone(defaultTokenCredit);
}
}
break;
case ArcGisBaseMapType.OCEANS:
{
accessToken = options.token ?? ArcGisMapService.defaultAccessToken;
server = Resource.createIfNeeded(
ArcGisMapService.defaultWorldOceanServer,
);
server.appendForwardSlash();
const defaultTokenCredit =
ArcGisMapService.getDefaultTokenCredit(accessToken);
if (defined(defaultTokenCredit)) {
warningCredit = Credit.clone(defaultTokenCredit);
}
}
break;
case ArcGisBaseMapType.HILLSHADE:
{
accessToken = options.token ?? ArcGisMapService.defaultAccessToken;
server = Resource.createIfNeeded(
ArcGisMapService.defaultWorldHillshadeServer,
);
server.appendForwardSlash();
const defaultTokenCredit =
ArcGisMapService.getDefaultTokenCredit(accessToken);
if (defined(defaultTokenCredit)) {
warningCredit = Credit.clone(defaultTokenCredit);
}
}
break;
default:
//>>includeStart('debug', pragmas.debug);
throw new DeveloperError(`Unsupported basemap type: ${style}`);
//>>includeEnd('debug');
}
return ArcGisMapServerImageryProvider.fromUrl(server, {
...options,
token: accessToken,
credit: warningCredit,
usePreCachedTilesIfAvailable: true, // ArcGIS Base Map Service Layers only support Tiled views
});
};
function buildImageResource(imageryProvider, x, y, level, request) {
let resource;
if (imageryProvider._useTiles) {
resource = imageryProvider._resource.getDerivedResource({
url: `tile/${level}/${y}/${x}`,
request: request,
});
} else {
const nativeRectangle =
imageryProvider._tilingScheme.tileXYToNativeRectangle(x, y, level);
const bbox = `${nativeRectangle.west},${nativeRectangle.south},${nativeRectangle.east},${nativeRectangle.north}`;
const query = {
bbox: bbox,
size: `${imageryProvider._tileWidth},${imageryProvider._tileHeight}`,
format: "png32",
transparent: true,
f: "image",
};
if (
imageryProvider._tilingScheme.projection instanceof GeographicProjection
) {
query.bboxSR = 4326;
query.imageSR = 4326;
} else {
query.bboxSR = 3857;
query.imageSR = 3857;
}
if (imageryProvider.layers) {
query.layers = `show:${imageryProvider.layers}`;
}
resource = imageryProvider._resource.getDerivedResource({
url: "export",
request: request,
queryParameters: query,
});
}
return resource;
}
Object.defineProperties(ArcGisMapServerImageryProvider.prototype, {
/**
* Gets the URL of the ArcGIS MapServer.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {string}
* @readonly
*/
url: {
get: function () {
return this._resource._url;
},
},
/**
* Gets the ArcGIS token used to authenticate with the ArcGis MapServer service.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {string}
* @readonly
*/
token: {
get: function () {
return this._resource.queryParameters.token;
},
},
/**
* Gets the proxy used by this provider.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {Proxy}
* @readonly
*/
proxy: {
get: function () {
return this._resource.proxy;
},
},
/**
* Gets the width of each tile, in pixels.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {number}
* @readonly
*/
tileWidth: {
get: function () {
return this._tileWidth;
},
},
/**
* Gets the height of each tile, in pixels.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {number}
* @readonly
*/
tileHeight: {
get: function () {
return this._tileHeight;
},
},
/**
* Gets the maximum level-of-detail that can be requested.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {number|undefined}
* @readonly
*/
maximumLevel: {
get: function () {
return this._maximumLevel;
},
},
/**
* Gets the minimum level-of-detail that can be requested.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {number}
* @readonly
*/
minimumLevel: {
get: function () {
return 0;
},
},
/**
* Gets the tiling scheme used by this provider.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {TilingScheme}
* @readonly
*/
tilingScheme: {
get: function () {
return this._tilingScheme;
},
},
/**
* Gets the rectangle, in radians, of the imagery provided by this instance.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {Rectangle}
* @readonly
*/
rectangle: {
get: function () {
return this._rectangle;
},
},
/**
* Gets the tile discard policy. If not undefined, the discard policy is responsible
* for filtering out "missing" tiles via its shouldDiscardImage function. If this function
* returns undefined, no tiles are filtered.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {TileDiscardPolicy}
* @readonly
*/
tileDiscardPolicy: {
get: function () {
return this._tileDiscardPolicy;
},
},
/**
* Gets an event that is raised when the imagery provider encounters an asynchronous error. By subscribing
* to the event, you will be notified of the error and can potentially recover from it. Event listeners
* are passed an instance of {@link TileProviderError}.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {Event}
* @readonly
*/
errorEvent: {
get: function () {
return this._errorEvent;
},
},
/**
* Gets the credit to display when this imagery provider is active. Typically this is used to credit
* the source of the imagery.
* @memberof ArcGisMapServerImageryProvider.prototype
* @type {Credit}
* @readonly
*/
credit: {
get: function () {
return this._credit;
},
},
/**
* Gets a value indicating whether this imagery provider is using pre-cached tiles from the
* ArcGIS MapServer.
* @memberof ArcGisMapServerImageryProvider.prototype
*
* @type {boolean}
* @readonly
* @default true
*/
usingPrecachedTiles: {
get: function () {
return this._useTiles;
},
},
/**
* Gets a value indicating whether or not the images provided by this imagery provider
* include an alpha channel. If this property is false, an alpha channel, if present, will
* be ignored. If this property is true, any images without an alpha channel will be treated
* as if their alpha is 1.0 everywhere. When this property is false, memory usage
* and texture upload time are reduced.
* @memberof ArcGisMapServerImageryProvider.prototype
*
* @type {boolean}
* @readonly
* @default true
*/
hasAlphaChannel: {
get: function () {
return true;
},
},
/**
* Gets the comma-separated list of layer IDs to show.
* @memberof ArcGisMapServerImageryProvider.prototype
*
* @type {string}
*/
layers: {
get: function () {
return this._layers;
},
},
});
/**
* Creates an {@link ImageryProvider} which provides tiled imagery hosted by an ArcGIS MapServer. By default, the server's pre-cached tiles are
* used, if available.
*
* @param {Resource|string} url The URL of the ArcGIS MapServer service.
* @param {ArcGisMapServerImageryProvider.ConstructorOptions} [options] Object describing initialization options.
* @returns {Promise<ArcGisMapServerImageryProvider>} A promise that resolves to the created ArcGisMapServerImageryProvider.
*
* @example
* const esri = await Cesium.ArcGisMapServerImageryProvider.fromUrl(
* "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer"
* );
*
* @exception {RuntimeError} metadata spatial reference specifies an unknown WKID
* @exception {RuntimeError} metadata fullExtent.spatialReference specifies an unknown WKID
*/
ArcGisMapServerImageryProvider.fromUrl = async function (url, options) {
//>>includeStart('debug', pragmas.debug);
Check.defined("url", url);
//>>includeEnd('debug');
options = options ?? Frozen.EMPTY_OBJECT;
const resource = Resource.createIfNeeded(url);
resource.appendForwardSlash();
if (defined(options.token)) {
resource.setQueryParameters({
token: options.token,
});
}
const provider = new ArcGisMapServerImageryProvider(options);
provider._resource = resource;
const imageryProviderBuilder = new ImageryProviderBuilder(options);
const useTiles = options.usePreCachedTilesIfAvailable ?? true;
if (useTiles) {
await requestMetadata(resource, imageryProviderBuilder);
}
imageryProviderBuilder.build(provider);
return provider;
};
/**
* Gets the credits to be displayed when a given tile is displayed.
*
* @param {number} x The tile X coordinate.
* @param {number} y The tile Y coordinate.
* @param {number} level The tile level;
* @returns {Credit[]} The credits to be displayed when the tile is displayed.
*/
ArcGisMapServerImageryProvider.prototype.getTileCredits = function (
x,
y,
level,
) {
return this._tileCredits;
};
/**
* Requests the image for a given tile.
*
* @param {number} x The tile X coordinate.
* @param {number} y The tile Y coordinate.
* @param {number} level The tile level.
* @param {Request} [request] The request object. Intended for internal use only.
* @returns {Promise<ImageryTypes>|undefined} A promise for the image that will resolve when the image is available, or
* undefined if there are too many active requests to the server, and the request should be retried later.
*/
ArcGisMapServerImageryProvider.prototype.requestImage = function (
x,
y,
level,
request,
) {
return ImageryProvider.loadImage(
this,
buildImageResource(this, x, y, level, request),
);
};
/**
/**
* Asynchronously determines what features, if any, are located at a given longitude and latitude within
* a tile.
*
* @param {number} x The tile X coordinate.
* @param {number} y The tile Y coordinate.
* @param {number} level The tile level.
* @param {number} longitude The longitude at which to pick features.
* @param {number} latitude The latitude at which to pick features.
* @return {Promise<ImageryLayerFeatureInfo[]>|undefined} A promise for the picked features that will resolve when the asynchronous
* picking completes. The resolved value is an array of {@link ImageryLayerFeatureInfo}
* instances. The array may be empty if no features are found at the given location.
*/
ArcGisMapServerImageryProvider.prototype.pickFeatures = function (
x,
y,
level,
longitude,
latitude,
) {
if (!this.enablePickFeatures) {
return undefined;
}
const rectangle = this._tilingScheme.tileXYToNativeRectangle(x, y, level);
let horizontal;
let vertical;
let sr;
if (this._tilingScheme.projection instanceof GeographicProjection) {
horizontal = CesiumMath.toDegrees(longitude);
vertical = CesiumMath.toDegrees(latitude);
sr = "4326";
} else {
const projected = this._tilingScheme.projection.project(
new Cartographic(longitude, latitude, 0.0),
);
horizontal = projected.x;
vertical = projected.y;
sr = "3857";
}
let layers = "visible";
if (defined(this._layers)) {
layers += `:${this._layers}`;
}
const query = {
f: "json",
tolerance: 2,
geometryType: "esriGeometryPoint",
geometry: `${horizontal},${vertical}`,
mapExtent: `${rectangle.west},${rectangle.south},${rectangle.east},${rectangle.north}`,
imageDisplay: `${this._tileWidth},${this._tileHeight},96`,
sr: sr,
layers: layers,
};
const resource = this._resource.getDerivedResource({
url: "identify",
queryParameters: query,
});
return resource.fetchJson().then(function (json) {
const result = [];
const features = json.results;
if (!defined(features)) {
return result;
}
for (let i = 0; i < features.length; ++i) {
const feature = features[i];
const featureInfo = new ImageryLayerFeatureInfo();
featureInfo.data = feature;
featureInfo.name = feature.value;
featureInfo.properties = feature.attributes;
featureInfo.configureDescriptionFromProperties(feature.attributes);
// If this is a point feature, use the coordinates of the point.
if (feature.geometryType === "esriGeometryPoint" && feature.geometry) {
const wkid =
feature.geometry.spatialReference &&
feature.geometry.spatialReference.wkid
? feature.geometry.spatialReference.wkid
: 4326;
if (wkid === 4326 || wkid === 4283) {
featureInfo.position = Cartographic.fromDegrees(
feature.geometry.x,
feature.geometry.y,
feature.geometry.z,
);
} else if (wkid === 102100 || wkid === 900913 || wkid === 3857) {
const projection = new WebMercatorProjection();
featureInfo.position = projection.unproject(
new Cartesian3(
feature.geometry.x,
feature.geometry.y,
feature.geometry.z,
),
);
}
}
result.push(featureInfo);
}
return result;
});
};
ArcGisMapServerImageryProvider._metadataCache = {};
export default ArcGisMapServerImageryProvider;
+80
View File
@@ -0,0 +1,80 @@
import Credit from "../Core/Credit.js";
import defined from "../Core/defined.js";
import Resource from "../Core/Resource.js";
let defaultTokenCredit;
const defaultAccessToken =
"AAPTa7BPWL4PoZRFPJ2CM2YRclg..ub1vMOBXctC7ozMMKNnmx3ZwVTsDJAXo2GLomQ2CZjuOc2HLr1-CryeuRo9ZsV65cuZ9xN1yFKeLTu7Cxld7B97aI28os_NnuC9nvWde_l4G1DTApSHmhrVoZfKgO0bqOrsDfvvSgO-cdkUEvBASNX_4Lb9tB5fEY7lbYaWdOOWBXyZylR9il0-biB248V6HdDT1kgkSwz9esfKialRTLgxJx8AFM9is--UXK0CLvZn6JUU44PZSFuMyAT1_rgxsirsZ";
/**
* Default options for accessing the ArcGIS image tile service.
*
* An ArcGIS access token is required to access ArcGIS image tile layers.
* A default token is provided for evaluation purposes only.
* To obtain an access token, go to {@link https://developers.arcgis.com} and create a free account.
* More info can be found in the {@link https://developers.arcgis.com/documentation/mapping-apis-and-services/security/ | ArcGIS developer guide}.
*
* @see ArcGisMapServerImageryProvider
* @namespace ArcGisMapService
*/
const ArcGisMapService = {};
/**
* Gets or sets the default ArcGIS access token.
*
* @type {string}
*/
ArcGisMapService.defaultAccessToken = defaultAccessToken;
/**
* Gets or sets the URL of the ArcGIS World Imagery tile service.
*
* @type {string|Resource}
* @default https://ibasemaps-api.arcgis.com/arcgis/rest/services/World_Imagery/MapServer
*/
ArcGisMapService.defaultWorldImageryServer = new Resource({
url: "https://ibasemaps-api.arcgis.com/arcgis/rest/services/World_Imagery/MapServer",
});
/**
* Gets or sets the URL of the ArcGIS World Hillshade tile service.
*
* @type {string|Resource}
* @default https://ibasemaps-api.arcgis.com/arcgis/rest/services/Elevation/World_Hillshade/MapServer
*/
ArcGisMapService.defaultWorldHillshadeServer = new Resource({
url: "https://ibasemaps-api.arcgis.com/arcgis/rest/services/Elevation/World_Hillshade/MapServer",
});
/**
* Gets or sets the URL of the ArcGIS World Oceans tile service.
*
* @type {string|Resource}
* @default https://ibasemaps-api.arcgis.com/arcgis/rest/services/Ocean/World_Ocean_Base/MapServer
*/
ArcGisMapService.defaultWorldOceanServer = new Resource({
url: "https://ibasemaps-api.arcgis.com/arcgis/rest/services/Ocean/World_Ocean_Base/MapServer",
});
/**
*
* @param {string} providedKey
* @return {string|undefined}
*/
ArcGisMapService.getDefaultTokenCredit = function (providedKey) {
if (providedKey !== defaultAccessToken) {
return undefined;
}
if (!defined(defaultTokenCredit)) {
const defaultTokenMessage =
'<b> \
This application is using a default ArcGIS access token. Please assign <i>Cesium.ArcGisMapService.defaultAccessToken</i> \
with an API key from your ArcGIS Developer account before using the ArcGIS tile services. \
You can sign up for a free ArcGIS Developer account at <a href="https://developers.arcgis.com/">https://developers.arcgis.com/</a>.</b>';
defaultTokenCredit = new Credit(defaultTokenMessage, true);
}
return defaultTokenCredit;
};
export default ArcGisMapService;
+149
View File
@@ -0,0 +1,149 @@
import Cartesian3 from "../Core/Cartesian3.js";
import CesiumMath from "../Core/Math.js";
import DynamicAtmosphereLightingType from "./DynamicAtmosphereLightingType.js";
/**
* Common atmosphere settings used by 3D Tiles and models for rendering sky atmosphere, ground atmosphere, and fog.
*
* <p>
* This class is not to be confused with {@link SkyAtmosphere}, which is responsible for rendering the sky.
* </p>
* <p>
* While the atmosphere settings affect the color of fog, see {@link Fog} to control how fog is rendered.
* </p>
*
* @alias Atmosphere
* @constructor
*
* @example
* // Turn on dynamic atmosphere lighting using the sun direction
* scene.atmosphere.dynamicLighting = Cesium.DynamicAtmosphereLightingType.SUNLIGHT;
*
* @example
* // Turn on dynamic lighting using whatever light source is in the scene
* scene.light = new Cesium.DirectionalLight({
* direction: new Cesium.Cartesian3(1, 0, 0)
* });
* scene.atmosphere.dynamicLighting = Cesium.DynamicAtmosphereLightingType.SCENE_LIGHT;
*
* @example
* // Adjust the color of the atmosphere effects.
* scene.atmosphere.hueShift = 0.4; // Cycle 40% around the color wheel
* scene.atmosphere.brightnessShift = 0.25; // Increase the brightness
* scene.atmosphere.saturationShift = -0.1; // Desaturate the colors
*
* @see SkyAtmosphere
* @see Globe
* @see Fog
*/
function Atmosphere() {
/**
* The intensity of the light that is used for computing the ground atmosphere color.
*
* @type {number}
* @default 10.0
*/
this.lightIntensity = 10.0;
/**
* The Rayleigh scattering coefficient used in the atmospheric scattering equations for the ground atmosphere.
*
* @type {Cartesian3}
* @default Cartesian3(5.5e-6, 13.0e-6, 28.4e-6)
*/
this.rayleighCoefficient = new Cartesian3(5.5e-6, 13.0e-6, 28.4e-6);
/**
* The Mie scattering coefficient used in the atmospheric scattering equations for the ground atmosphere.
*
* @type {Cartesian3}
* @default Cartesian3(21e-6, 21e-6, 21e-6)
*/
this.mieCoefficient = new Cartesian3(21e-6, 21e-6, 21e-6);
/**
* The Rayleigh scale height used in the atmospheric scattering equations for the ground atmosphere, in meters.
*
* @type {number}
* @default 10000.0
*/
this.rayleighScaleHeight = 10000.0;
/**
* The Mie scale height used in the atmospheric scattering equations for the ground atmosphere, in meters.
*
* @type {number}
* @default 3200.0
*/
this.mieScaleHeight = 3200.0;
/**
* The anisotropy of the medium to consider for Mie scattering.
* <p>
* Valid values are between -1.0 and 1.0.
* </p>
*
* @type {number}
* @default 0.9
*/
this.mieAnisotropy = 0.9;
/**
* The hue shift to apply to the atmosphere. Defaults to 0.0 (no shift).
* A hue shift of 1.0 indicates a complete rotation of the hues available.
*
* @type {number}
* @default 0.0
*/
this.hueShift = 0.0;
/**
* The saturation shift to apply to the atmosphere. Defaults to 0.0 (no shift).
* A saturation shift of -1.0 is monochrome.
*
* @type {number}
* @default 0.0
*/
this.saturationShift = 0.0;
/**
* The brightness shift to apply to the atmosphere. Defaults to 0.0 (no shift).
* A brightness shift of -1.0 is complete darkness, which will let space show through.
*
* @type {number}
* @default 0.0
*/
this.brightnessShift = 0.0;
/**
* When not DynamicAtmosphereLightingType.NONE, the selected light source will
* be used for dynamically lighting all atmosphere-related rendering effects.
*
* @type {DynamicAtmosphereLightingType}
* @default DynamicAtmosphereLightingType.NONE
*/
this.dynamicLighting = DynamicAtmosphereLightingType.NONE;
}
/**
* Returns <code>true</code> if the atmosphere shader requires a color correct step.
* @param {Atmosphere} atmosphere The atmosphere instance to check
* @returns {boolean} true if the atmosphere shader requires a color correct step
*/
Atmosphere.requiresColorCorrect = function (atmosphere) {
return !(
CesiumMath.equalsEpsilon(atmosphere.hueShift, 0.0, CesiumMath.EPSILON7) &&
CesiumMath.equalsEpsilon(
atmosphere.saturationShift,
0.0,
CesiumMath.EPSILON7,
) &&
CesiumMath.equalsEpsilon(
atmosphere.brightnessShift,
0.0,
CesiumMath.EPSILON7,
)
);
};
export default Atmosphere;
+202
View File
@@ -0,0 +1,202 @@
import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Cartesian4 from "../Core/Cartesian4.js";
import Check from "../Core/Check.js";
import DeveloperError from "../Core/DeveloperError.js";
import Matrix2 from "../Core/Matrix2.js";
import Matrix3 from "../Core/Matrix3.js";
import Matrix4 from "../Core/Matrix4.js";
/**
* An enum describing the attribute type for glTF and 3D Tiles.
*
* @enum {string}
*
* @private
*/
const AttributeType = {
/**
* The attribute is a single component.
*
* @type {string}
* @constant
*/
SCALAR: "SCALAR",
/**
* The attribute is a two-component vector.
*
* @type {string}
* @constant
*/
VEC2: "VEC2",
/**
* The attribute is a three-component vector.
*
* @type {string}
* @constant
*/
VEC3: "VEC3",
/**
* The attribute is a four-component vector.
*
* @type {string}
* @constant
*/
VEC4: "VEC4",
/**
* The attribute is a 2x2 matrix.
*
* @type {string}
* @constant
*/
MAT2: "MAT2",
/**
* The attribute is a 3x3 matrix.
*
* @type {string}
* @constant
*/
MAT3: "MAT3",
/**
* The attribute is a 4x4 matrix.
*
* @type {string}
* @constant
*/
MAT4: "MAT4",
};
/**
* Gets the scalar, vector, or matrix type for the attribute type.
*
* @param {AttributeType} attributeType The attribute type.
* @returns {*} The math type.
*
* @private
*/
AttributeType.getMathType = function (attributeType) {
switch (attributeType) {
case AttributeType.SCALAR:
return Number;
case AttributeType.VEC2:
return Cartesian2;
case AttributeType.VEC3:
return Cartesian3;
case AttributeType.VEC4:
return Cartesian4;
case AttributeType.MAT2:
return Matrix2;
case AttributeType.MAT3:
return Matrix3;
case AttributeType.MAT4:
return Matrix4;
//>>includeStart('debug', pragmas.debug);
default:
throw new DeveloperError("attributeType is not a valid value.");
//>>includeEnd('debug');
}
};
/**
* Gets the number of components per attribute.
*
* @param {AttributeType} attributeType The attribute type.
* @returns {number} The number of components.
*
* @private
*/
AttributeType.getNumberOfComponents = function (attributeType) {
switch (attributeType) {
case AttributeType.SCALAR:
return 1;
case AttributeType.VEC2:
return 2;
case AttributeType.VEC3:
return 3;
case AttributeType.VEC4:
case AttributeType.MAT2:
return 4;
case AttributeType.MAT3:
return 9;
case AttributeType.MAT4:
return 16;
//>>includeStart('debug', pragmas.debug);
default:
throw new DeveloperError("attributeType is not a valid value.");
//>>includeEnd('debug');
}
};
/**
* Get the number of attribute locations needed to fit this attribute. Most
* types require one, but matrices require multiple attribute locations.
*
* @param {AttributeType} attributeType The attribute type.
* @returns {number} The number of attribute locations needed in the shader
*
* @private
*/
AttributeType.getAttributeLocationCount = function (attributeType) {
switch (attributeType) {
case AttributeType.SCALAR:
case AttributeType.VEC2:
case AttributeType.VEC3:
case AttributeType.VEC4:
return 1;
case AttributeType.MAT2:
return 2;
case AttributeType.MAT3:
return 3;
case AttributeType.MAT4:
return 4;
//>>includeStart('debug', pragmas.debug);
default:
throw new DeveloperError("attributeType is not a valid value.");
//>>includeEnd('debug');
}
};
/**
* Gets the GLSL type for the attribute type.
*
* @param {AttributeType} attributeType The attribute type.
* @returns {string} The GLSL type for the attribute type.
*
* @private
*/
AttributeType.getGlslType = function (attributeType) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("attributeType", attributeType);
//>>includeEnd('debug');
switch (attributeType) {
case AttributeType.SCALAR:
return "float";
case AttributeType.VEC2:
return "vec2";
case AttributeType.VEC3:
return "vec3";
case AttributeType.VEC4:
return "vec4";
case AttributeType.MAT2:
return "mat2";
case AttributeType.MAT3:
return "mat3";
case AttributeType.MAT4:
return "mat4";
//>>includeStart('debug', pragmas.debug);
default:
throw new DeveloperError("attributeType is not a valid value.");
//>>includeEnd('debug');
}
};
Object.freeze(AttributeType);
export default AttributeType;
+386
View File
@@ -0,0 +1,386 @@
import Cartesian2 from "../Core/Cartesian2.js";
import Color from "../Core/Color.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import ClearCommand from "../Renderer/ClearCommand.js";
import FramebufferManager from "../Renderer/FramebufferManager.js";
import PixelDatatype from "../Renderer/PixelDatatype.js";
/**
* A post process stage that will get the luminance value at each pixel and
* uses parallel reduction to compute the average luminance in a 1x1 texture.
* This texture can be used as input for tone mapping.
*
* @constructor
* @private
*/
function AutoExposure() {
this._uniformMap = undefined;
this._command = undefined;
this._colorTexture = undefined;
this._depthTexture = undefined;
this._ready = false;
this._name = "czm_autoexposure";
this._logDepthChanged = undefined;
this._useLogDepth = undefined;
this._framebuffers = undefined;
this._previousLuminance = new FramebufferManager();
this._commands = undefined;
this._clearCommand = undefined;
this._minMaxLuminance = new Cartesian2();
/**
* Whether or not to execute this post-process stage when ready.
*
* @type {boolean}
*/
this.enabled = true;
this._enabled = true;
/**
* The minimum value used to clamp the luminance.
*
* @type {number}
* @default 0.1
*/
this.minimumLuminance = 0.1;
/**
* The maximum value used to clamp the luminance.
*
* @type {number}
* @default 10.0
*/
this.maximumLuminance = 10.0;
}
Object.defineProperties(AutoExposure.prototype, {
/**
* Determines if this post-process stage is ready to be executed. A stage is only executed when both <code>ready</code>
* and {@link AutoExposure#enabled} are <code>true</code>. A stage will not be ready while it is waiting on textures
* to load.
*
* @memberof AutoExposure.prototype
* @type {boolean}
* @readonly
*/
ready: {
get: function () {
return this._ready;
},
},
/**
* The unique name of this post-process stage for reference by other stages.
*
* @memberof AutoExposure.prototype
* @type {string}
* @readonly
*/
name: {
get: function () {
return this._name;
},
},
/**
* A reference to the texture written to when executing this post process stage.
*
* @memberof AutoExposure.prototype
* @type {Texture}
* @readonly
* @private
*/
outputTexture: {
get: function () {
const framebuffers = this._framebuffers;
if (!defined(framebuffers)) {
return undefined;
}
return framebuffers[framebuffers.length - 1].getColorTexture(0);
},
},
});
function destroyFramebuffers(autoexposure) {
const framebuffers = autoexposure._framebuffers;
if (!defined(framebuffers)) {
return;
}
const length = framebuffers.length;
for (let i = 0; i < length; ++i) {
framebuffers[i].destroy();
}
autoexposure._framebuffers = undefined;
autoexposure._previousLuminance.destroy();
autoexposure._previousLuminance = undefined;
}
function createFramebuffers(autoexposure, context) {
destroyFramebuffers(autoexposure);
let width = autoexposure._width;
let height = autoexposure._height;
const pixelDatatype = context.halfFloatingPointTexture
? PixelDatatype.HALF_FLOAT
: PixelDatatype.FLOAT;
const length = Math.ceil(Math.log(Math.max(width, height)) / Math.log(3.0));
const framebuffers = new Array(length);
for (let i = 0; i < length; ++i) {
width = Math.max(Math.ceil(width / 3.0), 1.0);
height = Math.max(Math.ceil(height / 3.0), 1.0);
framebuffers[i] = new FramebufferManager();
framebuffers[i].update(context, width, height, 1, pixelDatatype);
}
const lastTexture = framebuffers[length - 1].getColorTexture(0);
autoexposure._previousLuminance.update(
context,
lastTexture.width,
lastTexture.height,
1,
pixelDatatype,
);
autoexposure._framebuffers = framebuffers;
}
function destroyCommands(autoexposure) {
const commands = autoexposure._commands;
if (!defined(commands)) {
return;
}
const length = commands.length;
for (let i = 0; i < length; ++i) {
commands[i].shaderProgram.destroy();
}
autoexposure._commands = undefined;
}
function createUniformMap(autoexposure, index) {
let uniforms;
if (index === 0) {
uniforms = {
colorTexture: function () {
return autoexposure._colorTexture;
},
colorTextureDimensions: function () {
return autoexposure._colorTexture.dimensions;
},
};
} else {
const texture = autoexposure._framebuffers[index - 1].getColorTexture(0);
uniforms = {
colorTexture: function () {
return texture;
},
colorTextureDimensions: function () {
return texture.dimensions;
},
};
}
uniforms.minMaxLuminance = function () {
return autoexposure._minMaxLuminance;
};
uniforms.previousLuminance = function () {
return autoexposure._previousLuminance.getColorTexture(0);
};
return uniforms;
}
function getShaderSource(index, length) {
let source =
"uniform sampler2D colorTexture; \n" +
"in vec2 v_textureCoordinates; \n" +
"float sampleTexture(vec2 offset) { \n";
if (index === 0) {
source +=
" vec4 color = texture(colorTexture, v_textureCoordinates + offset); \n" +
" return czm_luminance(color.rgb); \n";
} else {
source +=
" return texture(colorTexture, v_textureCoordinates + offset).r; \n";
}
source += "}\n\n";
source +=
"uniform vec2 colorTextureDimensions; \n" +
"uniform vec2 minMaxLuminance; \n" +
"uniform sampler2D previousLuminance; \n" +
"void main() { \n" +
" float color = 0.0; \n" +
" float xStep = 1.0 / colorTextureDimensions.x; \n" +
" float yStep = 1.0 / colorTextureDimensions.y; \n" +
" int count = 0; \n" +
" for (int i = 0; i < 3; ++i) { \n" +
" for (int j = 0; j < 3; ++j) { \n" +
" vec2 offset; \n" +
" offset.x = -xStep + float(i) * xStep; \n" +
" offset.y = -yStep + float(j) * yStep; \n" +
" if (offset.x < 0.0 || offset.x > 1.0 || offset.y < 0.0 || offset.y > 1.0) { \n" +
" continue; \n" +
" } \n" +
" color += sampleTexture(offset); \n" +
" ++count; \n" +
" } \n" +
" } \n" +
" if (count > 0) { \n" +
" color /= float(count); \n" +
" } \n";
if (index === length - 1) {
source +=
" float previous = texture(previousLuminance, vec2(0.5)).r; \n" +
" color = clamp(color, minMaxLuminance.x, minMaxLuminance.y); \n" +
" color = previous + (color - previous) / (60.0 * 1.5); \n" +
" color = clamp(color, minMaxLuminance.x, minMaxLuminance.y); \n";
}
source += " out_FragColor = vec4(color); \n" + "} \n";
return source;
}
function createCommands(autoexposure, context) {
destroyCommands(autoexposure);
const framebuffers = autoexposure._framebuffers;
const length = framebuffers.length;
const commands = new Array(length);
for (let i = 0; i < length; ++i) {
commands[i] = context.createViewportQuadCommand(
getShaderSource(i, length),
{
framebuffer: framebuffers[i].framebuffer,
uniformMap: createUniformMap(autoexposure, i),
},
);
}
autoexposure._commands = commands;
}
/**
* A function that will be called before execute. Used to clear any textures attached to framebuffers.
* @param {Context} context The context.
* @private
*/
AutoExposure.prototype.clear = function (context) {
const framebuffers = this._framebuffers;
if (!defined(framebuffers)) {
return;
}
let clearCommand = this._clearCommand;
if (!defined(clearCommand)) {
clearCommand = this._clearCommand = new ClearCommand({
color: new Color(0.0, 0.0, 0.0, 0.0),
framebuffer: undefined,
});
}
const length = framebuffers.length;
for (let i = 0; i < length; ++i) {
framebuffers[i].clear(context, clearCommand);
}
};
/**
* A function that will be called before execute. Used to create WebGL resources and load any textures.
* @param {Context} context The context.
* @private
*/
AutoExposure.prototype.update = function (context) {
const width = context.drawingBufferWidth;
const height = context.drawingBufferHeight;
if (width !== this._width || height !== this._height) {
this._width = width;
this._height = height;
createFramebuffers(this, context);
createCommands(this, context);
if (!this._ready) {
this._ready = true;
}
}
this._minMaxLuminance.x = this.minimumLuminance;
this._minMaxLuminance.y = this.maximumLuminance;
const framebuffers = this._framebuffers;
const temp = framebuffers[framebuffers.length - 1];
framebuffers[framebuffers.length - 1] = this._previousLuminance;
this._commands[this._commands.length - 1].framebuffer =
this._previousLuminance.framebuffer;
this._previousLuminance = temp;
};
/**
* Executes the post-process stage. The color texture is the texture rendered to by the scene or from the previous stage.
* @param {Context} context The context.
* @param {Texture} colorTexture The input color texture.
* @private
*/
AutoExposure.prototype.execute = function (context, colorTexture) {
this._colorTexture = colorTexture;
const commands = this._commands;
if (!defined(commands)) {
return;
}
const length = commands.length;
for (let i = 0; i < length; ++i) {
commands[i].execute(context);
}
};
/**
* Returns true if this object was destroyed; otherwise, false.
* <p>
* If this object was destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
* </p>
*
* @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
*
* @see AutoExposure#destroy
*/
AutoExposure.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.
* <p>
* Once an object is destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
* assign the return value (<code>undefined</code>) to the object as done in the example.
* </p>
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
* @see AutoExposure#isDestroyed
*/
AutoExposure.prototype.destroy = function () {
destroyFramebuffers(this);
destroyCommands(this);
return destroyObject(this);
};
export default AutoExposure;
+118
View File
@@ -0,0 +1,118 @@
import Check from "../Core/Check.js";
import Matrix3 from "../Core/Matrix3.js";
import Matrix4 from "../Core/Matrix4.js";
/**
* An enum describing the x, y, and z axes and helper conversion functions.
*
* @enum {number}
*/
const Axis = {
/**
* Denotes the x-axis.
*
* @type {number}
* @constant
*/
X: 0,
/**
* Denotes the y-axis.
*
* @type {number}
* @constant
*/
Y: 1,
/**
* Denotes the z-axis.
*
* @type {number}
* @constant
*/
Z: 2,
};
/**
* Matrix used to convert from y-up to z-up
*
* @type {Matrix4}
* @constant
*/
Axis.Y_UP_TO_Z_UP = Matrix4.fromRotationTranslation(
// Rotation about PI/2 around the X-axis
Matrix3.fromArray([1, 0, 0, 0, 0, 1, 0, -1, 0]),
);
/**
* Matrix used to convert from z-up to y-up
*
* @type {Matrix4}
* @constant
*/
Axis.Z_UP_TO_Y_UP = Matrix4.fromRotationTranslation(
// Rotation about -PI/2 around the X-axis
Matrix3.fromArray([1, 0, 0, 0, 0, -1, 0, 1, 0]),
);
/**
* Matrix used to convert from x-up to z-up
*
* @type {Matrix4}
* @constant
*/
Axis.X_UP_TO_Z_UP = Matrix4.fromRotationTranslation(
// Rotation about -PI/2 around the Y-axis
Matrix3.fromArray([0, 0, 1, 0, 1, 0, -1, 0, 0]),
);
/**
* Matrix used to convert from z-up to x-up
*
* @type {Matrix4}
* @constant
*/
Axis.Z_UP_TO_X_UP = Matrix4.fromRotationTranslation(
// Rotation about PI/2 around the Y-axis
Matrix3.fromArray([0, 0, -1, 0, 1, 0, 1, 0, 0]),
);
/**
* Matrix used to convert from x-up to y-up
*
* @type {Matrix4}
* @constant
*/
Axis.X_UP_TO_Y_UP = Matrix4.fromRotationTranslation(
// Rotation about PI/2 around the Z-axis
Matrix3.fromArray([0, 1, 0, -1, 0, 0, 0, 0, 1]),
);
/**
* Matrix used to convert from y-up to x-up
*
* @type {Matrix4}
* @constant
*/
Axis.Y_UP_TO_X_UP = Matrix4.fromRotationTranslation(
// Rotation about -PI/2 around the Z-axis
Matrix3.fromArray([0, -1, 0, 1, 0, 0, 0, 0, 1]),
);
/**
* Gets the axis by name
*
* @param {string} name The name of the axis.
* @returns {number} The axis enum.
*/
Axis.fromName = function (name) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("name", name);
//>>includeEnd('debug');
return Axis[name];
};
Object.freeze(Axis);
export default Axis;
+393
View File
@@ -0,0 +1,393 @@
import Check from "../Core/Check.js";
import Frozen from "../Core/Frozen.js";
import Credit from "../Core/Credit.js";
import defined from "../Core/defined.js";
import Resource from "../Core/Resource.js";
import IonResource from "../Core/IonResource.js";
import UrlTemplateImageryProvider from "./UrlTemplateImageryProvider.js";
const trailingSlashRegex = /\/$/;
/**
* @typedef {object} Azure2DImageryProvider.ConstructorOptions
*
* Initialization options for the Azure2DImageryProvider constructor
*
* @property {string} subscriptionKey The public subscription key for the imagery.
* @property {string} [url="https://atlas.microsoft.com/"] The Azure server url.
* @property {string} [tilesetId="microsoft.imagery"] The Azure tileset ID. Valid options are {@link microsoft.imagery}, {@link microsoft.base.road}, and {@link microsoft.base.labels.road}
* @property {Ellipsoid} [ellipsoid=Ellipsoid.default] The ellipsoid. If not specified, the default ellipsoid is used.
* @property {number} [minimumLevel=0] The minimum level-of-detail supported by the imagery provider. Take care when specifying
* this that the number of tiles at the minimum level is small, such as four or less. A larger number is likely
* to result in rendering problems.
* @property {number} [maximumLevel=22] The maximum level-of-detail supported by the imagery provider.
* @property {Rectangle} [rectangle=Rectangle.MAX_VALUE] The rectangle, in radians, covered by the image.
*/
/**
* Provides 2D image tiles from Azure.
*
* @alias Azure2DImageryProvider
* @constructor
* @param {Azure2DImageryProvider.ConstructorOptions} options Object describing initialization options
*
* @example
* // Azure 2D imagery provider
* const azureImageryProvider = new Cesium.Azure2DImageryProvider({
* subscriptionKey: "subscription-key",
* tilesetId: "microsoft.base.road"
* });
*/
function Azure2DImageryProvider(options) {
options = options ?? {};
const tilesetId = options.tilesetId ?? "microsoft.imagery";
this._maximumLevel = options.maximumLevel ?? 22;
this._minimumLevel = options.minimumLevel ?? 0;
this._subscriptionKey =
options.subscriptionKey ?? options["subscription-key"];
//>>includeStart('debug', pragmas.debug);
Check.defined("options.subscriptionKey", this._subscriptionKey);
//>>includeEnd('debug');
this._tilesetId = options.tilesetId;
const resource =
options.url instanceof IonResource
? options.url
: Resource.createIfNeeded(options.url ?? "https://atlas.microsoft.com/");
let templateUrl = resource.getUrlComponent();
if (!trailingSlashRegex.test(templateUrl)) {
templateUrl += "/";
}
const tilesUrl = `${templateUrl}map/tile`;
this._viewportUrl = `${templateUrl}map/attribution`;
resource.url = tilesUrl;
resource.setQueryParameters({
"api-version": "2024-04-01",
tilesetId: tilesetId,
"subscription-key": this._subscriptionKey,
zoom: `{z}`,
x: `{x}`,
y: `{y}`,
});
this._resource = resource;
let credit;
if (defined(options.credit)) {
credit = options.credit;
if (typeof credit === "string") {
credit = new Credit(credit);
}
}
const provider = new UrlTemplateImageryProvider({
...options,
maximumLevel: this._maximumLevel,
minimumLevel: this._minimumLevel,
url: resource,
credit: credit,
});
provider._resource = resource;
this._imageryProvider = provider;
// This will be defined for ion resources
this._tileCredits = resource.credits;
this._attributionsByLevel = undefined;
}
Object.defineProperties(Azure2DImageryProvider.prototype, {
/**
* Gets the URL of the Azure 2D Imagery server.
* @memberof Azure2DImageryProvider.prototype
* @type {string}
* @readonly
*/
url: {
get: function () {
return this._imageryProvider.url;
},
},
/**
* Gets the rectangle, in radians, of the imagery provided by the instance.
* @memberof Azure2DImageryProvider.prototype
* @type {Rectangle}
* @readonly
*/
rectangle: {
get: function () {
return this._imageryProvider.rectangle;
},
},
/**
* Gets the width of each tile, in pixels.
* @memberof Azure2DImageryProvider.prototype
* @type {number}
* @readonly
*/
tileWidth: {
get: function () {
return this._imageryProvider.tileWidth;
},
},
/**
* Gets the height of each tile, in pixels.
* @memberof Azure2DImageryProvider.prototype
* @type {number}
* @readonly
*/
tileHeight: {
get: function () {
return this._imageryProvider.tileHeight;
},
},
/**
* Gets the maximum level-of-detail that can be requested.
* @memberof Azure2DImageryProvider.prototype
* @type {number|undefined}
* @readonly
*/
maximumLevel: {
get: function () {
return this._imageryProvider.maximumLevel;
},
},
/**
* Gets the minimum level-of-detail that can be requested. Generally,
* a minimum level should only be used when the rectangle of the imagery is small
* enough that the number of tiles at the minimum level is small. An imagery
* provider with more than a few tiles at the minimum level will lead to
* rendering problems.
* @memberof Azure2DImageryProvider.prototype
* @type {number}
* @readonly
*/
minimumLevel: {
get: function () {
return this._imageryProvider.minimumLevel;
},
},
/**
* Gets the tiling scheme used by the provider.
* @memberof Azure2DImageryProvider.prototype
* @type {TilingScheme}
* @readonly
*/
tilingScheme: {
get: function () {
return this._imageryProvider.tilingScheme;
},
},
/**
* Gets the tile discard policy. If not undefined, the discard policy is responsible
* for filtering out "missing" tiles via its shouldDiscardImage function. If this function
* returns undefined, no tiles are filtered.
* @memberof Azure2DImageryProvider.prototype
* @type {TileDiscardPolicy}
* @readonly
*/
tileDiscardPolicy: {
get: function () {
return this._imageryProvider.tileDiscardPolicy;
},
},
/**
* Gets an event that is raised when the imagery provider encounters an asynchronous error.. By subscribing
* to the event, you will be notified of the error and can potentially recover from it. Event listeners
* are passed an instance of {@link TileProviderError}.
* @memberof Azure2DImageryProvider.prototype
* @type {Event}
* @readonly
*/
errorEvent: {
get: function () {
return this._imageryProvider.errorEvent;
},
},
/**
* Gets the credit to display when this imagery provider is active. Typically this is used to credit
* the source of the imagery.
* @memberof Azure2DImageryProvider.prototype
* @type {Credit}
* @readonly
*/
credit: {
get: function () {
return this._imageryProvider.credit;
},
},
/**
* Gets the proxy used by this provider.
* @memberof Azure2DImageryProvider.prototype
* @type {Proxy}
* @readonly
*/
proxy: {
get: function () {
return this._imageryProvider.proxy;
},
},
/**
* Gets a value indicating whether or not the images provided by this imagery provider
* include an alpha channel. If this property is false, an alpha channel, if present, will
* be ignored. If this property is true, any images without an alpha channel will be treated
* as if their alpha is 1.0 everywhere. When this property is false, memory usage
* and texture upload time are reduced.
* @memberof Azure2DImageryProvider.prototype
* @type {boolean}
* @readonly
*/
hasAlphaChannel: {
get: function () {
return this._imageryProvider.hasAlphaChannel;
},
},
});
/**
* Gets the credits to be displayed when a given tile is displayed.
*
* @param {number} x The tile X coordinate.
* @param {number} y The tile Y coordinate.
* @param {number} level The tile level;
* @returns {Credit[]|undefined} The credits to be displayed when the tile is displayed.
*/
Azure2DImageryProvider.prototype.getTileCredits = function (x, y, level) {
const hasAttributions = defined(this._attributionsByLevel);
if (!hasAttributions || !defined(this._tileCredits)) {
return undefined;
}
const innerCredits = this._attributionsByLevel.get(level);
if (!defined(this._tileCredits)) {
return innerCredits;
}
return this._tileCredits.concat(innerCredits);
};
/**
* Requests the image for a given tile.
*
* @param {number} x The tile X coordinate.
* @param {number} y The tile Y coordinate.
* @param {number} level The tile level.
* @param {Request} [request] The request object. Intended for internal use only.
* @returns {Promise<ImageryTypes>|undefined} A promise for the image that will resolve when the image is available, or
* undefined if there are too many active requests to the server, and the request should be retried later.
*/
Azure2DImageryProvider.prototype.requestImage = function (
x,
y,
level,
request,
) {
const promise = this._imageryProvider.requestImage(x, y, level, request);
// If the requestImage call returns undefined, it couldn't be scheduled this frame. Make sure to return undefined so this can be handled upstream.
if (!defined(promise)) {
return undefined;
}
// Asynchronously request and populate _attributionsByLevel if it hasn't been already. We do this here so that the promise can be properly awaited.
if (!defined(this._attributionsByLevel)) {
return Promise.all([promise, this.getViewportCredits()]).then(
(results) => results[0],
);
}
return promise;
};
/**
* Picking features is not currently supported by this imagery provider, so this function simply returns
* undefined.
*
* @param {number} x The tile X coordinate.
* @param {number} y The tile Y coordinate.
* @param {number} level The tile level.
* @param {number} longitude The longitude at which to pick features.
* @param {number} latitude The latitude at which to pick features.
* @return {undefined} Undefined since picking is not supported.
*/
Azure2DImageryProvider.prototype.pickFeatures = function (
x,
y,
level,
longitude,
latitude,
) {
return undefined;
};
/**
* Get attribution for imagery from Azure Maps to display in the credits
* @private
* @return {Promise<Map<Credit[]>>} The list of attribution sources to display in the credits.
*/
Azure2DImageryProvider.prototype.getViewportCredits = async function () {
const maximumLevel = this._maximumLevel;
const promises = [];
for (let level = 0; level < maximumLevel + 1; level++) {
promises.push(
fetchViewportAttribution(
this._resource,
this._viewportUrl,
this._subscriptionKey,
this._tilesetId,
level,
),
);
}
const results = await Promise.all(promises);
const attributionsByLevel = new Map();
for (let level = 0; level < maximumLevel + 1; level++) {
const credits = [];
const attributions = results[level].join(",");
if (attributions) {
const levelCredits = new Credit(attributions);
credits.push(levelCredits);
}
attributionsByLevel.set(level, credits);
}
this._attributionsByLevel = attributionsByLevel;
return attributionsByLevel;
};
async function fetchViewportAttribution(resource, url, key, tilesetId, level) {
const viewportResource = resource.getDerivedResource({
url,
queryParameters: {
zoom: level,
bounds: "-180,-90,180,90",
},
data: JSON.stringify(Frozen.EMPTY_OBJECT),
});
const viewportJson = await viewportResource.fetchJson();
return viewportJson.copyrights;
}
// Exposed for tests
export default Azure2DImageryProvider;
+173
View File
@@ -0,0 +1,173 @@
import Check from "../Core/Check.js";
import deprecationWarning from "../Core/deprecationWarning.js";
import getJsonFromTypedArray from "../Core/getJsonFromTypedArray.js";
import RuntimeError from "../Core/RuntimeError.js";
/**
* Handles parsing of a Batched 3D Model.
*
* @namespace B3dmParser
* @private
*/
const B3dmParser = {};
B3dmParser._deprecationWarning = deprecationWarning;
const sizeOfUint32 = Uint32Array.BYTES_PER_ELEMENT;
/**
* Parses the contents of a {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/Batched3DModel|Batched 3D Model}.
*
* @private
*
* @param {ArrayBuffer} arrayBuffer The array buffer containing the b3dm.
* @param {number} [byteOffset=0] The byte offset of the beginning of the b3dm in the array buffer.
* @returns {object} Returns an object with the batch length, feature table (binary and json), batch table (binary and json) and glTF parts of the b3dm.
*/
B3dmParser.parse = function (arrayBuffer, byteOffset) {
const byteStart = byteOffset ?? 0;
//>>includeStart('debug', pragmas.debug);
Check.defined("arrayBuffer", arrayBuffer);
//>>includeEnd('debug');
byteOffset = byteStart;
const uint8Array = new Uint8Array(arrayBuffer);
const view = new DataView(arrayBuffer);
byteOffset += sizeOfUint32; // Skip magic
const version = view.getUint32(byteOffset, true);
if (version !== 1) {
throw new RuntimeError(
`Only Batched 3D Model version 1 is supported. Version ${version} is not.`,
);
}
byteOffset += sizeOfUint32;
const byteLength = view.getUint32(byteOffset, true);
byteOffset += sizeOfUint32;
let featureTableJsonByteLength = view.getUint32(byteOffset, true);
byteOffset += sizeOfUint32;
let featureTableBinaryByteLength = view.getUint32(byteOffset, true);
byteOffset += sizeOfUint32;
let batchTableJsonByteLength = view.getUint32(byteOffset, true);
byteOffset += sizeOfUint32;
let batchTableBinaryByteLength = view.getUint32(byteOffset, true);
byteOffset += sizeOfUint32;
let batchLength;
// Legacy header #1: [batchLength] [batchTableByteLength]
// Legacy header #2: [batchTableJsonByteLength] [batchTableBinaryByteLength] [batchLength]
// Current header: [featureTableJsonByteLength] [featureTableBinaryByteLength] [batchTableJsonByteLength] [batchTableBinaryByteLength]
// If the header is in the first legacy format 'batchTableJsonByteLength' will be the start of the JSON string (a quotation mark) or the glTF magic.
// Accordingly its first byte will be either 0x22 or 0x67, and so the minimum uint32 expected is 0x22000000 = 570425344 = 570MB. It is unlikely that the feature table JSON will exceed this length.
// The check for the second legacy format is similar, except it checks 'batchTableBinaryByteLength' instead
if (batchTableJsonByteLength >= 570425344) {
// First legacy check
byteOffset -= sizeOfUint32 * 2;
batchLength = featureTableJsonByteLength;
batchTableJsonByteLength = featureTableBinaryByteLength;
batchTableBinaryByteLength = 0;
featureTableJsonByteLength = 0;
featureTableBinaryByteLength = 0;
B3dmParser._deprecationWarning(
"b3dm-legacy-header",
"This b3dm header is using the legacy format [batchLength] [batchTableByteLength]. The new format is [featureTableJsonByteLength] [featureTableBinaryByteLength] [batchTableJsonByteLength] [batchTableBinaryByteLength] from https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/Batched3DModel.",
);
} else if (batchTableBinaryByteLength >= 570425344) {
// Second legacy check
byteOffset -= sizeOfUint32;
batchLength = batchTableJsonByteLength;
batchTableJsonByteLength = featureTableJsonByteLength;
batchTableBinaryByteLength = featureTableBinaryByteLength;
featureTableJsonByteLength = 0;
featureTableBinaryByteLength = 0;
B3dmParser._deprecationWarning(
"b3dm-legacy-header",
"This b3dm header is using the legacy format [batchTableJsonByteLength] [batchTableBinaryByteLength] [batchLength]. The new format is [featureTableJsonByteLength] [featureTableBinaryByteLength] [batchTableJsonByteLength] [batchTableBinaryByteLength] from https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/Batched3DModel.",
);
}
let featureTableJson;
if (featureTableJsonByteLength === 0) {
featureTableJson = {
BATCH_LENGTH: batchLength ?? 0,
};
} else {
featureTableJson = getJsonFromTypedArray(
uint8Array,
byteOffset,
featureTableJsonByteLength,
);
byteOffset += featureTableJsonByteLength;
}
const featureTableBinary = new Uint8Array(
arrayBuffer,
byteOffset,
featureTableBinaryByteLength,
);
byteOffset += featureTableBinaryByteLength;
let batchTableJson;
let batchTableBinary;
if (batchTableJsonByteLength > 0) {
// PERFORMANCE_IDEA: is it possible to allocate this on-demand? Perhaps keep the
// arraybuffer/string compressed in memory and then decompress it when it is first accessed.
//
// We could also make another request for it, but that would make the property set/get
// API async, and would double the number of numbers in some cases.
batchTableJson = getJsonFromTypedArray(
uint8Array,
byteOffset,
batchTableJsonByteLength,
);
byteOffset += batchTableJsonByteLength;
if (batchTableBinaryByteLength > 0) {
// Has a batch table binary
batchTableBinary = new Uint8Array(
arrayBuffer,
byteOffset,
batchTableBinaryByteLength,
);
// Copy the batchTableBinary section and let the underlying ArrayBuffer be freed
batchTableBinary = new Uint8Array(batchTableBinary);
byteOffset += batchTableBinaryByteLength;
}
}
const gltfByteLength = byteStart + byteLength - byteOffset;
if (gltfByteLength === 0) {
throw new RuntimeError("glTF byte length must be greater than 0.");
}
let gltfView;
if (byteOffset % 4 === 0) {
gltfView = new Uint8Array(arrayBuffer, byteOffset, gltfByteLength);
} else {
// Create a copy of the glb so that it is 4-byte aligned
B3dmParser._deprecationWarning(
"b3dm-glb-unaligned",
"The embedded glb is not aligned to a 4-byte boundary.",
);
gltfView = new Uint8Array(
uint8Array.subarray(byteOffset, byteOffset + gltfByteLength),
);
}
return {
batchLength: batchLength,
featureTableJson: featureTableJson,
featureTableBinary: featureTableBinary,
batchTableJson: batchTableJson,
batchTableBinary: batchTableBinary,
gltf: gltfView,
};
};
export default B3dmParser;
+636
View File
@@ -0,0 +1,636 @@
import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Cartesian4 from "../Core/Cartesian4.js";
import combine from "../Core/combine.js";
import ComponentDatatype from "../Core/ComponentDatatype.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
import PixelFormat from "../Core/PixelFormat.js";
import ContextLimits from "../Renderer/ContextLimits.js";
import PixelDatatype from "../Renderer/PixelDatatype.js";
import Sampler from "../Renderer/Sampler.js";
import Texture from "../Renderer/Texture.js";
/**
* Creates a texture to look up per instance attributes for batched primitives. For example, store each primitive's pick color in the texture.
*
* @alias BatchTable
* @constructor
* @private
*
* @param {Context} context The context in which the batch table is created.
* @param {object[]} attributes An array of objects describing a per instance attribute. Each object contains a datatype, components per attributes, whether it is normalized and a function name
* to retrieve the value in the vertex shader.
* @param {number} numberOfInstances The number of instances in a batch table.
*
* @example
* // create the batch table
* const attributes = [{
* functionName : 'getShow',
* componentDatatype : ComponentDatatype.UNSIGNED_BYTE,
* componentsPerAttribute : 1
* }, {
* functionName : 'getPickColor',
* componentDatatype : ComponentDatatype.UNSIGNED_BYTE,
* componentsPerAttribute : 4,
* normalize : true
* }];
* const batchTable = new BatchTable(context, attributes, 5);
*
* // when creating the draw commands, update the uniform map and the vertex shader
* vertexShaderSource = batchTable.getVertexShaderCallback()(vertexShaderSource);
* const shaderProgram = ShaderProgram.fromCache({
* // ...
* vertexShaderSource : vertexShaderSource,
* });
*
* drawCommand.shaderProgram = shaderProgram;
* drawCommand.uniformMap = batchTable.getUniformMapCallback()(uniformMap);
*
* // use the attribute function names in the shader to retrieve the instance values
* // ...
* attribute float batchId;
*
* void main() {
* // ...
* float show = getShow(batchId);
* vec3 pickColor = getPickColor(batchId);
* // ...
* }
*/
function BatchTable(context, attributes, numberOfInstances) {
//>>includeStart('debug', pragmas.debug);
if (!defined(context)) {
throw new DeveloperError("context is required");
}
if (!defined(attributes)) {
throw new DeveloperError("attributes is required");
}
if (!defined(numberOfInstances)) {
throw new DeveloperError("numberOfInstances is required");
}
//>>includeEnd('debug');
this._attributes = attributes;
this._numberOfInstances = numberOfInstances;
if (attributes.length === 0) {
return;
}
// PERFORMANCE_IDEA: We may be able to arrange the attributes so they can be packing into fewer texels.
// Right now, an attribute with one component uses an entire texel when 4 single component attributes can
// be packed into a texel.
//
// Packing floats into unsigned byte textures makes the problem worse. A single component float attribute
// will be packed into a single texel leaving 3 texels unused. 4 texels are reserved for each float attribute
// regardless of how many components it has.
const pixelDatatype = getDatatype(attributes);
const textureFloatSupported = context.floatingPointTexture;
const packFloats =
pixelDatatype === PixelDatatype.FLOAT && !textureFloatSupported;
const offsets = createOffsets(attributes, packFloats);
const stride = getStride(offsets, attributes, packFloats);
const maxNumberOfInstancesPerRow = Math.floor(
ContextLimits.maximumTextureSize / stride,
);
const instancesPerWidth = Math.min(
numberOfInstances,
maxNumberOfInstancesPerRow,
);
const width = stride * instancesPerWidth;
const height = Math.ceil(numberOfInstances / instancesPerWidth);
const stepX = 1.0 / width;
const centerX = stepX * 0.5;
const stepY = 1.0 / height;
const centerY = stepY * 0.5;
this._textureDimensions = new Cartesian2(width, height);
this._textureStep = new Cartesian4(stepX, centerX, stepY, centerY);
this._pixelDatatype = !packFloats
? pixelDatatype
: PixelDatatype.UNSIGNED_BYTE;
this._packFloats = packFloats;
this._offsets = offsets;
this._stride = stride;
this._texture = undefined;
const batchLength = 4 * width * height;
this._batchValues =
pixelDatatype === PixelDatatype.FLOAT && !packFloats
? new Float32Array(batchLength)
: new Uint8Array(batchLength);
this._batchValuesDirty = false;
}
Object.defineProperties(BatchTable.prototype, {
/**
* The attribute descriptions.
* @memberOf BatchTable.prototype
* @type {object[]}
* @readonly
*/
attributes: {
get: function () {
return this._attributes;
},
},
/**
* The number of instances.
* @memberOf BatchTable.prototype
* @type {number}
* @readonly
*/
numberOfInstances: {
get: function () {
return this._numberOfInstances;
},
},
});
function getDatatype(attributes) {
let foundFloatDatatype = false;
const length = attributes.length;
for (let i = 0; i < length; ++i) {
if (attributes[i].componentDatatype !== ComponentDatatype.UNSIGNED_BYTE) {
foundFloatDatatype = true;
break;
}
}
return foundFloatDatatype ? PixelDatatype.FLOAT : PixelDatatype.UNSIGNED_BYTE;
}
function getAttributeType(attributes, attributeIndex) {
const componentsPerAttribute =
attributes[attributeIndex].componentsPerAttribute;
if (componentsPerAttribute === 2) {
return Cartesian2;
} else if (componentsPerAttribute === 3) {
return Cartesian3;
} else if (componentsPerAttribute === 4) {
return Cartesian4;
}
return Number;
}
function createOffsets(attributes, packFloats) {
const offsets = new Array(attributes.length);
let currentOffset = 0;
const attributesLength = attributes.length;
for (let i = 0; i < attributesLength; ++i) {
const attribute = attributes[i];
const componentDatatype = attribute.componentDatatype;
offsets[i] = currentOffset;
if (componentDatatype !== ComponentDatatype.UNSIGNED_BYTE && packFloats) {
currentOffset += 4;
} else {
++currentOffset;
}
}
return offsets;
}
function getStride(offsets, attributes, packFloats) {
const length = offsets.length;
const lastOffset = offsets[length - 1];
const lastAttribute = attributes[length - 1];
const componentDatatype = lastAttribute.componentDatatype;
if (componentDatatype !== ComponentDatatype.UNSIGNED_BYTE && packFloats) {
return lastOffset + 4;
}
return lastOffset + 1;
}
const scratchPackedFloatCartesian4 = new Cartesian4();
function getPackedFloat(array, index, result) {
let packed = Cartesian4.unpack(array, index, scratchPackedFloatCartesian4);
const x = Cartesian4.unpackFloat(packed);
packed = Cartesian4.unpack(array, index + 4, scratchPackedFloatCartesian4);
const y = Cartesian4.unpackFloat(packed);
packed = Cartesian4.unpack(array, index + 8, scratchPackedFloatCartesian4);
const z = Cartesian4.unpackFloat(packed);
packed = Cartesian4.unpack(array, index + 12, scratchPackedFloatCartesian4);
const w = Cartesian4.unpackFloat(packed);
return Cartesian4.fromElements(x, y, z, w, result);
}
function setPackedAttribute(value, array, index) {
let packed = Cartesian4.packFloat(value.x, scratchPackedFloatCartesian4);
Cartesian4.pack(packed, array, index);
packed = Cartesian4.packFloat(value.y, packed);
Cartesian4.pack(packed, array, index + 4);
packed = Cartesian4.packFloat(value.z, packed);
Cartesian4.pack(packed, array, index + 8);
packed = Cartesian4.packFloat(value.w, packed);
Cartesian4.pack(packed, array, index + 12);
}
const scratchGetAttributeCartesian4 = new Cartesian4();
/**
* Gets the value of an attribute in the table.
*
* @param {number} instanceIndex The index of the instance.
* @param {number} attributeIndex The index of the attribute.
* @param {undefined|Cartesian2|Cartesian3|Cartesian4} [result] The object onto which to store the result. The type is dependent on the attribute's number of components.
* @returns {number|Cartesian2|Cartesian3|Cartesian4} The attribute value stored for the instance.
*
* @exception {DeveloperError} instanceIndex is out of range.
* @exception {DeveloperError} attributeIndex is out of range.
*/
BatchTable.prototype.getBatchedAttribute = function (
instanceIndex,
attributeIndex,
result,
) {
//>>includeStart('debug', pragmas.debug);
if (instanceIndex < 0 || instanceIndex >= this._numberOfInstances) {
throw new DeveloperError("instanceIndex is out of range.");
}
if (attributeIndex < 0 || attributeIndex >= this._attributes.length) {
throw new DeveloperError("attributeIndex is out of range");
}
//>>includeEnd('debug');
const attributes = this._attributes;
const offset = this._offsets[attributeIndex];
const stride = this._stride;
const index = 4 * stride * instanceIndex + 4 * offset;
let value;
if (
this._packFloats &&
attributes[attributeIndex].componentDatatype !== PixelDatatype.UNSIGNED_BYTE
) {
value = getPackedFloat(
this._batchValues,
index,
scratchGetAttributeCartesian4,
);
} else {
value = Cartesian4.unpack(
this._batchValues,
index,
scratchGetAttributeCartesian4,
);
}
const attributeType = getAttributeType(attributes, attributeIndex);
if (defined(attributeType.fromCartesian4)) {
return attributeType.fromCartesian4(value, result);
} else if (defined(attributeType.clone)) {
return attributeType.clone(value, result);
}
return value.x;
};
const setAttributeScratchValues = [
undefined,
undefined,
new Cartesian2(),
new Cartesian3(),
new Cartesian4(),
];
const setAttributeScratchCartesian4 = new Cartesian4();
/**
* Sets the value of an attribute in the table.
*
* @param {number} instanceIndex The index of the instance.
* @param {number} attributeIndex The index of the attribute.
* @param {number|Cartesian2|Cartesian3|Cartesian4} value The value to be stored in the table. The type of value will depend on the number of components of the attribute.
*
* @exception {DeveloperError} instanceIndex is out of range.
* @exception {DeveloperError} attributeIndex is out of range.
*/
BatchTable.prototype.setBatchedAttribute = function (
instanceIndex,
attributeIndex,
value,
) {
//>>includeStart('debug', pragmas.debug);
if (instanceIndex < 0 || instanceIndex >= this._numberOfInstances) {
throw new DeveloperError("instanceIndex is out of range.");
}
if (attributeIndex < 0 || attributeIndex >= this._attributes.length) {
throw new DeveloperError("attributeIndex is out of range");
}
if (!defined(value)) {
throw new DeveloperError("value is required.");
}
//>>includeEnd('debug');
const attributes = this._attributes;
const result =
setAttributeScratchValues[
attributes[attributeIndex].componentsPerAttribute
];
const currentAttribute = this.getBatchedAttribute(
instanceIndex,
attributeIndex,
result,
);
const attributeType = getAttributeType(this._attributes, attributeIndex);
const entriesEqual = defined(attributeType.equals)
? attributeType.equals(currentAttribute, value)
: currentAttribute === value;
if (entriesEqual) {
return;
}
const attributeValue = setAttributeScratchCartesian4;
attributeValue.x = defined(value.x) ? value.x : value;
attributeValue.y = defined(value.y) ? value.y : 0.0;
attributeValue.z = defined(value.z) ? value.z : 0.0;
attributeValue.w = defined(value.w) ? value.w : 0.0;
const offset = this._offsets[attributeIndex];
const stride = this._stride;
const index = 4 * stride * instanceIndex + 4 * offset;
if (
this._packFloats &&
attributes[attributeIndex].componentDatatype !== PixelDatatype.UNSIGNED_BYTE
) {
setPackedAttribute(attributeValue, this._batchValues, index);
} else {
Cartesian4.pack(attributeValue, this._batchValues, index);
}
this._batchValuesDirty = true;
};
function createTexture(batchTable, context) {
const dimensions = batchTable._textureDimensions;
batchTable._texture = new Texture({
context: context,
pixelFormat: PixelFormat.RGBA,
pixelDatatype: batchTable._pixelDatatype,
width: dimensions.x,
height: dimensions.y,
sampler: Sampler.NEAREST,
flipY: false,
});
}
function updateTexture(batchTable) {
const dimensions = batchTable._textureDimensions;
batchTable._texture.copyFrom({
source: {
width: dimensions.x,
height: dimensions.y,
arrayBufferView: batchTable._batchValues,
},
});
}
/**
* Creates/updates the batch table texture.
* @param {FrameState} frameState The frame state.
*
* @exception {RuntimeError} The floating point texture extension is required but not supported.
*/
BatchTable.prototype.update = function (frameState) {
if (
(defined(this._texture) && !this._batchValuesDirty) ||
this._attributes.length === 0
) {
return;
}
this._batchValuesDirty = false;
if (!defined(this._texture)) {
createTexture(this, frameState.context);
}
updateTexture(this);
};
/**
* Gets a function that will update a uniform map to contain values for looking up values in the batch table.
*
* @returns {BatchTable.updateUniformMapCallback} A callback for updating uniform maps.
*/
BatchTable.prototype.getUniformMapCallback = function () {
const that = this;
return function (uniformMap) {
if (that._attributes.length === 0) {
return uniformMap;
}
const batchUniformMap = {
batchTexture: function () {
return that._texture;
},
batchTextureDimensions: function () {
return that._textureDimensions;
},
batchTextureStep: function () {
return that._textureStep;
},
};
return combine(uniformMap, batchUniformMap);
};
};
function getGlslComputeSt(batchTable) {
const stride = batchTable._stride;
// GLSL batchId is zero-based: [0, numberOfInstances - 1]
if (batchTable._textureDimensions.y === 1) {
return (
`${
"uniform vec4 batchTextureStep; \n" +
"vec2 computeSt(float batchId) \n" +
"{ \n" +
" float stepX = batchTextureStep.x; \n" +
" float centerX = batchTextureStep.y; \n" +
" float numberOfAttributes = float("
}${stride}); \n` +
` return vec2(centerX + (batchId * numberOfAttributes * stepX), 0.5); \n` +
`} \n`
);
}
return (
`${
"uniform vec4 batchTextureStep; \n" +
"uniform vec2 batchTextureDimensions; \n" +
"vec2 computeSt(float batchId) \n" +
"{ \n" +
" float stepX = batchTextureStep.x; \n" +
" float centerX = batchTextureStep.y; \n" +
" float stepY = batchTextureStep.z; \n" +
" float centerY = batchTextureStep.w; \n" +
" float numberOfAttributes = float("
}${stride}); \n` +
` float xId = mod(batchId * numberOfAttributes, batchTextureDimensions.x); \n` +
` float yId = floor(batchId * numberOfAttributes / batchTextureDimensions.x); \n` +
` return vec2(centerX + (xId * stepX), centerY + (yId * stepY)); \n` +
`} \n`
);
}
function getComponentType(componentsPerAttribute) {
if (componentsPerAttribute === 1) {
return "float";
}
return `vec${componentsPerAttribute}`;
}
function getComponentSwizzle(componentsPerAttribute) {
if (componentsPerAttribute === 1) {
return ".x";
} else if (componentsPerAttribute === 2) {
return ".xy";
} else if (componentsPerAttribute === 3) {
return ".xyz";
}
return "";
}
function getGlslAttributeFunction(batchTable, attributeIndex) {
const attributes = batchTable._attributes;
const attribute = attributes[attributeIndex];
const componentsPerAttribute = attribute.componentsPerAttribute;
const functionName = attribute.functionName;
const functionReturnType = getComponentType(componentsPerAttribute);
const functionReturnValue = getComponentSwizzle(componentsPerAttribute);
const offset = batchTable._offsets[attributeIndex];
let glslFunction =
`${functionReturnType} ${functionName}(float batchId) \n` +
`{ \n` +
` vec2 st = computeSt(batchId); \n` +
` st.x += batchTextureStep.x * float(${offset}); \n`;
if (
batchTable._packFloats &&
attribute.componentDatatype !== PixelDatatype.UNSIGNED_BYTE
) {
glslFunction +=
"vec4 textureValue; \n" +
"textureValue.x = czm_unpackFloat(texture(batchTexture, st)); \n" +
"textureValue.y = czm_unpackFloat(texture(batchTexture, st + vec2(batchTextureStep.x, 0.0))); \n" +
"textureValue.z = czm_unpackFloat(texture(batchTexture, st + vec2(batchTextureStep.x * 2.0, 0.0))); \n" +
"textureValue.w = czm_unpackFloat(texture(batchTexture, st + vec2(batchTextureStep.x * 3.0, 0.0))); \n";
} else {
glslFunction += " vec4 textureValue = texture(batchTexture, st); \n";
}
glslFunction += ` ${functionReturnType} value = textureValue${functionReturnValue}; \n`;
if (
batchTable._pixelDatatype === PixelDatatype.UNSIGNED_BYTE &&
attribute.componentDatatype === ComponentDatatype.UNSIGNED_BYTE &&
!attribute.normalize
) {
glslFunction += "value *= 255.0; \n";
} else if (
batchTable._pixelDatatype === PixelDatatype.FLOAT &&
attribute.componentDatatype === ComponentDatatype.UNSIGNED_BYTE &&
attribute.normalize
) {
glslFunction += "value /= 255.0; \n";
}
glslFunction += " return value; \n" + "} \n";
return glslFunction;
}
/**
* Gets a function that will update a vertex shader to contain functions for looking up values in the batch table.
*
* @returns {BatchTable.updateVertexShaderSourceCallback} A callback for updating a vertex shader source.
*/
BatchTable.prototype.getVertexShaderCallback = function () {
const attributes = this._attributes;
if (attributes.length === 0) {
return function (source) {
return source;
};
}
let batchTableShader = "uniform highp sampler2D batchTexture; \n";
batchTableShader += `${getGlslComputeSt(this)}\n`;
const length = attributes.length;
for (let i = 0; i < length; ++i) {
batchTableShader += getGlslAttributeFunction(this, i);
}
return function (source) {
const mainIndex = source.indexOf("void main");
const beforeMain = source.substring(0, mainIndex);
const afterMain = source.substring(mainIndex);
return `${beforeMain}\n${batchTableShader}\n${afterMain}`;
};
};
/**
* Returns true if this object was destroyed; otherwise, false.
* <br /><br />
* If this object was destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
*
* @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
*
* @see BatchTable#destroy
*/
BatchTable.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.
* <br /><br />
* Once an object is destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
* assign the return value (<code>undefined</code>) to the object as done in the example.
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
* @see BatchTable#isDestroyed
*/
BatchTable.prototype.destroy = function () {
this._texture = this._texture && this._texture.destroy();
return destroyObject(this);
};
/**
* A callback for updating uniform maps.
* @callback BatchTable.updateUniformMapCallback
*
* @param {object} uniformMap The uniform map.
* @returns {object} The new uniform map with properties for retrieving values from the batch table.
*/
/**
* A callback for updating a vertex shader source.
* @callback BatchTable.updateVertexShaderSourceCallback
*
* @param {string} vertexShaderSource The vertex shader source.
* @returns {string} The new vertex shader source with the functions for retrieving batch table values injected.
*/
export default BatchTable;
+504
View File
@@ -0,0 +1,504 @@
import AttributeType from "./AttributeType.js";
import Check from "../Core/Check.js";
import clone from "../Core/clone.js";
import combine from "../Core/combine.js";
import ComponentDatatype from "../Core/ComponentDatatype.js";
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
import getBinaryAccessor from "./getBinaryAccessor.js";
import Cesium3DTileBatchTable from "./Cesium3DTileBatchTable.js";
/**
* Object for handling the <code>3DTILES_batch_table_hierarchy</code> extension
*
* @param {object} options Object with the following properties:
* @param {object} options.extension The <code>3DTILES_batch_table_hierarchy</code> extension object.
* @param {Uint8Array} [options.binaryBody] The binary body of the batch table
*
* @alias BatchTableHierarchy
* @constructor
*
* @private
*/
function BatchTableHierarchy(options) {
this._classes = undefined;
this._classIds = undefined;
this._classIndexes = undefined;
this._parentCounts = undefined;
this._parentIndexes = undefined;
this._parentIds = undefined;
// Total memory used by the typed arrays
this._byteLength = 0;
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options.extension", options.extension);
//>>includeEnd('debug');
initialize(this, options.extension, options.binaryBody);
//>>includeStart('debug', pragmas.debug);
validateHierarchy(this);
//>>includeEnd('debug');
}
Object.defineProperties(BatchTableHierarchy.prototype, {
byteLength: {
get: function () {
return this._byteLength;
},
},
});
/**
* Parse the batch table hierarchy from the
* <code>3DTILES_batch_table_hierarchy</code> extension.
*
* @param {BatchTableHierarchy} hierarchy The hierarchy instance
* @param {object} hierarchyJson The JSON of the extension
* @param {Uint8Array} [binaryBody] The binary body of the batch table for accessing binary properties
* @private
*/
function initialize(hierarchy, hierarchyJson, binaryBody) {
let i;
let classId;
let binaryAccessor;
const instancesLength = hierarchyJson.instancesLength;
const classes = hierarchyJson.classes;
let classIds = hierarchyJson.classIds;
let parentCounts = hierarchyJson.parentCounts;
let parentIds = hierarchyJson.parentIds;
let parentIdsLength = instancesLength;
let byteLength = 0;
if (defined(classIds.byteOffset)) {
classIds.componentType =
classIds.componentType ?? ComponentDatatype.UNSIGNED_SHORT;
classIds.type = AttributeType.SCALAR;
binaryAccessor = getBinaryAccessor(classIds);
classIds = binaryAccessor.createArrayBufferView(
binaryBody.buffer,
binaryBody.byteOffset + classIds.byteOffset,
instancesLength,
);
byteLength += classIds.byteLength;
}
let parentIndexes;
if (defined(parentCounts)) {
if (defined(parentCounts.byteOffset)) {
parentCounts.componentType =
parentCounts.componentType ?? ComponentDatatype.UNSIGNED_SHORT;
parentCounts.type = AttributeType.SCALAR;
binaryAccessor = getBinaryAccessor(parentCounts);
parentCounts = binaryAccessor.createArrayBufferView(
binaryBody.buffer,
binaryBody.byteOffset + parentCounts.byteOffset,
instancesLength,
);
byteLength += parentCounts.byteLength;
}
parentIndexes = new Uint16Array(instancesLength);
parentIdsLength = 0;
for (i = 0; i < instancesLength; ++i) {
parentIndexes[i] = parentIdsLength;
parentIdsLength += parentCounts[i];
}
byteLength += parentIndexes.byteLength;
}
if (defined(parentIds) && defined(parentIds.byteOffset)) {
parentIds.componentType =
parentIds.componentType ?? ComponentDatatype.UNSIGNED_SHORT;
parentIds.type = AttributeType.SCALAR;
binaryAccessor = getBinaryAccessor(parentIds);
parentIds = binaryAccessor.createArrayBufferView(
binaryBody.buffer,
binaryBody.byteOffset + parentIds.byteOffset,
parentIdsLength,
);
byteLength += parentIds.byteLength;
}
const classesLength = classes.length;
for (i = 0; i < classesLength; ++i) {
const classInstancesLength = classes[i].length;
const properties = classes[i].instances;
const binaryProperties = Cesium3DTileBatchTable.getBinaryProperties(
classInstancesLength,
properties,
binaryBody,
);
byteLength += countBinaryPropertyMemory(binaryProperties);
classes[i].instances = combine(binaryProperties, properties);
}
const classCounts = new Array(classesLength).fill(0);
const classIndexes = new Uint16Array(instancesLength);
for (i = 0; i < instancesLength; ++i) {
classId = classIds[i];
classIndexes[i] = classCounts[classId];
++classCounts[classId];
}
byteLength += classIndexes.byteLength;
hierarchy._classes = classes;
hierarchy._classIds = classIds;
hierarchy._classIndexes = classIndexes;
hierarchy._parentCounts = parentCounts;
hierarchy._parentIndexes = parentIndexes;
hierarchy._parentIds = parentIds;
hierarchy._byteLength = byteLength;
}
function countBinaryPropertyMemory(binaryProperties) {
let byteLength = 0;
for (const name in binaryProperties) {
if (binaryProperties.hasOwnProperty(name)) {
byteLength += binaryProperties[name].typedArray.byteLength;
}
}
return byteLength;
}
//>>includeStart('debug', pragmas.debug);
const scratchValidateStack = [];
function validateHierarchy(hierarchy) {
const stack = scratchValidateStack;
stack.length = 0;
const classIds = hierarchy._classIds;
const instancesLength = classIds.length;
for (let i = 0; i < instancesLength; ++i) {
validateInstance(hierarchy, i, stack);
}
}
function validateInstance(hierarchy, instanceIndex, stack) {
const parentCounts = hierarchy._parentCounts;
const parentIds = hierarchy._parentIds;
const parentIndexes = hierarchy._parentIndexes;
const classIds = hierarchy._classIds;
const instancesLength = classIds.length;
if (!defined(parentIds)) {
// No need to validate if there are no parents
return;
}
if (instanceIndex >= instancesLength) {
throw new DeveloperError(
`Parent index ${instanceIndex} exceeds the total number of instances: ${instancesLength}`,
);
}
if (stack.indexOf(instanceIndex) > -1) {
throw new DeveloperError(
"Circular dependency detected in the batch table hierarchy.",
);
}
stack.push(instanceIndex);
const parentCount = defined(parentCounts) ? parentCounts[instanceIndex] : 1;
const parentIndex = defined(parentCounts)
? parentIndexes[instanceIndex]
: instanceIndex;
for (let i = 0; i < parentCount; ++i) {
const parentId = parentIds[parentIndex + i];
// Stop the traversal when the instance has no parent (its parentId equals itself), else continue the traversal.
if (parentId !== instanceIndex) {
validateInstance(hierarchy, parentId, stack);
}
}
stack.pop(instanceIndex);
}
//>>includeEnd('debug');
// The size of this array equals the maximum instance count among all loaded tiles, which has the potential to be large.
const scratchVisited = [];
const scratchStack = [];
let marker = 0;
function traverseHierarchyMultipleParents(
hierarchy,
instanceIndex,
endConditionCallback,
) {
const classIds = hierarchy._classIds;
const parentCounts = hierarchy._parentCounts;
const parentIds = hierarchy._parentIds;
const parentIndexes = hierarchy._parentIndexes;
const instancesLength = classIds.length;
// Ignore instances that have already been visited. This occurs in diamond inheritance situations.
// Use a marker value to indicate that an instance has been visited, which increments with each run.
// This is more efficient than clearing the visited array every time.
const visited = scratchVisited;
visited.length = Math.max(visited.length, instancesLength);
const visitedMarker = ++marker;
const stack = scratchStack;
stack.length = 0;
stack.push(instanceIndex);
while (stack.length > 0) {
instanceIndex = stack.pop();
if (visited[instanceIndex] === visitedMarker) {
// This instance has already been visited, stop traversal
continue;
}
visited[instanceIndex] = visitedMarker;
const result = endConditionCallback(hierarchy, instanceIndex);
if (defined(result)) {
// The end condition was met, stop the traversal and return the result
return result;
}
const parentCount = parentCounts[instanceIndex];
const parentIndex = parentIndexes[instanceIndex];
for (let i = 0; i < parentCount; ++i) {
const parentId = parentIds[parentIndex + i];
// Stop the traversal when the instance has no parent (its parentId equals itself)
// else add the parent to the stack to continue the traversal.
if (parentId !== instanceIndex) {
stack.push(parentId);
}
}
}
}
function traverseHierarchySingleParent(
hierarchy,
instanceIndex,
endConditionCallback,
) {
let hasParent = true;
while (hasParent) {
const result = endConditionCallback(hierarchy, instanceIndex);
if (defined(result)) {
// The end condition was met, stop the traversal and return the result
return result;
}
const parentId = hierarchy._parentIds[instanceIndex];
hasParent = parentId !== instanceIndex;
instanceIndex = parentId;
}
}
function traverseHierarchy(hierarchy, instanceIndex, endConditionCallback) {
// Traverse over the hierarchy and process each instance with the endConditionCallback.
// When the endConditionCallback returns a value, the traversal stops and that value is returned.
const parentCounts = hierarchy._parentCounts;
const parentIds = hierarchy._parentIds;
if (!defined(parentIds)) {
return endConditionCallback(hierarchy, instanceIndex);
} else if (defined(parentCounts)) {
return traverseHierarchyMultipleParents(
hierarchy,
instanceIndex,
endConditionCallback,
);
}
return traverseHierarchySingleParent(
hierarchy,
instanceIndex,
endConditionCallback,
);
}
/**
* Returns whether the feature has this property.
*
* @param {number} batchId the batch ID of the feature
* @param {string} propertyId The case-sensitive ID of the property.
* @returns {boolean} Whether the feature has this property.
* @private
*/
BatchTableHierarchy.prototype.hasProperty = function (batchId, propertyId) {
const result = traverseHierarchy(
this,
batchId,
function (hierarchy, instanceIndex) {
const classId = hierarchy._classIds[instanceIndex];
const instances = hierarchy._classes[classId].instances;
if (defined(instances[propertyId])) {
return true;
}
},
);
return defined(result);
};
/**
* Returns whether any feature has this property.
*
* @param {string} propertyId The case-sensitive ID of the property.
* @returns {boolean} Whether any feature has this property.
* @private
*/
BatchTableHierarchy.prototype.propertyExists = function (propertyId) {
const classes = this._classes;
const classesLength = classes.length;
for (let i = 0; i < classesLength; ++i) {
const instances = classes[i].instances;
if (defined(instances[propertyId])) {
return true;
}
}
return false;
};
/**
* Returns an array of property IDs.
*
* @param {number} batchId the batch ID of the feature
* @param {number} index The index of the entity.
* @param {string[]} [results] An array into which to store the results.
* @returns {string[]} The property IDs.
* @private
*/
BatchTableHierarchy.prototype.getPropertyIds = function (batchId, results) {
results = defined(results) ? results : [];
results.length = 0;
traverseHierarchy(this, batchId, function (hierarchy, instanceIndex) {
const classId = hierarchy._classIds[instanceIndex];
const instances = hierarchy._classes[classId].instances;
for (const name in instances) {
if (instances.hasOwnProperty(name)) {
if (results.indexOf(name) === -1) {
results.push(name);
}
}
}
});
return results;
};
/**
* Returns a copy of the value of the property with the given ID.
*
* @param {number} batchId the batch ID of the feature
* @param {string} propertyId The case-sensitive ID of the property.
* @returns {*} The value of the property or <code>undefined</code> if the feature does not have this property.
* @private
*/
BatchTableHierarchy.prototype.getProperty = function (batchId, propertyId) {
return traverseHierarchy(this, batchId, function (hierarchy, instanceIndex) {
const classId = hierarchy._classIds[instanceIndex];
const instanceClass = hierarchy._classes[classId];
const indexInClass = hierarchy._classIndexes[instanceIndex];
const propertyValues = instanceClass.instances[propertyId];
if (defined(propertyValues)) {
if (defined(propertyValues.typedArray)) {
return getBinaryProperty(propertyValues, indexInClass);
}
return clone(propertyValues[indexInClass], true);
}
});
};
function getBinaryProperty(binaryProperty, index) {
const typedArray = binaryProperty.typedArray;
const componentCount = binaryProperty.componentCount;
if (componentCount === 1) {
return typedArray[index];
}
return binaryProperty.type.unpack(typedArray, index * componentCount);
}
/**
* Sets the value of the property with the given ID. Only properties of the
* instance may be set; parent properties may not be set.
*
* @param {number} batchId The batchId of the feature
* @param {string} propertyId The case-sensitive ID of the property.
* @param {*} value The value of the property that will be copied.
* @returns {boolean} <code>true</code> if the property was set, <code>false</code> otherwise.
*
* @exception {DeveloperError} when setting an inherited property
* @private
*/
BatchTableHierarchy.prototype.setProperty = function (
batchId,
propertyId,
value,
) {
const result = traverseHierarchy(
this,
batchId,
function (hierarchy, instanceIndex) {
const classId = hierarchy._classIds[instanceIndex];
const instanceClass = hierarchy._classes[classId];
const indexInClass = hierarchy._classIndexes[instanceIndex];
const propertyValues = instanceClass.instances[propertyId];
if (defined(propertyValues)) {
//>>includeStart('debug', pragmas.debug);
if (instanceIndex !== batchId) {
throw new DeveloperError(
`Inherited property "${propertyId}" is read-only.`,
);
}
//>>includeEnd('debug');
if (defined(propertyValues.typedArray)) {
setBinaryProperty(propertyValues, indexInClass, value);
} else {
propertyValues[indexInClass] = clone(value, true);
}
return true;
}
},
);
return defined(result);
};
function setBinaryProperty(binaryProperty, index, value) {
const typedArray = binaryProperty.typedArray;
const componentCount = binaryProperty.componentCount;
if (componentCount === 1) {
typedArray[index] = value;
} else {
binaryProperty.type.pack(value, typedArray, index * componentCount);
}
}
/**
* Check if a feature belongs to a class with the given name
*
* @param {number} batchId The batch ID of the feature
* @param {string} className The name of the class
* @return {boolean} <code>true</code> if the feature belongs to the class given by className, or <code>false</code> otherwise
* @private
*/
BatchTableHierarchy.prototype.isClass = function (batchId, className) {
// PERFORMANCE_IDEA : cache results in the ancestor classes to speed up this check if this area becomes a hotspot
// PERFORMANCE_IDEA : treat class names as integers for faster comparisons
const result = traverseHierarchy(
this,
batchId,
function (hierarchy, instanceIndex) {
const classId = hierarchy._classIds[instanceIndex];
const instanceClass = hierarchy._classes[classId];
if (instanceClass.name === className) {
return true;
}
},
);
return defined(result);
};
/**
* Get the name of the class a given feature belongs to
*
* @param {number} batchId The batch ID of the feature
* @return {string} The name of the class this feature belongs to
*/
BatchTableHierarchy.prototype.getClassName = function (batchId) {
const classId = this._classIds[batchId];
const instanceClass = this._classes[classId];
return instanceClass.name;
};
export default BatchTableHierarchy;
+574
View File
@@ -0,0 +1,574 @@
import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian4 from "../Core/Cartesian4.js";
import Check from "../Core/Check.js";
import Color from "../Core/Color.js";
import createGuid from "../Core/createGuid.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
import PixelFormat from "../Core/PixelFormat.js";
import ContextLimits from "../Renderer/ContextLimits.js";
import PixelDatatype from "../Renderer/PixelDatatype.js";
import Sampler from "../Renderer/Sampler.js";
import Texture from "../Renderer/Texture.js";
/**
* An object that manages color, show/hide and picking textures for a batch
* table or feature table.
*
* @param {object} options Object with the following properties:
* @param {number} featuresLength The number of features in the batch table or feature table
* @param {Cesium3DTileContent|ModelFeatureTable} owner The owner of this batch texture. For 3D Tiles, this will be a {@link Cesium3DTileContent}. For glTF models, this will be a {@link ModelFeatureTable}.
* @param {object} [statistics] The statistics object to update with information about the batch texture.
* @param {Function} [colorChangedCallback] A callback function that is called whenever the color of a feature changes.
*
* @alias BatchTexture
* @constructor
*
* @private
*/
function BatchTexture(options) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("options.featuresLength", options.featuresLength);
Check.typeOf.object("options.owner", options.owner);
//>>includeEnd('debug');
this._id = createGuid();
const featuresLength = options.featuresLength;
// PERFORMANCE_IDEA: These parallel arrays probably generate cache misses in get/set color/show
// and use A LOT of memory. How can we use less memory?
this._showAlphaProperties = undefined; // [Show (0 or 255), Alpha (0 to 255)] property for each feature
this._batchValues = undefined; // Per-feature RGBA (A is based on the color's alpha and feature's show property)
this._batchValuesDirty = false;
this._batchTexture = undefined;
this._defaultTexture = undefined;
this._pickTexture = undefined;
this._pickIds = [];
// Dimensions for batch and pick textures
let textureDimensions;
let textureStep;
if (featuresLength > 0) {
// PERFORMANCE_IDEA: this can waste memory in the last row in the uncommon case
// when more than one row is needed (e.g., > 16K features in one tile)
const width = Math.min(featuresLength, ContextLimits.maximumTextureSize);
const height = Math.ceil(featuresLength / ContextLimits.maximumTextureSize);
const stepX = 1.0 / width;
const centerX = stepX * 0.5;
const stepY = 1.0 / height;
const centerY = stepY * 0.5;
textureDimensions = new Cartesian2(width, height);
textureStep = new Cartesian4(stepX, centerX, stepY, centerY);
}
this._translucentFeaturesLength = 0;
this._featuresLength = featuresLength;
this._textureDimensions = textureDimensions;
this._textureStep = textureStep;
this._owner = options.owner;
this._statistics = options.statistics;
this._colorChangedCallback = options.colorChangedCallback;
}
Object.defineProperties(BatchTexture.prototype, {
/**
* Number of features that are translucent
*
* @memberof BatchTexture.prototype
* @type {number}
* @readonly
* @private
*/
translucentFeaturesLength: {
get: function () {
return this._translucentFeaturesLength;
},
},
/**
* Total size of all GPU resources used by this batch texture.
*
* @memberof BatchTexture.prototype
* @type {number}
* @readonly
* @private
*/
byteLength: {
get: function () {
let memory = 0;
if (defined(this._pickTexture)) {
memory += this._pickTexture.sizeInBytes;
}
if (defined(this._batchTexture)) {
memory += this._batchTexture.sizeInBytes;
}
return memory;
},
},
/**
* Dimensions of the underlying batch texture.
*
* @memberof BatchTexture.prototype
* @type {Cartesian2}
* @readonly
* @private
*/
textureDimensions: {
get: function () {
return this._textureDimensions;
},
},
/**
* Size of each texture and distance from side to center of a texel in
* each direction. Stored as (stepX, centerX, stepY, centerY)
*
* @memberof BatchTexture.prototype
* @type {Cartesian4}
* @readonly
* @private
*/
textureStep: {
get: function () {
return this._textureStep;
},
},
/**
* The underlying texture used for styling. The texels are accessed
* by batch ID, and the value is the color of this feature after accounting
* for show/hide settings.
*
* @memberof BatchTexture.prototype
* @type {Texture}
* @readonly
* @private
*/
batchTexture: {
get: function () {
return this._batchTexture;
},
},
/**
* The default texture to use when there are no batch values
*
* @memberof BatchTexture.prototype
* @type {Texture}
* @readonly
* @private
*/
defaultTexture: {
get: function () {
return this._defaultTexture;
},
},
/**
* The underlying texture used for picking. The texels are accessed by
* batch ID, and the value is the pick color.
*
* @memberof BatchTexture.prototype
* @type {Texture}
* @readonly
* @private
*/
pickTexture: {
get: function () {
return this._pickTexture;
},
},
});
BatchTexture.DEFAULT_COLOR_VALUE = Color.WHITE;
BatchTexture.DEFAULT_SHOW_VALUE = true;
function getByteLength(batchTexture) {
const dimensions = batchTexture._textureDimensions;
return dimensions.x * dimensions.y * 4;
}
function getBatchValues(batchTexture) {
if (!defined(batchTexture._batchValues)) {
// Default batch texture to RGBA = 255: white highlight (RGB) and show/alpha = true/255 (A).
const byteLength = getByteLength(batchTexture);
const bytes = new Uint8Array(byteLength).fill(255);
batchTexture._batchValues = bytes;
}
return batchTexture._batchValues;
}
function getShowAlphaProperties(batchTexture) {
if (!defined(batchTexture._showAlphaProperties)) {
const byteLength = 2 * batchTexture._featuresLength;
const bytes = new Uint8Array(byteLength).fill(255);
// [Show = true, Alpha = 255]
batchTexture._showAlphaProperties = bytes;
}
return batchTexture._showAlphaProperties;
}
function checkBatchId(batchId, featuresLength) {
if (!defined(batchId) || batchId < 0 || batchId >= featuresLength) {
throw new DeveloperError(
`batchId is required and between zero and featuresLength - 1 (${featuresLength}` -
+").",
);
}
}
/**
* Set whether a feature is visible.
*
* @param {number} batchId the ID of the feature
* @param {boolean} show <code>true</code> if the feature should be shown, <code>false</code> otherwise
* @private
*/
BatchTexture.prototype.setShow = function (batchId, show) {
//>>includeStart('debug', pragmas.debug);
checkBatchId(batchId, this._featuresLength);
Check.typeOf.bool("show", show);
//>>includeEnd('debug');
if (show && !defined(this._showAlphaProperties)) {
// Avoid allocating since the default is show = true
return;
}
const showAlphaProperties = getShowAlphaProperties(this);
const propertyOffset = batchId * 2;
const newShow = show ? 255 : 0;
if (showAlphaProperties[propertyOffset] !== newShow) {
showAlphaProperties[propertyOffset] = newShow;
const batchValues = getBatchValues(this);
// Compute alpha used in the shader based on show and color.alpha properties
const offset = batchId * 4 + 3;
batchValues[offset] = show ? showAlphaProperties[propertyOffset + 1] : 0;
this._batchValuesDirty = true;
}
};
/**
* Set the show for all features at once.
*
* @param {boolean} show <code>true</code> if the feature should be shown, <code>false</code> otherwise
* @private
*/
BatchTexture.prototype.setAllShow = function (show) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.bool("show", show);
//>>includeEnd('debug');
const featuresLength = this._featuresLength;
for (let i = 0; i < featuresLength; ++i) {
this.setShow(i, show);
}
};
/**
* Check the current show value for a feature
*
* @param {number} batchId the ID of the feature
* @return {boolean} <code>true</code> if the feature is shown, or <code>false</code> otherwise
* @private
*/
BatchTexture.prototype.getShow = function (batchId) {
//>>includeStart('debug', pragmas.debug);
checkBatchId(batchId, this._featuresLength);
//>>includeEnd('debug');
if (!defined(this._showAlphaProperties)) {
// Avoid allocating since the default is show = true
return true;
}
const offset = batchId * 2;
return this._showAlphaProperties[offset] === 255;
};
const scratchColorBytes = new Array(4);
/**
* Set the styling color of a feature
*
* @param {number} batchId the ID of the feature
* @param {Color} color the color to assign to this feature.
*
* @private
*/
BatchTexture.prototype.setColor = function (batchId, color) {
//>>includeStart('debug', pragmas.debug);
checkBatchId(batchId, this._featuresLength);
Check.typeOf.object("color", color);
//>>includeEnd('debug');
if (
Color.equals(color, BatchTexture.DEFAULT_COLOR_VALUE) &&
!defined(this._batchValues)
) {
// Avoid allocating since the default is white
return;
}
const newColor = color.toBytes(scratchColorBytes);
const newAlpha = newColor[3];
const batchValues = getBatchValues(this);
const offset = batchId * 4;
const showAlphaProperties = getShowAlphaProperties(this);
const propertyOffset = batchId * 2;
if (
batchValues[offset] !== newColor[0] ||
batchValues[offset + 1] !== newColor[1] ||
batchValues[offset + 2] !== newColor[2] ||
showAlphaProperties[propertyOffset + 1] !== newAlpha
) {
batchValues[offset] = newColor[0];
batchValues[offset + 1] = newColor[1];
batchValues[offset + 2] = newColor[2];
const wasTranslucent = showAlphaProperties[propertyOffset + 1] !== 255;
// Compute alpha used in the shader based on show and color.alpha properties
const show = showAlphaProperties[propertyOffset] !== 0;
batchValues[offset + 3] = show ? newAlpha : 0;
showAlphaProperties[propertyOffset + 1] = newAlpha;
// Track number of translucent features so we know if this tile needs
// opaque commands, translucent commands, or both for rendering.
const isTranslucent = newAlpha !== 255;
if (isTranslucent && !wasTranslucent) {
++this._translucentFeaturesLength;
} else if (!isTranslucent && wasTranslucent) {
--this._translucentFeaturesLength;
}
this._batchValuesDirty = true;
if (defined(this._colorChangedCallback)) {
this._colorChangedCallback(batchId, color);
}
}
};
/**
* Set the styling color for all features at once
*
* @param {Color} color the color to assign to all features.
*
* @private
*/
BatchTexture.prototype.setAllColor = function (color) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("color", color);
//>>includeEnd('debug');
const featuresLength = this._featuresLength;
for (let i = 0; i < featuresLength; ++i) {
this.setColor(i, color);
}
};
/**
* Get the current color of a feature
*
* @param {number} batchId The ID of the feature
* @param {Color} result A color object where the result will be stored.
* @return {Color} The color assigned to the selected feature
*
* @private
*/
BatchTexture.prototype.getColor = function (batchId, result) {
//>>includeStart('debug', pragmas.debug);
checkBatchId(batchId, this._featuresLength);
Check.typeOf.object("result", result);
//>>includeEnd('debug');
if (!defined(this._batchValues)) {
return Color.clone(BatchTexture.DEFAULT_COLOR_VALUE, result);
}
const batchValues = this._batchValues;
const offset = batchId * 4;
const showAlphaProperties = this._showAlphaProperties;
const propertyOffset = batchId * 2;
return Color.fromBytes(
batchValues[offset],
batchValues[offset + 1],
batchValues[offset + 2],
showAlphaProperties[propertyOffset + 1],
result,
);
};
/**
* Get the pick color of a feature. This feature is an RGBA encoding of the
* pick ID.
*
* @param {number} batchId The ID of the feature
* @return {PickId} The picking color assigned to this feature
*
* @private
*/
BatchTexture.prototype.getPickColor = function (batchId) {
//>>includeStart('debug', pragmas.debug);
checkBatchId(batchId, this._featuresLength);
//>>includeEnd('debug');
return this._pickIds[batchId];
};
function createTexture(batchTexture, context, bytes) {
const dimensions = batchTexture._textureDimensions;
return new Texture({
context: context,
pixelFormat: PixelFormat.RGBA,
pixelDatatype: PixelDatatype.UNSIGNED_BYTE,
source: {
width: dimensions.x,
height: dimensions.y,
arrayBufferView: bytes,
},
flipY: false,
sampler: Sampler.NEAREST,
});
}
function createPickTexture(batchTexture, context) {
const featuresLength = batchTexture._featuresLength;
if (!defined(batchTexture._pickTexture) && featuresLength > 0) {
const pickIds = batchTexture._pickIds;
const byteLength = getByteLength(batchTexture);
const bytes = new Uint8Array(byteLength);
const owner = batchTexture._owner;
const statistics = batchTexture._statistics;
// PERFORMANCE_IDEA: we could skip the pick texture completely by allocating
// a continuous range of pickIds and then converting the base pickId + batchId
// to RGBA in the shader. The only consider is precision issues, which might
// not be an issue in WebGL 2.
for (let i = 0; i < featuresLength; ++i) {
const pickId = context.createPickId(owner.getFeature(i));
pickIds.push(pickId);
const pickColor = pickId.color;
const offset = i * 4;
bytes[offset] = Color.floatToByte(pickColor.red);
bytes[offset + 1] = Color.floatToByte(pickColor.green);
bytes[offset + 2] = Color.floatToByte(pickColor.blue);
bytes[offset + 3] = Color.floatToByte(pickColor.alpha);
}
batchTexture._pickTexture = createTexture(batchTexture, context, bytes);
// Make sure the tileset statistics are updated the frame when the pick
// texture is created.
if (defined(statistics)) {
statistics.batchTableByteLength += batchTexture._pickTexture.sizeInBytes;
}
}
}
function updateBatchTexture(batchTexture) {
const dimensions = batchTexture._textureDimensions;
// PERFORMANCE_IDEA: Instead of rewriting the entire texture, use fine-grained
// texture updates when less than, for example, 10%, of the values changed. Or
// even just optimize the common case when one feature show/color changed.
batchTexture._batchTexture.copyFrom({
source: {
width: dimensions.x,
height: dimensions.y,
arrayBufferView: batchTexture._batchValues,
},
});
}
BatchTexture.prototype.update = function (tileset, frameState) {
const context = frameState.context;
this._defaultTexture = context.defaultTexture;
const passes = frameState.passes;
if (passes.pick || passes.postProcess) {
createPickTexture(this, context);
}
if (this._batchValuesDirty) {
this._batchValuesDirty = false;
// Create batch texture on-demand
if (!defined(this._batchTexture)) {
this._batchTexture = createTexture(this, context, this._batchValues);
// Make sure the tileset statistics are updated the frame when the
// batch texture is created.
if (defined(this._statistics)) {
this._statistics.batchTableByteLength += this._batchTexture.sizeInBytes;
}
}
updateBatchTexture(this); // Apply per-feature show/color updates
}
};
/**
* Returns true if this object was destroyed; otherwise, false.
* <p>
* If this object was destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
* </p>
*
* @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
*
* @see BatchTexture#destroy
* @private
*/
BatchTexture.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.
* <p>
* Once an object is destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
* assign the return value (<code>undefined</code>) to the object as done in the example.
* </p>
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
* @example
* e = e && e.destroy();
*
* @see BatchTexture#isDestroyed
* @private
*/
BatchTexture.prototype.destroy = function () {
this._batchTexture = this._batchTexture && this._batchTexture.destroy();
this._pickTexture = this._pickTexture && this._pickTexture.destroy();
const pickIds = this._pickIds;
const length = pickIds.length;
for (let i = 0; i < length; ++i) {
pickIds[i].destroy();
}
return destroyObject(this);
};
export default BatchTexture;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
/**
* Defines loading state of a image as any request is resolved and the WebGL resources are updated.
* @private
* @enum {number}
*/
const BillboardLoadState = Object.freeze({
/**
* There is no image data to load.
* @private
* @type {number}
* @constant
*/
NONE: 0,
/**
* Image data is in the process of downloading, or WebGL resources are being updated.
* @private
* @type {number}
* @constant
*/
LOADING: 2,
/**
* The image data has been downloaded and the WebGL resources have been created. It is ready for rendering.
* @private
* @type {number}
* @constant
*/
LOADED: 3,
/**
* There was an error while downloading an image or updating the WebGL resources.
* @private
* @type {number}
* @constant
*/
ERROR: 4,
/**
* Updating the WebGL resources failed, due to the resource being destroyed or another error.
* @private
* @type {number}
* @constant
*/
FAILED: 5,
});
export default BillboardLoadState;
+402
View File
@@ -0,0 +1,402 @@
import Check from "../Core/Check.js";
import defined from "../Core/defined.js";
import BillboardLoadState from "./BillboardLoadState.js";
/**
* Tracks a reference to an image and it's loading state, as used in a BillboardCollection and stored in a texture atlas.
* @constructor
* @private
* @see BillboardCollection
* @see Billboard#image
* @alias BillboardTexture
* @param {BillboardCollection} billboardCollection The associated billboard collecion.
*/
function BillboardTexture(billboardCollection) {
//>>includeStart('debug', pragmas.debug);
Check.defined("billboardCollection", billboardCollection);
//>>includeEnd('debug');
this._billboardCollection = billboardCollection;
this._id = undefined;
this._loadState = BillboardLoadState.NONE;
this._loadError = undefined;
this._index = -1;
this._width = undefined;
this._height = undefined;
this._hasSubregion = false;
/**
* Used by billboardCollection to track whcih billboards to update.
* @type {boolean}
* @private
*/
this.dirty = false;
}
Object.defineProperties(BillboardTexture.prototype, {
/**
* If defined, this error was encountered during the loading process.
* @memberof BillboardTexture.prototype
* @type {Error|undefined}
* @readonly
* @private
*/
loadError: {
get: function () {
return this._loadError;
},
},
/**
* The current status of the image load. When <code>BillboardLoadState.LOADED</code>, this billboard is ready to render, i.e., the image
* has been downloaded and the WebGL resources are created.
* @memberof BillboardTexture.prototype
* @type {BillboardLoadState}
* @readonly
* @default BillboardLoadState.NONE
* @private
*/
loadState: {
get: function () {
return this._loadState;
},
},
/**
* When <code>true</code>, this texture is ready to render, i.e., the image
* has been downloaded and the WebGL resources are created.
* @memberof BillboardTexture.prototype
* @type {boolean}
* @readonly
* @default false
* @private
*/
ready: {
get: function () {
return this._loadState === BillboardLoadState.LOADED;
},
},
/**
* Returns <code>true</code> if there is image data associated with this instance.
* @memberof BillboardTexture.prototype
* @type {boolean}
* @readonly
* @private
*/
hasImage: {
get: function () {
return this._loadState !== BillboardLoadState.NONE;
},
},
/**
* A unique identifier for the image, or undefined if no image data has been associated with this instance.
* @memberof BillboardTexture.prototype
* @type {string|undefined}
* @readonly
* @private
*/
id: {
get: function () {
return this._id;
},
},
/**
* The width of the associated image. Before the instance is <code>ready</code>, this will be <code>undefined</code>.
* @memberof BillboardTexture.prototype
* @type {number|undefined}
* @readonly
* @private
*/
width: {
get: function () {
return this._width;
},
},
/**
* The height of the associated image. Before the instance is <code>ready</code>, this will be <code>undefined</code>.
* @memberof BillboardTexture.prototype
* @type {number|undefined}
* @readonly
* @private
*/
height: {
get: function () {
return this._height;
},
},
});
/**
* Releases reference to any associated image data.
* @private
*/
BillboardTexture.prototype.unload = async function () {
if (this._loadState === BillboardLoadState.NONE) {
return;
}
this._id = undefined;
this._loadError = undefined;
this._loadState = BillboardLoadState.NONE;
this._index = -1;
this._width = undefined;
this._height = undefined;
this.dirty = true;
};
/**
* Starts loading an image into the texture atlas.
* @see {TextureAtlas#addImage}
* @private
* @param {string} id An identifier to detect whether the image already exists in the atlas.
* @param {HTMLImageElement|HTMLCanvasElement|string|Resource|Promise|TextureAtlas.CreateImageCallback} image An image or canvas to add to the texture atlas,
* or a URL to an Image, or a Promise for an image, or a function that creates an image.
* @param {number} width A number specifying the width of the texture. If undefined, the image width will be used.
* @param {number} height A number specifying the height of the texture. If undefined, the image height will be used.
*/
BillboardTexture.prototype.loadImage = async function (
id,
image,
width,
height,
) {
if (this._id === id) {
// This image has already been loaded
return;
}
const collection = this._billboardCollection;
const cache = collection.billboardTextureCache;
let billboardTexture = cache.get(id);
if (
(defined(billboardTexture) &&
image.loadState === BillboardLoadState.LOADING) ||
image.loadState === BillboardLoadState.LOADED
) {
// Use the cached texture if it is in progress or successful.
BillboardTexture.clone(billboardTexture, this);
return;
}
// Otherwise, load if not yet assigned an image, and try the load again if anything failed during the last billboard creation
if (!defined(billboardTexture)) {
billboardTexture = new BillboardTexture(collection);
cache.set(id, billboardTexture);
}
billboardTexture._id = this._id = id;
billboardTexture._loadState = this._loadState = BillboardLoadState.LOADING;
billboardTexture._loadError = this._loadError = undefined;
let index;
const atlas = this._billboardCollection.textureAtlas;
try {
const indexOrPromise = atlas.addImage(id, image, width, height);
if (typeof indexOrPromise === "number") {
index = indexOrPromise;
} else {
index = await indexOrPromise;
}
} catch (error) {
// There was an error loading the image
billboardTexture._loadState = BillboardLoadState.ERROR;
billboardTexture._loadError = error;
if (this._id !== id) {
// Another load was initiated and resolved resolved before this one. This operation is cancelled.
return;
}
this._loadState = BillboardLoadState.ERROR;
this._loadError = error;
return;
}
if (!defined(index) || index === -1) {
// Resources destroyed or otherwise
billboardTexture._loadState = BillboardLoadState.FAILED;
billboardTexture._index = -1;
if (this._id !== id) {
// Another load was initiated and resolved resolved before this one. This operation is cancelled.
return;
}
this._loadState = BillboardLoadState.FAILED;
this._index = -1;
return;
}
billboardTexture._index = index;
billboardTexture._loadState = BillboardLoadState.LOADED;
const rectangle = atlas.rectangles[index];
billboardTexture._width = rectangle.width;
billboardTexture._height = rectangle.height;
if (this._id !== id) {
// Another load was initiated and resolved resolved before this one. This operation is cancelled.
return;
}
this._index = index;
this._loadState = BillboardLoadState.LOADED;
this._width = rectangle.width;
this._height = rectangle.height;
this.dirty = true;
};
/**
* Track a reference to a sub-region of an existing image.
* @see {TextureAtlas#addImageSubRegion}
* @private
* @param {string} id An identifier to detect whether the image already exists in the atlas.
* @param {BoundingRectangle} subRegion An {@link BoundingRectangle} defining a region of an existing image, measured in pixels from the bottom-left of the image.
*/
BillboardTexture.prototype.addImageSubRegion = function (id, subRegion) {
this._id = id;
this._loadError = undefined;
this._hasSubregion = true;
const atlas = this._billboardCollection.textureAtlas;
const indexOrPromise = atlas.addImageSubRegion(id, subRegion);
if (typeof indexOrPromise === "number") {
this.setImageSubRegion(indexOrPromise, subRegion);
return;
}
this.loadImageSubRegion(id, subRegion, indexOrPromise);
};
/**
* @see {TextureAtlas#addImageSubRegion}
* @private
* @param {string} id An identifier to detect whether the image already exists in the atlas.
* @param {BoundingRectangle} subRegion An {@link BoundingRectangle} defining a region of an existing image, measured in pixels from the bottom-left of the image.
* @param {Promise<number>} indexPromise A promise that resolves to the image region index.
*/
BillboardTexture.prototype.loadImageSubRegion = async function (
id,
subRegion,
indexPromise,
) {
let index;
try {
this._loadState = BillboardLoadState.LOADING;
index = await indexPromise;
} catch (error) {
// There was an error loading the referenced image
this._loadState = BillboardLoadState.ERROR;
this._loadError = error;
return;
}
if (this._id !== id) {
// Another load was initiated and resolved resolved before this one. This operation is cancelled.
return;
}
this._loadState = BillboardLoadState.LOADED;
this.setImageSubRegion(index, subRegion);
};
/**
* @see {TextureAtlas#addImageSubRegion}
* @private
* @param {number} index The resolved index in the {@link TextureAtlas}
* @param {BoundingRectangle} subRegion An {@link BoundingRectangle} defining a region of an existing image, measured in pixels from the bottom-left of the image.
*/
BillboardTexture.prototype.setImageSubRegion = function (index, subRegion) {
if (this._index === index) {
return;
}
if (!defined(index) || index === -1) {
this._loadState = BillboardLoadState.FAILED;
this._index = -1;
this._width = undefined;
this._height = undefined;
return;
}
this._width = subRegion.width;
this._height = subRegion.height;
this._index = index;
this.dirty = true;
};
/**
* Get the texture coordinates for reading the loaded texture in shaders.
* @private
* @param {BoundingRectangle} [result] The modified result parameter or a new BoundingRectangle instance if one was not provided.
* @return {BoundingRectangle} The modified result parameter or a new BoundingRectangle instance if one was not provided.
*/
BillboardTexture.prototype.computeTextureCoordinates = function (result) {
const atlas = this._billboardCollection.textureAtlas;
return atlas.computeTextureCoordinates(this._index, result);
};
/**
* Clones an existing billboard texture, inlcuding any in-flight tracking, into the target billboard texture.
* @param {BillboardTexture} billboardTexture
* @param {BillboardTexture} target
* @returns {BillboardTexture} target
*/
BillboardTexture.clone = function (billboardTexture, target) {
target._id = billboardTexture._id;
target._loadState = billboardTexture._loadState;
target._loadError = undefined;
target._index = billboardTexture._index;
target._width = billboardTexture._width;
target._height = billboardTexture._height;
target._hasSubregion = billboardTexture._hasSubregion;
if (billboardTexture.ready) {
target.dirty = true;
return;
}
const completeLoad = async () => {
const id = billboardTexture._id;
const atlas = billboardTexture._billboardCollection.textureAtlas;
await atlas._indexPromiseById.get(id);
// Any errors should have already been handled
if (target._id !== id) {
// Another load was initiated and resolved resolved before this one. This operation is cancelled.
return;
}
if (billboardTexture._hasSubregion) {
// Subregions must wait an additional frame to be ready
await Promise.resolve();
}
target._id = id;
target._loadState = billboardTexture._loadState;
target._loadError = billboardTexture._loadError;
target._index = billboardTexture._index;
target._width = billboardTexture._width;
target._height = billboardTexture._height;
target.dirty = true;
};
completeLoad();
return target;
};
export default BillboardTexture;
+769
View File
@@ -0,0 +1,769 @@
import buildModuleUrl from "../Core/buildModuleUrl.js";
import Check from "../Core/Check.js";
import Credit from "../Core/Credit.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import Event from "../Core/Event.js";
import CesiumMath from "../Core/Math.js";
import Rectangle from "../Core/Rectangle.js";
import Resource from "../Core/Resource.js";
import RuntimeError from "../Core/RuntimeError.js";
import TileProviderError from "../Core/TileProviderError.js";
import WebMercatorTilingScheme from "../Core/WebMercatorTilingScheme.js";
import BingMapsStyle from "./BingMapsStyle.js";
import DiscardEmptyTilePolicy from "./DiscardEmptyTileImagePolicy.js";
import ImageryProvider from "./ImageryProvider.js";
/**
* @typedef {object} BingMapsImageryProvider.ConstructorOptions
*
* Initialization options for the BingMapsImageryProvider constructor
*
* @property {string} [key] The Bing Maps key for your application, which can be
* created at {@link https://www.bingmapsportal.com/}.
* @property {string} [tileProtocol] The protocol to use when loading tiles, e.g. 'http' or 'https'.
* By default, tiles are loaded using the same protocol as the page.
* @property {BingMapsStyle} [mapStyle=BingMapsStyle.AERIAL] The type of Bing Maps imagery to load.
* @property {string} [mapLayer] Additional display layer options as defined on {@link https://learn.microsoft.com/en-us/bingmaps/rest-services/imagery/get-imagery-metadata#template-parameters}
* @property {string} [culture=''] The culture to use when requesting Bing Maps imagery. Not
* all cultures are supported. See {@link http://msdn.microsoft.com/en-us/library/hh441729.aspx}
* for information on the supported cultures.
* @property {Ellipsoid} [ellipsoid=Ellipsoid.default] The ellipsoid. If not specified, the default ellipsoid is used.
* @property {TileDiscardPolicy} [tileDiscardPolicy] The policy that determines if a tile
* is invalid and should be discarded. By default, a {@link DiscardEmptyTileImagePolicy}
* will be used, with the expectation that the Bing Maps server will send a zero-length response for missing tiles.
* To ensure that no tiles are discarded, construct and pass a {@link NeverTileDiscardPolicy} for this parameter.
*/
/**
* Used to track creation details while fetching initial metadata
*
* @constructor
* @private
*
* @param {BingMapsImageryProvider.ConstructorOptions} options An object describing initialization options
*/
function ImageryProviderBuilder(options) {
this.tileWidth = undefined;
this.tileHeight = undefined;
this.maximumLevel = undefined;
this.imageUrlSubdomains = undefined;
this.imageUrlTemplate = undefined;
this.attributionList = undefined;
}
/**
* Complete BingMapsImageryProvider creation based on builder values.
*
* @private
*
* @param {BingMapsImageryProvider} provider
*/
ImageryProviderBuilder.prototype.build = function (provider) {
provider._tileWidth = this.tileWidth;
provider._tileHeight = this.tileHeight;
provider._maximumLevel = this.maximumLevel;
provider._imageUrlSubdomains = this.imageUrlSubdomains;
provider._imageUrlTemplate = this.imageUrlTemplate;
let attributionList = (provider._attributionList = this.attributionList);
if (!attributionList) {
attributionList = [];
}
provider._attributionList = attributionList;
for (
let attributionIndex = 0, attributionLength = attributionList.length;
attributionIndex < attributionLength;
++attributionIndex
) {
const attribution = attributionList[attributionIndex];
if (attribution.credit instanceof Credit) {
// If attribution.credit has already been created
// then we are using a cached value, which means
// none of the remaining processing needs to be done.
break;
}
attribution.credit = new Credit(attribution.attribution);
const coverageAreas = attribution.coverageAreas;
for (
let areaIndex = 0, areaLength = attribution.coverageAreas.length;
areaIndex < areaLength;
++areaIndex
) {
const area = coverageAreas[areaIndex];
const bbox = area.bbox;
area.bbox = new Rectangle(
CesiumMath.toRadians(bbox[1]),
CesiumMath.toRadians(bbox[0]),
CesiumMath.toRadians(bbox[3]),
CesiumMath.toRadians(bbox[2]),
);
}
}
};
function metadataSuccess(data, imageryProviderBuilder) {
if (data.resourceSets.length !== 1) {
throw new RuntimeError(
"metadata does not specify one resource in resourceSets",
);
}
const resource = data.resourceSets[0].resources[0];
imageryProviderBuilder.tileWidth = resource.imageWidth;
imageryProviderBuilder.tileHeight = resource.imageHeight;
imageryProviderBuilder.maximumLevel = resource.zoomMax - 1;
imageryProviderBuilder.imageUrlSubdomains = resource.imageUrlSubdomains;
imageryProviderBuilder.imageUrlTemplate = resource.imageUrl;
let validProviders = resource.imageryProviders;
if (defined(resource.imageryProviders)) {
// prevent issues with the imagery API from crashing the viewer when the expected properties are not there
// See https://github.com/CesiumGS/cesium/issues/12088
validProviders = resource.imageryProviders.filter((provider) =>
provider.coverageAreas?.some((area) => defined(area.bbox)),
);
}
imageryProviderBuilder.attributionList = validProviders;
}
function metadataFailure(metadataResource, error, provider) {
let message = `An error occurred while accessing ${metadataResource.url}`;
if (defined(error) && defined(error.message)) {
message += `: ${error.message}`;
}
TileProviderError.reportError(
undefined,
provider,
defined(provider) ? provider._errorEvent : undefined,
message,
undefined,
undefined,
undefined,
error,
);
throw new RuntimeError(message);
}
async function requestMetadata(
metadataResource,
imageryProviderBuilder,
provider,
) {
const cacheKey = metadataResource.url;
let promise = BingMapsImageryProvider._metadataCache[cacheKey];
if (!defined(promise)) {
promise = metadataResource.fetchJson();
BingMapsImageryProvider._metadataCache[cacheKey] = promise;
}
try {
const data = await promise;
return metadataSuccess(data, imageryProviderBuilder);
} catch (e) {
metadataFailure(metadataResource, e, provider);
}
}
/**
* <div class="notice">
* To construct a BingMapsImageryProvider, call {@link BingMapsImageryProvider.fromUrl}. Do not call the constructor directly.
* </div>
*
* Provides tiled imagery using the Bing Maps Imagery REST API.
*
* @alias BingMapsImageryProvider
* @constructor
*
* @param {BingMapsImageryProvider.ConstructorOptions} options Object describing initialization options
*
* @see BingMapsImageryProvider.fromUrl
* @see ArcGisMapServerImageryProvider
* @see GoogleEarthEnterpriseMapsProvider
* @see OpenStreetMapImageryProvider
* @see SingleTileImageryProvider
* @see TileMapServiceImageryProvider
* @see WebMapServiceImageryProvider
* @see WebMapTileServiceImageryProvider
* @see UrlTemplateImageryProvider
*
* @example
* const bing = await Cesium.BingMapsImageryProvider.fromUrl(
* "https://dev.virtualearth.net", {
* key: "get-yours-at-https://www.bingmapsportal.com/",
* mapStyle: Cesium.BingMapsStyle.AERIAL
* });
*
* @see {@link http://msdn.microsoft.com/en-us/library/ff701713.aspx|Bing Maps REST Services}
* @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing}
*/
function BingMapsImageryProvider(options) {
options = options ?? Frozen.EMPTY_OBJECT;
this._defaultAlpha = undefined;
this._defaultNightAlpha = undefined;
this._defaultDayAlpha = undefined;
this._defaultBrightness = undefined;
this._defaultContrast = undefined;
this._defaultHue = undefined;
this._defaultSaturation = undefined;
this._defaultGamma = 1.0;
this._defaultMinificationFilter = undefined;
this._defaultMagnificationFilter = undefined;
this._mapStyle = options.mapStyle ?? BingMapsStyle.AERIAL;
this._mapLayer = options.mapLayer;
this._culture = options.culture ?? "";
this._key = options.key;
this._tileDiscardPolicy = options.tileDiscardPolicy;
if (!defined(this._tileDiscardPolicy)) {
this._tileDiscardPolicy = new DiscardEmptyTilePolicy();
}
this._proxy = options.proxy;
this._credit = new Credit(
`<a href="https://www.microsoft.com/en-us/maps/bing-maps/product"><img src="${BingMapsImageryProvider.logoUrl}" title="Bing Imagery"/></a>`,
);
this._tilingScheme = new WebMercatorTilingScheme({
numberOfLevelZeroTilesX: 2,
numberOfLevelZeroTilesY: 2,
ellipsoid: options.ellipsoid,
});
this._tileWidth = undefined;
this._tileHeight = undefined;
this._maximumLevel = undefined;
this._imageUrlTemplate = undefined;
this._imageUrlSubdomains = undefined;
this._attributionList = undefined;
this._errorEvent = new Event();
}
Object.defineProperties(BingMapsImageryProvider.prototype, {
/**
* Gets the name of the BingMaps server url hosting the imagery.
* @memberof BingMapsImageryProvider.prototype
* @type {string}
* @readonly
*/
url: {
get: function () {
return this._resource.url;
},
},
/**
* Gets the proxy used by this provider.
* @memberof BingMapsImageryProvider.prototype
* @type {Proxy}
* @readonly
*/
proxy: {
get: function () {
return this._resource.proxy;
},
},
/**
* Gets the Bing Maps key.
* @memberof BingMapsImageryProvider.prototype
* @type {string}
* @readonly
*/
key: {
get: function () {
return this._key;
},
},
/**
* Gets the type of Bing Maps imagery to load.
* @memberof BingMapsImageryProvider.prototype
* @type {BingMapsStyle}
* @readonly
*/
mapStyle: {
get: function () {
return this._mapStyle;
},
},
/**
* Gets the additional map layer options as defined in {@link https://learn.microsoft.com/en-us/bingmaps/rest-services/imagery/get-imagery-metadata#template-parameters}/
* @memberof BingMapsImageryProvider.prototype
* @type {string}
* @readonly
*/
mapLayer: {
get: function () {
return this._mapLayer;
},
},
/**
* The culture to use when requesting Bing Maps imagery. Not
* all cultures are supported. See {@link http://msdn.microsoft.com/en-us/library/hh441729.aspx}
* for information on the supported cultures.
* @memberof BingMapsImageryProvider.prototype
* @type {string}
* @readonly
*/
culture: {
get: function () {
return this._culture;
},
},
/**
* Gets the width of each tile, in pixels.
* @memberof BingMapsImageryProvider.prototype
* @type {number}
* @readonly
*/
tileWidth: {
get: function () {
return this._tileWidth;
},
},
/**
* Gets the height of each tile, in pixels.
* @memberof BingMapsImageryProvider.prototype
* @type {number}
* @readonly
*/
tileHeight: {
get: function () {
return this._tileHeight;
},
},
/**
* Gets the maximum level-of-detail that can be requested.
* @memberof BingMapsImageryProvider.prototype
* @type {number|undefined}
* @readonly
*/
maximumLevel: {
get: function () {
return this._maximumLevel;
},
},
/**
* Gets the minimum level-of-detail that can be requested.
* @memberof BingMapsImageryProvider.prototype
* @type {number}
* @readonly
*/
minimumLevel: {
get: function () {
return 0;
},
},
/**
* Gets the tiling scheme used by this provider.
* @memberof BingMapsImageryProvider.prototype
* @type {TilingScheme}
* @readonly
*/
tilingScheme: {
get: function () {
return this._tilingScheme;
},
},
/**
* Gets the rectangle, in radians, of the imagery provided by this instance.
* @memberof BingMapsImageryProvider.prototype
* @type {Rectangle}
* @readonly
*/
rectangle: {
get: function () {
return this._tilingScheme.rectangle;
},
},
/**
* Gets the tile discard policy. If not undefined, the discard policy is responsible
* for filtering out "missing" tiles via its shouldDiscardImage function. If this function
* returns undefined, no tiles are filtered.
* @memberof BingMapsImageryProvider.prototype
* @type {TileDiscardPolicy}
* @readonly
*/
tileDiscardPolicy: {
get: function () {
return this._tileDiscardPolicy;
},
},
/**
* Gets an event that is raised when the imagery provider encounters an asynchronous error. By subscribing
* to the event, you will be notified of the error and can potentially recover from it. Event listeners
* are passed an instance of {@link TileProviderError}.
* @memberof BingMapsImageryProvider.prototype
* @type {Event}
* @readonly
*/
errorEvent: {
get: function () {
return this._errorEvent;
},
},
/**
* Gets the credit to display when this imagery provider is active. Typically this is used to credit
* the source of the imagery.
* @memberof BingMapsImageryProvider.prototype
* @type {Credit}
* @readonly
*/
credit: {
get: function () {
return this._credit;
},
},
/**
* Gets a value indicating whether or not the images provided by this imagery provider
* include an alpha channel. If this property is false, an alpha channel, if present, will
* be ignored. If this property is true, any images without an alpha channel will be treated
* as if their alpha is 1.0 everywhere. Setting this property to false reduces memory usage
* and texture upload time.
* @memberof BingMapsImageryProvider.prototype
* @type {boolean}
* @readonly
*/
hasAlphaChannel: {
get: function () {
return defined(this.mapLayer);
},
},
});
/**
* Creates an {@link ImageryProvider} which provides tiled imagery using the Bing Maps Imagery REST API.
*
* @param {Resource|string} url The url of the Bing Maps server hosting the imagery.
* @param {BingMapsImageryProvider.ConstructorOptions} options Object describing initialization options
* @returns {Promise<BingMapsImageryProvider>} A promise that resolves to the created BingMapsImageryProvider
*
* @example
* const bing = await Cesium.BingMapsImageryProvider.fromUrl(
* "https://dev.virtualearth.net", {
* key: "get-yours-at-https://www.bingmapsportal.com/",
* mapStyle: Cesium.BingMapsStyle.AERIAL
* });
*
* @exception {RuntimeError} metadata does not specify one resource in resourceSets
*/
BingMapsImageryProvider.fromUrl = async function (url, options) {
options = options ?? Frozen.EMPTY_OBJECT;
//>>includeStart('debug', pragmas.debug);
Check.defined("url", url);
Check.defined("options.key", options.key);
//>>includeEnd('debug');
let tileProtocol = options.tileProtocol;
// For backward compatibility reasons, the tileProtocol may end with
// a `:`. Remove it.
if (defined(tileProtocol)) {
if (
tileProtocol.length > 0 &&
tileProtocol[tileProtocol.length - 1] === ":"
) {
tileProtocol = tileProtocol.substr(0, tileProtocol.length - 1);
}
} else {
// use http if the document's protocol is http, otherwise use https
const documentProtocol = document.location.protocol;
tileProtocol = documentProtocol === "http:" ? "http" : "https";
}
const mapStyle = options.mapStyle ?? BingMapsStyle.AERIAL;
const resource = Resource.createIfNeeded(url);
resource.appendForwardSlash();
const queryParameters = {
incl: "ImageryProviders",
key: options.key,
uriScheme: tileProtocol,
};
if (defined(options.mapLayer)) {
queryParameters.mapLayer = options.mapLayer;
}
if (defined(options.culture)) {
queryParameters.culture = options.culture;
}
const metadataResource = resource.getDerivedResource({
url: `REST/v1/Imagery/Metadata/${mapStyle}`,
queryParameters: queryParameters,
});
const provider = new BingMapsImageryProvider(options);
provider._resource = resource;
const imageryProviderBuilder = new ImageryProviderBuilder(options);
await requestMetadata(metadataResource, imageryProviderBuilder);
imageryProviderBuilder.build(provider);
return provider;
};
const rectangleScratch = new Rectangle();
/**
* Gets the credits to be displayed when a given tile is displayed.
*
* @param {number} x The tile X coordinate.
* @param {number} y The tile Y coordinate.
* @param {number} level The tile level;
* @returns {Credit[]} The credits to be displayed when the tile is displayed.
*/
BingMapsImageryProvider.prototype.getTileCredits = function (x, y, level) {
const rectangle = this._tilingScheme.tileXYToRectangle(
x,
y,
level,
rectangleScratch,
);
const result = getRectangleAttribution(
this._attributionList,
level,
rectangle,
);
return result;
};
/**
* Requests the image for a given tile.
*
* @param {number} x The tile X coordinate.
* @param {number} y The tile Y coordinate.
* @param {number} level The tile level.
* @param {Request} [request] The request object. Intended for internal use only.
* @returns {Promise<ImageryTypes>|undefined} A promise for the image that will resolve when the image is available, or
* undefined if there are too many active requests to the server, and the request should be retried later.
*/
BingMapsImageryProvider.prototype.requestImage = function (
x,
y,
level,
request,
) {
const promise = ImageryProvider.loadImage(
this,
buildImageResource(this, x, y, level, request),
);
if (defined(promise)) {
return promise.catch(function (error) {
// One cause of an error here is that the image we tried to load was zero-length.
// This isn't actually a problem, since it indicates that there is no tile.
// So, in that case we return the EMPTY_IMAGE sentinel value for later discarding.
if (defined(error.blob) && error.blob.size === 0) {
return DiscardEmptyTilePolicy.EMPTY_IMAGE;
}
return Promise.reject(error);
});
}
return undefined;
};
/**
* Picking features is not currently supported by this imagery provider, so this function simply returns
* undefined.
*
* @param {number} x The tile X coordinate.
* @param {number} y The tile Y coordinate.
* @param {number} level The tile level.
* @param {number} longitude The longitude at which to pick features.
* @param {number} latitude The latitude at which to pick features.
* @return {undefined} Undefined since picking is not supported.
*/
BingMapsImageryProvider.prototype.pickFeatures = function (
x,
y,
level,
longitude,
latitude,
) {
return undefined;
};
/**
* Converts a tiles (x, y, level) position into a quadkey used to request an image
* from a Bing Maps server.
*
* @param {number} x The tile's x coordinate.
* @param {number} y The tile's y coordinate.
* @param {number} level The tile's zoom level.
*
* @see {@link http://msdn.microsoft.com/en-us/library/bb259689.aspx|Bing Maps Tile System}
* @see BingMapsImageryProvider#quadKeyToTileXY
*/
BingMapsImageryProvider.tileXYToQuadKey = function (x, y, level) {
let quadkey = "";
for (let i = level; i >= 0; --i) {
const bitmask = 1 << i;
let digit = 0;
if ((x & bitmask) !== 0) {
digit |= 1;
}
if ((y & bitmask) !== 0) {
digit |= 2;
}
quadkey += digit;
}
return quadkey;
};
/**
* Converts a tile's quadkey used to request an image from a Bing Maps server into the
* (x, y, level) position.
*
* @param {string} quadkey The tile's quad key
*
* @see {@link http://msdn.microsoft.com/en-us/library/bb259689.aspx|Bing Maps Tile System}
* @see BingMapsImageryProvider#tileXYToQuadKey
*/
BingMapsImageryProvider.quadKeyToTileXY = function (quadkey) {
let x = 0;
let y = 0;
const level = quadkey.length - 1;
for (let i = level; i >= 0; --i) {
const bitmask = 1 << i;
const digit = +quadkey[level - i];
if ((digit & 1) !== 0) {
x |= bitmask;
}
if ((digit & 2) !== 0) {
y |= bitmask;
}
}
return {
x: x,
y: y,
level: level,
};
};
BingMapsImageryProvider._logoUrl = undefined;
Object.defineProperties(BingMapsImageryProvider, {
/**
* Gets or sets the URL to the Bing logo for display in the credit.
* @memberof BingMapsImageryProvider
* @type {string}
*/
logoUrl: {
get: function () {
if (!defined(BingMapsImageryProvider._logoUrl)) {
BingMapsImageryProvider._logoUrl = buildModuleUrl(
"Assets/Images/bing_maps_credit.png",
);
}
return BingMapsImageryProvider._logoUrl;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.defined("value", value);
//>>includeEnd('debug');
BingMapsImageryProvider._logoUrl = value;
},
},
});
function buildImageResource(imageryProvider, x, y, level, request) {
const imageUrl = imageryProvider._imageUrlTemplate;
const subdomains = imageryProvider._imageUrlSubdomains;
const subdomainIndex = (x + y + level) % subdomains.length;
return imageryProvider._resource.getDerivedResource({
url: imageUrl,
request: request,
templateValues: {
quadkey: BingMapsImageryProvider.tileXYToQuadKey(x, y, level),
subdomain: subdomains[subdomainIndex],
culture: imageryProvider._culture,
},
queryParameters: {
// this parameter tells the Bing servers to send a zero-length response
// instead of a placeholder image for missing tiles.
n: "z",
},
});
}
const intersectionScratch = new Rectangle();
function getRectangleAttribution(attributionList, level, rectangle) {
// Bing levels start at 1, while ours start at 0.
++level;
const result = [];
for (
let attributionIndex = 0, attributionLength = attributionList.length;
attributionIndex < attributionLength;
++attributionIndex
) {
const attribution = attributionList[attributionIndex];
const coverageAreas = attribution.coverageAreas;
let included = false;
for (
let areaIndex = 0, areaLength = attribution.coverageAreas.length;
!included && areaIndex < areaLength;
++areaIndex
) {
const area = coverageAreas[areaIndex];
if (level >= area.zoomMin && level <= area.zoomMax) {
const intersection = Rectangle.intersection(
rectangle,
area.bbox,
intersectionScratch,
);
if (defined(intersection)) {
included = true;
}
}
}
if (included) {
result.push(attribution.credit);
}
}
return result;
}
// Exposed for testing
BingMapsImageryProvider._metadataCache = {};
export default BingMapsImageryProvider;
+98
View File
@@ -0,0 +1,98 @@
// @ts-check
/**
* The types of imagery provided by Bing Maps.
*
* @enum {number}
*
* @see BingMapsImageryProvider
*/
const BingMapsStyle = {
/**
* Aerial imagery.
*
* @type {string}
* @constant
*/
AERIAL: "Aerial",
/**
* Aerial imagery with a road overlay.
*
* @type {string}
* @constant
* @deprecated See https://github.com/CesiumGS/cesium/issues/7128.
* Use `BingMapsStyle.AERIAL_WITH_LABELS_ON_DEMAND` instead
*/
AERIAL_WITH_LABELS: "AerialWithLabels",
/**
* Aerial imagery with a road overlay.
*
* @type {string}
* @constant
*/
AERIAL_WITH_LABELS_ON_DEMAND: "AerialWithLabelsOnDemand",
/**
* Roads without additional imagery.
*
* @type {string}
* @constant
* @deprecated See https://github.com/CesiumGS/cesium/issues/7128.
* Use `BingMapsStyle.ROAD_ON_DEMAND` instead
*/
ROAD: "Road",
/**
* Roads without additional imagery.
*
* @type {string}
* @constant
*/
ROAD_ON_DEMAND: "RoadOnDemand",
/**
* A dark version of the road maps.
*
* @type {string}
* @constant
*/
CANVAS_DARK: "CanvasDark",
/**
* A lighter version of the road maps.
*
* @type {string}
* @constant
*/
CANVAS_LIGHT: "CanvasLight",
/**
* A grayscale version of the road maps.
*
* @type {string}
* @constant
*/
CANVAS_GRAY: "CanvasGray",
/**
* Ordnance Survey imagery. This imagery is visible only for the London, UK area.
*
* @type {string}
* @constant
*/
ORDNANCE_SURVEY: "OrdnanceSurvey",
/**
* Collins Bart imagery.
*
* @type {string}
* @constant
*/
COLLINS_BART: "CollinsBart",
};
Object.freeze(BingMapsStyle);
export default BingMapsStyle;
+58
View File
@@ -0,0 +1,58 @@
// @ts-check
import WebGLConstants from "../Core/WebGLConstants.js";
/**
* Determines how two pixels' values are combined.
*
* @enum {number}
*/
const BlendEquation = {
/**
* Pixel values are added componentwise. This is used in additive blending for translucency.
*
* @type {number}
* @constant
*/
ADD: WebGLConstants.FUNC_ADD,
/**
* Pixel values are subtracted componentwise (source - destination). This is used in alpha blending for translucency.
*
* @type {number}
* @constant
*/
SUBTRACT: WebGLConstants.FUNC_SUBTRACT,
/**
* Pixel values are subtracted componentwise (destination - source).
*
* @type {number}
* @constant
*/
REVERSE_SUBTRACT: WebGLConstants.FUNC_REVERSE_SUBTRACT,
/**
* Pixel values are given to the minimum function (min(source, destination)).
*
* This equation operates on each pixel color component.
*
* @type {number}
* @constant
*/
MIN: WebGLConstants.MIN,
/**
* Pixel values are given to the maximum function (max(source, destination)).
*
* This equation operates on each pixel color component.
*
* @type {number}
* @constant
*/
MAX: WebGLConstants.MAX,
};
Object.freeze(BlendEquation);
export default BlendEquation;
+134
View File
@@ -0,0 +1,134 @@
// @ts-check
import WebGLConstants from "../Core/WebGLConstants.js";
/**
* Determines how blending factors are computed.
*
* @enum {number}
*/
const BlendFunction = {
/**
* The blend factor is zero.
*
* @type {number}
* @constant
*/
ZERO: WebGLConstants.ZERO,
/**
* The blend factor is one.
*
* @type {number}
* @constant
*/
ONE: WebGLConstants.ONE,
/**
* The blend factor is the source color.
*
* @type {number}
* @constant
*/
SOURCE_COLOR: WebGLConstants.SRC_COLOR,
/**
* The blend factor is one minus the source color.
*
* @type {number}
* @constant
*/
ONE_MINUS_SOURCE_COLOR: WebGLConstants.ONE_MINUS_SRC_COLOR,
/**
* The blend factor is the destination color.
*
* @type {number}
* @constant
*/
DESTINATION_COLOR: WebGLConstants.DST_COLOR,
/**
* The blend factor is one minus the destination color.
*
* @type {number}
* @constant
*/
ONE_MINUS_DESTINATION_COLOR: WebGLConstants.ONE_MINUS_DST_COLOR,
/**
* The blend factor is the source alpha.
*
* @type {number}
* @constant
*/
SOURCE_ALPHA: WebGLConstants.SRC_ALPHA,
/**
* The blend factor is one minus the source alpha.
*
* @type {number}
* @constant
*/
ONE_MINUS_SOURCE_ALPHA: WebGLConstants.ONE_MINUS_SRC_ALPHA,
/**
* The blend factor is the destination alpha.
*
* @type {number}
* @constant
*/
DESTINATION_ALPHA: WebGLConstants.DST_ALPHA,
/**
* The blend factor is one minus the destination alpha.
*
* @type {number}
* @constant
*/
ONE_MINUS_DESTINATION_ALPHA: WebGLConstants.ONE_MINUS_DST_ALPHA,
/**
* The blend factor is the constant color.
*
* @type {number}
* @constant
*/
CONSTANT_COLOR: WebGLConstants.CONSTANT_COLOR,
/**
* The blend factor is one minus the constant color.
*
* @type {number}
* @constant
*/
ONE_MINUS_CONSTANT_COLOR: WebGLConstants.ONE_MINUS_CONSTANT_COLOR,
/**
* The blend factor is the constant alpha.
*
* @type {number}
* @constant
*/
CONSTANT_ALPHA: WebGLConstants.CONSTANT_ALPHA,
/**
* The blend factor is one minus the constant alpha.
*
* @type {number}
* @constant
*/
ONE_MINUS_CONSTANT_ALPHA: WebGLConstants.ONE_MINUS_CONSTANT_ALPHA,
/**
* The blend factor is the saturated source alpha.
*
* @type {number}
* @constant
*/
SOURCE_ALPHA_SATURATE: WebGLConstants.SRC_ALPHA_SATURATE,
};
Object.freeze(BlendFunction);
export default BlendFunction;
+33
View File
@@ -0,0 +1,33 @@
// @ts-check
/**
* Determines how opaque and translucent parts of billboards, points, and labels are blended with the scene.
*
* @enum {number}
*/
const BlendOption = {
/**
* The billboards, points, or labels in the collection are completely opaque.
* @type {number}
* @constant
*/
OPAQUE: 0,
/**
* The billboards, points, or labels in the collection are completely translucent.
* @type {number}
* @constant
*/
TRANSLUCENT: 1,
/**
* The billboards, points, or labels in the collection are both opaque and translucent.
* @type {number}
* @constant
*/
OPAQUE_AND_TRANSLUCENT: 2,
};
Object.freeze(BlendOption);
export default BlendOption;
+78
View File
@@ -0,0 +1,78 @@
// @ts-check
import BlendEquation from "./BlendEquation.js";
import BlendFunction from "./BlendFunction.js";
/**
* The blending state combines {@link BlendEquation} and {@link BlendFunction} and the
* <code>enabled</code> flag to define the full blending state for combining source and
* destination fragments when rendering.
* <p>
* This is a helper when using custom render states with {@link Appearance#renderState}.
* </p>
*
* @namespace
*/
const BlendingState = {
/**
* Blending is disabled.
*
* @type {object}
* @constant
*/
DISABLED: Object.freeze({
enabled: false,
}),
/**
* Blending is enabled using alpha blending, <code>source(source.alpha) + destination(1 - source.alpha)</code>.
*
* @type {object}
* @constant
*/
ALPHA_BLEND: Object.freeze({
enabled: true,
equationRgb: BlendEquation.ADD,
equationAlpha: BlendEquation.ADD,
functionSourceRgb: BlendFunction.SOURCE_ALPHA,
functionSourceAlpha: BlendFunction.ONE,
functionDestinationRgb: BlendFunction.ONE_MINUS_SOURCE_ALPHA,
functionDestinationAlpha: BlendFunction.ONE_MINUS_SOURCE_ALPHA,
}),
/**
* Blending is enabled using alpha blending with premultiplied alpha, <code>source + destination(1 - source.alpha)</code>.
*
* @type {object}
* @constant
*/
PRE_MULTIPLIED_ALPHA_BLEND: Object.freeze({
enabled: true,
equationRgb: BlendEquation.ADD,
equationAlpha: BlendEquation.ADD,
functionSourceRgb: BlendFunction.ONE,
functionSourceAlpha: BlendFunction.ONE,
functionDestinationRgb: BlendFunction.ONE_MINUS_SOURCE_ALPHA,
functionDestinationAlpha: BlendFunction.ONE_MINUS_SOURCE_ALPHA,
}),
/**
* Blending is enabled using additive blending, <code>source(source.alpha) + destination</code>.
*
* @type {object}
* @constant
*/
ADDITIVE_BLEND: Object.freeze({
enabled: true,
equationRgb: BlendEquation.ADD,
equationAlpha: BlendEquation.ADD,
functionSourceRgb: BlendFunction.SOURCE_ALPHA,
functionSourceAlpha: BlendFunction.ONE,
functionDestinationRgb: BlendFunction.ONE,
functionDestinationAlpha: BlendFunction.ONE,
}),
};
Object.freeze(BlendingState);
export default BlendingState;
+169
View File
@@ -0,0 +1,169 @@
import Check from "../Core/Check.js";
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
/**
* Utilities for parsing bounding volume semantics from 3D Tiles 1.1 metadata.
*
* @namespace BoundingVolumeSemantics
* @private
*/
const BoundingVolumeSemantics = {};
/**
* Parse the bounding volume-related semantics such as
* <code>TILE_BOUNDING_BOX</code> and <code>CONTENT_BOUNDING_BOX</code> from
* implicit tile or content metadata. Results are returned as a JSON object for
* use when transcoding tiles (see {@link Implicit3DTileContent}).
* <p>
* Bounding volumes are checked in the order box, region, then sphere. Only
* the first valid bounding volume is returned.
* </p>
* <p>
* This handles both tile and content bounding volumes, as the only difference
* is the prefix. e.g. <code>TILE_BOUNDING_BOX</code> and
* <code>CONTENT_BOUNDING_BOX</code> have the same memory layout.
* </p>
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata/Semantics|3D Metadata Semantic Reference} for the various bounding volumes and minimum/maximum heights.
*
* @param {string} prefix Either "TILE" or "CONTENT"
* @param {TileMetadata|ContentMetadata} metadata The metadata object for looking up values by semantic. In practice, this will typically be a {@link ImplicitMetadataView}
* @return {object} An object containing the bounding volume, and any minimum or maximum height.
*
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
BoundingVolumeSemantics.parseAllBoundingVolumeSemantics = function (
prefix,
metadata,
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("prefix", prefix);
if (prefix !== "TILE" && prefix !== "CONTENT") {
throw new DeveloperError("prefix must be either 'TILE' or 'CONTENT'");
}
Check.typeOf.object("metadata", metadata);
//>>includeEnd('debug');
return {
boundingVolume: BoundingVolumeSemantics.parseBoundingVolumeSemantic(
prefix,
metadata,
),
minimumHeight: BoundingVolumeSemantics._parseMinimumHeight(
prefix,
metadata,
),
maximumHeight: BoundingVolumeSemantics._parseMaximumHeight(
prefix,
metadata,
),
};
};
/**
* Parse the bounding volume from tile or content metadata. If the metadata
* specify multiple bounding volumes, only the first one is returned. Bounding
* volumes are checked in the order box, region, then sphere.
* <p>
* This handles both tile and content bounding volumes, as the only difference
* is the prefix. e.g. <code>TILE_BOUNDING_BOX</code> and
* <code>CONTENT_BOUNDING_BOX</code> have the same memory layout.
* </p>
*
* @param {string} prefix Either "TILE" or "CONTENT"
* @param {TileMetadata|ContentMetadata} metadata The metadata for looking up values
* @return {object} An object representing the JSON description of the tile or content bounding volume
* @private
*/
BoundingVolumeSemantics.parseBoundingVolumeSemantic = function (
prefix,
metadata,
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("prefix", prefix);
if (prefix !== "TILE" && prefix !== "CONTENT") {
throw new DeveloperError("prefix must be either 'TILE' or 'CONTENT'");
}
Check.typeOf.object("metadata", metadata);
//>>includeEnd('debug');
const boundingBoxSemantic = `${prefix}_BOUNDING_BOX`;
const boundingBox = metadata.getPropertyBySemantic(boundingBoxSemantic);
if (defined(boundingBox)) {
return {
box: boundingBox,
};
}
const boundingRegionSemantic = `${prefix}_BOUNDING_REGION`;
const boundingRegion = metadata.getPropertyBySemantic(boundingRegionSemantic);
if (defined(boundingRegion)) {
return {
region: boundingRegion,
};
}
const boundingSphereSemantic = `${prefix}_BOUNDING_SPHERE`;
const boundingSphere = metadata.getPropertyBySemantic(boundingSphereSemantic);
if (defined(boundingSphere)) {
// ARRAY with 4 elements is automatically converted to a Cartesian4
return {
sphere: boundingSphere,
};
}
return undefined;
};
/**
* Parse the minimum height from tile or content metadata. This is used for making
* tighter quadtree bounds for implicit tiling. This works for both
* <code>TILE_MINIMUM_HEIGHT</code> and <code>CONTENT_MINIMUM_HEIGHT</code>
*
* @param {string} prefix Either "TILE" or "CONTENT"
* @param {TileMetadata|ContentMetadata} metadata The metadata for looking up values
* @return {number} The minimum height
* @private
*/
BoundingVolumeSemantics._parseMinimumHeight = function (prefix, metadata) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("prefix", prefix);
if (prefix !== "TILE" && prefix !== "CONTENT") {
throw new DeveloperError("prefix must be either 'TILE' or 'CONTENT'");
}
Check.typeOf.object("metadata", metadata);
//>>includeEnd('debug');
const minimumHeightSemantic = `${prefix}_MINIMUM_HEIGHT`;
return metadata.getPropertyBySemantic(minimumHeightSemantic);
};
/**
* Parse the maximum height from tile or content metadata. This is used for
* making tighter quadtree bounds for implicit tiling. This works for both
* <code>TILE_MAXIMUM_HEIGHT</code> and <code>CONTENT_MAXIMUM_HEIGHT</code>
*
* @param {string} prefix Either "TILE" or "CONTENT"
* @param {TileMetadata|ContentMetadata} metadata The metadata for looking up values
* @return {number} The maximum height
* @private
*/
BoundingVolumeSemantics._parseMaximumHeight = function (prefix, metadata) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("prefix", prefix);
if (prefix !== "TILE" && prefix !== "CONTENT") {
throw new DeveloperError("prefix must be either 'TILE' or 'CONTENT'");
}
Check.typeOf.object("metadata", metadata);
//>>includeEnd('debug');
const maximumHeightSemantic = `${prefix}_MAXIMUM_HEIGHT`;
return metadata.getPropertyBySemantic(maximumHeightSemantic);
};
export default BoundingVolumeSemantics;
+74
View File
@@ -0,0 +1,74 @@
import Cartesian3 from "../Core/Cartesian3.js";
import Check from "../Core/Check.js";
import CesiumMath from "../Core/Math.js";
const defaultDimensions = new Cartesian3(1.0, 1.0, 1.0);
/**
* A ParticleEmitter that emits particles within a box.
* Particles will be positioned randomly within the box and have initial velocities emanating from the center of the box.
*
* @alias BoxEmitter
* @constructor
*
* @param {Cartesian3} dimensions The width, height and depth dimensions of the box.
*/
function BoxEmitter(dimensions) {
dimensions = dimensions ?? defaultDimensions;
//>>includeStart('debug', pragmas.debug);
Check.defined("dimensions", dimensions);
Check.typeOf.number.greaterThanOrEquals("dimensions.x", dimensions.x, 0.0);
Check.typeOf.number.greaterThanOrEquals("dimensions.y", dimensions.y, 0.0);
Check.typeOf.number.greaterThanOrEquals("dimensions.z", dimensions.z, 0.0);
//>>includeEnd('debug');
this._dimensions = Cartesian3.clone(dimensions);
}
Object.defineProperties(BoxEmitter.prototype, {
/**
* The width, height and depth dimensions of the box in meters.
* @memberof BoxEmitter.prototype
* @type {Cartesian3}
* @default new Cartesian3(1.0, 1.0, 1.0)
*/
dimensions: {
get: function () {
return this._dimensions;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.defined("value", value);
Check.typeOf.number.greaterThanOrEquals("value.x", value.x, 0.0);
Check.typeOf.number.greaterThanOrEquals("value.y", value.y, 0.0);
Check.typeOf.number.greaterThanOrEquals("value.z", value.z, 0.0);
//>>includeEnd('debug');
Cartesian3.clone(value, this._dimensions);
},
},
});
const scratchHalfDim = new Cartesian3();
/**
* Initializes the given {Particle} by setting it's position and velocity.
*
* @private
* @param {Particle} particle The particle to initialize.
*/
BoxEmitter.prototype.emit = function (particle) {
const dim = this._dimensions;
const halfDim = Cartesian3.multiplyByScalar(dim, 0.5, scratchHalfDim);
const x = CesiumMath.randomBetween(-halfDim.x, halfDim.x);
const y = CesiumMath.randomBetween(-halfDim.y, halfDim.y);
const z = CesiumMath.randomBetween(-halfDim.z, halfDim.z);
particle.position = Cartesian3.fromElements(x, y, z, particle.position);
particle.velocity = Cartesian3.normalize(
particle.position,
particle.velocity,
);
};
export default BoxEmitter;
+75
View File
@@ -0,0 +1,75 @@
import BoundingRectangle from "../Core/BoundingRectangle.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import PixelFormat from "../Core/PixelFormat.js";
import Framebuffer from "../Renderer/Framebuffer.js";
import PixelDatatype from "../Renderer/PixelDatatype.js";
import RenderState from "../Renderer/RenderState.js";
import Sampler from "../Renderer/Sampler.js";
import Texture from "../Renderer/Texture.js";
import BrdfLutGeneratorFS from "../Shaders/BrdfLutGeneratorFS.js";
/**
* @private
*/
function BrdfLutGenerator() {
this._colorTexture = undefined;
this._drawCommand = undefined;
}
Object.defineProperties(BrdfLutGenerator.prototype, {
colorTexture: {
get: function () {
return this._colorTexture;
},
},
});
function createCommand(generator, context, framebuffer) {
const drawCommand = context.createViewportQuadCommand(BrdfLutGeneratorFS, {
framebuffer: framebuffer,
renderState: RenderState.fromCache({
viewport: new BoundingRectangle(0.0, 0.0, 256.0, 256.0),
}),
});
generator._drawCommand = drawCommand;
}
BrdfLutGenerator.prototype.update = function (frameState) {
if (!defined(this._colorTexture)) {
const context = frameState.context;
const colorTexture = new Texture({
context: context,
width: 256,
height: 256,
pixelFormat: PixelFormat.RGBA,
pixelDatatype: PixelDatatype.UNSIGNED_BYTE,
sampler: Sampler.NEAREST,
});
this._colorTexture = colorTexture;
const framebuffer = new Framebuffer({
context: context,
colorTextures: [colorTexture],
destroyAttachments: false,
});
createCommand(this, context, framebuffer);
this._drawCommand.execute(context);
framebuffer.destroy();
this._drawCommand.shaderProgram =
this._drawCommand.shaderProgram &&
this._drawCommand.shaderProgram.destroy();
}
};
BrdfLutGenerator.prototype.isDestroyed = function () {
return false;
};
BrdfLutGenerator.prototype.destroy = function () {
this._colorTexture = this._colorTexture && this._colorTexture.destroy();
return destroyObject(this);
};
export default BrdfLutGenerator;
+130
View File
@@ -0,0 +1,130 @@
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
import ResourceLoader from "./ResourceLoader.js";
import ResourceLoaderState from "./ResourceLoaderState.js";
/**
* Loads an embedded or external buffer.
* <p>
* Implements the {@link ResourceLoader} interface.
* </p>
*
* @private
*/
class BufferLoader extends ResourceLoader {
/**
* @param {object} options Object with the following properties:
* @param {Uint8Array} [options.typedArray] The typed array containing the embedded buffer contents. Mutually exclusive with options.resource.
* @param {Resource} [options.resource] The {@link Resource} pointing to the external buffer. Mutually exclusive with options.typedArray.
* @param {string} [options.cacheKey] The cache key of the resource.
*
* @exception {DeveloperError} One of options.typedArray and options.resource must be defined.
*/
constructor(options) {
super();
options = options ?? Frozen.EMPTY_OBJECT;
const typedArray = options.typedArray;
const resource = options.resource;
const cacheKey = options.cacheKey;
//>>includeStart('debug', pragmas.debug);
if (defined(typedArray) === defined(resource)) {
throw new DeveloperError(
"One of options.typedArray and options.resource must be defined.",
);
}
//>>includeEnd('debug');
this._typedArray = typedArray;
this._resource = resource;
this._cacheKey = cacheKey;
this._state = ResourceLoaderState.UNLOADED;
this._promise = undefined;
}
/**
* The cache key of the resource.
*
*
* @type {string}
* @readonly
* @private
*/
get cacheKey() {
return this._cacheKey;
}
/**
* The typed array containing the embedded buffer contents.
*
*
* @type {Uint8Array}
* @readonly
* @private
*/
get typedArray() {
return this._typedArray;
}
/**
* Loads the resource.
* @returns {Promise<BufferLoader>} A promise which resolves to the loader when the resource loading is completed.
* @private
*/
async load() {
if (defined(this._promise)) {
return this._promise;
}
if (defined(this._typedArray)) {
this._promise = Promise.resolve(this);
return this._promise;
}
this._promise = loadExternalBuffer(this);
return this._promise;
}
/**
* Exposed for testing
* @private
*/
static _fetchArrayBuffer(resource) {
return resource.fetchArrayBuffer();
}
/**
* Unloads the resource.
* @private
*/
unload() {
this._typedArray = undefined;
}
}
async function loadExternalBuffer(bufferLoader) {
const resource = bufferLoader._resource;
bufferLoader._state = ResourceLoaderState.LOADING;
try {
const arrayBuffer = await BufferLoader._fetchArrayBuffer(resource);
if (bufferLoader.isDestroyed()) {
return;
}
bufferLoader._typedArray = new Uint8Array(arrayBuffer);
bufferLoader._state = ResourceLoaderState.READY;
return bufferLoader;
} catch (error) {
if (bufferLoader.isDestroyed()) {
return;
}
bufferLoader._state = ResourceLoaderState.FAILED;
const errorMessage = `Failed to load external buffer: ${resource.url}`;
throw bufferLoader.getError(errorMessage, error);
}
}
export default BufferLoader;
+149
View File
@@ -0,0 +1,149 @@
// @ts-check
import BufferPrimitive from "./BufferPrimitive.js";
import Cartesian3 from "../Core/Cartesian3.js";
import assert from "../Core/assert.js";
import BufferPrimitiveCollection from "./BufferPrimitiveCollection.js";
/** @import BufferPointCollection from "./BufferPointCollection.js"; */
const { ERR_CAPACITY } = BufferPrimitiveCollection.Error;
const scratchCartesian = new Cartesian3();
/**
* View bound to the underlying buffer data of a {@link BufferPointCollection}.
*
* <p>BufferPoint instances are {@link https://en.wikipedia.org/wiki/Flyweight_pattern|flyweights}:
* a single BufferPoint instance can be temporarily bound to any conceptual
* "point" in a BufferPointCollection, allowing very large collections to be
* iterated and updated with a minimal memory footprint.</p>
*
* Represented as one (1) position.
*
* @see BufferPointCollection
* @see BufferPointMaterial
* @see BufferPrimitive
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
* @extends BufferPrimitive
*/
class BufferPoint extends BufferPrimitive {
/**
* @type {BufferPointCollection}
* @ignore
*/
_collection = null;
/** @ignore */
static Layout = {
...BufferPrimitive.Layout,
/**
* Offset in position array to current point vertex, number of VEC3 elements.
* @type {number}
* @ignore
*/
POSITION_OFFSET_U32: BufferPrimitive.Layout.__BYTE_LENGTH,
/**
* @type {number}
* @ignore
*/
__BYTE_LENGTH: BufferPrimitive.Layout.__BYTE_LENGTH + 4,
};
/////////////////////////////////////////////////////////////////////////////
// LIFECYCLE
/**
* Copies data from source point to result.
*
* @param {BufferPoint} point
* @param {BufferPoint} result
* @return {BufferPoint}
* @override
*/
static clone(point, result) {
super.clone(point, result);
result.setPosition(point.getPosition(scratchCartesian));
return result;
}
/////////////////////////////////////////////////////////////////////////////
// GEOMETRY
/**
* Offset in collection position array to position of this point, number
* of VEC3 elements.
*
* @type {number}
* @readonly
* @ignore
*/
get vertexOffset() {
return this._getUint32(BufferPoint.Layout.POSITION_OFFSET_U32);
}
/**
* Count of positions (vertices) in this primitive. Always 1.
*
* @type {number}
* @readonly
*/
get vertexCount() {
return 1;
}
/**
* Gets the position of this point.
*
* @param {Cartesian3} [result]
* @returns {Cartesian3}
*/
getPosition(result) {
const positionF64 = this._collection._positionView;
// @ts-expect-error TODO(tsd-jsdoc): See https://github.com/CesiumGS/cesium/pull/13302.
return Cartesian3.fromArray(positionF64, this.vertexOffset * 3, result);
}
/**
* Sets the position of this point.
*
* @param {Cartesian3} position
*/
setPosition(position) {
const collection = this._collection;
const vertexOffset = this.vertexOffset;
//>>includeStart('debug', pragmas.debug);
assert(vertexOffset < collection.vertexCountMax, ERR_CAPACITY);
//>>includeEnd('debug');
collection._positionView[vertexOffset * 3] = position.x;
collection._positionView[vertexOffset * 3 + 1] = position.y;
collection._positionView[vertexOffset * 3 + 2] = position.z;
this._dirty = true;
collection._makeDirtyBoundingVolume();
}
/////////////////////////////////////////////////////////////////////////////
// DEBUG
/**
* Returns a JSON-serializable object representing the point. This encoding
* is not memory-efficient, and should generally be used for debugging and
* testing.
*
* @returns {Object} JSON-serializable object.
* @override
*/
toJSON() {
return {
...super.toJSON(),
position: Cartesian3.pack(this.getPosition(), []),
};
}
}
export default BufferPoint;
+151
View File
@@ -0,0 +1,151 @@
// @ts-check
import BufferPrimitiveCollection from "./BufferPrimitiveCollection.js";
import BufferPoint from "./BufferPoint.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Frozen from "../Core/Frozen.js";
import renderPoints from "./renderBufferPointCollection.js";
import BufferPointMaterial from "./BufferPointMaterial.js";
/** @import BlendOption from "./BlendOption.js"; */
/** @import BoundingSphere from "../Core/BoundingSphere.js"; */
/** @import ComponentDatatype from "../Core/ComponentDatatype.js"; */
/** @import Matrix4 from "../Core/Matrix4.js"; */
/** @import FrameState from "./FrameState.js"; */
/**
* @typedef {object} BufferPointOptions
* @property {Matrix4} [modelMatrix=Matrix4.IDENTITY] Transforms geometry from model to world coordinates.
* @property {boolean} [show=true]
* @property {BufferPointMaterial} [material=BufferPointMaterial.DEFAULT_MATERIAL]
* @property {number} [featureId]
* @property {object} [pickObject]
* @property {Cartesian3} [position=Cartesian3.ZERO]
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
/**
* Collection of points held in ArrayBuffer storage for performance and memory optimization.
*
* <p>Default buffer memory allocation is arbitrary, and collections cannot be resized,
* so specific per-buffer capacities should be provided in the collection
* constructor when available.</p>
*
* @example
* const collection = new BufferPointCollection({primitiveCountMax: 1024});
*
* const point = new BufferPoint();
* const material = new BufferPointMaterial({color: Color.WHITE});
*
* // Create a new point, temporarily bound to 'point' local variable.
* collection.add({
* position: new Cartesian3(0.0, 0.0, 0.0),
* material
* }, point);
*
* // Iterate over all points in collection, temporarily binding 'point'
* // local variable to each, and updating point material.
* for (let i = 0; i < collection.primitiveCount; i++) {
* collection.get(i, point);
* point.setMaterial(material);
* }
*
* @see BufferPoint
* @see BufferPrimitiveCollection
* @extends BufferPrimitiveCollection<BufferPoint>
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
class BufferPointCollection extends BufferPrimitiveCollection {
/**
* @param {object} options
* @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] Transforms geometry from model to world coordinates.
* @param {number} [options.primitiveCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY]
* @param {ComponentDatatype} [options.positionDatatype=ComponentDatatype.DOUBLE]
* @param {boolean} [options.positionNormalized=false]
* @param {boolean} [options.show=true]
* @param {boolean} [options.allowPicking=false] When <code>true</code>, primitives are pickable with {@link Scene#pick}. When <code>false</code>, memory and initialization cost are lower.
* @param {BoundingSphere} [options.boundingVolume] Bounding volume, in world space, for the collection. When
* unspecified, a bounding volume is computed automatically and updated when primitive positions change. When
* specified, users are responsible for updating bounding volume as needed. Pre-computing the bounding volume
* manually, and updating it only as needed, will improve performance for larger dynamic collections.
* @param {boolean} [options.debugShowBoundingVolume=false]
* @param {BlendOption} [options.blendOption=BlendOption.TRANSLUCENT]
*/
constructor(options = Frozen.EMPTY_OBJECT) {
super({ ...options, vertexCountMax: options.primitiveCountMax });
}
_getCollectionClass() {
return BufferPointCollection;
}
_getPrimitiveClass() {
return BufferPoint;
}
_getMaterialClass() {
return BufferPointMaterial;
}
/////////////////////////////////////////////////////////////////////////////
// COLLECTION LIFECYCLE
/**
* @param {BufferPointCollection} collection
* @returns {BufferPointCollection}
* @override
* @ignore
*/
static _cloneEmpty(collection) {
return new BufferPointCollection({
primitiveCountMax: collection.primitiveCountMax,
positionDatatype: collection.positionDatatype,
positionNormalized: collection.positionNormalized,
});
}
/////////////////////////////////////////////////////////////////////////////
// PRIMITIVE LIFECYCLE
/**
* Adds a new point to the collection, with the specified options. A
* {@link BufferPoint} instance is linked to the new point, using
* the 'result' argument if given, or a new instance if not. For repeated
* calls, prefer to reuse a single BufferPoint instance rather than
* allocating a new instance on each call.
*
* @param {BufferPointOptions} options
* @param {BufferPoint} result
* @returns {BufferPoint}
* @override
*/
add(options, result = new BufferPoint()) {
super.add(options, result);
result._setUint32(
BufferPoint.Layout.POSITION_OFFSET_U32,
this._positionCount++,
);
result.setPosition(options.position ?? Cartesian3.ZERO);
return result;
}
/////////////////////////////////////////////////////////////////////////////
// RENDER
/**
* @param {FrameState} frameState
* @ignore
*/
update(frameState) {
super.update(frameState);
const passes = frameState.passes;
if (this.show && (passes.render || passes.pick)) {
this._renderContext = renderPoints(this, frameState, this._renderContext);
}
}
}
export default BufferPointCollection;
+95
View File
@@ -0,0 +1,95 @@
// @ts-check
import Frozen from "../Core/Frozen.js";
import BufferPrimitiveMaterial from "./BufferPrimitiveMaterial.js";
/** @import Color from "../Core/Color.js"; */
/** @import BufferPoint from "./BufferPoint.js"; */
/**
* @typedef {object} BufferPointMaterialOptions
* @property {Color} [color=Color.WHITE] Color of fill.
* @property {Color} [outlineColor=Color.WHITE] Color of outline.
* @property {number} [outlineWidth=0.0] Width of outline, 0-255px.
* @property {number} [size=1.0] Size of point, 0-255px.
*/
/**
* Material description for a {@link BufferPoint}.
*
* <p>BufferPointMaterial objects are {@link Packable|packable}, stored
* when calling {@link BufferPoint#setMaterial}. Subsequent changes to the
* material will not affect the point until setMaterial() is called again.</p>
*
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
* @extends BufferPrimitiveMaterial
*/
class BufferPointMaterial extends BufferPrimitiveMaterial {
/** @ignore */
static Layout = {
...BufferPrimitiveMaterial.Layout,
SIZE_U8: BufferPrimitiveMaterial.Layout.__BYTE_LENGTH,
__BYTE_LENGTH: BufferPrimitiveMaterial.Layout.__BYTE_LENGTH + 4,
};
/**
* @type {BufferPointMaterial}
* @ignore
*/
static DEFAULT_MATERIAL = Object.freeze(new BufferPointMaterial());
/**
* @param {BufferPointMaterialOptions} [options]
*/
constructor(options = Frozen.EMPTY_OBJECT) {
super(options);
/**
* Size of point, 0-255px.
* @type {number}
*/
this.size = options.size ?? 1;
}
/**
* @override
* @param {BufferPointMaterial} material
* @param {DataView} view
* @param {number} byteOffset
* @override
*/
static pack(material, view, byteOffset) {
super.pack(material, view, byteOffset);
view.setUint8(this.Layout.SIZE_U8 + byteOffset, material.size);
}
/**
* @override
* @param {DataView} view
* @param {number} byteOffset
* @param {BufferPointMaterial} result
* @returns {BufferPointMaterial}
* @override
*/
static unpack(view, byteOffset, result) {
super.unpack(view, byteOffset, result);
result.size = view.getUint8(this.Layout.SIZE_U8 + byteOffset);
return result;
}
/////////////////////////////////////////////////////////////////////////////
// DEBUG
/**
* Returns a JSON-serializable object representing the material. This encoding
* is not memory-efficient, and should generally be used for debugging and
* testing.
*
* @returns {Object} JSON-serializable object.
*/
toJSON() {
return { ...super.toJSON(), size: this.size };
}
}
export default BufferPointMaterial;
+517
View File
@@ -0,0 +1,517 @@
// @ts-check
import assert from "../Core/assert.js";
import Check from "../Core/Check.js";
import defined from "../Core/defined.js";
import BufferPrimitive from "./BufferPrimitive.js";
import BufferPrimitiveCollection from "./BufferPrimitiveCollection.js";
/** @import { TypedArray, TypedArrayConstructor } from "../Core/globalTypes.js"; */
/** @import BufferPolygonCollection from "./BufferPolygonCollection.js"; */
const { ERR_CAPACITY, ERR_RESIZE, ERR_OUT_OF_RANGE } =
BufferPrimitiveCollection.Error;
/**
* View bound to the underlying buffer data of a {@link BufferPolygonCollection}.
*
* <p>BufferPolygon instances are {@link https://en.wikipedia.org/wiki/Flyweight_pattern|flyweights}:
* a single BufferPolygon instance can be temporarily bound to any conceptual
* "polygon" in a BufferPolygonCollection, allowing very large collections to be
* iterated and updated with a minimal memory footprint.</p>
*
* <p>Represented as one (1) external linear ring of three (3) or more positions.
* May optionally define one or more internal linear rings ("holes") within the
* polygon. Each hole is represented as a single index into the positions array,
* where the vertex at that index is the start of an internal linear ring that
* continues along the following vertices until reaching either the vertex
* index of the next hole, or the end of the vertex list. Stores a precomputed
* triangulation, represented as three vertex indices per triangle.</p>
*
* @see BufferPolygonCollection
* @see BufferPolygonMaterial
* @see BufferPrimitive
* @extends BufferPrimitive
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
class BufferPolygon extends BufferPrimitive {
/**
* @type {BufferPolygonCollection}
* @ignore
*/
_collection = null;
/** @ignore */
static Layout = {
...BufferPrimitive.Layout,
/**
* Offset in collection position array to first vertex in polygon, number
* of VEC3 elements.
* @type {number}
* @ignore
*/
POSITION_OFFSET_U32: BufferPrimitive.Layout.__BYTE_LENGTH,
/**
* Count of positions (vertices) in this polygon, number of VEC3 elements.
* @type {number}
* @ignore
*/
POSITION_COUNT_U32: BufferPrimitive.Layout.__BYTE_LENGTH + 4,
/**
* Offset in collection holes array to first hole in polygon, number of
* integer elements.
* @type {number}
* @ignore
*/
HOLE_OFFSET_U32: BufferPrimitive.Layout.__BYTE_LENGTH + 8,
/**
* Count of holes (indices) in this polygon.
* @type {number}
* @ignore
*/
HOLE_COUNT_U32: BufferPrimitive.Layout.__BYTE_LENGTH + 12,
/**
* Offset in collection triangles array to first triangle in polygon,
* number of VEC3 elements.
* @type {number}
* @ignore
*/
TRIANGLE_OFFSET_U32: BufferPrimitive.Layout.__BYTE_LENGTH + 16,
/**
* Count of triangles in this polygon, number of VEC3 elements.
* @type {number}
* @ignore
*/
TRIANGLE_COUNT_U32: BufferPrimitive.Layout.__BYTE_LENGTH + 20,
/**
* @type {number}
* @ignore
*/
__BYTE_LENGTH: BufferPrimitive.Layout.__BYTE_LENGTH + 24,
};
/////////////////////////////////////////////////////////////////////////////
// LIFECYCLE
/**
* Copies data from source polygon to result. If the result polygon is not
* new (the last polygon in the collection) then source and result polygons
* must have the same vertex counts, hole counts, and triangle counts.
*
* @param {BufferPolygon} polygon
* @param {BufferPolygon} result
* @return {BufferPolygon}
* @override
*/
static clone(polygon, result) {
super.clone(polygon, result);
result.setPositions(polygon.getPositions());
result.setHoles(polygon.getHoles());
result.setTriangles(polygon.getTriangles());
return result;
}
/////////////////////////////////////////////////////////////////////////////
// GEOMETRY
/**
* Offset in collection position array to first vertex in polygon, number
* of VEC3 elements.
*
* @type {number}
* @readonly
* @ignore
*/
get vertexOffset() {
return this._getUint32(BufferPolygon.Layout.POSITION_OFFSET_U32);
}
/**
* Count of positions (vertices) in this polygon, including both outer ring and
* internal rings (holes), number of VEC3 elements.
*
* @type {number}
* @readonly
*/
get vertexCount() {
return this._getUint32(BufferPolygon.Layout.POSITION_COUNT_U32);
}
/**
* Returns an array view of this polygon's vertex positions. If 'result'
* argument is given, vertex positions are written to that array and returned.
* Otherwise, returns an ArrayView on collection memory — changes to this array
* will not trigger render updates, which requires `.setPositions()`.
*
* @param {TypedArray} [result]
* return {TypedArray}
*/
getPositions(result) {
return this._getPositionsRange(0, this.vertexCount, result);
}
/** @param {TypedArray} positions */
setPositions(positions) {
const collection = this._collection;
const vertexOffset = this.vertexOffset;
const srcCount = this.vertexCount;
const dstCount = positions.length / 3;
const collectionCount = collection.vertexCount + dstCount - srcCount;
//>>includeStart('debug', pragmas.debug);
assert(srcCount === dstCount || this._isResizable(), ERR_RESIZE);
assert(collectionCount <= collection.vertexCountMax, ERR_CAPACITY);
//>>includeEnd('debug');
collection._positionCount = collectionCount;
this._setUint32(BufferPolygon.Layout.POSITION_COUNT_U32, dstCount);
const positionView = collection._positionView;
for (let i = 0; i < dstCount; i++) {
positionView[(vertexOffset + i) * 3] = positions[i * 3];
positionView[(vertexOffset + i) * 3 + 1] = positions[i * 3 + 1];
positionView[(vertexOffset + i) * 3 + 2] = positions[i * 3 + 2];
}
this._dirty = true;
collection._makeDirtyBoundingVolume();
}
/**
* Offset in collection position array to first vertex in polygon's outer
* linear ring, number of VEC3 elements.
*
* @type {number}
* @readonly
*/
get outerVertexOffset() {
return this.vertexOffset;
}
/**
* Count of positions (vertices) in this polygon's outer linear ring, number
* of VEC3 elements.
*
* @type {number}
* @readonly
*/
get outerVertexCount() {
if (this.holeCount > 0) {
return this.getHoles()[0];
}
return this.vertexCount;
}
/**
* Returns an array view of this polygon's outer linear ring vertex positions.
* If 'result' argument is given, vertex positions are written to that array
* and returned. Otherwise, returns an ArrayView on collection memory —
* changes to this array will not trigger render updates, which requires
* `.setPositions()`.
*
* @param {TypedArray} [result]
* @returns {TypedArray}
*/
getOuterPositions(result) {
return this._getPositionsRange(0, this.outerVertexCount, result);
}
/**
* Offset in collection holes array to first hole in polygon, number of
* integer elements.
*
* @type {number}
* @readonly
* @ignore
*/
get holeOffset() {
return this._getUint32(BufferPolygon.Layout.HOLE_OFFSET_U32);
}
/**
* Count of holes (indices) in this polygon.
*
* @type {number}
* @readonly
*/
get holeCount() {
return this._getUint32(BufferPolygon.Layout.HOLE_COUNT_U32);
}
/**
* Gets this polygon's hole indices, with each hole represented as a single
* offset into this polygon's positions array. Each hole implicitly
* continues along an internal linear ring from that vertex offset until
* reaching either the end of the positions array, or the next hole offset.
*
* If 'result' argument is given, hole indices are written to that array and
* returned. Otherwise, returns an ArrayView on collection memory — changes
* to this array will not trigger render updates, which requires `.setHoles()`.
*
* @param {TypedArray} [result]
* @returns {TypedArray}
*/
getHoles(result) {
const { holeOffset, holeCount } = this;
const holeIndexView = this._collection._holeIndexView;
if (!defined(result)) {
const byteOffset =
holeIndexView.byteOffset + holeOffset * holeIndexView.BYTES_PER_ELEMENT;
const TypedArray = /** @type {TypedArrayConstructor} */ (
holeIndexView.constructor
);
return new TypedArray(
/** @type {ArrayBuffer} */ (holeIndexView.buffer),
byteOffset,
holeCount,
);
}
for (let i = 0; i < holeCount; i++) {
result[i] = holeIndexView[holeOffset + i];
}
return result;
}
/**
* Sets this polygon's hole indices, with holes represented as a single
* offset into this polygon's positions array. Each hole implicitly
* continues along an internal linear ring from that vertex offset until
* reaching either the end of the positions array, or the next hole offset.
*
* @param {TypedArray} holes
*/
setHoles(holes) {
const collection = this._collection;
const holeOffset = this.holeOffset;
const srcCount = this.holeCount;
const dstCount = holes.length;
const collectionCount = collection.holeCount + dstCount - srcCount;
//>>includeStart('debug', pragmas.debug);
assert(srcCount === dstCount || this._isResizable(), ERR_RESIZE);
assert(collectionCount <= collection.holeCountMax, ERR_CAPACITY);
//>>includeEnd('debug');
collection._holeCount = collectionCount;
this._setUint32(BufferPolygon.Layout.HOLE_COUNT_U32, dstCount);
const holeIndexView = collection._holeIndexView;
for (let i = 0; i < dstCount; i++) {
holeIndexView[holeOffset + i] = holes[i];
}
this._dirty = true;
}
/**
* Returns the number of (VEC3) vertices in the specified hole.
*
* @param {number} holeIndex
* @returns {number}
*/
getHoleVertexCount(holeIndex) {
const holes = this.getHoles();
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number.greaterThanOrEquals("holeIndex", holeIndex, 0);
Check.typeOf.number.lessThan("holeIndex", holeIndex, holes.length);
//>>includeEnd('debug');
const holeVertexOffset = holes[holeIndex];
return holeIndex === holes.length - 1
? this.vertexCount - holeVertexOffset
: holes[holeIndex + 1] - holeVertexOffset;
}
/**
* Returns an array view of the inner linear ring vertex positions for the
* specified hole. If 'result' argument is given, vertex positions are written
* to that array and returned. Otherwise, returns an ArrayView on collection
* memory — changes to this array will not trigger render updates, which
* requires `.setPositions()`.
*
* @param {number} holeIndex
* @param {TypedArray} [result]
* return {TypedArray}
*/
getHolePositions(holeIndex, result) {
const holes = this.getHoles();
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number.greaterThanOrEquals("holeIndex", holeIndex, 0);
Check.typeOf.number.lessThan("holeIndex", holeIndex, holes.length);
//>>includeEnd('debug');
const holeVertexOffset = holes[holeIndex];
const holeVertexCount = this.getHoleVertexCount(holeIndex);
return this._getPositionsRange(holeVertexOffset, holeVertexCount, result);
}
/**
* Internal helper for accessing vertex positions. 'vertexOffset' argument
* is relative to the start of the polygon's vertex block, with 0 being
* the first vertex in the polygon. If 'result' argument is given, the
* requested range of vertices are written to the result array and returned.
* Otherwise, returns an ArrayView on collection memory.
*
* @param {number} vertexOffset
* @param {number} vertexCount
* @param {TypedArray} [result]
* @returns {TypedArray}
* @private
*/
_getPositionsRange(vertexOffset, vertexCount, result) {
const collection = this._collection;
const positionView = this._collection._positionView;
const collectionVertexOffset = this.vertexOffset + vertexOffset;
//>>includeStart('debug', pragmas.debug);
assert(collectionVertexOffset >= 0, ERR_OUT_OF_RANGE);
assert(collectionVertexOffset < collection.vertexCount, ERR_OUT_OF_RANGE);
assert(vertexCount > 0, ERR_OUT_OF_RANGE);
assert(vertexCount <= this.vertexCount, ERR_OUT_OF_RANGE);
//>>includeEnd('debug');
if (!defined(result)) {
const byteOffset =
positionView.byteOffset +
collectionVertexOffset * 3 * positionView.BYTES_PER_ELEMENT;
const TypedArray = /** @type {TypedArrayConstructor} */ (
positionView.constructor
);
return new TypedArray(
/** @type {ArrayBuffer} */ (positionView.buffer),
byteOffset,
vertexCount * 3,
);
}
for (let i = 0; i < vertexCount; i++) {
result[i * 3] = positionView[(collectionVertexOffset + i) * 3];
result[i * 3 + 1] = positionView[(collectionVertexOffset + i) * 3 + 1];
result[i * 3 + 2] = positionView[(collectionVertexOffset + i) * 3 + 2];
}
return result;
}
/**
* Offset in collection triangles array to first triangle in polygon,
* number of VEC3 elements.
* @type {number}
* @readonly
* @ignore
*/
get triangleOffset() {
return this._getUint32(BufferPolygon.Layout.TRIANGLE_OFFSET_U32);
}
/**
* Count of triangles in this polygon, number of VEC3 elements.
*
* @type {number}
* @readonly
*/
get triangleCount() {
return this._getUint32(BufferPolygon.Layout.TRIANGLE_COUNT_U32);
}
/**
* Returns an array view of this polygon's triangle indices, represented as
* three vertex indices per triangle.
*
* If 'result' argument is given, triangle indices are written to that array
* and returned. Otherwise, returns an ArrayView on collection memory —
* changes to this array will not trigger render updates, which requires
* `.setTriangles()`.
*
* @param {TypedArray} [result]
* @returns {TypedArray}
*/
getTriangles(result) {
const { triangleOffset, triangleCount } = this;
const indices = this._collection._triangleIndexView;
if (!defined(result)) {
const byteOffset =
indices.byteOffset + triangleOffset * 3 * indices.BYTES_PER_ELEMENT;
const TypedArray = /** @type {TypedArrayConstructor} */ (
indices.constructor
);
return new TypedArray(
/** @type {ArrayBuffer} */ (indices.buffer),
byteOffset,
triangleCount * 3,
);
}
for (let i = 0; i < triangleCount; i++) {
result[i * 3] = indices[(triangleOffset + i) * 3];
result[i * 3 + 1] = indices[(triangleOffset + i) * 3 + 1];
result[i * 3 + 2] = indices[(triangleOffset + i) * 3 + 2];
}
return result;
}
/**
* Sets this polygon's triangle indices, represented as three vertex indices
* per triangle.
*
* @param {TypedArray} indices
*/
setTriangles(indices) {
const collection = this._collection;
const triangleOffset = this.triangleOffset;
const srcCount = this.triangleCount;
const dstCount = indices.length / 3;
const collectionCount = collection.triangleCount + dstCount - srcCount;
//>>includeStart('debug', pragmas.debug);
assert(srcCount === dstCount || this._isResizable(), ERR_RESIZE);
assert(collectionCount <= collection.triangleCountMax, ERR_CAPACITY);
//>>includeEnd('debug');
collection._triangleCount += dstCount - srcCount;
this._setUint32(BufferPolygon.Layout.TRIANGLE_COUNT_U32, dstCount);
const dstIndices = collection._triangleIndexView;
for (let i = 0; i < dstCount; i++) {
dstIndices[(triangleOffset + i) * 3] = indices[i * 3];
dstIndices[(triangleOffset + i) * 3 + 1] = indices[i * 3 + 1];
dstIndices[(triangleOffset + i) * 3 + 2] = indices[i * 3 + 2];
}
this._dirty = true;
}
/////////////////////////////////////////////////////////////////////////////
// DEBUG
/**
* Returns a JSON-serializable object representing the polygon. This encoding
* is not memory-efficient, and should generally be used for debugging and
* testing.
*
* @returns {Object} JSON-serializable object.
* @override
*/
toJSON() {
return {
...super.toJSON(),
positions: Array.from(this.getPositions()),
holes: Array.from(this.getHoles()),
triangles: Array.from(this.getTriangles()),
};
}
}
export default BufferPolygon;
+380
View File
@@ -0,0 +1,380 @@
// @ts-check
import defined from "../Core/defined.js";
import BufferPrimitiveCollection from "./BufferPrimitiveCollection.js";
import BufferPolygon from "./BufferPolygon.js";
import Frozen from "../Core/Frozen.js";
import assert from "../Core/assert.js";
import IndexDatatype from "../Core/IndexDatatype.js";
import renderPolygons from "./renderBufferPolygonCollection.js";
import BufferPolygonMaterial from "./BufferPolygonMaterial.js";
/** @import BlendOption from "./BlendOption.js"; */
/** @import BoundingSphere from "../Core/BoundingSphere.js"; */
/** @import { TypedArray } from "../Core/globalTypes.js"; */
/** @import Matrix4 from "../Core/Matrix4.js"; */
/** @import FrameState from "./FrameState.js" */
/** @import ComponentDatatype from "../Core/ComponentDatatype.js"; */
const { ERR_CAPACITY } = BufferPrimitiveCollection.Error;
/**
* @typedef {object} BufferPolygonOptions
* @property {Matrix4} [modelMatrix=Matrix4.IDENTITY] Transforms geometry from model to world coordinates.
* @property {boolean} [show=true]
* @property {BufferPolygonMaterial} [material=BufferPolygonMaterial.DEFAULT_MATERIAL]
* @property {number} [featureId]
* @property {object} [pickObject]
* @property {TypedArray} [positions]
* @property {TypedArray} [holes]
* @property {TypedArray} [triangles]
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
/**
* Collection of polygons held in ArrayBuffer storage for performance and memory optimization.
*
* <p>Default buffer memory allocation is arbitrary, and collections cannot be resized,
* so specific per-buffer capacities should be provided in the collection
* constructor when available.</p>
*
* @example
* import earcut from "earcut";
*
* const collection = new BufferPolygonCollection({
* primitiveCountMax: 1024,
* vertexCountMax: 4096,
* holeCountMax: 1024,
* triangleCountMax: 2048,
* });
*
* const polygon = new BufferPolygon();
* const positions = [ ... ];
* const holes = [ ... ];
* const material = new BufferPolygonMaterial({color: Color.WHITE});
*
* // Create a new polygon, temporarily bound to 'polygon' local variable.
* collection.add({
* positions: new Float64Array(positions),
* holes: new Uint32Array(holes),
* triangles: new Uint32Array(earcut(positions, holes, 3)),
* material
* }, polygon);
*
* // Iterate over all polygons in collection, temporarily binding 'polygon'
* // local variable to each, and updating polygon material.
* for (let i = 0; i < collection.primitiveCount; i++) {
* collection.get(i, polygon);
* polygon.setMaterial(material);
* }
*
* @see BufferPolygon
* @see BufferPolygonMaterial
* @see BufferPrimitiveCollection
* @extends BufferPrimitiveCollection<BufferPolygon>
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
class BufferPolygonCollection extends BufferPrimitiveCollection {
/**
* @param {object} options
* @param {number} [options.primitiveCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY]
* @param {number} [options.vertexCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY]
* @param {number} [options.holeCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY]
* @param {number} [options.triangleCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY]
* @param {ComponentDatatype} [options.positionDatatype=ComponentDatatype.DOUBLE]
* @param {boolean} [options.positionNormalized=false]
* @param {boolean} [options.show=true]
* @param {boolean} [options.allowPicking=true] When <code>true</code>, primitives are pickable with {@link Scene#pick}. When <code>false</code>, memory and initialization cost are lower.
* @param {BoundingSphere} [options.boundingVolume] Bounding volume, in world space, for the collection. When
* unspecified, a bounding volume is computed automatically and updated when primitive positions change. When
* specified, users are responsible for updating bounding volume as needed. Pre-computing the bounding volume
* manually, and updating it only as needed, will improve performance for larger dynamic collections.
* @param {boolean} [options.debugShowBoundingVolume=false]
* @param {BlendOption} [options.blendOption=BlendOption.TRANSLUCENT]
*/
constructor(options = Frozen.EMPTY_OBJECT) {
super(options);
/**
* @type {number}
* @ignore
*/
this._holeCount = 0;
/**
* @type {number}
* @protected
* @ignore
*/
this._holeCountMax =
options.holeCountMax ?? BufferPrimitiveCollection.DEFAULT_CAPACITY;
/**
* @type {TypedArray}
* @ignore
*/
this._holeIndexView = null;
/**
* @type {number}
* @ignore
*/
this._triangleCount = 0;
/**
* @type {number}
* @protected
* @ignore
*/
this._triangleCountMax =
options.triangleCountMax ?? BufferPrimitiveCollection.DEFAULT_CAPACITY;
/**
* @type {TypedArray}
* @ignore
*/
this._triangleIndexView = null;
this._allocateHoleIndexBuffer();
this._allocateTriangleIndexBuffer();
}
_getCollectionClass() {
return BufferPolygonCollection;
}
_getPrimitiveClass() {
return BufferPolygon;
}
_getMaterialClass() {
return BufferPolygonMaterial;
}
/////////////////////////////////////////////////////////////////////////////
// COLLECTION LIFECYCLE
/**
* @private
* @ignore
*/
_allocateHoleIndexBuffer() {
// @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
this._holeIndexView = IndexDatatype.createTypedArray(
this._positionCountMax,
this._holeCountMax,
);
}
/**
* @private
* @ignore
*/
_allocateTriangleIndexBuffer() {
// @ts-expect-error Requires https://github.com/CesiumGS/cesium/pull/13203.
this._triangleIndexView = IndexDatatype.createTypedArray(
this._positionCountMax,
this._triangleCountMax * 3,
);
}
/**
* Duplicates the contents of this collection into the result collection.
* Result collection is not resized, and must contain enough space for all
* primitives in the source collection. Existing polygons in the result
* collection will be overwritten.
*
* <p>Useful when allocating more space for a collection that has reached its
* capacity, and efficiently transferring polygons to the new collection.</p>
*
* @example
* const result = new BufferPolygonCollection({ ... }); // allocate larger 'result' collection
* BufferPolygonCollection.clone(collection, result); // copy polygons from 'collection' into 'result'
*
* @param {BufferPolygonCollection} collection
* @param {BufferPolygonCollection} result
* @returns {BufferPolygonCollection}
*/
static clone(collection, result) {
super.clone(collection, result);
//>>includeStart('debug', pragmas.debug);
assert(collection.holeCount <= result.holeCountMax, ERR_CAPACITY);
assert(collection.triangleCount <= result.triangleCountMax, ERR_CAPACITY);
//>>includeEnd('debug');
this._copySubArray(
collection._holeIndexView,
result._holeIndexView,
collection.holeCount,
);
this._copySubArray(
collection._triangleIndexView,
result._triangleIndexView,
collection._triangleCount * 3,
);
result._holeCount = collection._holeCount;
result._triangleCount = collection._triangleCount;
return result;
}
/**
* @param {BufferPolygonCollection} collection
* @returns {BufferPolygonCollection}
* @override
* @ignore
*/
static _cloneEmpty(collection) {
return new BufferPolygonCollection({
primitiveCountMax: collection.primitiveCountMax,
vertexCountMax: collection.vertexCountMax,
holeCountMax: collection.holeCountMax,
triangleCountMax: collection.triangleCountMax,
positionDatatype: collection.positionDatatype,
positionNormalized: collection.positionNormalized,
});
}
/**
* @param {BufferPolygonCollection} src
* @param {BufferPolygonCollection} dst
* @override
* @ignore
*/
static _replaceBuffers(src, dst) {
super._replaceBuffers(src, dst);
dst._holeIndexView = src._holeIndexView;
dst._triangleIndexView = src._triangleIndexView;
}
/////////////////////////////////////////////////////////////////////////////
// PRIMITIVE LIFECYCLE
/**
* Adds a new polygon to the collection, with the specified options. A
* {@link BufferPolygon} instance is linked to the new polygon, using
* the 'result' argument if given, or a new instance if not. For repeated
* calls, prefer to reuse a single BufferPolygon instance rather than
* allocating a new instance on each call.
*
* @param {BufferPolygonOptions} options
* @param {BufferPolygon} result
* @returns {BufferPolygon}
* @override
*/
add(options, result = new BufferPolygon()) {
super.add(options, result);
const vertexOffset = this._positionCount;
result._setUint32(BufferPolygon.Layout.POSITION_OFFSET_U32, vertexOffset);
result._setUint32(BufferPolygon.Layout.POSITION_COUNT_U32, 0);
const holeOffset = this._holeCount;
result._setUint32(BufferPolygon.Layout.HOLE_OFFSET_U32, holeOffset);
result._setUint32(BufferPolygon.Layout.HOLE_COUNT_U32, 0);
const triangleOffset = this._triangleCount;
result._setUint32(BufferPolygon.Layout.TRIANGLE_OFFSET_U32, triangleOffset);
result._setUint32(BufferPolygon.Layout.TRIANGLE_COUNT_U32, 0);
if (defined(options.positions)) {
result.setPositions(options.positions);
}
if (defined(options.holes)) {
result.setHoles(options.holes);
}
if (defined(options.triangles)) {
result.setTriangles(options.triangles);
}
return result;
}
/////////////////////////////////////////////////////////////////////////////
// RENDER
/**
* @param {FrameState} frameState
* @ignore
*/
update(frameState) {
super.update(frameState);
const passes = frameState.passes;
if (this.show && (passes.render || passes.pick)) {
this._renderContext = renderPolygons(
this,
frameState,
this._renderContext,
);
}
}
/////////////////////////////////////////////////////////////////////////////
// ACCESSORS
/**
* Total byte length of buffers owned by this collection. Includes any unused
* space allocated by {@link primitiveCountMax}, even if no polygons have
* yet been added in that space.
*
* @type {number}
* @readonly
* @override
*/
get byteLength() {
return (
super.byteLength +
this._holeIndexView.byteLength +
this._triangleIndexView.byteLength
);
}
/**
* Number of holes in collection. Must be <= {@link holeCountMax}.
*
* @type {number}
* @readonly
*/
get holeCount() {
return this._holeCount;
}
/**
* Maximum number of holes in collection. Must be >= {@link holeCount}.
*
* @type {number}
* @readonly
* @default {@link BufferPrimitiveCollection.DEFAULT_CAPACITY}
*/
get holeCountMax() {
return this._holeCountMax;
}
/**
* Number of triangles in collection. Must be <= {@link triangleCountMax}.
*
* @type {number}
* @readonly
*/
get triangleCount() {
return this._triangleCount;
}
/**
* Maximum number of triangles in collection. Must be >= {@link triangleCount}.
*
* @type {number}
* @readonly
* @default {@link BufferPrimitiveCollection.DEFAULT_CAPACITY}
*/
get triangleCountMax() {
return this._triangleCountMax;
}
}
export default BufferPolygonCollection;
+41
View File
@@ -0,0 +1,41 @@
// @ts-check
import Frozen from "../Core/Frozen.js";
import BufferPrimitiveMaterial from "./BufferPrimitiveMaterial.js";
/** @import Color from "../Core/Color.js"; */
/** @import BufferPolygon from "./BufferPolygon.js"; */
/**
* @typedef {object} BufferPolygonMaterialOptions
* @property {Color} [color=Color.WHITE] Color of fill.
* @property {Color} [outlineColor=Color.WHITE] Color of outline.
* @property {number} [outlineWidth=0.0] Width of outline, 0-255px.
*/
/**
* Material description for a {@link BufferPolygon}.
*
* <p>BufferPolygonMaterial objects are {@link Packable|packable}, stored
* when calling {@link BufferPolygon#setMaterial}. Subsequent changes to the
* material will not affect the polygon until setMaterial() is called again.</p>
*
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
* @extends BufferPrimitiveMaterial
*/
class BufferPolygonMaterial extends BufferPrimitiveMaterial {
/**
* @type {BufferPolygonMaterial}
* @ignore
*/
static DEFAULT_MATERIAL = Object.freeze(new BufferPolygonMaterial());
/**
* @param {BufferPolygonMaterialOptions} [options]
*/
constructor(options = Frozen.EMPTY_OBJECT) {
super(options);
}
}
export default BufferPolygonMaterial;
+186
View File
@@ -0,0 +1,186 @@
// @ts-check
import BufferPrimitive from "./BufferPrimitive.js";
import assert from "../Core/assert.js";
import BufferPrimitiveCollection from "./BufferPrimitiveCollection.js";
import defined from "../Core/defined.js";
/** @import { TypedArray, TypedArrayConstructor } from "../Core/globalTypes.js"; */
/** @import BufferPolylineCollection from "./BufferPolylineCollection.js"; */
const { ERR_RESIZE, ERR_CAPACITY } = BufferPrimitiveCollection.Error;
/**
* View bound to the underlying buffer data of a {@link BufferPolylineCollection}.
*
* <p>BufferPolyline instances are {@link https://en.wikipedia.org/wiki/Flyweight_pattern|flyweights}:
* a single BufferPolyline instance can be temporarily bound to any conceptual
* "polyline" in a BufferPolylineCollection, allowing very large collections to be
* iterated and updated with a minimal memory footprint.</p>
*
* Represented as two (2) or more positions.
*
* @see BufferPolylineCollection
* @see BufferPolylineMaterial
* @see BufferPrimitive
* @extends BufferPrimitive
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
class BufferPolyline extends BufferPrimitive {
/**
* @type {BufferPolylineCollection}
* @ignore
*/
_collection = null;
/** @ignore */
static Layout = {
...BufferPrimitive.Layout,
/**
* Offset in position array to first vertex in polyline, number of VEC3 elements.
* @type {number}
* @ignore
*/
POSITION_OFFSET_U32: BufferPrimitive.Layout.__BYTE_LENGTH,
/**
* Count of positions (vertices) in this polyline, number of VEC3 elements.
* @type {number}
* @ignore
*/
POSITION_COUNT_U32: BufferPrimitive.Layout.__BYTE_LENGTH + 4,
/**
* @type {number}
* @ignore
*/
__BYTE_LENGTH: BufferPrimitive.Layout.__BYTE_LENGTH + 8,
};
/////////////////////////////////////////////////////////////////////////////
// LIFECYCLE
/**
* Copies data from source polyline to result. If the result polyline is not
* new (the last polyline in the collection) then source and result polylines
* must have the same vertex counts.
*
* @param {BufferPolyline} polyline
* @param {BufferPolyline} result
* @return {BufferPolyline}
* @override
*/
static clone(polyline, result) {
super.clone(polyline, result);
result.setPositions(polyline.getPositions());
return result;
}
/////////////////////////////////////////////////////////////////////////////
// GEOMETRY
/**
* Offset in collection position array to first vertex in polyline, number
* of VEC3 elements.
*
* @type {number}
* @readonly
* @ignore
*/
get vertexOffset() {
return this._getUint32(BufferPolyline.Layout.POSITION_OFFSET_U32);
}
/**
* Count of positions (vertices) in this polyline, number of VEC3 elements.
*
* @type {number}
* @readonly
*/
get vertexCount() {
return this._getUint32(BufferPolyline.Layout.POSITION_COUNT_U32);
}
/**
* Returns an array view of this polyline's vertex positions. If 'result'
* argument is given, vertex positions are written to that array and returned.
* Otherwise, returns an ArrayView on collection memory — changes to this array
* will not trigger render updates, which requires `.setPositions()`.
*
* @param {TypedArray} [result]
* return {TypedArray}
*/
getPositions(result) {
const { vertexOffset, vertexCount } = this;
const positionView = this._collection._positionView;
if (!defined(result)) {
const byteOffset =
positionView.byteOffset +
vertexOffset * 3 * positionView.BYTES_PER_ELEMENT;
const TypedArray = /** @type {TypedArrayConstructor} */ (
positionView.constructor
);
return new TypedArray(
/** @type {ArrayBuffer} */ (positionView.buffer),
byteOffset,
vertexCount * 3,
);
}
for (let i = 0; i < vertexCount; i++) {
result[i * 3] = positionView[(vertexOffset + i) * 3];
result[i * 3 + 1] = positionView[(vertexOffset + i) * 3 + 1];
result[i * 3 + 2] = positionView[(vertexOffset + i) * 3 + 2];
}
return result;
}
/** @param {TypedArray} positions */
setPositions(positions) {
const collection = this._collection;
const vertexOffset = this.vertexOffset;
const srcCount = this.vertexCount;
const dstCount = positions.length / 3;
const collectionCount = collection._positionCount + dstCount - srcCount;
//>>includeStart('debug', pragmas.debug);
assert(srcCount === dstCount || this._isResizable(), ERR_RESIZE);
assert(collectionCount <= collection.vertexCountMax, ERR_CAPACITY);
//>>includeEnd('debug');
collection._positionCount = collectionCount;
this._setUint32(BufferPolyline.Layout.POSITION_COUNT_U32, dstCount);
const positionView = collection._positionView;
for (let i = 0; i < dstCount; i++) {
positionView[(vertexOffset + i) * 3] = positions[i * 3];
positionView[(vertexOffset + i) * 3 + 1] = positions[i * 3 + 1];
positionView[(vertexOffset + i) * 3 + 2] = positions[i * 3 + 2];
}
this._dirty = true;
collection._makeDirtyBoundingVolume();
}
/////////////////////////////////////////////////////////////////////////////
// DEBUG
/**
* Returns a JSON-serializable object representing the polyline. This encoding
* is not memory-efficient, and should generally be used for debugging and
* testing.
*
* @returns {Object} JSON-serializable object.
* @override
*/
toJSON() {
return {
...super.toJSON(),
positions: Array.from(this.getPositions()),
};
}
}
export default BufferPolyline;
+140
View File
@@ -0,0 +1,140 @@
// @ts-check
import defined from "../Core/defined.js";
import BufferPrimitiveCollection from "./BufferPrimitiveCollection.js";
import BufferPolyline from "./BufferPolyline.js";
import renderPolylines from "./renderBufferPolylineCollection.js";
import BufferPolylineMaterial from "./BufferPolylineMaterial.js";
/** @import { TypedArray } from "../Core/globalTypes.js"; */
/** @import Matrix4 from "../Core/Matrix4.js"; */
/** @import FrameState from "./FrameState.js" */
/**
* @typedef {object} BufferPolylineOptions
* @property {Matrix4} [modelMatrix=Matrix4.IDENTITY] Transforms geometry from model to world coordinates.
* @property {boolean} [show=true]
* @property {BufferPolylineMaterial} [material=BufferPolylineMaterial.DEFAULT_MATERIAL]
* @property {number} [featureId]
* @property {object} [pickObject]
* @property {TypedArray} [positions]
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
/**
* Collection of polylines held in ArrayBuffer storage for performance and memory optimization.
*
* <p>Default buffer memory allocation is arbitrary, and collections cannot be resized,
* so specific per-buffer capacities should be provided in the collection
* constructor when available.</p>
*
* @example
* const collection = new BufferPolylineCollection({
* primitiveCountMax: 1024,
* vertexCountMax: 4096,
* });
*
* const polyline = new BufferPolyline();
* const material = new BufferPolylineMaterial({color: Color.WHITE});
*
* // Create a new polyline, temporarily bound to 'polyline' local variable.
* collection.add({
* positions: new Float64Array([ ... ]),
* material,
* }, polyline);
*
* // Iterate over all polylines in collection, temporarily binding 'polyline'
* // local variable to each, and updating polyline material.
* for (let i = 0; i < collection.primitiveCount; i++) {
* collection.get(i, polyline);
* polyline.setMaterial(material);
* }
*
* @see BufferPolyline
* @see BufferPolylineMaterial
* @see BufferPrimitiveCollection
* @extends BufferPrimitiveCollection<BufferPolyline>
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
class BufferPolylineCollection extends BufferPrimitiveCollection {
_getCollectionClass() {
return BufferPolylineCollection;
}
_getPrimitiveClass() {
return BufferPolyline;
}
_getMaterialClass() {
return BufferPolylineMaterial;
}
/////////////////////////////////////////////////////////////////////////////
// COLLECTION LIFECYCLE
/**
* @param {BufferPolylineCollection} collection
* @returns {BufferPolylineCollection}
* @override
* @ignore
*/
static _cloneEmpty(collection) {
return new BufferPolylineCollection({
primitiveCountMax: collection.primitiveCountMax,
vertexCountMax: collection.vertexCountMax,
positionDatatype: collection.positionDatatype,
positionNormalized: collection.positionNormalized,
});
}
/////////////////////////////////////////////////////////////////////////////
// PRIMITIVE LIFECYCLE
/**
* Adds a new polyline to the collection, with the specified options. A
* {@link BufferPolyline} instance is linked to the new polyline, using
* the 'result' argument if given, or a new instance if not. For repeated
* calls, prefer to reuse a single BufferPolyline instance rather than
* allocating a new instance on each call.
*
* @param {BufferPolylineOptions} options
* @param {BufferPolyline} result
* @returns {BufferPolyline}
* @override
*/
add(options, result = new BufferPolyline()) {
super.add(options, result);
const vertexOffset = this._positionCount;
result._setUint32(BufferPolyline.Layout.POSITION_OFFSET_U32, vertexOffset);
result._setUint32(BufferPolyline.Layout.POSITION_COUNT_U32, 0);
if (defined(options.positions)) {
result.setPositions(options.positions);
}
return result;
}
/////////////////////////////////////////////////////////////////////////////
// RENDER
/**
* @param {FrameState} frameState
* @ignore
*/
update(frameState) {
super.update(frameState);
const passes = frameState.passes;
if (this.show && (passes.render || passes.pick)) {
this._renderContext = renderPolylines(
this,
frameState,
this._renderContext,
);
}
}
}
export default BufferPolylineCollection;
+93
View File
@@ -0,0 +1,93 @@
// @ts-check
import Frozen from "../Core/Frozen.js";
import BufferPrimitiveMaterial from "./BufferPrimitiveMaterial.js";
/** @import Color from "../Core/Color.js"; */
/** @import BufferPolyline from "./BufferPolyline.js"; */
/**
* @typedef {object} BufferPolylineMaterialOptions
* @property {Color} [color=Color.WHITE] Color of fill.
* @property {Color} [outlineColor=Color.WHITE] Color of outline.
* @property {number} [outlineWidth=0.0] Width of outline, 0-255px.
* @property {number} [width=1.0] Width of line, 0-255px.
*/
/**
* Material description for a {@link BufferPolyline}.
*
* <p>BufferPolylineMaterial objects are {@link Packable|packable}, stored
* when calling {@link BufferPolyline#setMaterial}. Subsequent changes to the
* material will not affect the polyline until setMaterial() is called again.</p>
*
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
* @extends BufferPrimitiveMaterial
*/
class BufferPolylineMaterial extends BufferPrimitiveMaterial {
/** @ignore */
static Layout = {
...BufferPrimitiveMaterial.Layout,
WIDTH_U8: BufferPrimitiveMaterial.Layout.__BYTE_LENGTH,
__BYTE_LENGTH: BufferPrimitiveMaterial.Layout.__BYTE_LENGTH + 4,
};
/**
* @type {BufferPolylineMaterial}
* @ignore
*/
static DEFAULT_MATERIAL = Object.freeze(new BufferPolylineMaterial());
/**
* @param {BufferPolylineMaterialOptions} [options]
*/
constructor(options = Frozen.EMPTY_OBJECT) {
super(options);
/**
* Width of polyline, 0255px.
* @type {number}
*/
this.width = options.width ?? 1;
}
/**
* @param {BufferPolylineMaterial} material
* @param {DataView} view
* @param {number} byteOffset
* @override
*/
static pack(material, view, byteOffset) {
super.pack(material, view, byteOffset);
view.setUint8(this.Layout.WIDTH_U8 + byteOffset, material.width);
}
/**
* @param {DataView} view
* @param {number} byteOffset
* @param {BufferPolylineMaterial} result
* @returns {BufferPolylineMaterial}
* @override
*/
static unpack(view, byteOffset, result) {
super.unpack(view, byteOffset, result);
result.width = view.getUint8(this.Layout.WIDTH_U8 + byteOffset);
return result;
}
/////////////////////////////////////////////////////////////////////////////
// DEBUG
/**
* Returns a JSON-serializable object representing the material. This encoding
* is not memory-efficient, and should generally be used for debugging and
* testing.
*
* @returns {Object} JSON-serializable object.
*/
toJSON() {
return { ...super.toJSON(), width: this.width };
}
}
export default BufferPolylineMaterial;
+347
View File
@@ -0,0 +1,347 @@
// @ts-check
import assert from "../Core/assert.js";
/** @import BufferPrimitiveCollection from './BufferPrimitiveCollection.js'; */
/** @import BufferPrimitiveMaterial from "./BufferPrimitiveMaterial.js"; */
/**
* View bound to the underlying buffer data of a {@link BufferPrimitiveCollection}. Abstract.
*
* <p>BufferPrimitive instances are intended to be reused when iterating over large collections,
* and temporarily bound to a primitive index while performing read/write operations on that primitive,
* before being rebound to the next primitive, using the
* {@link https://en.wikipedia.org/wiki/Flyweight_pattern|flyweight pattern}.</p>
*
* @see BufferPrimitiveCollection
* @see BufferPrimitiveMaterial
* @see BufferPoint
* @see BufferPolyline
* @see BufferPolygon
*
* @abstract
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
class BufferPrimitive {
/**
* Collecton containing the primitive(s) for which this instance currently
* provides a view.
*
* @type {BufferPrimitiveCollection<BufferPrimitive>}
* @ignore
*/
_collection = null;
/**
* Index of the primitive for which this instance currently provides a view.
*
* @type {number}
* @ignore
*/
_index = -1;
/**
* Byte offset, into the collection's primitive buffer, of the primitive for
* which this instance currently provides a view.
*
* @type {number}
* @ignore
*/
_byteOffset = -1;
/**
* Binary layout for this primitive type in collection's primitive buffer.
* Each `Layout.MY_KEY_U32` entry is an offset in bytes, relative to the
* start offset of the primitive, with the suffix indicating the data type
* stored at that offset.
*
* The final entry, `__BYTE_LENGTH`, is not a pointer into a buffer — its
* literal value is the total byte length of one primitive in the primitive
* buffer, exclusive of other buffers.
*
* @ignore
*/
static Layout = {
/**
* Feature ID associated with the primitive; not required to be unique.
* @type {number}
* @ignore
*/
FEATURE_ID_U32: 0,
/**
* Boolean (0 or 1) flag indicating whether primitive is shown.
* @type {number}
* @ignore
*/
SHOW_U8: 4,
/**
* Boolean (0 or 1) flag indicating whether primitive is dirty.
* @type {number}
* @ignore
*/
DIRTY_U8: 5,
/**
* Pick ID (uint32) of primitive.
* @type {number}
* @ignore
*/
PICK_ID_U32: 8,
/**
* Byte length of one primitive in the primitive buffer, exclusive of
* other buffers. Literal value, not a pointer.
* @type {number}
* @ignore
*/
__BYTE_LENGTH: 12,
};
/////////////////////////////////////////////////////////////////////////////
// LIFECYCLE
/**
* Copies data from source primitive to result. If the result primitive is not
* new (the last primitive in the collection) then source and result primitives
* must have the same vertex counts.
*
* @param {BufferPrimitive} primitive
* @param {BufferPrimitive} result
* @returns {BufferPrimitive}
*/
static clone(primitive, result) {
const SrcMaterialClass = primitive._collection._getMaterialClass();
//>>includeStart('debug', pragmas.debug);
const DstMaterialClass = result._collection._getMaterialClass();
assert(SrcMaterialClass === DstMaterialClass, "Incompatible materials");
//>>includeEnd('debug');
result.featureId = primitive.featureId;
result.show = primitive.show;
result.setMaterial(primitive.getMaterial(new SrcMaterialClass()));
return result;
}
/**
* Returns true if this primitive's memory footprint is resizable. Only the
* newest (most recently created) primitive in a collection can be resized,
* to guarantee fast and stable performance.
*
* @returns {boolean}
* @protected
* @ignore
*/
_isResizable() {
return this._index === this._collection.primitiveCount - 1;
}
/////////////////////////////////////////////////////////////////////////////
// ACCESSORS
/**
* Feature ID associated with the primitive; not required to be unique.
* @type {number}
*/
get featureId() {
return this._getUint32(BufferPrimitive.Layout.FEATURE_ID_U32);
}
set featureId(featureId) {
this._setUint32(BufferPrimitive.Layout.FEATURE_ID_U32, featureId);
}
/**
* Whether primitive is shown.
* @type {boolean}
*/
get show() {
return this._getUint8(BufferPrimitive.Layout.SHOW_U8) === 1;
}
set show(show) {
this._setUint8(BufferPrimitive.Layout.SHOW_U8, show ? 1 : 0);
}
/**
* @param {BufferPrimitiveMaterial} result
* @returns {BufferPrimitiveMaterial}
*/
getMaterial(result) {
const collection = this._collection;
const MaterialClass = collection._getMaterialClass();
return MaterialClass.unpack(
collection._materialView,
this._index * MaterialClass.packedLength,
result,
);
}
/**
* @param {BufferPrimitiveMaterial} material
*/
setMaterial(material) {
const collection = this._collection;
const MaterialClass = collection._getMaterialClass();
MaterialClass.pack(
material,
collection._materialView,
this._index * MaterialClass.packedLength,
);
this._dirty = true;
return material;
}
/**
* Whether the primitive requires an update on next render. Renderers should
* _not_ iterate over all primitives each frame, but must instead inspect
* only the dirty range of the parent collection. This flag is managed
* automatically, by primitive setters and collection renderers.
*
* @type {boolean}
* @ignore
*
* @see BufferPrimitiveCollection#_dirtyOffset
* @see BufferPrimitiveCollection#_dirtyCount
*/
get _dirty() {
return this._getUint8(BufferPrimitive.Layout.DIRTY_U8) === 1;
}
set _dirty(dirty) {
// Avoid `._setUint8()` here, which would infinitely loop `._dirty = true`.
this._collection._primitiveView.setUint8(
this._byteOffset + BufferPrimitive.Layout.DIRTY_U8,
dirty ? 1 : 0,
);
// A 'dirty' primitive is responsible for notifying the collection. Applying
// updates and marking the primitive 'clean' will be handled by the collection,
// so we don't notify the collection here in that case.
if (dirty) {
this._collection._makeDirty(this._index);
}
}
/**
* Pick ID (uint32) of primitive.
* @type {number}
* @ignore
*/
get _pickId() {
return this._getUint32(BufferPrimitive.Layout.PICK_ID_U32);
}
set _pickId(pickId) {
this._setUint32(BufferPrimitive.Layout.PICK_ID_U32, pickId);
}
/////////////////////////////////////////////////////////////////////////////
// BUFFER ACCESSORS
/**
* @param {number} itemByteOffset
* @returns {number}
* @ignore
*/
_getUint8(itemByteOffset) {
return this._collection._primitiveView.getUint8(
this._byteOffset + itemByteOffset,
);
}
/**
* @param {number} itemByteOffset
* @param {number} itemValue
* @ignore
*/
_setUint8(itemByteOffset, itemValue) {
this._collection._primitiveView.setUint8(
this._byteOffset + itemByteOffset,
itemValue,
);
this._dirty = true;
}
/**
* @param {number} itemByteOffset
* @returns {number}
* @ignore
*/
_getUint32(itemByteOffset) {
return this._collection._primitiveView.getUint32(
this._byteOffset + itemByteOffset,
true,
);
}
/**
* @param {number} itemByteOffset
* @param {number} itemValue
* @ignore
*/
_setUint32(itemByteOffset, itemValue) {
this._collection._primitiveView.setUint32(
this._byteOffset + itemByteOffset,
itemValue,
true,
);
this._dirty = true;
}
/**
* @param {number} itemByteOffset
* @returns {number}
* @ignore
*/
_getFloat32(itemByteOffset) {
return this._collection._primitiveView.getFloat32(
this._byteOffset + itemByteOffset,
true,
);
}
/**
* @param {number} itemByteOffset
* @param {number} itemValue
* @ignore
*/
_setFloat32(itemByteOffset, itemValue) {
this._collection._primitiveView.setFloat32(
this._byteOffset + itemByteOffset,
itemValue,
true,
);
this._dirty = true;
}
/////////////////////////////////////////////////////////////////////////////
// DEBUG
/**
* Returns a JSON-serializable object representing the primitive. This encoding
* is not memory-efficient, and should generally be used for debugging and
* testing.
*
* @returns {Object} JSON-serializable object.
*/
toJSON() {
const collection = this._collection;
const MaterialClass = collection._getMaterialClass();
return {
featureId: this.featureId,
show: this.show,
dirty: this._dirty,
material: this.getMaterial(new MaterialClass()).toJSON(),
};
}
}
export default BufferPrimitive;
+891
View File
@@ -0,0 +1,891 @@
// @ts-check
import BoundingSphere from "../Core/BoundingSphere.js";
import Cartesian3 from "../Core/Cartesian3.js";
import DeveloperError from "../Core/DeveloperError.js";
import Frozen from "../Core/Frozen.js";
import Matrix4 from "../Core/Matrix4.js";
import assert from "../Core/assert.js";
import ComponentDatatype from "../Core/ComponentDatatype.js";
import defined from "../Core/defined.js";
import Check from "../Core/Check.js";
import AttributeCompression from "../Core/AttributeCompression.js";
import SceneMode from "./SceneMode.js";
import AttributeType from "./AttributeType.js";
import oneTimeWarning from "../Core/oneTimeWarning.js";
import BlendOption from "../Scene/BlendOption.js";
/** @import { Destroyable, TypedArray, TypedArrayConstructor } from "../Core/globalTypes.js"; */
/** @import Context from "../Renderer/Context.js"; */
/** @import FrameState from "./FrameState.js"; */
/** @import BufferPrimitive from "./BufferPrimitive.js"; */
/** @import BufferPrimitiveMaterial from "./BufferPrimitiveMaterial.js"; */
/** @import PickId from "../Renderer/PickId.js"; */
/**
* @typedef {object} BufferPrimitiveOptions
* @property {Matrix4} [modelMatrix=Matrix4.IDENTITY] Transforms geometry from model to world coordinates.
* @property {boolean} [show=true]
* @property {BufferPrimitiveMaterial} [material]
* @property {number} [featureId]
* @property {object} [pickObject]
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
/**
* Collection of primitives held in ArrayBuffer storage for performance and memory optimization.
*
* <p>To get the full performance benefit of using a BufferPrimitiveCollection containing "N" primitives,
* be careful to avoid allocating "N" instances of any related JavaScript object. {@link BufferPrimitive},
* {@link Color}, {@link Cartesian3}, and other objects can all be reused when working with large collections,
* using the {@link https://en.wikipedia.org/wiki/Flyweight_pattern|flyweight pattern}.</p>
*
* @abstract
* @template T extends BufferPrimitive
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*
* @see BufferPrimitive
* @see BufferPrimitiveMaterial
* @see BufferPointCollection
* @see BufferPolylineCollection
* @see BufferPolygonCollection
*/
class BufferPrimitiveCollection {
/** @ignore */
static Error = {
ERR_RESIZE: "BufferPrimitive range cannot be resized after initialization.",
ERR_CAPACITY: "BufferPrimitiveCollection capacity exceeded.",
ERR_MULTIPLE_OF_FOUR:
"BufferPrimitive byte length must be a multiple of 4.",
ERR_OUT_OF_RANGE: "BufferPrimitive buffer access out of range.",
};
/**
* Resources managed by the collection's renderer. Collections may have multiple renderer
* implementations, so the collection should be ignorant of the renderer's implementation
* and context data. A collection only has one renderer active at a time.
*
* @type {Destroyable|null}
* @ignore
*/
_renderContext = null;
/**
* @param {object} options
* @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] Transforms geometry from model to world coordinates.
* @param {number} [options.primitiveCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY]
* @param {number} [options.vertexCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY]
* @param {boolean} [options.show=true]
* @param {ComponentDatatype} [options.positionDatatype=ComponentDatatype.DOUBLE]
* @param {boolean} [options.positionNormalized=false] When <code>true</code>, integer position values are treated as normalized,
* where the full integer range maps to [-1, 1] (signed) or [0, 1] (unsigned). Only relevant for integer position datatypes
* (BYTE, UNSIGNED_BYTE, SHORT, UNSIGNED_SHORT).
* @param {boolean} [options.allowPicking=false] When <code>true</code>, primitives are pickable with {@link Scene#pick}. When <code>false</code>, memory and initialization cost are lower.
* @param {BoundingSphere} [options.boundingVolume] Bounding volume, in world space, for the collection. When
* unspecified, a bounding volume is computed automatically and updated when primitive positions change. When
* specified, users are responsible for updating bounding volume as needed. Pre-computing the bounding volume
* manually, and updating it only as needed, will improve performance for larger dynamic collections.
* @param {boolean} [options.debugShowBoundingVolume=false]
* @param {BlendOption} [options.blendOption=BlendOption.TRANSLUCENT]
*/
constructor(options = Frozen.EMPTY_OBJECT) {
/**
* Determines if primitives in this collection will be shown.
* @type {boolean}
* @default true
*/
this.show = options.show ?? true;
/**
* Collection blend option; must be OPAQUE or TRANSLUCENT.
* @type {BlendOption}
* @readonly
* @ignore
*/
this._blendOption = options.blendOption ?? BlendOption.TRANSLUCENT;
/**
* Transforms geometry from model to world coordinates.
* @type {Matrix4}
* @default Matrix4.IDENTITY
* @readonly
* @protected
*/
this._modelMatrix = Matrix4.clone(options.modelMatrix ?? Matrix4.IDENTITY);
/**
* @type {BoundingSphere}
* @readonly
* @protected
*/
this._boundingVolume = BoundingSphere.clone(
options.boundingVolume ?? new BoundingSphere(),
new BoundingSphere(),
);
/**
* @type {boolean}
* @readonly
* @protected
*/
this._boundingVolumeAutoUpdate = !defined(options.boundingVolume);
/**
* When <code>true</code>, primitives are pickable with {@link Scene#pick}.
* When <code>false</code>, memory and initialization cost are lower.
* @type {boolean}
* @readonly
* @ignore
* @default false
*/
this._allowPicking = options.allowPicking ?? false;
/**
* @type {Map<Context, PickId[]>}
* @readonly
* @ignore
*/
this._pickIds = new Map();
/**
* @type {object[]}
* @readonly
* @ignore
*/
this._pickObjects = [];
/**
* This property is for debugging only; it is not for production use nor is it optimized.
* <p>
* Draws the bounding sphere for each draw command in the primitive.
* </p>
*
* @type {boolean}
* @default false
*/
this.debugShowBoundingVolume = options.debugShowBoundingVolume ?? false;
/**
* @type {number}
* @protected
* @ignore
*/
this._primitiveCount = 0;
/**
* @type {number}
* @protected
* @ignore
*/
this._primitiveCountMax =
options.primitiveCountMax ?? BufferPrimitiveCollection.DEFAULT_CAPACITY;
/**
* @type {DataView<ArrayBuffer>}
* @ignore
*/
this._primitiveView = null;
/**
* @type {number}
* @ignore
*/
this._positionCount = 0;
/**
* @type {number}
* @ignore
*/
this._positionCountMax =
options.vertexCountMax ?? BufferPrimitiveCollection.DEFAULT_CAPACITY;
/**
* @type {TypedArray}
* @ignore
*/
this._positionView = null;
/**
* @type {ComponentDatatype}
* @ignore
*/
this._positionDatatype =
options.positionDatatype ?? ComponentDatatype.DOUBLE;
/**
* When <code>true</code>, integer position values represent normalized floats
* in [-1, 1] (signed) or [0, 1] (unsigned). Only applicable to integer datatypes.
* @type {boolean}
* @ignore
*/
this._positionNormalized = options.positionNormalized ?? false;
/**
* @type {DataView<ArrayBuffer>}
* @ignore
*/
this._materialView = null;
// Potentially-dirty primitives are tracked as a contiguous range, with
// 'clean' primitives potentially within the range. Individual primitive
// 'dirty' flags are source-of-truth.
/**
* @type {number}
* @ignore
*/
this._dirtyOffset = 0;
/**
* @type {number}
* @ignore
*/
this._dirtyCount = 0;
/**
* @type {boolean}
* @ignore
*/
this._dirtyBoundingVolume = false;
/**
* Monotonically increasing counter, bumped each time collection is marked "clean".
* @type {number}
* @ignore
*/
this._version = 0;
this._allocatePrimitiveBuffer();
this._allocatePositionBuffer();
this._allocateMaterialBuffer();
}
/**
* Accessing `this.constructor` can cause JSDoc builds to fail, so use this
* protected getter function instead.
* @protected
* @return {*}
* @ignore
*/
_getCollectionClass() {
DeveloperError.throwInstantiationError();
}
/**
* @protected
* @return {*}
* @ignore
*/
_getPrimitiveClass() {
DeveloperError.throwInstantiationError();
}
/**
* @return {*}
* @ignore
*/
_getMaterialClass() {
DeveloperError.throwInstantiationError();
}
/////////////////////////////////////////////////////////////////////////////
// COLLECTION LIFECYCLE
/**
* @private
* @ignore
*/
_allocatePrimitiveBuffer() {
const layout = this._getPrimitiveClass().Layout;
//>>includeStart('debug', pragmas.debug);
const { ERR_MULTIPLE_OF_FOUR } = BufferPrimitiveCollection.Error;
assert(layout.__BYTE_LENGTH % 4 === 0, ERR_MULTIPLE_OF_FOUR);
//>>includeEnd('debug');
this._primitiveView = new DataView(
new ArrayBuffer(this._primitiveCountMax * layout.__BYTE_LENGTH),
);
}
/**
* @private
* @ignore
*/
_allocatePositionBuffer() {
// @ts-expect-error https://github.com/CesiumGS/cesium/issues/13420
this._positionView = ComponentDatatype.createTypedArray(
this._positionDatatype,
this._positionCountMax * 3,
);
}
/**
* @private
* @ignore
*/
_allocateMaterialBuffer() {
const MaterialClass = this._getMaterialClass();
this._materialView = new DataView(
new ArrayBuffer(this._primitiveCountMax * MaterialClass.packedLength),
);
}
/**
* Returns true if this object was destroyed; otherwise, false.
*
* @returns {boolean} True if this object was destroyed; otherwise, false.
*/
isDestroyed() {
return false;
}
/** Destroys collection and its GPU resources. */
destroy() {
this._pickObjects.length = 0;
for (const contextPickIds of this._pickIds.values()) {
for (const pickId of contextPickIds) {
pickId.destroy();
}
}
if (defined(this._renderContext)) {
this._renderContext.destroy();
this._renderContext = undefined;
this._dirtyOffset = 0;
this._dirtyCount = this.primitiveCount;
}
}
/**
* Sorts primitives of the collection.
*
* Because sorting changes the indices (but not the feature IDs) of primitives
* in the collection, the function also returns an array mapping from previous
* index to new index. When sorting repeatedly, the array can be reused and
* passed as the 'result' argument for each call.
*
* @param {Function} sortFn
* @param {Uint32Array} result
* @returns {Uint32Array} Mapping from previous index to new index.
*/
sort(sortFn, result = new Uint32Array(this.primitiveCount)) {
const PrimitiveClass = this._getPrimitiveClass();
const CollectionClass = this._getCollectionClass();
const { primitiveCount } = this;
const a = new PrimitiveClass();
const b = new PrimitiveClass();
// Mapping from NEW index to PREVIOUS index.
const dstSrcMap = new Uint32Array(primitiveCount);
for (let i = 0; i < primitiveCount; i++) {
dstSrcMap[i] = i;
}
dstSrcMap.sort((indexA, indexB) =>
sortFn(this.get(indexA, a), this.get(indexB, b)),
);
// Mapping from PREVIOUS index to NEW index.
for (let i = 0; i < primitiveCount; i++) {
result[dstSrcMap[i]] = i;
}
// Copy primitives to temporary collection, in sort order.
const tmp = CollectionClass._cloneEmpty(this);
for (let i = 0; i < primitiveCount; i++) {
const src = this.get(dstSrcMap[i], a);
const dst = tmp.add({}, b);
PrimitiveClass.clone(src, dst);
}
// Assign buffers from temporary collection onto this one.
CollectionClass._replaceBuffers(tmp, this);
this._dirtyOffset = 0;
this._dirtyCount = primitiveCount;
return result;
}
/**
* Duplicates the contents of this collection into the result collection.
* Result collection is not resized, and must contain enough space for all
* primitives in the source collection. Existing primitives in the result
* collection will be overwritten.
*
* <p>Useful when allocating more space for a collection that has reached its
* capacity, and efficiently transferring features to the new collection.</p>
*
* @example
* const result = new BufferPrimitiveCollection({ ... }); // allocate larger 'result' collection
* BufferPrimitiveCollection.clone(collection, result); // copy primitives from 'collection' into 'result'
*
* @param {BufferPrimitiveCollection<T>} collection
* @param {BufferPrimitiveCollection<T>} result
* @template T extends BufferPrimitive
*/
static clone(collection, result) {
//>>includeStart('debug', pragmas.debug);
const { ERR_CAPACITY } = BufferPrimitiveCollection.Error;
assert(collection.primitiveCount <= result.primitiveCountMax, ERR_CAPACITY);
assert(collection.vertexCount <= result.vertexCountMax, ERR_CAPACITY);
//>>includeEnd('debug');
const layout = collection._getPrimitiveClass().Layout;
const MaterialClass = collection._getMaterialClass();
const PrimitiveClass = collection._getPrimitiveClass();
this._copySubDataView(
collection._primitiveView,
result._primitiveView,
collection.primitiveCount * layout.__BYTE_LENGTH,
);
this._copySubArray(
collection._positionView,
result._positionView,
collection.vertexCount * 3,
);
this._copySubDataView(
collection._materialView,
result._materialView,
collection.primitiveCount * MaterialClass.packedLength,
);
result.show = collection.show;
result.debugShowBoundingVolume = collection.debugShowBoundingVolume;
result._primitiveCount = collection._primitiveCount;
result._positionCount = collection._positionCount;
// Unset PickIds.
const primitive = new PrimitiveClass();
for (let i = 0, il = result.primitiveCount; i < il; i++) {
result.get(i, primitive)._pickId = 0;
}
result._dirtyOffset = 0;
result._dirtyCount = result.primitiveCount;
collection.boundingVolume.clone(result.boundingVolume);
return result;
}
/**
* Returns an empty collection with the same buffer sizes as this collection.
* Internal utility for operations requiring a working copy of memory.
*
* @param {BufferPrimitiveCollection<T>} collection
* @returns {BufferPrimitiveCollection<T>}
* @template T extends BufferPrimitive
* @protected
* @abstract
* @ignore
*/
static _cloneEmpty(collection) {
DeveloperError.throwInstantiationError();
}
/**
* Assigns buffers from source collection to target collection, without
* validation or side effects. Callers must handle any validation, dirty
* flag updates, etc.
*
* @param {BufferPrimitiveCollection<T>} src
* @param {BufferPrimitiveCollection<T>} dst
* @template T extends BufferPrimitive
* @protected
* @ignore
*/
static _replaceBuffers(src, dst) {
dst._primitiveView = src._primitiveView;
dst._positionView = src._positionView;
dst._materialView = src._materialView;
}
/**
* Rebuilds collection bounding volume.
* @protected
* @ignore
*/
_updateBoundingVolume() {
// Exclude unused space in the position buffer.
let vertices = this._positionView.subarray(0, this._positionCount * 3);
if (this._positionNormalized) {
vertices = AttributeCompression.dequantize(
/** @type {Int8Array|Uint8Array|Int16Array|Uint16Array|Int32Array|Uint32Array} */ (
vertices
),
this._positionDatatype,
AttributeType.VEC3,
this._positionCount,
);
}
BoundingSphere.fromVertices(
vertices,
Cartesian3.ZERO,
3,
this._boundingVolume,
);
BoundingSphere.transform(
this._boundingVolume,
this._modelMatrix,
this._boundingVolume,
);
this._dirtyBoundingVolume = false;
}
/**
* Updates PickIds for the given context.
* @param {Context} context
* @protected
* @ignore
*/
_updatePickIds(context) {
let pickIds = this._pickIds.get(context);
if (pickIds && pickIds.length === this._primitiveCount) {
return;
}
if (!pickIds) {
pickIds = [];
this._pickIds.set(context, pickIds);
}
const collection = this;
const PrimitiveClass = this._getPrimitiveClass();
const primitive = new PrimitiveClass();
// Fill in missing PickIDs for recently-added primitives.
for (let i = pickIds.length, il = this._primitiveCount; i < il; i++) {
this.get(i, primitive);
const pickObject = this._pickObjects[i] || {
collection: this,
index: i,
get primitive() {
// Cannot reuse primitives; scene.drillPick() appends to a list.
return collection.get(i, new PrimitiveClass());
},
};
const pickId = context.createPickId(pickObject);
primitive._pickId = pickId.key;
pickIds.push(pickId);
}
}
/////////////////////////////////////////////////////////////////////////////
// PRIMITIVE LIFECYCLE
/**
* Makes the given {@link BufferPrimitive} a view onto this collection's
* primitive at the given index, for use when reading/writing primitive
* properties. When iterating over a large collection, prefer to reuse
* the same BufferPrimitive instance throughout the loop — rebinding
* an existing instance to a different primitive is cheap, and avoids
* allocating in-memory objects for every object.
*
* @example
* const primitive = new BufferPrimitive();
* for (let i = 0; i < collection.primitiveCount; i++) {
* collection.get(i, primitive);
* primitive.setColor(Color.RED);
* }
*
* @param {number} index
* @param {BufferPrimitive} result
* @returns {BufferPrimitive} The BufferPrimitive instance passed as the
* 'result' argument, now bound to the specified primitive index.
*/
get(index, result) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number.greaterThanOrEquals("index", index, 0);
Check.typeOf.number.lessThan("index", index, this._primitiveCount);
//>>includeEnd('debug');
result._collection = this;
result._index = index;
result._byteOffset = index * this._getPrimitiveClass().Layout.__BYTE_LENGTH;
return result;
}
/**
* Adds a new primitive to the collection, with the specified options. A
* {@link BufferPrimitive} instance is linked to the new primitive, using
* the 'result' argument if given, or a new instance if not. For repeated
* calls, prefer to reuse a single BufferPrimitive instance rather than
* allocating a new instance on each call.
*
* @param {BufferPrimitiveOptions} options
* @param {BufferPrimitive} result
* @returns {BufferPrimitive}
*/
add(options = Frozen.EMPTY_OBJECT, result) {
//>>includeStart('debug', pragmas.debug);
const { ERR_CAPACITY } = BufferPrimitiveCollection.Error;
assert(this.primitiveCount < this.primitiveCountMax, ERR_CAPACITY);
//>>includeEnd('debug');
const MaterialClass = this._getMaterialClass();
const index = this._primitiveCount++;
result = this.get(index, result);
result.featureId = options.featureId ?? index;
result.show = options.show ?? true;
result.setMaterial(options.material ?? MaterialClass.DEFAULT_MATERIAL);
result._pickId = 0; // unset
result._dirty = true;
if (defined(options.pickObject)) {
this._pickObjects[index] = options.pickObject;
}
return result;
}
/**
* Marks primitive at given index as 'dirty', to be updated on next render.
* @param {number} index
* @ignore
*/
_makeDirty(index) {
if (this._dirtyCount === 0) {
this._dirtyCount = 1;
this._dirtyOffset = index;
} else if (index < this._dirtyOffset) {
this._dirtyCount += this._dirtyOffset - index;
this._dirtyOffset = index;
} else if (index + 1 > this._dirtyOffset + this._dirtyCount) {
this._dirtyCount = index + 1 - this._dirtyOffset;
}
}
/**
* Marks all primitives 'clean', and updates version counter.
* @ignore
*/
_makeClean() {
if (this._dirtyCount > 0) {
this._dirtyCount = 0;
this._dirtyOffset = 0;
this._version++;
}
}
/**
* Marks collection bounding volume as 'dirty', to be updated on next render,
* if automatic bounding volume updates are enabled.
* @ignore
*/
_makeDirtyBoundingVolume() {
if (this._boundingVolumeAutoUpdate) {
this._dirtyBoundingVolume = true;
}
}
/////////////////////////////////////////////////////////////////////////////
// RENDER
/** @param {object} frameState */
update(frameState) {
if (/** @type {FrameState} */ (frameState).mode !== SceneMode.SCENE3D) {
oneTimeWarning(
"bufferprim-scenemode",
"BufferPrimitiveCollection requires SceneMode.SCENE3D.",
);
}
if (this._dirtyBoundingVolume) {
this._updateBoundingVolume();
}
if (this._allowPicking && this._dirtyCount > 0) {
this._updatePickIds(/** @type {FrameState} */ (frameState).context);
}
}
/////////////////////////////////////////////////////////////////////////////
// ACCESSORS
/**
* Number of primitives in collection. Must be <= {@link primitiveCountMax}.
*
* @type {number}
* @readonly
*/
get primitiveCount() {
return this._primitiveCount;
}
/**
* Maximum number of primitives this collection can contain. Must be >=
* {@link primitiveCount}.
*
* @type {number}
* @readonly
* @default {@link BufferPrimitiveCollection.DEFAULT_CAPACITY}
*/
get primitiveCountMax() {
return this._primitiveCountMax;
}
/**
* Total byte length of buffers owned by this collection. Includes any unused
* space allocated by {@link primitiveCountMax}, even if no primitives have
* yet been added in that space.
*
* @type {number}
* @readonly
*/
get byteLength() {
return (
this._primitiveView.byteLength +
this._positionView.byteLength +
this._materialView.byteLength
);
}
/**
* Number of vertices in collection. Must be <= {@link vertexCountMax}.
*
* @type {number}
* @readonly
*/
get vertexCount() {
return this._positionCount;
}
/**
* Maximum number of vertices this collection can contain. Must be >=
* {@link vertexCount}.
*
* @type {number}
* @readonly
* @default {@link BufferPrimitiveCollection.DEFAULT_CAPACITY}
*/
get vertexCountMax() {
return this._positionCountMax;
}
/**
* Transforms geometry from model to world coordinates.
* @type {Matrix4}
* @default Matrix4.IDENTITY
* @readonly
*/
get modelMatrix() {
return this._modelMatrix;
}
/**
* World-space bounding volume for all primitives in the collection, including both
* shown and hidden primitives.
* @type {BoundingSphere}
* @readonly
*/
get boundingVolume() {
if (this._dirtyBoundingVolume) {
this._updateBoundingVolume();
}
return this._boundingVolume;
}
/**
* The component datatype used to store position values.
* @type {ComponentDatatype}
* @readonly
*/
get positionDatatype() {
return this._positionDatatype;
}
/**
* When <code>true</code>, integer position values are treated as normalized
* values, where the full integer range maps to [-1, 1] (signed) or [0, 1]
* (unsigned).
* @type {boolean}
* @readonly
*/
get positionNormalized() {
return this._positionNormalized;
}
/////////////////////////////////////////////////////////////////////////////
// UTILS
/**
* @param {TypedArray} src
* @param {TypedArray} dst
* @param {number} count
* @protected
* @ignore
*/
static _copySubArray(src, dst, count) {
for (let i = 0; i < count; i++) {
dst[i] = src[i];
}
}
/**
* @param {DataView} src
* @param {DataView} dst
* @param {number} byteLength
* @protected
* @ignore
*/
static _copySubDataView(src, dst, byteLength) {
// No need to match the original array type, just copy in 4-byte chunks.
this._copySubArray(
new Uint32Array(src.buffer, src.byteOffset, src.byteLength / 4),
new Uint32Array(dst.buffer, dst.byteOffset, dst.byteLength / 4),
byteLength / 4,
);
}
/////////////////////////////////////////////////////////////////////////////
// DEBUG
/**
* Returns a JSON-serializable array representing the collection. This encoding
* is not memory-efficient, and should generally be used for debugging and
* testing.
*
* @example
* console.table(collection.toJSON());
*
* @returns {Array<Object>} List of JSON-serializable objects, one for each
* primitive in the collection.
*/
toJSON() {
const PrimitiveClass = this._getPrimitiveClass();
const primitive = new PrimitiveClass();
const results = [];
for (let i = 0, il = this.primitiveCount; i < il; i++) {
results.push(this.get(i, primitive).toJSON());
}
return results;
}
}
/**
* Default capacity of buffers on new collections. A quantity of elements:
* number of vertices in the vertex buffer, primitives in the primitive
* buffer, etc. This value is arbitrary, and collections cannot be resized,
* so specific per-buffer capacities should be provided in the collection
* constructor when available.
*
* @type {number}
* @static
* @constant
*/
BufferPrimitiveCollection.DEFAULT_CAPACITY = 1024;
export default BufferPrimitiveCollection;
+140
View File
@@ -0,0 +1,140 @@
// @ts-check
import Color from "../Core/Color.js";
import Frozen from "../Core/Frozen.js";
/** @import Packable from "../Core/Packable.js"; */
/** @import BufferPrimitive from "./BufferPrimitive.js"; */
/**
* @typedef {object} BufferPrimitiveMaterialOptions
* @property {Color} [color=Color.WHITE] Color of fill.
* @property {Color} [outlineColor=Color.WHITE] Color of outline.
* @property {number} [outlineWidth=0.0] Width of outline, 0-255px.
*/
/**
* Material description for a {@link BufferPrimitive}. Abstract.
*
* <p>BufferPrimitiveMaterial objects are {@link Packable|packable}, stored
* when calling {@link BufferPrimitive#setMaterial}. Subsequent changes to the
* material will not affect the primitive until setMaterial() is called again.</p>
*
* @see BufferPointMaterial
* @see BufferPolylineMaterial
* @see BufferPolygonMaterial
* @see Packable
*
* @abstract
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
class BufferPrimitiveMaterial {
/** @ignore */
static Layout = {
COLOR_U32: 0,
OUTLINE_COLOR_U32: 4,
OUTLINE_WIDTH_U8: 8,
__BYTE_LENGTH: 12,
};
/**
* @type {BufferPrimitiveMaterial}
* @ignore
*/
static DEFAULT_MATERIAL;
/**
* @param {BufferPrimitiveMaterialOptions} [options]
*/
constructor(options = Frozen.EMPTY_OBJECT) {
/**
* Color of fill.
* @type {Color}
*/
this.color = Color.clone(options.color ?? Color.WHITE);
/**
* Color of outline.
* @type {Color}
*/
this.outlineColor = Color.clone(options.outlineColor ?? Color.WHITE);
/**
* Width of outline, 0-255px.
* @type {number}
*/
this.outlineWidth = options.outlineWidth ?? 0;
}
/** @type {number} */
static get packedLength() {
return this.Layout.__BYTE_LENGTH;
}
/**
* Stores the provided material into the provided array.
*
* @param {BufferPrimitiveMaterial} material
* @param {DataView} view
* @param {number} byteOffset
*/
static pack(material, view, byteOffset) {
view.setUint32(
this.Layout.COLOR_U32 + byteOffset,
material.color.toRgba(),
true,
);
view.setUint32(
this.Layout.OUTLINE_COLOR_U32 + byteOffset,
material.outlineColor.toRgba(),
true,
);
view.setUint8(
this.Layout.OUTLINE_WIDTH_U8 + byteOffset,
material.outlineWidth,
);
}
/**
* Retrieves a material from a packed array.
*
* @param {DataView} view The packed array.
* @param {number} byteOffset Starting index of the element to be unpacked.
* @param {BufferPrimitiveMaterial} result Material into which results are unpacked.
* @returns {BufferPrimitiveMaterial} Modified result material, with results unpacked.
*/
static unpack(view, byteOffset, result) {
Color.fromRgba(
view.getUint32(this.Layout.COLOR_U32 + byteOffset, true),
result.color,
);
Color.fromRgba(
view.getUint32(this.Layout.OUTLINE_COLOR_U32 + byteOffset, true),
result.outlineColor,
);
result.outlineWidth = view.getUint8(
this.Layout.OUTLINE_WIDTH_U8 + byteOffset,
);
return result;
}
/////////////////////////////////////////////////////////////////////////////
// DEBUG
/**
* Returns a JSON-serializable object representing the material. This encoding
* is not memory-efficient, and should generally be used for debugging and
* testing.
*
* @returns {Object} JSON-serializable object.
*/
toJSON() {
return {
color: this.color.toCssHexString(),
outlineColor: this.outlineColor.toCssHexString(),
outlineWidth: this.outlineWidth,
};
}
}
export default BufferPrimitiveMaterial;
File diff suppressed because it is too large Load Diff
+631
View File
@@ -0,0 +1,631 @@
import Cartesian2 from "../Core/Cartesian2.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
import KeyboardEventModifier from "../Core/KeyboardEventModifier.js";
import CesiumMath from "../Core/Math.js";
import ScreenSpaceEventHandler from "../Core/ScreenSpaceEventHandler.js";
import ScreenSpaceEventType from "../Core/ScreenSpaceEventType.js";
import CameraEventType from "./CameraEventType.js";
const keyboardModifierCombinations = [
KeyboardEventModifier.SHIFT,
KeyboardEventModifier.CTRL,
KeyboardEventModifier.ALT,
[KeyboardEventModifier.SHIFT, KeyboardEventModifier.CTRL],
[KeyboardEventModifier.SHIFT, KeyboardEventModifier.ALT],
[KeyboardEventModifier.CTRL, KeyboardEventModifier.ALT],
[
KeyboardEventModifier.SHIFT,
KeyboardEventModifier.CTRL,
KeyboardEventModifier.ALT,
],
];
function getKey(type, modifiers) {
if (!defined(modifiers)) {
return `${type}`;
}
const modifierList = Array.isArray(modifiers)
? modifiers.toSorted()
: [modifiers];
return `${type}+${modifierList.join("+")}`;
}
function clonePinchMovement(pinchMovement, result) {
Cartesian2.clone(
pinchMovement.distance.startPosition,
result.distance.startPosition,
);
Cartesian2.clone(
pinchMovement.distance.endPosition,
result.distance.endPosition,
);
Cartesian2.clone(
pinchMovement.angleAndHeight.startPosition,
result.angleAndHeight.startPosition,
);
Cartesian2.clone(
pinchMovement.angleAndHeight.endPosition,
result.angleAndHeight.endPosition,
);
}
function listenToPinch(aggregator, modifier, canvas) {
const key = getKey(CameraEventType.PINCH, modifier);
const update = aggregator._update;
const isDown = aggregator._isDown;
const eventStartPosition = aggregator._eventStartPosition;
const pressTime = aggregator._pressTime;
const releaseTime = aggregator._releaseTime;
update[key] = true;
isDown[key] = false;
eventStartPosition[key] = new Cartesian2();
let movement = aggregator._movement[key];
if (!defined(movement)) {
movement = aggregator._movement[key] = {};
}
movement.distance = {
startPosition: new Cartesian2(),
endPosition: new Cartesian2(),
};
movement.angleAndHeight = {
startPosition: new Cartesian2(),
endPosition: new Cartesian2(),
};
movement.prevAngle = 0.0;
aggregator._eventHandler.setInputAction(
function (event) {
aggregator._buttonsDown++;
isDown[key] = true;
pressTime[key] = new Date();
// Compute center position and store as start point.
Cartesian2.lerp(
event.position1,
event.position2,
0.5,
eventStartPosition[key],
);
},
ScreenSpaceEventType.PINCH_START,
modifier,
);
aggregator._eventHandler.setInputAction(
function () {
aggregator._buttonsDown = Math.max(aggregator._buttonsDown - 1, 0);
isDown[key] = false;
releaseTime[key] = new Date();
},
ScreenSpaceEventType.PINCH_END,
modifier,
);
aggregator._eventHandler.setInputAction(
function (mouseMovement) {
if (isDown[key]) {
// Aggregate several input events into a single animation frame.
if (!update[key]) {
Cartesian2.clone(
mouseMovement.distance.endPosition,
movement.distance.endPosition,
);
Cartesian2.clone(
mouseMovement.angleAndHeight.endPosition,
movement.angleAndHeight.endPosition,
);
} else {
clonePinchMovement(mouseMovement, movement);
update[key] = false;
movement.prevAngle = movement.angleAndHeight.startPosition.x;
}
// Make sure our aggregation of angles does not "flip" over 360 degrees.
let angle = movement.angleAndHeight.endPosition.x;
const prevAngle = movement.prevAngle;
const TwoPI = Math.PI * 2;
while (angle >= prevAngle + Math.PI) {
angle -= TwoPI;
}
while (angle < prevAngle - Math.PI) {
angle += TwoPI;
}
movement.angleAndHeight.endPosition.x =
(-angle * canvas.clientWidth) / 12;
movement.angleAndHeight.startPosition.x =
(-prevAngle * canvas.clientWidth) / 12;
}
},
ScreenSpaceEventType.PINCH_MOVE,
modifier,
);
}
function listenToWheel(aggregator, modifier) {
const key = getKey(CameraEventType.WHEEL, modifier);
const pressTime = aggregator._pressTime;
const releaseTime = aggregator._releaseTime;
const update = aggregator._update;
update[key] = true;
let movement = aggregator._movement[key];
if (!defined(movement)) {
movement = aggregator._movement[key] = {};
}
let lastMovement = aggregator._lastMovement[key];
if (!defined(lastMovement)) {
lastMovement = aggregator._lastMovement[key] = {
startPosition: new Cartesian2(),
endPosition: new Cartesian2(),
valid: false,
};
}
movement.startPosition = new Cartesian2();
Cartesian2.clone(Cartesian2.ZERO, movement.startPosition);
movement.endPosition = new Cartesian2();
aggregator._eventHandler.setInputAction(
function (delta) {
const arcLength = 7.5 * CesiumMath.toRadians(delta);
pressTime[key] = releaseTime[key] = new Date();
movement.endPosition.x = 0.0;
movement.endPosition.y = arcLength;
Cartesian2.clone(movement.endPosition, lastMovement.endPosition);
lastMovement.valid = true;
update[key] = false;
},
ScreenSpaceEventType.WHEEL,
modifier,
);
}
function listenMouseButtonDownUp(aggregator, modifier, type) {
const key = getKey(type, modifier);
const isDown = aggregator._isDown;
const eventStartPosition = aggregator._eventStartPosition;
const pressTime = aggregator._pressTime;
isDown[key] = false;
eventStartPosition[key] = new Cartesian2();
let lastMovement = aggregator._lastMovement[key];
if (!defined(lastMovement)) {
lastMovement = aggregator._lastMovement[key] = {
startPosition: new Cartesian2(),
endPosition: new Cartesian2(),
valid: false,
};
}
let down;
let up;
if (type === CameraEventType.LEFT_DRAG) {
down = ScreenSpaceEventType.LEFT_DOWN;
up = ScreenSpaceEventType.LEFT_UP;
} else if (type === CameraEventType.RIGHT_DRAG) {
down = ScreenSpaceEventType.RIGHT_DOWN;
up = ScreenSpaceEventType.RIGHT_UP;
} else if (type === CameraEventType.MIDDLE_DRAG) {
down = ScreenSpaceEventType.MIDDLE_DOWN;
up = ScreenSpaceEventType.MIDDLE_UP;
}
aggregator._eventHandler.setInputAction(
function (event) {
aggregator._buttonsDown++;
lastMovement.valid = false;
isDown[key] = true;
pressTime[key] = new Date();
Cartesian2.clone(event.position, eventStartPosition[key]);
},
down,
modifier,
);
aggregator._eventHandler.setInputAction(
function () {
cancelMouseDownAction(getKey(type, undefined), aggregator);
for (const modifier of keyboardModifierCombinations) {
const cancelKey = getKey(type, modifier);
cancelMouseDownAction(cancelKey, aggregator);
}
},
up,
modifier,
);
}
function cancelMouseDownAction(cancelKey, aggregator) {
const releaseTime = aggregator._releaseTime;
const isDown = aggregator._isDown;
if (isDown[cancelKey]) {
aggregator._buttonsDown = Math.max(aggregator._buttonsDown - 1, 0);
}
isDown[cancelKey] = false;
releaseTime[cancelKey] = new Date();
}
function cloneMouseMovement(mouseMovement, result) {
Cartesian2.clone(mouseMovement.startPosition, result.startPosition);
Cartesian2.clone(mouseMovement.endPosition, result.endPosition);
}
function refreshMouseDownStatus(type, modifier, aggregator) {
// first: Judge if the mouse is pressed
const isDown = aggregator._isDown;
let anyButtonIsDown = false;
const currentKey = getKey(type, modifier);
for (const [downKey, downValue] of Object.entries(isDown)) {
if (downKey.startsWith(type) && downValue && downKey !== currentKey) {
anyButtonIsDown = true;
cancelMouseDownAction(downKey, aggregator);
}
}
if (!anyButtonIsDown) {
return;
}
// second: If it is pressed, it will be transferred to the current modifier.
const pressTime = aggregator._pressTime;
let lastMovement = aggregator._lastMovement[currentKey];
if (!defined(lastMovement)) {
lastMovement = aggregator._lastMovement[currentKey] = {
startPosition: new Cartesian2(),
endPosition: new Cartesian2(),
valid: false,
};
}
aggregator._buttonsDown++;
lastMovement.valid = false;
isDown[currentKey] = true;
pressTime[currentKey] = new Date();
}
function listenMouseMove(aggregator, modifier) {
const update = aggregator._update;
const movement = aggregator._movement;
const lastMovement = aggregator._lastMovement;
const isDown = aggregator._isDown;
for (const typeName in CameraEventType) {
if (CameraEventType.hasOwnProperty(typeName)) {
const type = CameraEventType[typeName];
if (defined(type)) {
const key = getKey(type, modifier);
update[key] = true;
if (!defined(aggregator._lastMovement[key])) {
aggregator._lastMovement[key] = {
startPosition: new Cartesian2(),
endPosition: new Cartesian2(),
valid: false,
};
}
if (!defined(aggregator._movement[key])) {
aggregator._movement[key] = {
startPosition: new Cartesian2(),
endPosition: new Cartesian2(),
};
}
}
}
}
aggregator._eventHandler.setInputAction(
function (mouseMovement) {
for (const typeName in CameraEventType) {
if (CameraEventType.hasOwnProperty(typeName)) {
const type = CameraEventType[typeName];
if (defined(type)) {
const key = getKey(type, modifier);
refreshMouseDownStatus(type, modifier, aggregator);
if (isDown[key]) {
if (!update[key]) {
Cartesian2.clone(
mouseMovement.endPosition,
movement[key].endPosition,
);
} else {
cloneMouseMovement(movement[key], lastMovement[key]);
lastMovement[key].valid = true;
cloneMouseMovement(mouseMovement, movement[key]);
update[key] = false;
}
}
}
}
}
Cartesian2.clone(
mouseMovement.endPosition,
aggregator._currentMousePosition,
);
},
ScreenSpaceEventType.MOUSE_MOVE,
modifier,
);
}
/**
* Aggregates input events. For example, suppose the following inputs are received between frames:
* left mouse button down, mouse move, mouse move, left mouse button up. These events will be aggregated into
* one event with a start and end position of the mouse.
*
* @alias CameraEventAggregator
* @constructor
*
* @param {HTMLCanvasElement} [canvas=document] The element to handle events for.
*
* @see ScreenSpaceEventHandler
*/
function CameraEventAggregator(canvas) {
//>>includeStart('debug', pragmas.debug);
if (!defined(canvas)) {
throw new DeveloperError("canvas is required.");
}
//>>includeEnd('debug');
this._eventHandler = new ScreenSpaceEventHandler(canvas);
this._update = {};
this._movement = {};
this._lastMovement = {};
this._isDown = {};
this._eventStartPosition = {};
this._pressTime = {};
this._releaseTime = {};
this._buttonsDown = 0;
this._currentMousePosition = new Cartesian2();
listenToWheel(this, undefined);
listenToPinch(this, undefined, canvas);
listenMouseButtonDownUp(this, undefined, CameraEventType.LEFT_DRAG);
listenMouseButtonDownUp(this, undefined, CameraEventType.RIGHT_DRAG);
listenMouseButtonDownUp(this, undefined, CameraEventType.MIDDLE_DRAG);
listenMouseMove(this, undefined);
for (const modifiers of keyboardModifierCombinations) {
listenToWheel(this, modifiers);
listenToPinch(this, modifiers, canvas);
listenMouseButtonDownUp(this, modifiers, CameraEventType.LEFT_DRAG);
listenMouseButtonDownUp(this, modifiers, CameraEventType.RIGHT_DRAG);
listenMouseButtonDownUp(this, modifiers, CameraEventType.MIDDLE_DRAG);
listenMouseMove(this, modifiers);
}
}
Object.defineProperties(CameraEventAggregator.prototype, {
/**
* Gets the current mouse position.
* @memberof CameraEventAggregator.prototype
* @type {Cartesian2}
*/
currentMousePosition: {
get: function () {
return this._currentMousePosition;
},
},
/**
* Gets whether any mouse button is down, a touch has started, or the wheel has been moved.
* @memberof CameraEventAggregator.prototype
* @type {boolean}
*/
anyButtonDown: {
get: function () {
const wheelMoved =
!this._update[getKey(CameraEventType.WHEEL)] ||
!this._update[
getKey(CameraEventType.WHEEL, KeyboardEventModifier.SHIFT)
] ||
!this._update[
getKey(CameraEventType.WHEEL, KeyboardEventModifier.CTRL)
] ||
!this._update[getKey(CameraEventType.WHEEL, KeyboardEventModifier.ALT)];
return this._buttonsDown > 0 || wheelMoved;
},
},
});
/**
* Gets if a mouse button down or touch has started and has been moved.
*
* @param {CameraEventType} type The camera event type.
* @param {KeyboardEventModifier} [modifier] The keyboard modifier.
* @returns {boolean} Returns <code>true</code> if a mouse button down or touch has started and has been moved; otherwise, <code>false</code>
*/
CameraEventAggregator.prototype.isMoving = function (type, modifier) {
//>>includeStart('debug', pragmas.debug);
if (!defined(type)) {
throw new DeveloperError("type is required.");
}
//>>includeEnd('debug');
const key = getKey(type, modifier);
return !this._update[key];
};
/**
* Gets the aggregated start and end position of the current event.
*
* @param {CameraEventType} type The camera event type.
* @param {KeyboardEventModifier} [modifier] The keyboard modifier.
* @returns {object} An object with two {@link Cartesian2} properties: <code>startPosition</code> and <code>endPosition</code>.
*/
CameraEventAggregator.prototype.getMovement = function (type, modifier) {
//>>includeStart('debug', pragmas.debug);
if (!defined(type)) {
throw new DeveloperError("type is required.");
}
//>>includeEnd('debug');
const key = getKey(type, modifier);
const movement = this._movement[key];
return movement;
};
/**
* Gets the start and end position of the last move event (not the aggregated event).
*
* @param {CameraEventType} type The camera event type.
* @param {KeyboardEventModifier} [modifier] The keyboard modifier.
* @returns {object|undefined} An object with two {@link Cartesian2} properties: <code>startPosition</code> and <code>endPosition</code> or <code>undefined</code>.
*/
CameraEventAggregator.prototype.getLastMovement = function (type, modifier) {
//>>includeStart('debug', pragmas.debug);
if (!defined(type)) {
throw new DeveloperError("type is required.");
}
//>>includeEnd('debug');
const key = getKey(type, modifier);
const lastMovement = this._lastMovement[key];
if (lastMovement.valid) {
return lastMovement;
}
return undefined;
};
/**
* Gets whether the mouse button is down or a touch has started.
*
* @param {CameraEventType} type The camera event type.
* @param {KeyboardEventModifier} [modifier] The keyboard modifier.
* @returns {boolean} Whether the mouse button is down or a touch has started.
*/
CameraEventAggregator.prototype.isButtonDown = function (type, modifier) {
//>>includeStart('debug', pragmas.debug);
if (!defined(type)) {
throw new DeveloperError("type is required.");
}
//>>includeEnd('debug');
const key = getKey(type, modifier);
return this._isDown[key];
};
/**
* Gets the mouse position that started the aggregation.
*
* @param {CameraEventType} type The camera event type.
* @param {KeyboardEventModifier} [modifier] The keyboard modifier.
* @returns {Cartesian2} The mouse position.
*/
CameraEventAggregator.prototype.getStartMousePosition = function (
type,
modifier,
) {
//>>includeStart('debug', pragmas.debug);
if (!defined(type)) {
throw new DeveloperError("type is required.");
}
//>>includeEnd('debug');
if (type === CameraEventType.WHEEL) {
return this._currentMousePosition;
}
const key = getKey(type, modifier);
return this._eventStartPosition[key];
};
/**
* Gets the time the button was pressed or the touch was started.
*
* @param {CameraEventType} type The camera event type.
* @param {KeyboardEventModifier} [modifier] The keyboard modifier.
* @returns {Date} The time the button was pressed or the touch was started.
*/
CameraEventAggregator.prototype.getButtonPressTime = function (type, modifier) {
//>>includeStart('debug', pragmas.debug);
if (!defined(type)) {
throw new DeveloperError("type is required.");
}
//>>includeEnd('debug');
const key = getKey(type, modifier);
return this._pressTime[key];
};
/**
* Gets the time the button was released or the touch was ended.
*
* @param {CameraEventType} type The camera event type.
* @param {KeyboardEventModifier} [modifier] The keyboard modifier.
* @returns {Date} The time the button was released or the touch was ended.
*/
CameraEventAggregator.prototype.getButtonReleaseTime = function (
type,
modifier,
) {
//>>includeStart('debug', pragmas.debug);
if (!defined(type)) {
throw new DeveloperError("type is required.");
}
//>>includeEnd('debug');
const key = getKey(type, modifier);
return this._releaseTime[key];
};
/**
* Signals that all of the events have been handled and the aggregator should be reset to handle new events.
*/
CameraEventAggregator.prototype.reset = function () {
for (const name in this._update) {
if (this._update.hasOwnProperty(name)) {
this._update[name] = true;
}
}
};
/**
* Returns true if this object was destroyed; otherwise, false.
* <br /><br />
* If this object was destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
*
* @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
*
* @see CameraEventAggregator#destroy
*/
CameraEventAggregator.prototype.isDestroyed = function () {
return false;
};
/**
* Removes mouse listeners held by this object.
* <br /><br />
* Once an object is destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
* assign the return value (<code>undefined</code>) to the object as done in the example.
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
*
* @example
* handler = handler && handler.destroy();
*
* @see CameraEventAggregator#isDestroyed
*/
CameraEventAggregator.prototype.destroy = function () {
this._eventHandler = this._eventHandler && this._eventHandler.destroy();
return destroyObject(this);
};
export default CameraEventAggregator;
+52
View File
@@ -0,0 +1,52 @@
// @ts-check
/**
* Enumerates the available input for interacting with the camera.
*
* @enum {number}
*/
const CameraEventType = {
/**
* A left mouse button press followed by moving the mouse and releasing the button.
*
* @type {number}
* @constant
*/
LEFT_DRAG: 0,
/**
* A right mouse button press followed by moving the mouse and releasing the button.
*
* @type {number}
* @constant
*/
RIGHT_DRAG: 1,
/**
* A middle mouse button press followed by moving the mouse and releasing the button.
*
* @type {number}
* @constant
*/
MIDDLE_DRAG: 2,
/**
* Scrolling the middle mouse button.
*
* @type {number}
* @constant
*/
WHEEL: 3,
/**
* A two-finger touch on a touch surface.
*
* @type {number}
* @constant
*/
PINCH: 4,
};
Object.freeze(CameraEventType);
export default CameraEventType;
+576
View File
@@ -0,0 +1,576 @@
import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Cartographic from "../Core/Cartographic.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
import EasingFunction from "../Core/EasingFunction.js";
import CesiumMath from "../Core/Math.js";
import PerspectiveFrustum from "../Core/PerspectiveFrustum.js";
import PerspectiveOffCenterFrustum from "../Core/PerspectiveOffCenterFrustum.js";
import SceneMode from "./SceneMode.js";
/**
* Creates tweens for camera flights.
* <br /><br />
* Mouse interaction is disabled during flights.
*
* @private
*/
const CameraFlightPath = {};
function getAltitude(frustum, dx, dy) {
let near;
let top;
let right;
if (frustum instanceof PerspectiveFrustum) {
const tanTheta = Math.tan(0.5 * frustum.fovy);
near = frustum.near;
top = frustum.near * tanTheta;
right = frustum.aspectRatio * top;
return Math.max((dx * near) / right, (dy * near) / top);
} else if (frustum instanceof PerspectiveOffCenterFrustum) {
near = frustum.near;
top = frustum.top;
right = frustum.right;
return Math.max((dx * near) / right, (dy * near) / top);
}
return Math.max(dx, dy);
}
const scratchCart = new Cartesian3();
const scratchCart2 = new Cartesian3();
function createPitchFunction(
startPitch,
endPitch,
heightFunction,
pitchAdjustHeight,
) {
if (defined(pitchAdjustHeight) && heightFunction(0.5) > pitchAdjustHeight) {
const startHeight = heightFunction(0.0);
const endHeight = heightFunction(1.0);
const middleHeight = heightFunction(0.5);
const d1 = middleHeight - startHeight;
const d2 = middleHeight - endHeight;
return function (time) {
const altitude = heightFunction(time);
if (time <= 0.5) {
const t1 = (altitude - startHeight) / d1;
return CesiumMath.lerp(startPitch, -CesiumMath.PI_OVER_TWO, t1);
}
const t2 = (altitude - endHeight) / d2;
return CesiumMath.lerp(-CesiumMath.PI_OVER_TWO, endPitch, 1 - t2);
};
}
return function (time) {
return CesiumMath.lerp(startPitch, endPitch, time);
};
}
function createHeightFunction(
camera,
destination,
startHeight,
endHeight,
optionAltitude,
) {
let altitude = optionAltitude;
const maxHeight = Math.max(startHeight, endHeight);
if (!defined(altitude)) {
const start = camera.position;
const end = destination;
const up = camera.up;
const right = camera.right;
const frustum = camera.frustum;
const diff = Cartesian3.subtract(start, end, scratchCart);
const verticalDistance = Cartesian3.magnitude(
Cartesian3.multiplyByScalar(up, Cartesian3.dot(diff, up), scratchCart2),
);
const horizontalDistance = Cartesian3.magnitude(
Cartesian3.multiplyByScalar(
right,
Cartesian3.dot(diff, right),
scratchCart2,
),
);
altitude = Math.min(
getAltitude(frustum, verticalDistance, horizontalDistance) * 0.2,
1000000000.0,
);
}
if (maxHeight < altitude) {
const power = 8.0;
const factor = 1000000.0;
const s = -Math.pow((altitude - startHeight) * factor, 1.0 / power);
const e = Math.pow((altitude - endHeight) * factor, 1.0 / power);
return function (t) {
const x = t * (e - s) + s;
return -Math.pow(x, power) / factor + altitude;
};
}
return function (t) {
return CesiumMath.lerp(startHeight, endHeight, t);
};
}
function adjustAngleForLERP(startAngle, endAngle) {
if (
CesiumMath.equalsEpsilon(
startAngle,
CesiumMath.TWO_PI,
CesiumMath.EPSILON11,
)
) {
startAngle = 0.0;
}
if (endAngle > startAngle + Math.PI) {
startAngle += CesiumMath.TWO_PI;
} else if (endAngle < startAngle - Math.PI) {
startAngle -= CesiumMath.TWO_PI;
}
return startAngle;
}
const scratchStart = new Cartesian3();
function createUpdateCV(
scene,
duration,
destination,
heading,
pitch,
roll,
optionAltitude,
optionPitchAdjustHeight,
) {
const camera = scene.camera;
const start = Cartesian3.clone(camera.position, scratchStart);
const startPitch = camera.pitch;
const startHeading = adjustAngleForLERP(camera.heading, heading);
const startRoll = adjustAngleForLERP(camera.roll, roll);
const heightFunction = createHeightFunction(
camera,
destination,
start.z,
destination.z,
optionAltitude,
);
const pitchFunction = createPitchFunction(
startPitch,
pitch,
heightFunction,
optionPitchAdjustHeight,
);
function update(value) {
const time = value.time / duration;
camera.setView({
orientation: {
heading: CesiumMath.lerp(startHeading, heading, time),
pitch: pitchFunction(time),
roll: CesiumMath.lerp(startRoll, roll, time),
},
});
Cartesian2.lerp(start, destination, time, camera.position);
camera.position.z = heightFunction(time);
}
return update;
}
function useLongestFlight(startCart, destCart) {
if (startCart.longitude < destCart.longitude) {
startCart.longitude += CesiumMath.TWO_PI;
} else {
destCart.longitude += CesiumMath.TWO_PI;
}
}
function useShortestFlight(startCart, destCart) {
const diff = startCart.longitude - destCart.longitude;
if (diff < -CesiumMath.PI) {
startCart.longitude += CesiumMath.TWO_PI;
} else if (diff > CesiumMath.PI) {
destCart.longitude += CesiumMath.TWO_PI;
}
}
const scratchStartCart = new Cartographic();
const scratchEndCart = new Cartographic();
function createUpdate3D(
scene,
duration,
destination,
heading,
pitch,
roll,
optionAltitude,
optionFlyOverLongitude,
optionFlyOverLongitudeWeight,
optionPitchAdjustHeight,
) {
const camera = scene.camera;
const projection = scene.mapProjection;
const ellipsoid = projection.ellipsoid;
const startCart = Cartographic.clone(
camera.positionCartographic,
scratchStartCart,
);
const startPitch = camera.pitch;
const startHeading = adjustAngleForLERP(camera.heading, heading);
const startRoll = adjustAngleForLERP(camera.roll, roll);
const destCart = ellipsoid.cartesianToCartographic(
destination,
scratchEndCart,
);
startCart.longitude = CesiumMath.zeroToTwoPi(startCart.longitude);
destCart.longitude = CesiumMath.zeroToTwoPi(destCart.longitude);
let useLongFlight = false;
if (defined(optionFlyOverLongitude)) {
const hitLon = CesiumMath.zeroToTwoPi(optionFlyOverLongitude);
const lonMin = Math.min(startCart.longitude, destCart.longitude);
const lonMax = Math.max(startCart.longitude, destCart.longitude);
const hitInside = hitLon >= lonMin && hitLon <= lonMax;
if (defined(optionFlyOverLongitudeWeight)) {
// Distance inside (0...2Pi)
const din = Math.abs(startCart.longitude - destCart.longitude);
// Distance outside (0...2Pi)
const dot = CesiumMath.TWO_PI - din;
const hitDistance = hitInside ? din : dot;
const offDistance = hitInside ? dot : din;
if (
hitDistance < offDistance * optionFlyOverLongitudeWeight &&
!hitInside
) {
useLongFlight = true;
}
} else if (!hitInside) {
useLongFlight = true;
}
}
if (useLongFlight) {
useLongestFlight(startCart, destCart);
} else {
useShortestFlight(startCart, destCart);
}
const heightFunction = createHeightFunction(
camera,
destination,
startCart.height,
destCart.height,
optionAltitude,
);
const pitchFunction = createPitchFunction(
startPitch,
pitch,
heightFunction,
optionPitchAdjustHeight,
);
// Isolate scope for update function.
// to have local copies of vars used in lerp
// Othervise, if you call nex
// createUpdate3D (createAnimationTween)
// before you played animation, variables will be overwriten.
function isolateUpdateFunction() {
const startLongitude = startCart.longitude;
const destLongitude = destCart.longitude;
const startLatitude = startCart.latitude;
const destLatitude = destCart.latitude;
return function update(value) {
const time = value.time / duration;
const position = Cartesian3.fromRadians(
CesiumMath.lerp(startLongitude, destLongitude, time),
CesiumMath.lerp(startLatitude, destLatitude, time),
heightFunction(time),
ellipsoid,
);
camera.setView({
destination: position,
orientation: {
heading: CesiumMath.lerp(startHeading, heading, time),
pitch: pitchFunction(time),
roll: CesiumMath.lerp(startRoll, roll, time),
},
});
};
}
return isolateUpdateFunction();
}
function createUpdate2D(
scene,
duration,
destination,
heading,
pitch,
roll,
optionAltitude,
) {
const camera = scene.camera;
const start = Cartesian3.clone(camera.position, scratchStart);
const startHeading = adjustAngleForLERP(camera.heading, heading);
const startHeight = camera.frustum.right - camera.frustum.left;
const heightFunction = createHeightFunction(
camera,
destination,
startHeight,
destination.z,
optionAltitude,
);
function update(value) {
const time = value.time / duration;
camera.setView({
orientation: {
heading: CesiumMath.lerp(startHeading, heading, time),
},
});
Cartesian2.lerp(start, destination, time, camera.position);
const zoom = heightFunction(time);
const frustum = camera.frustum;
const ratio = frustum.top / frustum.right;
const incrementAmount = (zoom - (frustum.right - frustum.left)) * 0.5;
frustum.right += incrementAmount;
frustum.left -= incrementAmount;
frustum.top = ratio * frustum.right;
frustum.bottom = -frustum.top;
}
return update;
}
const scratchCartographic = new Cartographic();
const scratchDestination = new Cartesian3();
function emptyFlight(complete, cancel) {
return {
startObject: {},
stopObject: {},
duration: 0.0,
complete: complete,
cancel: cancel,
};
}
function wrapCallback(controller, cb) {
function wrapped() {
if (typeof cb === "function") {
cb();
}
controller.enableInputs = true;
}
return wrapped;
}
CameraFlightPath.createTween = function (scene, options) {
options = options ?? Frozen.EMPTY_OBJECT;
let destination = options.destination;
//>>includeStart('debug', pragmas.debug);
if (!defined(scene)) {
throw new DeveloperError("scene is required.");
}
if (!defined(destination)) {
throw new DeveloperError("destination is required.");
}
//>>includeEnd('debug');
const mode = scene.mode;
if (mode === SceneMode.MORPHING) {
return emptyFlight();
}
const convert = options.convert ?? true;
const projection = scene.mapProjection;
const ellipsoid = projection.ellipsoid;
const maximumHeight = options.maximumHeight;
const flyOverLongitude = options.flyOverLongitude;
const flyOverLongitudeWeight = options.flyOverLongitudeWeight;
const pitchAdjustHeight = options.pitchAdjustHeight;
let easingFunction = options.easingFunction;
if (convert && mode !== SceneMode.SCENE3D) {
ellipsoid.cartesianToCartographic(destination, scratchCartographic);
destination = projection.project(scratchCartographic, scratchDestination);
}
const camera = scene.camera;
const transform = options.endTransform;
if (defined(transform)) {
camera._setTransform(transform);
}
let duration = options.duration;
if (!defined(duration)) {
duration =
Math.ceil(Cartesian3.distance(camera.position, destination) / 1000000.0) +
2.0;
duration = Math.min(duration, 3.0);
}
const heading = options.heading ?? 0.0;
const pitch = options.pitch ?? -CesiumMath.PI_OVER_TWO;
const roll = options.roll ?? 0.0;
const controller = scene.screenSpaceCameraController;
controller.enableInputs = false;
const complete = wrapCallback(controller, options.complete);
const cancel = wrapCallback(controller, options.cancel);
const frustum = camera.frustum;
let empty = scene.mode === SceneMode.SCENE2D;
empty =
empty &&
Cartesian2.equalsEpsilon(camera.position, destination, CesiumMath.EPSILON6);
empty =
empty &&
CesiumMath.equalsEpsilon(
Math.max(frustum.right - frustum.left, frustum.top - frustum.bottom),
destination.z,
CesiumMath.EPSILON6,
);
empty =
empty ||
(scene.mode !== SceneMode.SCENE2D &&
Cartesian3.equalsEpsilon(
destination,
camera.position,
CesiumMath.EPSILON10,
));
empty =
empty &&
CesiumMath.equalsEpsilon(
CesiumMath.negativePiToPi(heading),
CesiumMath.negativePiToPi(camera.heading),
CesiumMath.EPSILON10,
) &&
CesiumMath.equalsEpsilon(
CesiumMath.negativePiToPi(pitch),
CesiumMath.negativePiToPi(camera.pitch),
CesiumMath.EPSILON10,
) &&
CesiumMath.equalsEpsilon(
CesiumMath.negativePiToPi(roll),
CesiumMath.negativePiToPi(camera.roll),
CesiumMath.EPSILON10,
);
if (empty) {
return emptyFlight(complete, cancel);
}
const updateFunctions = new Array(4);
updateFunctions[SceneMode.SCENE2D] = createUpdate2D;
updateFunctions[SceneMode.SCENE3D] = createUpdate3D;
updateFunctions[SceneMode.COLUMBUS_VIEW] = createUpdateCV;
if (duration <= 0.0) {
const newOnComplete = function () {
const update = updateFunctions[mode](
scene,
1.0,
destination,
heading,
pitch,
roll,
maximumHeight,
flyOverLongitude,
flyOverLongitudeWeight,
pitchAdjustHeight,
);
update({ time: 1.0 });
if (typeof complete === "function") {
complete();
}
};
return emptyFlight(newOnComplete, cancel);
}
const update = updateFunctions[mode](
scene,
duration,
destination,
heading,
pitch,
roll,
maximumHeight,
flyOverLongitude,
flyOverLongitudeWeight,
pitchAdjustHeight,
);
if (!defined(easingFunction)) {
const startHeight = camera.positionCartographic.height;
const endHeight =
mode === SceneMode.SCENE3D
? ellipsoid.cartesianToCartographic(destination).height
: destination.z;
if (startHeight > endHeight && startHeight > 11500.0) {
easingFunction = EasingFunction.CUBIC_OUT;
} else {
easingFunction = EasingFunction.QUINTIC_IN_OUT;
}
}
return {
duration: duration,
easingFunction: easingFunction,
startObject: {
time: 0.0,
},
stopObject: {
time: duration,
},
update: update,
complete: complete,
cancel: cancel,
};
};
export default CameraFlightPath;
+44
View File
@@ -0,0 +1,44 @@
import Check from "../Core/Check.js";
import Frozen from "../Core/Frozen.js";
/**
* Simple abstraction for a group. This class exists to make the metadata API
* more consistent, i.e. metadata can be accessed via
* <code>content.group.metadata</code> much like tile metadata is accessed as
* <code>tile.metadata</code>.
*
* @param {object} options Object with the following properties:
* @param {GroupMetadata} options.metadata The metadata associated with this group.
*
* @alias Cesium3DContentGroup
* @constructor
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
function Cesium3DContentGroup(options) {
options = options ?? Frozen.EMPTY_OBJECT;
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options.metadata", options.metadata);
//>>includeEnd('debug');
this._metadata = options.metadata;
}
Object.defineProperties(Cesium3DContentGroup.prototype, {
/**
* Get the metadata for this group
*
* @memberof Cesium3DContentGroup.prototype
*
* @type {GroupMetadata}
*
* @readonly
*/
metadata: {
get: function () {
return this._metadata;
},
},
});
export default Cesium3DContentGroup;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+57
View File
@@ -0,0 +1,57 @@
// @ts-check
/**
* Defines how per-feature colors set from the Cesium API or declarative styling blend with the source colors from
* the original feature, e.g. glTF material or per-point color in the tile.
* <p>
* When <code>REPLACE</code> or <code>MIX</code> are used and the source color is a glTF material, the technique must assign the
* <code>_3DTILESDIFFUSE</code> semantic to the diffuse color parameter. Otherwise only <code>HIGHLIGHT</code> is supported.
* </p>
* <p>
* A feature whose color evaluates to white (1.0, 1.0, 1.0) is always rendered without color blending, regardless of the
* tileset's color blend mode.
* </p>
* <pre><code>
* "techniques": {
* "technique0": {
* "parameters": {
* "diffuse": {
* "semantic": "_3DTILESDIFFUSE",
* "type": 35666
* }
* }
* }
* }
* </code></pre>
*
* @enum {number}
*/
const Cesium3DTileColorBlendMode = {
/**
* Multiplies the source color by the feature color.
*
* @type {number}
* @constant
*/
HIGHLIGHT: 0,
/**
* Replaces the source color with the feature color.
*
* @type {number}
* @constant
*/
REPLACE: 1,
/**
* Blends the source color and feature color together.
*
* @type {number}
* @constant
*/
MIX: 2,
};
Object.freeze(Cesium3DTileColorBlendMode);
export default Cesium3DTileColorBlendMode;
+356
View File
@@ -0,0 +1,356 @@
// @ts-check
import DeveloperError from "../Core/DeveloperError.js";
/** @import Cartesian3 from "../Core/Cartesian3.js"; */
/** @import Cesium3DContentGroup from "./Cesium3DContentGroup.js"; */
/** @import Cesium3DTile from "./Cesium3DTile.js"; */
/** @import Cesium3DTileBatchTable from "./Cesium3DTileBatchTable.js"; */
/** @import Cesium3DTileFeature from "./Cesium3DTileFeature.js"; */
/** @import Cesium3DTileStyle from "./Cesium3DTileStyle.js"; */
/** @import Cesium3DTileset from "./Cesium3DTileset.js"; */
/** @import Color from "../Core/Color.js"; */
/** @import FrameState from "./FrameState.js"; */
/** @import ImplicitMetadataView from "./ImplicitMetadataView.js"; */
/** @import Ray from "../Core/Ray.js"; */
/**
* The content of a tile in a {@link Cesium3DTileset}.
* <p>
* Derived classes of this interface provide access to individual features in the tile.
* Access derived objects through {@link Cesium3DTile#content}.
* </p>
* <p>
* This type describes an interface and is not intended to be instantiated directly.
* </p>
*
* @interface
*/
class Cesium3DTileContent {
constructor() {
/**
* Gets or sets if any feature's property changed. Used to
* optimized applying a style when a feature's property changed.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @type {boolean}
*
* @protected
* @ignore
*/
this.featurePropertiesDirty = false;
}
/**
* Gets the number of features in the tile.
*
*
* @type {number}
* @readonly
* @constant
*/
featuresLength;
/**
* Gets the number of points in the tile.
* <p>
* Only applicable for tiles with Point Cloud content. This is different than {@link Cesium3DTileContent#featuresLength} which
* equals the number of groups of points as distinguished by the <code>BATCH_ID</code> feature table semantic.
* </p>
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/PointCloud#batched-points}
*
*
* @type {number}
* @readonly
* @constant
*/
pointsLength;
/**
* Gets the number of triangles in the tile.
*
*
* @type {number}
* @readonly
* @constant
*/
trianglesLength;
/**
* Gets the tile's geometry memory in bytes.
*
*
* @type {number}
* @readonly
* @constant
*/
geometryByteLength;
/**
* Gets the tile's texture memory in bytes.
*
*
* @type {number}
* @readonly
* @constant
*/
texturesByteLength;
/**
* Gets the amount of memory used by the batch table textures and any binary
* metadata properties not accounted for in geometryByteLength or
* texturesByteLength
*
*
* @type {number}
* @readonly
* @constant
*/
batchTableByteLength;
/**
* Gets the array of {@link Cesium3DTileContent} objects for contents that contain other contents, such as composite tiles. The inner contents may in turn have inner contents, such as a composite tile that contains a composite tile.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/Composite|Composite specification}
*
*
* @type {Array<*>}
* @readonly
* @constant
*/
innerContents;
/**
* Returns true when the tile's content is ready to render; otherwise false
*
*
* @type {boolean}
* @readonly
* @constant
*/
ready;
/**
* Gets the tileset for this tile.
*
*
* @type {Cesium3DTileset}
* @readonly
* @constant
*/
tileset;
/**
* Gets the tile containing this content.
*
*
* @type {Cesium3DTile}
* @readonly
* @constant
*/
tile;
/**
* Gets the url of the tile's content.
*
* @type {string}
* @readonly
* @constant
*/
url;
/**
* Gets the batch table for this content.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @type {Cesium3DTileBatchTable}
* @readonly
* @constant
*
* @private
*/
batchTable;
/**
* Gets the metadata for this content, whether it is available explicitly or via
* implicit tiling. If there is no metadata, this property should be undefined.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @type {ImplicitMetadataView|undefined}
*
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
metadata;
/**
* Gets the group for this content if the content has metadata (3D Tiles 1.1) or
* if it uses the <code>3DTILES_metadata</code> extension. If neither are present,
* this property should be undefined.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @type {Cesium3DContentGroup|undefined}
*
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
group;
/**
* Returns whether the feature has this property.
*
* @param {number} batchId The batchId for the feature.
* @param {string} name The case-sensitive name of the property.
* @returns {boolean} <code>true</code> if the feature has this property; otherwise, <code>false</code>.
*/
hasProperty(batchId, name) {
DeveloperError.throwInstantiationError();
}
/**
* Returns the {@link Cesium3DTileFeature} object for the feature with the
* given <code>batchId</code>. This object is used to get and modify the
* feature's properties.
* <p>
* Features in a tile are ordered by <code>batchId</code>, an index used to retrieve their metadata from the batch table.
* </p>
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/BatchTable}.
*
* @param {number} batchId The batchId for the feature.
* @returns {Cesium3DTileFeature} The corresponding {@link Cesium3DTileFeature} object.
*
* @exception {DeveloperError} batchId must be between zero and {@link Cesium3DTileContent#featuresLength} - 1.
*/
getFeature(batchId) {
DeveloperError.throwInstantiationError();
}
/**
* Called when {@link Cesium3DTileset#debugColorizeTiles} changes.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @param {boolean} enabled Whether to enable or disable debug settings.
* @param {Color} color Debug color.
* @returns {Cesium3DTileFeature} The corresponding {@link Cesium3DTileFeature} object.
* @private
*/
applyDebugSettings(enabled, color) {
DeveloperError.throwInstantiationError();
}
/**
* Apply a style to the content
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @param {Cesium3DTileStyle} style The style.
* @returns {void}
*
* @private
*/
applyStyle(style) {
DeveloperError.throwInstantiationError();
}
/**
* Called by the tile during tileset traversal to get the draw commands needed to render this content.
* When the tile's content is in the PROCESSING state, this creates WebGL resources to ultimately
* move to the READY state.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @param {Cesium3DTileset} tileset The tileset containing this tile.
* @param {FrameState} frameState The frame state.
* @returns {void}
*
* @private
*/
update(tileset, frameState) {
DeveloperError.throwInstantiationError();
}
/**
* Find an intersection between a ray and the tile content surface that was rendered. The ray must be given in world coordinates.
*
* @param {Ray} ray The ray to test for intersection.
* @param {FrameState} frameState The frame state.
* @param {Cartesian3|undefined} [result] The intersection or <code>undefined</code> if none was found.
* @returns {Cartesian3|undefined} The intersection or <code>undefined</code> if none was found.
*
* @private
*/
pick(ray, frameState, result) {
DeveloperError.throwInstantiationError();
}
/**
* Returns true if this object was destroyed; otherwise, false.
* <br /><br />
* If this object was destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
*
* @see Cesium3DTileContent#destroy
*
* @private
*/
isDestroyed() {
DeveloperError.throwInstantiationError();
}
/**
* 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.
* <br /><br />
* Once an object is destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
* assign the return value (<code>undefined</code>) to the object as done in the example.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
* @example
* content = content && content.destroy();
*
* @see Cesium3DTileContent#isDestroyed
*
* @returns {void}
*
* @private
*/
destroy() {
DeveloperError.throwInstantiationError();
}
}
export default Cesium3DTileContent;
+135
View File
@@ -0,0 +1,135 @@
import Composite3DTileContent from "./Composite3DTileContent.js";
import Geometry3DTileContent from "./Geometry3DTileContent.js";
import Implicit3DTileContent from "./Implicit3DTileContent.js";
import Model3DTileContent from "./Model/Model3DTileContent.js";
import Tileset3DTileContent from "./Tileset3DTileContent.js";
import Vector3DTileContent from "./Vector3DTileContent.js";
import VectorGltf3DTileContent from "./VectorGltf3DTileContent.js";
import GaussianSplat3DTileContent from "./GaussianSplat3DTileContent.js";
import RuntimeError from "../Core/RuntimeError.js";
/**
* Maps a tile's magic field in its header to a new content object for the tile's payload.
*
* @private
*/
const Cesium3DTileContentFactory = {
b3dm: function (tileset, tile, resource, arrayBuffer, byteOffset) {
return Model3DTileContent.fromB3dm(
tileset,
tile,
resource,
arrayBuffer,
byteOffset,
);
},
pnts: function (tileset, tile, resource, arrayBuffer, byteOffset) {
return Model3DTileContent.fromPnts(
tileset,
tile,
resource,
arrayBuffer,
byteOffset,
);
},
i3dm: function (tileset, tile, resource, arrayBuffer, byteOffset) {
return Model3DTileContent.fromI3dm(
tileset,
tile,
resource,
arrayBuffer,
byteOffset,
);
},
cmpt: function (tileset, tile, resource, arrayBuffer, byteOffset) {
// Send in the factory in order to avoid a cyclical dependency
return Composite3DTileContent.fromTileType(
tileset,
tile,
resource,
arrayBuffer,
byteOffset,
Cesium3DTileContentFactory,
);
},
externalTileset: function (tileset, tile, resource, json) {
return Tileset3DTileContent.fromJson(tileset, tile, resource, json);
},
geom: function (tileset, tile, resource, arrayBuffer, byteOffset) {
return new Geometry3DTileContent(
tileset,
tile,
resource,
arrayBuffer,
byteOffset,
);
},
vctr: function (tileset, tile, resource, arrayBuffer, byteOffset) {
return new Vector3DTileContent(
tileset,
tile,
resource,
arrayBuffer,
byteOffset,
);
},
subt: function (tileset, tile, resource, arrayBuffer, byteOffset) {
return Implicit3DTileContent.fromSubtreeJson(
tileset,
tile,
resource,
undefined,
arrayBuffer,
byteOffset,
);
},
subtreeJson: function (tileset, tile, resource, json) {
return Implicit3DTileContent.fromSubtreeJson(tileset, tile, resource, json);
},
glb: function (tileset, tile, resource, arrayBuffer, byteOffset) {
const arrayBufferByteLength = arrayBuffer.byteLength;
if (arrayBufferByteLength < 12) {
throw new RuntimeError("Invalid glb content");
}
const dataView = new DataView(arrayBuffer, byteOffset);
const byteLength = dataView.getUint32(8, true);
const glb = new Uint8Array(arrayBuffer, byteOffset, byteLength);
if (
GaussianSplat3DTileContent.tilesetRequiresGaussianSplattingExt(tileset)
) {
return GaussianSplat3DTileContent.fromGltf(tileset, tile, resource, glb);
}
// @deprecated CESIUM_mesh_vector to be removed after v1.142 release.
if (
tileset.hasExtension("3DTILES_content_gltf_vector") ||
tileset.isGltfExtensionUsed("CESIUM_mesh_vector")
) {
return VectorGltf3DTileContent.fromGltf(tileset, tile, resource, glb);
}
return Model3DTileContent.fromGltf(tileset, tile, resource, glb);
},
gltf: function (tileset, tile, resource, json) {
if (
GaussianSplat3DTileContent.tilesetRequiresGaussianSplattingExt(tileset)
) {
return GaussianSplat3DTileContent.fromGltf(tileset, tile, resource, json);
}
// @deprecated CESIUM_mesh_vector to be removed after v1.142 release.
if (
tileset.hasExtension("3DTILES_content_gltf_vector") ||
tileset.isGltfExtensionUsed("CESIUM_mesh_vector")
) {
return VectorGltf3DTileContent.fromGltf(tileset, tile, resource, json);
}
return Model3DTileContent.fromGltf(tileset, tile, resource, json);
},
geoJson: function (tileset, tile, resource, json) {
return Model3DTileContent.fromGeoJson(tileset, tile, resource, json);
},
};
export default Cesium3DTileContentFactory;
+17
View File
@@ -0,0 +1,17 @@
// @ts-check
/**
* @private
*/
const Cesium3DTileContentState = {
UNLOADED: 0, // Has never been requested
LOADING: 1, // Is waiting on a pending request
PROCESSING: 2, // Request received. Contents are being processed for rendering. Depending on the content, it might make its own requests for external data.
READY: 3, // Ready to render.
EXPIRED: 4, // Is expired and will be unloaded once new content is loaded.
FAILED: 5, // Request failed.
};
Object.freeze(Cesium3DTileContentState);
export default Cesium3DTileContentState;
+181
View File
@@ -0,0 +1,181 @@
/**
* An enum to indicate the different types of {@link Cesium3DTileContent}.
* For binary files, the enum value is the magic number of the binary file
* unless otherwise noted. For JSON files, the enum value is a unique name
* for internal use.
*
* @enum {string}
* @see Cesium3DTileContent
*
* @private
*/
const Cesium3DTileContentType = {
/**
* A Batched 3D Model. This is a binary format with
* magic number <code>b3dm</code>
*
* @type {string}
* @constant
* @private
*/
BATCHED_3D_MODEL: "b3dm",
/**
* An Instanced 3D Model. This is a binary format with magic number
* <code>i3dm</code>
*
* @type {string}
* @constant
* @private
*/
INSTANCED_3D_MODEL: "i3dm",
/**
* A Composite model. This is a binary format with magic number
* <code>cmpt</code>
*
* @type {string}
* @constant
* @private
*/
COMPOSITE: "cmpt",
/**
* A Point Cloud model. This is a binary format with magic number
* <code>pnts</code>
*
* @type {string}
* @constant
* @private
*/
POINT_CLOUD: "pnts",
/**
* Vector tiles. This is a binary format with magic number
* <code>vctr</code>
*
* @type {string}
* @constant
* @private
*/
VECTOR: "vctr",
/**
* Geometry tiles. This is a binary format with magic number
* <code>geom</code>
*
* @type {string}
* @constant
* @private
*/
GEOMETRY: "geom",
/**
* A glTF model in JSON + external BIN form. This is treated
* as a JSON format.
*
* @type {string}
* @constant
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
GLTF: "gltf",
/**
* The binary form of a glTF file. Internally, the magic number is
* changed from <code>glTF</code> to <code>glb</code> to distinguish it from
* the JSON glTF format.
*
* @type {string}
* @constant
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
GLTF_BINARY: "glb",
/**
* For implicit tiling, availability bitstreams are stored in binary subtree files.
* The magic number is <code>subt</code>
*
* @type {string}
* @constant
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
IMPLICIT_SUBTREE: "subt",
/**
* For implicit tiling. Subtrees can also be represented as JSON files.
*
* @type {string}
* @constant
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
IMPLICIT_SUBTREE_JSON: "subtreeJson",
/**
* Contents can reference another tileset.json to use
* as an external tileset. This is a JSON-based format.
*
* @type {string}
* @constant
* @private
*/
EXTERNAL_TILESET: "externalTileset",
/**
* Multiple contents are handled separately from the other content types
* due to differences in request scheduling.
*
* @type {string}
* @constant
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
MULTIPLE_CONTENT: "multipleContent",
/**
* GeoJSON content for <code>MAXAR_content_geojson</code> extension.
*
* @type {string}
* @constant
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
GEOJSON: "geoJson",
/**
* Binary voxel content for <code>3DTILES_content_voxels</code> extension.
*
* @type {string}
* @constant
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
VOXEL_BINARY: "voxl",
/**
* Binary voxel content for <code>3DTILES_content_voxels</code> extension.
*
* @type {string}
* @constant
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
VOXEL_JSON: "voxelJson",
};
/**
* Check if a content is one of the supported binary formats. Otherwise,
* the caller can assume a JSON format.
* @param {Cesium3DTileContentType} contentType The content type of the content payload.
* @return {boolean} <code>true</code> if the content type is a binary format, or <code>false</code> if the content type is a JSON format.
* @private
*/
Cesium3DTileContentType.isBinaryFormat = function (contentType) {
switch (contentType) {
case Cesium3DTileContentType.BATCHED_3D_MODEL:
case Cesium3DTileContentType.INSTANCED_3D_MODEL:
case Cesium3DTileContentType.COMPOSITE:
case Cesium3DTileContentType.POINT_CLOUD:
case Cesium3DTileContentType.VECTOR:
case Cesium3DTileContentType.GEOMETRY:
case Cesium3DTileContentType.IMPLICIT_SUBTREE:
case Cesium3DTileContentType.VOXEL_BINARY:
case Cesium3DTileContentType.GLTF_BINARY:
return true;
default:
return false;
}
};
Object.freeze(Cesium3DTileContentType);
export default Cesium3DTileContentType;
+427
View File
@@ -0,0 +1,427 @@
import Color from "../Core/Color.js";
import defined from "../Core/defined.js";
/** @import Cesium3DTileBatchTable from "./Cesium3DTileBatchTable.js"; */
/** @import Cesium3DTileContent from "./Cesium3DTileContent.js"; */
/** @import Cesium3DTileset from "./Cesium3DTileset.js"; */
/**
* A feature of a {@link Cesium3DTileset}.
* <p>
* Provides access to a feature's properties stored in the tile's batch table, as well
* as the ability to show/hide a feature and change its highlight color via
* {@link Cesium3DTileFeature#show} and {@link Cesium3DTileFeature#color}, respectively.
* </p>
* <p>
* Modifications to a <code>Cesium3DTileFeature</code> object have the lifetime of the tile's
* content. If the tile's content is unloaded, e.g., due to it going out of view and needing
* to free space in the cache for visible tiles, listen to the {@link Cesium3DTileset#tileUnload} event to save any
* modifications. Also listen to the {@link Cesium3DTileset#tileVisible} event to reapply any modifications.
* </p>
* <p>
* Do not construct this directly. Access it through {@link Cesium3DTileContent#getFeature}
* or picking using {@link Scene#pick}.
* </p>
*
* @example
* // On mouse over, display all the properties for a feature in the console log.
* handler.setInputAction(function(movement) {
* const feature = scene.pick(movement.endPosition);
* if (feature instanceof Cesium.Cesium3DTileFeature) {
* const propertyIds = feature.getPropertyIds();
* const length = propertyIds.length;
* for (let i = 0; i < length; ++i) {
* const propertyId = propertyIds[i];
* console.log(`{propertyId}: ${feature.getProperty(propertyId)}`);
* }
* }
* }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
*/
class Cesium3DTileFeature {
/**
* @param {Cesium3DTileContent} content
* @param {number} batchId
*/
constructor(content, batchId) {
this._content = content;
this._batchId = batchId;
this._color = undefined; // for calling getColor
}
/**
* Gets or sets if the feature will be shown. This is set for all features
* when a style's show is evaluated.
*
* @type {boolean}
*
* @default true
*/
get show() {
return this._content.batchTable.getShow(this._batchId);
}
set show(value) {
this._content.batchTable.setShow(this._batchId, value);
}
/**
* Gets or sets the highlight color multiplied with the feature's color. When
* this is white, the feature's color is not changed. This is set for all features
* when a style's color is evaluated.
*
* @type {Color}
*
* @default {@link Color.WHITE}
*/
get color() {
if (!defined(this._color)) {
this._color = new Color();
}
return this._content.batchTable.getColor(this._batchId, this._color);
}
set color(value) {
this._content.batchTable.setColor(this._batchId, value);
}
/**
* Gets a typed array containing the ECEF positions of the polyline.
* Returns undefined if {@link Cesium3DTileset#vectorKeepDecodedPositions} is false
* or the feature is not a polyline in a vector tile.
*
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*
* @type {Float64Array}
*/
get polylinePositions() {
if (!defined(this._content.getPolylinePositions)) {
return undefined;
}
return this._content.getPolylinePositions(this._batchId);
}
/**
* Gets the content of the tile containing the feature.
*
* @type {Cesium3DTileContent}
*
* @readonly
* @private
*/
get content() {
return this._content;
}
/**
* Gets the tileset containing the feature.
*
* @type {Cesium3DTileset}
*
* @readonly
*/
get tileset() {
return this._content.tileset;
}
/**
* All objects returned by {@link Scene#pick} have a <code>primitive</code> property. This returns
* the tileset containing the feature.
*
* @type {Cesium3DTileset}
*
* @readonly
*/
get primitive() {
return this._content.tileset;
}
/**
* Get the feature ID associated with this feature. For 3D Tiles 1.0, the
* batch ID is returned. For EXT_mesh_features, this is the feature ID from
* the selected feature ID set.
*
* @type {number}
*
* @readonly
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
get featureId() {
return this._batchId;
}
/**
* @private
*/
get pickId() {
return this._content.batchTable.getPickColor(this._batchId);
}
/**
* Returns whether the feature contains this property. This includes properties from this feature's
* class and inherited classes when using a batch table hierarchy.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy}
*
* @param {string} name The case-sensitive name of the property.
* @returns {boolean} Whether the feature contains this property.
*/
hasProperty(name) {
return this._content.batchTable.hasProperty(this._batchId, name);
}
/**
* Returns an array of property IDs for the feature. This includes properties from this feature's
* class and inherited classes when using a batch table hierarchy.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy}
*
* @param {string[]} [results] An array into which to store the results.
* @returns {string[]} The IDs of the feature's properties.
*/
getPropertyIds(results) {
return this._content.batchTable.getPropertyIds(this._batchId, results);
}
/**
* Returns a copy of the value of the feature's property with the given name. This includes properties from this feature's
* class and inherited classes when using a batch table hierarchy.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy}
*
* @param {string} name The case-sensitive name of the property.
* @returns {*} The value of the property or <code>undefined</code> if the feature does not have this property.
*
* @example
* // Display all the properties for a feature in the console log.
* const propertyIds = feature.getPropertyIds();
* const length = propertyIds.length;
* for (let i = 0; i < length; ++i) {
* const propertyId = propertyIds[i];
* console.log(`{propertyId}: ${feature.getProperty(propertyId)}`);
* }
*/
getProperty(name) {
return this._content.batchTable.getProperty(this._batchId, name);
}
/**
* Returns a copy of the feature's property with the given name, examining all
* the metadata from 3D Tiles 1.0 formats, the EXT_structural_metadata and legacy
* EXT_feature_metadata glTF extensions, and the metadata present either in the
* tileset JSON (3D Tiles 1.1) or in the 3DTILES_metadata 3D Tiles extension.
* Metadata is checked against name from most specific to most general and the
* first match is returned. Metadata is checked in this order:
*
* <ol>
* <li>Batch table (structural metadata) property by semantic</li>
* <li>Batch table (structural metadata) property by property ID</li>
* <li>Content metadata property by semantic</li>
* <li>Content metadata property by property</li>
* <li>Tile metadata property by semantic</li>
* <li>Tile metadata property by property ID</li>
* <li>Subtree metadata property by semantic</li>
* <li>Subtree metadata property by property ID</li>
* <li>Group metadata property by semantic</li>
* <li>Group metadata property by property ID</li>
* <li>Tileset metadata property by semantic</li>
* <li>Tileset metadata property by property ID</li>
* <li>Otherwise, return undefined</li>
* </ol>
* <p>
* For 3D Tiles Next details, see the {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_metadata|3DTILES_metadata Extension}
* for 3D Tiles, as well as the {@link https://github.com/CesiumGS/glTF/tree/3d-tiles-next/extensions/2.0/Vendor/EXT_structural_metadata|EXT_structural_metadata Extension}
* for glTF. For the legacy glTF extension, see {@link https://github.com/CesiumGS/glTF/tree/3d-tiles-next/extensions/2.0/Vendor/EXT_feature_metadata|EXT_feature_metadata Extension}
* </p>
*
* @param {Cesium3DTileContent} content The content for accessing the metadata
* @param {number} batchId The batch ID (or feature ID) of the feature to get a property for
* @param {string} name The semantic or property ID of the feature. Semantics are checked before property IDs in each granularity of metadata.
* @privateParam {Cesium3DTileBatchTable} [batchTable] Batch table in which to look up the feature property. If unspecified, `content.batchTable` is used.
* @return {*} The value of the property or <code>undefined</code> if the feature does not have this property.
*
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
static getPropertyInherited(
content,
batchId,
name,
batchTable = content.batchTable,
) {
if (defined(batchTable)) {
if (batchTable.hasPropertyBySemantic(batchId, name)) {
return batchTable.getPropertyBySemantic(batchId, name);
}
if (batchTable.hasProperty(batchId, name)) {
return batchTable.getProperty(batchId, name);
}
}
const contentMetadata = content.metadata;
if (defined(contentMetadata)) {
if (contentMetadata.hasPropertyBySemantic(name)) {
return contentMetadata.getPropertyBySemantic(name);
}
if (contentMetadata.hasProperty(name)) {
return contentMetadata.getProperty(name);
}
}
const tile = content.tile;
const tileMetadata = tile.metadata;
if (defined(tileMetadata)) {
if (tileMetadata.hasPropertyBySemantic(name)) {
return tileMetadata.getPropertyBySemantic(name);
}
if (tileMetadata.hasProperty(name)) {
return tileMetadata.getProperty(name);
}
}
let subtreeMetadata;
if (defined(tile.implicitSubtree)) {
subtreeMetadata = tile.implicitSubtree.metadata;
}
if (defined(subtreeMetadata)) {
if (subtreeMetadata.hasPropertyBySemantic(name)) {
return subtreeMetadata.getPropertyBySemantic(name);
}
if (subtreeMetadata.hasProperty(name)) {
return subtreeMetadata.getProperty(name);
}
}
const groupMetadata = defined(content.group)
? content.group.metadata
: undefined;
if (defined(groupMetadata)) {
if (groupMetadata.hasPropertyBySemantic(name)) {
return groupMetadata.getPropertyBySemantic(name);
}
if (groupMetadata.hasProperty(name)) {
return groupMetadata.getProperty(name);
}
}
const tilesetMetadata = content.tileset.metadata;
if (defined(tilesetMetadata)) {
if (tilesetMetadata.hasPropertyBySemantic(name)) {
return tilesetMetadata.getPropertyBySemantic(name);
}
if (tilesetMetadata.hasProperty(name)) {
return tilesetMetadata.getProperty(name);
}
}
return undefined;
}
/**
* Returns a copy of the value of the feature's property with the given name.
* If the feature is contained within a tileset that has metadata (3D Tiles 1.1)
* or uses the <code>3DTILES_metadata</code> extension, tileset, group and tile
* metadata is inherited.
* <p>
* To resolve name conflicts, this method resolves names from most specific to
* least specific by metadata granularity in the order: feature, tile, group,
* tileset. Within each granularity, semantics are resolved first, then other
* properties.
* </p>
* @param {string} name The case-sensitive name of the property.
* @returns {*} The value of the property or <code>undefined</code> if the feature does not have this property.
* @private
*/
getPropertyInherited(name) {
return Cesium3DTileFeature.getPropertyInherited(
this._content,
this._batchId,
name,
);
}
/**
* Sets the value of the feature's property with the given name.
* <p>
* If a property with the given name doesn't exist, it is created.
* </p>
*
* @param {string} name The case-sensitive name of the property.
* @param {*} value The value of the property that will be copied.
*
* @exception {DeveloperError} Inherited batch table hierarchy property is read only.
*
* @example
* const height = feature.getProperty('Height'); // e.g., the height of a building
*
* @example
* const name = 'clicked';
* if (feature.getProperty(name)) {
* console.log('already clicked');
* } else {
* feature.setProperty(name, true);
* console.log('first click');
* }
*/
setProperty(name, value) {
this._content.batchTable.setProperty(this._batchId, name, value);
// PERFORMANCE_IDEA: Probably overkill, but maybe only mark the tile dirty if the
// property is in one of the style's expressions or - if it can be done quickly -
// if the new property value changed the result of an expression.
this._content.featurePropertiesDirty = true;
}
/**
* Returns whether the feature's class name equals <code>className</code>. Unlike {@link Cesium3DTileFeature#isClass}
* this function only checks the feature's exact class and not inherited classes.
* <p>
* This function returns <code>false</code> if no batch table hierarchy is present.
* </p>
*
* @param {string} className The name to check against.
* @returns {boolean} Whether the feature's class name equals <code>className</code>
*
* @private
*/
isExactClass(className) {
return this._content.batchTable.isExactClass(this._batchId, className);
}
/**
* Returns whether the feature's class or any inherited classes are named <code>className</code>.
* <p>
* This function returns <code>false</code> if no batch table hierarchy is present.
* </p>
*
* @param {string} className The name to check against.
* @returns {boolean} Whether the feature's class or inherited classes are named <code>className</code>
*
* @private
*/
isClass(className) {
return this._content.batchTable.isClass(this._batchId, className);
}
/**
* Returns the feature's class name.
* <p>
* This function returns <code>undefined</code> if no batch table hierarchy is present.
* </p>
*
* @returns {string} The feature's class name.
*
* @private
*/
getExactClassName() {
return this._content.batchTable.getExactClassName(this._batchId);
}
}
export default Cesium3DTileFeature;
+131
View File
@@ -0,0 +1,131 @@
import ComponentDatatype from "../Core/ComponentDatatype.js";
import defined from "../Core/defined.js";
/**
* @private
*/
function Cesium3DTileFeatureTable(featureTableJson, featureTableBinary) {
this.json = featureTableJson;
this.buffer = featureTableBinary;
this._cachedTypedArrays = {};
this.featuresLength = 0;
}
function getTypedArrayFromBinary(
featureTable,
semantic,
componentType,
componentLength,
count,
byteOffset,
) {
const cachedTypedArrays = featureTable._cachedTypedArrays;
let typedArray = cachedTypedArrays[semantic];
if (!defined(typedArray)) {
typedArray = ComponentDatatype.createArrayBufferView(
componentType,
featureTable.buffer.buffer,
featureTable.buffer.byteOffset + byteOffset,
count * componentLength,
);
cachedTypedArrays[semantic] = typedArray;
}
return typedArray;
}
function getTypedArrayFromArray(featureTable, semantic, componentType, array) {
const cachedTypedArrays = featureTable._cachedTypedArrays;
let typedArray = cachedTypedArrays[semantic];
if (!defined(typedArray)) {
typedArray = ComponentDatatype.createTypedArray(componentType, array);
cachedTypedArrays[semantic] = typedArray;
}
return typedArray;
}
Cesium3DTileFeatureTable.prototype.getGlobalProperty = function (
semantic,
componentType,
componentLength,
) {
const jsonValue = this.json[semantic];
if (!defined(jsonValue)) {
return undefined;
}
if (defined(jsonValue.byteOffset)) {
componentType = componentType ?? ComponentDatatype.UNSIGNED_INT;
componentLength = componentLength ?? 1;
return getTypedArrayFromBinary(
this,
semantic,
componentType,
componentLength,
1,
jsonValue.byteOffset,
);
}
return jsonValue;
};
Cesium3DTileFeatureTable.prototype.hasProperty = function (semantic) {
return defined(this.json[semantic]);
};
Cesium3DTileFeatureTable.prototype.getPropertyArray = function (
semantic,
componentType,
componentLength,
) {
const jsonValue = this.json[semantic];
if (!defined(jsonValue)) {
return undefined;
}
if (defined(jsonValue.byteOffset)) {
if (defined(jsonValue.componentType)) {
componentType = ComponentDatatype.fromName(jsonValue.componentType);
}
return getTypedArrayFromBinary(
this,
semantic,
componentType,
componentLength,
this.featuresLength,
jsonValue.byteOffset,
);
}
return getTypedArrayFromArray(this, semantic, componentType, jsonValue);
};
Cesium3DTileFeatureTable.prototype.getProperty = function (
semantic,
componentType,
componentLength,
featureId,
result,
) {
const jsonValue = this.json[semantic];
if (!defined(jsonValue)) {
return undefined;
}
const typedArray = this.getPropertyArray(
semantic,
componentType,
componentLength,
);
if (componentLength === 1) {
return typedArray[featureId];
}
for (let i = 0; i < componentLength; ++i) {
result[i] = typedArray[componentLength * featureId + i];
}
return result;
};
export default Cesium3DTileFeatureTable;
@@ -0,0 +1,18 @@
// @ts-check
/**
* Hint defining optimization support for a 3D tile
*
* @enum {number}
*
* @private
*/
const Cesium3DTileOptimizationHint = {
NOT_COMPUTED: -1,
USE_OPTIMIZATION: 1,
SKIP_OPTIMIZATION: 0,
};
Object.freeze(Cesium3DTileOptimizationHint);
export default Cesium3DTileOptimizationHint;
+113
View File
@@ -0,0 +1,113 @@
import Cartesian3 from "../Core/Cartesian3.js";
import Check from "../Core/Check.js";
import Cesium3DTileOptimizationHint from "./Cesium3DTileOptimizationHint.js";
import TileBoundingRegion from "./TileBoundingRegion.js";
import TileOrientedBoundingBox from "./TileOrientedBoundingBox.js";
/**
* Utility functions for computing optimization hints for a {@link Cesium3DTileset}.
*
* @namespace Cesium3DTileOptimizations
*
* @private
*/
const Cesium3DTileOptimizations = {};
const scratchAxis = new Cartesian3();
/**
* Evaluates support for the childrenWithinParent optimization. This is used to more tightly cull tilesets if
* children bounds are fully contained within the parent. Currently, support for the optimization only works for
* oriented bounding boxes, so both the child and parent tile must be either a {@link TileOrientedBoundingBox} or
* {@link TileBoundingRegion}. The purpose of this check is to prevent use of a culling optimization when the child
* bounds exceed those of the parent. If the child bounds are greater, it is more likely that the optimization will
* waste CPU cycles. Bounding spheres are not supported for the reason that the child bounds can very often be
* partially outside of the parent bounds.
*
* @param {Cesium3DTile} tile The tile to check.
* @returns {boolean} Whether the childrenWithinParent optimization is supported.
*/
Cesium3DTileOptimizations.checkChildrenWithinParent = function (tile) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("tile", tile);
//>>includeEnd('debug');
const children = tile.children;
const length = children.length;
// Check if the parent has an oriented bounding box.
const boundingVolume = tile.boundingVolume;
if (
boundingVolume instanceof TileOrientedBoundingBox ||
boundingVolume instanceof TileBoundingRegion
) {
const orientedBoundingBox = boundingVolume._orientedBoundingBox;
tile._optimChildrenWithinParent =
Cesium3DTileOptimizationHint.USE_OPTIMIZATION;
for (let i = 0; i < length; ++i) {
const child = children[i];
// Check if the child has an oriented bounding box.
const childBoundingVolume = child.boundingVolume;
if (!(
childBoundingVolume instanceof TileOrientedBoundingBox ||
childBoundingVolume instanceof TileBoundingRegion
)) {
// Do not support if the parent and child both do not have oriented bounding boxes.
tile._optimChildrenWithinParent =
Cesium3DTileOptimizationHint.SKIP_OPTIMIZATION;
break;
}
const childOrientedBoundingBox = childBoundingVolume._orientedBoundingBox;
// Compute the axis from the parent to the child.
const axis = Cartesian3.subtract(
childOrientedBoundingBox.center,
orientedBoundingBox.center,
scratchAxis,
);
const axisLength = Cartesian3.magnitude(axis);
Cartesian3.divideByScalar(axis, axisLength, axis);
// Project the bounding box of the parent onto the axis. Because the axis is a ray from the parent
// to the child, the projection parameterized along the ray will be (+/- proj1).
const proj1 =
Math.abs(orientedBoundingBox.halfAxes[0] * axis.x) +
Math.abs(orientedBoundingBox.halfAxes[1] * axis.y) +
Math.abs(orientedBoundingBox.halfAxes[2] * axis.z) +
Math.abs(orientedBoundingBox.halfAxes[3] * axis.x) +
Math.abs(orientedBoundingBox.halfAxes[4] * axis.y) +
Math.abs(orientedBoundingBox.halfAxes[5] * axis.z) +
Math.abs(orientedBoundingBox.halfAxes[6] * axis.x) +
Math.abs(orientedBoundingBox.halfAxes[7] * axis.y) +
Math.abs(orientedBoundingBox.halfAxes[8] * axis.z);
// Project the bounding box of the child onto the axis. Because the axis is a ray from the parent
// to the child, the projection parameterized along the ray will be (+/- proj2) + axis.length.
const proj2 =
Math.abs(childOrientedBoundingBox.halfAxes[0] * axis.x) +
Math.abs(childOrientedBoundingBox.halfAxes[1] * axis.y) +
Math.abs(childOrientedBoundingBox.halfAxes[2] * axis.z) +
Math.abs(childOrientedBoundingBox.halfAxes[3] * axis.x) +
Math.abs(childOrientedBoundingBox.halfAxes[4] * axis.y) +
Math.abs(childOrientedBoundingBox.halfAxes[5] * axis.z) +
Math.abs(childOrientedBoundingBox.halfAxes[6] * axis.x) +
Math.abs(childOrientedBoundingBox.halfAxes[7] * axis.y) +
Math.abs(childOrientedBoundingBox.halfAxes[8] * axis.z);
// If the child extends the parent's bounds, the optimization is not valid and we skip it.
if (proj1 <= proj2 + axisLength) {
tile._optimChildrenWithinParent =
Cesium3DTileOptimizationHint.SKIP_OPTIMIZATION;
break;
}
}
}
return (
tile._optimChildrenWithinParent ===
Cesium3DTileOptimizationHint.USE_OPTIMIZATION
);
};
export default Cesium3DTileOptimizations;
+83
View File
@@ -0,0 +1,83 @@
/**
* The pass in which a 3D Tileset is updated.
*
* @enum {number}
* @private
*/
const Cesium3DTilePass = {
RENDER: 0,
PICK: 1,
SHADOW: 2,
PRELOAD: 3,
PRELOAD_FLIGHT: 4,
REQUEST_RENDER_MODE_DEFER_CHECK: 5,
MOST_DETAILED_PRELOAD: 6,
MOST_DETAILED_PICK: 7,
NUMBER_OF_PASSES: 8,
};
const passOptions = new Array(Cesium3DTilePass.NUMBER_OF_PASSES);
passOptions[Cesium3DTilePass.RENDER] = Object.freeze({
pass: Cesium3DTilePass.RENDER,
isRender: true,
requestTiles: true,
ignoreCommands: false,
});
passOptions[Cesium3DTilePass.PICK] = Object.freeze({
pass: Cesium3DTilePass.PICK,
isRender: false,
requestTiles: false,
ignoreCommands: false,
});
passOptions[Cesium3DTilePass.SHADOW] = Object.freeze({
pass: Cesium3DTilePass.SHADOW,
isRender: false,
requestTiles: true,
ignoreCommands: false,
});
passOptions[Cesium3DTilePass.PRELOAD] = Object.freeze({
pass: Cesium3DTilePass.PRELOAD,
isRender: false,
requestTiles: true,
ignoreCommands: true,
});
passOptions[Cesium3DTilePass.PRELOAD_FLIGHT] = Object.freeze({
pass: Cesium3DTilePass.PRELOAD_FLIGHT,
isRender: false,
requestTiles: true,
ignoreCommands: true,
});
passOptions[Cesium3DTilePass.REQUEST_RENDER_MODE_DEFER_CHECK] = Object.freeze({
pass: Cesium3DTilePass.REQUEST_RENDER_MODE_DEFER_CHECK,
isRender: false,
requestTiles: true,
ignoreCommands: true,
});
passOptions[Cesium3DTilePass.MOST_DETAILED_PRELOAD] = Object.freeze({
pass: Cesium3DTilePass.MOST_DETAILED_PRELOAD,
isRender: false,
requestTiles: true,
ignoreCommands: true,
});
passOptions[Cesium3DTilePass.MOST_DETAILED_PICK] = Object.freeze({
pass: Cesium3DTilePass.MOST_DETAILED_PICK,
isRender: false,
requestTiles: false,
ignoreCommands: false,
});
Cesium3DTilePass.getPassOptions = function (pass) {
return passOptions[pass];
};
Object.freeze(Cesium3DTilePass);
export default Cesium3DTilePass;
+52
View File
@@ -0,0 +1,52 @@
import Check from "../Core/Check.js";
/**
* The state for a 3D Tiles update pass.
*
* @private
* @constructor
*/
function Cesium3DTilePassState(options) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options", options);
Check.typeOf.number("options.pass", options.pass);
//>>includeEnd('debug');
/**
* The pass.
*
* @type {Cesium3DTilePass}
*/
this.pass = options.pass;
/**
* An array of rendering commands to use instead of {@link FrameState.commandList} for the current pass.
*
* @type {DrawCommand[]}
*/
this.commandList = options.commandList;
/**
* A camera to use instead of {@link FrameState.camera} for the current pass.
*
* @type {Camera}
*/
this.camera = options.camera;
/**
* A culling volume to use instead of {@link FrameState.cullingVolume} for the current pass.
*
* @type {CullingVolume}
*/
this.cullingVolume = options.cullingVolume;
/**
* A read-only property that indicates whether the pass is ready, i.e. all tiles needed by the pass are loaded.
*
* @type {boolean}
* @readonly
* @default false
*/
this.ready = false;
}
export default Cesium3DTilePassState;
+778
View File
@@ -0,0 +1,778 @@
import Cartographic from "../Core/Cartographic.js";
import Color from "../Core/Color.js";
import defined from "../Core/defined.js";
import Cesium3DTileFeature from "./Cesium3DTileFeature.js";
import createBillboardPointCallback from "./createBillboardPointCallback.js";
/** @import Billboard from "./Billboard.js"; */
/** @import Cesium3DTileContent from "./Cesium3DTileContent.js"; */
/** @import Cesium3DTileset from "./Cesium3DTileset.js"; */
/** @import DistanceDisplayCondition from "../Core/DistanceDisplayCondition.js"; */
/** @import HorizontalOrigin from "./HorizontalOrigin.js"; */
/** @import Label from "./Label.js"; */
/** @import NearFarScalar from "../Core/NearFarScalar.js"; */
/** @import Polyline from "./Polyline.js"; */
/** @import VerticalOrigin from "./VerticalOrigin.js"; */
/** @ignore */
const scratchCartographic = new Cartographic();
/**
* A point feature of a {@link Cesium3DTileset}.
* <p>
* Provides access to a feature's properties stored in the tile's batch table, as well
* as the ability to show/hide a feature and change its point properties
* </p>
* <p>
* Modifications to a <code>Cesium3DTilePointFeature</code> object have the lifetime of the tile's
* content. If the tile's content is unloaded, e.g., due to it going out of view and needing
* to free space in the cache for visible tiles, listen to the {@link Cesium3DTileset#tileUnload} event to save any
* modifications. Also listen to the {@link Cesium3DTileset#tileVisible} event to reapply any modifications.
* </p>
* <p>
* Do not construct this directly. Access it through {@link Cesium3DTileContent#getFeature}
* or picking using {@link Scene#pick} and {@link Scene#pickPosition}.
* </p>
*
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*
* @example
* // On mouse over, display all the properties for a feature in the console log.
* handler.setInputAction(function(movement) {
* const feature = scene.pick(movement.endPosition);
* if (feature instanceof Cesium.Cesium3DTilePointFeature) {
* const propertyIds = feature.getPropertyIds();
* const length = propertyIds.length;
* for (let i = 0; i < length; ++i) {
* const propertyId = propertyIds[i];
* console.log(`{propertyId}: ${feature.getProperty(propertyId)}`);
* }
* }
* }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
*/
class Cesium3DTilePointFeature {
static defaultColor = Color.WHITE;
static defaultPointOutlineColor = Color.BLACK;
static defaultPointOutlineWidth = 0.0;
static defaultPointSize = 8.0;
/**
* @param {Cesium3DTileContent} content
* @param {number} batchId
* @param {Billboard} billboard
* @param {Label} label
* @param {Polyline} polyline
*/
constructor(content, batchId, billboard, label, polyline) {
this._content = content;
this._billboard = billboard;
this._label = label;
this._polyline = polyline;
this._batchId = batchId;
this._billboardImage = undefined;
this._billboardColor = undefined;
this._billboardOutlineColor = undefined;
this._billboardOutlineWidth = undefined;
this._billboardSize = undefined;
this._pointSize = undefined;
this._color = undefined;
this._pointSize = undefined;
this._pointOutlineColor = undefined;
this._pointOutlineWidth = undefined;
this._heightOffset = undefined;
this._pickIds = new Array(3);
setBillboardImage(this);
}
/**
* Gets or sets if the feature will be shown. This is set for all features
* when a style's show is evaluated.
*
* @type {boolean}
*
* @default true
*/
get show() {
return this._label.show;
}
set show(value) {
this._label.show = value;
this._billboard.show = value;
this._polyline.show = value;
}
/**
* Gets or sets the color of the point of this feature.
* <p>
* Only applied when <code>image</code> is <code>undefined</code>.
* </p>
*
* @type {Color}
*/
get color() {
return this._color;
}
set color(value) {
this._color = Color.clone(value, this._color);
setBillboardImage(this);
}
/**
* Gets or sets the point size of this feature.
* <p>
* Only applied when <code>image</code> is <code>undefined</code>.
* </p>
*
* @type {number}
*/
get pointSize() {
return this._pointSize;
}
set pointSize(value) {
this._pointSize = value;
setBillboardImage(this);
}
/**
* Gets or sets the point outline color of this feature.
* <p>
* Only applied when <code>image</code> is <code>undefined</code>.
* </p>
*
* @type {Color}
*/
get pointOutlineColor() {
return this._pointOutlineColor;
}
set pointOutlineColor(value) {
this._pointOutlineColor = Color.clone(value, this._pointOutlineColor);
setBillboardImage(this);
}
/**
* Gets or sets the point outline width in pixels of this feature.
* <p>
* Only applied when <code>image</code> is <code>undefined</code>.
* </p>
*
* @type {number}
*/
get pointOutlineWidth() {
return this._pointOutlineWidth;
}
set pointOutlineWidth(value) {
this._pointOutlineWidth = value;
setBillboardImage(this);
}
/**
* Gets or sets the label color of this feature.
* <p>
* The color will be applied to the label if <code>labelText</code> is defined.
* </p>
*
* @type {Color}
*/
get labelColor() {
return this._label.fillColor;
}
set labelColor(value) {
this._label.fillColor = value;
this._polyline.show = this._label.show && value.alpha > 0.0;
}
/**
* Gets or sets the label outline color of this feature.
* <p>
* The outline color will be applied to the label if <code>labelText</code> is defined.
* </p>
*
* @type {Color}
*/
get labelOutlineColor() {
return this._label.outlineColor;
}
set labelOutlineColor(value) {
this._label.outlineColor = value;
}
/**
* Gets or sets the outline width in pixels of this feature.
* <p>
* The outline width will be applied to the point if <code>labelText</code> is defined.
* </p>
*
* @type {number}
*/
get labelOutlineWidth() {
return this._label.outlineWidth;
}
set labelOutlineWidth(value) {
this._label.outlineWidth = value;
}
/**
* Gets or sets the font of this feature.
* <p>
* Only applied when the <code>labelText</code> is defined.
* </p>
*
* @type {string}
*/
get font() {
return this._label.font;
}
set font(value) {
this._label.font = value;
}
/**
* Gets or sets the fill and outline style of this feature.
* <p>
* Only applied when <code>labelText</code> is defined.
* </p>
*
* @type {LabelStyle}
*/
get labelStyle() {
return this._label.style;
}
set labelStyle(value) {
this._label.style = value;
}
/**
* Gets or sets the text for this feature.
*
* @type {string}
*/
get labelText() {
return this._label.text;
}
set labelText(value) {
if (!defined(value)) {
value = "";
}
this._label.text = value;
}
/**
* Gets or sets the background color of the text for this feature.
* <p>
* Only applied when <code>labelText</code> is defined.
* </p>
*
* @type {Color}
*/
get backgroundColor() {
return this._label.backgroundColor;
}
set backgroundColor(value) {
this._label.backgroundColor = value;
}
/**
* Gets or sets the background padding of the text for this feature.
* <p>
* Only applied when <code>labelText</code> is defined.
* </p>
*
* @type {Cartesian2}
*/
get backgroundPadding() {
return this._label.backgroundPadding;
}
set backgroundPadding(value) {
this._label.backgroundPadding = value;
}
/**
* Gets or sets whether to display the background of the text for this feature.
* <p>
* Only applied when <code>labelText</code> is defined.
* </p>
*
* @type {boolean}
*/
get backgroundEnabled() {
return this._label.showBackground;
}
set backgroundEnabled(value) {
this._label.showBackground = value;
}
/**
* Gets or sets the near and far scaling properties for this feature.
*
* @type {NearFarScalar}
*/
get scaleByDistance() {
return this._label.scaleByDistance;
}
set scaleByDistance(value) {
this._label.scaleByDistance = value;
this._billboard.scaleByDistance = value;
}
/**
* Gets or sets the near and far translucency properties for this feature.
*
* @type {NearFarScalar}
*/
get translucencyByDistance() {
return this._label.translucencyByDistance;
}
set translucencyByDistance(value) {
this._label.translucencyByDistance = value;
this._billboard.translucencyByDistance = value;
}
/**
* Gets or sets the condition specifying at what distance from the camera that this feature will be displayed.
*
* @type {DistanceDisplayCondition}
*/
get distanceDisplayCondition() {
return this._label.distanceDisplayCondition;
}
set distanceDisplayCondition(value) {
this._label.distanceDisplayCondition = value;
this._polyline.distanceDisplayCondition = value;
this._billboard.distanceDisplayCondition = value;
}
/**
* Gets or sets the height offset in meters of this feature.
*
* @type {number}
*/
get heightOffset() {
return this._heightOffset;
}
set heightOffset(value) {
const offset = this._heightOffset ?? 0.0;
const ellipsoid = this._content.tileset.ellipsoid;
const cart = ellipsoid.cartesianToCartographic(
this._billboard.position,
scratchCartographic,
);
cart.height = cart.height - offset + value;
const newPosition = ellipsoid.cartographicToCartesian(cart);
this._billboard.position = newPosition;
this._label.position = this._billboard.position;
this._polyline.positions = [this._polyline.positions[0], newPosition];
this._heightOffset = value;
}
/**
* Gets or sets whether the anchor line is displayed.
* <p>
* Only applied when <code>heightOffset</code> is defined.
* </p>
*
* @type {boolean}
*/
get anchorLineEnabled() {
return this._polyline.show;
}
set anchorLineEnabled(value) {
this._polyline.show = value;
}
/**
* Gets or sets the color for the anchor line.
* <p>
* Only applied when <code>heightOffset</code> is defined.
* </p>
*
* @type {Color}
*/
get anchorLineColor() {
return this._polyline.material.uniforms.color;
}
set anchorLineColor(value) {
this._polyline.material.uniforms.color = Color.clone(
value,
this._polyline.material.uniforms.color,
);
}
/**
* Gets or sets the image of this feature.
*
* @type {string}
*/
get image() {
return this._billboardImage;
}
set image(value) {
const imageChanged = this._billboardImage !== value;
this._billboardImage = value;
if (imageChanged) {
setBillboardImage(this);
}
}
/**
* Gets or sets the distance where depth testing will be disabled.
*
* @type {number}
*/
get disableDepthTestDistance() {
return this._label.disableDepthTestDistance;
}
set disableDepthTestDistance(value) {
this._label.disableDepthTestDistance = value;
this._billboard.disableDepthTestDistance = value;
}
/**
* Gets or sets the horizontal origin of this point, which determines if the point is
* to the left, center, or right of its anchor position.
*
* @type {HorizontalOrigin}
*/
get horizontalOrigin() {
return this._billboard.horizontalOrigin;
}
set horizontalOrigin(value) {
this._billboard.horizontalOrigin = value;
}
/**
* Gets or sets the vertical origin of this point, which determines if the point is
* to the bottom, center, or top of its anchor position.
*
* @type {VerticalOrigin}
*/
get verticalOrigin() {
return this._billboard.verticalOrigin;
}
set verticalOrigin(value) {
this._billboard.verticalOrigin = value;
}
/**
* Gets or sets the horizontal origin of this point's text, which determines if the point's text is
* to the left, center, or right of its anchor position.
*
* @type {HorizontalOrigin}
*/
get labelHorizontalOrigin() {
return this._label.horizontalOrigin;
}
set labelHorizontalOrigin(value) {
this._label.horizontalOrigin = value;
}
/**
* Get or sets the vertical origin of this point's text, which determines if the point's text is
* to the bottom, center, top, or baseline of it's anchor point.
*
* @type {VerticalOrigin}
*/
get labelVerticalOrigin() {
return this._label.verticalOrigin;
}
set labelVerticalOrigin(value) {
this._label.verticalOrigin = value;
}
/**
* Gets the content of the tile containing the feature.
*
* @type {Cesium3DTileContent}
*
* @readonly
* @private
*/
get content() {
return this._content;
}
/**
* Gets the tileset containing the feature.
*
* @type {Cesium3DTileset}
*
* @readonly
*/
get tileset() {
return this._content.tileset;
}
/**
* All objects returned by {@link Scene#pick} have a <code>primitive</code> property. This returns
* the tileset containing the feature.
*
* @type {Cesium3DTileset}
*
* @readonly
*/
get primitive() {
return this._content.tileset;
}
/**
* @private
*/
get pickIds() {
const ids = this._pickIds;
ids[0] = this._billboard.pickId;
ids[1] = this._label.pickId;
ids[2] = this._polyline.pickId;
return ids;
}
/**
* Returns whether the feature contains this property. This includes properties from this feature's
* class and inherited classes when using a batch table hierarchy.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy}
*
* @param {string} name The case-sensitive name of the property.
* @returns {boolean} Whether the feature contains this property.
*/
hasProperty(name) {
return this._content.batchTable.hasProperty(this._batchId, name);
}
/**
* Returns an array of property IDs for the feature. This includes properties from this feature's
* class and inherited classes when using a batch table hierarchy.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy}
*
* @param {string[]} [results] An array into which to store the results.
* @returns {string[]} The IDs of the feature's properties.
*/
getPropertyIds(results) {
return this._content.batchTable.getPropertyIds(this._batchId, results);
}
/**
* Returns a copy of the value of the feature's property with the given name. This includes properties from this feature's
* class and inherited classes when using a batch table hierarchy.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy}
*
* @param {string} name The case-sensitive name of the property.
* @returns {*} The value of the property or <code>undefined</code> if the feature does not have this property.
*
* @example
* // Display all the properties for a feature in the console log.
* const propertyIds = feature.getPropertyIds();
* const length = propertyIds.length;
* for (let i = 0; i < length; ++i) {
* const propertyId = propertyIds[i];
* console.log(`{propertyId} : ${feature.getProperty(propertyId)}`);
* }
*/
getProperty(name) {
return this._content.batchTable.getProperty(this._batchId, name);
}
/**
* Returns a copy of the value of the feature's property with the given name.
* If the feature is contained within a tileset that has metadata (3D Tiles 1.1)
* or uses the <code>3DTILES_metadata</code> extension, tileset, group and tile metadata is
* inherited.
* <p>
* To resolve name conflicts, this method resolves names from most specific to
* least specific by metadata granularity in the order: feature, tile, group,
* tileset. Within each granularity, semantics are resolved first, then other
* properties.
* </p>
* @param {string} name The case-sensitive name of the property.
* @returns {*} The value of the property or <code>undefined</code> if the feature does not have this property.
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
getPropertyInherited(name) {
return Cesium3DTileFeature.getPropertyInherited(
this._content,
this._batchId,
name,
);
}
/**
* Sets the value of the feature's property with the given name.
* <p>
* If a property with the given name doesn't exist, it is created.
* </p>
*
* @param {string} name The case-sensitive name of the property.
* @param {*} value The value of the property that will be copied.
*
* @exception {DeveloperError} Inherited batch table hierarchy property is read only.
*
* @example
* const height = feature.getProperty('Height'); // e.g., the height of a building
*
* @example
* const name = 'clicked';
* if (feature.getProperty(name)) {
* console.log('already clicked');
* } else {
* feature.setProperty(name, true);
* console.log('first click');
* }
*/
setProperty(name, value) {
this._content.batchTable.setProperty(this._batchId, name, value);
// PERFORMANCE_IDEA: Probably overkill, but maybe only mark the tile dirty if the
// property is in one of the style's expressions or - if it can be done quickly -
// if the new property value changed the result of an expression.
this._content.featurePropertiesDirty = true;
}
/**
* Returns whether the feature's class name equals <code>className</code>. Unlike {@link Cesium3DTilePointFeature#isClass}
* this function only checks the feature's exact class and not inherited classes.
* <p>
* This function returns <code>false</code> if no batch table hierarchy is present.
* </p>
*
* @param {string} className The name to check against.
* @returns {boolean} Whether the feature's class name equals <code>className</code>
*
* @private
*/
isExactClass(className) {
return this._content.batchTable.isExactClass(this._batchId, className);
}
/**
* Returns whether the feature's class or any inherited classes are named <code>className</code>.
* <p>
* This function returns <code>false</code> if no batch table hierarchy is present.
* </p>
*
* @param {string} className The name to check against.
* @returns {boolean} Whether the feature's class or inherited classes are named <code>className</code>
*
* @private
*/
isClass(className) {
return this._content.batchTable.isClass(this._batchId, className);
}
/**
* Returns the feature's class name.
* <p>
* This function returns <code>undefined</code> if no batch table hierarchy is present.
* </p>
*
* @returns {string} The feature's class name.
*
* @private
*/
getExactClassName() {
return this._content.batchTable.getExactClassName(this._batchId);
}
}
/**
* @param {Cesium3DTilePointFeature} feature
* @ignore
*/
function setBillboardImage(feature) {
const b = feature._billboard;
if (defined(feature._billboardImage) && feature._billboardImage !== b.image) {
b.image = feature._billboardImage;
return;
}
if (defined(feature._billboardImage)) {
return;
}
const newColor = feature._color ?? Cesium3DTilePointFeature.defaultColor;
const newOutlineColor =
feature._pointOutlineColor ??
Cesium3DTilePointFeature.defaultPointOutlineColor;
const newOutlineWidth =
feature._pointOutlineWidth ??
Cesium3DTilePointFeature.defaultPointOutlineWidth;
const newPointSize =
feature._pointSize ?? Cesium3DTilePointFeature.defaultPointSize;
const currentColor = feature._billboardColor;
const currentOutlineColor = feature._billboardOutlineColor;
const currentOutlineWidth = feature._billboardOutlineWidth;
const currentPointSize = feature._billboardSize;
if (
Color.equals(newColor, currentColor) &&
Color.equals(newOutlineColor, currentOutlineColor) &&
newOutlineWidth === currentOutlineWidth &&
newPointSize === currentPointSize
) {
return;
}
feature._billboardColor = Color.clone(newColor, feature._billboardColor);
feature._billboardOutlineColor = Color.clone(
newOutlineColor,
feature._billboardOutlineColor,
);
feature._billboardOutlineWidth = newOutlineWidth;
feature._billboardSize = newPointSize;
const centerAlpha = newColor.alpha;
const cssColor = newColor.toCssColorString();
const cssOutlineColor = newOutlineColor.toCssColorString();
const textureId = JSON.stringify([
cssColor,
newPointSize,
cssOutlineColor,
newOutlineWidth,
]);
b.setImage(
textureId,
createBillboardPointCallback(
centerAlpha,
cssColor,
cssOutlineColor,
newOutlineWidth,
newPointSize,
),
);
}
export default Cesium3DTilePointFeature;
+34
View File
@@ -0,0 +1,34 @@
// @ts-check
/**
* The refinement approach for a tile.
* <p>
* See the {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification#refinement|Refinement}
* in the 3D Tiles spec.
* </p>
*
* @enum {number}
*
* @private
*/
const Cesium3DTileRefine = {
/**
* Render this tile and, if it doesn't meet the screen space error, also refine to its children.
*
* @type {number}
* @constant
*/
ADD: 0,
/**
* Render this tile or, if it doesn't meet the screen space error, refine to its descendants instead.
*
* @type {number}
* @constant
*/
REPLACE: 1,
};
Object.freeze(Cesium3DTileRefine);
export default Cesium3DTileRefine;
File diff suppressed because it is too large Load Diff
+78
View File
@@ -0,0 +1,78 @@
import defined from "../Core/defined.js";
/**
* @private
*/
function Cesium3DTileStyleEngine() {
this._style = undefined; // The style provided by the user
this._styleDirty = false; // true when the style is reassigned
this._lastStyleTime = 0; // The "time" when the last style was assigned
}
Object.defineProperties(Cesium3DTileStyleEngine.prototype, {
style: {
get: function () {
return this._style;
},
set: function (value) {
if (value === this._style) {
return;
}
this._style = value;
this._styleDirty = true;
},
},
});
Cesium3DTileStyleEngine.prototype.makeDirty = function () {
this._styleDirty = true;
};
Cesium3DTileStyleEngine.prototype.resetDirty = function () {
this._styleDirty = false;
};
Cesium3DTileStyleEngine.prototype.applyStyle = function (tileset) {
if (!defined(tileset.root)) {
return;
}
if (defined(this._style) && !this._style._ready) {
return;
}
const styleDirty = this._styleDirty;
if (styleDirty) {
// Increase "time", so the style is applied to all visible tiles
++this._lastStyleTime;
}
const lastStyleTime = this._lastStyleTime;
const statistics = tileset._statistics;
// If a new style was assigned, loop through all the visible tiles; otherwise, loop through
// only the tiles that are newly visible, i.e., they are visible this frame, but were not
// visible last frame. In many cases, the newly selected tiles list will be short or empty.
const tiles = styleDirty
? tileset._selectedTiles
: tileset._selectedTilesToStyle;
// PERFORMANCE_IDEA: does mouse-over picking basically trash this? We need to style on
// pick, for example, because a feature's show may be false.
const length = tiles.length;
for (let i = 0; i < length; ++i) {
const tile = tiles[i];
if (tile.lastStyleTime !== lastStyleTime) {
// Apply the style to this tile if it wasn't already applied because:
// 1) the user assigned a new style to the tileset
// 2) this tile is now visible, but it wasn't visible when the style was first assigned
const content = tile.content;
tile.lastStyleTime = lastStyleTime;
content.applyStyle(this._style);
statistics.numberOfFeaturesStyled += content.featuresLength;
++statistics.numberOfTilesStyled;
}
}
};
export default Cesium3DTileStyleEngine;
+586
View File
@@ -0,0 +1,586 @@
// @ts-check
import DeveloperError from "../Core/DeveloperError.js";
import BufferPoint from "./BufferPoint.js";
import BufferPointCollection from "./BufferPointCollection.js";
import BufferPointMaterial from "./BufferPointMaterial.js";
import BufferPolygon from "./BufferPolygon.js";
import BufferPolygonCollection from "./BufferPolygonCollection.js";
import BufferPolygonMaterial from "./BufferPolygonMaterial.js";
import BufferPolyline from "./BufferPolyline.js";
import BufferPolylineCollection from "./BufferPolylineCollection.js";
import BufferPolylineMaterial from "./BufferPolylineMaterial.js";
import Cesium3DTileFeature from "./Cesium3DTileFeature.js";
import Color from "../Core/Color.js";
import defined from "../Core/defined.js";
/** @import BufferPrimitive from "./BufferPrimitive.js"; */
/** @import BufferPrimitiveMaterial from "./BufferPrimitiveMaterial.js"; */
/** @import Cesium3DTileBatchTable from "./Cesium3DTileBatchTable.js"; */
/** @import Cesium3DTileContent from "./Cesium3DTileContent.js"; */
/** @import Cesium3DTileset from "./Cesium3DTileset.js"; */
/** @import VectorGltf3DTileContent from "./VectorGltf3DTileContent.js"; */
const point = new BufferPoint();
const polyline = new BufferPolyline();
const polygon = new BufferPolygon();
const pointMaterial = new BufferPointMaterial();
const polylineMaterial = new BufferPolylineMaterial();
const polygonMaterial = new BufferPolygonMaterial();
/**
* A vector feature of a {@link Cesium3DTileset}.
* <p>
* Provides access to a feature's properties stored in the tile's batch table, as well
* as the ability to show/hide and style the feature
* </p>
* <p>
* Modifications to a <code>Cesium3DTileVectorFeature</code> object have the lifetime of the tile's
* content. If the tile's content is unloaded, e.g., due to it going out of view and needing
* to free space in the cache for visible tiles, listen to the {@link Cesium3DTileset#tileUnload} event to save any
* modifications. Also listen to the {@link Cesium3DTileset#tileVisible} event to reapply any modifications.
* </p>
* <p>
* Do not construct this directly. Access it through {@link Cesium3DTileContent#getFeature}
* or picking using {@link Scene#pick} and {@link Scene#pickPosition}.
* </p>
*
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*
* @example
* // On mouse over, display all the properties for a feature in the console log.
* handler.setInputAction(function(movement) {
* const feature = scene.pick(movement.endPosition);
* if (feature instanceof Cesium.Cesium3DTileVectorFeature) {
* const propertyIds = feature.getPropertyIds();
* const length = propertyIds.length;
* for (let i = 0; i < length; ++i) {
* const propertyId = propertyIds[i];
* console.log(`{propertyId}: ${feature.getProperty(propertyId)}`);
* }
* }
* }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
*
* @ignore
*/
class Cesium3DTileVectorFeature {
/** @private */
_color = new Color();
/** @private */
_outlineColor = new Color();
/**
* @param {VectorGltf3DTileContent} content
* @param {number} batchId
* @param {number} [batchTableId=0]
*/
constructor(content, batchId, batchTableId = 0) {
this._content = content;
this._batchId = batchId;
this._batchTableId = batchTableId;
/**
* For each collection index N, this map returns the indices of all
* primitive in the collection associated with this feature.
* @type {Map<number, number[]>}
* @private
*/
this._primitivesByCollection = new Map();
}
/**
* @param {number} collectionIndex
* @param {number} primitiveIndex
*/
addPrimitiveByCollection(collectionIndex, primitiveIndex) {
let primitiveIndices = this._primitivesByCollection.get(collectionIndex);
if (!primitiveIndices) {
primitiveIndices = [];
this._primitivesByCollection.set(collectionIndex, primitiveIndices);
}
primitiveIndices.push(primitiveIndex);
}
/**
* @type {boolean}
* @default true
*/
get show() {
for (const prim of this._iteratePrimitives()) {
if (prim.show) {
return true;
}
}
return false;
}
set show(value) {
for (const prim of this._iteratePrimitives()) {
prim.show = value;
}
}
/**
* @type {Color}
* @default Color.WHITE
*/
get color() {
for (const material of this._iterateMaterials()) {
return Color.clone(material.color, this._color);
}
return Color.clone(Color.WHITE, this._color);
}
set color(value) {
for (const material of this._iterateMaterials()) {
Color.clone(value, material.color);
}
}
/**
* @type {number}
* @default 1
*/
get pointSize() {
for (const material of this._iteratePointMaterials()) {
return material.size;
}
return 1;
}
set pointSize(value) {
for (const material of this._iteratePointMaterials()) {
material.size = value;
}
}
/**
* @type {Color}
* @default Color.WHITE
*/
get pointOutlineColor() {
for (const material of this._iteratePointMaterials()) {
return Color.clone(material.outlineColor, this._outlineColor);
}
return Color.clone(Color.WHITE, this._outlineColor);
}
set pointOutlineColor(value) {
for (const material of this._iteratePointMaterials()) {
Color.clone(value, material.outlineColor);
}
}
/**
* @type {number}
* @default 0
*/
get pointOutlineWidth() {
for (const material of this._iteratePointMaterials()) {
return material.outlineWidth;
}
return 0;
}
set pointOutlineWidth(value) {
for (const material of this._iteratePointMaterials()) {
material.outlineWidth = value;
}
}
/**
* @type {number}
* @default 1
*/
get lineWidth() {
for (const material of this._iteratePolylineMaterials()) {
return material.width;
}
return 1;
}
set lineWidth(value) {
for (const material of this._iteratePolylineMaterials()) {
material.width = value;
}
}
/**
* @type {Color}
* @default Color.WHITE
*/
get lineOutlineColor() {
for (const material of this._iteratePolylineMaterials()) {
return Color.clone(material.outlineColor, this._outlineColor);
}
return Color.clone(Color.WHITE, this._outlineColor);
}
set lineOutlineColor(value) {
for (const material of this._iteratePolylineMaterials()) {
Color.clone(value, material.outlineColor);
}
}
/**
* @type {number}
* @default 0
*/
get lineOutlineWidth() {
for (const material of this._iteratePolylineMaterials()) {
return material.outlineWidth;
}
return 0;
}
set lineOutlineWidth(value) {
for (const material of this._iteratePolylineMaterials()) {
material.outlineWidth = value;
}
}
/**
* @type {Color}
* @default Color.WHITE
*/
get polygonOutlineColor() {
for (const material of this._iteratePolygonMaterials()) {
return Color.clone(material.outlineColor, this._outlineColor);
}
return Color.clone(Color.WHITE, this._outlineColor);
}
set polygonOutlineColor(value) {
for (const material of this._iteratePolygonMaterials()) {
Color.clone(value, material.outlineColor);
}
}
/**
* @type {number}
* @default 0
*/
get polygonOutlineWidth() {
for (const material of this._iteratePolygonMaterials()) {
return material.outlineWidth;
}
return 0;
}
set polygonOutlineWidth(value) {
for (const material of this._iteratePolygonMaterials()) {
material.outlineWidth = value;
}
}
/**
* Gets the content of the tile containing the feature.
*
* @type {VectorGltf3DTileContent}
*
* @ignore
*/
get content() {
return this._content;
}
/**
* Gets the tileset containing the feature.
*
* @type {Cesium3DTileset}
*/
get tileset() {
return this._content.tileset;
}
/**
* All objects returned by {@link Scene#pick} have a <code>primitive</code> property. This returns
* the tileset containing the feature.
*
* @type {Cesium3DTileset}
*/
get primitive() {
return this._content.tileset;
}
/**
* Get the feature ID associated with this feature. Using EXT_mesh_features,
* this is the feature ID from the selected feature ID set.
*
* @type {number}
*
* @readonly
*/
get featureId() {
return this._batchId;
}
/**
* @type {Cesium3DTileBatchTable|undefined}
* @private
*/
get _batchTable() {
return this._content.batchTables[this._batchTableId];
}
/**
* @type {number[]}
* @ignore
*/
get pickIds() {
const pickIds = [];
for (const prim of this._iteratePrimitives()) {
pickIds.push(prim._pickId);
}
return pickIds;
}
/**
* Returns whether the feature contains this property. This includes properties from this feature's
* class and inherited classes when using a batch table hierarchy.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy}
*
* @param {string} name The case-sensitive name of the property.
* @returns {boolean} Whether the feature contains this property.
*/
hasProperty(name) {
if (!defined(this._batchTable)) {
return false;
}
return this._batchTable.hasProperty(this._batchId, name);
}
/**
* Returns an array of property IDs for the feature. This includes properties from this feature's
* class and inherited classes when using a batch table hierarchy.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy}
*
* @param {string[]} [results] An array into which to store the results.
* @returns {string[]} The IDs of the feature's properties.
*/
getPropertyIds(results) {
if (!defined(this._batchTable)) {
return [];
}
return this._batchTable.getPropertyIds(this._batchId, results);
}
/**
* Returns a copy of the value of the feature's property with the given name. This includes properties from this feature's
* class and inherited classes when using a batch table hierarchy.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_batch_table_hierarchy}
*
* @param {string} name The case-sensitive name of the property.
* @returns {*} The value of the property or <code>undefined</code> if the feature does not have this property.
*
* @example
* // Display all the properties for a feature in the console log.
* const propertyIds = feature.getPropertyIds();
* const length = propertyIds.length;
* for (let i = 0; i < length; ++i) {
* const propertyId = propertyIds[i];
* console.log(`{propertyId} : ${feature.getProperty(propertyId)}`);
* }
*/
getProperty(name) {
if (!defined(this._batchTable)) {
return undefined;
}
return this._batchTable.getProperty(this._batchId, name);
}
/**
* Returns a copy of the value of the feature's property with the given name.
* If the feature is contained within a tileset that has metadata (3D Tiles 1.1)
* or uses the <code>3DTILES_metadata</code> extension, tileset, group and tile metadata is
* inherited.
* <p>
* To resolve name conflicts, this method resolves names from most specific to
* least specific by metadata granularity in the order: feature, tile, group,
* tileset. Within each granularity, semantics are resolved first, then other
* properties.
* </p>
* @param {string} name The case-sensitive name of the property.
* @returns {*} The value of the property or <code>undefined</code> if the feature does not have this property.
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
getPropertyInherited(name) {
return Cesium3DTileFeature.getPropertyInherited(
// @ts-expect-error Requires type checking in Cesium3DTileContent.
this._content,
this._batchId,
name,
this._batchTable,
);
}
/**
* Sets the value of the feature's property with the given name.
* <p>
* If a property with the given name doesn't exist, it is created.
* </p>
*
* @param {string} name The case-sensitive name of the property.
* @param {*} value The value of the property that will be copied.
*
* @exception {DeveloperError} Inherited batch table hierarchy property is read only.
*
* @example
* const height = feature.getProperty('Height'); // e.g., the height of a building
*
* @example
* const name = 'clicked';
* if (feature.getProperty(name)) {
* console.log('already clicked');
* } else {
* feature.setProperty(name, true);
* console.log('first click');
* }
*/
setProperty(name, value) {
throw new DeveloperError("Not implemented");
}
/**
* Returns whether the feature's class name equals <code>className</code>. Unlike {@link Cesium3DTileVectorFeature#isClass}
* this function only checks the feature's exact class and not inherited classes.
* <p>
* This function returns <code>false</code> if no batch table hierarchy is present.
* </p>
*
* @param {string} className The name to check against.
* @returns {boolean} Whether the feature's class name equals <code>className</code>
*
* @private
*/
isExactClass(className) {
if (!defined(this._batchTable)) {
return false;
}
return this._batchTable.isExactClass(this._batchId, className);
}
/**
* Returns whether the feature's class or any inherited classes are named <code>className</code>.
* <p>
* This function returns <code>false</code> if no batch table hierarchy is present.
* </p>
*
* @param {string} className The name to check against.
* @returns {boolean} Whether the feature's class or inherited classes are named <code>className</code>
*
* @private
*/
isClass(className) {
if (!defined(this._batchTable)) {
return false;
}
return this._batchTable.isClass(this._batchId, className);
}
/**
* Returns the feature's class name.
* <p>
* This function returns <code>undefined</code> if no batch table hierarchy is present.
* </p>
*
* @returns {string} The feature's class name.
*
* @private
*/
getExactClassName() {
if (!defined(this._batchTable)) {
return undefined;
}
return this._batchTable.getExactClassName(this._batchId);
}
/////////////////////////////////////////////////////////////////////////////
// INTERNAL ITERATORS
/**
* @returns {Iterable<BufferPrimitive>}
*/
*_iteratePrimitives() {
yield* this._iteratePrimitivesWith(BufferPointCollection, point);
yield* this._iteratePrimitivesWith(BufferPolylineCollection, polyline);
yield* this._iteratePrimitivesWith(BufferPolygonCollection, polygon);
}
/**
* @param {*} CollectionType
* @param {BufferPrimitive} result
* @returns {Iterable<BufferPrimitive>}
*/
*_iteratePrimitivesWith(CollectionType, result) {
const collections = this._content._collections;
for (let i = 0; i < collections.length; i++) {
const collection = collections[i];
const primitiveIndices = this._primitivesByCollection.get(i);
if (primitiveIndices && collection instanceof CollectionType) {
for (const primitiveIndex of primitiveIndices) {
collection.get(primitiveIndex, result);
yield result;
}
}
}
}
/** @returns {Iterable<BufferPrimitiveMaterial>} */
*_iterateMaterials() {
yield* this._iteratePointMaterials();
yield* this._iteratePolylineMaterials();
yield* this._iteratePolygonMaterials();
}
/** @returns {Iterable<BufferPointMaterial>} */
*_iteratePointMaterials() {
yield* /** @type {Iterable<BufferPointMaterial>} */ (
this._iterateMaterialsWith(BufferPointCollection, point, pointMaterial)
);
}
/** @returns {Iterable<BufferPolylineMaterial>} */
*_iteratePolylineMaterials() {
yield* /** @type {Iterable<BufferPolylineMaterial>} */ (
this._iterateMaterialsWith(
BufferPolylineCollection,
polyline,
polylineMaterial,
)
);
}
/** @returns {Iterable<BufferPolygonMaterial>} */
*_iteratePolygonMaterials() {
yield* /** @type {Iterable<BufferPolygonMaterial>} */ (
this._iterateMaterialsWith(
BufferPolygonCollection,
polygon,
polygonMaterial,
)
);
}
/**
* @param {*} CollectionType
* @param {BufferPrimitive} primitive
* @param {BufferPrimitiveMaterial} result
* @returns {Iterable<BufferPrimitiveMaterial>}
*/
*_iterateMaterialsWith(CollectionType, primitive, result) {
for (const prim of this._iteratePrimitivesWith(CollectionType, primitive)) {
prim.getMaterial(result);
yield result;
prim.setMaterial(result);
}
}
}
export default Cesium3DTileVectorFeature;
+760
View File
@@ -0,0 +1,760 @@
import Cartesian3 from "../Core/Cartesian3.js";
import Cesium3DTilesetMetadata from "./Cesium3DTilesetMetadata.js";
import Check from "../Core/Check.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import Ellipsoid from "../Core/Ellipsoid.js";
import hasExtension from "./hasExtension.js";
import ImplicitSubtree from "./ImplicitSubtree.js";
import ImplicitSubtreeCache from "./ImplicitSubtreeCache.js";
import ImplicitTileCoordinates from "./ImplicitTileCoordinates.js";
import ImplicitTileset from "./ImplicitTileset.js";
import Matrix3 from "../Core/Matrix3.js";
import Matrix4 from "../Core/Matrix4.js";
import MetadataSemantic from "./MetadataSemantic.js";
import MetadataType from "./MetadataType.js";
import OrientedBoundingBox from "../Core/OrientedBoundingBox.js";
import preprocess3DTileContent from "./preprocess3DTileContent.js";
import Resource from "../Core/Resource.js";
import ResourceCache from "./ResourceCache.js";
import RuntimeError from "../Core/RuntimeError.js";
import VoxelContent from "./VoxelContent.js";
import VoxelMetadataOrder from "./VoxelMetadataOrder.js";
import VoxelShapeType from "./VoxelShapeType.js";
import CesiumMath from "../Core/Math.js";
import Quaternion from "../Core/Quaternion.js";
/**
* @typedef {object} Cesium3DTilesVoxelProvider.ConstructorOptions
*
* Initialization options for the Cesium3DTilesVoxelProvider constructor
*
* @property {string} className The class in the tileset schema describing voxel metadata.
* @property {string[]} names The metadata names.
* @property {MetadataType[]} types The metadata types.
* @property {MetadataComponentType[]} componentTypes The metadata component types.
* @property {VoxelShapeType} shape The {@link VoxelShapeType}.
* @property {Cartesian3} dimensions The number of voxels per dimension of a tile. This is the same for all tiles in the dataset.
* @property {Cartesian3} [paddingBefore=Cartesian3.ZERO] The number of padding voxels before the tile. This improves rendering quality when sampling the edge of a tile, but it increases memory usage.
* @property {Cartesian3} [paddingAfter=Cartesian3.ZERO] The number of padding voxels after the tile. This improves rendering quality when sampling the edge of a tile, but it increases memory usage.
* @property {Matrix4} [globalTransform=Matrix4.IDENTITY] A transform from local space to global space.
* @property {Matrix4} [shapeTransform=Matrix4.IDENTITY] A transform from shape space to local space.
* @property {Cartesian3} [minBounds] The minimum bounds.
* @property {Cartesian3} [maxBounds] The maximum bounds.
* @property {number[][]} [minimumValues] The metadata minimum values.
* @property {number[][]} [maximumValues] The metadata maximum values.
* @property {number} [maximumTileCount] The maximum number of tiles that exist for this provider. This value is used as a hint to the voxel renderer to allocate an appropriate amount of GPU memory. If this value is not known it can be undefined.
*/
/**
* A {@link VoxelProvider} that fetches voxel data from a 3D Tiles tileset.
* <p>
* Implements the {@link VoxelProvider} interface.
* </p>
* <div class="notice">
* This object is normally not instantiated directly, use {@link Cesium3DTilesVoxelProvider.fromUrl}.
* </div>
*
* @alias Cesium3DTilesVoxelProvider
* @constructor
* @augments VoxelProvider
*
* @param {Cesium3DTilesVoxelProvider.ConstructorOptions} options An object describing initialization options
*
* @see Cesium3DTilesVoxelProvider.fromUrl
* @see VoxelProvider
* @see VoxelPrimitive
* @see VoxelShapeType
*
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
function Cesium3DTilesVoxelProvider(options) {
options = options ?? Frozen.EMPTY_OBJECT;
const {
className,
names,
types,
componentTypes,
shape,
dimensions,
paddingBefore = Cartesian3.ZERO.clone(),
paddingAfter = Cartesian3.ZERO.clone(),
globalTransform = Matrix4.IDENTITY.clone(),
shapeTransform = Matrix4.IDENTITY.clone(),
minBounds,
maxBounds,
minimumValues,
maximumValues,
maximumTileCount,
} = options;
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("className", className);
Check.typeOf.object("names", names);
Check.typeOf.object("types", types);
Check.typeOf.object("componentTypes", componentTypes);
Check.typeOf.string("shape", shape);
Check.typeOf.object("dimensions", dimensions);
//>>includeEnd('debug');
this._shapeTransform = shapeTransform;
this._globalTransform = globalTransform;
this._shape = shape;
this._minBounds = minBounds;
this._maxBounds = maxBounds;
this._dimensions = dimensions;
this._paddingBefore = paddingBefore;
this._paddingAfter = paddingAfter;
this._className = className;
this._names = names;
this._types = types;
this._componentTypes = componentTypes;
this._metadataOrder =
shape === VoxelShapeType.ELLIPSOID
? VoxelMetadataOrder.Z_UP
: VoxelMetadataOrder.Y_UP;
this._minimumValues = minimumValues;
this._maximumValues = maximumValues;
this._maximumTileCount = maximumTileCount;
this._availableLevels = undefined;
this._implicitTileset = undefined;
this._subtreeCache = new ImplicitSubtreeCache();
}
Object.defineProperties(Cesium3DTilesVoxelProvider.prototype, {
/**
* A transform from local space to global space.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {Matrix4}
* @default Matrix4.IDENTITY
* @readonly
*/
globalTransform: {
get: function () {
return this._globalTransform;
},
},
/**
* A transform from shape space to local space.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {Matrix4}
* @default Matrix4.IDENTITY
* @readonly
*/
shapeTransform: {
get: function () {
return this._shapeTransform;
},
},
/**
* Gets the {@link VoxelShapeType}
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {VoxelShapeType}
* @readonly
*/
shape: {
get: function () {
return this._shape;
},
},
/**
* Gets the minimum bounds.
* If undefined, the shape's default minimum bounds will be used instead.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {Cartesian3|undefined}
* @readonly
*/
minBounds: {
get: function () {
return this._minBounds;
},
},
/**
* Gets the maximum bounds.
* If undefined, the shape's default maximum bounds will be used instead.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {Cartesian3|undefined}
* @readonly
*/
maxBounds: {
get: function () {
return this._maxBounds;
},
},
/**
* Gets the number of voxels per dimension of a tile. This is the same for all tiles in the dataset.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {Cartesian3}
* @readonly
*/
dimensions: {
get: function () {
return this._dimensions;
},
},
/**
* Gets the number of padding voxels before the tile. This improves rendering quality when sampling the edge of a tile, but it increases memory usage.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {Cartesian3}
* @default Cartesian3.ZERO
* @readonly
*/
paddingBefore: {
get: function () {
return this._paddingBefore;
},
},
/**
* Gets the number of padding voxels after the tile. This improves rendering quality when sampling the edge of a tile, but it increases memory usage.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {Cartesian3}
* @default Cartesian3.ZERO
* @readonly
*/
paddingAfter: {
get: function () {
return this._paddingAfter;
},
},
/**
* The metadata class for this tileset.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {string}
* @readonly
*/
className: {
get: function () {
return this._className;
},
},
/**
* Gets the metadata names.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {string[]}
* @readonly
*/
names: {
get: function () {
return this._names;
},
},
/**
* Gets the metadata types.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {MetadataType[]}
* @readonly
*/
types: {
get: function () {
return this._types;
},
},
/**
* Gets the metadata component types.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {MetadataComponentType[]}
* @readonly
*/
componentTypes: {
get: function () {
return this._componentTypes;
},
},
/**
* Gets the ordering of the metadata in the buffers.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {VoxelMetadataOrder}
* @readonly
* @private
*/
metadataOrder: {
get: function () {
return this._metadataOrder;
},
},
/**
* Gets the metadata minimum values.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {number[][]|undefined}
* @readonly
*/
minimumValues: {
get: function () {
return this._minimumValues;
},
},
/**
* Gets the metadata maximum values.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {number[][]|undefined}
* @readonly
*/
maximumValues: {
get: function () {
return this._maximumValues;
},
},
/**
* The maximum number of tiles that exist for this provider.
* This value is used as a hint to the voxel renderer to allocate an appropriate amount of GPU memory.
* If this value is not known it can be undefined.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {number|undefined}
* @readonly
*/
maximumTileCount: {
get: function () {
return this._maximumTileCount;
},
},
/**
* The number of levels of detail containing available tiles in the tileset.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {number|undefined}
* @readonly
*/
availableLevels: {
get: function () {
return this._availableLevels;
},
},
});
/**
* Creates a {@link Cesium3DTilesVoxelProvider} that fetches voxel data from a 3D Tiles tileset.
*
* @param {Resource|string} url The URL to a tileset JSON file
* @returns {Promise<Cesium3DTilesVoxelProvider>} The created provider
*
* @exception {RuntimeException} Root must have content
* @exception {RuntimeException} Root tile content must have 3DTILES_content_voxels extension
* @exception {RuntimeException} Root tile must have implicit tiling
* @exception {RuntimeException} Tileset must have a metadata schema
* @exception {RuntimeException} Only box, region and 3DTILES_bounding_volume_cylinder are supported in Cesium3DTilesVoxelProvider
*
* @example
* try {
* const voxelProvider = await Cesium3DTilesVoxelProvider.fromUrl(
* "http://localhost:8002/tilesets/voxel/tileset.json"
* );
* const voxelPrimitive = new VoxelPrimitive({
* provider: voxelProvider,
* customShader: customShader,
* });
* scene.primitives.add(voxelPrimitive);
* } catch (error) {
* console.error(`Error creating voxel primitive: ${error}`);
* }
*
* @see {@link VoxelPrimitive}
*/
Cesium3DTilesVoxelProvider.fromUrl = async function (url) {
//>>includeStart('debug', pragmas.debug);
Check.defined("url", url);
//>>includeEnd('debug');
const resource = Resource.createIfNeeded(url);
const tilesetJson = await resource.fetchJson();
validate(tilesetJson);
const schemaLoader = getMetadataSchemaLoader(tilesetJson, resource);
await schemaLoader.load();
const { root } = tilesetJson;
const metadataJson = hasExtension(tilesetJson, "3DTILES_metadata")
? tilesetJson.extensions["3DTILES_metadata"]
: tilesetJson;
const tilesetMetadata = new Cesium3DTilesetMetadata({
metadataJson: metadataJson,
schema: schemaLoader.schema,
});
const voxel = root.content.extensions["3DTILES_content_voxels"];
const className = voxel.class;
const providerOptions = getAttributeInfo(tilesetMetadata, className);
Object.assign(providerOptions, getShape(root));
if (defined(root.transform)) {
providerOptions.globalTransform = Matrix4.unpack(root.transform);
} else {
providerOptions.globalTransform = Matrix4.clone(Matrix4.IDENTITY);
}
providerOptions.dimensions = Cartesian3.unpack(voxel.dimensions);
providerOptions.maximumTileCount = getTileCount(tilesetMetadata);
if (defined(voxel.padding)) {
providerOptions.paddingBefore = Cartesian3.unpack(voxel.padding.before);
providerOptions.paddingAfter = Cartesian3.unpack(voxel.padding.after);
}
const provider = new Cesium3DTilesVoxelProvider(providerOptions);
const implicitTileset = new ImplicitTileset(
resource,
root,
schemaLoader.schema,
);
provider._implicitTileset = implicitTileset;
provider._availableLevels = implicitTileset.availableLevels;
ResourceCache.unload(schemaLoader);
return provider;
};
function getTileCount(metadata) {
if (!defined(metadata.tileset)) {
return undefined;
}
return metadata.tileset.getPropertyBySemantic(
MetadataSemantic.TILESET_TILE_COUNT,
);
}
function validate(tileset) {
const root = tileset.root;
if (!defined(root.content)) {
throw new RuntimeError("Root must have content");
}
if (!hasExtension(root.content, "3DTILES_content_voxels")) {
throw new RuntimeError(
"Root tile content must have 3DTILES_content_voxels extension",
);
}
if (
!hasExtension(root, "3DTILES_implicit_tiling") &&
!defined(root.implicitTiling)
) {
throw new RuntimeError("Root tile must have implicit tiling");
}
if (
!defined(tileset.schema) &&
!defined(tileset.schemaUri) &&
!hasExtension(tileset, "3DTILES_metadata")
) {
throw new RuntimeError("Tileset must have a metadata schema");
}
}
function getShape(tile) {
const boundingVolume = tile.boundingVolume;
if (defined(boundingVolume.box)) {
return getBoxShape(boundingVolume.box);
} else if (defined(boundingVolume.region)) {
return getEllipsoidShape(boundingVolume.region);
} else if (hasExtension(boundingVolume, "3DTILES_bounding_volume_cylinder")) {
return getCylinderShape(
boundingVolume.extensions["3DTILES_bounding_volume_cylinder"],
);
}
throw new RuntimeError(
"Only box, region and 3DTILES_bounding_volume_cylinder are supported in Cesium3DTilesVoxelProvider",
);
}
function getEllipsoidShape(region) {
const west = region[0];
const south = region[1];
const east = region[2];
const north = region[3];
const minHeight = region[4];
const maxHeight = region[5];
const shapeTransform = Matrix4.fromScale(Ellipsoid.WGS84.radii);
const minBounds = new Cartesian3(west, south, minHeight);
const maxBounds = new Cartesian3(east, north, maxHeight);
return {
shape: VoxelShapeType.ELLIPSOID,
minBounds: minBounds,
maxBounds: maxBounds,
shapeTransform: shapeTransform,
};
}
const scratchScale = new Cartesian3();
const scratchRotation = new Matrix3();
function getBoxShape(box) {
const obb = OrientedBoundingBox.unpack(box);
const scale = Matrix3.getScale(obb.halfAxes, scratchScale);
const rotation = Matrix3.getRotation(obb.halfAxes, scratchRotation);
return {
shape: VoxelShapeType.BOX,
minBounds: Cartesian3.negate(scale, new Cartesian3()),
maxBounds: Cartesian3.clone(scale),
shapeTransform: Matrix4.fromRotationTranslation(rotation, obb.center),
};
}
function getCylinderShape(cylinder) {
const {
minRadius,
maxRadius,
height,
minAngle = -CesiumMath.PI,
maxAngle = CesiumMath.PI,
translation = [0, 0, 0],
rotation = [0, 0, 0, 1],
} = cylinder;
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("minRadius", minRadius);
Check.typeOf.number("maxRadius", maxRadius);
Check.typeOf.number("height", height);
Check.typeOf.number("minAngle", minAngle);
Check.typeOf.number("maxAngle", maxAngle);
Check.typeOf.object("translation", translation);
Check.typeOf.object("rotation", rotation);
//>>includeEnd('debug');
const minHeight = -0.5 * height + translation[2];
const maxHeight = 0.5 * height + translation[2];
const shapeTransform = Matrix4.fromTranslationQuaternionRotationScale(
Cartesian3.unpack(translation),
Quaternion.unpack(rotation),
Cartesian3.ONE,
);
return {
shape: VoxelShapeType.CYLINDER,
minBounds: Cartesian3.fromElements(minRadius, minAngle, minHeight),
maxBounds: Cartesian3.fromElements(maxRadius, maxAngle, maxHeight),
shapeTransform: shapeTransform,
};
}
function getMetadataSchemaLoader(tilesetJson, resource) {
const { schemaUri, schema } = tilesetJson;
if (!defined(schemaUri)) {
return ResourceCache.getSchemaLoader({ schema });
}
return ResourceCache.getSchemaLoader({
resource: resource.getDerivedResource({
url: schemaUri,
}),
});
}
function getAttributeInfo(metadata, className) {
const { schema, statistics } = metadata;
const classStatistics = statistics?.classes[className];
const properties = schema.classes[className].properties;
const propertyInfo = Object.entries(properties).map(([id, property]) => {
const { type, componentType } = property;
const min = classStatistics?.properties[id].min;
const max = classStatistics?.properties[id].max;
const componentCount = MetadataType.getComponentCount(type);
const minValue = copyArray(min, componentCount);
const maxValue = copyArray(max, componentCount);
return { id, type, componentType, minValue, maxValue };
});
const names = propertyInfo.map((info) => info.id);
const types = propertyInfo.map((info) => info.type);
const componentTypes = propertyInfo.map((info) => info.componentType);
const minimumValues = propertyInfo.map((info) => info.minValue);
const maximumValues = propertyInfo.map((info) => info.maxValue);
const hasMinimumValues = minimumValues.some(defined);
return {
className,
names,
types,
componentTypes,
minimumValues: hasMinimumValues ? minimumValues : undefined,
maximumValues: hasMinimumValues ? maximumValues : undefined,
};
}
function copyArray(values, length) {
// Copy input values into a new array of a specified length.
// If the input is not an array, its value will be copied into the first element
// of the returned array. If the input is an array shorter than the returned
// array, the extra elements in the returned array will be undefined. If the
// input is undefined, the return will be undefined.
if (!defined(values)) {
return;
}
const valuesArray = Array.isArray(values) ? values : [values];
return Array.from({ length }, (v, i) => valuesArray[i]);
}
/**
* Get the subtree at a given subtree coordinate
* @param {VoxelProvider} provider The voxel provider
* @param {ImplicitTileCoordinates} subtreeCoord The coordinate at which to retrieve the subtree
* @returns {Promise<ImplicitSubtree>} The subtree at the given coordinate
* @private
*/
async function getSubtree(provider, subtreeCoord) {
const implicitTileset = provider._implicitTileset;
const subtreeCache = provider._subtreeCache;
// First load the subtree to check if the tile is available.
// If the subtree has been requested previously it might still be in the cache
let subtree = subtreeCache.find(subtreeCoord);
if (defined(subtree)) {
return subtree;
}
const subtreeRelative = implicitTileset.subtreeUriTemplate.getDerivedResource(
{
templateValues: subtreeCoord.getTemplateValues(),
},
);
const subtreeResource = implicitTileset.baseResource.getDerivedResource({
url: subtreeRelative.url,
});
const arrayBuffer = await subtreeResource.fetchArrayBuffer();
// Check one more time if the subtree is in the cache.
// This could happen if there are two in-flight tile requests from the same
// subtree and one finishes before the other.
subtree = subtreeCache.find(subtreeCoord);
if (defined(subtree)) {
return subtree;
}
const preprocessed = preprocess3DTileContent(arrayBuffer);
subtree = await ImplicitSubtree.fromSubtreeJson(
subtreeResource,
preprocessed.jsonPayload,
preprocessed.binaryPayload,
implicitTileset,
subtreeCoord,
);
subtreeCache.addSubtree(subtree);
return subtree;
}
/**
* Requests the data for a given tile.
*
* @param {object} [options] Object with the following properties:
* @param {number} [options.tileLevel=0] The tile's level.
* @param {number} [options.tileX=0] The tile's X coordinate.
* @param {number} [options.tileY=0] The tile's Y coordinate.
* @param {number} [options.tileZ=0] The tile's Z coordinate.
* @privateparam {number} [options.keyframe=0] The requested keyframe.
* @returns {Promise<VoxelContent>|undefined} A promise resolving to a VoxelContent containing the data for the tile, or undefined if the request could not be scheduled this frame.
*/
Cesium3DTilesVoxelProvider.prototype.requestData = async function (options) {
options = options ?? Frozen.EMPTY_OBJECT;
const {
tileLevel = 0,
tileX = 0,
tileY = 0,
tileZ = 0,
keyframe = 0,
} = options;
if (keyframe !== 0) {
return Promise.reject(
`3D Tiles currently doesn't support time-dynamic data.`,
);
}
// 1. Load the subtree that the tile belongs to (possibly from the subtree cache)
// 2. Load the voxel content if available
// Can't use a scratch variable here because the object is used inside the promise chain.
const implicitTileset = this._implicitTileset;
const tileCoordinates = new ImplicitTileCoordinates({
subdivisionScheme: implicitTileset.subdivisionScheme,
subtreeLevels: implicitTileset.subtreeLevels,
level: tileLevel,
x: tileX,
y: tileY,
z: tileZ,
});
// Find the coordinates of the parent subtree containing tileCoordinates
// If tileCoordinates is a subtree child, use that subtree
// If tileCoordinates is a subtree root, use its parent subtree
const isSubtreeRoot =
tileCoordinates.isSubtreeRoot() && tileCoordinates.level > 0;
const subtreeCoord = isSubtreeRoot
? tileCoordinates.getParentSubtreeCoordinates()
: tileCoordinates.getSubtreeCoordinates();
const that = this;
const subtree = await getSubtree(that, subtreeCoord);
// NOTE: these two subtree methods are ONLY used by voxels!
const isAvailable = isSubtreeRoot
? subtree.childSubtreeIsAvailableAtCoordinates
: subtree.tileIsAvailableAtCoordinates;
const available = isAvailable.call(subtree, tileCoordinates);
if (!available) {
return Promise.reject(
`Tile is not available at level ${tileLevel}, x ${tileX}, y ${tileY}, z ${tileZ}.`,
);
}
const { contentUriTemplates, baseResource } = implicitTileset;
const gltfRelative = contentUriTemplates[0].getDerivedResource({
templateValues: tileCoordinates.getTemplateValues(),
});
const gltfResource = baseResource.getDerivedResource({
url: gltfRelative.url,
});
return VoxelContent.fromGltf(gltfResource);
};
export default Cesium3DTilesVoxelProvider;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,291 @@
import defined from "../Core/defined.js";
import ManagedArray from "../Core/ManagedArray.js";
import Cesium3DTileRefine from "./Cesium3DTileRefine.js";
import Cesium3DTilesetTraversal from "./Cesium3DTilesetTraversal.js";
/**
* Depth-first traversal that traverses all visible tiles and marks tiles for selection.
* A tile does not refine until all children are loaded.
* This is the traditional replacement refinement approach and is called the base traversal.
*
* @alias Cesium3DTilesetBaseTraversal
* @constructor
*
* @private
*/
function Cesium3DTilesetBaseTraversal() {}
const traversal = {
stack: new ManagedArray(),
stackMaximumLength: 0,
};
const emptyTraversal = {
stack: new ManagedArray(),
stackMaximumLength: 0,
};
/**
* Traverses a {@link Cesium3DTileset} to determine which tiles to load and render.
*
* @private
* @param {Cesium3DTileset} tileset
* @param {FrameState} frameState
*/
Cesium3DTilesetBaseTraversal.selectTiles = function (tileset, frameState) {
tileset._requestedTiles.length = 0;
if (tileset.debugFreezeFrame) {
return;
}
tileset._selectedTiles.length = 0;
tileset._selectedTilesToStyle.length = 0;
tileset._emptyTiles.length = 0;
tileset.hasMixedContent = false;
const root = tileset.root;
Cesium3DTilesetTraversal.updateTile(root, frameState);
if (!root.isVisible) {
return;
}
if (
root.getScreenSpaceError(frameState, true) <=
tileset.memoryAdjustedScreenSpaceError
) {
return;
}
executeTraversal(root, frameState);
traversal.stack.trim(traversal.stackMaximumLength);
emptyTraversal.stack.trim(emptyTraversal.stackMaximumLength);
// Update the priority for any requests found during traversal
// Update after traversal so that min and max values can be used to normalize priority values
const requestedTiles = tileset._requestedTiles;
for (let i = 0; i < requestedTiles.length; ++i) {
requestedTiles[i].updatePriority();
}
};
/**
* Mark a tile as selected if it has content available.
*
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
*/
function selectDesiredTile(tile, frameState) {
if (tile.contentAvailable) {
Cesium3DTilesetTraversal.selectTile(tile, frameState);
}
}
/**
* @private
* @param {Cesium3DTile} tile
* @param {ManagedArray} stack
* @param {FrameState} frameState
* @returns {boolean}
*/
function updateAndPushChildren(tile, stack, frameState) {
const replace = tile.refine === Cesium3DTileRefine.REPLACE;
const { tileset, children } = tile;
const { updateTile, loadTile, touchTile } = Cesium3DTilesetTraversal;
for (let i = 0; i < children.length; ++i) {
updateTile(children[i], frameState);
}
// Sort by distance to take advantage of early Z and reduce artifacts for skipLevelOfDetail
children.sort(Cesium3DTilesetTraversal.sortChildrenByDistanceToCamera);
// For traditional replacement refinement only refine if all children are loaded.
// Empty tiles are exempt since it looks better if children stream in as they are loaded to fill the empty space.
const checkRefines = replace && tile.hasRenderableContent;
let refines = true;
let anyChildrenVisible = false;
// Determining min child
let minIndex = -1;
let minimumPriority = Number.MAX_VALUE;
for (let i = 0; i < children.length; ++i) {
const child = children[i];
if (child.isVisible) {
stack.push(child);
if (child._foveatedFactor < minimumPriority) {
minIndex = i;
minimumPriority = child._foveatedFactor;
}
anyChildrenVisible = true;
} else if (checkRefines || tileset.loadSiblings) {
// Keep non-visible children loaded since they are still needed before the parent can refine.
// Or loadSiblings is true so always load tiles regardless of visibility.
if (child._foveatedFactor < minimumPriority) {
minIndex = i;
minimumPriority = child._foveatedFactor;
}
loadTile(child, frameState);
touchTile(child, frameState);
}
if (checkRefines) {
let childRefines;
if (!child._inRequestVolume) {
childRefines = false;
} else if (!child.hasRenderableContent) {
childRefines = executeEmptyTraversal(child, frameState);
} else {
childRefines = child.contentAvailable;
}
refines = refines && childRefines;
}
}
if (!anyChildrenVisible) {
refines = false;
}
if (minIndex !== -1 && replace) {
// An ancestor will hold the _foveatedFactor and _distanceToCamera for descendants between itself and its highest priority descendant. Siblings of a min children along the way use this ancestor as their priority holder as well.
// Priority of all tiles that refer to the _foveatedFactor and _distanceToCamera stored in the common ancestor will be differentiated based on their _depth.
const minPriorityChild = children[minIndex];
minPriorityChild._wasMinPriorityChild = true;
const priorityHolder =
(tile._wasMinPriorityChild || tile === tileset.root) &&
minimumPriority <= tile._priorityHolder._foveatedFactor
? tile._priorityHolder
: tile; // This is where priority dependency chains are wired up or started anew.
priorityHolder._foveatedFactor = Math.min(
minPriorityChild._foveatedFactor,
priorityHolder._foveatedFactor,
);
priorityHolder._distanceToCamera = Math.min(
minPriorityChild._distanceToCamera,
priorityHolder._distanceToCamera,
);
for (let i = 0; i < children.length; ++i) {
children[i]._priorityHolder = priorityHolder;
}
}
return refines;
}
/**
* Depth-first traversal that traverses all visible tiles and marks tiles for selection.
* A tile does not refine until all children are loaded.
* This is the traditional replacement refinement approach and is called the base traversal.
*
* @private
* @param {Cesium3DTile} root
* @param {FrameState} frameState
*/
function executeTraversal(root, frameState) {
const { tileset } = root;
const { canTraverse, loadTile, visitTile, touchTile } =
Cesium3DTilesetTraversal;
const stack = traversal.stack;
stack.push(root);
while (stack.length > 0) {
traversal.stackMaximumLength = Math.max(
traversal.stackMaximumLength,
stack.length,
);
const tile = stack.pop();
const parent = tile.parent;
const parentRefines = !defined(parent) || parent._refines;
tile._refines = canTraverse(tile)
? updateAndPushChildren(tile, stack, frameState) && parentRefines
: false;
const stoppedRefining = !tile._refines && parentRefines;
if (!tile.hasRenderableContent) {
// Add empty tile just to show its debug bounding volume
// If the tile has tileset content load the external tileset
tileset._emptyTiles.push(tile);
loadTile(tile, frameState);
if (stoppedRefining) {
selectDesiredTile(tile, frameState);
}
} else if (tile.refine === Cesium3DTileRefine.ADD) {
// Additive tiles are always loaded and selected
selectDesiredTile(tile, frameState);
loadTile(tile, frameState);
} else if (tile.refine === Cesium3DTileRefine.REPLACE) {
loadTile(tile, frameState);
if (stoppedRefining) {
selectDesiredTile(tile, frameState);
}
}
visitTile(tile, frameState);
touchTile(tile, frameState);
}
}
/**
* Depth-first traversal that checks if all nearest descendants with content are loaded.
* Ignores visibility.
*
* @private
* @param {Cesium3DTile} root
* @param {FrameState} frameState
* @returns {boolean}
*/
function executeEmptyTraversal(root, frameState) {
const { canTraverse, updateTile, loadTile, touchTile } =
Cesium3DTilesetTraversal;
let allDescendantsLoaded = true;
const stack = emptyTraversal.stack;
stack.push(root);
while (stack.length > 0) {
emptyTraversal.stackMaximumLength = Math.max(
emptyTraversal.stackMaximumLength,
stack.length,
);
const tile = stack.pop();
const children = tile.children;
const childrenLength = children.length;
// Only traverse if the tile is empty - traversal stops at descendants with content
const traverse = !tile.hasRenderableContent && canTraverse(tile);
// Traversal stops but the tile does not have content yet
// There will be holes if the parent tries to refine to its children, so don't refine
if (!traverse && !tile.contentAvailable) {
allDescendantsLoaded = false;
}
updateTile(tile, frameState);
if (!tile.isVisible) {
// Load tiles that aren't visible since they are still needed for the parent to refine
loadTile(tile, frameState);
touchTile(tile, frameState);
}
if (traverse) {
for (let i = 0; i < childrenLength; ++i) {
const child = children[i];
stack.push(child);
}
}
}
return root.hasEmptyContent || allDescendantsLoaded;
}
export default Cesium3DTilesetBaseTraversal;
+80
View File
@@ -0,0 +1,80 @@
import defined from "../Core/defined.js";
import DoublyLinkedList from "../Core/DoublyLinkedList.js";
/**
* Stores tiles with content loaded.
*
* @private
*/
function Cesium3DTilesetCache() {
// [head, sentinel) -> tiles that weren't selected this frame and may be removed from the cache
// (sentinel, tail] -> tiles that were selected this frame
this._list = new DoublyLinkedList();
this._sentinel = this._list.add();
this._trimTiles = false;
}
Cesium3DTilesetCache.prototype.reset = function () {
// Move sentinel node to the tail so, at the start of the frame, all tiles
// may be potentially replaced. Tiles are moved to the right of the sentinel
// when they are selected so they will not be replaced.
this._list.splice(this._list.tail, this._sentinel);
};
Cesium3DTilesetCache.prototype.touch = function (tile) {
const node = tile.cacheNode;
if (defined(node)) {
this._list.splice(this._sentinel, node);
}
};
Cesium3DTilesetCache.prototype.add = function (tile) {
if (!defined(tile.cacheNode)) {
tile.cacheNode = this._list.add(tile);
}
};
Cesium3DTilesetCache.prototype.unloadTile = function (
tileset,
tile,
unloadCallback,
) {
const node = tile.cacheNode;
if (!defined(node)) {
return;
}
this._list.remove(node);
tile.cacheNode = undefined;
unloadCallback(tileset, tile);
};
Cesium3DTilesetCache.prototype.unloadTiles = function (
tileset,
unloadCallback,
) {
const trimTiles = this._trimTiles;
this._trimTiles = false;
const list = this._list;
// Traverse the list only to the sentinel since tiles/nodes to the
// right of the sentinel were used this frame.
//
// The sub-list to the left of the sentinel is ordered from LRU to MRU.
const sentinel = this._sentinel;
let node = list.head;
while (
node !== sentinel &&
(tileset.totalMemoryUsageInBytes > tileset.cacheBytes || trimTiles)
) {
const tile = node.item;
node = node.next;
this.unloadTile(tileset, tile, unloadCallback);
}
};
Cesium3DTilesetCache.prototype.trim = function () {
this._trimTiles = true;
};
export default Cesium3DTilesetCache;
+165
View File
@@ -0,0 +1,165 @@
import Color from "../Core/Color.js";
import defined from "../Core/defined.js";
import JulianDate from "../Core/JulianDate.js";
import CesiumMath from "../Core/Math.js";
/**
* A heatmap colorizer in a {@link Cesium3DTileset}. A tileset can colorize its visible tiles in a heatmap style.
*
* @alias Cesium3DTilesetHeatmap
* @constructor
* @private
*/
function Cesium3DTilesetHeatmap(tilePropertyName) {
/**
* The tile variable to track for heatmap colorization.
* Tile's will be colorized relative to the other visible tile's values for this variable.
*
* @type {string}
*/
this.tilePropertyName = tilePropertyName;
// Members that are updated every time a tile is colorized
this._minimum = Number.MAX_VALUE;
this._maximum = -Number.MAX_VALUE;
// Members that are updated once every frame
this._previousMinimum = Number.MAX_VALUE;
this._previousMaximum = -Number.MAX_VALUE;
// If defined uses a reference minimum maximum to colorize by instead of using last frames minimum maximum of rendered tiles.
// For example, the _loadTimestamp can get a better colorization using setReferenceMinimumMaximum in order to take accurate colored timing diffs of various scenes.
this._referenceMinimum = {};
this._referenceMaximum = {};
}
/**
* Convert to a usable heatmap value (i.e. a number). Ensures that tile values that aren't stored as numbers can be used for colorization.
* @private
*/
function getHeatmapValue(tileValue, tilePropertyName) {
let value;
if (tilePropertyName === "_loadTimestamp") {
value = JulianDate.toDate(tileValue).getTime();
} else {
value = tileValue;
}
return value;
}
/**
* Sets the reference minimum and maximum for the variable name. Converted to numbers before they are stored.
*
* @param {object} minimum The minimum reference value.
* @param {object} maximum The maximum reference value.
* @param {string} tilePropertyName The tile variable that will use these reference values when it is colorized.
*/
Cesium3DTilesetHeatmap.prototype.setReferenceMinimumMaximum = function (
minimum,
maximum,
tilePropertyName,
) {
this._referenceMinimum[tilePropertyName] = getHeatmapValue(
minimum,
tilePropertyName,
);
this._referenceMaximum[tilePropertyName] = getHeatmapValue(
maximum,
tilePropertyName,
);
};
function getHeatmapValueAndUpdateMinimumMaximum(heatmap, tile) {
const tilePropertyName = heatmap.tilePropertyName;
if (defined(tilePropertyName)) {
const heatmapValue = getHeatmapValue(
tile[tilePropertyName],
tilePropertyName,
);
if (!defined(heatmapValue)) {
heatmap.tilePropertyName = undefined;
return heatmapValue;
}
heatmap._maximum = Math.max(heatmapValue, heatmap._maximum);
heatmap._minimum = Math.min(heatmapValue, heatmap._minimum);
return heatmapValue;
}
}
const heatmapColors = [
new Color(0.1, 0.1, 0.1, 1), // Dark Gray
new Color(0.153, 0.278, 0.878, 1), // Blue
new Color(0.827, 0.231, 0.49, 1), // Pink
new Color(0.827, 0.188, 0.22, 1), // Red
new Color(1.0, 0.592, 0.259, 1), // Orange
new Color(1.0, 0.843, 0.0, 1),
]; // Yellow
/**
* Colorize the tile in heat map style based on where it lies within the minimum maximum window.
* Heatmap colors are black, blue, pink, red, orange, yellow. 'Cold' or low numbers will be black and blue, 'Hot' or high numbers will be orange and yellow,
* @param {Cesium3DTile} tile The tile to colorize relative to last frame's minimum and maximum values of all visible tiles.
* @param {FrameState} frameState The frame state.
*/
Cesium3DTilesetHeatmap.prototype.colorize = function (tile, frameState) {
const tilePropertyName = this.tilePropertyName;
if (
!defined(tilePropertyName) ||
!tile.contentAvailable ||
tile._selectedFrame !== frameState.frameNumber
) {
return;
}
const heatmapValue = getHeatmapValueAndUpdateMinimumMaximum(this, tile);
const minimum = this._previousMinimum;
const maximum = this._previousMaximum;
if (minimum === Number.MAX_VALUE || maximum === -Number.MAX_VALUE) {
return;
}
// Shift the minimum maximum window down to 0
const shiftedMax = maximum - minimum + CesiumMath.EPSILON7; // Prevent divide by 0
const shiftedValue = CesiumMath.clamp(
heatmapValue - minimum,
0.0,
shiftedMax,
);
// Get position between minimum and maximum and convert that to a position in the color array
const zeroToOne = shiftedValue / shiftedMax;
const lastIndex = heatmapColors.length - 1.0;
const colorPosition = zeroToOne * lastIndex;
// Take floor and ceil of the value to get the two colors to lerp between, lerp using the fractional portion
const colorPositionFloor = Math.floor(colorPosition);
const colorPositionCeil = Math.ceil(colorPosition);
const t = colorPosition - colorPositionFloor;
const colorZero = heatmapColors[colorPositionFloor];
const colorOne = heatmapColors[colorPositionCeil];
// Perform the lerp
const finalColor = Color.clone(Color.WHITE);
finalColor.red = CesiumMath.lerp(colorZero.red, colorOne.red, t);
finalColor.green = CesiumMath.lerp(colorZero.green, colorOne.green, t);
finalColor.blue = CesiumMath.lerp(colorZero.blue, colorOne.blue, t);
tile._debugColor = finalColor;
};
/**
* Resets the tracked minimum maximum values for heatmap colorization. Happens right before tileset traversal.
*/
Cesium3DTilesetHeatmap.prototype.resetMinimumMaximum = function () {
// For heat map colorization
const tilePropertyName = this.tilePropertyName;
if (defined(tilePropertyName)) {
const referenceMinimum = this._referenceMinimum[tilePropertyName];
const referenceMaximum = this._referenceMaximum[tilePropertyName];
const useReference = defined(referenceMinimum) && defined(referenceMaximum);
this._previousMinimum = useReference ? referenceMinimum : this._minimum;
this._previousMaximum = useReference ? referenceMaximum : this._maximum;
this._minimum = Number.MAX_VALUE;
this._maximum = -Number.MAX_VALUE;
}
};
export default Cesium3DTilesetHeatmap;
+199
View File
@@ -0,0 +1,199 @@
import Check from "../Core/Check.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import GroupMetadata from "./GroupMetadata.js";
import TilesetMetadata from "./TilesetMetadata.js";
/**
* An object containing metadata about a 3D Tileset.
* <p>
* See the {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_metadata|3DTILES_metadata Extension} for 3D Tiles.
* </p>
* <p>
* This object represents the tileset JSON (3D Tiles 1.1) or the <code>3DTILES_metadata</code> object that contains
* the schema ({@link MetadataSchema}), tileset metadata ({@link TilesetMetadata}), group metadata (dictionary of {@link GroupMetadata}), and metadata statistics (dictionary)
* </p>
*
* @param {object} options Object with the following properties:
* @param {object} options.metadataJson Either the tileset JSON (3D Tiles 1.1) or the <code>3DTILES_metadata</code> extension object that contains the tileset metadata.
* @param {MetadataSchema} options.schema The parsed schema.
*
* @alias Cesium3DTilesetMetadata
* @constructor
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
function Cesium3DTilesetMetadata(options) {
options = options ?? Frozen.EMPTY_OBJECT;
const metadataJson = options.metadataJson;
// The calling code is responsible for loading the schema.
// This keeps metadata parsing synchronous.
const schema = options.schema;
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options.metadataJson", metadataJson);
Check.typeOf.object("options.schema", schema);
//>>includeEnd('debug');
// An older schema stored the tileset metadata in the "tileset" property.
const metadata = metadataJson.metadata ?? metadataJson.tileset;
let tileset;
if (defined(metadata)) {
tileset = new TilesetMetadata({
tileset: metadata,
class: schema.classes[metadata.class],
});
}
let groupIds = [];
const groups = [];
const groupsJson = metadataJson.groups;
if (Array.isArray(groupsJson)) {
const length = groupsJson.length;
for (let i = 0; i < length; i++) {
const group = groupsJson[i];
groups.push(
new GroupMetadata({
group: group,
class: schema.classes[group.class],
}),
);
}
} else if (defined(groupsJson)) {
// An older version of group metadata stored groups in a dictionary
// instead of an array.
groupIds = Object.keys(groupsJson).sort();
const length = groupIds.length;
for (let i = 0; i < length; i++) {
const groupId = groupIds[i];
if (groupsJson.hasOwnProperty(groupId)) {
const group = groupsJson[groupId];
groups.push(
new GroupMetadata({
id: groupId,
group: groupsJson[groupId],
class: schema.classes[group.class],
}),
);
}
}
}
this._schema = schema;
this._groups = groups;
this._groupIds = groupIds;
this._tileset = tileset;
this._statistics = metadataJson.statistics;
this._extras = metadataJson.extras;
this._extensions = metadataJson.extensions;
}
Object.defineProperties(Cesium3DTilesetMetadata.prototype, {
/**
* Schema containing classes and enums.
*
* @memberof Cesium3DTilesetMetadata.prototype
* @type {MetadataSchema}
* @readonly
* @private
*/
schema: {
get: function () {
return this._schema;
},
},
/**
* Metadata about groups of content.
*
* @memberof Cesium3DTilesetMetadata.prototype
* @type {GroupMetadata[]}
* @readonly
* @private
*/
groups: {
get: function () {
return this._groups;
},
},
/**
* The IDs of the group metadata in the corresponding groups dictionary.
* Only populated if using the legacy schema.
*
* @memberof Cesium3DTilesetMetadata.prototype
* @type {}
* @readonly
* @private
*/
groupIds: {
get: function () {
return this._groupIds;
},
},
/**
* Metadata about the tileset as a whole.
*
* @memberof Cesium3DTilesetMetadata.prototype
* @type {TilesetMetadata}
* @readonly
* @private
*/
tileset: {
get: function () {
return this._tileset;
},
},
/**
* Statistics about the metadata.
* <p>
* See the {@link https://github.com/CesiumGS/3d-tiles/blob/main/extensions/3DTILES_metadata/schema/statistics.schema.json|statistics schema reference}
* in the 3D Tiles spec for the full set of properties.
* </p>
*
* @memberof Cesium3DTilesetMetadata.prototype
* @type {object}
* @readonly
* @private
*/
statistics: {
get: function () {
return this._statistics;
},
},
/**
* Extra user-defined properties.
*
* @memberof Cesium3DTilesetMetadata.prototype
* @type {*}
* @readonly
* @private
*/
extras: {
get: function () {
return this._extras;
},
},
/**
* An object containing extensions.
*
* @memberof Cesium3DTilesetMetadata.prototype
* @type {object}
* @readonly
* @private
*/
extensions: {
get: function () {
return this._extensions;
},
},
});
export default Cesium3DTilesetMetadata;
@@ -0,0 +1,131 @@
import Intersect from "../Core/Intersect.js";
import ManagedArray from "../Core/ManagedArray.js";
import Cesium3DTileRefine from "./Cesium3DTileRefine.js";
import Cesium3DTilesetTraversal from "./Cesium3DTilesetTraversal.js";
/**
* Traversal that loads all leaves that intersect the camera frustum.
* Used to determine ray-tileset intersections during a pickFromRayMostDetailed call.
*
* @alias Cesium3DTilesetMostDetailedTraversal
* @constructor
*
* @private
*/
function Cesium3DTilesetMostDetailedTraversal() {}
const traversal = {
stack: new ManagedArray(),
stackMaximumLength: 0,
};
/**
* Traverses a {@link Cesium3DTileset} to determine which tiles to load and render.
*
* @private
* @param {Cesium3DTileset} tileset
* @param {FrameState} frameState
* @returns {boolean} Whether the appropriate tile is ready for picking
*/
Cesium3DTilesetMostDetailedTraversal.selectTiles = function (
tileset,
frameState,
) {
tileset._selectedTiles.length = 0;
tileset._requestedTiles.length = 0;
tileset.hasMixedContent = false;
let ready = true;
const root = tileset.root;
root.updateVisibility(frameState);
if (!root.isVisible) {
return ready;
}
const { touchTile, visitTile } = Cesium3DTilesetTraversal;
const stack = traversal.stack;
stack.push(root);
while (stack.length > 0) {
traversal.stackMaximumLength = Math.max(
traversal.stackMaximumLength,
stack.length,
);
const tile = stack.pop();
const add = tile.refine === Cesium3DTileRefine.ADD;
const replace = tile.refine === Cesium3DTileRefine.REPLACE;
const traverse = canTraverse(tile);
if (traverse) {
updateAndPushChildren(tile, stack, frameState);
}
if (add || (replace && !traverse)) {
loadTile(tileset, tile);
touchTile(tile, frameState);
selectDesiredTile(tile, frameState);
if (tile.hasRenderableContent && !tile.contentAvailable) {
ready = false;
}
}
visitTile(tile, frameState);
}
traversal.stack.trim(traversal.stackMaximumLength);
return ready;
};
function canTraverse(tile) {
if (tile.children.length === 0) {
return false;
}
if (tile.hasTilesetContent || tile.hasImplicitContent) {
// Traverse external tileset to visit its root tile
// Don't traverse if the subtree is expired because it will be destroyed
return !tile.contentExpired;
}
if (tile.hasEmptyContent) {
return true;
}
return true; // Keep traversing until a leaf is hit
}
function updateAndPushChildren(tile, stack, frameState) {
const { children } = tile;
for (let i = 0; i < children.length; ++i) {
const child = children[i];
child.updateVisibility(frameState);
if (child.isVisible) {
stack.push(child);
}
}
}
function loadTile(tileset, tile) {
if (tile.hasUnloadedRenderableContent || tile.contentExpired) {
tile._priority = 0.0; // Highest priority
tileset._requestedTiles.push(tile);
}
}
function selectDesiredTile(tile, frameState) {
if (
tile.contentAvailable &&
tile.contentVisibility(frameState) !== Intersect.OUTSIDE
) {
tile.tileset._selectedTiles.push(tile);
}
}
export default Cesium3DTilesetMostDetailedTraversal;
@@ -0,0 +1,420 @@
import defined from "../Core/defined.js";
import ManagedArray from "../Core/ManagedArray.js";
import Cesium3DTileRefine from "./Cesium3DTileRefine.js";
import Cesium3DTilesetTraversal from "./Cesium3DTilesetTraversal.js";
/**
* Depth-first traversal that traverses all visible tiles and marks tiles for selection.
* Allows for skipping levels of the tree and rendering children and parent tiles simultaneously.
*
* @alias Cesium3DTilesetSkipTraversal
* @constructor
*
* @private
*/
function Cesium3DTilesetSkipTraversal() {}
const traversal = {
stack: new ManagedArray(),
stackMaximumLength: 0,
};
const descendantTraversal = {
stack: new ManagedArray(),
stackMaximumLength: 0,
};
const selectionTraversal = {
stack: new ManagedArray(),
stackMaximumLength: 0,
ancestorStack: new ManagedArray(),
ancestorStackMaximumLength: 0,
};
const descendantSelectionDepth = 2;
/**
* Traverses a {@link Cesium3DTileset} to determine which tiles to load and render.
*
* @private
* @param {Cesium3DTileset} tileset
* @param {FrameState} frameState
*/
Cesium3DTilesetSkipTraversal.selectTiles = function (tileset, frameState) {
tileset._requestedTiles.length = 0;
if (tileset.debugFreezeFrame) {
return;
}
tileset._selectedTiles.length = 0;
tileset._selectedTilesToStyle.length = 0;
tileset._emptyTiles.length = 0;
tileset.hasMixedContent = false;
const root = tileset.root;
Cesium3DTilesetTraversal.updateTile(root, frameState);
if (!root.isVisible) {
return;
}
if (
root.getScreenSpaceError(frameState, true) <=
tileset.memoryAdjustedScreenSpaceError
) {
return;
}
executeTraversal(root, frameState);
traverseAndSelect(root, frameState);
traversal.stack.trim(traversal.stackMaximumLength);
descendantTraversal.stack.trim(descendantTraversal.stackMaximumLength);
selectionTraversal.stack.trim(selectionTraversal.stackMaximumLength);
selectionTraversal.ancestorStack.trim(
selectionTraversal.ancestorStackMaximumLength,
);
// Update the priority for any requests found during traversal
// Update after traversal so that min and max values can be used to normalize priority values
const requestedTiles = tileset._requestedTiles;
for (let i = 0; i < requestedTiles.length; ++i) {
requestedTiles[i].updatePriority();
}
};
/**
* Mark descendant tiles for rendering, and update as needed
*
* @private
* @param {Cesium3DTile} root
* @param {FrameState} frameState
*/
function selectDescendants(root, frameState) {
const { updateTile, touchTile, selectTile } = Cesium3DTilesetTraversal;
const stack = descendantTraversal.stack;
stack.push(root);
while (stack.length > 0) {
descendantTraversal.stackMaximumLength = Math.max(
descendantTraversal.stackMaximumLength,
stack.length,
);
const tile = stack.pop();
const children = tile.children;
for (let i = 0; i < children.length; ++i) {
const child = children[i];
if (child.isVisible) {
if (child.contentAvailable) {
updateTile(child, frameState);
touchTile(child, frameState);
selectTile(child, frameState);
} else if (child._depth - root._depth < descendantSelectionDepth) {
// Continue traversing, but not too far
stack.push(child);
}
}
}
}
}
/**
* Mark a tile as selected if it has content available.
* If its content is not available, and we are skipping levels of detail,
* select an ancestor or descendant tile instead
*
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
*/
function selectDesiredTile(tile, frameState) {
// If this tile is not loaded attempt to select its ancestor instead
const loadedTile = tile.contentAvailable
? tile
: tile._ancestorWithContentAvailable;
if (defined(loadedTile)) {
// Tiles will actually be selected in traverseAndSelect
loadedTile._shouldSelect = true;
} else {
// If no ancestors are ready traverse down and select tiles to minimize empty regions.
// This happens often for immediatelyLoadDesiredLevelOfDetail where parent tiles are not necessarily loaded before zooming out.
selectDescendants(tile, frameState);
}
}
/**
* Update links to the ancestor tiles that have content
*
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
*/
function updateTileAncestorContentLinks(tile, frameState) {
tile._ancestorWithContent = undefined;
tile._ancestorWithContentAvailable = undefined;
const { parent } = tile;
if (!defined(parent)) {
return;
}
const parentHasContent =
!parent.hasUnloadedRenderableContent ||
parent._requestedFrame === frameState.frameNumber;
// ancestorWithContent is an ancestor that has content or has the potential to have
// content. Used in conjunction with tileset.skipLevels to know when to skip a tile.
tile._ancestorWithContent = parentHasContent
? parent
: parent._ancestorWithContent;
// ancestorWithContentAvailable is an ancestor that is rendered if a desired tile is not loaded
tile._ancestorWithContentAvailable = parent.contentAvailable
? parent
: parent._ancestorWithContentAvailable;
}
/**
* Determine if a tile has reached the limit of level of detail skipping.
* If so, it should _not_ be skipped: it should be loaded and rendered
*
* @private
* @param {Cesium3DTileset} tileset
* @param {Cesium3DTile} tile
* @returns {boolean} true if this tile should not be skipped
*/
function reachedSkippingThreshold(tileset, tile) {
const ancestor = tile._ancestorWithContent;
return (
!tileset.immediatelyLoadDesiredLevelOfDetail &&
(tile._priorityProgressiveResolutionScreenSpaceErrorLeaf ||
(defined(ancestor) &&
tile._screenSpaceError <
ancestor._screenSpaceError / tileset.skipScreenSpaceErrorFactor &&
tile._depth > ancestor._depth + tileset.skipLevels))
);
}
/**
* @private
* @param {Cesium3DTile} tile
* @param {ManagedArray} stack
* @param {FrameState} frameState
* @returns {boolean}
*/
function updateAndPushChildren(tile, stack, frameState) {
const { tileset, children } = tile;
const { updateTile, loadTile, touchTile } = Cesium3DTilesetTraversal;
for (let i = 0; i < children.length; ++i) {
updateTile(children[i], frameState);
}
// Sort by distance to take advantage of early Z and reduce artifacts
children.sort(Cesium3DTilesetTraversal.sortChildrenByDistanceToCamera);
let anyChildrenVisible = false;
for (let i = 0; i < children.length; ++i) {
const child = children[i];
if (child.isVisible) {
stack.push(child);
anyChildrenVisible = true;
} else if (tileset.loadSiblings) {
loadTile(child, frameState);
touchTile(child, frameState);
}
}
return anyChildrenVisible;
}
/**
* Determine if a tile is part of the base traversal.
* If not, this tile could be considered for level of detail skipping
*
* @private
* @param {Cesium3DTile} tile
* @param {number} baseScreenSpaceError
* @returns {boolean}
*/
function inBaseTraversal(tile, baseScreenSpaceError) {
const { tileset } = tile;
if (tileset.immediatelyLoadDesiredLevelOfDetail) {
return false;
}
if (!defined(tile._ancestorWithContent)) {
// Include root or near-root tiles in the base traversal so there is something to select up to
return true;
}
if (tile._screenSpaceError === 0.0) {
// If a leaf, use parent's SSE
return tile.parent._screenSpaceError > baseScreenSpaceError;
}
return tile._screenSpaceError > baseScreenSpaceError;
}
/**
* Depth-first traversal that traverses all visible tiles and marks tiles for selection.
* Tiles that have a greater screen space error than the base screen space error are part of the base traversal,
* all other tiles are part of the skip traversal. The skip traversal allows for skipping levels of the tree
* and rendering children and parent tiles simultaneously.
*
* @private
* @param {Cesium3DTile} root
* @param {FrameState} frameState
*/
function executeTraversal(root, frameState) {
const { tileset } = root;
const baseScreenSpaceError = tileset.immediatelyLoadDesiredLevelOfDetail
? Number.MAX_VALUE
: Math.max(
tileset.baseScreenSpaceError,
tileset.memoryAdjustedScreenSpaceError,
);
const { canTraverse, loadTile, visitTile, touchTile } =
Cesium3DTilesetTraversal;
const stack = traversal.stack;
stack.push(root);
while (stack.length > 0) {
traversal.stackMaximumLength = Math.max(
traversal.stackMaximumLength,
stack.length,
);
const tile = stack.pop();
updateTileAncestorContentLinks(tile, frameState);
const parent = tile.parent;
const parentRefines = !defined(parent) || parent._refines;
tile._refines = canTraverse(tile)
? updateAndPushChildren(tile, stack, frameState) && parentRefines
: false;
const stoppedRefining = !tile._refines && parentRefines;
if (!tile.hasRenderableContent) {
// Add empty tile just to show its debug bounding volume
// If the tile has tileset content load the external tileset
// If the tile cannot refine further select its nearest loaded ancestor
tileset._emptyTiles.push(tile);
loadTile(tile, frameState);
if (stoppedRefining) {
selectDesiredTile(tile, frameState);
}
} else if (tile.refine === Cesium3DTileRefine.ADD) {
// Additive tiles are always loaded and selected
selectDesiredTile(tile, frameState);
loadTile(tile, frameState);
} else if (tile.refine === Cesium3DTileRefine.REPLACE) {
if (inBaseTraversal(tile, baseScreenSpaceError)) {
// Always load tiles in the base traversal
// Select tiles that can't refine further
loadTile(tile, frameState);
if (stoppedRefining) {
selectDesiredTile(tile, frameState);
}
} else if (stoppedRefining) {
// In skip traversal, load and select tiles that can't refine further
selectDesiredTile(tile, frameState);
loadTile(tile, frameState);
} else if (reachedSkippingThreshold(tileset, tile)) {
// In skip traversal, load tiles that aren't skipped
loadTile(tile, frameState);
}
}
visitTile(tile, frameState);
touchTile(tile, frameState);
}
}
/**
* Traverse the tree and check if their selected frame is the current frame. If so, add it to a selection queue.
* This is a preorder traversal so children tiles are selected before ancestor tiles.
*
* The reason for the preorder traversal is so that tiles can easily be marked with their
* selection depth. A tile's _selectionDepth is its depth in the tree where all non-selected tiles are removed.
* This property is important for use in the stencil test because we want to render deeper tiles on top of their
* ancestors. If a tileset is very deep, the depth is unlikely to fit into the stencil buffer.
*
* We want to select children before their ancestors because there is no guarantee on the relationship between
* the children's z-depth and the ancestor's z-depth. We cannot rely on Z because we want the child to appear on top
* of ancestor regardless of true depth. The stencil tests used require children to be drawn first.
*
* NOTE: 3D Tiles uses 3 bits from the stencil buffer meaning this will not work when there is a chain of
* selected tiles that is deeper than 7. This is not very likely.
*
* @private
* @param {Cesium3DTile} root
* @param {FrameState} frameState
*/
function traverseAndSelect(root, frameState) {
const { selectTile, canTraverse } = Cesium3DTilesetTraversal;
const { stack, ancestorStack } = selectionTraversal;
let lastAncestor;
stack.push(root);
while (stack.length > 0 || ancestorStack.length > 0) {
selectionTraversal.stackMaximumLength = Math.max(
selectionTraversal.stackMaximumLength,
stack.length,
);
selectionTraversal.ancestorStackMaximumLength = Math.max(
selectionTraversal.ancestorStackMaximumLength,
ancestorStack.length,
);
if (ancestorStack.length > 0) {
const waitingTile = ancestorStack.peek();
if (waitingTile._stackLength === stack.length) {
ancestorStack.pop();
if (waitingTile !== lastAncestor) {
waitingTile._finalResolution = false;
}
selectTile(waitingTile, frameState);
continue;
}
}
const tile = stack.pop();
if (!defined(tile)) {
// stack is empty but ancestorStack isn't
continue;
}
const traverse = canTraverse(tile);
if (tile._shouldSelect) {
if (tile.refine === Cesium3DTileRefine.ADD) {
selectTile(tile, frameState);
} else {
tile._selectionDepth = ancestorStack.length;
if (tile._selectionDepth > 0) {
tile.tileset.hasMixedContent = true;
}
lastAncestor = tile;
if (!traverse) {
selectTile(tile, frameState);
continue;
}
ancestorStack.push(tile);
tile._stackLength = stack.length;
}
}
if (traverse) {
const children = tile.children;
for (let i = 0; i < children.length; ++i) {
const child = children[i];
if (child.isVisible) {
stack.push(child);
}
}
}
}
}
export default Cesium3DTilesetSkipTraversal;
+195
View File
@@ -0,0 +1,195 @@
import defined from "../Core/defined.js";
import Model3DTileContent from "./Model/Model3DTileContent.js";
/**
* @private
*/
function Cesium3DTilesetStatistics() {
// Rendering statistics
this.selected = 0;
this.visited = 0;
// Loading statistics
this.numberOfCommands = 0;
this.numberOfAttemptedRequests = 0;
this.numberOfPendingRequests = 0;
this.numberOfTilesProcessing = 0;
this.numberOfTilesWithContentReady = 0; // Number of tiles with content loaded, does not include empty tiles
this.numberOfTilesTotal = 0; // Number of tiles in tileset JSON (and other tileset JSON files as they are loaded)
this.numberOfLoadedTilesTotal = 0; // Running total of loaded tiles for the lifetime of the session
// Features statistics
this.numberOfFeaturesSelected = 0; // Number of features rendered
this.numberOfFeaturesLoaded = 0; // Number of features in memory
this.numberOfPointsSelected = 0;
this.numberOfPointsLoaded = 0;
this.numberOfTrianglesSelected = 0;
// Styling statistics
this.numberOfTilesStyled = 0;
this.numberOfFeaturesStyled = 0;
// Optimization statistics
this.numberOfTilesCulledWithChildrenUnion = 0;
// Memory statistics
this.geometryByteLength = 0;
this.texturesByteLength = 0;
this.texturesReferenceCounterById = {};
this.batchTableByteLength = 0; // batch textures and any binary metadata properties not otherwise accounted for
}
Cesium3DTilesetStatistics.prototype.clear = function () {
this.selected = 0;
this.visited = 0;
this.numberOfCommands = 0;
this.numberOfAttemptedRequests = 0;
this.numberOfFeaturesSelected = 0;
this.numberOfPointsSelected = 0;
this.numberOfTrianglesSelected = 0;
this.numberOfTilesStyled = 0;
this.numberOfFeaturesStyled = 0;
this.numberOfTilesCulledWithChildrenUnion = 0;
};
/**
* Increment the counters for the points, triangles, and features
* that are currently selected for rendering.
*
* This will be called recursively for the given content and
* all its inner contents
*
* @param {Cesium3DTileContent} content
*/
Cesium3DTilesetStatistics.prototype.incrementSelectionCounts = function (
content,
) {
this.numberOfFeaturesSelected += content.featuresLength;
this.numberOfPointsSelected += content.pointsLength;
this.numberOfTrianglesSelected += content.trianglesLength;
// Recursive calls on all inner contents
const contents = content.innerContents;
if (defined(contents)) {
const length = contents.length;
for (let i = 0; i < length; ++i) {
this.incrementSelectionCounts(contents[i]);
}
}
};
/**
* Increment the counters for the number of features and points that
* are currently loaded, and the lengths (size in bytes) of the
* occupied memory.
*
* This will be called recursively for the given content and
* all its inner contents
*
* @param {Cesium3DTileContent} content
*/
Cesium3DTilesetStatistics.prototype.incrementLoadCounts = function (content) {
this.numberOfFeaturesLoaded += content.featuresLength;
this.numberOfPointsLoaded += content.pointsLength;
this.geometryByteLength += content.geometryByteLength;
this.batchTableByteLength += content.batchTableByteLength;
// When the content is not a `Model3DTileContent`, then its
// textures byte length is added directly
if (!(content instanceof Model3DTileContent)) {
this.texturesByteLength += content.texturesByteLength;
} else {
// When the content is a `Model3DTileContent`, then increment the
// reference counter for all its textures. The byte length of any
// newly tracked texture to the total textures byte length
const textureIds = content.getTextureIds();
for (const textureId of textureIds) {
const referenceCounter =
this.texturesReferenceCounterById[textureId] ?? 0;
if (referenceCounter === 0) {
const textureByteLength = content.getTextureByteLengthById(textureId);
this.texturesByteLength += textureByteLength;
}
this.texturesReferenceCounterById[textureId] = referenceCounter + 1;
}
}
// Recursive calls on all inner contents
const contents = content.innerContents;
if (defined(contents)) {
const length = contents.length;
for (let i = 0; i < length; ++i) {
this.incrementLoadCounts(contents[i]);
}
}
};
/**
* Decrement the counters for the number of features and points that
* are currently loaded, and the lengths (size in bytes) of the
* occupied memory.
*
* This will be called recursively for the given content and
* all its inner contents
*
* @param {Cesium3DTileContent} content
*/
Cesium3DTilesetStatistics.prototype.decrementLoadCounts = function (content) {
this.numberOfFeaturesLoaded -= content.featuresLength;
this.numberOfPointsLoaded -= content.pointsLength;
this.geometryByteLength -= content.geometryByteLength;
this.batchTableByteLength -= content.batchTableByteLength;
// When the content is not a `Model3DTileContent`, then its
// textures byte length is subtracted directly
if (!(content instanceof Model3DTileContent)) {
this.texturesByteLength -= content.texturesByteLength;
} else {
// When the content is a `Model3DTileContent`, then decrement the
// reference counter for all its textures. The byte length of any
// texture that is no longer references is subtracted from the
// total textures byte length
const textureIds = content.getTextureIds();
for (const textureId of textureIds) {
const referenceCounter = this.texturesReferenceCounterById[textureId];
if (referenceCounter === 1) {
delete this.texturesReferenceCounterById[textureId];
const textureByteLength = content.getTextureByteLengthById(textureId);
this.texturesByteLength -= textureByteLength;
} else {
this.texturesReferenceCounterById[textureId] = referenceCounter - 1;
}
}
}
// Recursive calls on all inner contents
const contents = content.innerContents;
if (defined(contents)) {
const length = contents.length;
for (let i = 0; i < length; ++i) {
this.decrementLoadCounts(contents[i]);
}
}
};
Cesium3DTilesetStatistics.clone = function (statistics, result) {
result.selected = statistics.selected;
result.visited = statistics.visited;
result.numberOfCommands = statistics.numberOfCommands;
result.numberOfAttemptedRequests = statistics.numberOfAttemptedRequests;
result.numberOfPendingRequests = statistics.numberOfPendingRequests;
result.numberOfTilesProcessing = statistics.numberOfTilesProcessing;
result.numberOfTilesWithContentReady =
statistics.numberOfTilesWithContentReady;
result.numberOfTilesTotal = statistics.numberOfTilesTotal;
result.numberOfFeaturesSelected = statistics.numberOfFeaturesSelected;
result.numberOfFeaturesLoaded = statistics.numberOfFeaturesLoaded;
result.numberOfPointsSelected = statistics.numberOfPointsSelected;
result.numberOfPointsLoaded = statistics.numberOfPointsLoaded;
result.numberOfTrianglesSelected = statistics.numberOfTrianglesSelected;
result.numberOfTilesStyled = statistics.numberOfTilesStyled;
result.numberOfFeaturesStyled = statistics.numberOfFeaturesStyled;
result.numberOfTilesCulledWithChildrenUnion =
statistics.numberOfTilesCulledWithChildrenUnion;
result.geometryByteLength = statistics.geometryByteLength;
result.texturesByteLength = statistics.texturesByteLength;
result.texturesReferenceCounterById = {
...statistics.texturesReferenceCounterById,
};
result.batchTableByteLength = statistics.batchTableByteLength;
};
export default Cesium3DTilesetStatistics;
+323
View File
@@ -0,0 +1,323 @@
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
import Intersect from "../Core/Intersect.js";
import Cesium3DTileOptimizationHint from "./Cesium3DTileOptimizationHint.js";
import Cesium3DTileRefine from "./Cesium3DTileRefine.js";
/**
* Traverses a {@link Cesium3DTileset} to determine which tiles to load and render.
* This type describes an interface and is not intended to be instantiated directly.
*
* @alias Cesium3DTilesetTraversal
* @constructor
* @abstract
*
* @see Cesium3DTilesetBaseTraversal
* @see Cesium3DTilesetSkipTraversal
* @see Cesium3DTilesetMostDetailedTraversal
*
* @private
*/
function Cesium3DTilesetTraversal() {}
/**
* Traverses a {@link Cesium3DTileset} to determine which tiles to load and render.
*
* @private
* @param {Cesium3DTileset} tileset
* @param {FrameState} frameState
*/
Cesium3DTilesetTraversal.selectTiles = function (tileset, frameState) {
DeveloperError.throwInstantiationError();
};
/**
* Sort by farthest child first since this is going on a stack
*
* @private
* @param {Cesium3DTile} a
* @param {Cesium3DTile} b
* @returns {number}
*/
Cesium3DTilesetTraversal.sortChildrenByDistanceToCamera = function (a, b) {
if (b._distanceToCamera === 0 && a._distanceToCamera === 0) {
return b._centerZDepth - a._centerZDepth;
}
return b._distanceToCamera - a._distanceToCamera;
};
/**
* Determine if a tile can and should be traversed for children tiles that
* would contribute to rendering the current view
*
* @private
* @param {Cesium3DTile} tile
* @returns {boolean}
*/
Cesium3DTilesetTraversal.canTraverse = function (tile) {
if (tile.children.length === 0) {
return false;
}
if (tile.hasTilesetContent || tile.hasImplicitContent) {
// Traverse external tileset to visit its root tile
// Don't traverse if the subtree is expired because it will be destroyed
return !tile.contentExpired;
}
return tile._screenSpaceError > tile.tileset.memoryAdjustedScreenSpaceError;
};
/**
* Mark a tile as selected, and add it to the tileset's list of selected tiles
*
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
*/
Cesium3DTilesetTraversal.selectTile = function (tile, frameState) {
if (tile.contentVisibility(frameState) === Intersect.OUTSIDE) {
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.
content.featurePropertiesDirty = false;
tile.lastStyleTime = 0; // Force applying the style to this tile
tileset._selectedTilesToStyle.push(tile);
} 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);
};
/**
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
*/
Cesium3DTilesetTraversal.visitTile = function (tile, frameState) {
++tile.tileset._statistics.visited;
tile._visitedFrame = frameState.frameNumber;
};
/**
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
*/
Cesium3DTilesetTraversal.touchTile = function (tile, frameState) {
if (tile._touchedFrame === frameState.frameNumber) {
// Prevents another pass from touching the frame again
return;
}
tile.tileset._cache.touch(tile);
tile._touchedFrame = frameState.frameNumber;
};
/**
* Add a tile to the list of requested tiles, if appropriate
*
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
*/
Cesium3DTilesetTraversal.loadTile = function (tile, frameState) {
const { tileset } = tile;
if (
tile._requestedFrame === frameState.frameNumber ||
(!tile.hasUnloadedRenderableContent && !tile.contentExpired)
) {
return;
}
if (!isOnScreenLongEnough(tile, frameState)) {
return;
}
const cameraHasNotStoppedMovingLongEnough =
frameState.camera.timeSinceMoved < tileset.foveatedTimeDelay;
if (tile.priorityDeferred && cameraHasNotStoppedMovingLongEnough) {
return;
}
tile._requestedFrame = frameState.frameNumber;
tileset._requestedTiles.push(tile);
};
/**
* Prevent unnecessary loads while camera is moving by getting the ratio of travel distance to tile size.
*
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
* @returns {boolean}
*/
function isOnScreenLongEnough(tile, frameState) {
const { tileset } = tile;
if (!tileset._cullRequestsWhileMoving) {
return true;
}
const { positionWCDeltaMagnitude, positionWCDeltaMagnitudeLastFrame } =
frameState.camera;
const deltaMagnitude =
positionWCDeltaMagnitude !== 0.0
? positionWCDeltaMagnitude
: positionWCDeltaMagnitudeLastFrame;
// How do n frames of this movement compare to the tile's physical size.
const diameter = Math.max(tile.boundingSphere.radius * 2.0, 1.0);
const movementRatio =
(tileset.cullRequestsWhileMovingMultiplier * deltaMagnitude) / diameter;
return movementRatio < 1.0;
}
/**
* Reset some of the tile's flags and re-evaluate visibility and priority
*
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
*/
Cesium3DTilesetTraversal.updateTile = function (tile, frameState) {
updateTileVisibility(tile, frameState);
tile.updateExpiration();
tile._wasMinPriorityChild = false;
tile._priorityHolder = tile;
updateMinimumMaximumPriority(tile);
// SkipLOD
tile._shouldSelect = false;
tile._finalResolution = true;
};
/**
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
*/
function updateTileVisibility(tile, frameState) {
tile.updateVisibility(frameState);
if (!tile.isVisible) {
return;
}
const hasChildren = tile.children.length > 0;
if ((tile.hasTilesetContent || tile.hasImplicitContent) && hasChildren) {
// Use the root tile's visibility instead of this tile's visibility.
// The root tile may be culled by the children bounds optimization in which
// case this tile should also be culled.
const child = tile.children[0];
updateTileVisibility(child, frameState);
tile._visible = child._visible;
return;
}
if (meetsScreenSpaceErrorEarly(tile, frameState)) {
tile._visible = false;
return;
}
// Optimization - if none of the tile's children are visible then this tile isn't visible
const replace = tile.refine === Cesium3DTileRefine.REPLACE;
const useOptimization =
tile._optimChildrenWithinParent ===
Cesium3DTileOptimizationHint.USE_OPTIMIZATION;
if (replace && useOptimization && hasChildren) {
if (!anyChildrenVisible(tile, frameState)) {
++tile.tileset._statistics.numberOfTilesCulledWithChildrenUnion;
tile._visible = false;
return;
}
}
}
/**
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
* @returns {boolean}
*/
function meetsScreenSpaceErrorEarly(tile, frameState) {
const { parent, tileset } = tile;
if (
!defined(parent) ||
parent.hasTilesetContent ||
parent.hasImplicitContent ||
parent.refine !== Cesium3DTileRefine.ADD
) {
return false;
}
// Use parent's geometric error with child's box to see if the tile already meet the SSE
return (
tile.getScreenSpaceError(frameState, true) <=
tileset.memoryAdjustedScreenSpaceError
);
}
/**
* @private
* @param {Cesium3DTile} tile
* @param {FrameState} frameState
* @returns {boolean}
*/
function anyChildrenVisible(tile, frameState) {
let anyVisible = false;
const children = tile.children;
for (let i = 0; i < children.length; ++i) {
const child = children[i];
child.updateVisibility(frameState);
anyVisible = anyVisible || child.isVisible;
}
return anyVisible;
}
/**
* @private
* @param {Cesium3DTile} tile
*/
function updateMinimumMaximumPriority(tile) {
const minimumPriority = tile.tileset._minimumPriority;
const maximumPriority = tile.tileset._maximumPriority;
const priorityHolder = tile._priorityHolder;
maximumPriority.distance = Math.max(
priorityHolder._distanceToCamera,
maximumPriority.distance,
);
minimumPriority.distance = Math.min(
priorityHolder._distanceToCamera,
minimumPriority.distance,
);
maximumPriority.depth = Math.max(tile._depth, maximumPriority.depth);
minimumPriority.depth = Math.min(tile._depth, minimumPriority.depth);
maximumPriority.foveatedFactor = Math.max(
priorityHolder._foveatedFactor,
maximumPriority.foveatedFactor,
);
minimumPriority.foveatedFactor = Math.min(
priorityHolder._foveatedFactor,
minimumPriority.foveatedFactor,
);
maximumPriority.reverseScreenSpaceError = Math.max(
tile._priorityReverseScreenSpaceError,
maximumPriority.reverseScreenSpaceError,
);
minimumPriority.reverseScreenSpaceError = Math.min(
tile._priorityReverseScreenSpaceError,
minimumPriority.reverseScreenSpaceError,
);
}
export default Cesium3DTilesetTraversal;
+61
View File
@@ -0,0 +1,61 @@
import Cartesian3 from "../Core/Cartesian3.js";
import Check from "../Core/Check.js";
import CesiumMath from "../Core/Math.js";
/**
* A ParticleEmitter that emits particles from a circle.
* Particles will be positioned within a circle and have initial velocities going along the z vector.
*
* @alias CircleEmitter
* @constructor
*
* @param {number} [radius=1.0] The radius of the circle in meters.
*/
function CircleEmitter(radius) {
radius = radius ?? 1.0;
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number.greaterThan("radius", radius, 0.0);
//>>includeEnd('debug');
this._radius = radius ?? 1.0;
}
Object.defineProperties(CircleEmitter.prototype, {
/**
* The radius of the circle in meters.
* @memberof CircleEmitter.prototype
* @type {number}
* @default 1.0
*/
radius: {
get: function () {
return this._radius;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number.greaterThan("value", value, 0.0);
//>>includeEnd('debug');
this._radius = value;
},
},
});
/**
* Initializes the given {@link Particle} by setting it's position and velocity.
*
* @private
* @param {Particle} particle The particle to initialize.
*/
CircleEmitter.prototype.emit = function (particle) {
const theta = CesiumMath.randomBetween(0.0, CesiumMath.TWO_PI);
const rad = CesiumMath.randomBetween(0.0, this._radius);
const x = rad * Math.cos(theta);
const y = rad * Math.sin(theta);
const z = 0.0;
particle.position = Cartesian3.fromElements(x, y, z, particle.position);
particle.velocity = Cartesian3.clone(Cartesian3.UNIT_Z, particle.velocity);
};
export default CircleEmitter;
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
/**
* Whether a classification affects terrain, 3D Tiles or both.
*
* @enum {number}
*/
const ClassificationType = {
/**
* Only terrain will be classified.
*
* @type {number}
* @constant
*/
TERRAIN: 0,
/**
* Only 3D Tiles will be classified.
*
* @type {number}
* @constant
*/
CESIUM_3D_TILE: 1,
/**
* Both terrain and 3D Tiles will be classified.
*
* @type {number}
* @constant
*/
BOTH: 2,
};
/**
* @private
*/
ClassificationType.NUMBER_OF_CLASSIFICATION_TYPES = 3;
Object.freeze(ClassificationType);
export default ClassificationType;
+184
View File
@@ -0,0 +1,184 @@
import Cartesian3 from "../Core/Cartesian3.js";
import Check from "../Core/Check.js";
import defined from "../Core/defined.js";
/**
* A Plane in Hessian Normal form to be used with {@link ClippingPlaneCollection}.
* Compatible with mathematics functions in {@link Plane}
*
* @alias ClippingPlane
* @constructor
*
* @param {Cartesian3} normal The plane's normal (normalized).
* @param {number} distance The shortest distance from the origin to the plane. The sign of
* <code>distance</code> determines which side of the plane the origin
* is on. If <code>distance</code> is positive, the origin is in the half-space
* in the direction of the normal; if negative, the origin is in the half-space
* opposite to the normal; if zero, the plane passes through the origin.
*/
function ClippingPlane(normal, distance) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("normal", normal);
Check.typeOf.number("distance", distance);
//>>includeEnd('debug');
this._distance = distance;
this._normal = new UpdateChangedCartesian3(normal, this);
this.onChangeCallback = undefined;
this.index = -1; // to be set by ClippingPlaneCollection
}
Object.defineProperties(ClippingPlane.prototype, {
/**
* The shortest distance from the origin to the plane. The sign of
* <code>distance</code> determines which side of the plane the origin
* is on. If <code>distance</code> is positive, the origin is in the half-space
* in the direction of the normal; if negative, the origin is in the half-space
* opposite to the normal; if zero, the plane passes through the origin.
*
* @type {number}
* @memberof ClippingPlane.prototype
*/
distance: {
get: function () {
return this._distance;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("value", value);
//>>includeEnd('debug');
if (defined(this.onChangeCallback) && value !== this._distance) {
this.onChangeCallback(this.index);
}
this._distance = value;
},
},
/**
* The plane's normal.
*
* @type {Cartesian3}
* @memberof ClippingPlane.prototype
*/
normal: {
get: function () {
return this._normal;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("value", value);
//>>includeEnd('debug');
if (
defined(this.onChangeCallback) &&
!Cartesian3.equals(this._normal._cartesian3, value)
) {
this.onChangeCallback(this.index);
}
// Set without firing callback again
Cartesian3.clone(value, this._normal._cartesian3);
},
},
});
/**
* Create a ClippingPlane from a Plane object.
*
* @param {Plane} plane The plane containing parameters to copy
* @param {ClippingPlane} [result] The object on which to store the result
* @returns {ClippingPlane} The ClippingPlane generated from the plane's parameters.
*/
ClippingPlane.fromPlane = function (plane, result) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("plane", plane);
//>>includeEnd('debug');
if (!defined(result)) {
result = new ClippingPlane(plane.normal, plane.distance);
} else {
result.normal = plane.normal;
result.distance = plane.distance;
}
return result;
};
/**
* Clones the ClippingPlane without setting its ownership.
* @param {ClippingPlane} clippingPlane The ClippingPlane to be cloned
* @param {ClippingPlane} [result] The object on which to store the cloned parameters.
* @returns {ClippingPlane} a clone of the input ClippingPlane
*/
ClippingPlane.clone = function (clippingPlane, result) {
if (!defined(result)) {
return new ClippingPlane(clippingPlane.normal, clippingPlane.distance);
}
result.normal = clippingPlane.normal;
result.distance = clippingPlane.distance;
return result;
};
/**
* Wrapper on Cartesian3 that allows detection of Plane changes from "members of members," for example:
*
* const clippingPlane = new ClippingPlane(...);
* clippingPlane.normal.z = -1.0;
*
* @private
*/
function UpdateChangedCartesian3(normal, clippingPlane) {
this._clippingPlane = clippingPlane;
this._cartesian3 = Cartesian3.clone(normal);
}
Object.defineProperties(UpdateChangedCartesian3.prototype, {
x: {
get: function () {
return this._cartesian3.x;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("value", value);
//>>includeEnd('debug');
if (
defined(this._clippingPlane.onChangeCallback) &&
value !== this._cartesian3.x
) {
this._clippingPlane.onChangeCallback(this._clippingPlane.index);
}
this._cartesian3.x = value;
},
},
y: {
get: function () {
return this._cartesian3.y;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("value", value);
//>>includeEnd('debug');
if (
defined(this._clippingPlane.onChangeCallback) &&
value !== this._cartesian3.y
) {
this._clippingPlane.onChangeCallback(this._clippingPlane.index);
}
this._cartesian3.y = value;
},
},
z: {
get: function () {
return this._cartesian3.z;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("value", value);
//>>includeEnd('debug');
if (
defined(this._clippingPlane.onChangeCallback) &&
value !== this._cartesian3.z
) {
this._clippingPlane.onChangeCallback(this._clippingPlane.index);
}
this._cartesian3.z = value;
},
},
});
export default ClippingPlane;
+764
View File
@@ -0,0 +1,764 @@
import AttributeCompression from "../Core/AttributeCompression.js";
import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Cartesian4 from "../Core/Cartesian4.js";
import Check from "../Core/Check.js";
import Color from "../Core/Color.js";
import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
import Event from "../Core/Event.js";
import Intersect from "../Core/Intersect.js";
import Matrix4 from "../Core/Matrix4.js";
import PixelFormat from "../Core/PixelFormat.js";
import Plane from "../Core/Plane.js";
import ContextLimits from "../Renderer/ContextLimits.js";
import PixelDatatype from "../Renderer/PixelDatatype.js";
import Sampler from "../Renderer/Sampler.js";
import Texture from "../Renderer/Texture.js";
import ClippingPlane from "./ClippingPlane.js";
/**
* Specifies a set of clipping planes. Clipping planes selectively disable rendering in a region on the
* outside of the specified list of {@link ClippingPlane} objects for a single gltf model, 3D Tileset, or the globe.
* <p>
* In general the clipping planes' coordinates are relative to the object they're attached to, so a plane with distance set to 0 will clip
* through the center of the object.
* </p>
* <p>
* For 3D Tiles, the root tile's transform is used to position the clipping planes. If a transform is not defined, the root tile's {@link Cesium3DTile#boundingSphere} is used instead.
* </p>
*
* @alias ClippingPlaneCollection
* @constructor
*
* @param {object} [options] Object with the following properties:
* @param {ClippingPlane[]} [options.planes=[]] An array of {@link ClippingPlane} objects used to selectively disable rendering on the outside of each plane.
* @param {boolean} [options.enabled=true] Determines whether the clipping planes are active.
* @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix specifying an additional transform relative to the clipping planes original coordinate system.
* @param {boolean} [options.unionClippingRegions=false] If true, a region will be clipped if it is on the outside of any plane in the collection. Otherwise, a region will only be clipped if it is on the outside of every plane.
* @param {Color} [options.edgeColor=Color.WHITE] The color applied to highlight the edge along which an object is clipped.
* @param {number} [options.edgeWidth=0.0] The width, in pixels, of the highlight applied to the edge along which an object is clipped.
*
* @demo {@link https://sandcastle.cesium.com/?id=3d-tiles-clipping-planes|Clipping 3D Tiles and glTF models.}
* @demo {@link https://sandcastle.cesium.com/?id=terrain-clipping-planes|Clipping the Globe.}
*
* @example
* // This clipping plane's distance is positive, which means its normal
* // is facing the origin. This will clip everything that is behind
* // the plane, which is anything with y coordinate < -5.
* const clippingPlanes = new Cesium.ClippingPlaneCollection({
* planes : [
* new Cesium.ClippingPlane(new Cesium.Cartesian3(0.0, 1.0, 0.0), 5.0)
* ],
* });
* // Create an entity and attach the ClippingPlaneCollection to the model.
* const entity = viewer.entities.add({
* position : Cesium.Cartesian3.fromDegrees(-123.0744619, 44.0503706, 10000),
* model : {
* uri : 'model.gltf',
* minimumPixelSize : 128,
* maximumScale : 20000,
* clippingPlanes : clippingPlanes
* }
* });
* viewer.zoomTo(entity);
*/
function ClippingPlaneCollection(options) {
options = options ?? Frozen.EMPTY_OBJECT;
this._planes = [];
// Do partial texture updates if just one plane is dirty.
// If many planes are dirty, refresh the entire texture.
this._dirtyIndex = -1;
this._multipleDirtyPlanes = false;
this._enabled = options.enabled ?? true;
/**
* The 4x4 transformation matrix specifying an additional transform relative to the clipping planes
* original coordinate system.
*
* @type {Matrix4}
* @default Matrix4.IDENTITY
*/
this.modelMatrix = Matrix4.clone(options.modelMatrix ?? Matrix4.IDENTITY);
/**
* The color applied to highlight the edge along which an object is clipped.
*
* @type {Color}
* @default Color.WHITE
*/
this.edgeColor = Color.clone(options.edgeColor ?? Color.WHITE);
/**
* The width, in pixels, of the highlight applied to the edge along which an object is clipped.
*
* @type {number}
* @default 0.0
*/
this.edgeWidth = options.edgeWidth ?? 0.0;
/**
* An event triggered when a new clipping plane is added to the collection. Event handlers
* are passed the new plane and the index at which it was added.
* @type {Event}
* @readonly
*/
this.planeAdded = new Event();
/**
* An event triggered when a new clipping plane is removed from the collection. Event handlers
* are passed the new plane and the index from which it was removed.
* @type {Event}
* @readonly
*/
this.planeRemoved = new Event();
// If this ClippingPlaneCollection has an owner, only its owner should update or destroy it.
// This is because in a Cesium3DTileset multiple models may reference the tileset's ClippingPlaneCollection.
this._owner = undefined;
const unionClippingRegions = options.unionClippingRegions ?? false;
this._unionClippingRegions = unionClippingRegions;
this._testIntersection = unionClippingRegions
? unionIntersectFunction
: defaultIntersectFunction;
this._uint8View = undefined;
this._float32View = undefined;
this._clippingPlanesTexture = undefined;
// Add each ClippingPlane object.
const planes = options.planes;
if (defined(planes)) {
const planesLength = planes.length;
for (let i = 0; i < planesLength; ++i) {
this.add(planes[i]);
}
}
}
function unionIntersectFunction(value) {
return value === Intersect.OUTSIDE;
}
function defaultIntersectFunction(value) {
return value === Intersect.INSIDE;
}
Object.defineProperties(ClippingPlaneCollection.prototype, {
/**
* Returns the number of planes in this collection. This is commonly used with
* {@link ClippingPlaneCollection#get} to iterate over all the planes
* in the collection.
*
* @memberof ClippingPlaneCollection.prototype
* @type {number}
* @readonly
*/
length: {
get: function () {
return this._planes.length;
},
},
/**
* If true, a region will be clipped if it is on the outside of any plane in the
* collection. Otherwise, a region will only be clipped if it is on the
* outside of every plane.
*
* @memberof ClippingPlaneCollection.prototype
* @type {boolean}
* @default false
*/
unionClippingRegions: {
get: function () {
return this._unionClippingRegions;
},
set: function (value) {
if (this._unionClippingRegions === value) {
return;
}
this._unionClippingRegions = value;
this._testIntersection = value
? unionIntersectFunction
: defaultIntersectFunction;
},
},
/**
* If true, clipping will be enabled.
*
* @memberof ClippingPlaneCollection.prototype
* @type {boolean}
* @default true
*/
enabled: {
get: function () {
return this._enabled;
},
set: function (value) {
if (this._enabled === value) {
return;
}
this._enabled = value;
},
},
/**
* Returns a texture containing packed, untransformed clipping planes.
*
* @memberof ClippingPlaneCollection.prototype
* @type {Texture}
* @readonly
* @private
*/
texture: {
get: function () {
return this._clippingPlanesTexture;
},
},
/**
* A reference to the ClippingPlaneCollection's owner, if any.
*
* @memberof ClippingPlaneCollection.prototype
* @readonly
* @private
*/
owner: {
get: function () {
return this._owner;
},
},
/**
* Returns a Number encapsulating the state for this ClippingPlaneCollection.
*
* Clipping mode is encoded in the sign of the number, which is just the plane count.
* If this value changes, then shader regeneration is necessary.
*
* @memberof ClippingPlaneCollection.prototype
* @returns {number} A Number that describes the ClippingPlaneCollection's state.
* @readonly
* @private
*/
clippingPlanesState: {
get: function () {
return this._unionClippingRegions
? this._planes.length
: -this._planes.length;
},
},
});
function setIndexDirty(collection, index) {
// If there's already a different _dirtyIndex set, more than one plane has changed since update.
// Entire texture must be reloaded
collection._multipleDirtyPlanes =
collection._multipleDirtyPlanes ||
(collection._dirtyIndex !== -1 && collection._dirtyIndex !== index);
collection._dirtyIndex = index;
}
/**
* Adds the specified {@link ClippingPlane} to the collection to be used to selectively disable rendering
* on the outside of each plane. Use {@link ClippingPlaneCollection#unionClippingRegions} to modify
* how modify the clipping behavior of multiple planes.
*
* @param {ClippingPlane} plane The ClippingPlane to add to the collection.
*
* @see ClippingPlaneCollection#unionClippingRegions
* @see ClippingPlaneCollection#remove
* @see ClippingPlaneCollection#removeAll
*/
ClippingPlaneCollection.prototype.add = function (plane) {
const newPlaneIndex = this._planes.length;
const that = this;
plane.onChangeCallback = function (index) {
setIndexDirty(that, index);
};
plane.index = newPlaneIndex;
setIndexDirty(this, newPlaneIndex);
this._planes.push(plane);
this.planeAdded.raiseEvent(plane, newPlaneIndex);
};
/**
* Returns the plane in the collection at the specified index. Indices are zero-based
* and increase as planes are added. Removing a plane shifts all planes after
* it to the left, changing their indices. This function is commonly used with
* {@link ClippingPlaneCollection#length} to iterate over all the planes
* in the collection.
*
* @param {number} index The zero-based index of the plane.
* @returns {ClippingPlane} The ClippingPlane at the specified index.
*
* @see ClippingPlaneCollection#length
*/
ClippingPlaneCollection.prototype.get = function (index) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("index", index);
//>>includeEnd('debug');
return this._planes[index];
};
function indexOf(planes, plane) {
const length = planes.length;
for (let i = 0; i < length; ++i) {
if (Plane.equals(planes[i], plane)) {
return i;
}
}
return -1;
}
/**
* Checks whether this collection contains a ClippingPlane equal to the given ClippingPlane.
*
* @param {ClippingPlane} [clippingPlane] The ClippingPlane to check for.
* @returns {boolean} true if this collection contains the ClippingPlane, false otherwise.
*
* @see ClippingPlaneCollection#get
*/
ClippingPlaneCollection.prototype.contains = function (clippingPlane) {
return indexOf(this._planes, clippingPlane) !== -1;
};
/**
* Removes the first occurrence of the given ClippingPlane from the collection.
*
* @param {ClippingPlane} clippingPlane
* @returns {boolean} <code>true</code> if the plane was removed; <code>false</code> if the plane was not found in the collection.
*
* @see ClippingPlaneCollection#add
* @see ClippingPlaneCollection#contains
* @see ClippingPlaneCollection#removeAll
*/
ClippingPlaneCollection.prototype.remove = function (clippingPlane) {
const planes = this._planes;
const index = indexOf(planes, clippingPlane);
if (index === -1) {
return false;
}
// Unlink this ClippingPlaneCollection from the ClippingPlane
if (clippingPlane instanceof ClippingPlane) {
clippingPlane.onChangeCallback = undefined;
clippingPlane.index = -1;
}
// Shift and update indices
const length = planes.length - 1;
for (let i = index; i < length; ++i) {
const planeToKeep = planes[i + 1];
planes[i] = planeToKeep;
if (planeToKeep instanceof ClippingPlane) {
planeToKeep.index = i;
}
}
// Indicate planes texture is dirty
this._multipleDirtyPlanes = true;
planes.length = length;
this.planeRemoved.raiseEvent(clippingPlane, index);
return true;
};
/**
* Removes all planes from the collection.
*
* @see ClippingPlaneCollection#add
* @see ClippingPlaneCollection#remove
*/
ClippingPlaneCollection.prototype.removeAll = function () {
// Dereference this ClippingPlaneCollection from all ClippingPlanes
const planes = this._planes;
const planesCount = planes.length;
for (let i = 0; i < planesCount; ++i) {
const plane = planes[i];
if (plane instanceof ClippingPlane) {
plane.onChangeCallback = undefined;
plane.index = -1;
}
this.planeRemoved.raiseEvent(plane, i);
}
this._multipleDirtyPlanes = true;
this._planes = [];
};
const distanceEncodeScratch = new Cartesian4();
const oct32EncodeScratch = new Cartesian4();
function packPlanesAsUint8(clippingPlaneCollection, startIndex, endIndex) {
const uint8View = clippingPlaneCollection._uint8View;
const planes = clippingPlaneCollection._planes;
let byteIndex = 0;
for (let i = startIndex; i < endIndex; ++i) {
const plane = planes[i];
const oct32Normal = AttributeCompression.octEncodeToCartesian4(
plane.normal,
oct32EncodeScratch,
);
uint8View[byteIndex] = oct32Normal.x;
uint8View[byteIndex + 1] = oct32Normal.y;
uint8View[byteIndex + 2] = oct32Normal.z;
uint8View[byteIndex + 3] = oct32Normal.w;
const encodedDistance = Cartesian4.packFloat(
plane.distance,
distanceEncodeScratch,
);
uint8View[byteIndex + 4] = encodedDistance.x;
uint8View[byteIndex + 5] = encodedDistance.y;
uint8View[byteIndex + 6] = encodedDistance.z;
uint8View[byteIndex + 7] = encodedDistance.w;
byteIndex += 8;
}
}
// Pack starting at the beginning of the buffer to allow partial update
function packPlanesAsFloats(clippingPlaneCollection, startIndex, endIndex) {
const float32View = clippingPlaneCollection._float32View;
const planes = clippingPlaneCollection._planes;
let floatIndex = 0;
for (let i = startIndex; i < endIndex; ++i) {
const plane = planes[i];
const normal = plane.normal;
float32View[floatIndex] = normal.x;
float32View[floatIndex + 1] = normal.y;
float32View[floatIndex + 2] = normal.z;
float32View[floatIndex + 3] = plane.distance;
floatIndex += 4; // each plane is 4 floats
}
}
function computeTextureResolution(pixelsNeeded, result) {
const maxSize = ContextLimits.maximumTextureSize;
result.x = Math.min(pixelsNeeded, maxSize);
result.y = Math.ceil(pixelsNeeded / result.x);
return result;
}
const textureResolutionScratch = new Cartesian2();
/**
* Called when {@link Viewer} or {@link CesiumWidget} render the scene to
* build the resources for clipping planes.
* <p>
* Do not call this function directly.
* </p>
*/
ClippingPlaneCollection.prototype.update = function (frameState) {
let clippingPlanesTexture = this._clippingPlanesTexture;
const context = frameState.context;
const useFloatTexture = ClippingPlaneCollection.useFloatTexture(context);
// Compute texture requirements for current planes
// In RGBA FLOAT, A plane is 4 floats packed to a RGBA.
// In RGBA UNSIGNED_BYTE, A plane is a float in [0, 1) packed to RGBA and an Oct32 quantized normal,
// so 8 bytes or 2 pixels in RGBA.
const pixelsNeeded = useFloatTexture ? this.length : this.length * 2;
if (defined(clippingPlanesTexture)) {
const currentPixelCount =
clippingPlanesTexture.width * clippingPlanesTexture.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 < pixelsNeeded ||
pixelsNeeded < 0.25 * currentPixelCount
) {
clippingPlanesTexture.destroy();
clippingPlanesTexture = undefined;
this._clippingPlanesTexture = undefined;
}
}
// If there are no clipping planes, there's nothing to update.
if (this.length === 0) {
return;
}
if (!defined(clippingPlanesTexture)) {
const requiredResolution = computeTextureResolution(
pixelsNeeded,
textureResolutionScratch,
);
// Allocate twice as much space as needed to avoid frequent texture reallocation.
// Allocate in the Y direction, since texture may be as wide as context texture support.
requiredResolution.y *= 2;
if (useFloatTexture) {
clippingPlanesTexture = new Texture({
context: context,
width: requiredResolution.x,
height: requiredResolution.y,
pixelFormat: PixelFormat.RGBA,
pixelDatatype: PixelDatatype.FLOAT,
sampler: Sampler.NEAREST,
flipY: false,
});
this._float32View = new Float32Array(
requiredResolution.x * requiredResolution.y * 4,
);
} else {
clippingPlanesTexture = new Texture({
context: context,
width: requiredResolution.x,
height: requiredResolution.y,
pixelFormat: PixelFormat.RGBA,
pixelDatatype: PixelDatatype.UNSIGNED_BYTE,
sampler: Sampler.NEAREST,
flipY: false,
});
this._uint8View = new Uint8Array(
requiredResolution.x * requiredResolution.y * 4,
);
}
this._clippingPlanesTexture = clippingPlanesTexture;
this._multipleDirtyPlanes = true;
}
const dirtyIndex = this._dirtyIndex;
if (!this._multipleDirtyPlanes && dirtyIndex === -1) {
return;
}
if (!this._multipleDirtyPlanes) {
// partial updates possible
let offsetX;
let offsetY;
if (useFloatTexture) {
offsetY = Math.floor(dirtyIndex / clippingPlanesTexture.width);
offsetX = Math.floor(dirtyIndex - offsetY * clippingPlanesTexture.width);
packPlanesAsFloats(this, dirtyIndex, dirtyIndex + 1);
clippingPlanesTexture.copyFrom({
source: {
width: 1,
height: 1,
arrayBufferView: this._float32View,
},
xOffset: offsetX,
yOffset: offsetY,
});
} else {
offsetY = Math.floor((dirtyIndex * 2) / clippingPlanesTexture.width);
offsetX = Math.floor(
dirtyIndex * 2 - offsetY * clippingPlanesTexture.width,
);
packPlanesAsUint8(this, dirtyIndex, dirtyIndex + 1);
clippingPlanesTexture.copyFrom({
source: {
width: 2,
height: 1,
arrayBufferView: this._uint8View,
},
xOffset: offsetX,
yOffset: offsetY,
});
}
} else if (useFloatTexture) {
packPlanesAsFloats(this, 0, this._planes.length);
clippingPlanesTexture.copyFrom({
source: {
width: clippingPlanesTexture.width,
height: clippingPlanesTexture.height,
arrayBufferView: this._float32View,
},
});
} else {
packPlanesAsUint8(this, 0, this._planes.length);
clippingPlanesTexture.copyFrom({
source: {
width: clippingPlanesTexture.width,
height: clippingPlanesTexture.height,
arrayBufferView: this._uint8View,
},
});
}
this._multipleDirtyPlanes = false;
this._dirtyIndex = -1;
};
const scratchMatrix = new Matrix4();
const scratchPlane = new Plane(Cartesian3.UNIT_X, 0.0);
/**
* Determines the type intersection with the planes of this ClippingPlaneCollection instance and the specified {@link TileBoundingVolume}.
* @ignore
*
* @param {object} tileBoundingVolume The volume to determine the intersection with the planes.
* @param {Matrix4} [transform] An optional, additional matrix to transform the plane to world coordinates.
* @returns {Intersect} {@link Intersect.INSIDE} if the entire volume is on the side of the planes
* the normal is pointing and should be entirely rendered, {@link Intersect.OUTSIDE}
* if the entire volume is on the opposite side and should be clipped, and
* {@link Intersect.INTERSECTING} if the volume intersects the planes.
*/
ClippingPlaneCollection.prototype.computeIntersectionWithBoundingVolume =
function (tileBoundingVolume, transform) {
const planes = this._planes;
const length = planes.length;
let modelMatrix = this.modelMatrix;
if (defined(transform)) {
modelMatrix = Matrix4.multiply(transform, modelMatrix, scratchMatrix);
}
// If the collection is not set to union the clipping regions, the volume must be outside of all planes to be
// considered completely clipped. If the collection is set to union the clipping regions, if the volume can be
// outside any the planes, it is considered completely clipped.
// Lastly, if not completely clipped, if any plane is intersecting, more calculations must be performed.
let intersection = Intersect.INSIDE;
if (!this.unionClippingRegions && length > 0) {
intersection = Intersect.OUTSIDE;
}
for (let i = 0; i < length; ++i) {
const plane = planes[i];
Plane.transform(plane, modelMatrix, scratchPlane); // ClippingPlane can be used for Plane math
const value = tileBoundingVolume.intersectPlane(scratchPlane);
if (value === Intersect.INTERSECTING) {
intersection = value;
} else if (this._testIntersection(value)) {
return value;
}
}
return intersection;
};
/**
* Sets the owner for the input ClippingPlaneCollection if there wasn't another owner.
* Destroys the owner's previous ClippingPlaneCollection if setting is successful.
*
* @param {ClippingPlaneCollection} [clippingPlaneCollection] A ClippingPlaneCollection (or undefined) being attached to an object
* @param {object} owner An Object that should receive the new ClippingPlaneCollection
* @param {string} key The Key for the Object to reference the ClippingPlaneCollection
* @ignore
*/
ClippingPlaneCollection.setOwner = function (
clippingPlaneCollection,
owner,
key,
) {
// Don't destroy the ClippingPlaneCollection if it is already owned by newOwner
if (clippingPlaneCollection === owner[key]) {
return;
}
// Destroy the existing ClippingPlaneCollection, if any
owner[key] = owner[key] && owner[key].destroy();
if (defined(clippingPlaneCollection)) {
//>>includeStart('debug', pragmas.debug);
if (defined(clippingPlaneCollection._owner)) {
throw new DeveloperError(
"ClippingPlaneCollection should only be assigned to one object",
);
}
//>>includeEnd('debug');
clippingPlaneCollection._owner = owner;
owner[key] = clippingPlaneCollection;
}
};
/**
* Function for checking if the context will allow clipping planes with floating point textures.
*
* @param {Context} context The Context that will contain clipped objects and clipping textures.
* @returns {boolean} <code>true</code> if floating point textures can be used for clipping planes.
* @private
*/
ClippingPlaneCollection.useFloatTexture = function (context) {
return context.floatingPointTexture;
};
/**
* Function for getting the clipping plane collection's texture resolution.
* If the ClippingPlaneCollection hasn't been updated, returns the resolution that will be
* allocated based on the current plane count.
*
* @param {ClippingPlaneCollection} clippingPlaneCollection The clipping plane collection
* @param {Context} context The rendering context
* @param {Cartesian2} result A Cartesian2 for the result.
* @returns {Cartesian2} The required resolution.
* @private
*/
ClippingPlaneCollection.getTextureResolution = function (
clippingPlaneCollection,
context,
result,
) {
const texture = clippingPlaneCollection.texture;
if (defined(texture)) {
result.x = texture.width;
result.y = texture.height;
return result;
}
const pixelsNeeded = ClippingPlaneCollection.useFloatTexture(context)
? clippingPlaneCollection.length
: clippingPlaneCollection.length * 2;
const requiredResolution = computeTextureResolution(pixelsNeeded, result);
// Allocate twice as much space as needed to avoid frequent texture reallocation.
requiredResolution.y *= 2;
return requiredResolution;
};
/**
* Returns true if this object was destroyed; otherwise, false.
* <br /><br />
* If this object was destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
*
* @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
*
* @see ClippingPlaneCollection#destroy
*/
ClippingPlaneCollection.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.
* <br /><br />
* Once an object is destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
* assign the return value (<code>undefined</code>) to the object as done in the example.
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
*
* @example
* clippingPlanes = clippingPlanes && clippingPlanes.destroy();
*
* @see ClippingPlaneCollection#isDestroyed
*/
ClippingPlaneCollection.prototype.destroy = function () {
this._clippingPlanesTexture =
this._clippingPlanesTexture && this._clippingPlanesTexture.destroy();
return destroyObject(this);
};
export default ClippingPlaneCollection;
+302
View File
@@ -0,0 +1,302 @@
import Check from "../Core/Check.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Cartographic from "../Core/Cartographic.js";
import defined from "../Core/defined.js";
import Ellipsoid from "../Core/Ellipsoid.js";
import CesiumMath from "../Core/Math.js";
import PolygonGeometry from "../Core/PolygonGeometry.js";
import Rectangle from "../Core/Rectangle.js";
/**
* A geodesic polygon to be used with {@link ClippingPlaneCollection} for selectively hiding regions in a model, a 3D tileset, or the globe.
* @alias ClippingPolygon
* @constructor
*
* @param {object} options Object with the following properties:
* @param {Cartesian3[]} options.positions A list of three or more Cartesian coordinates defining the outer ring of the clipping polygon.
* @param {Ellipsoid} [options.ellipsoid=Ellipsoid.default]
*
* @example
* const positions = Cesium.Cartesian3.fromRadiansArray([
* -1.3194369277314022,
* 0.6988062530900625,
* -1.31941,
* 0.69879,
* -1.3193955980204217,
* 0.6988091578771254,
* -1.3193931220959367,
* 0.698743632490865,
* -1.3194358224045408,
* 0.6987471965556998,
* ]);
*
* const polygon = new Cesium.ClippingPolygon({
* positions: positions
* });
*/
function ClippingPolygon(options) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options", options);
Check.typeOf.object("options.positions", options.positions);
Check.typeOf.number.greaterThanOrEquals(
"options.positions.length",
options.positions.length,
3,
);
//>>includeEnd('debug');
this._ellipsoid = options.ellipsoid ?? Ellipsoid.default;
this._positions = copyArrayCartesian3(options.positions);
/**
* A copy of the input positions.
*
* This is used to detect modifications of the positions in
* <code>coputeRectangle</code>: The rectangle only has
* to be re-computed when these positions have changed.
*
* @type {Cartesian3[]|undefined}
* @private
*/
this._cachedPositions = undefined;
/**
* A cached version of the rectangle that is computed in
* <code>computeRectangle</code>.
*
* This is only re-computed when the positions have changed, as
* determined by comparing the <code>_positions</code> to the
* <code>_cachedPositions</code>
*
* @type {Rectangle|undefined}
* @private
*/
this._cachedRectangle = undefined;
}
/**
* Returns a deep copy of the given array.
*
* If the input is undefined, then <code>undefined</code> is returned.
*
* Otherwise, the result will be a copy of the given array, where
* each element is copied with <code>Cartesian3.clone</code>.
*
* @param {Cartesian3[]|undefined} input The input array
* @returns {Cartesian3[]|undefined} The copy
* @ignore
*/
function copyArrayCartesian3(input) {
if (!defined(input)) {
return undefined;
}
const n = input.length;
const output = Array(n);
for (let i = 0; i < n; i++) {
output[i] = Cartesian3.clone(input[i]);
}
return output;
}
/**
* Returns whether the given arrays are component-wise equal.
*
* When both arrays are undefined, then <code>true</code> is returned.
* When only one array is defined, or they are both defined but have
* different lengths, then <code>false</code> is returned.
*
* Otherwise, returns whether the corresponding elements of the arrays
* are equal, as of <code>Cartesian3.equals</code>.
*
* @param {Cartesian3[]|undefined} a The first array
* @param {Cartesian3[]|undefined} b The second array
* @returns {boolean} Whether the arrays are equal
* @ignore
*/
function equalsArrayCartesian3(a, b) {
if (!defined(a) && !defined(b)) {
return true;
}
if (defined(a) !== defined(b)) {
return false;
}
if (a.length !== b.length) {
return false;
}
const n = a.length;
for (let i = 0; i < n; i++) {
const ca = a[i];
const cb = b[i];
if (!Cartesian3.equals(ca, cb)) {
return false;
}
}
return true;
}
Object.defineProperties(ClippingPolygon.prototype, {
/**
* Returns the total number of positions in the polygon, include any holes.
*
* @memberof ClippingPolygon.prototype
* @type {number}
* @readonly
*/
length: {
get: function () {
return this._positions.length;
},
},
/**
* Returns the outer ring of positions.
*
* @memberof ClippingPolygon.prototype
* @type {Cartesian3[]}
* @readonly
*/
positions: {
get: function () {
return this._positions;
},
},
/**
* Returns the ellipsoid used to project the polygon onto surfaces when clipping.
*
* @memberof ClippingPolygon.prototype
* @type {Ellipsoid}
* @readonly
*/
ellipsoid: {
get: function () {
return this._ellipsoid;
},
},
});
/**
* Clones the ClippingPolygon without setting its ownership.
* @param {ClippingPolygon} polygon The ClippingPolygon to be cloned
* @param {ClippingPolygon} [result] The object on which to store the cloned parameters.
* @returns {ClippingPolygon} a clone of the input ClippingPolygon
*/
ClippingPolygon.clone = function (polygon, result) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("polygon", polygon);
//>>includeEnd('debug');
if (!defined(result)) {
return new ClippingPolygon({
positions: polygon.positions,
ellipsoid: polygon.ellipsoid,
});
}
result._ellipsoid = polygon.ellipsoid;
result._positions.length = 0;
result._positions.push(...polygon.positions);
return result;
};
/**
* Compares the provided ClippingPolygons and returns
* <code>true</code> if they are equal, <code>false</code> otherwise.
*
* @param {ClippingPolygon} left The first polygon.
* @param {ClippingPolygon} right The second polygon.
* @returns {boolean} <code>true</code> if left and right are equal, <code>false</code> otherwise.
*/
ClippingPolygon.equals = function (left, right) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("left", left);
Check.typeOf.object("right", right);
//>>includeEnd('debug');
return (
left.ellipsoid.equals(right.ellipsoid) && left.positions === right.positions
);
};
/**
* Computes a cartographic rectangle which encloses the polygon defined by the list of positions, including cases over the international date line and the poles.
*
* @param {Rectangle} [result] An object in which to store the result.
* @returns {Rectangle} The result rectangle
*/
ClippingPolygon.prototype.computeRectangle = function (result) {
if (equalsArrayCartesian3(this._positions, this._cachedPositions)) {
return Rectangle.clone(this._cachedRectangle, result);
}
const rectangle = PolygonGeometry.computeRectangleFromPositions(
this.positions,
this.ellipsoid,
undefined,
result,
);
this._cachedPositions = copyArrayCartesian3(this._positions);
this._cachedRectangle = Rectangle.clone(rectangle);
return rectangle;
};
const scratchRectangle = new Rectangle();
const spherePointScratch = new Cartesian3();
/**
* Computes a rectangle with the spherical extents that encloses the polygon defined by the list of positions, including cases over the international date line and the poles.
*
* @private
*
* @param {Rectangle} [result] An object in which to store the result.
* @returns {Rectangle} The result rectangle with spherical extents.
*/
ClippingPolygon.prototype.computeSphericalExtents = function (result) {
if (!defined(result)) {
result = new Rectangle();
}
const rectangle = this.computeRectangle(scratchRectangle);
let spherePoint = Cartographic.toCartesian(
Rectangle.southwest(rectangle),
this.ellipsoid,
spherePointScratch,
);
// Project into plane with vertical for latitude
let magXY = Math.sqrt(
spherePoint.x * spherePoint.x + spherePoint.y * spherePoint.y,
);
// Use fastApproximateAtan2 for alignment with shader
let sphereLatitude = CesiumMath.fastApproximateAtan2(magXY, spherePoint.z);
let sphereLongitude = CesiumMath.fastApproximateAtan2(
spherePoint.x,
spherePoint.y,
);
result.south = sphereLatitude;
result.west = sphereLongitude;
spherePoint = Cartographic.toCartesian(
Rectangle.northeast(rectangle),
this.ellipsoid,
spherePointScratch,
);
// Project into plane with vertical for latitude
magXY = Math.sqrt(
spherePoint.x * spherePoint.x + spherePoint.y * spherePoint.y,
);
// Use fastApproximateAtan2 for alignment with shader
sphereLatitude = CesiumMath.fastApproximateAtan2(magXY, spherePoint.z);
sphereLongitude = CesiumMath.fastApproximateAtan2(
spherePoint.x,
spherePoint.y,
);
result.north = sphereLatitude;
result.east = sphereLongitude;
return result;
};
export default ClippingPolygon;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
/**
* Specifies the type of the cloud that is added to a {@link CloudCollection} in {@link CloudCollection#add}.
*
* @enum {number}
*/
const CloudType = {
/**
* Cumulus cloud.
*
* @type {number}
* @constant
*/
CUMULUS: 0,
};
/**
* Validates that the provided cloud type is a valid {@link CloudType}
*
* @param {CloudType} cloudType The cloud type to validate.
* @returns {boolean} <code>true</code> if the provided cloud type is a valid value; otherwise, <code>false</code>.
*
* @example
* if (!Cesium.CloudType.validate(cloudType)) {
* throw new Cesium.DeveloperError('cloudType must be a valid value.');
* }
*/
CloudType.validate = function (cloudType) {
return cloudType === CloudType.CUMULUS;
};
Object.freeze(CloudType);
export default CloudType;
+36
View File
@@ -0,0 +1,36 @@
import CesiumMath from "../Core/Math.js";
/**
* Defines different modes for blending between a target color and a primitive's source color.
*
* HIGHLIGHT multiplies the source color by the target color
* REPLACE replaces the source color with the target color
* MIX blends the source color and target color together
*
* @enum {number}
*
* @see Model.colorBlendMode
*/
const ColorBlendMode = {
HIGHLIGHT: 0,
REPLACE: 1,
MIX: 2,
};
/**
* @private
*/
ColorBlendMode.getColorBlend = function (colorBlendMode, colorBlendAmount) {
if (colorBlendMode === ColorBlendMode.HIGHLIGHT) {
return 0.0;
} else if (colorBlendMode === ColorBlendMode.REPLACE) {
return 1.0;
} else if (colorBlendMode === ColorBlendMode.MIX) {
// The value 0.0 is reserved for highlight, so clamp to just above 0.0.
return CesiumMath.clamp(colorBlendAmount, CesiumMath.EPSILON4, 1.0);
}
};
Object.freeze(ColorBlendMode);
export default ColorBlendMode;
+365
View File
@@ -0,0 +1,365 @@
import Cartesian3 from "../Core/Cartesian3.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import getMagic from "../Core/getMagic.js";
import RuntimeError from "../Core/RuntimeError.js";
/**
* Represents the contents of a
* {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/Composite|Composite}
* tile in a {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification|3D Tiles} tileset.
* <p>
* Implements the {@link Cesium3DTileContent} interface.
* </p>
*
* @implements Cesium3DTileContent
* @private
*/
class Composite3DTileContent {
constructor(tileset, tile, resource, contents) {
this._tileset = tileset;
this._tile = tile;
this._resource = resource;
if (!defined(contents)) {
contents = [];
}
this._contents = contents;
this._metadata = undefined;
this._group = undefined;
this._ready = false;
}
get featurePropertiesDirty() {
const contents = this._contents;
const length = contents.length;
for (let i = 0; i < length; ++i) {
if (contents[i].featurePropertiesDirty) {
return true;
}
}
return false;
}
set featurePropertiesDirty(value) {
const contents = this._contents;
const length = contents.length;
for (let i = 0; i < length; ++i) {
contents[i].featurePropertiesDirty = value;
}
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* always returns <code>0</code>. Instead call <code>featuresLength</code> for a tile in the composite.
*/
get featuresLength() {
return 0;
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* always returns <code>0</code>. Instead call <code>pointsLength</code> for a tile in the composite.
*/
get pointsLength() {
return 0;
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* always returns <code>0</code>. Instead call <code>trianglesLength</code> for a tile in the composite.
*/
get trianglesLength() {
return 0;
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* always returns <code>0</code>. Instead call <code>geometryByteLength</code> for a tile in the composite.
*/
get geometryByteLength() {
return 0;
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* always returns <code>0</code>. Instead call <code>texturesByteLength</code> for a tile in the composite.
*/
get texturesByteLength() {
return 0;
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* always returns <code>0</code>. Instead call <code>batchTableByteLength</code> for a tile in the composite.
*/
get batchTableByteLength() {
return 0;
}
get innerContents() {
return this._contents;
}
/**
* Returns true when the tile's content is ready to render; otherwise false
*
*
* @type {boolean}
* @readonly
* @private
*/
get ready() {
return this._ready;
}
get tileset() {
return this._tileset;
}
get tile() {
return this._tile;
}
get url() {
return this._resource.getUrlComponent(true);
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* both stores the content metadata and propagates the content metadata to all of its children.
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
get metadata() {
return this._metadata;
}
set metadata(value) {
this._metadata = value;
const contents = this._contents;
const length = contents.length;
for (let i = 0; i < length; ++i) {
contents[i].metadata = value;
}
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* always returns <code>undefined</code>. Instead call <code>batchTable</code> for a tile in the composite.
*/
get batchTable() {
return undefined;
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* both stores the group metadata and propagates the group metadata to all of its children.
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
get group() {
return this._group;
}
set group(value) {
this._group = value;
const contents = this._contents;
const length = contents.length;
for (let i = 0; i < length; ++i) {
contents[i].group = value;
}
}
static async fromTileType(
tileset,
tile,
resource,
arrayBuffer,
byteOffset,
factory,
) {
byteOffset = byteOffset ?? 0;
const uint8Array = new Uint8Array(arrayBuffer);
const view = new DataView(arrayBuffer);
byteOffset += sizeOfUint32; // Skip magic
const version = view.getUint32(byteOffset, true);
if (version !== 1) {
throw new RuntimeError(
`Only Composite Tile version 1 is supported. Version ${version} is not.`,
);
}
byteOffset += sizeOfUint32;
// Skip byteLength
byteOffset += sizeOfUint32;
const tilesLength = view.getUint32(byteOffset, true);
byteOffset += sizeOfUint32;
// For caching purposes, models within the composite tile must be
// distinguished. To do this, add a query parameter ?compositeIndex=i.
// Since composite tiles may contain other composite tiles, check for an
// existing prefix and separate them with underscores. e.g.
// ?compositeIndex=0_1_1
let prefix = resource.queryParameters.compositeIndex;
if (defined(prefix)) {
// We'll be adding another value at the end, so add an underscore.
prefix = `${prefix}_`;
} else {
// no prefix
prefix = "";
}
const promises = [];
promises.length = tilesLength;
for (let i = 0; i < tilesLength; ++i) {
const tileType = getMagic(uint8Array, byteOffset);
// Tile byte length is stored after magic and version
const tileByteLength = view.getUint32(
byteOffset + sizeOfUint32 * 2,
true,
);
const contentFactory = factory[tileType];
// Label which content within the composite this is
const compositeIndex = `${prefix}${i}`;
const childResource = resource.getDerivedResource({
queryParameters: {
compositeIndex: compositeIndex,
},
});
if (defined(contentFactory)) {
promises[i] = Promise.resolve(
contentFactory(tileset, tile, childResource, arrayBuffer, byteOffset),
);
} else {
throw new RuntimeError(
`Unknown tile content type, ${tileType}, inside Composite tile`,
);
}
byteOffset += tileByteLength;
}
const innerContents = await Promise.all(promises);
const content = new Composite3DTileContent(
tileset,
tile,
resource,
innerContents,
);
return content;
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* always returns <code>false</code>. Instead call <code>hasProperty</code> for a tile in the composite.
*/
hasProperty(batchId, name) {
return false;
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Composite3DTileContent</code>
* always returns <code>undefined</code>. Instead call <code>getFeature</code> for a tile in the composite.
*/
getFeature(batchId) {
return undefined;
}
applyDebugSettings(enabled, color) {
const contents = this._contents;
const length = contents.length;
for (let i = 0; i < length; ++i) {
contents[i].applyDebugSettings(enabled, color);
}
}
applyStyle(style) {
const contents = this._contents;
const length = contents.length;
for (let i = 0; i < length; ++i) {
contents[i].applyStyle(style);
}
}
update(tileset, frameState) {
const contents = this._contents;
const length = contents.length;
let ready = true;
for (let i = 0; i < length; ++i) {
contents[i].update(tileset, frameState);
ready = ready && contents[i].ready;
}
if (!this._ready && ready) {
this._ready = true;
}
}
/**
* Find an intersection between a ray and the tile content surface that was rendered. The ray must be given in world coordinates.
*
* @param {Ray} ray The ray to test for intersection.
* @param {FrameState} frameState The frame state.
* @param {Cartesian3|undefined} [result] The intersection or <code>undefined</code> if none was found.
* @returns {Cartesian3|undefined} The intersection or <code>undefined</code> if none was found.
*
* @private
*/
pick(ray, frameState, result) {
if (!this._ready) {
return undefined;
}
let intersection;
let minDistance = Number.POSITIVE_INFINITY;
const contents = this._contents;
const length = contents.length;
for (let i = 0; i < length; ++i) {
const candidate = contents[i].pick(ray, frameState, result);
if (!defined(candidate)) {
continue;
}
const distance = Cartesian3.distance(ray.origin, candidate);
if (distance < minDistance) {
intersection = candidate;
minDistance = distance;
}
}
if (!defined(intersection)) {
return undefined;
}
return result;
}
isDestroyed() {
return false;
}
destroy() {
const contents = this._contents;
const length = contents.length;
for (let i = 0; i < length; ++i) {
contents[i].destroy();
}
return destroyObject(this);
}
}
const sizeOfUint32 = Uint32Array.BYTES_PER_ELEMENT;
export default Composite3DTileContent;
+219
View File
@@ -0,0 +1,219 @@
import addAllToArray from "../Core/addAllToArray.js";
import clone from "../Core/clone.js";
import defined from "../Core/defined.js";
import Expression from "./Expression.js";
/**
* An expression for a style applied to a {@link Cesium3DTileset}.
* <p>
* Evaluates a conditions expression defined using the
* {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/Styling|3D Tiles Styling language}.
* </p>
* <p>
* Implements the {@link StyleExpression} interface.
* </p>
*
* @alias ConditionsExpression
* @constructor
*
* @param {object} [conditionsExpression] The conditions expression defined using the 3D Tiles Styling language.
* @param {object} [defines] Defines in the style.
*
* @example
* const expression = new Cesium.ConditionsExpression({
* conditions : [
* ['${Area} > 10, 'color("#FF0000")'],
* ['${id} !== "1"', 'color("#00FF00")'],
* ['true', 'color("#FFFFFF")']
* ]
* });
* expression.evaluateColor(feature, result); // returns a Cesium.Color object
*/
function ConditionsExpression(conditionsExpression, defines) {
this._conditionsExpression = clone(conditionsExpression, true);
this._conditions = conditionsExpression.conditions;
this._runtimeConditions = undefined;
setRuntime(this, defines);
}
Object.defineProperties(ConditionsExpression.prototype, {
/**
* Gets the conditions expression defined in the 3D Tiles Styling language.
*
* @memberof ConditionsExpression.prototype
*
* @type {object}
* @readonly
*
* @default undefined
*/
conditionsExpression: {
get: function () {
return this._conditionsExpression;
},
},
});
function Statement(condition, expression) {
this.condition = condition;
this.expression = expression;
}
function setRuntime(expression, defines) {
const runtimeConditions = [];
const conditions = expression._conditions;
if (!defined(conditions)) {
return;
}
const length = conditions.length;
for (let i = 0; i < length; ++i) {
const statement = conditions[i];
const cond = String(statement[0]);
const condExpression = String(statement[1]);
runtimeConditions.push(
new Statement(
new Expression(cond, defines),
new Expression(condExpression, defines),
),
);
}
expression._runtimeConditions = runtimeConditions;
}
/**
* Evaluates the result of an expression, optionally using the provided feature's properties. If the result of
* the expression in the
* {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/Styling|3D Tiles Styling language}
* is of type <code>Boolean</code>, <code>Number</code>, or <code>String</code>, the corresponding JavaScript
* primitive type will be returned. If the result is a <code>RegExp</code>, a Javascript <code>RegExp</code>
* object will be returned. If the result is a <code>Cartesian2</code>, <code>Cartesian3</code>, or <code>Cartesian4</code>,
* a {@link Cartesian2}, {@link Cartesian3}, or {@link Cartesian4} object will be returned. If the <code>result</code> argument is
* a {@link Color}, the {@link Cartesian4} value is converted to a {@link Color} and then returned.
*
* @param {Cesium3DTileFeature} feature The feature whose properties may be used as variables in the expression.
* @param {object} [result] The object onto which to store the result.
* @returns {boolean|number|string|RegExp|Cartesian2|Cartesian3|Cartesian4|Color} The result of evaluating the expression.
*/
ConditionsExpression.prototype.evaluate = function (feature, result) {
const conditions = this._runtimeConditions;
if (!defined(conditions)) {
return undefined;
}
const length = conditions.length;
for (let i = 0; i < length; ++i) {
const statement = conditions[i];
if (statement.condition.evaluate(feature)) {
return statement.expression.evaluate(feature, result);
}
}
};
/**
* Evaluates the result of a Color expression, using the values defined by a feature.
* <p>
* This is equivalent to {@link ConditionsExpression#evaluate} but always returns a {@link Color} object.
* </p>
* @param {Cesium3DTileFeature} feature The feature whose properties may be used as variables in the expression.
* @param {Color} [result] The object in which to store the result
* @returns {Color} The modified result parameter or a new Color instance if one was not provided.
*/
ConditionsExpression.prototype.evaluateColor = function (feature, result) {
const conditions = this._runtimeConditions;
if (!defined(conditions)) {
return undefined;
}
const length = conditions.length;
for (let i = 0; i < length; ++i) {
const statement = conditions[i];
if (statement.condition.evaluate(feature)) {
return statement.expression.evaluateColor(feature, result);
}
}
};
/**
* Gets the shader function for this expression.
* Returns undefined if the shader function can't be generated from this expression.
*
* @param {string} functionSignature Signature of the generated function.
* @param {object} variableSubstitutionMap Maps variable names to shader variable names.
* @param {object} shaderState Stores information about the generated shader function, including whether it is translucent.
* @param {string} returnType The return type of the generated function.
*
* @returns {string} The shader function.
*
* @private
*/
ConditionsExpression.prototype.getShaderFunction = function (
functionSignature,
variableSubstitutionMap,
shaderState,
returnType,
) {
const conditions = this._runtimeConditions;
if (!defined(conditions) || conditions.length === 0) {
return undefined;
}
let shaderFunction = "";
const length = conditions.length;
for (let i = 0; i < length; ++i) {
const statement = conditions[i];
const condition = statement.condition.getShaderExpression(
variableSubstitutionMap,
shaderState,
);
const expression = statement.expression.getShaderExpression(
variableSubstitutionMap,
shaderState,
);
// Build the if/else chain from the list of conditions
shaderFunction +=
` ${i === 0 ? "if" : "else if"} (${condition})\n` +
` {\n` +
` return ${expression};\n` +
` }\n`;
}
shaderFunction =
`${returnType} ${functionSignature}\n` +
`{\n${shaderFunction} return ${returnType}(1.0);\n` + // Return a default value if no conditions are met
`}\n`;
return shaderFunction;
};
/**
* Gets the variables used by the expression.
*
* @returns {string[]} The variables used by the expression.
*
* @private
*/
ConditionsExpression.prototype.getVariables = function () {
let variables = [];
const conditions = this._runtimeConditions;
if (!defined(conditions) || conditions.length === 0) {
return variables;
}
const length = conditions.length;
for (let i = 0; i < length; ++i) {
const statement = conditions[i];
addAllToArray(variables, statement.condition.getVariables());
addAllToArray(variables, statement.expression.getVariables());
}
// Remove duplicates
variables = variables.filter(function (variable, index, variables) {
return variables.indexOf(variable) === index;
});
return variables;
};
export default ConditionsExpression;
+61
View File
@@ -0,0 +1,61 @@
import Cartesian3 from "../Core/Cartesian3.js";
import Check from "../Core/Check.js";
import CesiumMath from "../Core/Math.js";
const defaultAngle = CesiumMath.toRadians(30.0);
/**
* A ParticleEmitter that emits particles within a cone.
* Particles will be positioned at the tip of the cone and have initial velocities going towards the base.
*
* @alias ConeEmitter
* @constructor
*
* @param {number} [angle=Cesium.Math.toRadians(30.0)] The angle of the cone in radians.
*/
function ConeEmitter(angle) {
this._angle = angle ?? defaultAngle;
}
Object.defineProperties(ConeEmitter.prototype, {
/**
* The angle of the cone in radians.
* @memberof CircleEmitter.prototype
* @type {number}
* @default Cesium.Math.toRadians(30.0)
*/
angle: {
get: function () {
return this._angle;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("value", value);
//>>includeEnd('debug');
this._angle = value;
},
},
});
/**
* Initializes the given {Particle} by setting it's position and velocity.
*
* @private
* @param {Particle} particle The particle to initialize
*/
ConeEmitter.prototype.emit = function (particle) {
const radius = Math.tan(this._angle);
// Compute a random point on the cone's base
const theta = CesiumMath.randomBetween(0.0, CesiumMath.TWO_PI);
const rad = CesiumMath.randomBetween(0.0, radius);
const x = rad * Math.cos(theta);
const y = rad * Math.sin(theta);
const z = 1.0;
particle.velocity = Cartesian3.fromElements(x, y, z, particle.velocity);
Cartesian3.normalize(particle.velocity, particle.velocity);
particle.position = Cartesian3.clone(Cartesian3.ZERO, particle.position);
};
export default ConeEmitter;
+184
View File
@@ -0,0 +1,184 @@
import Check from "../Core/Check.js";
import Frozen from "../Core/Frozen.js";
import MetadataEntity from "./MetadataEntity.js";
/**
* Metadata about the content of a 3D Tile. This represents the content metadata JSON (3D Tiles 1.1)
* or the <code>3DTILES_metadata</code> extension on a single {@link Cesium3DTileContent}
* <p>
* See the {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_metadata|3DTILES_metadata Extension} for 3D Tiles
* </p>
*
* @param {object} options Object with the following properties:
* @param {object} options.content Either the content metadata JSON (3D Tiles 1.1) or the extension JSON attached to the content.
* @param {MetadataClass} options.class The class that the content metadata conforms to.
*
* @alias ContentMetadata
* @constructor
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
function ContentMetadata(options) {
options = options ?? Frozen.EMPTY_OBJECT;
const content = options.content;
const metadataClass = options.class;
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options.content", content);
Check.typeOf.object("options.class", metadataClass);
//>>includeEnd('debug');
this._class = metadataClass;
this._properties = content.properties;
this._extensions = content.extensions;
this._extras = content.extras;
}
Object.defineProperties(ContentMetadata.prototype, {
/**
* The class that properties conform to.
*
* @memberof ContentMetadata.prototype
* @type {MetadataClass}
* @readonly
* @private
*/
class: {
get: function () {
return this._class;
},
},
/**
* Extra user-defined properties.
*
* @memberof ContentMetadata.prototype
* @type {object}
* @readonly
* @private
*/
extras: {
get: function () {
return this._extras;
},
},
/**
* An object containing extensions.
*
* @memberof ContentMetadata.prototype
* @type {object}
* @readonly
* @private
*/
extensions: {
get: function () {
return this._extensions;
},
},
});
/**
* Returns whether the content has this property.
*
* @param {string} propertyId The case-sensitive ID of the property.
* @returns {boolean} Whether the content has this property.
* @private
*/
ContentMetadata.prototype.hasProperty = function (propertyId) {
return MetadataEntity.hasProperty(propertyId, this._properties, this._class);
};
/**
* Returns whether the content has a property with the given semantic.
*
* @param {string} semantic The case-sensitive semantic of the property.
* @returns {boolean} Whether the content has a property with the given semantic.
* @private
*/
ContentMetadata.prototype.hasPropertyBySemantic = function (semantic) {
return MetadataEntity.hasPropertyBySemantic(
semantic,
this._properties,
this._class,
);
};
/**
* Returns an array of property IDs.
*
* @param {string[]} [results] An array into which to store the results.
* @returns {string[]} The property IDs.
* @private
*/
ContentMetadata.prototype.getPropertyIds = function (results) {
return MetadataEntity.getPropertyIds(this._properties, this._class, results);
};
/**
* Returns a copy of the value of the property with the given ID.
* <p>
* If the property is normalized the normalized value is returned.
* </p>
*
* @param {string} propertyId The case-sensitive ID of the property.
* @returns {*} The value of the property or <code>undefined</code> if the content does not have this property.
* @private
*/
ContentMetadata.prototype.getProperty = function (propertyId) {
return MetadataEntity.getProperty(propertyId, this._properties, this._class);
};
/**
* Sets the value of the property with the given ID.
* <p>
* If the property is normalized a normalized value must be provided to this function.
* </p>
*
* @param {string} propertyId The case-sensitive ID of the property.
* @param {*} value The value of the property that will be copied.
* @returns {boolean} <code>true</code> if the property was set, <code>false</code> otherwise.
* @private
*/
ContentMetadata.prototype.setProperty = function (propertyId, value) {
return MetadataEntity.setProperty(
propertyId,
value,
this._properties,
this._class,
);
};
/**
* Returns a copy of the value of the property with the given semantic.
*
* @param {string} semantic The case-sensitive semantic of the property.
* @returns {*} The value of the property or <code>undefined</code> if the content does not have this semantic.
* @private
*/
ContentMetadata.prototype.getPropertyBySemantic = function (semantic) {
return MetadataEntity.getPropertyBySemantic(
semantic,
this._properties,
this._class,
);
};
/**
* Sets the value of the property with the given semantic.
*
* @param {string} semantic The case-sensitive semantic of the property.
* @param {*} value The value of the property that will be copied.
* @returns {boolean} <code>true</code> if the property was set, <code>false</code> otherwise.
* @private
*/
ContentMetadata.prototype.setPropertyBySemantic = function (semantic, value) {
return MetadataEntity.setPropertyBySemantic(
semantic,
value,
this._properties,
this._class,
);
};
export default ContentMetadata;
+63
View File
@@ -0,0 +1,63 @@
import DeveloperError from "../../Core/DeveloperError.js";
/**
* An interface for a camera controller that can be registered with the scene to handle input events, camera animations, and other interactions. Implementations of this interface are expected to be registered with the scene via a {@link ControllerHost}.
* This type describes an
* interface and is not intended to be instantiated directly.
* @class
* @abstract
* @see {@link HybridScreenSpacePanCameraController}
* @see {@link ScreenSpaceElevatorCameraController}
* @see {@link ScreenSpaceMapCameraController}
* @see {@link ScreenSpaceTiltOrbitCameraController}
*/
class Controller {
/**
* Determines if the controller is enabled and should be updated by the host scene.
* @type {boolean}
*/
get enabled() {
return DeveloperError.throwInstantiationError();
}
set enabled(value) {
DeveloperError.throwInstantiationError();
}
/**
* Invoked when the controller is added to the DOM. Implement <code>connectedCallback</code> to set up any DOM event listeners.
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
connectedCallback(element) {
DeveloperError.throwInstantiationError();
}
/**
* Invoked when the controller is removed from the DOM. Implement <code>disconnectedCallback</code> to tear down any DOM event listeners.
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
disconnectedCallback(element) {
DeveloperError.throwInstantiationError();
}
/**
* Invoked once per frame. Implement <code>update</code> to modify the camera or other parts of the scene.
* @see {@link https://cesium.com/blog/2018/01/24/cesium-scene-rendering-performance/#updaterender-cycle-events|Update/Render Cycle Events}
* @param {Scene} scene
* @param {JulianDate} time The current simulation time.
*/
update(scene, time) {
DeveloperError.throwInstantiationError();
}
/**
* Invoked when the controller is being updated the first time, immediately before <code>update</code> is called. Implement <code>firstUpdate</code> to perform one-time work after the relevant scene has begun its render loop. Some examples might include initializing simulation time values or adding a primitive to the scene.
* @see Controller#update
* @param {Scene} scene
* @param {JulianDate} time The current simulation time.
*/
firstUpdate(scene, time) {
DeveloperError.throwInstantiationError();
}
}
export default Controller;
+78
View File
@@ -0,0 +1,78 @@
/**
* Collects an array of Controller objects that can be registered with the scene to handle input events, camera animations, and other interactions.
* @class
* @see {@link Controller}
* @see {@link Scene#controllerHost}
*/
class ControllerHost {
/**
* Creates an instance of a <code>ControllerHost</code>. Typically, a <code>ControllerHost</code> is created by the Scene constructor and accessed via {@link Scene#controllerHost}.
* @see {@link Scene#controllerHost}
*/
constructor() {
/**
* @type {Controller[]}
* @private
*/
this._controllers = [];
this._needsUpdate = new Set();
}
/**
* The number of controllers registered to this host.
* @type {number}
* @readonly
*/
get controllerCount() {
return this._controllers.length;
}
/**
* Registers a controller implementation with this host.
* @param {Controller} controller An implementation of the Controller interface to register with this host.
* @param {HTMLElement} element The DOM element containing the Cesium scene.
* @param {number} [priority=0] An index, less than or equal to the current count of registed controllers, that defines the precedence of the new controller relative to those previously registered. A priority of <code>0</code> would mean the new controller would apply its updates before any other controller. As subsequent controllers are updated, their effects are applied on top of any previous update effects. If omitted, the new controller becomes the highest priority, i.e., its updates are applied after all other controllers.
*/
registerController(controller, element, priority) {
const index = priority ?? this.controllerCount;
this._controllers.splice(index, 0, controller);
this._needsUpdate.add(controller);
controller.connectedCallback(element);
}
/**
* Unregisters a controller implementation from this host.
* @param {Controller} controller An implementation of the Controller interface to unregister from this host.
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
unregisterController(controller, element) {
const controllers = this._controllers;
const index = controllers.indexOf(controller);
if (index !== -1) {
controllers.splice(index, 1);
controller.disconnectedCallback(element);
}
}
/**
* Invoked once per frame by the host scene. Updates all registered controllers in order of their priority.
* @param {Scene} scene The host scene.
* @param {JulianDate} time The current simulation time.
*/
update(scene, time) {
for (const controller of this._controllers) {
if (!controller.enabled) {
continue;
}
if (this._needsUpdate.has(controller)) {
controller.firstUpdate(scene, time);
this._needsUpdate.delete(controller);
}
controller.update(scene, time);
}
}
}
export default ControllerHost;
@@ -0,0 +1,106 @@
import ScreenSpaceElevatorCameraController from "./ScreenSpaceElevatorCameraController.js";
import ScreenSpaceMapCameraController from "./ScreenSpaceMapCameraController.js";
import Cartesian3 from "../../Core/Cartesian3.js";
import CesiumMath from "../../Core/Math.js";
/**
* A contextual camera controller that combines screenspace map panning and screenspace elevator panning. The controller automatically switches between the two based on the camera's angle relative to nadir. If the camera is looking mostly down (within angleThreshold of nadir), <code>ScreenSpaceMapCameraController</code> is used.
* If the camera is looking towards the horizon (beyond angleThreshold from nadir), the <code>ScreenSpaceElevatorCameraController</code> is used.
* @implements Controller
* @example
* viewer.scene.screenSpaceCameraController.enableInputs = false;
* viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
*
* const hybridController = new HybridScreenSpacePanCameraController();
* viewer.addController(hybridController);
*/
class HybridScreenSpacePanCameraController {
constructor() {
this._elevatorController = new ScreenSpaceElevatorCameraController();
this._mapController = new ScreenSpaceMapCameraController();
this._enabled = true;
this._ellipsoidNormal = new Cartesian3();
/**
* The angle threshold in radians that determines which controller is used. If the camera is looking within this angle of nadir, the map controller is used. Otherwise, the elevator controller is used.
* @type {number}
* @default CesiumMath.toRadians(125)
*/
this.angleThreshold = CesiumMath.toRadians(125);
}
get enabled() {
return this._enabled;
}
set enabled(value) {
this._enabled = value;
}
/**
* The controller that is used when the camera is looking more horizontally (beyond angleThreshold from nadir).
* @type {ScreenSpaceElevatorCameraController}
* @readonly
*/
get elevatorController() {
return this._elevatorController;
}
/**
* The controller that is used when the camera is looking mostly down (within angleThreshold of nadir).
* @type {ScreenSpaceMapCameraController}
* @readonly
*/
get mapController() {
return this._mapController;
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
connectedCallback(element) {
this._elevatorController.connectedCallback(element);
this._mapController.connectedCallback(element);
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
disconnectedCallback(element) {
this._elevatorController.disconnectedCallback(element);
this._mapController.disconnectedCallback(element);
}
/**
* @inheritdoc
*/
firstUpdate() {
this._elevatorController.firstUpdate();
this._mapController.firstUpdate();
}
/**
* @inheritdoc
* @param {Scene} scene
*/
update(scene) {
const camera = scene.camera;
const normal = scene.ellipsoid.geodeticSurfaceNormal(
camera.positionWC,
this._ellipsoidNormal,
);
const angle = Math.abs(Cartesian3.angleBetween(normal, camera.directionWC));
const activeController =
angle < this.angleThreshold && angle > Math.PI - this.angleThreshold
? this._elevatorController
: this._mapController;
activeController.update(scene);
}
}
export default HybridScreenSpacePanCameraController;
+32
View File
@@ -0,0 +1,32 @@
// @ts-check
/**
* This enumerated type is for classifying mouse buttons: left, middle, and right.
* @enum {number}
*/
const MouseButton = {
/**
* Represents a mouse left button.
* @type {number}
* @constant
*/
LEFT: 0,
/**
* Represents a mouse middle button.
* @type {number}
* @constant
*/
MIDDLE: 1,
/**
* Represents a mouse right button.
* @type {number}
* @constant
*/
RIGHT: 2,
};
Object.freeze(MouseButton);
export default MouseButton;
@@ -0,0 +1,295 @@
import Cartesian2 from "../../Core/Cartesian2.js";
import Cartesian3 from "../../Core/Cartesian3.js";
import defined from "../../Core/defined.js";
import Frozen from "../../Core/Frozen.js";
import getTimestamp from "../../Core/getTimestamp.js";
import CesiumMath from "../../Core/Math.js";
import ScreenSpaceEventHandler from "../../Core/ScreenSpaceEventHandler.js";
import TimeConstants from "../../Core/TimeConstants.js";
import defaultPickWorldPosition from "./defaultPickWorldPosition.js";
import ScreenSpaceInputBindings from "./ScreenSpaceInputBindings.js";
import MouseButton from "./MouseButton.js";
/**
* @typedef {object} ControllerOptions
* @memberof ScreenSpaceElevatorCameraController
* @property {ScreenSpaceInputBindings.InputBinding[]} [dragInputs] The drag input bindings that control panning.
*/
/**
* A camera controller that allows panning the camera tangential to the ellipsoid, i.e., up and down relative to the ellipsoid normal, in screen space
* by clicking and dragging the mouse.
* @class
* @implements Controller
* @example
* viewer.scene.screenSpaceCameraController.enableInputs = false;
* viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
*
* const elevatorCameraController = new Cesium.ScreenSpaceElevatorCameraController();
* viewer.addController(elevatorCameraController);
*
* @example
* // Configure the controller to use the right mouse button for panning instead of the default left mouse button.
* const elevatorCameraController = new Cesium.ScreenSpaceElevatorCameraController({
* dragInputs: [{ button: Cesium.MouseButton.RIGHT}]
* });
* viewer.addController(elevatorCameraController);
*/
class ScreenSpaceElevatorCameraController {
/**
* @private
* @returns {ScreenSpaceInputBindings.InputBinding[]} The default drag input bindings.
*/
static _getDefaultDragInputs() {
return [
Object.freeze({
button: MouseButton.LEFT,
}),
];
}
/**
* Creates an instance of a ScreenSpaceElevatorCameraController.
* @param {ScreenSpaceElevatorCameraController.ControllerOptions} [options] The options for configuring the controller.
*/
constructor(options = Frozen.EMPTY_OBJECT) {
this._enabled = true;
this._handler = undefined;
this._lastUpdateTime = undefined;
/**
* The drag input bindings that control vertical panning. Each binding is a combination of the mouse button
* and an optional keyboard modifier.
* @type {ScreenSpaceInputBindings.InputBinding[]}
* @see ScreenSpaceEventHandler
*/
this.dragInputs =
options.dragInputs ??
ScreenSpaceElevatorCameraController._getDefaultDragInputs();
this._dragInputState = undefined;
this._panDelta = new Cartesian2();
this._panPosition = new Cartesian2();
/**
* A callback function used to pick the world position from which to pan. The function is called with {@link Scene}, the {@link Cartesian2} screen space position, and a {@link Cartesian3} instance to store the result. The function should return the {@link Cartesian3} world position from which to pan, or <code>undefined</code> if no position could be picked. If <code>undefined</code> is returned, the camera will pan relative to the ellipsoid surface below the camera.
* @type {Function(Scene, Cartesian2, Cartesian3): Cartesian3|undefined}
* @default defaultPickWorldPosition
* @example
* const elevatorCameraController = new Cesium.ScreenSpaceElevatorCameraController();
* elevatorCameraController.pickWorldPosition = function (scene, windowPosition, result) {
* // Pick the world position from the depth buffer
* return scene.pickPosition(windowPosition, result);
* };
* viewer.addController(elevatorCameraController);
*/
this.pickWorldPosition = defaultPickWorldPosition;
this._ellipsoidNormal = new Cartesian3();
this._ellipsoidSurfacePosition = new Cartesian3();
this._panDirectionX = new Cartesian3();
this._panDirectionY = new Cartesian3();
this._pixelSize = new Cartesian2();
this._panVelocity = new Cartesian2();
/**
* The speed in meters per pixel at which the camera pans.
* @type {number}
* @default 1.0
*/
this.panSpeed = 1.0;
/**
* Enable or disable inertia when panning. When enabled, the camera will continue to move after the user stops dragging, gradually slowing down based on {@link ScreenSpaceMapCameraController#inertialDecay}.
* @type {boolean}
* @default true
*/
this.inertiaEnabled = true;
/**
* The rate at which the camera's pan velocity decays over time.
* @type {number}
* @default 6.0
*/
this.inertialDecay = 6.0;
/**
* A parameter in the range <code>[0, 1)</code> used to limit the range
* of inputs to a percentage of the window width/height per animation frame.
* This helps keep the camera under control in low-frame-rate situations.
* @type {number}
* @default 0.1
*/
this.maximumMovementRatio = 0.1;
}
/**
* @inheritdoc
*/
get enabled() {
return this._enabled;
}
set enabled(value) {
this._enabled = value;
if (value) {
this._lastUpdateTime = getTimestamp();
this._panDelta.x = 0;
this._panDelta.y = 0;
} else if (defined(this._dragInputState)) {
this._dragInputState.isDragging = false;
}
}
/**
* @private
* @type {boolean}
*/
get isDragging() {
return defined(this._dragInputState) && this._dragInputState.isDragging;
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
connectedCallback(element) {
const handler = new ScreenSpaceEventHandler(element);
this._handler = handler;
this._dragInputState = ScreenSpaceInputBindings.registerDragInputBindings(
handler,
this.dragInputs,
{
start: this._handleStartPan.bind(this),
change: this._handlePan.bind(this),
},
);
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
disconnectedCallback(element) {
const handler = this._handler;
if (defined(handler) && !handler.isDestroyed()) {
handler.destroy();
}
}
/**
* @inheritdoc
*/
firstUpdate() {
this._lastUpdateTime = getTimestamp();
this._panDelta.x = 0;
this._panDelta.y = 0;
}
/**
* @inheritdoc
* @param {any} scene
*/
update(scene) {
const dt =
(getTimestamp() - this._lastUpdateTime) *
TimeConstants.SECONDS_PER_MILLISECOND;
const { camera, ellipsoid, canvas } = scene;
let dx = -this._panDelta.x;
let dy = this._panDelta.y;
if (this.inertiaEnabled && !this.isDragging) {
const damping = Math.exp(-this.inertialDecay * dt);
this._panVelocity.x *= damping;
this._panVelocity.y *= damping;
dx = this._panVelocity.x * dt;
dy = this._panVelocity.y * dt;
}
const { clientWidth, clientHeight } = canvas;
if (
dt === 0 ||
clientWidth === 0 ||
clientHeight === 0 ||
(Math.abs(dx) <= CesiumMath.EPSILON3 &&
Math.abs(dy) <= CesiumMath.EPSILON3)
) {
// Reset for next frame
this._lastUpdateTime = getTimestamp();
this._panDelta.x = 0;
this._panDelta.y = 0;
return;
}
const windowPosition = this._panPosition;
let surface = this.pickWorldPosition(
scene,
windowPosition,
this._ellipsoidSurfacePosition,
);
if (!defined(surface)) {
surface = ellipsoid.scaleToGeodeticSurface(
camera.positionWC,
this._ellipsoidSurfacePosition,
);
}
let xAxis = Cartesian3.clone(camera.rightWC, this._panDirectionX);
xAxis = Cartesian3.normalize(xAxis, this._panDirectionX);
const zAxis = Cartesian3.normalize(surface, this._panDirectionY);
const distance = Cartesian3.distance(camera.positionWC, surface);
const { drawingBufferWidth, drawingBufferHeight, pixelRatio } = scene;
const pixelSize = camera.frustum.getPixelDimensions(
drawingBufferWidth,
drawingBufferHeight,
distance,
pixelRatio,
this._pixelSize,
);
const maxPixels =
this.maximumMovementRatio * Math.max(clientWidth, clientHeight);
dx = CesiumMath.clamp(dx, -maxPixels, maxPixels);
this._panVelocity.x = dx / dt;
dx *= this.panSpeed * pixelSize.x;
dy = CesiumMath.clamp(dy, -maxPixels, maxPixels);
this._panVelocity.y = dy / dt;
dy *= this.panSpeed * pixelSize.y;
camera.move(xAxis, dx);
camera.move(zAxis, dy);
// Reset for next frame
this._lastUpdateTime = getTimestamp();
this._panDelta.x = 0;
this._panDelta.y = 0;
}
_handleStartPan() {
if (!this.enabled) {
return;
}
this._panDelta.x = 0;
this._panDelta.y = 0;
}
/**
* @private
*/
_handlePan(event) {
this._panDelta.x += event.endPosition.x - event.startPosition.x;
this._panDelta.y += event.endPosition.y - event.startPosition.y;
this._panPosition.x = event.endPosition.x;
this._panPosition.y = event.endPosition.y;
}
}
export default ScreenSpaceElevatorCameraController;
@@ -0,0 +1,154 @@
import Check from "../../Core/Check.js";
import defined from "../../Core/defined.js";
import ScreenSpaceEventType from "../../Core/ScreenSpaceEventType.js";
import MouseButton from "./MouseButton.js";
/**
* @typedef {object} InputBinding
* @memberof ScreenSpaceInputBindings
* @property {MouseButton} button The mouse button used for drag start/stop.
* @property {number} [modifier] The optional keyboard modifier to register.
*/
/**
* @typedef {object} DragInputActions
* @memberof ScreenSpaceInputBindings
* @property {Function} [start] Called on drag start.
* @property {Function} [end] Called on drag stop.
* @property {Function} [change] Called on drag move.
*/
/**
* @typedef {object} DragInputState
* @memberof ScreenSpaceInputBindings
* @property {boolean} isDragging True if a drag is in progress, false otherwise.
*/
/**
* @private
* @param {MouseButton} button The mouse button.
* @returns {ScreenSpaceEventType|undefined} The corresponding down event type.
*/
function getDownEventType(button) {
if (button === MouseButton.LEFT) {
return ScreenSpaceEventType.LEFT_DOWN;
}
if (button === MouseButton.MIDDLE) {
return ScreenSpaceEventType.MIDDLE_DOWN;
}
if (button === MouseButton.RIGHT) {
return ScreenSpaceEventType.RIGHT_DOWN;
}
return undefined;
}
/**
* @private
* @param {MouseButton} button The mouse button.
* @returns {ScreenSpaceEventType|undefined} The corresponding down event type.
*/
function getUpEventType(button) {
if (button === MouseButton.LEFT) {
return ScreenSpaceEventType.LEFT_UP;
}
if (button === MouseButton.MIDDLE) {
return ScreenSpaceEventType.MIDDLE_UP;
}
if (button === MouseButton.RIGHT) {
return ScreenSpaceEventType.RIGHT_UP;
}
return undefined;
}
/**
* @namespace
*/
class ScreenSpaceInputBindings {
/**
* Registers drag input bindings on a screen space event handler.
* @param {ScreenSpaceEventHandler} handler The screen space event handler.
* @param {InputBinding[]} inputBindings The drag bindings to register.
* @param {DragInputActions} dragInputActions The callbacks to invoke for drag actions.
* @returns {DragInputState} The drag input state.
*/
static registerDragInputBindings(handler, inputBindings, dragInputActions) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("handler", handler);
Check.defined("inputBindings", inputBindings);
Check.typeOf.object("dragInputActions", dragInputActions);
//>>includeEnd('debug');
const changeModifiers = new Set();
const dragInputState = {
isDragging: false,
};
for (const binding of inputBindings) {
dragInputState.isDragging = false;
const downEventType = getDownEventType(binding.button);
const upEventType = getUpEventType(binding.button);
if (defined(downEventType)) {
handler.setInputAction(
(...e) => {
dragInputState.isDragging = true;
if (defined(dragInputActions.start)) {
dragInputActions.start(...e);
}
},
downEventType,
binding.modifier,
);
}
if (defined(upEventType)) {
handler.setInputAction(
(...e) => {
if (dragInputState.isDragging) {
dragInputState.isDragging = false;
if (defined(dragInputActions.end)) {
dragInputActions.end(...e);
}
}
},
upEventType,
binding.modifier,
);
// Register a global up event to ensure that the drag end callback is called even if the mouse is released outside of the canvas or the modifier key is released before the mouse button.
handler.setInputAction((...e) => {
if (dragInputState.isDragging) {
dragInputState.isDragging = false;
if (defined(dragInputActions.end)) {
dragInputActions.end(...e);
}
}
}, upEventType);
}
changeModifiers.add(binding.modifier);
}
for (const modifier of changeModifiers) {
handler.setInputAction(
(...e) => {
if (dragInputState.isDragging && defined(dragInputActions.change)) {
dragInputActions.change(...e);
}
},
ScreenSpaceEventType.MOUSE_MOVE,
modifier,
);
}
return dragInputState;
}
}
export default ScreenSpaceInputBindings;
@@ -0,0 +1,310 @@
import Cartesian2 from "../../Core/Cartesian2.js";
import Cartesian3 from "../../Core/Cartesian3.js";
import defined from "../../Core/defined.js";
import Frozen from "../../Core/Frozen.js";
import getTimestamp from "../../Core/getTimestamp.js";
import CesiumMath from "../../Core/Math.js";
import ScreenSpaceEventHandler from "../../Core/ScreenSpaceEventHandler.js";
import TimeConstants from "../../Core/TimeConstants.js";
import defaultPickWorldPosition from "./defaultPickWorldPosition.js";
import ScreenSpaceInputBindings from "./ScreenSpaceInputBindings.js";
import MouseButton from "./MouseButton.js";
/**
* @typedef {object} ControllerOptions
* @memberof ScreenSpaceMapCameraController
* @property {ScreenSpaceInputBindings.InputBinding[]} [dragInputs] The drag input bindings that control panning.
*/
/**
* A camera controller that allows panning the camera tangential to the ellipsoid in screen space
* by clicking and dragging the mouse.
* @class
* @implements Controller
* @example
* viewer.scene.screenSpaceCameraController.enableInputs = false;
*
* const mapCameraController = new Cesium.ScreenSpaceMapCameraController();
* viewer.addController(mapCameraController);
*
* @example
* // Configure the controller to use the right mouse button for panning instead of the default left mouse button.
* const mapCameraController = new Cesium.ScreenSpaceMapCameraController({
* dragInputs: [{ button: Cesium.MouseButton.RIGHT}]
* });
* viewer.addController(mapCameraController);
*/
class ScreenSpaceMapCameraController {
/**
* @private
* @returns {ScreenSpaceInputBindings.InputBinding[]} The default drag input bindings.
*/
static _getDefaultDragInputs() {
return [
Object.freeze({
button: MouseButton.LEFT,
}),
];
}
/**
* Creates an instance of a ScreenSpaceMapCameraController.
* @param {ScreenSpaceMapCameraController.ControllerOptions} [options] The options for configuring the controller.
*/
constructor(options = Frozen.EMPTY_OBJECT) {
this._enabled = true;
this._handler = undefined;
this._lastUpdateTime = undefined;
/**
* The drag input bindings that map panning. Each binding is a combination of the mouse button
* and an optional keyboard modifier.
* @type {ScreenSpaceInputBindings.InputBinding[]}
* @see ScreenSpaceEventHandler
*/
this.dragInputs =
options.dragInputs ??
ScreenSpaceMapCameraController._getDefaultDragInputs();
this._dragInputState = undefined;
this._panDelta = new Cartesian2();
this._panPosition = new Cartesian2();
/**
* A callback function used to pick the world position from which to pan. The function is called with {@link Scene}, the {@link Cartesian2} screen space position, and a {@link Cartesian3} instance to store the result. The function should return the {@link Cartesian3} world position from which to pan, or <code>undefined</code> if no position could be picked. If <code>undefined</code> is returned, the camera will pan relative to the ellipsoid surface below the camera.
* @type {Function(Scene, Cartesian2, Cartesian3): Cartesian3|undefined}
* @default defaultPickWorldPosition
* @example
* const mapCameraController = new Cesium.ScreenSpaceMapCameraController();
* mapCameraController.pickWorldPosition = function (scene, windowPosition, result) {
* // Pick the world position from the depth buffer
* return scene.pickPosition(windowPosition, result);
* };
* viewer.addController(mapCameraController);
*/
this.pickWorldPosition = defaultPickWorldPosition;
this._ellipsoidNormal = new Cartesian3();
this._ellipsoidSurfacePosition = new Cartesian3();
this._panDirectionX = new Cartesian3();
this._panDirectionY = new Cartesian3();
this._pixelSize = new Cartesian2();
this._panVelocity = new Cartesian2();
/**
* The speed in meters per pixel at which the camera pans.
* @type {number}
* @default 1.0
*/
this.panSpeed = 1.0;
/**
* Enable or disable inertia when panning. When enabled, the camera will continue to move after the user stops dragging, gradually slowing down based on {@link ScreenSpaceMapCameraController#inertialDecay}.
* @type {boolean}
* @default true
*/
this.inertiaEnabled = true;
/**
* The rate at which the camera's pan velocity decays over time.
* @type {number}
* @default 6.0
*/
this.inertialDecay = 6.0;
/**
* A parameter in the range <code>[0, 1)</code> used to limit the range
* of inputs to a percentage of the window width/height per animation frame.
* This helps keep the camera under control in low-frame-rate situations.
* @type {number}
* @default 0.1
*/
this.maximumMovementRatio = 0.1;
}
/**
* @inheritdoc
*/
get enabled() {
return this._enabled;
}
set enabled(value) {
this._enabled = value;
if (value) {
this._panDelta.x = 0;
this._panDelta.y = 0;
} else if (defined(this._dragInputState)) {
this._dragInputState.isDragging = false;
}
}
/**
* @private
* @type {boolean}
*/
get isDragging() {
return defined(this._dragInputState) && this._dragInputState.isDragging;
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
connectedCallback(element) {
const handler = new ScreenSpaceEventHandler(element);
this._handler = handler;
this._dragInputState = ScreenSpaceInputBindings.registerDragInputBindings(
handler,
this.dragInputs,
{
start: this._handleStartPan.bind(this),
change: this._handlePan.bind(this),
},
);
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
disconnectedCallback(element) {
const handler = this._handler;
if (defined(handler) && !handler.isDestroyed()) {
handler.destroy();
}
}
/**
* @inheritdoc
*/
firstUpdate() {
this._lastUpdateTime = getTimestamp();
this._panDelta.x = 0;
this._panDelta.y = 0;
}
/**
* @inheritdoc
* @param {any} scene
*/
update(scene) {
const dt =
(getTimestamp() - this._lastUpdateTime) *
TimeConstants.SECONDS_PER_MILLISECOND;
let dx = -this._panDelta.x;
let dy = this._panDelta.y;
if (this.inertiaEnabled && !this.isDragging) {
const damping = Math.exp(-this.inertialDecay * dt);
this._panVelocity.x *= damping;
this._panVelocity.y *= damping;
dx = this._panVelocity.x * dt;
dy = this._panVelocity.y * dt;
}
const { camera, ellipsoid, canvas } = scene;
const { clientWidth, clientHeight } = canvas;
if (
dt === 0 ||
clientWidth === 0 ||
clientHeight === 0 ||
(Math.abs(dx) <= CesiumMath.EPSILON3 &&
Math.abs(dy) <= CesiumMath.EPSILON3)
) {
// Reset for next frame
this._lastUpdateTime = getTimestamp();
this._panDelta.x = 0;
this._panDelta.y = 0;
return;
}
const windowPosition = this._panPosition;
let surface = this.pickWorldPosition(
scene,
windowPosition,
this._ellipsoidSurfacePosition,
);
if (!defined(surface)) {
surface = ellipsoid.scaleToGeodeticSurface(
camera.positionWC,
this._ellipsoidSurfacePosition,
);
}
const zAxis = ellipsoid.geodeticSurfaceNormal(
surface,
this._ellipsoidNormal,
);
let xAxis = Cartesian3.clone(camera.rightWC, this._panDirectionX);
xAxis = Cartesian3.normalize(xAxis, this._panDirectionX);
// If z-axis is parallel to camera forward, we use the camera up vector to compute the y-axis. Otherwise, we use the z-axis and x-axis to compute the y-axis.
let yAxis = Cartesian3.clone(camera.upWC, this._panDirectionY);
const theta = Math.abs(Cartesian3.dot(zAxis, camera.directionWC));
if (CesiumMath.lessThan(theta, 1.0, CesiumMath.EPSILON6)) {
yAxis = Cartesian3.cross(zAxis, xAxis, this._panDirectionY);
}
yAxis = Cartesian3.normalize(yAxis, this._panDirectionY);
const distance = Cartesian3.distance(camera.positionWC, surface);
const { drawingBufferWidth, drawingBufferHeight, pixelRatio } = scene;
const pixelSize = camera.frustum.getPixelDimensions(
drawingBufferWidth,
drawingBufferHeight,
distance,
pixelRatio,
this._pixelSize,
);
const maxPixels =
this.maximumMovementRatio * Math.max(clientWidth, clientHeight);
dx = CesiumMath.clamp(dx, -maxPixels, maxPixels);
this._panVelocity.x = dx / dt;
dx *= this.panSpeed * pixelSize.x;
dy = CesiumMath.clamp(dy, -maxPixels, maxPixels);
this._panVelocity.y = dy / dt;
dy *= this.panSpeed * pixelSize.y;
camera.move(xAxis, dx);
camera.move(yAxis, dy);
// Reset for next frame
this._lastUpdateTime = getTimestamp();
this._panDelta.x = 0;
this._panDelta.y = 0;
}
/**
* @private
* @param {Event} event
*/
_handleStartPan(event) {
if (!this.enabled) {
return;
}
this._panDelta.x = 0;
this._panDelta.y = 0;
}
/**
* @private
*/
_handlePan(event) {
this._panDelta.x += event.endPosition.x - event.startPosition.x;
this._panDelta.y += event.endPosition.y - event.startPosition.y;
this._panPosition.x = event.endPosition.x;
this._panPosition.y = event.endPosition.y;
}
}
export default ScreenSpaceMapCameraController;
@@ -0,0 +1,642 @@
import Cartesian2 from "../../Core/Cartesian2.js";
import Cartesian3 from "../../Core/Cartesian3.js";
import Check from "../../Core/Check.js";
import defined from "../../Core/defined.js";
import Ellipsoid from "../../Core/Ellipsoid.js";
import Frozen from "../../Core/Frozen.js";
import getTimestamp from "../../Core/getTimestamp.js";
import KeyboardEventModifier from "../../Core/KeyboardEventModifier.js";
import CesiumMath from "../../Core/Math.js";
import Matrix3 from "../../Core/Matrix3.js";
import Matrix4 from "../../Core/Matrix4.js";
import ScreenSpaceEventHandler from "../../Core/ScreenSpaceEventHandler.js";
import Quaternion from "../../Core/Quaternion.js";
import TimeConstants from "../../Core/TimeConstants.js";
import Transforms from "../../Core/Transforms.js";
import defaultPickWorldPosition from "./defaultPickWorldPosition.js";
import ScreenSpaceInputBindings from "./ScreenSpaceInputBindings.js";
import MouseButton from "./MouseButton.js";
/**
* @typedef {object} ControllerOptions
* @memberof ScreenSpaceTiltOrbitCameraController
* @property {ScreenSpaceInputBindings.InputBinding[]} [dragInputs] The drag input bindings that control tilting and orbiting.
*/
/**
* A camera controller that allows tilting and orbiting the camera around a target position in screen space by clicking and dragging the mouse or touching and dragging on a touch screen.
* @class
* @implements Controller
* @example
* viewer.scene.screenSpaceCameraController.enableInputs = false;
* viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
*
* const tiltOrbitController = new Cesium.ScreenSpaceTiltOrbitCameraController();
* viewer.addController(tiltOrbitController);
*
* @example
* // Tilt around the position under the cursor or tap when dragging starts instead of the position at the center of the screen.
* viewer.scene.screenSpaceCameraController.enableInputs = false;
* viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
*
* const tiltOrbitController = new Cesium.ScreenSpaceTiltOrbitCameraController();
* tiltOrbitController.useDragPosition = true;
* viewer.addController(tiltOrbitController);
*
* @example
* // Configure the controller to use the left mouse button for tilting and orbiting instead of the default right mouse button.
* const tiltOrbitController = new Cesium.ScreenSpaceTiltOrbitCameraController({
* dragInputs: [{ button: Cesium.MouseButton.LEFT }]
* });
* viewer.addController(tiltOrbitController);
*/
class ScreenSpaceTiltOrbitCameraController {
/**
* @private
* @returns {ScreenSpaceInputBindings.InputBinding[]} The default drag input bindings.
*/
static _getDefaultDragInputs() {
return [
Object.freeze({
button: MouseButton.LEFT,
modifier: KeyboardEventModifier.CTRL,
}),
Object.freeze({
button: MouseButton.RIGHT,
}),
];
}
/**
* Creates a new instance of <code>ScreenSpaceTiltOrbitCameraController</code>.
* @param {ScreenSpaceTiltOrbitCameraController.ControllerOptions} [options] The options for configuring the controller.
*/
constructor(options = Frozen.EMPTY_OBJECT) {
this._enabled = true;
this._handler = undefined;
this._lastUpdateTime = undefined;
/**
* Enabled dragging to tilt the camera.
* @type {boolean}
* @default true
*/
this.tiltEnabled = true;
/**
* Enabled dragging to orbit the camera.
* @type {boolean}
* @default true
*/
this.orbitEnabled = true;
/**
* If false, the camera will orbit and tilt around the position at the center of the screen. If true, the camera will orbit and tilt around the position under the cursor or tap when dragging starts.
* @type {boolean}
* @default false
*/
this.useDragPosition = false;
/**
* The drag input bindings that control tilting. Each binding is a combination of the mouse button
* and an optional keyboard modifier.
* @type {ScreenSpaceInputBindings.InputBinding[]}
* @see ScreenSpaceEventHandler
*/
this.dragInputs =
options.dragInputs ??
ScreenSpaceTiltOrbitCameraController._getDefaultDragInputs();
this._dragInputState = undefined;
this._dragDelta = new Cartesian2();
this._screenSpaceDragPosition = new Cartesian2();
this._screenSpaceOrigin = new Cartesian2();
/**
* A callback function used to pick the world position around which to tilt or orbit. The function is called with {@link Scene}, the {@link Cartesian2} screen space position, and a {@link Cartesian3} instance to store the result. The function should return the {@link Cartesian3} world position from which to tilt or orbit, or <code>undefined</code> if no position could be picked.
* @type {Function(Scene, Cartesian2, Cartesian3): Cartesian3|undefined}
* @default defaultPickWorldPosition
* @example
* const tiltOrbitCameraController = new Cesium.ScreenSpaceTiltOrbitCameraController();
* tiltOrbitCameraController.pickWorldPosition = function (scene, windowPosition, result) {
* // Pick the world position from the depth buffer
* return scene.pickPosition(windowPosition, result);
* };
* viewer.addController(tiltOrbitCameraController);
*/
this.pickWorldPosition = defaultPickWorldPosition;
this._hasTarget = false;
this._target = new Cartesian3();
this._axis = new Cartesian3();
/**
* The amount at which the camera tilts per dragged pixel. A value of 1.0 means that dragging the mouse across the entire canvas will tilt the camera by 90 degrees.
* @type {number}
* @default 2.0
*/
this.tiltMagnitude = 2.0;
/**
* Enables or disables damping for tilt and orbit animations. Damping smooths out the camera movement and makes it feel more natural or weighty, but it can also introduce a slight delay in the camera response. If damping is disabled, the camera will respond immediately to user input.
* @type {boolean}
* @default true
*/
this.dampingEnabled = true;
/**
* Specifies the length of time in seconds in which a single tilt animation is targeted to complete.
* @type {number}
* @default 0.0045
*/
this.tiltAnimationDuration = 0.0045;
/**
* The maximum tilt velocity in radians per second. A value of Number.POSITIVE_INFINITY means that the maximum tilt velocity is unbounded.
* @type {number}
* @default CesiumMath.PI
*/
this.maximumTiltVelocity = CesiumMath.PI;
/**
* @private
* @type {number}
* @default CesiumMath.EPSILON20
*/
this.minimumTiltVelocity = CesiumMath.EPSILON20;
/**
* The amount at which the camera orbits per dragged pixel. A value of 1.0 means that dragging the mouse across the entire canvas will orbit the camera by 180 degrees.
* @type {number}
* @default 2.0
*/
this.orbitMagnitude = 2.0;
/**
* Specifies the length of time in seconds in which a single orbit animation completes.
* @type {number}
* @default 0.0045
*/
this.orbitAnimationDuration = 0.0045;
/**
* The maximum orbit velocity in radians per second. A value of Number.POSITIVE_INFINITY means that the maximum orbit velocity is unbounded.
* @type {number}
* @default CesiumMath.TWO_PI
*/
this.maximumOrbitVelocity = CesiumMath.TWO_PI;
/**
* @private
* @type {number}
* @default CesiumMath.EPSILON20
*/
this.minimumOrbitVelocity = CesiumMath.EPSILON20;
this._tiltAxis = new Cartesian3();
this._tiltQuaternion = new Quaternion();
this._tiltOffset = new Cartesian3();
this._tiltOrigin = new Cartesian3();
this._tiltDampenedResults = {
velocity: 0.0,
value: 0.0,
};
this._orbitTargetEnu = new Matrix4();
this._orbitTargetEast = new Cartesian3();
this._orbitQuaternion = new Quaternion();
this._orbitOffset = new Cartesian3();
this._orbitLookOffset = new Cartesian3();
this._orbitOrigin = new Cartesian3();
this._orbitDampenedResults = {
velocity: 0.0,
value: 0.0,
};
/**
* A parameter in the range <code>[0, 1)</code> used to limit the range
* of inputs to a percentage of the window width/height per animation frame.
* This helps keep the camera under control in low-frame-rate situations.
* @type {number}
* @default 0.1
*/
this.maximumMovementRatio = 0.1;
}
/**
* @inheritdoc
*/
get enabled() {
return this._enabled;
}
set enabled(value) {
this._enabled = value;
if (value) {
this._lastUpdateTime = getTimestamp();
this._dragDelta.x = 0.0;
this._dragDelta.y = 0.0;
} else if (defined(this._dragInputState)) {
this._dragInputState.isDragging = false;
}
}
/**
* @private
* @type {boolean}
*/
get isDragging() {
return defined(this._dragInputState) && this._dragInputState.isDragging;
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
connectedCallback(element) {
const handler = new ScreenSpaceEventHandler(element);
this._handler = handler;
this._dragInputState = ScreenSpaceInputBindings.registerDragInputBindings(
handler,
this.dragInputs,
{
start: this._handleStartDrag.bind(this),
change: this._handleDrag.bind(this),
},
);
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
disconnectedCallback(element) {
const handler = this._handler;
if (defined(handler) && !handler.isDestroyed()) {
handler.destroy();
}
}
/**
* @inheritdoc
*/
firstUpdate() {
this._lastUpdateTime = getTimestamp();
this._dragDelta.x = 0.0;
this._dragDelta.y = 0.0;
}
/**
* @private
*/
_handleStartDrag(event) {
if (!this.enabled) {
return;
}
this._hasTarget = false;
this._screenSpaceDragPosition.x = event.position.x;
this._screenSpaceDragPosition.y = event.position.y;
this._dragDelta.x = 0.0;
this._dragDelta.y = 0.0;
}
/**
* @private
*/
_handleDrag(event) {
this._dragDelta.x += event.endPosition.x - event.startPosition.x;
this._dragDelta.y += event.endPosition.y - event.startPosition.y;
}
/**
* The current tilt angle of the camera in radians. A value of 0.0 means that the camera is looking straight down at the ellipsoid, and a value of PI/2 means that the camera is looking at the horizon.
* @type {number}
* @private
*/
get tiltAngle() {
return this._tiltDampenedResults.value;
}
/**
* The current tilt velocity of the camera in radians per second.
* @type {number}
* @private
*/
get tiltVelocity() {
return this._tiltDampenedResults.velocity;
}
/**
* The current tilt velocity of the camera in radians per second.
* @type {number}
* @private
*/
set tiltVelocity(value) {
this._tiltDampenedResults.velocity = value;
}
/**
* The current orbit angle of the camera in radians around the target. A value of 0.0 means that the camera is looking at the target from the east, and a value of PI/2 means that the camera is looking at the target from the north.
* @type {number}
* @private
*/
get orbitAngle() {
return this._orbitDampenedResults.value;
}
/**
* The current orbit velocity of the camera in radians per second.
* @type {number}
* @private
*/
get orbitVelocity() {
return this._orbitDampenedResults.velocity;
}
/**
* The current orbit velocity of the camera in radians per second.
* @type {number}
* @private
*/
set orbitVelocity(value) {
this._orbitDampenedResults.velocity = value;
}
/**
* Attempts to orbit the camera around the specified origin by the specified amount in radians. Positive values orbit the camera clockwise, negative values orbit the camera counterclockwise. If the drag origin is not on the ellipsoid, no orbit is applied.
* @param {Camera} camera The camera to orbit.
* @param {Cartesian3} target The origin position to orbit around in world coordinates.
* @param {Cartesian3} axis The axis to orbit around, typically the negative of the surface normal at the target position.
* @param {number} amount The amount to orbit the camera in radians. Positive values orbit the camera clockwise, negative values orbit the camera counterclockwise.
* @param {number} dt The time delta in seconds since the last update.
* @param {Ellipsoid} [ellipsoid=Ellipsoid.default] The ellipsoid to pick for the orbit origin. If undefined, the default ellipsoid is used.
*/
orbit(camera, target, axis, amount, dt, ellipsoid = Ellipsoid.default) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("camera", camera);
Check.typeOf.object("target", target);
Check.typeOf.object("axis", axis);
Check.typeOf.number("amount", amount);
Check.typeOf.number.greaterThan("dt", dt, 0);
Check.typeOf.object("ellipsoid", ellipsoid);
//>>includeEnd('debug');
const enu = Transforms.eastNorthUpToFixedFrame(
target,
ellipsoid,
this._orbitTargetEnu,
);
const east = Matrix4.multiplyByPointAsVector(
enu,
Cartesian3.UNIT_X,
this._orbitTargetEast,
);
const currentOrbitAngle = Cartesian3.angleBetween(camera.directionWC, east);
if (Math.abs(this.orbitVelocity) < this.minimumOrbitVelocity) {
this.orbitVelocity = 0.0;
}
// Apply inertia
if (!this.isDragging && this.dampingEnabled) {
amount += this.orbitVelocity * dt;
}
if (amount === 0.0) {
return;
}
const targetOrbitAngle = currentOrbitAngle + amount;
// Apply critical damping
const maxSpeed = this.dampingEnabled
? this.maximumOrbitVelocity * this.orbitMagnitude
: undefined;
const smoothTime = this.dampingEnabled
? this.orbitAnimationDuration
: undefined;
this._orbitDampenedResults = CesiumMath.smoothDamp(
currentOrbitAngle,
targetOrbitAngle,
this.orbitVelocity,
dt,
maxSpeed,
smoothTime,
this._orbitDampenedResults,
);
const rho = this.orbitAngle - currentOrbitAngle;
const rotation = Matrix3.fromQuaternion(
Quaternion.fromAxisAngle(axis, -rho, this._orbitQuaternion),
);
const targetOffset = Cartesian3.subtract(
camera.positionWC,
target,
this._orbitOffset,
);
const t = Cartesian3.dot(targetOffset, camera.directionWC);
const offset = Cartesian3.multiplyByScalar(
camera.directionWC,
t,
this._orbitLookOffset,
);
const lookOffset = Cartesian3.subtract(
targetOffset,
offset,
this._orbitLookOffset,
);
const rotatedTargetOffset = Matrix3.multiplyByVector(
rotation,
targetOffset,
this._orbitOffset,
);
const rotatedLookOffset = Matrix3.multiplyByVector(
rotation,
lookOffset,
this._orbitLookOffset,
);
Cartesian3.add(target, rotatedTargetOffset, camera.position);
const lookTarget = Cartesian3.add(
target,
rotatedLookOffset,
this._orbitOrigin,
);
camera.lookAtWorldPosition(lookTarget, ellipsoid);
}
/**
* Attempts to tilt the camera by the specified amount in radians. Positive values tilt the camera down, negative values tilt the camera up. If the drag origin is not on the ellipsoid, no tilt is applied.
* @param {Camera} camera The camera to tilt.
* @param {Cartesian3} target The origin position to tilt around in world coordinates.
* @param {Cartesian3} axis The axis to tilt around, typically the negative of the surface normal at the target position.
* @param {number} amount The amount to tilt the camera in radians. Positive values tilt the camera down, negative values tilt the camera up.
* @param {number} dt The time delta in seconds since the last update. Value must be greater than 0.
* @param {Ellipsoid} [ellipsoid=Ellipsoid.default] The ellipsoid to pick for the tilt origin. If undefined, the default ellipsoid is used.
*/
tilt(camera, target, axis, amount, dt, ellipsoid = Ellipsoid.default) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("camera", camera);
Check.typeOf.object("target", target);
Check.typeOf.object("axis", axis);
Check.typeOf.number("amount", amount);
Check.typeOf.number.greaterThan("dt", dt, 0);
Check.typeOf.object("ellipsoid", ellipsoid);
//>>includeEnd('debug');
if (Math.abs(this.tiltVelocity) < this.minimumTiltVelocity) {
this.tiltVelocity = 0.0;
}
// Apply inertia
if (!this.isDragging && this.dampingEnabled) {
amount += this.tiltVelocity * dt;
}
if (amount === 0.0) {
return;
}
const currentTiltAngle = Cartesian3.angleBetween(camera.direction, axis);
// Avoid large deltas when the sign is close to flipping, which can happen when the camera is looking straight down at the ellipsoid.
if (
(currentTiltAngle < CesiumMath.PI_OVER_TWO && amount > 0.0) ||
(currentTiltAngle > CesiumMath.PI_OVER_TWO && amount < 0.0)
) {
amount *= Math.abs(Math.sin(currentTiltAngle));
}
const targetTiltAngle = currentTiltAngle + amount;
const maxSpeed = this.dampingEnabled
? this.maximumTiltVelocity * this.tiltMagnitude
: undefined;
const smoothTime = this.dampingEnabled
? this.tiltAnimationDuration
: undefined;
CesiumMath.smoothDamp(
currentTiltAngle,
targetTiltAngle,
this.tiltVelocity,
dt,
maxSpeed,
smoothTime,
this._tiltDampenedResults,
);
const theta = this.tiltAngle - currentTiltAngle;
const rotation = Matrix3.fromQuaternion(
Quaternion.fromAxisAngle(camera.rightWC, -theta, this._tiltQuaternion),
);
const offset = Cartesian3.subtract(
camera.position,
target,
this._tiltOffset,
);
const t = Cartesian3.dot(offset, camera.directionWC);
const lookOffset = Cartesian3.multiplyByScalar(
camera.directionWC,
t,
this._tiltOffset,
);
const lookTarget = Cartesian3.subtract(
camera.position,
lookOffset,
this._tiltOrigin,
);
const rotatedOffset = Matrix3.multiplyByVector(
rotation,
lookOffset,
this._tiltOffset,
);
Cartesian3.add(lookTarget, rotatedOffset, camera.position);
camera.lookAtWorldPosition(lookTarget, ellipsoid);
}
/**
* @inheritdoc
* @param {Scene} scene
*/
update(scene) {
const dt =
(getTimestamp() - this._lastUpdateTime) *
TimeConstants.SECONDS_PER_MILLISECOND;
const { canvas } = scene;
const { clientWidth, clientHeight } = canvas;
if (dt === 0 || clientWidth === 0 || clientHeight === 0) {
// Reset for next frame
this._lastUpdateTime = getTimestamp();
this._dragDelta.x = 0;
this._dragDelta.y = 0;
return;
}
// Target position to orbit and tilt around. Pick the world position when dragging begins, and use that position for the duration of the drag.
let target = this._target;
if (this.isDragging && !this._hasTarget) {
let windowPosition = this._screenSpaceDragPosition;
if (!this.useDragPosition) {
windowPosition = this._screenSpaceOrigin;
windowPosition.x = clientWidth / 2.0;
windowPosition.y = clientHeight / 2.0;
}
const dragPositionTarget = this.pickWorldPosition(
scene,
windowPosition,
this._target,
);
const picked = defined(dragPositionTarget);
this._hasTarget = picked;
target = dragPositionTarget;
}
if (this._hasTarget) {
const { camera, ellipsoid } = scene;
const normal = ellipsoid.geodeticSurfaceNormal(target, this._axis);
const axis = Cartesian3.negate(normal, this._axis);
if (this.orbitEnabled) {
let dx = this._dragDelta.x / clientWidth;
dx = CesiumMath.clamp(
dx,
-this.maximumMovementRatio,
this.maximumMovementRatio,
);
dx *= this.orbitMagnitude * CesiumMath.TWO_PI;
this.orbit(camera, target, axis, dx, dt, ellipsoid);
}
if (this.tiltEnabled) {
let dy = this._dragDelta.y / clientHeight;
dy = CesiumMath.clamp(
dy,
-this.maximumMovementRatio,
this.maximumMovementRatio,
);
dy *= this.tiltMagnitude * CesiumMath.PI;
this.tilt(camera, target, axis, dy, dt, ellipsoid);
}
}
// Reset for next frame
this._lastUpdateTime = getTimestamp();
this._dragDelta.x = 0;
this._dragDelta.y = 0;
}
}
export default ScreenSpaceTiltOrbitCameraController;
@@ -0,0 +1,431 @@
import Cartesian2 from "../../Core/Cartesian2.js";
import Cartesian3 from "../../Core/Cartesian3.js";
import defined from "../../Core/defined.js";
import Frozen from "../../Core/Frozen.js";
import getTimestamp from "../../Core/getTimestamp.js";
import CesiumMath from "../../Core/Math.js";
import ScreenSpaceEventHandler from "../../Core/ScreenSpaceEventHandler.js";
import ScreenSpaceEventType from "../../Core/ScreenSpaceEventType.js";
import defaultPickWorldPosition from "./defaultPickWorldPosition.js";
import ScreenSpaceInputBindings from "./ScreenSpaceInputBindings.js";
import TimeConstants from "../../Core/TimeConstants.js";
import MouseButton from "./MouseButton.js";
/**
* @typedef {object} ControllerOptions
* @memberof ScreenSpaceZoomCameraController
* @property {ScreenSpaceInputBindings.InputBinding[]} [dragInputs] The drag input bindings that control zooming.
* @property {ScreenSpaceEventType[]} [scrollInputs] The scroll input bindings that control zooming.
*/
/**
* A camera controller that allows zooming the camera in and out based on the pointer location in screen space.
* @class
* @implements Controller
* @example
* viewer.scene.screenSpaceCameraController.enableInputs = false;
* viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;
*
* const zoomCameraController = new Cesium.ScreenSpaceZoomCameraController();
* viewer.addController(zoomCameraController);
*/
class ScreenSpaceZoomCameraController {
/**
* @private
* @returns {ScreenSpaceInputBindings.InputBinding[]} The default drag input bindings.
*/
static _getDefaultDragInputs() {
return [
Object.freeze({
button: MouseButton.MIDDLE,
}),
];
}
/**
* @private
* @returns {ScreenSpaceEventType[]} The default scroll input bindings.
*/
static _getDefaultScrollInputs() {
return [ScreenSpaceEventType.WHEEL];
}
/**
* Creates a new instance of <code>ScreenSpaceZoomCameraController</code>.
* @param {ScreenSpaceZoomCameraController.ControllerOptions} [options] The options for configuring the controller.
*/
constructor(options = Frozen.EMPTY_OBJECT) {
this._enabled = true;
this._handler = undefined;
this._lastUpdateTime = undefined;
/**
* If false, the camera will zoom to the position at the center of the screen. If true, the camera will zoom to the position under the cursor or tap when dragging starts or when scrolling with the scroll wheel.
* @type {boolean}
* @default false
*/
this.usePointerPosition = false;
/**
* The drag input bindings that control zooming. Each binding is a combination of the mouse button
* and an optional keyboard modifier.
* @type {ScreenSpaceInputBindings.InputBinding[]}
* @see ScreenSpaceEventHandler
*/
this.dragInputs =
options.dragInputs ??
ScreenSpaceZoomCameraController._getDefaultDragInputs();
/**
* The scroll input bindings that control zooming.
* @type {ScreenSpaceEventType[]}
* @see ScreenSpaceEventHandler
* @default [ScreenSpaceEventType.WHEEL]
*/
this.scrollInputs =
options.scrollInputs ??
ScreenSpaceZoomCameraController._getDefaultScrollInputs();
this._dragInputState = undefined;
this._dragDelta = new Cartesian2();
this._scrollDelta = 0.0;
this._zoomInputVelocity = 0.0;
this._screenSpaceScrollPosition = new Cartesian2();
this._screenSpaceDragPosition = new Cartesian2();
this._screenSpaceOrigin = new Cartesian2();
/**
* The rate at which the camera zooms in and out based on the mouse wheel delta.
* @type {number}
* @default 0.2
*/
this.zoomSensitivity = 0.2;
/**
* A callback function used to pick the world position from which to zoom. The function is called with {@link Scene}, the {@link Cartesian2} screen space position, and a {@link Cartesian3} instance to store the result. The function should return the {@link Cartesian3} world position from which to zoom, or <code>undefined</code> if no position could be picked.
* @type {Function(Scene, Cartesian2, Cartesian3): Cartesian3|undefined}
* @default defaultPickWorldPosition
* @example
* const zoomCameraController = new Cesium.ScreenSpaceZoomCameraController();
* zoomCameraController.pickWorldPosition = function (scene, windowPosition, result) {
* // Pick the world position from the depth buffer
* return scene.pickPosition(windowPosition, result);
* };
* viewer.addController(zoomCameraController);
*/
this.pickWorldPosition = defaultPickWorldPosition;
/**
* The ratio of the camera's distance to the zoom target that defines how much the camera zooms in and out per second.
* @type {number}
* @default 0.4
*/
this.zoomDistanceRatio = 0.4;
/**
* Enable or disable inertia when zooming. When enabled, the camera will continue to move after the user input stops, gradually slowing down based on {@link ScreenSpaceZoomCameraController#inertialDecay}.
* @type {boolean}
* @default true
*/
this.inertiaEnabled = false;
/**
* The rate at which the camera's zoom velocity decays over time.
* @type {number}
* @default 6.0
*/
this.inertialDecay = 6.0;
/**
* @private
* @type number
* @default 0.0
*/
this.minimumZoomDistance = 0.0;
/**
* Maximum distance from the zoom target that the camera can move away.
* @type {number}
* @default 100000.0
*/
this.maximumZoomDistance = 100000.0;
/**
* @private
* @type {number}
* @default CesiumMath.EPSILON20
*/
this.minimumZoomVelocity = CesiumMath.EPSILON20;
/**
* The maximum zoom velocity in meters per second. This limits the speed at which the camera can zoom in and out.
* @type {number}
* @default 1.0
*/
this.maximumZoomVelocity = 1.0;
/**
* Enables or disables damping for zooming. Damping smooths out the camera movement and makes it feel more natural or weighty, but it can also introduce a slight delay in the camera response. If damping is disabled, the camera will respond immediately to user input.
* @type {boolean}
* @default true
*/
this.dampingEnabled = true;
/**
* Specifies the length of time in seconds in which a single zoom animation is targeted to complete.
* @type {number}
* @default 0.45
*/
this.zoomAnimationDuration = 0.45;
this._target = new Cartesian3();
this._zoomDirection = new Cartesian3();
this._zoomDampenedResults = {
velocity: 0.0,
value: 0.0,
};
}
/**
* @inheritdoc
*/
get enabled() {
return this._enabled;
}
set enabled(value) {
this._enabled = value;
if (value) {
this._lastUpdateTime = getTimestamp();
this._scrollDelta = 0.0;
this._dragDelta.x = 0.0;
this._dragDelta.y = 0.0;
} else if (defined(this._dragInputState)) {
this._dragInputState.isDragging = false;
}
}
/**
* @private
* @type {boolean}
*/
get isDragging() {
return defined(this._dragInputState) && this._dragInputState.isDragging;
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
connectedCallback(element) {
const handler = new ScreenSpaceEventHandler(element);
this._handler = handler;
for (const input of this.scrollInputs) {
handler.setInputAction(this._handleZoom.bind(this), input);
}
handler.setInputAction(
this._handleZoomPosition.bind(this),
ScreenSpaceEventType.MOUSE_MOVE,
);
this._dragInputState = ScreenSpaceInputBindings.registerDragInputBindings(
handler,
this.dragInputs,
{
start: this._handleStartDrag.bind(this),
change: this._handleDrag.bind(this),
},
);
}
/**
* @inheritdoc
* @param {HTMLElement} element The DOM element containing the Cesium scene.
*/
disconnectedCallback(element) {
const handler = this._handler;
if (defined(handler) && !handler.isDestroyed()) {
handler.destroy();
}
}
/**
* @inheritdoc
*/
firstUpdate() {
this._lastUpdateTime = getTimestamp();
this._scrollDelta = 0.0;
this._dragDelta.x = 0.0;
this._dragDelta.y = 0.0;
}
/**
* The current zoom distance of the camera in meters. This is the distance from the camera to the zoom target.
* @type {number}
* @private
*/
get zoomDistance() {
return this._zoomDampenedResults.value;
}
/**
* The current zoom velocity of the camera in radians per second.
* @type {number}
* @private
*/
get zoomVelocity() {
return this._zoomDampenedResults.velocity;
}
/**
* The current zoom velocity of the camera in radians per second.
* @type {number}
* @private
*/
set zoomVelocity(value) {
this._zoomDampenedResults.velocity = value;
}
/**
* @inheritdoc
* @param {Scene} scene
*/
update(scene) {
const now = getTimestamp();
const dt =
(now - this._lastUpdateTime) * TimeConstants.SECONDS_PER_MILLISECOND;
const { canvas } = scene;
const { clientWidth, clientHeight } = canvas;
if (dt === 0 || clientWidth === 0 || clientHeight === 0) {
this._lastUpdateTime = getTimestamp();
this._scrollDelta = 0.0;
this._dragDelta.x = 0;
this._dragDelta.y = 0;
return;
}
let dz = this._scrollDelta + this._dragDelta.y;
if (dz === 0.0 && this.inertiaEnabled) {
const damping = Math.exp(-this.inertialDecay * dt);
this._zoomInputVelocity *= damping;
dz = this._zoomInputVelocity * dt;
}
if (
Math.abs(this.zoomVelocity) < this.minimumZoomVelocity &&
dz <= CesiumMath.EPSILON3 &&
dz >= -CesiumMath.EPSILON3 &&
this.zoomVelocity <= CesiumMath.EPSILON3
) {
this._lastUpdateTime = getTimestamp();
this._scrollDelta = 0.0;
this._dragDelta.x = 0;
this._dragDelta.y = 0;
this.zoomVelocity = 0.0;
return;
}
this._zoomInputVelocity = CesiumMath.clamp(
dz / dt,
-this.maximumZoomVelocity,
this.maximumZoomVelocity,
);
const { camera, ellipsoid } = scene;
let direction = camera.direction;
let distance =
Cartesian3.magnitude(camera.positionWC) - ellipsoid.maximumRadius;
let windowPosition = this.isDragging
? this._screenSpaceDragPosition
: this._screenSpaceScrollPosition;
if (!this.useDragPosition) {
windowPosition = this._screenSpaceOrigin;
windowPosition.x = clientWidth / 2.0;
windowPosition.y = clientHeight / 2.0;
}
const target = this.pickWorldPosition(scene, windowPosition, this._target);
if (defined(target)) {
direction = Cartesian3.subtract(
target,
camera.positionWC,
this._zoomDirection,
);
direction = Cartesian3.normalize(direction, this._zoomDirection);
distance = Cartesian3.distance(target, camera.positionWC);
}
distance = CesiumMath.clamp(
distance,
distance > 0.0 ? this.minimumZoomDistance : -this.maximumZoomDistance,
distance > 0.0 ? this.maximumZoomDistance : -this.minimumZoomDistance,
);
const zoom = dz * distance * this.zoomDistanceRatio;
const smoothTime = this.dampingEnabled
? this.zoomAnimationDuration
: undefined;
this._zoomDampenedResults = CesiumMath.smoothDamp(
0.0,
zoom,
this.zoomVelocity,
dt,
undefined,
smoothTime,
this._zoomDampenedResults,
);
camera.move(direction, this.zoomDistance);
// Reset for next frame
this._lastUpdateTime = getTimestamp();
this._scrollDelta = 0.0;
this._dragDelta.x = 0;
this._dragDelta.y = 0;
}
/**
* @private
* @param {number} amount
*/
_handleZoom(amount) {
this._scrollDelta += amount * this.zoomSensitivity;
}
/**
* @private
*/
_handleZoomPosition(event) {
this._screenSpaceScrollPosition.x = event.endPosition.x;
this._screenSpaceScrollPosition.y = event.endPosition.y;
}
/**
* @private
*/
_handleStartDrag(event) {
if (!this.enabled) {
return;
}
this._screenSpaceDragPosition.x = event.position.x;
this._screenSpaceDragPosition.y = event.position.y;
this._dragDelta.x = 0.0;
this._dragDelta.y = 0.0;
}
/**
* @private
*/
_handleDrag(event) {
this._dragDelta.x += event.endPosition.x - event.startPosition.x;
this._dragDelta.y += event.endPosition.y - event.startPosition.y;
}
}
export default ScreenSpaceZoomCameraController;
@@ -0,0 +1,84 @@
import Cartesian3 from "../../Core/Cartesian3.js";
import Cartesian2 from "../../Core/Cartesian2.js";
import Check from "../../Core/Check.js";
import defined from "../../Core/defined.js";
import IntersectionTests from "../../Core/IntersectionTests.js";
import Plane from "../../Core/Plane.js";
import Ray from "../../Core/Ray.js";
const scratchSurfaceCartesian = new Cartesian3();
const scratchPlane = new Plane(Cartesian3.UNIT_X, 0.0);
const scratchRay = new Ray();
const defaultTargetPixelSize = new Cartesian2(1.0, 1.0, 1.0);
/**
* Picks a cartesian worldspace position based on the specified window coordinates and the camera's current position and orientation.
* <ol>
* <li>If the camera is above the scene's defined ellipsoid, the position is picked on the ellipsoid.</li>
* <li> If the camera is below the ellipsoid, a temporary plane is created relative to the camera's position and orientation, and the position is picked on that plane.</li>
* </ol>
* @param {Scene} scene The scene to pick the world position from.
* @param {Cartesian2} windowPosition The x and y coordinates of a pixel.
* @param {Cartesian3} result The object onto which to store the result.
* @param {Cartesian2} targetPixelSize The pixel size at the target position, used to preserve relative camera distance from the target position when navigating.
* @returns {Cartesian3|undefined} The picked cartesian worldspace position, or <code>undefined</code> if no position could be picked.
* @see {@link ScreenSpaceMapCameraController#pickWorldPosition}
* @see {@link ScreenSpaceElevatorCameraController#pickWorldPosition}
* @see {@link ScreenSpaceTiltOrbitCameraController#pickWorldPosition}
* @see {@link ScreenSpaceZoomCameraController#pickWorldPosition}
*/
export default function (
scene,
windowPosition,
result,
targetPixelSize = defaultTargetPixelSize,
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("scene", scene);
Check.typeOf.object("windowPosition", windowPosition);
Check.typeOf.object("result", result);
//>>includeEnd('debug');
const { camera, ellipsoid } = scene;
const surface = ellipsoid.scaleToGeodeticSurface(
camera.positionWC,
scratchSurfaceCartesian,
);
// Camera is at the origin
if (!defined(surface)) {
return undefined;
}
const cameraMagnitude = Cartesian3.magnitude(camera.positionWC);
const surfaceMagnitude = Cartesian3.magnitude(surface);
const belowEllipsoid = cameraMagnitude <= surfaceMagnitude;
const normal = ellipsoid.geodeticSurfaceNormal(
camera.positionWC,
scratchSurfaceCartesian,
);
const dot = Cartesian3.dot(normal, camera.directionWC);
const lookingUp = dot > 0.0;
if (belowEllipsoid || lookingUp) {
// Camera is inside the ellipsoid. When underground, create a temporary plane beneath the ellipsoid surface to avoid picking a position on the inside and opposite side of the ellipsoid.
const plane = Plane.fromPointNormal(
camera.positionWC,
normal,
scratchPlane,
);
const { clientHeight } = scene.canvas;
const focusDistance =
(targetPixelSize.y * clientHeight) /
(2.0 * Math.tan(camera.frustum.fovy * 0.5));
plane.distance -= dot * focusDistance;
const ray = camera.getPickRay(windowPosition, scratchRay);
return IntersectionTests.rayPlane(ray, plane, result);
}
return camera.pickEllipsoid(windowPosition, ellipsoid, result);
}
+654
View File
@@ -0,0 +1,654 @@
import AssociativeArray from "../Core/AssociativeArray.js";
import buildModuleUrl from "../Core/buildModuleUrl.js";
import Check from "../Core/Check.js";
import Credit from "../Core/Credit.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import Uri from "urijs";
const mobileWidth = 576;
const lightboxHeight = 100;
const textColor = "#ffffff";
const highlightColor = "#48b";
/**
* Used to sort the credits by frequency of appearance
* when they are later displayed.
*
* @alias CreditDisplay.CreditDisplayElement
* @constructor
*
* @private
*/
function CreditDisplayElement(credit, count) {
this.credit = credit;
this.count = count ?? 1;
}
function contains(credits, credit) {
const len = credits.length;
for (let i = 0; i < len; i++) {
const existingCredit = credits[i];
if (Credit.equals(existingCredit, credit)) {
return true;
}
}
return false;
}
function swapCesiumCredit(creditDisplay) {
// We don't want to clutter the screen with the Cesium logo and the Cesium ion
// logo at the same time. Since the ion logo is required, we just replace the
// Cesium logo or add the logo if the Cesium one was removed.
const previousCredit = creditDisplay._previousCesiumCredit;
const currentCredit = creditDisplay._currentCesiumCredit;
if (Credit.equals(currentCredit, previousCredit)) {
return;
}
if (defined(previousCredit)) {
creditDisplay._cesiumCreditContainer.removeChild(previousCredit.element);
}
if (defined(currentCredit)) {
creditDisplay._cesiumCreditContainer.appendChild(currentCredit.element);
}
creditDisplay._previousCesiumCredit = currentCredit;
}
const delimiterClassName = "cesium-credit-delimiter";
function createDelimiterElement(delimiter) {
const delimiterElement = document.createElement("span");
delimiterElement.textContent = delimiter;
delimiterElement.className = delimiterClassName;
return delimiterElement;
}
function createCreditElement(element, elementWrapperTagName) {
// may need to wrap the credit in another element
if (defined(elementWrapperTagName)) {
const wrapper = document.createElement(elementWrapperTagName);
wrapper._creditId = element._creditId;
wrapper.appendChild(element);
element = wrapper;
}
return element;
}
function displayCredits(container, credits, delimiter, elementWrapperTagName) {
const childNodes = container.childNodes;
let domIndex = -1;
// Sort the credits such that more frequent credits appear first
credits.sort(function (credit1, credit2) {
return credit2.count - credit1.count;
});
for (let creditIndex = 0; creditIndex < credits.length; ++creditIndex) {
const credit = credits[creditIndex].credit;
if (defined(credit)) {
domIndex = creditIndex;
if (defined(delimiter)) {
// credits may be separated by delimiters
domIndex *= 2;
if (creditIndex > 0) {
const delimiterDomIndex = domIndex - 1;
if (childNodes.length <= delimiterDomIndex) {
container.appendChild(createDelimiterElement(delimiter));
} else {
const existingDelimiter = childNodes[delimiterDomIndex];
if (existingDelimiter.className !== delimiterClassName) {
container.replaceChild(
createDelimiterElement(delimiter),
existingDelimiter,
);
}
}
}
}
const element = credit.element;
// check to see if the correct credit is in the right place
if (childNodes.length <= domIndex) {
container.appendChild(
createCreditElement(element, elementWrapperTagName),
);
} else {
const existingElement = childNodes[domIndex];
if (existingElement._creditId !== credit._id) {
// not the right credit, swap it in
container.replaceChild(
createCreditElement(element, elementWrapperTagName),
existingElement,
);
}
}
}
}
// any remaining nodes in the container are unnecessary
++domIndex;
while (domIndex < childNodes.length) {
container.removeChild(childNodes[domIndex]);
}
}
function styleLightboxContainer(that) {
const lightboxCredits = that._lightboxCredits;
const width = that.viewport.clientWidth;
const height = that.viewport.clientHeight;
if (width !== that._lastViewportWidth) {
if (width < mobileWidth) {
lightboxCredits.className =
"cesium-credit-lightbox cesium-credit-lightbox-mobile";
lightboxCredits.style.marginTop = "0";
} else {
lightboxCredits.className =
"cesium-credit-lightbox cesium-credit-lightbox-expanded";
lightboxCredits.style.marginTop = `${Math.floor(
(height - lightboxCredits.clientHeight) * 0.5,
)}px`;
}
that._lastViewportWidth = width;
}
if (width >= mobileWidth && height !== that._lastViewportHeight) {
lightboxCredits.style.marginTop = `${Math.floor(
(height - lightboxCredits.clientHeight) * 0.5,
)}px`;
that._lastViewportHeight = height;
}
}
function appendCss(container) {
const style = /*css*/ `
.cesium-credit-lightbox-overlay {
display: none;
z-index: 1;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(80, 80, 80, 0.8);
}
.cesium-credit-lightbox {
background-color: #303336;
color: ${textColor};
position: relative;
min-height: ${lightboxHeight}px;
margin: auto;
}
.cesium-credit-lightbox > ul > li a,
.cesium-credit-lightbox > ul > li a:visited,
.cesium-credit-wrapper a,
.cesium-credit-wrapper a:visited {
color: ${textColor};
}
.cesium-credit-lightbox > ul > li a:hover {
color: ${highlightColor};
}
.cesium-credit-lightbox.cesium-credit-lightbox-expanded {
border: 1px solid #444;
border-radius: 5px;
max-width: 470px;
}
.cesium-credit-lightbox.cesium-credit-lightbox-mobile {
height: 100%;
width: 100%;
}
.cesium-credit-lightbox-title {
padding: 20px 20px 0 20px;
}
.cesium-credit-lightbox-close {
font-size: 18pt;
cursor: pointer;
position: absolute;
top: 0;
right: 6px;
color: ${textColor};
}
.cesium-credit-lightbox-close:hover {
color: ${highlightColor};
}
.cesium-credit-lightbox > ul {
margin: 0;
padding: 12px 20px 12px 40px;
font-size: 13px;
}
.cesium-credit-lightbox > ul > li {
padding-bottom: 6px;
}
.cesium-credit-lightbox > ul > li * {
padding: 0;
margin: 0;
}
.cesium-credit-expand-link {
padding-left: 5px;
cursor: pointer;
text-decoration: underline;
color: ${textColor};
}
.cesium-credit-expand-link:hover {
color: ${highlightColor};
}
.cesium-credit-text {
color: ${textColor};
}
.cesium-credit-delimiter {
padding: 0 5px;
}
.cesium-credit-textContainer *,
.cesium-credit-logoContainer * {
display: inline;
}
.cesium-credit-textContainer a:hover {
color: ${highlightColor}
}
.cesium-credit-textContainer .cesium-credit-wrapper:first-of-type {
padding-left: 5px;
}
`;
function getShadowRoot(container) {
if (container.shadowRoot) {
return container.shadowRoot;
}
if (container.getRootNode) {
const root = container.getRootNode();
if (root instanceof ShadowRoot) {
return root;
}
}
return undefined;
}
const shadowRootOrDocumentHead = getShadowRoot(container) ?? document.head;
const styleElem = document.createElement("style");
styleElem.innerHTML = style;
shadowRootOrDocumentHead.appendChild(styleElem);
}
/**
* The credit display is responsible for displaying credits on screen.
*
* @param {HTMLElement} container The HTML element where credits will be displayed
* @param {string} [delimiter= '•'] The string to separate text credits
* @param {HTMLElement} [viewport=document.body] The HTML element that will contain the credits popup
*
* @alias CreditDisplay
* @constructor
*
* @example
* // Add a credit with a tooltip, image and link to display onscreen
* const credit = new Cesium.Credit(`<a href="https://cesium.com/" target="_blank"><img src="/images/cesium_logo.png" title="Cesium"/></a>`, true);
* viewer.creditDisplay.addStaticCredit(credit);
*
* @example
* // Add a credit with a plaintext link to display in the lightbox
* const credit = new Cesium.Credit('<a href="https://cesium.com/" target="_blank">Cesium</a>');
* viewer.creditDisplay.addStaticCredit(credit);
*/
function CreditDisplay(container, delimiter, viewport) {
//>>includeStart('debug', pragmas.debug);
Check.defined("container", container);
//>>includeEnd('debug');
const that = this;
viewport = viewport ?? document.body;
const lightbox = document.createElement("div");
lightbox.className = "cesium-credit-lightbox-overlay";
viewport.appendChild(lightbox);
const lightboxCredits = document.createElement("div");
lightboxCredits.className = "cesium-credit-lightbox";
lightbox.appendChild(lightboxCredits);
function hideLightbox(event) {
if (lightboxCredits.contains(event.target)) {
return;
}
that.hideLightbox();
}
lightbox.addEventListener("click", hideLightbox, false);
const title = document.createElement("div");
title.className = "cesium-credit-lightbox-title";
title.textContent = "Data provided by:";
lightboxCredits.appendChild(title);
const closeButton = document.createElement("a");
closeButton.onclick = this.hideLightbox.bind(this);
closeButton.innerHTML = "&times;";
closeButton.className = "cesium-credit-lightbox-close";
lightboxCredits.appendChild(closeButton);
const creditList = document.createElement("ul");
lightboxCredits.appendChild(creditList);
const cesiumCreditContainer = document.createElement("div");
cesiumCreditContainer.className = "cesium-credit-logoContainer";
cesiumCreditContainer.style.display = "inline";
container.appendChild(cesiumCreditContainer);
const screenContainer = document.createElement("div");
screenContainer.className = "cesium-credit-textContainer";
screenContainer.style.display = "inline";
container.appendChild(screenContainer);
const expandLink = document.createElement("a");
expandLink.className = "cesium-credit-expand-link";
expandLink.onclick = this.showLightbox.bind(this);
expandLink.textContent = "Data attribution";
container.appendChild(expandLink);
appendCss(container);
const cesiumCredit = Credit.clone(CreditDisplay.cesiumCredit);
this._delimiter = delimiter ?? "•";
this._screenContainer = screenContainer;
this._cesiumCreditContainer = cesiumCreditContainer;
this._lastViewportHeight = undefined;
this._lastViewportWidth = undefined;
this._lightboxCredits = lightboxCredits;
this._creditList = creditList;
this._lightbox = lightbox;
this._hideLightbox = hideLightbox;
this._expandLink = expandLink;
this._expanded = false;
this._staticCredits = [];
this._cesiumCredit = cesiumCredit;
this._previousCesiumCredit = undefined;
this._currentCesiumCredit = cesiumCredit;
this._creditDisplayElementPool = [];
this._creditDisplayElementIndex = 0;
this._currentFrameCredits = {
screenCredits: new AssociativeArray(),
lightboxCredits: new AssociativeArray(),
};
this._defaultCredit = undefined;
this.viewport = viewport;
/**
* The HTML element where credits will be displayed.
* @type {HTMLElement}
*/
this.container = container;
}
function setCredit(creditDisplay, credits, credit, count) {
count = count ?? 1;
let creditDisplayElement = credits.get(credit.id);
if (!defined(creditDisplayElement)) {
const pool = creditDisplay._creditDisplayElementPool;
const poolIndex = creditDisplay._creditDisplayElementPoolIndex;
if (poolIndex < pool.length) {
creditDisplayElement = pool[poolIndex];
creditDisplayElement.credit = credit;
creditDisplayElement.count = count;
} else {
creditDisplayElement = new CreditDisplayElement(credit, count);
pool.push(creditDisplayElement);
}
++creditDisplay._creditDisplayElementPoolIndex;
credits.set(credit.id, creditDisplayElement);
} else if (creditDisplayElement.count < Number.MAX_VALUE) {
creditDisplayElement.count += count;
}
}
/**
* Adds a {@link Credit} that will show on screen or in the lightbox until
* the next frame. This is mostly for internal use. Use {@link CreditDisplay.addStaticCredit} to add a persistent credit to the screen.
*
* @see CreditDisplay.addStaticCredit
*
* @param {Credit} credit The credit to display in the next frame.
*/
CreditDisplay.prototype.addCreditToNextFrame = function (credit) {
//>>includeStart('debug', pragmas.debug);
Check.defined("credit", credit);
//>>includeEnd('debug');
if (credit.isIon()) {
// If this is the an ion logo credit from the ion server
// Just use the default credit (which is identical) to avoid blinking
if (!defined(this._defaultCredit)) {
this._defaultCredit = Credit.clone(getDefaultCredit());
}
this._currentCesiumCredit = this._defaultCredit;
return;
}
let credits;
if (!credit.showOnScreen) {
credits = this._currentFrameCredits.lightboxCredits;
} else {
credits = this._currentFrameCredits.screenCredits;
}
setCredit(this, credits, credit);
};
/**
* Adds a {@link Credit} that will show on screen or in the lightbox until removed with {@link CreditDisplay.removeStaticCredit}.
*
* @param {Credit} credit The credit to added
*
* @example
* // Add a credit with a tooltip, image and link to display onscreen
* const credit = new Cesium.Credit(`<a href="https://cesium.com/" target="_blank"><img src="/images/cesium_logo.png" title="Cesium"/></a>`, true);
* viewer.creditDisplay.addStaticCredit(credit);
*
* @example
* // Add a credit with a plaintext link to display in the lightbox
* const credit = new Cesium.Credit('<a href="https://cesium.com/" target="_blank">Cesium</a>');
* viewer.creditDisplay.addStaticCredit(credit);
*/
CreditDisplay.prototype.addStaticCredit = function (credit) {
//>>includeStart('debug', pragmas.debug);
Check.defined("credit", credit);
//>>includeEnd('debug');
const staticCredits = this._staticCredits;
if (!contains(staticCredits, credit)) {
staticCredits.push(credit);
}
};
/**
* Removes a static credit shown on screen or in the lightbox.
*
* @param {Credit} credit The credit to be removed.
*/
CreditDisplay.prototype.removeStaticCredit = function (credit) {
//>>includeStart('debug', pragmas.debug);
Check.defined("credit", credit);
//>>includeEnd('debug');
const staticCredits = this._staticCredits;
const index = staticCredits.indexOf(credit);
if (index !== -1) {
staticCredits.splice(index, 1);
}
};
/**
* @private
*/
CreditDisplay.prototype.showLightbox = function () {
this._lightbox.style.display = "block";
this._expanded = true;
};
/**
* @private
*/
CreditDisplay.prototype.hideLightbox = function () {
this._lightbox.style.display = "none";
this._expanded = false;
};
/**
* Updates the credit display before a new frame is rendered.
*/
CreditDisplay.prototype.update = function () {
if (this._expanded) {
styleLightboxContainer(this);
}
};
/**
* Resets the credit display to a beginning of frame state, clearing out current credits.
*/
CreditDisplay.prototype.beginFrame = function () {
const currentFrameCredits = this._currentFrameCredits;
this._creditDisplayElementPoolIndex = 0;
const screenCredits = currentFrameCredits.screenCredits;
const lightboxCredits = currentFrameCredits.lightboxCredits;
screenCredits.removeAll();
lightboxCredits.removeAll();
const staticCredits = this._staticCredits;
for (let i = 0; i < staticCredits.length; ++i) {
const staticCredit = staticCredits[i];
const creditCollection = staticCredit.showOnScreen
? screenCredits
: lightboxCredits;
if (
staticCredit.isIon() &&
Credit.equals(CreditDisplay.cesiumCredit, this._cesiumCredit)
) {
// If this is an ion logo credit from the ion server,
// make sure to de-duplicate with the default ion credit
continue;
}
setCredit(this, creditCollection, staticCredit, Number.MAX_VALUE);
}
if (!Credit.equals(CreditDisplay.cesiumCredit, this._cesiumCredit)) {
this._cesiumCredit = Credit.clone(CreditDisplay.cesiumCredit);
}
this._currentCesiumCredit = this._cesiumCredit;
};
/**
* Sets the credit display to the end of frame state, displaying credits from the last frame in the credit container.
*/
CreditDisplay.prototype.endFrame = function () {
const screenCredits = this._currentFrameCredits.screenCredits.values;
displayCredits(
this._screenContainer,
screenCredits,
this._delimiter,
undefined,
);
const lightboxCredits = this._currentFrameCredits.lightboxCredits.values;
this._expandLink.style.display =
lightboxCredits.length > 0 ? "inline" : "none";
displayCredits(this._creditList, lightboxCredits, undefined, "li");
swapCesiumCredit(this);
};
/**
* Destroys the resources held by this object. Destroying an object allows for deterministic
* release of resources, instead of relying on the garbage collector to destroy this object.
* <br /><br />
* Once an object is destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
* assign the return value (<code>undefined</code>) to the object as done in the example.
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*/
CreditDisplay.prototype.destroy = function () {
this._lightbox.removeEventListener("click", this._hideLightbox, false);
this.container.removeChild(this._cesiumCreditContainer);
this.container.removeChild(this._screenContainer);
this.container.removeChild(this._expandLink);
this.viewport.removeChild(this._lightbox);
return destroyObject(this);
};
/**
* Returns true if this object was destroyed; otherwise, false.
* <br /><br />
*
* @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
*/
CreditDisplay.prototype.isDestroyed = function () {
return false;
};
CreditDisplay._cesiumCredit = undefined;
CreditDisplay._cesiumCreditInitialized = false;
let defaultCredit;
function getDefaultCredit() {
if (!defined(defaultCredit)) {
let logo = buildModuleUrl("Assets/Images/ion-credit.png");
// When hosting in a WebView, the base URL scheme is file:// or ms-appx-web://
// which is stripped out from the Credit's <img> tag; use the full path instead
if (
logo.indexOf("http://") !== 0 &&
logo.indexOf("https://") !== 0 &&
logo.indexOf("data:") !== 0
) {
const logoUrl = new Uri(logo);
logo = logoUrl.path();
}
defaultCredit = new Credit(
`<a href="https://cesium.com/" target="_blank"><img src="${logo}" style="vertical-align: -7px" title="Cesium ion"/></a>`,
true,
);
}
if (!CreditDisplay._cesiumCreditInitialized) {
CreditDisplay._cesiumCredit = defaultCredit;
CreditDisplay._cesiumCreditInitialized = true;
}
return defaultCredit;
}
Object.defineProperties(CreditDisplay, {
/**
* Gets or sets the Cesium logo credit.
* @memberof CreditDisplay
* @type {Credit}
*/
cesiumCredit: {
get: function () {
getDefaultCredit();
return CreditDisplay._cesiumCredit;
},
set: function (value) {
CreditDisplay._cesiumCredit = value;
CreditDisplay._cesiumCreditInitialized = true;
},
},
});
CreditDisplay.CreditDisplayElement = CreditDisplayElement;
export default CreditDisplay;
+351
View File
@@ -0,0 +1,351 @@
import BoxGeometry from "../Core/BoxGeometry.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Check from "../Core/Check.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
import GeometryPipeline from "../Core/GeometryPipeline.js";
import Matrix4 from "../Core/Matrix4.js";
import VertexFormat from "../Core/VertexFormat.js";
import BufferUsage from "../Renderer/BufferUsage.js";
import CubeMap from "../Renderer/CubeMap.js";
import DrawCommand from "../Renderer/DrawCommand.js";
import loadCubeMap from "../Renderer/loadCubeMap.js";
import RenderState from "../Renderer/RenderState.js";
import ShaderProgram from "../Renderer/ShaderProgram.js";
import ShaderSource from "../Renderer/ShaderSource.js";
import VertexArray from "../Renderer/VertexArray.js";
import SkyBoxFS from "../Shaders/SkyBoxFS.js";
import SkyBoxVS from "../Shaders/SkyBoxVS.js";
import CubeMapPanoramaVS from "../Shaders/CubeMapPanoramaVS.js";
import BlendingState from "./BlendingState.js";
import SceneMode from "./SceneMode.js";
import Pass from "../Renderer/Pass.js";
import Credit from "../Core/Credit.js";
/**
* @typedef {object} CubeMapPanorama.ConstructorOptions
*
* Initialization options for the CubeMapPanorama constructor
*
* @property {object} [options.sources] The source URL or <code>Image</code> object for each of the six cube map faces. See the example below.
* @property {Matrix3} [options.transform] A 3x3 transformation matrix that defines the panoramas orientation. If not specified, the default orientation is defined using the True Equator Mean Equinox (TEME) axes.
* @property {boolean} [options.show=true] Determines if this primitive will be shown.
* @property {Credit|string} [options.credit] A credit for the panorama, which is displayed on the canvas.
*
*/
/**
* A {@link Panorama} that displays imagery in cube map format in a scene.
* <p>
* This is only supported in 3D. The cube map panorama is faded out when morphing to 2D or Columbus view. The size of
* the cube map panorama must not exceed {@link Scene#maximumSkyBoxSize}.
* </p>
*
* @alias CubeMapPanorama
* @constructor
*
* @param {CubeMapPanorama.ConstructorOptions} options Object describing initialization options
*
* @example
* const modelMatrix = Cesium.Matrix4.getMatrix3(
* Cesium.Transforms.localFrameToFixedFrameGenerator("north", "down")(
* Cesium.Cartesian3.fromDegrees(longitude, latitude, height),
* Cesium.Ellipsoid.default
* ),
* new Cesium.Matrix3()
* );
*
*
* scene.primitives.add(new Cesium.CubeMapPanorama({
* sources : {
* positiveX : 'cubemap_px.png',
* negativeX : 'cubemap_nx.png',
* positiveY : 'cubemap_py.png',
* negativeY : 'cubemap_ny.png',
* positiveZ : 'cubemap_pz.png',
* negativeZ : 'cubemap_nz.png'
* }
* transform: modelMatrix,
* }));
*
* @see SkyBox
*
* @demo {@link https://sandcastle.cesium.com/index.html?id=panorama|Cesium Sandcastle Panorama}
*/
function CubeMapPanorama(options) {
/**
* The sources used to create the cube map faces: an object
* with <code>positiveX</code>, <code>negativeX</code>, <code>positiveY</code>,
* <code>negativeY</code>, <code>positiveZ</code>, and <code>negativeZ</code> properties.
* These can be either URLs or <code>Image</code> objects.
*
* @type {object}
* @default undefined
*/
this.sources = options.sources;
this._sources = undefined;
this._transform = options.transform;
/**
* Determines if the cube map panorama will be shown.
*
* @type {boolean}
* @default true
*/
this.show = options.show ?? true;
this._returnCommand = options.returnCommand ?? false;
this._addToPanoramaCommandList = !this._returnCommand;
this._command = new DrawCommand({
modelMatrix: Matrix4.clone(Matrix4.IDENTITY),
owner: this,
// render before everything else
pass: Pass.ENVIRONMENT,
});
this._cubeMap = undefined;
this._attributeLocations = undefined;
this._useHdr = undefined;
this._hasError = false;
this._error = undefined;
// Credit specified by the user.
let credit = options.credit;
if (typeof credit === "string") {
credit = new Credit(credit);
}
this._credit = credit;
}
Object.defineProperties(CubeMapPanorama.prototype, {
/**
* Gets the transform of the panorama. If undefined, the default orientation uses the True Equator Mean Equinox (TEME) axes.
* @memberof CubeMapPanorama.prototype
* @type {Matrix3}
* @readonly
*/
transform: {
get: function () {
return this._transform;
},
},
/**
* Gets the credits of the panorama.
* @memberof CubeMapPanorama.prototype
* @type {Credit}
* @readonly
*/
credit: {
get: function () {
return defined(this._credit) ? this._credit : undefined;
},
},
});
/**
* Called when {@link Viewer} or {@link CesiumWidget} render the scene to
* get the draw commands needed to render this primitive.
* <p>
* Do not call this function directly. This is documented just to
* list the exceptions that may be propagated when the scene is rendered:
* </p>
*
* @exception {DeveloperError} this.sources is required and must have positiveX, negativeX, positiveY, negativeY, positiveZ, and negativeZ properties.
* @exception {DeveloperError} this.sources properties must all be the same type.
*/
CubeMapPanorama.prototype.update = function (frameState, useHdr) {
const that = this;
const { mode, passes, context, panoramaCommandList } = frameState;
if (!this.show) {
return undefined;
}
if (mode !== SceneMode.SCENE3D && mode !== SceneMode.MORPHING) {
return undefined;
}
// The cube map panorama is only rendered during the render pass; it is not pickable, it doesn't cast shadows, etc.
if (!passes.render) {
return undefined;
}
// Throw any errors that had previously occurred asynchronously so they aren't
// ignored when running. See https://github.com/CesiumGS/cesium/pull/12307
if (this._hasError) {
const error = this._error;
this._hasError = false;
this._error = undefined;
throw error;
}
if (this._sources !== this.sources) {
this._sources = this.sources;
const sources = this.sources;
//>>includeStart('debug', pragmas.debug);
Check.defined("this.sources", sources);
if (
Object.values(CubeMap.FaceName).some(
(faceName) => !defined(sources[faceName]),
)
) {
throw new DeveloperError(
"this.sources must have positiveX, negativeX, positiveY, negativeY, positiveZ, and negativeZ properties.",
);
}
const sourceType = typeof sources.positiveX;
if (
Object.values(CubeMap.FaceName).some(
(faceName) => typeof sources[faceName] !== sourceType,
)
) {
throw new DeveloperError(
"this.sources properties must all be the same type.",
);
}
//>>includeEnd('debug');
if (typeof sources.positiveX === "string") {
// Given urls for cube-map images. Load them.
loadCubeMap(context, this._sources)
.then(function (cubeMap) {
that._cubeMap = that._cubeMap && that._cubeMap.destroy();
that._cubeMap = cubeMap;
})
.catch((error) => {
// Defer throwing the error until the next call to update to prevent
// test from failing in `afterAll` if this is rejected after the test
// using the Skybox ends. See https://github.com/CesiumGS/cesium/pull/12307
this._hasError = true;
this._error = error;
});
} else {
this._cubeMap = this._cubeMap && this._cubeMap.destroy();
this._cubeMap = new CubeMap({
context: context,
source: sources,
});
}
this._addToPanoramaCommandList = true;
}
const command = this._command;
if (!defined(command.vertexArray)) {
command.uniformMap = {
u_cubeMap: function () {
return that._cubeMap;
},
u_cubeMapPanoramaTransform: function () {
return that._transform;
},
};
const geometry = BoxGeometry.createGeometry(
BoxGeometry.fromDimensions({
dimensions: new Cartesian3(2.0, 2.0, 2.0),
vertexFormat: VertexFormat.POSITION_ONLY,
}),
);
const attributeLocations = (this._attributeLocations =
GeometryPipeline.createAttributeLocations(geometry));
command.vertexArray = VertexArray.fromGeometry({
context: context,
geometry: geometry,
attributeLocations: attributeLocations,
bufferUsage: BufferUsage.STATIC_DRAW,
});
// no depth test/write
command.renderState = RenderState.fromCache({
depthTest: { enabled: false },
depthMask: false,
blending: BlendingState.ALPHA_BLEND,
});
this._addToPanoramaCommandList = true;
}
if (!defined(command.shaderProgram) || this._useHdr !== useHdr) {
const fs = new ShaderSource({
defines: [useHdr ? "HDR" : ""],
sources: [SkyBoxFS],
});
command.shaderProgram = ShaderProgram.fromCache({
context: context,
//vertexShaderSource: SkyBoxVS,
vertexShaderSource: defined(this._transform)
? CubeMapPanoramaVS
: SkyBoxVS,
fragmentShaderSource: fs,
attributeLocations: this._attributeLocations,
});
this._useHdr = useHdr;
this._addToPanoramaCommandList = true;
}
if (!defined(this._cubeMap)) {
return undefined;
}
if (this.show && defined(this._credit) && !this._returnCommand) {
const creditDisplay = frameState.creditDisplay;
creditDisplay.addCreditToNextFrame(this._credit);
}
if (this._returnCommand) {
return command;
}
if (this._addToPanoramaCommandList) {
panoramaCommandList.push(command);
this._addToPanoramaCommandList = false;
}
};
/**
* Returns true if this object was destroyed; otherwise, false.
* <br /><br />
* If this object was destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
*
* @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
*
* @see CubeMapPanorama#destroy
*/
CubeMapPanorama.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.
* <br /><br />
* Once an object is destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
* assign the return value (<code>undefined</code>) to the object as done in the example.
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
*
* @example
* cubeMapPanorama = cubeMapPanorama && cubeMapPanorama.destroy();
*
* @see CubeMapPanorama#isDestroyed
*/
CubeMapPanorama.prototype.destroy = function () {
const command = this._command;
command.vertexArray = command.vertexArray && command.vertexArray.destroy();
command.shaderProgram =
command.shaderProgram && command.shaderProgram.destroy();
this._cubeMap = this._cubeMap && this._cubeMap.destroy();
return destroyObject(this);
};
export default CubeMapPanorama;
+38
View File
@@ -0,0 +1,38 @@
// @ts-check
import WebGLConstants from "../Core/WebGLConstants.js";
/**
* Determines which triangles, if any, are culled.
*
* @enum {number}
*/
const CullFace = {
/**
* Front-facing triangles are culled.
*
* @type {number}
* @constant
*/
FRONT: WebGLConstants.FRONT,
/**
* Back-facing triangles are culled.
*
* @type {number}
* @constant
*/
BACK: WebGLConstants.BACK,
/**
* Both front-facing and back-facing triangles are culled.
*
* @type {number}
* @constant
*/
FRONT_AND_BACK: WebGLConstants.FRONT_AND_BACK,
};
Object.freeze(CullFace);
export default CullFace;

Some files were not shown because too many files have changed in this diff Show More