@cesium/engine
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
1,060 lines (967 loc) • 30.3 kB
JavaScript
// @ts-check
import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian3 from "../Core/Cartesian3.js";
import ComponentDatatype from "../Core/ComponentDatatype.js";
import PolygonPipeline from "../Core/PolygonPipeline.js";
import PrimitiveType from "../Core/PrimitiveType.js";
import Rectangle from "../Core/Rectangle.js";
import WebGLConstants from "../Core/WebGLConstants.js";
import WebMercatorTilingScheme from "../Core/WebMercatorTilingScheme.js";
import defined from "../Core/defined.js";
import oneTimeWarning from "../Core/oneTimeWarning.js";
import MetadataType from "./MetadataType.js";
/** @import { TypedArray } from "../Core/globalTypes.js"; */
/** @ignore */
const DEFAULT_HEIGHT = 0;
const scratchWorld = new Cartesian3();
const scratchLocal = new Cartesian3();
const tilingScheme = new WebMercatorTilingScheme();
/**
* @typedef {object} VectorTilePoint
* @property {number} x Tile-local x (0–extent)
* @property {number} y Tile-local y (0–extent)
* @ignore
*/
/**
* @typedef {object} VectorTileFeature
* @property {"Point"|"LineString"|"Polygon"|"Unknown"} type
* @property {Array<VectorTilePoint>|Array<Array<VectorTilePoint>>} geometry
* @property {object} [properties]
* @ignore
*/
/**
* @typedef {object} VectorTileLayer
* @property {string} [name]
* @property {number} extent
* @property {VectorTileFeature[]} features
* @ignore
*/
/**
* @typedef {object} DecodedVectorTile
* @property {VectorTileLayer[]} layers
* @ignore
*/
/**
* @typedef {object} PolygonRingGroup
* @property {Array.<VectorTilePoint>} outerRing
* @property {Array.<Array.<VectorTilePoint>>} holes
* @ignore
*/
/**
* @typedef {object} BuildVectorGltfOptions
* @property {string} [featureIdProperty] MVT property name to use as feature ID.
* @ignore
*/
/**
* Build a vector glTF payload from decoded tile-local vector geometry.
* Each MVT layer produces a separate glTF node (named after the layer),
* enabling layer-specific styling in the future.
*
* @param {DecodedVectorTile} decoded
* @param {{tileX:number, tileY:number, tileZ:number}} tileCoordinates
* @param {BuildVectorGltfOptions} [options]
* @returns {Uint8Array|undefined}
*
* @ignore
*/
function buildVectorGltfFromMVT(decoded, tileCoordinates, options) {
const tileX = tileCoordinates.tileX;
const tileY = tileCoordinates.tileY;
const tileZ = tileCoordinates.tileZ;
const featureIdProperty = options?.featureIdProperty;
const tileRect = tilingScheme.tileXYToRectangle(tileX, tileY, tileZ);
const tileCenter = Rectangle.center(tileRect);
const origin = Cartesian3.fromRadians(
tileCenter.longitude,
tileCenter.latitude,
0,
);
// Maximum value of a Uint32; used as sentinel for null feature IDs and primitive restart indices.
const MAX_INT_U32 = 0xffffffff;
const nullFeatureId = MAX_INT_U32;
const primitiveRestartIndex = MAX_INT_U32;
// Maps a property value (or auto-increment key) to a compact integer feature ID.
const featureIdLookup = new Map();
// Maps featureId -> properties object (first-seen wins for ID collisions).
/** @type {Map<number, Object.<string, *>>} */
const featureProperties = new Map();
/** @type {object[]} */
const bufferViews = [];
/** @type {object[]} */
const accessors = [];
/** @type {Uint8Array[]} */
const chunks = [];
let byteLength = 0;
function addPadding() {
const padding = (4 - (byteLength % 4)) % 4;
if (padding > 0) {
chunks.push(new Uint8Array(padding));
byteLength += padding;
}
}
/**
* @param {TypedArray} typedArray
* @param {number} target
* @returns {number}
*/
function addBufferView(typedArray, target) {
addPadding();
const byteOffset = byteLength;
chunks.push(
new Uint8Array(
typedArray.buffer,
typedArray.byteOffset,
typedArray.byteLength,
),
);
byteLength += typedArray.byteLength;
const bufferViewIndex = bufferViews.length;
bufferViews.push({
buffer: 0,
byteOffset: byteOffset,
byteLength: typedArray.byteLength,
target: target,
});
return bufferViewIndex;
}
/**
* @param {TypedArray} typedArray
* @param {object} options
* @param {string} options.type
* @param {number} options.componentType
* @param {number} options.target
* @param {number[]} [options.min]
* @param {number[]} [options.max]
* @returns {number}
*/
function addAccessor(typedArray, options) {
const bufferView = addBufferView(typedArray, options.target);
const componentCount = /** @type {*} */ (MetadataType).getComponentCount(
options.type,
);
const accessor = /** @type {*} */ ({
bufferView: bufferView,
byteOffset: 0,
componentType: options.componentType,
count: typedArray.length / componentCount,
type: options.type,
});
if (defined(options.min)) {
accessor.min = options.min;
}
if (defined(options.max)) {
accessor.max = options.max;
}
const accessorIndex = accessors.length;
accessors.push(accessor);
return accessorIndex;
}
/**
* @param {Float32Array} positions
* @returns {{min:number[],max:number[]}}
*/
function computeMinMax(positions) {
let minX = Number.POSITIVE_INFINITY;
let minY = Number.POSITIVE_INFINITY;
let minZ = Number.POSITIVE_INFINITY;
let maxX = Number.NEGATIVE_INFINITY;
let maxY = Number.NEGATIVE_INFINITY;
let maxZ = Number.NEGATIVE_INFINITY;
for (let i = 0; i < positions.length; i += 3) {
const x = positions[i];
const y = positions[i + 1];
const z = positions[i + 2];
if (x < minX) {
minX = x;
}
if (y < minY) {
minY = y;
}
if (z < minZ) {
minZ = z;
}
if (x > maxX) {
maxX = x;
}
if (y > maxY) {
maxY = y;
}
if (z > maxZ) {
maxZ = z;
}
}
return {
min: [minX, minY, minZ],
max: [maxX, maxY, maxZ],
};
}
/**
* @param {*} attributes
* @param {*} extensions
* @param {number[]} featureIdValues
*/
function addFeatureIdsToPrimitive(attributes, extensions, featureIdValues) {
if (featureIdValues.length === 0) {
return;
}
const featureIds = new Uint32Array(featureIdValues);
const featureAccessor = addAccessor(featureIds, {
type: "SCALAR",
componentType: ComponentDatatype.UNSIGNED_INT,
target: WebGLConstants.ARRAY_BUFFER,
});
attributes._FEATURE_ID_0 = featureAccessor;
/** @type {*} */
const featureIdDef = {
featureCount: featureIdLookup.size,
nullFeatureId: nullFeatureId,
attribute: 0,
};
if (featureProperties.size > 0) {
featureIdDef.propertyTable = 0;
}
extensions.EXT_mesh_features = {
featureIds: [featureIdDef],
};
}
/**
* Adds a raw buffer view for metadata (no accessor target).
* @param {Uint8Array} bytes
* @param {number} [alignment=4] Required byte alignment (e.g., 8 for Float64).
* @returns {number} bufferView index
*/
function addMetadataBufferView(bytes, alignment) {
alignment = alignment ?? 4;
// Align to the required boundary.
const pad = (alignment - (byteLength % alignment)) % alignment;
if (pad > 0) {
chunks.push(new Uint8Array(pad));
byteLength += pad;
}
const byteOffset = byteLength;
chunks.push(bytes);
byteLength += bytes.byteLength;
const bufferViewIndex = bufferViews.length;
bufferViews.push({
buffer: 0,
byteOffset: byteOffset,
byteLength: bytes.byteLength,
});
return bufferViewIndex;
}
/**
* Builds the EXT_structural_metadata extension object with schema and
* property table from the collected feature properties.
* @returns {object|undefined}
*/
function buildStructuralMetadata() {
if (featureProperties.size === 0) {
return undefined;
}
// 1. Determine union of all property names and infer types.
// propertyName -> "STRING"|"SCALAR"|"BOOLEAN"
/** @type {Map<string, string>} */
const propertyTypes = new Map();
for (const props of featureProperties.values()) {
for (const [key, value] of Object.entries(props)) {
if (!defined(value)) {
continue;
}
const jsType = typeof value;
let metaType;
if (jsType === "string") {
metaType = "STRING";
} else if (jsType === "number") {
metaType = "SCALAR";
} else if (jsType === "boolean") {
metaType = "BOOLEAN";
} else {
// Objects/arrays: coerce to string
metaType = "STRING";
}
const existing = propertyTypes.get(key);
if (!defined(existing)) {
propertyTypes.set(key, metaType);
} else if (existing !== metaType) {
// Mixed types: coerce to STRING
propertyTypes.set(key, "STRING");
}
}
}
if (propertyTypes.size === 0) {
return undefined;
}
// 2. Build schema class properties.
/** @type {Object.<string, *>} */
const classProperties = {};
for (const [name, type] of propertyTypes) {
if (type === "SCALAR") {
classProperties[name] = {
type: "SCALAR",
componentType: "FLOAT64",
};
} else if (type === "BOOLEAN") {
classProperties[name] = {
type: "BOOLEAN",
};
} else {
classProperties[name] = {
type: "STRING",
};
}
}
// 3. Encode property values into binary buffers.
/** @type {Object.<string, *>} */
const tableProperties = {};
const count = featureIdLookup.size;
for (const [name, type] of propertyTypes) {
if (type === "SCALAR") {
const values = new Float64Array(count);
for (let i = 0; i < count; i++) {
const props = featureProperties.get(i);
const raw = props?.[name];
values[i] =
typeof raw === "number" && Number.isFinite(raw) ? raw : NaN;
}
const bvIndex = addMetadataBufferView(
new Uint8Array(values.buffer, values.byteOffset, values.byteLength),
8,
);
tableProperties[name] = { values: bvIndex };
} else if (type === "BOOLEAN") {
const byteCount = Math.ceil(count / 8);
const values = new Uint8Array(byteCount);
for (let i = 0; i < count; i++) {
const props = featureProperties.get(i);
const raw = props?.[name];
if (raw) {
values[i >> 3] |= 1 << (i & 7);
}
}
const bvIndex = addMetadataBufferView(values);
tableProperties[name] = { values: bvIndex };
} else {
// STRING encoding: values (UTF-8 bytes) + stringOffsets (Uint32)
const encoder = new TextEncoder();
/** @type {Uint8Array[]} */
const stringParts = [];
const offsets = new Uint32Array(count + 1);
let totalBytes = 0;
for (let i = 0; i < count; i++) {
offsets[i] = totalBytes;
const props = featureProperties.get(i);
const raw = props?.[name];
let str;
if (raw === null || raw === undefined) {
str = "";
} else if (typeof raw === "string") {
str = raw;
} else {
str = String(raw);
}
const encoded = encoder.encode(str);
stringParts.push(encoded);
totalBytes += encoded.byteLength;
}
offsets[count] = totalBytes;
// Concatenate string bytes
const valuesBuffer = new Uint8Array(totalBytes);
let writeOffset = 0;
for (const part of stringParts) {
valuesBuffer.set(part, writeOffset);
writeOffset += part.byteLength;
}
const valuesBv = addMetadataBufferView(valuesBuffer);
const offsetsBv = addMetadataBufferView(
new Uint8Array(
offsets.buffer,
offsets.byteOffset,
offsets.byteLength,
),
);
tableProperties[name] = {
values: valuesBv,
stringOffsets: offsetsBv,
stringOffsetType: "UINT32",
};
}
}
return {
schema: {
classes: {
mvt_feature: {
properties: classProperties,
},
},
},
propertyTables: [
{
class: "mvt_feature",
count: count,
properties: tableProperties,
},
],
};
}
/** @type {object[]} */
const meshes = [];
/** @type {object[]} */
const nodes = [];
const translation = [origin.x, origin.y, origin.z];
for (const layer of decoded.layers) {
const extent = layer.extent;
/** @type {number[]} */
const pointPositions = [];
/** @type {number[]} */
const pointFeatureIds = [];
/** @type {number[]} */
const linePositions = [];
/** @type {number[]} */
const lineFeatureIds = [];
/** @type {number[]} */
const lineIndices = [];
let lineCount = 0;
/** @type {number[]} */
const polygonPositions = [];
/** @type {number[]} */
const polygonFeatureIds = [];
/** @type {number[]} */
const polygonIndices = [];
/** @type {number[]} */
const polygonAttributeOffsets = [];
/** @type {number[]} */
const polygonIndicesOffsets = [];
/** @type {number[]} */
const polygonHoleCounts = [];
/** @type {number[]} */
const polygonHoleOffsets = [];
let polygonCount = 0;
for (const feature of layer.features) {
const currentFeatureId = defined(featureIdProperty)
? (mapFeatureIdFromProperty(
feature,
featureIdProperty,
featureIdLookup,
) ?? nullFeatureId)
: getOrAssignAutoFeatureId(feature, featureIdLookup);
// Collect properties for the property table (first-seen wins).
if (
currentFeatureId !== nullFeatureId &&
!featureProperties.has(currentFeatureId)
) {
/** @type {Object.<string, *>} */
const props = Object.assign({}, feature.properties);
props["_layer"] = layer.name ?? "";
featureProperties.set(currentFeatureId, props);
}
if (feature.type === "Point") {
const points = /** @type {VectorTilePoint[]} */ (feature.geometry);
for (const point of points) {
appendTilePointAsLocalPosition(
point,
tileX,
tileY,
tileZ,
extent,
DEFAULT_HEIGHT,
origin,
pointPositions,
);
pointFeatureIds.push(currentFeatureId);
}
continue;
}
if (feature.type === "LineString") {
const lines = /** @type {VectorTilePoint[][]} */ (feature.geometry);
for (const line of lines) {
const lineStart = linePositions.length / 3;
for (const point of line) {
appendTilePointAsLocalPosition(
point,
tileX,
tileY,
tileZ,
extent,
DEFAULT_HEIGHT,
origin,
linePositions,
);
lineFeatureIds.push(currentFeatureId);
}
for (let i = 0; i < line.length; i++) {
lineIndices.push(lineStart + i);
}
lineIndices.push(primitiveRestartIndex);
lineCount++;
}
continue;
}
if (feature.type === "Polygon") {
const rawRings = /** @type {VectorTilePoint[][]} */ (feature.geometry);
const groups = groupPolygonRings(rawRings);
for (const group of groups) {
const rings = [group.outerRing, ...group.holes];
/** @type {Cartesian2[]} */
const positions2D = [];
/** @type {number[]} */
const polygonPositionComponents = [];
/** @type {number[]} */
const holeOffsets = [];
let vertexOffset = 0;
for (let ringIndex = 0; ringIndex < rings.length; ringIndex++) {
const ring = rings[ringIndex];
if (ringIndex > 0) {
holeOffsets.push(vertexOffset);
}
for (const point of ring) {
positions2D.push(new Cartesian2(point.x, point.y));
appendTilePointAsLocalPosition(
point,
tileX,
tileY,
tileZ,
extent,
DEFAULT_HEIGHT,
origin,
polygonPositionComponents,
);
vertexOffset++;
}
}
if (positions2D.length < 3) {
continue;
}
const triangles = PolygonPipeline.triangulate(
positions2D,
holeOffsets.length > 0 ? holeOffsets : undefined,
);
if (!defined(triangles) || triangles.length === 0) {
oneTimeWarning(
"buildVectorGltfFromMVT-triangulation-failed",
"Polygon triangulation failed; skipping polygon.",
);
continue;
}
const globalVertexStart = polygonPositions.length / 3;
const globalIndexStart = polygonIndices.length;
polygonAttributeOffsets.push(globalVertexStart);
polygonIndicesOffsets.push(globalIndexStart);
polygonHoleCounts.push(holeOffsets.length);
for (let i = 0; i < holeOffsets.length; i++) {
polygonHoleOffsets.push(globalVertexStart + holeOffsets[i]);
}
for (let i = 0; i < polygonPositionComponents.length; i++) {
polygonPositions.push(polygonPositionComponents[i]);
}
for (let i = 0; i < polygonPositionComponents.length / 3; i++) {
polygonFeatureIds.push(currentFeatureId);
}
for (let i = 0; i < triangles.length; i++) {
polygonIndices.push(triangles[i] + globalVertexStart);
}
polygonCount++;
}
}
}
// Skip layers with no geometry.
if (
pointPositions.length === 0 &&
linePositions.length === 0 &&
polygonPositions.length === 0
) {
continue;
}
if (
lineIndices.length > 0 &&
lineIndices[lineIndices.length - 1] === primitiveRestartIndex
) {
lineIndices.pop();
}
/** @type {object[]} */
const primitives = [];
if (pointPositions.length > 0) {
const positions = new Float32Array(pointPositions);
const minMax = computeMinMax(positions);
const positionAccessor = addAccessor(positions, {
type: "VEC3",
componentType: ComponentDatatype.FLOAT,
target: WebGLConstants.ARRAY_BUFFER,
min: minMax.min,
max: minMax.max,
});
const attributes = /** @type {*} */ ({
POSITION: positionAccessor,
});
const extensions = /** @type {*} */ ({
CESIUM_mesh_vector: {
vector: true,
count: positions.length / 3,
},
});
addFeatureIdsToPrimitive(attributes, extensions, pointFeatureIds);
primitives.push({
mode: PrimitiveType.POINTS,
attributes: attributes,
extensions: extensions,
});
}
if (linePositions.length > 0 && lineIndices.length > 1) {
const positions = new Float32Array(linePositions);
const indices = new Uint32Array(lineIndices);
const minMax = computeMinMax(positions);
const positionAccessor = addAccessor(positions, {
type: "VEC3",
componentType: ComponentDatatype.FLOAT,
target: WebGLConstants.ARRAY_BUFFER,
min: minMax.min,
max: minMax.max,
});
const indicesAccessor = addAccessor(indices, {
type: "SCALAR",
componentType: ComponentDatatype.UNSIGNED_INT,
target: WebGLConstants.ELEMENT_ARRAY_BUFFER,
});
const attributes = /** @type {*} */ ({
POSITION: positionAccessor,
});
const extensions = /** @type {*} */ ({
CESIUM_mesh_vector: {
vector: true,
count: lineCount,
},
});
addFeatureIdsToPrimitive(attributes, extensions, lineFeatureIds);
primitives.push({
mode: PrimitiveType.LINE_STRIP,
indices: indicesAccessor,
attributes: attributes,
extensions: extensions,
});
}
if (polygonPositions.length > 0 && polygonIndices.length >= 3) {
const positions = new Float32Array(polygonPositions);
const indices = new Uint32Array(polygonIndices);
const attributeOffsets = new Uint32Array(polygonAttributeOffsets);
const indicesOffsets = new Uint32Array(polygonIndicesOffsets);
const hasPolygonHoles = polygonHoleOffsets.length > 0;
const holeCounts = hasPolygonHoles
? new Uint32Array(polygonHoleCounts)
: undefined;
const holeOffsets = hasPolygonHoles
? new Uint32Array(polygonHoleOffsets)
: undefined;
const minMax = computeMinMax(positions);
const positionAccessor = addAccessor(positions, {
type: "VEC3",
componentType: ComponentDatatype.FLOAT,
target: WebGLConstants.ARRAY_BUFFER,
min: minMax.min,
max: minMax.max,
});
const indicesAccessor = addAccessor(indices, {
type: "SCALAR",
componentType: ComponentDatatype.UNSIGNED_INT,
target: WebGLConstants.ELEMENT_ARRAY_BUFFER,
});
const attributeOffsetsAccessor = addAccessor(attributeOffsets, {
type: "SCALAR",
componentType: ComponentDatatype.UNSIGNED_INT,
target: WebGLConstants.ARRAY_BUFFER,
});
const indicesOffsetsAccessor = addAccessor(indicesOffsets, {
type: "SCALAR",
componentType: ComponentDatatype.UNSIGNED_INT,
target: WebGLConstants.ARRAY_BUFFER,
});
const holeCountsAccessor = defined(holeCounts)
? addAccessor(holeCounts, {
type: "SCALAR",
componentType: ComponentDatatype.UNSIGNED_INT,
target: WebGLConstants.ARRAY_BUFFER,
})
: undefined;
const holeOffsetsAccessor = defined(holeOffsets)
? addAccessor(holeOffsets, {
type: "SCALAR",
componentType: ComponentDatatype.UNSIGNED_INT,
target: WebGLConstants.ARRAY_BUFFER,
})
: undefined;
const attributes = /** @type {*} */ ({
POSITION: positionAccessor,
});
const extensions = /** @type {*} */ ({
CESIUM_mesh_vector: {
vector: true,
count: polygonCount,
polygonAttributeOffsets: attributeOffsetsAccessor,
polygonIndicesOffsets: indicesOffsetsAccessor,
},
});
if (hasPolygonHoles) {
extensions.CESIUM_mesh_vector.polygonHoleCounts = holeCountsAccessor;
extensions.CESIUM_mesh_vector.polygonHoleOffsets = holeOffsetsAccessor;
}
addFeatureIdsToPrimitive(attributes, extensions, polygonFeatureIds);
primitives.push({
mode: PrimitiveType.TRIANGLES,
indices: indicesAccessor,
attributes: attributes,
extensions: extensions,
});
}
if (primitives.length === 0) {
continue;
}
const meshIndex = meshes.length;
meshes.push({ primitives: primitives });
nodes.push({
name: layer.name,
mesh: meshIndex,
translation: translation,
});
}
if (nodes.length === 0) {
return undefined;
}
// Build property table AFTER primitives (adds metadata buffer views).
const structuralMetadata = buildStructuralMetadata();
const binaryChunk = concatChunks(chunks, byteLength);
const extensionsUsed = ["CESIUM_mesh_vector"];
if (featureIdLookup.size > 0) {
extensionsUsed.push("EXT_mesh_features");
}
if (defined(structuralMetadata)) {
extensionsUsed.push("EXT_structural_metadata");
}
const nodeIndices = nodes.map((_, i) => i);
const gltfJson = {
asset: {
version: "2.0",
},
extensionsUsed: extensionsUsed,
scene: 0,
scenes: [
{
nodes: nodeIndices,
},
],
nodes: nodes,
meshes: meshes,
accessors: accessors,
bufferViews: bufferViews,
buffers: [
{
byteLength: binaryChunk.byteLength,
},
],
extensions: /** @type {Object|undefined} */ (undefined),
};
if (defined(structuralMetadata)) {
gltfJson.extensions = {
EXT_structural_metadata: structuralMetadata,
};
}
return buildGlb(gltfJson, binaryChunk);
}
/**
* Assigns a stable auto-incrementing integer ID to each unique feature.
*
* @param {VectorTileFeature} feature
* @param {Map<VectorTileFeature, number>} featureIdLookup
* @returns {number}
* @ignore
*/
function getOrAssignAutoFeatureId(feature, featureIdLookup) {
let id = featureIdLookup.get(feature);
if (!defined(id)) {
id = featureIdLookup.size;
featureIdLookup.set(feature, id);
}
return id;
}
/**
* @param {VectorTileFeature} feature
* @param {string} featureIdProperty
* @param {Map<string, number>} featureIdLookup
* @returns {number|undefined}
* @ignore
*/
function mapFeatureIdFromProperty(feature, featureIdProperty, featureIdLookup) {
const properties = feature.properties;
if (!defined(properties)) {
return undefined;
}
const propertyValue = /** @type {*} */ (properties)[featureIdProperty];
if (!defined(propertyValue)) {
return undefined;
}
if (
typeof propertyValue !== "string" &&
typeof propertyValue !== "number" &&
typeof propertyValue !== "boolean"
) {
return undefined;
}
if (typeof propertyValue === "number" && !Number.isFinite(propertyValue)) {
return undefined;
}
const mapKey = `${typeof propertyValue}:${propertyValue}`;
let mappedFeatureId = featureIdLookup.get(mapKey);
if (defined(mappedFeatureId)) {
return mappedFeatureId;
}
mappedFeatureId = featureIdLookup.size;
featureIdLookup.set(mapKey, mappedFeatureId);
return mappedFeatureId;
}
/**
* @param {VectorTilePoint[][]} rawRings
* @returns {Array.<PolygonRingGroup>}
* @ignore
*/
function groupPolygonRings(rawRings) {
/** @type {Array.<PolygonRingGroup>} */
const groups = [];
for (const rawRing of rawRings) {
const ring = stripClosingVertex(rawRing);
if (ring.length < 3) {
continue;
}
const area = ringSignedArea(ring);
if (area <= 0) {
groups.push({ outerRing: ring, holes: [] });
} else if (groups.length > 0) {
groups[groups.length - 1].holes.push(ring);
}
}
return groups;
}
/**
* @param {VectorTilePoint[]} ring
* @returns {number}
* @ignore
*/
function ringSignedArea(ring) {
let area = 0;
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
area += (ring[j].x + ring[i].x) * (ring[j].y - ring[i].y);
}
return area / 2;
}
/**
* @param {VectorTilePoint[]} ring
* @returns {VectorTilePoint[]}
* @ignore
*/
function stripClosingVertex(ring) {
if (
ring.length > 1 &&
ring[0].x === ring[ring.length - 1].x &&
ring[0].y === ring[ring.length - 1].y
) {
return ring.slice(0, ring.length - 1);
}
return ring;
}
/**
* @param {VectorTilePoint} point
* @param {number} tileX
* @param {number} tileY
* @param {number} tileZ
* @param {number} extent
* @param {number} height
* @param {Cartesian3} origin
* @param {number[]} out
* @ignore
*/
function appendTilePointAsLocalPosition(
point,
tileX,
tileY,
tileZ,
extent,
height,
origin,
out,
) {
const n = 1 << tileZ;
const u = (tileX + point.x / extent) / n;
const v = (tileY + point.y / extent) / n;
const lon = u * 2 * Math.PI - Math.PI;
const lat = Math.atan(Math.sinh(Math.PI * (1 - 2 * v)));
Cartesian3.fromRadians(lon, lat, height, undefined, scratchWorld);
Cartesian3.subtract(scratchWorld, origin, scratchLocal);
out.push(scratchLocal.x, scratchLocal.y, scratchLocal.z);
}
/**
* Packs a glTF JSON object and binary buffer into a GLB (Binary glTF) Uint8Array.
* GLB spec: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#binary-gltf-layout
*
* @param {object} gltfJson
* @param {Uint8Array} binaryChunk
* @returns {Uint8Array}
* @ignore
*/
function buildGlb(gltfJson, binaryChunk) {
const GLB_MAGIC = 0x46546c67; // "glTF"
const GLB_VERSION = 2;
const CHUNK_TYPE_JSON = 0x4e4f534a; // "JSON"
const CHUNK_TYPE_BIN = 0x004e4942; // "BIN\0"
const jsonBytes = new TextEncoder().encode(JSON.stringify(gltfJson));
// Pad JSON to 4-byte boundary with spaces (0x20)
const jsonPaddedLength = Math.ceil(jsonBytes.length / 4) * 4;
const jsonChunk = new Uint8Array(jsonPaddedLength);
jsonChunk.fill(0x20);
jsonChunk.set(jsonBytes);
// Pad binary to 4-byte boundary with zeros
const binPaddedLength = Math.ceil(binaryChunk.byteLength / 4) * 4;
const binChunk = new Uint8Array(binPaddedLength);
binChunk.set(binaryChunk);
const totalLength =
12 + // header
8 +
jsonPaddedLength + // JSON chunk header + data
8 +
binPaddedLength; // BIN chunk header + data
const glb = new Uint8Array(totalLength);
const view = new DataView(glb.buffer);
let offset = 0;
// Header
view.setUint32(offset, GLB_MAGIC, true);
offset += 4;
view.setUint32(offset, GLB_VERSION, true);
offset += 4;
view.setUint32(offset, totalLength, true);
offset += 4;
// JSON chunk
view.setUint32(offset, jsonPaddedLength, true);
offset += 4;
view.setUint32(offset, CHUNK_TYPE_JSON, true);
offset += 4;
glb.set(jsonChunk, offset);
offset += jsonPaddedLength;
// BIN chunk
view.setUint32(offset, binPaddedLength, true);
offset += 4;
view.setUint32(offset, CHUNK_TYPE_BIN, true);
offset += 4;
glb.set(binChunk, offset);
return glb;
}
/**
* @param {Uint8Array[]} chunks
* @param {number} totalByteLength
* @returns {Uint8Array}
* @ignore
*/
function concatChunks(chunks, totalByteLength) {
const out = new Uint8Array(totalByteLength);
let offset = 0;
for (const chunk of chunks) {
out.set(chunk, offset);
offset += chunk.byteLength;
}
return out;
}
export default buildVectorGltfFromMVT;