Skip to content

Commit

Permalink
Camera target control (google#733)
Browse files Browse the repository at this point in the history
* partly there

* camera-target working

* fixed shadow

* cleanup

* made target interpolate

* fixed tests

* added target test

* updating CI test rules

* fixed comparison

* added loadedTime

* re-enabled skipped test

* removed extra wait

* addressing feedback

* adding auto

* looks like 584 is still flakey afterall

* another deflake attempt

* added camera-target doc
  • Loading branch information
elalish authored Sep 10, 2019
1 parent 69c545a commit ed84f55
Show file tree
Hide file tree
Showing 16 changed files with 241 additions and 369 deletions.
8 changes: 4 additions & 4 deletions examples/staging-and-camera-control.html
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,12 @@ <h4></h4>
<div class="content">
<div class="wrapper">
<div class="heading">
<h2 class="demo-title">By default, models are aligned by the center of their bounding box</h2>
<h2 class="demo-title">By default, the camera targets the center of the model's bounding box</h2>
<h4></h4>
</div>
<example-snippet stamp-to="demo-container-4" highlight-as="html">
<template>
<model-viewer camera-controls src="assets/odd-shape-labeled.glb" background-color="#eee" alt="An abstract 3D model with labeled origin and center"></model-viewer>
<model-viewer camera-controls src="assets/odd-shape-labeled.glb" background-color="#eee" alt="An abstract 3D model with labeled origin and center" shadow-intensity="1"></model-viewer>
</template>
</example-snippet>
</div>
Expand All @@ -140,12 +140,12 @@ <h4></h4>
<div class="content">
<div class="wrapper">
<div class="heading">
<h2 class="demo-title">Use the <span class="attribute">align-model</span> attribute to center the model at its origin</h2>
<h2 class="demo-title">Use the <span class="attribute">camera-target</span> attribute to orbit a different coordinate</h2>
<h4></h4>
</div>
<example-snippet stamp-to="demo-container-5" highlight-as="html">
<template>
<model-viewer camera-controls align-model="origin" src="assets/odd-shape-labeled.glb" background-color="#eee" alt="An abstract 3D model with labeled origin and center"></model-viewer>
<model-viewer camera-controls camera-target="0m 0m 0m" src="assets/odd-shape-labeled.glb" background-color="#eee" alt="An abstract 3D model with labeled origin and center" shadow-intensity="1"></model-viewer>
</template>
</example-snippet>
</div>
Expand Down
14 changes: 13 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,19 @@ <h3 class="grouping-title">Attributes</h3>
changes from its initially configured value, the camera will
interpolate from its current position to the new value. Defaults to
"0deg 75deg auto".</p>
</li><li>
</li>
<li>
<div>camera-target</div>
<p>Set the starting and/or subsequent point the camera orbits
around. Accepts values of the form "$X $Y $Z", like "0m 1.5m
-0.5m". Also supports units in centimeters ("cm") or millimeters
("mm"). A special value "auto" can be used, which sets the target
to the center of the model's bounding box in that dimension. Any
time this value changes from its initially configured value, the
camera will interpolate from its current position to the new
value. Defaults to "auto auto auto".</p>
</li>
<li>
<div>environment-image</div>
<p>Controls the environmental reflection of the model. Normally if
background-image is set, that image will also be used for the
Expand Down
2 changes: 1 addition & 1 deletion scripts/ci-run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ if [ "${TEST_TYPE}" = "unit" ]; then

xvfb-run npm run test

if [ "${TRAVIS_PULL_REQUEST}" = "false" ] && [ "${TRAVIS_BRANCH}" != "master" ]; then
if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then
./scripts/run-sauce-tests.sh;
fi
fi
Expand Down
2 changes: 1 addition & 1 deletion scripts/run-fidelity-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# limitations under the License.
#

if [ "${TEST_TYPE}" = "fidelity" ] || [ "${TRAVIS_BRANCH}" = "master" ]; then
if [ "${TEST_TYPE}" = "fidelity" ] || [ "${TRAVIS_PULL_REQUEST_BRANCH}" = "master" ]; then
npm run build
npm run check-fidelity
fi
32 changes: 32 additions & 0 deletions src/conversions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

import {Math as ThreeMath} from 'three';

import {parseValues, ValueNode} from './parsers.js';


Expand Down Expand Up @@ -106,6 +107,37 @@ export const deserializeSpherical =
return null;
};

