diff --git a/packages/model-viewer/src/features/annotation.ts b/packages/model-viewer/src/features/annotation.ts index 69b7f285c5..212335ff4b 100644 --- a/packages/model-viewer/src/features/annotation.ts +++ b/packages/model-viewer/src/features/annotation.ts @@ -16,7 +16,7 @@ import {Matrix3, Matrix4} from 'three'; -import ModelViewerElementBase, {$needsRender, $scene, $tick, toVector3D, Vector3D} from '../model-viewer-base.js'; +import ModelViewerElementBase, {$needsRender, $scene, $tick, toVector3D, Vector3D, toVector2D, Vector2D} from '../model-viewer-base.js'; import {Hotspot, HotspotConfiguration} from '../three-components/Hotspot.js'; import {Constructor} from '../utilities.js'; @@ -34,7 +34,7 @@ const worldToModelNormal = new Matrix3(); export declare interface AnnotationInterface { updateHotspot(config: HotspotConfiguration): void; positionAndNormalFromPoint(pixelX: number, pixelY: number): - {position: Vector3D, normal: Vector3D}|null + {position: Vector3D, normal: Vector3D, uv: Vector2D | null}|null } /** @@ -127,14 +127,15 @@ export const AnnotationMixin = >( } /** - * This method returns the model position and normal of the point on the - * mesh corresponding to the input pixel coordinates given relative to the - * model-viewer element. The position and normal are returned as strings in - * the format suitable for putting in a hotspot's data-position and - * data-normal attributes. If the mesh is not hit, the result is null. + * This method returns the model position, normal and texture coordinate + * of the point on the mesh corresponding to the input pixel coordinates + * given relative to the model-viewer element. The position and normal + * are returned as strings in the format suitable for putting in a + * hotspot's data-position and data-normal attributes. If the mesh is + * not hit, the result is null. */ positionAndNormalFromPoint(pixelX: number, pixelY: number): - {position: Vector3D, normal: Vector3D}|null { + {position: Vector3D, normal: Vector3D, uv: Vector2D | null}|null { const scene = this[$scene]; const ndcPosition = scene.getNDC(pixelX, pixelY); @@ -150,7 +151,12 @@ export const AnnotationMixin = >( const normal = toVector3D(hit.normal.applyNormalMatrix(worldToModelNormal)); - return {position: position, normal: normal}; + let uv = null; + if (hit.uv != null){ + uv = toVector2D(hit.uv); + } + + return {position: position, normal: normal, uv: uv}; } private[$addHotspot](node: Node) { diff --git a/packages/model-viewer/src/features/scene-graph.ts b/packages/model-viewer/src/features/scene-graph.ts index eea26d2f32..b740561ca7 100644 --- a/packages/model-viewer/src/features/scene-graph.ts +++ b/packages/model-viewer/src/features/scene-graph.ts @@ -59,7 +59,7 @@ export interface SceneGraphInterface { * objects were intersected. * @param pixelX X coordinate of the mouse. * @param pixelY Y coordinate of the mouse. - * @returns a material, if no intersection is made than null is returned. + * @returns a material, if no intersection is made then null is returned. */ materialFromPoint(pixelX: number, pixelY: number): Material|null; } diff --git a/packages/model-viewer/src/model-viewer-base.ts b/packages/model-viewer/src/model-viewer-base.ts index e062984e32..fa8c9f25f8 100644 --- a/packages/model-viewer/src/model-viewer-base.ts +++ b/packages/model-viewer/src/model-viewer-base.ts @@ -15,7 +15,7 @@ import {property} from 'lit-element'; import {UpdatingElement} from 'lit-element/lib/updating-element'; -import {Event as ThreeEvent, Vector3} from 'three'; +import {Event as ThreeEvent, Vector3, Vector2} from 'three'; import {HAS_INTERSECTION_OBSERVER, HAS_RESIZE_OBSERVER} from './constants.js'; import {makeTemplate} from './template.js'; @@ -83,6 +83,22 @@ export const toVector3D = (v: Vector3) => { }; }; +export interface Vector2D { + u: number + v: number + toString(): string +} + +export const toVector2D = (v: Vector2) => { + return { + u: v.x, + v: v.y, + toString() { + return `${this.u} ${this.v}`; + } + }; +}; + interface ToBlobOptions { mimeType?: string, qualityArgument?: number, idealAspect?: boolean } diff --git a/packages/model-viewer/src/test/features/annotation-spec.ts b/packages/model-viewer/src/test/features/annotation-spec.ts index 970364ac7e..bddcfee72c 100644 --- a/packages/model-viewer/src/test/features/annotation-spec.ts +++ b/packages/model-viewer/src/test/features/annotation-spec.ts @@ -16,7 +16,7 @@ import {Vector3} from 'three'; import {AnnotationInterface, AnnotationMixin} from '../../features/annotation'; -import ModelViewerElementBase, {$needsRender, $scene, Vector3D} from '../../model-viewer-base'; +import ModelViewerElementBase, {$needsRender, $scene, Vector2D, Vector3D} from '../../model-viewer-base'; import {Hotspot} from '../../three-components/Hotspot.js'; import {ModelScene} from '../../three-components/ModelScene'; import {timePasses, waitForEvent} from '../../utilities'; @@ -49,6 +49,11 @@ const closeToVector3 = (a: Vector3D, b: Vector3) => { expect(a.z).to.be.closeTo(b.z, delta); }; +const withinRange = (a: Vector2D, start: number, finish: number) => { + expect(a.u).to.be.within(start, finish); + expect(a.v).to.be.within(start, finish); +} + suite('ModelViewerElementBase with AnnotationMixin', () => { let nextId = 0; let tagName: string; @@ -205,9 +210,12 @@ suite('ModelViewerElementBase with AnnotationMixin', () => { const hitResult = element.positionAndNormalFromPoint(width / 2, height / 2); expect(hitResult).to.be.ok; - const {position, normal} = hitResult!; + const {position, normal, uv} = hitResult!; closeToVector3(position, new Vector3(0, 0, 0.5)); closeToVector3(normal, new Vector3(0, 0, 1)); + if(uv != null){ + withinRange(uv, 0, 1); + } }); test('gets expected hit result when turned', () => { @@ -216,9 +224,12 @@ suite('ModelViewerElementBase with AnnotationMixin', () => { const hitResult = element.positionAndNormalFromPoint(width / 2, height / 2); expect(hitResult).to.be.ok; - const {position, normal} = hitResult!; + const {position, normal, uv} = hitResult!; closeToVector3(position, new Vector3(0.5, 0, 0)); closeToVector3(normal, new Vector3(1, 0, 0)); + if(uv != null){ + withinRange(uv, 0, 1); + } }); }); }); \ No newline at end of file diff --git a/packages/model-viewer/src/three-components/ModelScene.ts b/packages/model-viewer/src/three-components/ModelScene.ts index 6ffe7332ea..eb85617a58 100644 --- a/packages/model-viewer/src/three-components/ModelScene.ts +++ b/packages/model-viewer/src/three-components/ModelScene.ts @@ -655,12 +655,13 @@ export class ModelScene extends Scene { } /** - * This method returns the world position and model-space normal of the point - * on the mesh corresponding to the input pixel coordinates given relative to - * the model-viewer element. If the mesh is not hit, the result is null. + * This method returns the world position, model-space normal and texture + * coordinate of the point on the mesh corresponding to the input pixel + * coordinates given relative to the model-viewer element. If the mesh + * is not hit, the result is null. */ positionAndNormalFromPoint(ndcPosition: Vector2, object: Object3D = this): - {position: Vector3, normal: Vector3}|null { + {position: Vector3, normal: Vector3, uv: Vector2 | null}|null { this.raycaster.setFromCamera(ndcPosition, this.getCamera()); const hits = this.raycaster.intersectObject(object, true); @@ -673,10 +674,14 @@ export class ModelScene extends Scene { return null; } + if (hit.uv == null) { + return {position: hit.point, normal: hit.face.normal, uv: null}; + } + hit.face.normal.applyNormalMatrix( new Matrix3().getNormalMatrix(hit.object.matrixWorld)); - return {position: hit.point, normal: hit.face.normal}; + return {position: hit.point, normal: hit.face.normal, uv: hit.uv}; } /** diff --git a/packages/modelviewer.dev/data/docs.json b/packages/modelviewer.dev/data/docs.json index 84b0bd2d86..797b0fe315 100644 --- a/packages/modelviewer.dev/data/docs.json +++ b/packages/modelviewer.dev/data/docs.json @@ -714,7 +714,7 @@ { "name": "positionAndNormalFromPoint(clientX, clientY)", "htmlName": "positionAndNormalFromPoint", - "description": "Returns the world position and normal of the point on the mesh corresponding to the input pixel coordinates given relative to the screen. The position and normal are returned as Vector3D, which has a method toString() that outputs a format suitable for putting in a hotspot's data-position and data-normal attributes. The function returns null if no object is hit.", + "description": "Returns the world position, normal and texture coordinate of the point on the mesh corresponding to the input pixel coordinates given relative to the screen. The position and normal are returned as Vector3D, which has a method toString() that outputs a format suitable for putting in a hotspot's data-position and data-normal attributes. The texture coordinate is returned as Vector2D, which has also a own toString() method. The function returns null if no object is hit.", "links": [ "positionAndNormalFromPoint example" ]