Skip to content
This repository has been archived by the owner on Mar 8, 2023. It is now read-only.

HARP-13901: Fix THREE.Line and THREE.LineSegments picking. #2075

Merged
merged 5 commits into from
Feb 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions @here/harp-mapview/lib/MapObjectAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under Apache 2.0, see full license in LICENSE
* SPDX-License-Identifier: Apache-2.0
*/
import { GeometryKind, MapEnv, Pickability, Technique } from "@here/harp-datasource-protocol";
import { GeometryKind, Pickability, Technique } from "@here/harp-datasource-protocol";
import * as THREE from "three";

import { DataSource } from "./DataSource";
Expand Down Expand Up @@ -135,9 +135,8 @@ export class MapObjectAdapter {

/**
* Whether underlying `THREE.Object3D` should be pickable by {@link PickHandler}.
* @param env - Property lookup environment.
*/
isPickable(env: MapEnv) {
isPickable() {
// An object is pickable only if it's visible and Pickabilty.onlyVisible or
// Pickabililty.all set.
return (
Expand Down
30 changes: 6 additions & 24 deletions @here/harp-mapview/lib/MapView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ import { MapViewFog } from "./MapViewFog";
import { MapViewTaskScheduler } from "./MapViewTaskScheduler";
import { MapViewThemeManager } from "./MapViewThemeManager";
import { PickHandler, PickResult } from "./PickHandler";
import { PickingRaycaster } from "./PickingRaycaster";
import { PoiManager } from "./poi/PoiManager";
import { PoiTableManager } from "./poi/PoiTableManager";
import { PolarTileDataSource } from "./PolarTileDataSource";
Expand Down Expand Up @@ -869,7 +868,7 @@ export class MapView extends EventDispatcher {
private readonly m_enablePolarDataSource: boolean = true;

// gestures
private readonly m_raycaster: PickingRaycaster;
private readonly m_raycaster = new THREE.Raycaster();
private readonly m_plane = new THREE.Plane(new THREE.Vector3(0, 0, 1));
private readonly m_sphere = new THREE.Sphere(undefined, EarthConstants.EQUATORIAL_RADIUS);

Expand Down Expand Up @@ -1007,11 +1006,6 @@ export class MapView extends EventDispatcher {
this.m_politicalView = this.m_options.politicalView;

this.handleRequestAnimationFrame = this.renderLoop.bind(this);
this.m_pickHandler = new PickHandler(
this,
this.m_rteCamera,
this.m_options.enablePickTechnique === true
);

if (this.m_options.tileWrappingEnabled !== undefined) {
this.m_tileWrappingEnabled = this.m_options.tileWrappingEnabled;
Expand Down Expand Up @@ -1087,7 +1081,11 @@ export class MapView extends EventDispatcher {
// setup camera with initial position
this.setupCamera();

this.m_raycaster = new PickingRaycaster(width, height, this.m_env);
this.m_pickHandler = new PickHandler(
this,
this.m_rteCamera,
this.m_options.enablePickTechnique === true
);

this.m_movementDetector = new CameraMovementDetector(
this.m_options.movementThrottleTimeout,
Expand Down Expand Up @@ -2569,22 +2567,6 @@ export class MapView extends EventDispatcher {
return p;
}

/**
* Returns a ray caster using the supplied screen positions.
*
* @param x - The X position in css/client coordinates (without applied display ratio).
* @param y - The Y position in css/client coordinates (without applied display ratio).
*
* @alpha
*
* @return Raycaster with origin at the camera and direction based on the supplied x / y screen
* points.
*/
raycasterFromScreenPoint(x: number, y: number): THREE.Raycaster {
this.m_raycaster.setFromCamera(this.getNormalizedScreenCoordinates(x, y), this.m_rteCamera);
return this.m_raycaster;
}

getWorldPositionAt(x: number, y: number, fallback: true): THREE.Vector3;
getWorldPositionAt(x: number, y: number, fallback?: boolean): THREE.Vector3 | null;

Expand Down
84 changes: 30 additions & 54 deletions @here/harp-mapview/lib/MapViewPoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,25 @@ export abstract class MapViewPoints extends THREE.Points {

const geometry = this.geometry;
const matrixWorld = this.matrixWorld;
const screenCoords = raycaster.ray.origin
const ndc = raycaster.ray.origin
.clone()
.add(raycaster.ray.direction)
.project(raycaster.camera);
const mouseCoords = new THREE.Vector2(
Math.ceil(((screenCoords.x + 1) / 2) * raycaster.width),
Math.ceil(((1 - screenCoords.y) / 2) * raycaster.height)
);
const mouseCoords = ndcToScreen(ndc, raycaster);

const testPoint = (point: THREE.Vector3, index: number) => {
const pointInfo = getPointInfo(point, matrixWorld, raycaster);
if (pointInfo.pointIsOnScreen) {
this.testPoint(
point,
pointInfo.absoluteScreenPosition!,
mouseCoords,
index,
pointInfo.distance!,
intersects
);
}
};

if (geometry instanceof THREE.BufferGeometry) {
const point = new THREE.Vector3();
Expand All @@ -85,56 +96,29 @@ export abstract class MapViewPoints extends THREE.Points {
if (index !== null) {
const indices = index.array;
for (let i = 0, il = indices.length; i < il; i++) {
const a = indices[i];
point.fromArray(positions as number[], a * 3);
const pointInfo = getPointInfo(point, matrixWorld, raycaster);
if (pointInfo.pointIsOnScreen) {
this.testPoint(
point,
pointInfo.absoluteScreenPosition!,
mouseCoords,
i,
pointInfo.distance!,
intersects
);
}
testPoint(point.fromArray(positions as number[], indices[i] * 3), i);
}
} else {
for (let i = 0, l = positions.length / 3; i < l; i++) {
point.fromArray(positions as number[], i * 3);
const pointInfo = getPointInfo(point, matrixWorld, raycaster);
if (pointInfo.pointIsOnScreen) {
this.testPoint(
point,
pointInfo.absoluteScreenPosition!,
mouseCoords,
i,
pointInfo.distance!,
intersects
);
}
testPoint(point.fromArray(positions as number[], i * 3), i);
}
}
} else {
const vertices = geometry.vertices;
for (let index = 0; index < vertices.length; index++) {
const point = vertices[index];
const pointInfo = getPointInfo(point, matrixWorld, raycaster);
if (pointInfo.pointIsOnScreen) {
this.testPoint(
point,
pointInfo.absoluteScreenPosition!,
mouseCoords,
index,
pointInfo.distance!,
intersects
);
}
testPoint(vertices[index], index);
}
}
}
}

function ndcToScreen(ndc: THREE.Vector3, raycaster: PickingRaycaster): THREE.Vector2 {
return new THREE.Vector2(ndc.x + 1, 1 - ndc.y)
.divideScalar(2)
.multiply(raycaster.canvasSize)
.ceil();
}

function getPointInfo(
point: THREE.Vector3,
matrixWorld: THREE.Matrix4,
Expand All @@ -144,20 +128,12 @@ function getPointInfo(
absoluteScreenPosition?: THREE.Vector2;
distance?: number;
} {
const worldPosition = point.clone();
worldPosition.applyMatrix4(matrixWorld);
const worldPosition = point.clone().applyMatrix4(matrixWorld);
const distance = worldPosition.distanceTo(raycaster.ray.origin);
worldPosition.project(raycaster.camera);
const relativeScreenPosition = new THREE.Vector2(worldPosition.x, worldPosition.y);
const pointIsOnScreen =
relativeScreenPosition.x < 1 &&
relativeScreenPosition.x > -1 &&
relativeScreenPosition.y < 1 &&
relativeScreenPosition.y > -1;
const ndc = worldPosition.project(raycaster.camera);
const pointIsOnScreen = ndc.x < 1 && ndc.x > -1 && ndc.y < 1 && ndc.y > -1;
if (pointIsOnScreen) {
worldPosition.x = ((worldPosition.x + 1) / 2) * raycaster.width;
worldPosition.y = ((1 - worldPosition.y) / 2) * raycaster.height;
const absoluteScreenPosition = new THREE.Vector2(worldPosition.x, worldPosition.y);
const absoluteScreenPosition = ndcToScreen(ndc, raycaster);
return {
absoluteScreenPosition,
pointIsOnScreen,
Expand Down
63 changes: 54 additions & 9 deletions @here/harp-mapview/lib/PickHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import * as THREE from "three";
import { IntersectParams } from "./IntersectParams";
import { MapView } from "./MapView";
import { MapViewPoints } from "./MapViewPoints";
import { PickingRaycaster } from "./PickingRaycaster";
import { PickListener } from "./PickListener";
import { Tile, TileFeatureData } from "./Tile";
import { MapViewUtils } from "./Utils";

/**
* Describes the general type of a picked object.
Expand Down Expand Up @@ -108,6 +110,7 @@ export interface PickResult {
userData?: any;
}

const tmpV3 = new THREE.Vector3();
const tmpOBB = new OrientedBox3();

// Intersects the dependent tile objects using the supplied raycaster. Note, because multiple
Expand Down Expand Up @@ -143,16 +146,20 @@ function intersectDependentObjects(
* @internal
*/
export class PickHandler {
private readonly m_pickingRaycaster: PickingRaycaster;

constructor(
readonly mapView: MapView,
readonly camera: THREE.Camera,
public enablePickTechnique = false
) {}
) {
this.m_pickingRaycaster = new PickingRaycaster(
mapView.renderer.getSize(new THREE.Vector2())
);
}

/**
* Does a raycast on all objects in the scene; useful for picking. This function is Limited to
* objects that THREE.js can raycast. However, any solid lines that have their geometry in the
* shader cannot be tested for intersection.
* Does a raycast on all objects in the scene; useful for picking.
*
* @param x - The X position in CSS/client coordinates, without the applied display ratio.
* @param y - The Y position in CSS/client coordinates, without the applied display ratio.
Expand All @@ -161,15 +168,14 @@ export class PickHandler {
* @returns the list of intersection results.
*/
intersectMapObjects(x: number, y: number, parameters?: IntersectParams): PickResult[] {
const worldPos = this.mapView.getNormalizedScreenCoordinates(x, y);
const rayCaster = this.mapView.raycasterFromScreenPoint(x, y);

const ndc = this.mapView.getNormalizedScreenCoordinates(x, y);
const rayCaster = this.setupRaycaster(x, y);
const pickListener = new PickListener(parameters);

if (this.mapView.textElementsRenderer !== undefined) {
const { clientWidth, clientHeight } = this.mapView.canvas;
const screenX = worldPos.x * clientWidth * 0.5;
const screenY = worldPos.y * clientHeight * 0.5;
const screenX = ndc.x * clientWidth * 0.5;
const screenY = ndc.y * clientHeight * 0.5;
const scenePosition = new THREE.Vector2(screenX, screenY);
this.mapView.textElementsRenderer.pickTextElements(scenePosition, pickListener);
}
Expand Down Expand Up @@ -217,6 +223,25 @@ export class PickHandler {
return pickListener.results;
}

/**
* Returns a ray caster using the supplied screen positions.
*
* @param x - The X position in css/client coordinates (without applied display ratio).
* @param y - The Y position in css/client coordinates (without applied display ratio).
*
* @return Raycaster with origin at the camera and direction based on the supplied x / y screen
* points.
*/
raycasterFromScreenPoint(x: number, y: number): THREE.Raycaster {
this.m_pickingRaycaster.setFromCamera(
this.mapView.getNormalizedScreenCoordinates(x, y),
this.camera
);

this.mapView.renderer.getSize(this.m_pickingRaycaster.canvasSize);
return this.m_pickingRaycaster;
}

private createResult(intersection: THREE.Intersection): PickResult {
const pickResult: PickResult = {
type: PickObjectType.Unspecified,
Expand Down Expand Up @@ -350,4 +375,24 @@ export class PickHandler {
}
pickResult.userData = featureData.objInfos[objInfosIndex - 1];
}

private setupRaycaster(x: number, y: number): THREE.Raycaster {
const camera = this.mapView.camera;
const rayCaster = this.raycasterFromScreenPoint(x, y);

// A threshold must be set for picking of line and line segments, indicating the maximum
// distance in world units from the ray to a line to consider it as picked. Use the world
// units equivalent to one pixel at the furthest intersection (i.e. intersection with ground
// or far plane).
const furthestIntersection = this.mapView.getWorldPositionAt(x, y, true);
const furthestDistance =
camera.position.distanceTo(furthestIntersection) /
this.mapView.camera.getWorldDirection(tmpV3).dot(rayCaster.ray.direction);
rayCaster.params.Line!.threshold = MapViewUtils.calculateWorldSizeByFocalLength(
this.mapView.focalLength,
furthestDistance,
1
);
return rayCaster;
}
}
16 changes: 6 additions & 10 deletions @here/harp-mapview/lib/PickingRaycaster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,26 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { MapEnv } from "@here/harp-datasource-protocol";
import * as THREE from "three";

import { MapObjectAdapter } from "./MapObjectAdapter";

function intersectObject(
object: THREE.Object3D,
raycaster: PickingRaycaster,
env: MapEnv,
intersects: THREE.Intersection[],
recursive?: boolean
) {
if (object.layers.test(raycaster.layers) && object.visible) {
const mapObjectAdapter = MapObjectAdapter.get(object);
if (!mapObjectAdapter || mapObjectAdapter.isPickable(env)) {
if (!mapObjectAdapter || mapObjectAdapter.isPickable()) {
object.raycast(raycaster, intersects);
}
}

if (recursive === true) {
for (const child of object.children) {
intersectObject(child, raycaster, env, intersects, true);
intersectObject(child, raycaster, intersects, true);
}
}
}
Expand All @@ -41,11 +39,9 @@ export class PickingRaycaster extends THREE.Raycaster {
/**
* Constructor.
*
* @param width - the canvas width.
* @param height - the canvas height.
* @param m_env - the view enviroment.
* @param canvasSize - the canvas width and height.
*/
constructor(public width: number, public height: number, private readonly m_env: MapEnv) {
constructor(readonly canvasSize: THREE.Vector2) {
super();
}

Expand All @@ -58,7 +54,7 @@ export class PickingRaycaster extends THREE.Raycaster {
): THREE.Intersection[] {
const intersects: THREE.Intersection[] = optionalTarget ?? [];

intersectObject(object, this, this.m_env, intersects, recursive);
intersectObject(object, this, intersects, recursive);

return intersects;
}
Expand All @@ -73,7 +69,7 @@ export class PickingRaycaster extends THREE.Raycaster {
const intersects: THREE.Intersection[] = optionalTarget ?? [];

for (const object of objects) {
intersectObject(object, this, this.m_env, intersects, recursive);
intersectObject(object, this, intersects, recursive);
}

return intersects;
Expand Down
Loading