UNPKG

@here/harp-mapview

Version:

Functionality needed to render a map.

366 lines 17.3 kB
"use strict"; /* * Copyright (C) 2020-2021 HERE Europe B.V. * Licensed under Apache 2.0, see full license in LICENSE * SPDX-License-Identifier: Apache-2.0 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.SolidLineMesh = void 0; const harp_geoutils_1 = require("@here/harp-geoutils"); const harp_materials_1 = require("@here/harp-materials"); const harp_utils_1 = require("@here/harp-utils"); const THREE = require("three"); const DisplacedBufferGeometry_1 = require("./DisplacedBufferGeometry"); const tmpSphere = new THREE.Sphere(); const tmpInverseMatrix = new THREE.Matrix4(); const tmpRay = new THREE.Ray(); const tmpLine1 = new THREE.Line3(); const tmpBox = new THREE.Box3(); const tmpOBB = new harp_geoutils_1.OrientedBox3(); const tmpPlane = new THREE.Plane(); const tmpV1 = new THREE.Vector3(); const tmpV2 = new THREE.Vector3(); const tmpV3 = new THREE.Vector3(); const tmpV4 = new THREE.Vector3(); // Strides to access the index buffer. See [[createLineGeometry]]. // Stride between the start vertex indices of consecutive segments, each one made of 2 triangles. const SEGMENT_STRIDE = 6; // Stride between the start and end vertex indices of a segment. Vertices are duplicated so that // each copy is extruded in opposite directions in the vertex shader. const VERTEX_STRIDE = 2; function isSolidLineMaterial(material) { return Array.isArray(material) ? material.every(mat => mat instanceof harp_materials_1.SolidLineMaterial) : material instanceof harp_materials_1.SolidLineMaterial; } /** * Create an [[AttributeInfo]] for the specified attribute. * @param attribute The attribute to retrieve version info from. * @returns The [[AttributeInfo]] containing a reference and version of the attribute's data. */ function getAttributeInfo(attribute) { const isBufferAttribute = attribute.isBufferAttribute === true; const data = isBufferAttribute ? attribute : attribute.data; return { data, version: data.version }; } /** * Check if an attribute has changed compared to the version info. * @param attribute Attribute to check. * @param attrInfo Attribute version info. * @returns `true` if the attribute is the same, `false` otherwise. */ function attributeChanged(attribute, attrInfo) { const isBufferAttribute = attribute.isBufferAttribute === true; const data = isBufferAttribute ? attribute : attribute.data; return (attrInfo === undefined || attrInfo.data !== data || (attribute.isBufferAttribute && attrInfo.version !== data.version)); } /** * Computes the bounding sphere of the part of a given geometry corresponding to a feature. * @param geometry - The geometry containing the feature. * @param featureBeginIndex - The index where the feature starts in the geometry's * indices attribute. * @param featureEndIndex - The index where the feature end in the geometry's indices attribute. * @returns The feature bounding sphere. */ function computeFeatureBoundingSphere(geometry, featureBeginIndex, featureEndIndex) { let displacementRange; if (geometry instanceof DisplacedBufferGeometry_1.DisplacedBufferGeometry) { displacementRange = geometry.displacementRange; geometry = geometry.originalGeometry; } const attributes = geometry.attributes; const pos = attributes.position; const indices = geometry.index.array; const sphere = new THREE.Sphere(); const bbox = tmpBox.makeEmpty(); const vertex = tmpV1; // First compute the bounding box for all line segments. for (let i = featureBeginIndex; i < featureEndIndex; i += SEGMENT_STRIDE) { bbox.expandByPoint(vertex.fromBufferAttribute(pos, indices[i])); bbox.expandByPoint(vertex.fromBufferAttribute(pos, indices[i + VERTEX_STRIDE])); } if (displacementRange) { // If geometry is displaced, expand the bounding box to cover the whole displacement range, // and return the sphere bounding the box. This is a coarse estimation, but avoids having // to displace all vertices. // All normals in the geometry are assumed to be the same or close enough so that any of // them can be used as displacement direction. For sphere projection, the surface normals // within a tile are approximately the same from level 4 onwards. Here are some examples of // the minimum dot product between normals in a tile (normals at tile's opposite corners): // TILE: (6,9,4): 0.9806892129880023 // TILE: (12,17,5): 0.9946739445457075 // TILE: (25,34,6): 0.9986326302953471 // TILE: (50,68,7): 0.9996583822992287 // TILE: (1620,2199,12): 0.9999996706085572 const normal = tmpV2; normal.fromBufferAttribute(geometry.attributes.normal, 0); return DisplacedBufferGeometry_1.displaceBox(bbox, displacementRange, normal).getBoundingSphere(sphere); } return bbox.getBoundingSphere(sphere); } /** * Finds the intersection of a ray with a extruded line. * @param ray - Intersection ray in object's local space. * @param line - The centerline. * @param vExtrusion - Line extrusion vector. * @param normal - Extrusion plane normal. * @param hWidth - Extrusion half width. * @returns Distance of the extruded line intersection to the ray origin. */ function intersectExtrudedLine(ray, line, vExtrusion, normal, hWidth) { var _a; const obb = tmpOBB; line.getCenter(obb.position); line.delta(obb.xAxis).normalize(); obb.yAxis.copy(vExtrusion); obb.zAxis.copy(normal); obb.extents.set(line.distance() / 2, hWidth, hWidth); if (obb.contains(ray.origin)) { return 0; } return (_a = obb.intersectsRay(ray)) !== null && _a !== void 0 ? _a : Infinity; } /** * Finds the intersection of a ray with the closest end cap of a extruded line. * @param ray - Intersection ray in object's local space. * @param line - The centerline. * @param hWidth - Extrusion half width. * @returns Distance of the end cap intersection to the ray origin. */ function intersectClosestEndCap(ray, line, hWidth) { const sphere = new THREE.Sphere(line.start, hWidth); const startCapT = sphere.containsPoint(ray.origin) ? 0 : ray.intersectSphere(sphere, tmpV4) ? tmpV4.sub(ray.origin).length() : Infinity; sphere.center.copy(line.end); const endCapT = sphere.containsPoint(ray.origin) ? 0 : ray.intersectSphere(sphere, tmpV4) ? tmpV4.sub(ray.origin).length() : Infinity; return Math.min(startCapT, endCapT); } /** * Intersects line * @param ray - Intersection ray in object's local space. * @param line - The line to intersect. * @param vExtrusion - Line extrusion vector. * @param hWidth - The line's extrusion half width. * @param hWidthSq - The line's extrusion half width squared. * @param plane - The extrusion plane. * @param interPlane - The intersection of the ray with the extrusion plane. * @param outInterLine - The ray intersection with the extruded line. * @returns true if ray intersects the extruded line, false otherwise. */ function intersectLine(ray, line, vExtrusion, hWidth, hWidthSq, plane, interPlane, outInterLine) { if (interPlane.equals(ray.origin) && ray.direction.dot(plane.normal) === 0) { // Corner case: ray is coplanar to extruded line, find distance to extruded line sides // and end caps. const extrLineT = intersectExtrudedLine(ray, line, vExtrusion, plane.normal, hWidth); const endCapT = intersectClosestEndCap(ray, line, hWidth); const minT = Math.min(extrLineT, endCapT); if (minT === Infinity) { return false; } ray.at(minT, outInterLine); return true; } // The plain intersection is also a line intersection only if it's closer to the line // than the extrusion half width. const distSq = interPlane.distanceToSquared(line.closestPointToPoint(interPlane, true, tmpV4)); if (distSq > hWidthSq) { return false; } outInterLine.copy(interPlane); return true; } /** * Finds the intersections of a ray with a partition of a solid line mesh representing a feature. * @param mesh - The mesh whose intersections will be found. * @param raycaster - Contains the intersection ray. * @param localRay - Same ray as raycaster.ray but in object's local space. * @param halfWidth - The line's extrusion half width. * @param lHalfWidth - The line's extrusion half width in mesh local space. * @param lHalfWidthSq - The line's extrusion half width squared in mesh local space. * @param beginIdx - The index where the feature starts in the mesh geometry's indices attribute. * @param endIdx - The index where the feature end in the mesh geometry's indices attribute. * @param bSphere - The feature bounding sphere. * @param intersections - Array where all intersections found between ray and feature will * be pushed. */ function intersectFeature(mesh, raycaster, localRay, halfWidth, lHalfWidth, lHalfWidthSq, beginIdx, endIdx, bSphere, intersections) { const vExt = tmpV1; const plane = tmpPlane; const interPlane = tmpV2; const line = tmpLine1; const geometry = mesh.geometry; const attributes = geometry.attributes; const position = attributes.position; const bitangent = attributes.biTangent; const indices = geometry.index.array; tmpSphere.copy(bSphere); tmpSphere.applyMatrix4(mesh.matrixWorld); tmpSphere.radius += halfWidth; if (!raycaster.ray.intersectsSphere(tmpSphere)) { return; } for (let i = beginIdx; i < endIdx; i += SEGMENT_STRIDE) { const a = indices[i]; const b = indices[i + VERTEX_STRIDE]; // Find the plane containing the line segment, using the segment start, end and extrusion // vector. line.start.fromBufferAttribute(position, a); line.end.fromBufferAttribute(position, b); vExt.set(bitangent.getX(a), bitangent.getY(a), bitangent.getZ(a)).normalize(); plane.setFromCoplanarPoints(line.start, tmpV3.copy(line.start).add(vExt), line.end); if (plane.normal.manhattanLength() === 0) { // Invalid plane, coplanar points are actually collinear because: // a) The line segment has length 0. // b) The extrusion vector has length 0. // c) The extrusion and segment directions are the same. // In any case it's a degenerate segment, skip it. continue; } // The ray intersection if any, will be on the extrusion plane. if (!localRay.intersectPlane(plane, interPlane)) { continue; } const interLine = tmpV3; if (!intersectLine(localRay, line, vExt, lHalfWidth, lHalfWidthSq, plane, interPlane, interLine)) { continue; } // Move back to world space for distance calculation const interLineWorld = interLine.applyMatrix4(mesh.matrixWorld); const distance = raycaster.ray.origin.distanceTo(interLineWorld); if (distance < raycaster.near || distance > raycaster.far) { continue; } intersections.push({ distance, point: interLineWorld.clone(), index: i, object: mesh }); } } const singleFeatureStart = [0]; const MAX_SCALE_RATIO_DIFF = 1e-2; /** * Finds the intersections of a ray with a group within a solid line mesh. * @param mesh - The mesh whose intersections will be found. * @param material - The material used by the group inside the mesh. * @param raycaster - Contains the intersection ray. * @param localRay - Same ray as raycaster.ray but in object's local space. * @param firstFeatureIdx - Index of the first feature in the group. * @param groupEndIdx - Index of the last vertex in the group. * @param intersections - Array where all intersections found between ray and group will be pushed. * @returns The next feature index after the group. */ function intersectGroup(mesh, material, raycaster, localRay, firstFeatureIdx, groupEndIdx, intersections) { var _a; const bVolumes = mesh.userData.feature.boundingVolumes; harp_utils_1.assert(mesh.geometry instanceof THREE.BufferGeometry, "Unsupported geometry type."); const geometry = mesh.geometry; harp_utils_1.assert(isSolidLineMaterial(material), "Unsupported material type"); const solidLineMaterial = material; const halfWidth = (solidLineMaterial.lineWidth + solidLineMaterial.outlineWidth) / 2; // Assumption: scaling is uniform or close enough to use a local width independent of direction. harp_utils_1.assert(Math.abs(1 - mesh.scale.x / mesh.scale.y) < MAX_SCALE_RATIO_DIFF); harp_utils_1.assert(Math.abs(1 - mesh.scale.x / mesh.scale.z) < MAX_SCALE_RATIO_DIFF); harp_utils_1.assert(Math.abs(1 - mesh.scale.y / mesh.scale.z) < MAX_SCALE_RATIO_DIFF); const localHalfWidth = halfWidth / ((mesh.scale.x + mesh.scale.y + mesh.scale.z) / 3); const localHalfWidthSq = localHalfWidth * localHalfWidth; const featureStarts = (_a = mesh.userData.feature.starts) !== null && _a !== void 0 ? _a : singleFeatureStart; let featureIdx = firstFeatureIdx; let beginIdx = featureStarts[featureIdx]; const lastFeatureIdx = featureStarts.length - 1; while (beginIdx < groupEndIdx) { const bVolumeIdx = featureIdx; const endIdx = featureIdx < lastFeatureIdx ? featureStarts[++featureIdx] : groupEndIdx; if (bVolumeIdx >= bVolumes.length) { // Geometry might be extruded on any direction. To avoid extruding all vertices, the // centerline geometry is used to compute a bounding sphere whose radius is later // expanded by the extrusion half width to ensure it contains the extruded geometry. bVolumes.push(computeFeatureBoundingSphere(geometry, beginIdx, endIdx)); } intersectFeature(mesh, raycaster, localRay, halfWidth, localHalfWidth, localHalfWidthSq, beginIdx, endIdx, bVolumes[bVolumeIdx], intersections); beginIdx = endIdx; } return featureIdx; } /** * Mesh formed by extruding a polyline in the shaders. Overrides raycasting behaviour to account for * extrusion, see [[SolidLineMaterial]]. * @internal */ class SolidLineMesh extends THREE.Mesh { /** * Finds the intersections of a ray with a mesh, assuming the mesh is a polyline extruded in * the shaders (see [[SolidLineMaterial]]). * @param mesh - The mesh whose intersections will be found. * @param raycaster - Contains the intersection ray. * @param intersections - Array where all intersections found between ray and mesh will * be pushed. */ static raycast(mesh, raycaster, intersections) { harp_utils_1.assert(mesh.geometry instanceof THREE.BufferGeometry, "Unsupported geometry type"); const geometry = mesh.geometry; harp_utils_1.assert(geometry.index !== null, "Geometry does not have indices"); const matrixWorld = mesh.matrixWorld; tmpInverseMatrix.copy(matrixWorld).invert(); const localRay = tmpRay.copy(raycaster.ray).applyMatrix4(tmpInverseMatrix); // Test intersection of ray with each of the features within the mesh. if (mesh.userData.feature === undefined) { mesh.userData.feature = {}; } const positionAttribute = geometry.attributes["position"]; const attributeInfo = mesh.userData.feature .attributeInfo; // Rebuild bounding volumes if geometry has been modified. if (attributeInfo === undefined || mesh.userData.feature.boundingVolumes === undefined || attributeChanged(positionAttribute, attributeInfo)) { mesh.userData.feature.boundingVolumes = []; mesh.userData.feature.attributeInfo = getAttributeInfo(positionAttribute); } const indices = geometry.index.array; if (Array.isArray(mesh.material)) { let nextFeatureIdx = 0; for (const group of geometry.groups) { const material = mesh.material[group.materialIndex]; const groupEndIdx = group.start + group.count; nextFeatureIdx = intersectGroup(mesh, material, raycaster, localRay, nextFeatureIdx, groupEndIdx, intersections); } } else { intersectGroup(mesh, mesh.material, raycaster, localRay, 0, indices.length, intersections); } } /** * Creates an instance of SolidLineMesh. * @param geometry - Mesh geometry. * @param material - Material(s) to be used by the mesh. They must be instances of * [[SolidLineMaterial]]. */ constructor(geometry, material) { super(geometry, material); } // HARP-9585: Override of base class method, however tslint doesn't recognize it as such. raycast(raycaster, intersects) { SolidLineMesh.raycast(this, raycaster, intersects); } } exports.SolidLineMesh = SolidLineMesh; //# sourceMappingURL=SolidLineMesh.js.map