angular-3d-viewer
Version:
585 lines (490 loc) • 13.4 kB
JavaScript
import { Coord2D, CoordDistance2D, SubCoord2D } from '../geometry/coord2d.js';
import { CoordDistance3D, CrossVector3D, SubCoord3D, VectorAngle3D } from '../geometry/coord3d.js';
import { DegRad, IsGreater, IsLower, IsZero } from '../geometry/geometry.js';
import { ParabolicTweenFunction, TweenCoord3D } from '../geometry/tween.js';
import { CameraIsEqual3D } from './camera.js';
import { GetDomElementClientCoordinates } from './domutils.js';
export class MouseInteraction
{
constructor ()
{
this.prev = new Coord2D (0.0, 0.0);
this.curr = new Coord2D (0.0, 0.0);
this.diff = new Coord2D (0.0, 0.0);
this.buttons = [];
}
Down (canvas, ev)
{
this.buttons.push (ev.which);
this.curr = this.GetPositionFromEvent (canvas, ev);
this.prev = this.curr.Clone ();
}
Move (canvas, ev)
{
this.curr = this.GetPositionFromEvent (canvas, ev);
this.diff = SubCoord2D (this.curr, this.prev);
this.prev = this.curr.Clone ();
}
Up (canvas, ev)
{
let buttonIndex = this.buttons.indexOf (ev.which);
if (buttonIndex !== -1) {
this.buttons.splice (buttonIndex, 1);
}
this.curr = this.GetPositionFromEvent (canvas, ev);
}
Leave (canvas, ev)
{
this.buttons = [];
this.curr = this.GetPositionFromEvent (canvas, ev);
}
IsButtonDown ()
{
return this.buttons.length > 0;
}
GetButton ()
{
let length = this.buttons.length;
if (length === 0) {
return 0;
}
return this.buttons[length - 1];
}
GetPosition ()
{
return this.curr;
}
GetMoveDiff ()
{
return this.diff;
}
GetPositionFromEvent (canvas, ev)
{
return GetDomElementClientCoordinates (canvas, ev.clientX, ev.clientY);
}
}
export class TouchInteraction
{
constructor ()
{
this.prevPos = new Coord2D (0.0, 0.0);
this.currPos = new Coord2D (0.0, 0.0);
this.diffPos = new Coord2D (0.0, 0.0);
this.prevDist = 0.0;
this.currDist = 0.0;
this.diffDist = 0.0;
this.fingers = 0;
}
Start (canvas, ev)
{
if (ev.touches.length === 0) {
return;
}
this.fingers = ev.touches.length;
this.currPos = this.GetPositionFromEvent (canvas, ev);
this.prevPos = this.currPos.Clone ();
this.currDist = this.GetTouchDistanceFromEvent (canvas, ev);
this.prevDist = this.currDist;
}
Move (canvas, ev)
{
if (ev.touches.length === 0) {
return;
}
this.currPos = this.GetPositionFromEvent (canvas, ev);
this.diffPos = SubCoord2D (this.currPos, this.prevPos);
this.prevPos = this.currPos.Clone ();
this.currDist = this.GetTouchDistanceFromEvent (canvas, ev);
this.diffDist = this.currDist - this.prevDist;
this.prevDist = this.currDist;
}
End (canvas, ev)
{
if (ev.touches.length === 0) {
return;
}
this.fingers = 0;
this.currPos = this.GetPositionFromEvent (canvas, ev);
this.currDist = this.GetTouchDistanceFromEvent (canvas, ev);
}
IsFingerDown ()
{
return this.fingers !== 0;
}
GetFingerCount ()
{
return this.fingers;
}
GetPosition ()
{
return this.currPos;
}
GetMoveDiff ()
{
return this.diffPos;
}
GetDistanceDiff ()
{
return this.diffDist;
}
GetPositionFromEvent (canvas, ev)
{
let coord = null;
if (ev.touches.length !== 0) {
let touchEv = ev.touches[0];
coord = GetDomElementClientCoordinates (canvas, touchEv.pageX, touchEv.pageY);
}
return coord;
}
GetTouchDistanceFromEvent (canvas, ev)
{
if (ev.touches.length !== 2) {
return 0.0;
}
let touchEv1 = ev.touches[0];
let touchEv2 = ev.touches[1];
let distance = CoordDistance2D (
GetDomElementClientCoordinates (canvas, touchEv1.pageX, touchEv1.pageY),
GetDomElementClientCoordinates (canvas, touchEv2.pageX, touchEv2.pageY)
);
return distance;
}
}
export class ClickDetector
{
constructor ()
{
this.isClick = false;
this.startPosition = null;
}
Start (startPosition)
{
this.isClick = true;
this.startPosition = startPosition;
}
Move (currentPosition)
{
if (!this.isClick) {
return;
}
if (this.startPosition !== null) {
const maxClickDistance = 3.0;
const currentDistance = CoordDistance2D (this.startPosition, currentPosition);
if (currentDistance > maxClickDistance) {
this.Cancel ();
}
} else {
this.Cancel ();
}
}
End ()
{
this.startPosition = null;
}
Cancel ()
{
this.isClick = false;
this.startPosition = null;
}
IsClick ()
{
return this.isClick;
}
}
export const NavigationType =
{
None : 0,
Orbit : 1,
Pan : 2,
Zoom : 3
};
export class Navigation
{
constructor (canvas, camera, callbacks)
{
this.canvas = canvas;
this.camera = camera;
this.callbacks = callbacks;
this.fixUpVector = true;
this.mouse = new MouseInteraction ();
this.touch = new TouchInteraction ();
this.clickDetector = new ClickDetector ();
this.onMouseClick = null;
this.onMouseMove = null;
this.onContext = null;
if (this.canvas.addEventListener) {
this.canvas.addEventListener ('mousedown', this.OnMouseDown.bind (this));
this.canvas.addEventListener ('wheel', this.OnMouseWheel.bind (this));
this.canvas.addEventListener ('touchstart', this.OnTouchStart.bind (this));
this.canvas.addEventListener ('touchmove', this.OnTouchMove.bind (this));
this.canvas.addEventListener ('touchcancel', this.OnTouchEnd.bind (this));
this.canvas.addEventListener ('touchend', this.OnTouchEnd.bind (this));
this.canvas.addEventListener ('contextmenu', this.OnContextMenu.bind (this));
}
if (document.addEventListener) {
document.addEventListener ('mousemove', this.OnMouseMove.bind (this));
document.addEventListener ('mouseup', this.OnMouseUp.bind (this));
document.addEventListener ('mouseleave', this.OnMouseLeave.bind (this));
}
}
SetMouseClickHandler (onMouseClick)
{
this.onMouseClick = onMouseClick;
}
SetMouseMoveHandler (onMouseMove)
{
this.onMouseMove = onMouseMove;
}
SetContextMenuHandler (onContext)
{
this.onContext = onContext;
}
IsFixUpVector ()
{
return this.fixUpVector;
}
SetFixUpVector (isFixUpVector)
{
this.fixUpVector = isFixUpVector;
}
GetCamera ()
{
return this.camera;
}
SetCamera (camera)
{
this.camera = camera;
}
MoveCamera (newCamera, stepCount)
{
function Step (obj, steps, count, index)
{
obj.camera.eye = steps.eye[index];
obj.camera.center = steps.center[index];
obj.camera.up = steps.up[index];
obj.Update ();
if (index < count - 1) {
requestAnimationFrame (() => {
Step (obj, steps, count, index + 1);
});
}
}
if (newCamera === null) {
return;
}
if (stepCount === 0 || CameraIsEqual3D (this.camera, newCamera)) {
this.camera = newCamera;
} else {
let tweenFunc = ParabolicTweenFunction;
let steps = {
eye : TweenCoord3D (this.camera.eye, newCamera.eye, stepCount, tweenFunc),
center : TweenCoord3D (this.camera.center, newCamera.center, stepCount, tweenFunc),
up : TweenCoord3D (this.camera.up, newCamera.up, stepCount, tweenFunc)
};
requestAnimationFrame (() => {
Step (this, steps, stepCount, 0);
});
}
this.Update ();
}
GetFitToSphereCamera (center, radius)
{
if (IsZero (radius)) {
return null;
}
let fitCamera = this.camera.Clone ();
let offsetToOrigo = SubCoord3D (fitCamera.center, center);
fitCamera.eye = SubCoord3D (fitCamera.eye, offsetToOrigo);
fitCamera.center = center.Clone ();
let centerEyeDirection = SubCoord3D (fitCamera.eye, fitCamera.center).Normalize ();
let fieldOfView = this.camera.fov / 2.0;
if (this.canvas.width < this.canvas.height) {
fieldOfView = fieldOfView * this.canvas.width / this.canvas.height;
}
let distance = radius / Math.sin (fieldOfView * DegRad);
fitCamera.eye = fitCamera.center.Clone ().Offset (centerEyeDirection, distance);
return fitCamera;
}
OnMouseDown (ev)
{
ev.preventDefault ();
this.mouse.Down (this.canvas, ev);
this.clickDetector.Start (this.mouse.GetPosition ());
}
OnMouseMove (ev)
{
this.mouse.Move (this.canvas, ev);
this.clickDetector.Move (this.mouse.GetPosition ());
if (this.onMouseMove) {
let mouseCoords = GetDomElementClientCoordinates (this.canvas, ev.clientX, ev.clientY);
this.onMouseMove (mouseCoords);
}
if (!this.mouse.IsButtonDown ()) {
return;
}
let moveDiff = this.mouse.GetMoveDiff ();
let mouseButton = this.mouse.GetButton ();
let navigationType = NavigationType.None;
if (mouseButton === 1) {
if (ev.ctrlKey) {
navigationType = NavigationType.Zoom;
} else if (ev.shiftKey) {
navigationType = NavigationType.Pan;
} else {
navigationType = NavigationType.Orbit;
}
} else if (mouseButton === 2 || mouseButton === 3) {
navigationType = NavigationType.Pan;
}
if (navigationType === NavigationType.Orbit) {
let orbitRatio = 0.5;
this.Orbit (moveDiff.x * orbitRatio, moveDiff.y * orbitRatio);
} else if (navigationType === NavigationType.Pan) {
let eyeCenterDistance = CoordDistance3D (this.camera.eye, this.camera.center);
let panRatio = 0.001 * eyeCenterDistance;
this.Pan (moveDiff.x * panRatio, moveDiff.y * panRatio);
} else if (navigationType === NavigationType.Zoom) {
let zoomRatio = 0.005;
this.Zoom (-moveDiff.y * zoomRatio);
}
this.Update ();
}
OnMouseUp (ev)
{
this.mouse.Up (this.canvas, ev);
this.clickDetector.End ();
if (this.clickDetector.IsClick ()) {
let mouseCoords = this.mouse.GetPosition ();
this.Click (ev.which, mouseCoords);
}
}
OnMouseLeave (ev)
{
this.mouse.Leave (this.canvas, ev);
this.clickDetector.Cancel ();
}
OnTouchStart (ev)
{
ev.preventDefault ();
this.touch.Start (this.canvas, ev);
this.clickDetector.Start (this.touch.GetPosition ());
}
OnTouchMove (ev)
{
ev.preventDefault ();
this.touch.Move (this.canvas, ev);
this.clickDetector.Move (this.touch.GetPosition ());
if (!this.touch.IsFingerDown ()) {
return;
}
let moveDiff = this.touch.GetMoveDiff ();
let distanceDiff = this.touch.GetDistanceDiff ();
let fingerCount = this.touch.GetFingerCount ();
let navigationType = NavigationType.None;
if (fingerCount === 1) {
navigationType = NavigationType.Orbit;
} else if (fingerCount === 2) {
navigationType = NavigationType.Pan;
}
if (navigationType === NavigationType.Orbit) {
let orbitRatio = 0.5;
this.Orbit (moveDiff.x * orbitRatio, moveDiff.y * orbitRatio);
} else if (navigationType === NavigationType.Pan) {
let zoomRatio = 0.005;
this.Zoom (distanceDiff * zoomRatio);
let panRatio = 0.001 * CoordDistance3D (this.camera.eye, this.camera.center);
this.Pan (moveDiff.x * panRatio, moveDiff.y * panRatio);
}
this.Update ();
}
OnTouchEnd (ev)
{
ev.preventDefault ();
this.touch.End (this.canvas, ev);
this.clickDetector.End ();
if (this.clickDetector.IsClick ()) {
let touchCoords = this.touch.GetPosition ();
if (this.touch.GetFingerCount () === 1) {
this.Click (1, touchCoords);
}
}
}
OnMouseWheel (ev)
{
let params = ev || window.event;
params.preventDefault ();
let delta = -params.deltaY / 40;
let ratio = 0.1;
if (delta < 0) {
ratio = ratio * -1.0;
}
this.Zoom (ratio);
this.Update ();
}
OnContextMenu (ev)
{
ev.preventDefault ();
if (this.clickDetector.IsClick ()) {
this.Context (ev.clientX, ev.clientY);
this.clickDetector.Cancel ();
}
}
Orbit (angleX, angleY)
{
let radAngleX = angleX * DegRad;
let radAngleY = angleY * DegRad;
let viewDirection = SubCoord3D (this.camera.center, this.camera.eye).Normalize ();
let horizontalDirection = CrossVector3D (viewDirection, this.camera.up).Normalize ();
if (this.fixUpVector) {
let originalAngle = VectorAngle3D (viewDirection, this.camera.up);
let newAngle = originalAngle + radAngleY;
if (IsGreater (newAngle, 0.0) && IsLower (newAngle, Math.PI)) {
this.camera.eye.Rotate (horizontalDirection, -radAngleY, this.camera.center);
}
this.camera.eye.Rotate (this.camera.up, -radAngleX, this.camera.center);
} else {
let verticalDirection = CrossVector3D (horizontalDirection, viewDirection).Normalize ();
this.camera.eye.Rotate (horizontalDirection, -radAngleY, this.camera.center);
this.camera.eye.Rotate (verticalDirection, -radAngleX, this.camera.center);
this.camera.up = verticalDirection;
}
}
Pan (moveX, moveY)
{
let viewDirection = SubCoord3D (this.camera.center, this.camera.eye).Normalize ();
let horizontalDirection = CrossVector3D (viewDirection, this.camera.up).Normalize ();
let verticalDirection = CrossVector3D (horizontalDirection, viewDirection).Normalize ();
this.camera.eye.Offset (horizontalDirection, -moveX);
this.camera.center.Offset (horizontalDirection, -moveX);
this.camera.eye.Offset (verticalDirection, moveY);
this.camera.center.Offset (verticalDirection, moveY);
}
Zoom (ratio)
{
let direction = SubCoord3D (this.camera.center, this.camera.eye);
let distance = direction.Length ();
let move = distance * ratio;
this.camera.eye.Offset (direction, move);
}
Update ()
{
this.callbacks.onUpdate ();
}
Click (button, mouseCoords)
{
if (this.onMouseClick) {
this.onMouseClick (button, mouseCoords);
}
}
Context (clientX, clientY)
{
if (this.onContext) {
let globalCoords = {
x : clientX,
y : clientY
};
let localCoords = GetDomElementClientCoordinates (this.canvas, clientX, clientY);
this.onContext (globalCoords, localCoords);
}
}
}