Skip to content

Commit

Permalink
Pan attribute (google#3286)
Browse files Browse the repository at this point in the history
* pan working

* recenter works

* cleanup

* use bounding sphere to limit pan

* added pan to editor

* changed fov defaults for pan

* deflake test

* simplified thor example

* added docs
  • Loading branch information
elalish authored Mar 18, 2022
1 parent 5900593 commit d12b6ca
Show file tree
Hide file tree
Showing 13 changed files with 290 additions and 349 deletions.
94 changes: 56 additions & 38 deletions packages/model-viewer/src/features/controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,11 @@ import {degreesToRadians, normalizeUnit} from '../styles/conversions.js';
import {EvaluatedStyle, Intrinsics, SphericalIntrinsics, StyleEvaluator, Vector3Intrinsics} from '../styles/evaluators.js';
import {IdentNode, NumberNode, numberNode, parseExpressions} from '../styles/parsers.js';
import {DECAY_MILLISECONDS} from '../three-components/Damper.js';
import {DEFAULT_FOV_DEG} from '../three-components/ModelScene.js';
import {ChangeEvent, ChangeSource, PointerChangeEvent, SmoothControls} from '../three-components/SmoothControls.js';
import {Constructor} from '../utilities.js';
import {timeline} from '../utilities/animation.js';



// NOTE(cdata): The following "animation" timing functions are deliberately
// being used in favor of CSS animations. In Safari 12.1 and 13, CSS animations
// would cause the interaction prompt to glitch unexpectedly
Expand All @@ -53,6 +51,11 @@ const fade = timeline(0, [
{frames: 6, value: 0}
]);

export const DEFAULT_FOV_DEG = 30;
export const OLD_DEFAULT_FOV_DEG = 45;
const DEFAULT_MIN_FOV_DEG = 12;
const OLD_DEFAULT_MIN_FOV_DEG = 25;

export const DEFAULT_CAMERA_ORBIT = '0deg 75deg 105%';
const DEFAULT_CAMERA_TARGET = 'auto auto auto';
const DEFAULT_FIELD_OF_VIEW = 'auto';
Expand Down Expand Up @@ -106,25 +109,22 @@ export const TouchAction: {[index: string]: TouchAction} = {
NONE: 'none'
};

export const fieldOfViewIntrinsics = () => {
return {
basis:
[degreesToRadians(numberNode(DEFAULT_FOV_DEG, 'deg')) as
NumberNode<'rad'>],
keywords: {auto: [null]}
};
};
export const fieldOfViewIntrinsics =
(element: ModelViewerElementBase&ControlsInterface) => {
const fov = element.enablePan ? DEFAULT_FOV_DEG : OLD_DEFAULT_FOV_DEG;

const minFieldOfViewIntrinsics = {
basis: [degreesToRadians(numberNode(25, 'deg')) as NumberNode<'rad'>],
keywords: {auto: [null]}
};
return {
basis: [degreesToRadians(numberNode(fov, 'deg')) as NumberNode<'rad'>],
keywords: {auto: [null]}
};
};

const minFieldOfViewIntrinsics = (element: ModelViewerElementBase&
ControlsInterface) => {
const fov = element.enablePan ? DEFAULT_MIN_FOV_DEG : OLD_DEFAULT_MIN_FOV_DEG;

const maxFieldOfViewIntrinsics = () => {
return {
basis:
[degreesToRadians(numberNode(DEFAULT_FOV_DEG, 'deg')) as
NumberNode<'rad'>],
basis: [degreesToRadians(numberNode(fov, 'deg')) as NumberNode<'rad'>],
keywords: {auto: [null]}
};
};
Expand All @@ -147,18 +147,20 @@ export const cameraOrbitIntrinsics = (() => {
};
})();

const minCameraOrbitIntrinsics = (element: ModelViewerElementBase) => {
const radius = MINIMUM_RADIUS_RATIO * element[$scene].boundingRadius;
const minCameraOrbitIntrinsics =
(element: ModelViewerElementBase&ControlsInterface) => {
const radius = MINIMUM_RADIUS_RATIO *
element[$scene].boundingSphere.radius * (element.enablePan ? 2 : 1);

return {
basis: [
numberNode(-Infinity, 'rad'),
numberNode(Math.PI / 8, 'rad'),
numberNode(radius, 'm')
],
keywords: {auto: [null, null, null]}
};
};
return {
basis: [
numberNode(-Infinity, 'rad'),
numberNode(Math.PI / 8, 'rad'),
numberNode(radius, 'm')
],
keywords: {auto: [null, null, null]}
};
};

const maxCameraOrbitIntrinsics = (element: ModelViewerElementBase) => {
const orbitIntrinsics = cameraOrbitIntrinsics(element);
Expand Down Expand Up @@ -195,6 +197,7 @@ const TAU = 2.0 * Math.PI;

export const $controls = Symbol('controls');
export const $promptElement = Symbol('promptElement');
export const $panElement = Symbol('panElement');
export const $promptAnimatedContainer = Symbol('promptAnimatedContainer');

const $deferInteractionPrompt = Symbol('deferInteractionPrompt');
Expand Down Expand Up @@ -243,6 +246,8 @@ export declare interface ControlsInterface {
touchAction: TouchAction;
bounds: Bounds;
interpolationDecay: number;
disableZoom: boolean;
enablePan: boolean;
getCameraOrbit(): SphericalPosition;
getCameraTarget(): Vector3D;
getFieldOfView(): number;
Expand Down Expand Up @@ -311,10 +316,8 @@ export const ControlsMixin = <T extends Constructor<ModelViewerElementBase>>(
{type: String, attribute: 'min-field-of-view', hasChanged: () => true})
minFieldOfView: string = 'auto';

@style({
intrinsics: maxFieldOfViewIntrinsics,
updateHandler: $syncMaxFieldOfView
})
@style(
{intrinsics: fieldOfViewIntrinsics, updateHandler: $syncMaxFieldOfView})
@property(
{type: String, attribute: 'max-field-of-view', hasChanged: () => true})
maxFieldOfView: string = 'auto';
Expand Down Expand Up @@ -342,6 +345,9 @@ export const ControlsMixin = <T extends Constructor<ModelViewerElementBase>>(
@property({type: Boolean, attribute: 'disable-zoom'})
disableZoom: boolean = false;

@property({type: Boolean, attribute: 'enable-pan'})
enablePan: boolean = false;

@property({type: Number, attribute: 'interpolation-decay'})
interpolationDecay: number = DECAY_MILLISECONDS;

Expand All @@ -352,6 +358,8 @@ export const ControlsMixin = <T extends Constructor<ModelViewerElementBase>>(
protected[$promptAnimatedContainer] =
this.shadowRoot!.querySelector(
'.interaction-prompt > .animated-container') as HTMLElement;
protected[$panElement] =
this.shadowRoot!.querySelector('.pan-target') as HTMLElement;

protected[$focusedTime] = Infinity;
protected[$lastPromptOffset] = 0;
Expand All @@ -360,7 +368,8 @@ export const ControlsMixin = <T extends Constructor<ModelViewerElementBase>>(
protected[$waitingToPromptUser] = false;

protected[$controls] = new SmoothControls(
this[$scene].camera as PerspectiveCamera, this[$userInputElement]);
this[$scene].camera as PerspectiveCamera, this[$userInputElement],
this[$scene]);

protected[$lastSpherical] = new Spherical();
protected[$jumpCamera] = false;
Expand Down Expand Up @@ -476,6 +485,13 @@ export const ControlsMixin = <T extends Constructor<ModelViewerElementBase>>(
controls.disableZoom = this.disableZoom;
}

if (changedProperties.has('enablePan')) {
controls.enablePan = this.enablePan;
this.oncontextmenu = this.enablePan ? function() {
return false;
} : null;
}

if (changedProperties.has('bounds')) {
this[$scene].tightBounds = this.bounds === 'tight';
}
Expand Down Expand Up @@ -662,7 +678,8 @@ export const ControlsMixin = <T extends Constructor<ModelViewerElementBase>>(
* orbiting at the supplied radius.
*/
[$updateCameraForRadius](radius: number) {
const maximumRadius = Math.max(this[$scene].boundingRadius, radius);
const maximumRadius =
Math.max(this[$scene].boundingSphere.radius, radius);

const near = 0;
const far = 2 * maximumRadius;
Expand Down Expand Up @@ -700,13 +717,14 @@ export const ControlsMixin = <T extends Constructor<ModelViewerElementBase>>(
// compare the before and after to calculate the proper zoom.
super[$onResize](event);

const newFramedFoV = scene.adjustedFoV(scene.framedFoVDeg);
const zoom = controls.getFieldOfView() / oldFramedFoV;
const fovRatio = scene.adjustedFoV(scene.framedFoVDeg) / oldFramedFoV;
const fov =
controls.getFieldOfView() * (isFinite(fovRatio) ? fovRatio : 1);

controls.updateAspect(this[$scene].aspect);

await this.requestUpdate('maxFieldOfView', this.maxFieldOfView);
this[$controls].setFieldOfView(newFramedFoV * zoom);
this[$controls].setFieldOfView(fov);

this.jumpCameraToGoal();
}
Expand Down
28 changes: 28 additions & 0 deletions packages/model-viewer/src/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,27 @@ canvas.show {
transform-origin: bottom right;
}
.slot.pan-target {
display: block;
position: absolute;
width: 0;
height: 0;
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
background-color: transparent;
opacity: 0;
transition: opacity 0.3s;
}
#default-pan-target {
width: 6px;
height: 6px;
border-radius: 6px;
border: 1px solid white;
box-shadow: 0px 0px 2px 1px rgba(0, 0, 0, 0.8);
}
.slot.default {
pointer-events: none;
}
Expand Down Expand Up @@ -318,6 +339,13 @@ canvas.show {
</slot>
</div>
<div class="slot pan-target">
<slot name="pan-target">
<div id="default-pan-target">
</div>
</slot>
</div>
<div class="slot interaction-prompt">
<div class="animated-container">
<slot name="interaction-prompt" aria-hidden="true">
Expand Down
5 changes: 2 additions & 3 deletions packages/model-viewer/src/test/features/controls-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@

import {Camera, Vector3} from 'three';

import {$controls, $promptAnimatedContainer, $promptElement, CameraChangeDetails, cameraOrbitIntrinsics, ControlsInterface, ControlsMixin, INTERACTION_PROMPT, SphericalPosition} from '../../features/controls.js';
import {$controls, $promptAnimatedContainer, $promptElement, CameraChangeDetails, cameraOrbitIntrinsics, ControlsInterface, ControlsMixin, INTERACTION_PROMPT, OLD_DEFAULT_FOV_DEG, SphericalPosition} from '../../features/controls.js';
import ModelViewerElementBase, {$canvas, $scene, $statusElement, $userInputElement, Vector3D} from '../../model-viewer-base.js';
import {StyleEvaluator} from '../../styles/evaluators.js';
import {DEFAULT_FOV_DEG} from '../../three-components/ModelScene.js';
import {ChangeSource, SmoothControls} from '../../three-components/SmoothControls.js';
import {Constructor, step, timePasses, waitForEvent} from '../../utilities.js';
import {assetPath, dispatchSyntheticEvent, rafPasses, until} from '../helpers.js';
Expand Down Expand Up @@ -325,7 +324,7 @@ suite('ModelViewerElementBase with ControlsMixin', () => {
await timePasses();
settleControls(controls);
expect(element.getFieldOfView())
.to.be.closeTo(DEFAULT_FOV_DEG, 0.001);
.to.be.closeTo(OLD_DEFAULT_FOV_DEG, 0.001);
});

test('jumps to maxCameraOrbit when outside', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import {$correlatedObjects} from '../../../features/scene-graph/three-dom-elemen
import {$scene} from '../../../model-viewer-base.js';
import {ModelViewerElement} from '../../../model-viewer.js';
import {CorrelatedSceneGraph} from '../../../three-components/gltf-instance/correlated-scene-graph.js';
import {timePasses, waitForEvent} from '../../../utilities.js';
import {assetPath, loadThreeGLTF} from '../../helpers.js';
import {waitForEvent} from '../../../utilities.js';
import {assetPath, loadThreeGLTF, rafPasses} from '../../helpers.js';



Expand Down Expand Up @@ -319,6 +319,8 @@ suite('scene-graph/model', () => {
test('materialFromPoint returns material', async () => {
await loadModel(ASTRONAUT_GLB_PATH);

await rafPasses();

const material = element.materialFromPoint(
element[$scene].width / 2, element[$scene].height / 2)!;

Expand All @@ -329,7 +331,7 @@ suite('scene-graph/model', () => {
'materialFromPoint returns null when intersect fails', async () => {
await loadModel(ASTRONAUT_GLB_PATH);

await timePasses(1000);
await rafPasses();

const material = element.materialFromPoint(
element[$scene].width, element[$scene].height)!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import {Matrix4, Mesh, SphereBufferGeometry, Vector3} from 'three';

import ModelViewerElementBase, {$canvas} from '../../model-viewer-base.js';
import {DEFAULT_FOV_DEG, ModelScene} from '../../three-components/ModelScene.js';
import {ModelScene} from '../../three-components/ModelScene.js';
import {assetPath} from '../helpers.js';


Expand Down Expand Up @@ -103,13 +103,15 @@ suite('ModelScene', () => {
test('idealCameraDistance is set correctly', async () => {
await scene.setObject(dummyMesh);

const halfFov = (DEFAULT_FOV_DEG / 2) * Math.PI / 180;
scene.framedFoVDeg = 35;
const halfFov = (scene.framedFoVDeg / 2) * Math.PI / 180;
const expectedDistance = dummyRadius / Math.sin(halfFov);
expect(scene.idealCameraDistance())
.to.be.closeTo(expectedDistance, 0.0001);
});

test('idealAspect is set correctly', async () => {
scene.framedFoVDeg = 35;
await scene.setObject(dummyMesh);

expect(scene.idealAspect).to.be.closeTo(1, 0.0001);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@

import {PerspectiveCamera, Vector3} from 'three';

import {$controls} from '../../features/controls.js';
import {$userInputElement} from '../../model-viewer-base.js';
import {ModelViewerElement} from '../../model-viewer.js';
import {ChangeSource, DEFAULT_OPTIONS, KeyCode, SmoothControls} from '../../three-components/SmoothControls.js';
import {waitForEvent} from '../../utilities.js';
import {dispatchSyntheticEvent} from '../helpers.js';
Expand All @@ -40,23 +43,25 @@ export const settleControls = (controls: SmoothControls) =>
suite('SmoothControls', () => {
let controls: SmoothControls;
let camera: PerspectiveCamera;
let modelViewer: ModelViewerElement;
let element: HTMLDivElement;

setup(() => {
element = document.createElement<'div'>('div');
camera = new PerspectiveCamera();
controls = new SmoothControls(camera, element);
modelViewer = new ModelViewerElement();
element = modelViewer[$userInputElement];
controls = (modelViewer as any)[$controls];
camera = controls.camera;

element.style.height = '100px';
modelViewer.style.height = '100px';
element.tabIndex = 0;

document.body.insertBefore(element, document.body.firstChild);
document.body.insertBefore(modelViewer, document.body.firstChild);

controls.enableInteraction();
});

teardown(() => {
document.body.removeChild(element);
document.body.removeChild(modelViewer);

controls.disableInteraction();
});
Expand Down
3 changes: 2 additions & 1 deletion packages/model-viewer/src/three-components/ARRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,8 @@ export class ARRenderer extends EventDispatcher {

private moveScene(delta: number) {
const scene = this.presentedScene!;
const {position, yaw, boundingRadius} = scene;
const {position, yaw} = scene;
const boundingRadius = scene.boundingSphere.radius;
const goal = this.goalPosition;
const oldScale = scene.scale.x;
const box = this.placementBox!;
Expand Down
Loading

0 comments on commit d12b6ca

Please sign in to comment.