@here/harp-mapview
Version:
Functionality needed to render a map.
467 lines • 19.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BoxBuffer = exports.BoxBufferMesh = void 0;
const harp_utils_1 = require("@here/harp-utils");
const THREE = require("three");
const PixelPicker_1 = require("./PixelPicker");
/**
* Initial number of boxes in BoxBuffer.
*/
const START_BOX_BUFFER_SIZE = 0;
/**
* Maximum number of boxes in BoxBuffer.
*/
const MAX_BOX_BUFFER_SIZE = 32 * 1024;
/**
* Number of vertices per box/glyph element: 4 corners.
*/
const NUM_VERTICES_PER_ELEMENT = 4;
/**
* Number of indices added per box/glyph: 2 triangles, 6 indices.
*/
const NUM_INDICES_PER_ELEMENT = 6;
/**
* Number of values per position.
*/
const NUM_POSITION_VALUES_PER_VERTEX = 3;
/**
* Number of values per color.
*/
const NUM_COLOR_VALUES_PER_VERTEX = 4;
/**
* Number of values per UV.
*/
const NUM_UV_VALUES_PER_VERTEX = 4;
/**
* Number of values per index.
*/
const NUM_INDEX_VALUES_PER_VERTEX = 1;
/**
* Number of bytes for float in an Float32Array.
*/
const NUM_BYTES_PER_FLOAT = 4;
/**
* Number of bytes for integer number in an UInt32Array.
*/
const NUM_BYTES_PER_INT32 = 4;
/**
* SubClass of [[THREE.Mesh]] to identify meshes that have been created by [[BoxBuffer]] and
* [[TextBuffer]]. Add the isEmpty flag to quickly test for empty meshes.
*/
class BoxBufferMesh extends THREE.Mesh {
constructor(geometry, material) {
super(geometry, material);
this.type = "BoxBufferMesh";
}
/**
* A mesh that has no positions and indices set is defined to be empty.
*
* @returns `True` if no indices have been added to the mesh.
*/
get isEmpty() {
if (this.geometry === undefined) {
return true;
}
else {
const bufferGeometry = this.geometry;
return bufferGeometry.index === null || bufferGeometry.index.count === 0;
}
}
}
exports.BoxBufferMesh = BoxBufferMesh;
/**
* Buffer for (untransformed) `Box2` objects. Can be used to create a single geometry for screen-
* aligned boxes, like POIs.
*/
class BoxBuffer {
/**
* Creates a new `BoxBuffer`.
*
* @param m_material - Material to be used for [[Mesh]] of this `BoxBuffer`.
* @param m_renderOrder - Optional renderOrder of this buffer.
* @param startElementCount - Initial number of elements this `BoxBuffer` can hold.
* @param m_maxElementCount - Maximum number of elements this `BoxBuffer` can hold.
*/
constructor(m_material, m_renderOrder = 0, startElementCount = START_BOX_BUFFER_SIZE, m_maxElementCount = MAX_BOX_BUFFER_SIZE) {
this.m_material = m_material;
this.m_renderOrder = m_renderOrder;
this.m_maxElementCount = m_maxElementCount;
this.m_size = 0;
this.resizeBuffer(startElementCount);
this.m_pickInfos = new Array();
}
/**
* Duplicate this `BoxBuffer` with same material and renderOrder.
*
* @returns A clone of this `BoxBuffer`.
*/
clone() {
return new BoxBuffer(this.m_material, this.m_renderOrder);
}
/**
* Dispose of the geometry.
*/
dispose() {
if (this.m_geometry !== undefined) {
this.m_geometry.dispose();
this.m_geometry = undefined;
}
this.m_mesh = undefined;
}
/**
* Return the current number of elements the buffer can hold.
*/
get size() {
return this.m_size;
}
/**
* Clear's the `BoxBuffer` attribute buffers.
*/
reset() {
if (this.m_positionAttribute !== undefined) {
this.m_positionAttribute.count = 0;
this.m_colorAttribute.count = 0;
this.m_uvAttribute.count = 0;
this.m_indexAttribute.count = 0;
this.m_pickInfos.length = 0;
}
}
/**
* Returns `true` if this `BoxBuffer` can hold the specified amount of glyphs. If the buffer
* can only add the glyph by increasing the buffer size, the resize() method is called, which
* will then create a new geometry for the mesh.
*
* @param glyphCount - Number of glyphs to be added to the buffer.
* @returns `true` if the element (box or glyph) can be added to the buffer, `false` otherwise.
*/
canAddElements(glyphCount = 1) {
const indexAttribute = this.m_indexAttribute;
if (indexAttribute.count + glyphCount * NUM_INDICES_PER_ELEMENT >=
indexAttribute.array.length) {
// Too many elements for the current buffer, check if we can resize the buffer.
if (indexAttribute.array.length >= this.m_maxElementCount * NUM_INDICES_PER_ELEMENT) {
return false;
}
const newSize = Math.min(this.m_maxElementCount, this.size === 0 ? 256 : this.size * 2);
this.resize(newSize);
}
return true;
}
/**
* Returns this `BoxBuffer`'s attribute [[State]].
*/
saveState() {
const state = {
positionAttributeCount: this.m_positionAttribute.count,
colorAttributeCount: this.m_colorAttribute.count,
uvAttributeCount: this.m_uvAttribute.count,
indexAttributeCount: this.m_indexAttribute.count,
pickInfoCount: this.m_pickInfos.length
};
return state;
}
/**
* Store this `BoxBuffer`'s attribute [[State]] to a previously stored one.
*
* @param state - [[State]] struct describing a previous attribute state.
*/
restoreState(state) {
this.m_positionAttribute.count = state.positionAttributeCount;
this.m_colorAttribute.count = state.colorAttributeCount;
this.m_uvAttribute.count = state.uvAttributeCount;
this.m_indexAttribute.count = state.indexAttributeCount;
this.m_pickInfos.length = state.pickInfoCount;
}
/**
* Adds a new box to this `BoxBuffer`.
*
* @param screenBox - [[Math2D.Box]] holding screen coordinates for this box.
* @param uvBox - [[Math2D.UvBox]] holding uv coordinates for this box.
* @param color - Box's color.
* @param opacity - Box's opacity.
* @param distance - Box's distance to camera.
* @param pickInfo - Box's picking information.
*/
addBox(screenBox, uvBox, color, opacity, distance, pickInfo) {
if (!this.canAddElements()) {
return false;
}
const { s0, t0, s1, t1 } = uvBox;
const { x, y, w, h } = screenBox;
// Premultiply alpha into vertex colors
const r = Math.round(color.r * opacity * 255);
const g = Math.round(color.g * opacity * 255);
const b = Math.round(color.b * opacity * 255);
const a = Math.round(opacity * 255);
const positionAttribute = this.m_positionAttribute;
const colorAttribute = this.m_colorAttribute;
const uvAttribute = this.m_uvAttribute;
const indexAttribute = this.m_indexAttribute;
const baseVertex = positionAttribute.count;
const baseIndex = indexAttribute.count;
positionAttribute.setXYZ(baseVertex, x, y, distance);
positionAttribute.setXYZ(baseVertex + 1, x + w, y, distance);
positionAttribute.setXYZ(baseVertex + 2, x, y + h, distance);
positionAttribute.setXYZ(baseVertex + 3, x + w, y + h, distance);
colorAttribute.setXYZW(baseVertex, r, g, b, a);
colorAttribute.setXYZW(baseVertex + 1, r, g, b, a);
colorAttribute.setXYZW(baseVertex + 2, r, g, b, a);
colorAttribute.setXYZW(baseVertex + 3, r, g, b, a);
uvAttribute.setXY(baseVertex, s0, t0);
uvAttribute.setXY(baseVertex + 1, s1, t0);
uvAttribute.setXY(baseVertex + 2, s0, t1);
uvAttribute.setXY(baseVertex + 3, s1, t1);
indexAttribute.setX(baseIndex, baseVertex);
indexAttribute.setX(baseIndex + 1, baseVertex + 1);
indexAttribute.setX(baseIndex + 2, baseVertex + 2);
indexAttribute.setX(baseIndex + 3, baseVertex + 2);
indexAttribute.setX(baseIndex + 4, baseVertex + 1);
indexAttribute.setX(baseIndex + 5, baseVertex + 3);
positionAttribute.count += NUM_VERTICES_PER_ELEMENT;
colorAttribute.count += NUM_VERTICES_PER_ELEMENT;
uvAttribute.count += NUM_VERTICES_PER_ELEMENT;
indexAttribute.count += NUM_INDICES_PER_ELEMENT;
this.m_pickInfos.push(pickInfo);
return true;
}
/**
* Updates a [[BufferGeometry]] object to reflect the changes in this `TextBuffer`'s attribute
* data.
*/
updateBufferGeometry() {
const positionAttribute = this.m_positionAttribute;
const colorAttribute = this.m_colorAttribute;
const uvAttribute = this.m_uvAttribute;
const indexAttribute = this.m_indexAttribute;
if (positionAttribute.count > 0) {
positionAttribute.needsUpdate = true;
positionAttribute.updateRange.offset = 0;
positionAttribute.updateRange.count =
positionAttribute.count * NUM_VERTICES_PER_ELEMENT;
}
if (colorAttribute.count > 0) {
colorAttribute.needsUpdate = true;
colorAttribute.updateRange.offset = 0;
colorAttribute.updateRange.count = colorAttribute.count * NUM_VERTICES_PER_ELEMENT;
}
if (uvAttribute.count > 0) {
uvAttribute.needsUpdate = true;
uvAttribute.updateRange.offset = 0;
uvAttribute.updateRange.count = uvAttribute.count * NUM_VERTICES_PER_ELEMENT;
}
if (indexAttribute.count > 0) {
indexAttribute.needsUpdate = true;
indexAttribute.updateRange.offset = 0;
indexAttribute.updateRange.count = indexAttribute.count;
}
if (this.m_geometry !== undefined) {
this.m_geometry.clearGroups();
this.m_geometry.addGroup(0, this.m_indexAttribute.count);
}
}
/**
* Check if the buffer is empty. If it is empty, the memory usage is minimized to reduce
* footprint.
*/
cleanUp() {
// If there is nothing in this buffer, resize it, it may never be used again.
if (this.m_indexAttribute.count === 0 && this.size > START_BOX_BUFFER_SIZE) {
this.clearAttributes();
}
}
/**
* Determine if the mesh is empty.
*/
get isEmpty() {
return this.m_mesh.isEmpty;
}
/**
* Get the [[Mesh]] object. The geometry instance of the mesh may change if the buffers are
* resized. The mesh, once created, will not change, so it can always be added to the scene.
*/
get mesh() {
if (this.m_mesh === undefined) {
this.resize();
}
return this.m_mesh;
}
/**
* Fill the picking results for the pixel with the given screen coordinate. If multiple
* boxes are found, the order of the results is unspecified.
*
* @param screenPosition - Screen coordinate of picking position.
* @param pickCallback - Callback to be called for every picked element.
* @param image - Image to test if the pixel is transparent
*/
pickBoxes(screenPosition, pickCallback, image) {
const n = this.m_pickInfos.length;
const pickInfos = this.m_pickInfos;
const positions = this.m_positionAttribute;
const screenX = screenPosition.x;
const screenY = screenPosition.y;
for (let pickInfoIndex = 0; pickInfoIndex < n; pickInfoIndex++) {
const positionIndex = pickInfoIndex * NUM_VERTICES_PER_ELEMENT;
const minX = positions.getX(positionIndex);
if (screenX < minX) {
continue;
}
const maxX = positions.getX(positionIndex + 1);
if (screenX > maxX) {
continue;
}
const minY = positions.getY(positionIndex);
if (screenY < minY) {
continue;
}
const maxY = positions.getY(positionIndex + 2);
if (screenY > maxY) {
continue;
}
const box = new harp_utils_1.Math2D.Box(minX, minY, maxX - minX, maxY - minY);
if (image !== undefined &&
pickInfos[pickInfoIndex].poiInfo !== undefined &&
pickInfos[pickInfoIndex].poiInfo.uvBox !== undefined &&
this.isPixelTransparent(image, screenX, screenY, box, pickInfos[pickInfoIndex].poiInfo.uvBox, document.createElement("canvas"))) {
continue;
}
if (pickInfos[pickInfoIndex] !== undefined) {
pickCallback(pickInfos[pickInfoIndex]);
}
}
}
/**
* Creates a new {@link @here/harp-datasource-protocol#Geometry} object
* from all the attribute data stored in this `BoxBuffer`.
*
* @remarks
* The [[Mesh]] object may be created if it is not initialized already.
*
* @param newSize - Optional number of elements to resize the buffer to.
* @param forceResize - Optional flag to force a resize even if new size is smaller than before.
*/
resize(newSize, forceResize) {
if (this.m_geometry !== undefined) {
this.m_geometry.dispose();
}
this.m_geometry = new THREE.BufferGeometry();
if (newSize !== undefined && (forceResize === true || newSize > this.size)) {
this.resizeBuffer(newSize);
}
this.m_geometry.setAttribute("position", this.m_positionAttribute);
this.m_geometry.setAttribute("color", this.m_colorAttribute);
this.m_geometry.setAttribute("uv", this.m_uvAttribute);
this.m_geometry.setIndex(this.m_indexAttribute);
this.m_geometry.addGroup(0, this.m_indexAttribute.count);
if (this.m_mesh === undefined) {
this.m_mesh = new BoxBufferMesh(this.m_geometry, this.m_material);
this.m_mesh.renderOrder = this.m_renderOrder;
}
else {
this.m_mesh.geometry = this.m_geometry;
}
return this.m_mesh;
}
/**
* Update the info with the memory footprint caused by objects owned by the `BoxBuffer`.
*
* @param info - The info object to increment with the values from this `BoxBuffer`.
*/
updateMemoryUsage(info) {
const numBytes = this.m_positionAttribute.count * NUM_POSITION_VALUES_PER_VERTEX * NUM_BYTES_PER_FLOAT +
this.m_colorAttribute.count * NUM_COLOR_VALUES_PER_VERTEX +
this.m_uvAttribute.count * NUM_UV_VALUES_PER_VERTEX * NUM_BYTES_PER_FLOAT +
this.m_indexAttribute.count * NUM_BYTES_PER_INT32; // May be UInt16, so we overestimate
info.heapSize += numBytes;
info.gpuSize += numBytes;
}
/**
* Check if a pixel is transparent or not.
*
* @param image - Image source.
* @param xScreenPos - X position of the pixel.
* @param yScreenPos - Y position of the pixel.
* @param box - Bounding box of the image in screen coordinates.
* @param uvBox - Uv box referred to the given bounding box.
* @param canvas - Canvas element to draw the image if it's not a `ImageData` object.
*/
isPixelTransparent(image, xScreenPos, yScreenPos, box, uvBox, canvas) {
const { u, v } = PixelPicker_1.screenToUvCoordinates(xScreenPos, yScreenPos, box, uvBox);
const { width, height } = image instanceof SVGImageElement ? image.getBBox() : image;
const x = width * u;
const y = height * v;
const pixel = PixelPicker_1.getPixelFromImage(x, y, image, canvas);
return pixel !== undefined && pixel[3] === 0;
}
/**
* Remove current attributes and arrays. Minimizes memory footprint.
*/
clearAttributes() {
this.m_positionAttribute = undefined;
this.m_colorAttribute = undefined;
this.m_uvAttribute = undefined;
this.m_indexAttribute = undefined;
this.resize(START_BOX_BUFFER_SIZE, true);
}
/**
* Resize the attribute buffers. New value must be larger than the previous one.
*
* @param newSize - New number of elements in the buffer. Number has to be larger than the
* previous size.
*/
resizeBuffer(newSize) {
const newPositionArray = new Float32Array(newSize * NUM_VERTICES_PER_ELEMENT * NUM_POSITION_VALUES_PER_VERTEX);
if (this.m_positionAttribute !== undefined && this.m_positionAttribute.array.length > 0) {
const positionAttributeCount = this.m_positionAttribute.count;
newPositionArray.set(this.m_positionAttribute.array);
this.m_positionAttribute.array = newPositionArray;
this.m_positionAttribute.count = positionAttributeCount;
}
else {
this.m_positionAttribute = new THREE.BufferAttribute(newPositionArray, NUM_POSITION_VALUES_PER_VERTEX);
this.m_positionAttribute.count = 0;
this.m_positionAttribute.setUsage(THREE.DynamicDrawUsage);
}
const newColorArray = new Uint8Array(newSize * NUM_VERTICES_PER_ELEMENT * NUM_COLOR_VALUES_PER_VERTEX);
if (this.m_colorAttribute !== undefined) {
const colorAttributeCount = this.m_colorAttribute.count;
newColorArray.set(this.m_colorAttribute.array);
this.m_colorAttribute.array = newColorArray;
this.m_colorAttribute.count = colorAttributeCount;
}
else {
this.m_colorAttribute = new THREE.BufferAttribute(newColorArray, NUM_COLOR_VALUES_PER_VERTEX, true);
this.m_colorAttribute.count = 0;
this.m_colorAttribute.setUsage(THREE.DynamicDrawUsage);
}
const newUvArray = new Float32Array(newSize * NUM_VERTICES_PER_ELEMENT * NUM_UV_VALUES_PER_VERTEX);
if (this.m_uvAttribute !== undefined) {
const uvAttributeCount = this.m_uvAttribute.count;
newUvArray.set(this.m_uvAttribute.array);
this.m_uvAttribute.array = newUvArray;
this.m_uvAttribute.count = uvAttributeCount;
}
else {
this.m_uvAttribute = new THREE.BufferAttribute(newUvArray, NUM_UV_VALUES_PER_VERTEX);
this.m_uvAttribute.count = 0;
this.m_uvAttribute.setUsage(THREE.DynamicDrawUsage);
}
const numIndexValues = newSize * NUM_INDICES_PER_ELEMENT * NUM_INDEX_VALUES_PER_VERTEX;
const newIndexArray = numIndexValues > 65535
? new Uint32Array(numIndexValues)
: new Uint16Array(numIndexValues);
if (this.m_indexAttribute !== undefined) {
const indexAttributeCount = this.m_indexAttribute.count;
newIndexArray.set(this.m_indexAttribute.array);
this.m_indexAttribute.array = newIndexArray;
this.m_indexAttribute.count = indexAttributeCount;
}
else {
this.m_indexAttribute = new THREE.BufferAttribute(newIndexArray, NUM_INDEX_VALUES_PER_VERTEX);
this.m_indexAttribute.count = 0;
this.m_indexAttribute.setUsage(THREE.DynamicDrawUsage);
}
this.m_size = newSize;
}
}
exports.BoxBuffer = BoxBuffer;
//# sourceMappingURL=BoxBuffer.js.map