/**
* Vector String => Vector Values
*
* Converts a "vector string" to 3 values, either numbers in meters or the
* string 'auto'. Position strings are of the form "$x $y $z". Accepted units
* include meters (m), centimeters (cm) and millimeters (mm).
*
* Returns null if the vector string cannot be parsed.
*/
export const deserializeVector3 =
(vectorString: string): (number|string)[]|null => {
try {
const vectorValueNodes = parseValues(vectorString);

const xyz = [];
if (vectorValueNodes.length === 3) {
for (let i = 0; i < 3; i++) {
xyz.push(
vectorValueNodes[i].value === 'auto' ?
'auto' :
lengthValueNodeToMeters(vectorValueNodes[i]));
}

return xyz;
}
} catch (_error) {
}

return null;
};

export const deserializeAngleToDeg = (angleString: string): number|null => {
try {
const angleValueNode = parseValues(angleString);
Expand Down
58 changes: 39 additions & 19 deletions src/features/controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
*/

import {property} from 'lit-element';
import {Event, Spherical} from 'three';
import {Event, Spherical, Vector3} from 'three';

import {deserializeAngleToDeg, deserializeSpherical} from '../conversions.js';
import ModelViewerElementBase, {$ariaLabel, $needsRender, $onModelLoad, $onResize, $scene, $tick} from '../model-viewer-base.js';
import {deserializeAngleToDeg, deserializeSpherical, deserializeVector3} from '../conversions.js';
import ModelViewerElementBase, {$ariaLabel, $loadedTime, $needsRender, $onModelLoad, $onResize, $scene, $tick} from '../model-viewer-base.js';
import {ChangeEvent, ChangeSource, SmoothControls} from '../three-components/SmoothControls.js';
import {Constructor} from '../utilities.js';

Expand Down Expand Up @@ -47,6 +47,7 @@ const InteractionPolicy: {[index: string]: InteractionPolicy} = {
};

export const DEFAULT_CAMERA_ORBIT = '0deg 75deg auto';
const DEFAULT_CAMERA_TARGET = 'auto auto auto';
const DEFAULT_FIELD_OF_VIEW = '45deg';

const HALF_PI = Math.PI / 2.0;
Expand All @@ -69,6 +70,7 @@ const $deferInteractionPrompt = Symbol('deferInteractionPrompt');
const $updateAria = Symbol('updateAria');
const $updateCamera = Symbol('updateCamera');
const $updateCameraOrbit = Symbol('updateCameraOrbit');
const $updateCameraTarget = Symbol('updateCameraTarget');
const $updateFieldOfView = Symbol('updateFieldOfView');

const $blurHandler = Symbol('blurHandler');
Expand All @@ -84,19 +86,20 @@ const $onPromptTransitionend = Symbol('onPromptTransitionend');
const $shouldPromptUserToInteract = Symbol('shouldPromptUserToInteract');
const $waitingToPromptUser = Symbol('waitingToPromptUser');
const $userPromptedOnce = Symbol('userPromptedOnce');
const $idleTime = Symbol('idleTime');

const $lastSpherical = Symbol('lastSpherical');
const $jumpCamera = Symbol('jumpCamera');

export interface ControlsInterface {
cameraControls: boolean;
cameraOrbit: string;
cameraTarget: string;
fieldOfView: string;
interactionPrompt: InteractionPromptStrategy;
interactionPolicy: InteractionPolicy;
interactionPromptThreshold: number;
getCameraOrbit(): SphericalPosition;
getCameraTarget(): Vector3;
getFieldOfView(): number;
jumpCameraToGoal(): void;
}
Expand All @@ -112,6 +115,10 @@ export const ControlsMixin = (ModelViewerElement:
{type: String, attribute: 'camera-orbit', hasChanged: () => true})
cameraOrbit: string = DEFAULT_CAMERA_ORBIT;

@property(
{type: String, attribute: 'camera-target', hasChanged: () => true})
cameraTarget: string = DEFAULT_CAMERA_TARGET;

@property(
{type: String, attribute: 'field-of-view', hasChanged: () => true})
fieldOfView: string = DEFAULT_FIELD_OF_VIEW;
Expand All @@ -129,7 +136,6 @@ export const ControlsMixin = (ModelViewerElement:

protected[$promptElement]: Element;

protected[$idleTime] = 0;
protected[$userPromptedOnce] = false;
protected[$waitingToPromptUser] = false;
protected[$shouldPromptUserToInteract] = true;
Expand Down Expand Up @@ -166,6 +172,10 @@ export const ControlsMixin = (ModelViewerElement:
return {theta, phi, radius};
}

getCameraTarget(): Vector3 {
return this[$controls].getTarget();
}

getFieldOfView(): number {
return this[$controls].getFieldOfView();
}
Expand Down Expand Up @@ -227,6 +237,10 @@ export const ControlsMixin = (ModelViewerElement:
this[$updateCameraOrbit]();
}

if (changedProperties.has('cameraTarget')) {
this[$updateCameraTarget]();
}

if (changedProperties.has('fieldOfView')) {
this[$updateFieldOfView]();
}
Expand Down Expand Up @@ -265,16 +279,28 @@ export const ControlsMixin = (ModelViewerElement:
this[$controls].setOrbit(theta, phi, radius as number);
}

[$updateCameraTarget]() {
const targetValues = deserializeVector3(this.cameraTarget);
let target = this[$scene].model.boundingBox.getCenter(new Vector3);

if (targetValues != null) {
for (let i = 0; i < 3; i++) {
if (targetValues[i] !== 'auto') {
target.setComponent(i, targetValues[i] as number);
}
}
}

this[$controls].setTarget(target);
}

[$tick](time: number, delta: number) {
super[$tick](time, delta);

if (this[$waitingToPromptUser] &&
this.interactionPrompt !== InteractionPromptStrategy.NONE) {
if (this.loaded) {
this[$idleTime] += delta;
}

if (this[$idleTime] > this.interactionPromptThreshold) {
if (this.loaded &&
time > this[$loadedTime] + this.interactionPromptThreshold) {
(this as any)[$scene].canvas.setAttribute(
'aria-label', INTERACTION_PROMPT);

Expand All @@ -296,7 +322,7 @@ export const ControlsMixin = (ModelViewerElement:
[$deferInteractionPrompt]() {
// Effectively cancel the timer waiting for user interaction:
this[$waitingToPromptUser] = false;
this[$promptElement]!.classList.remove('visible');
this[$promptElement].classList.remove('visible');

// Implicitly there was some reason to defer the prompt. If the user
// has been prompted at least once already, we no longer need to
Expand Down Expand Up @@ -341,7 +367,6 @@ export const ControlsMixin = (ModelViewerElement:
controls.applyOptions({minimumRadius, maximumRadius});

controls.setRadius(zoom * this[$idealCameraDistance]!);
controls.setTarget(scene.target);
controls.jumpToGoal();
}

Expand Down Expand Up @@ -409,6 +434,7 @@ export const ControlsMixin = (ModelViewerElement:
super[$onModelLoad](event);
this[$updateCamera]();
this[$updateCameraOrbit]();
this[$updateCameraTarget]();
this[$controls].jumpToGoal();
}

Expand All @@ -432,7 +458,6 @@ export const ControlsMixin = (ModelViewerElement:
// prompt threshold:
if (this[$shouldPromptUserToInteract]) {
this[$waitingToPromptUser] = true;
this[$idleTime] = 0;
}
}

Expand All @@ -442,15 +467,10 @@ export const ControlsMixin = (ModelViewerElement:
}

[$onChange]({source}: ChangeEvent) {
if (this.interactionPrompt ===
InteractionPromptStrategy.WHEN_FOCUSED) {
this[$deferInteractionPrompt]();
}
this[$updateAria]();
this[$needsRender]();

if (source === ChangeSource.USER_INTERACTION &&
this.interactionPrompt === InteractionPromptStrategy.AUTO) {
if (source === ChangeSource.USER_INTERACTION) {
this[$deferInteractionPrompt]();
}

Expand Down
Loading

0 comments on commit ed84f55

Please sign in to comment.