@egjs/view3d
Version:
Fast & Customizable glTF 3D model viewer, packed with full of features!
185 lines (151 loc) • 5.17 kB
text/typescript
/*
* Copyright (c) 2020 NAVER Corp.
* egjs projects are licensed under the MIT license
*/
import * as THREE from "three";
import Model from "../../core/Model";
import Motion from "../../core/Motion";
import ARScene from "../../xr/ARScene";
import { AUTO } from "../../const/external";
import { clamp } from "../../utils";
import { XRRenderContext, XRInputs } from "../../type/xr";
import ARControl from "./ARControl";
import ScaleUI from "./ScaleUI";
import { WebARControlOptions } from "./WebARControl";
/**
* Options for {@link ARScaleControl}
* @interface
* @property {number} [min=0.05] Minimum scale, default is 0.05(5%)
* @property {number} [max=2.5] Maximum scale, default is 2.5(250%)
*/
export interface ARScaleControlOptions {
min: number;
max: number;
}
/**
* Model's scale controller which works on AR(WebXR) mode.
*/
class ARScaleControl implements ARControl {
// TODO: add option for "user scale"
// Internal states
private _motion: Motion;
private _enabled = false;
private _active = false;
private _prevCoordDistance = -1;
private _scaleMultiplier = 1;
private _ui = new ScaleUI();
/**
* Whether this control is enabled or not
* @readonly
*/
public get enabled() { return this._enabled; }
public get scale() { return this._scaleMultiplier; }
public get ui() { return this._ui; }
/**
* Range of the scale
* @readonly
*/
public get range() { return this._motion.range; }
/**
* Create new instance of ARScaleControl
* @param {ARScaleControlOptions} [options={}] Options
* @param {number} [options.min=0.05] Minimum scale, default is 0.05(5%)
* @param {number} [options.max=5] Maximum scale, default is 5(500%)
*/
public constructor({
min = 0.05,
max = 5
}: Partial<ARScaleControlOptions> = {}) {
this._motion = new Motion({ duration: 0, range: { min, max } });
this._motion.reset(1); // default scale is 1(100%)
this._ui = new ScaleUI();
}
public setInitialScale({
scene,
model,
floorPosition,
xrCam,
initialScale
}: {
scene: ARScene;
model: Model;
floorPosition: THREE.Vector3;
xrCam: THREE.PerspectiveCamera;
initialScale: WebARControlOptions["initialScale"];
}) {
const motion = this._motion;
const scaleRange = motion.range;
if (initialScale === AUTO) {
const camFov = 2 * Math.atan(1 / xrCam.projectionMatrix.elements[5]); // in radians
const aspectInv = xrCam.projectionMatrix.elements[0] / xrCam.projectionMatrix.elements[5]; // x/y
const camPos = xrCam.position;
const modelHeight = model.bbox.max.y - model.bbox.min.y;
const camToFloorDist = camPos.distanceTo(new THREE.Vector3().addVectors(floorPosition, new THREE.Vector3(0, modelHeight / 2, 0)));
const viewY = camToFloorDist * Math.tan(camFov / 2);
const viewX = viewY * aspectInv;
const modelBoundingSphere = model.bbox.getBoundingSphere(new THREE.Sphere());
const scaleY = viewY / modelBoundingSphere.radius;
const scaleX = viewX / modelBoundingSphere.radius;
const scale = clamp(Math.min(scaleX, scaleY), scaleRange.min, 1);
motion.reset(scale);
} else {
motion.reset(clamp(initialScale, scaleRange.min, scaleRange.max));
}
const scale = this._motion.val;
this._scaleMultiplier = scale;
scene.setModelScale(scale);
}
public setInitialPos(coords: THREE.Vector2[]) {
this._prevCoordDistance = new THREE.Vector2().subVectors(coords[0], coords[1]).length();
}
/**
* Enable this control
*/
public enable() {
this._enabled = true;
}
/**
* Disable this control
*/
public disable() {
this._enabled = false;
this.deactivate();
}
public activate(ctx: XRRenderContext) {
this._active = true;
this._ui.show();
this._updateUIPosition(ctx);
}
public deactivate() {
this._active = false;
this._ui.hide();
this._prevCoordDistance = -1;
}
public process(ctx: XRRenderContext, { coords }: XRInputs) {
if (coords.length !== 2 || !this._enabled || !this._active) return;
const motion = this._motion;
const distance = new THREE.Vector2().subVectors(coords[0], coords[1]).length();
const delta = (distance - this._prevCoordDistance);
motion.setEndDelta(delta);
this._prevCoordDistance = distance;
this._updateUIPosition(ctx);
}
public update({ scene }: XRRenderContext, deltaTime: number) {
if (!this._enabled || !this._active) return;
const motion = this._motion;
motion.update(deltaTime);
this._scaleMultiplier = motion.val;
this._ui.updateScale(this._scaleMultiplier);
scene.setModelScale(this._scaleMultiplier);
}
private _updateUIPosition({ view3D, scene, xrCam, vertical }: XRRenderContext) {
// Update UI
const model = view3D.model!;
const camPos = new THREE.Vector3().setFromMatrixPosition(xrCam.matrixWorld);
const modelHeight = vertical
? model.bbox.getBoundingSphere(new THREE.Sphere()).radius
: model.bbox.max.y - model.bbox.min.y;
this._ui.updatePosition(scene.root.quaternion, camPos, modelHeight);
}
}
export default ARScaleControl;