Files
random_tools/node_modules/@cesium/engine/Source/Scene/buildVectorGltfFromMVT.js
T
2026-08-11 09:53:42 -04:00

1060 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @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 (0extent)
* @property {number} y Tile-local y (0extent)
* @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;