/* This file is part of the source code for 3D-XplorMath-J, Version 1.0 (January 2008). * Copyright (c) 2008 The 3D-XplorMath Consortium (http://3d-xplormath.org). * This source code is released under a BSD License, which allows redistribution * in source and binary form, with or without modification, provided copyright * and license information are included, and with no warranty or guarantee of * any kind. For details, see http://3d-xplormath.org/j/source/BSDLicense.txt */ package vmm.core3D; import java.awt.Cursor; import java.awt.Rectangle; import java.awt.event.MouseEvent; import java.awt.geom.Point2D; import vmm.core.Display; import vmm.core.MouseTask; import vmm.core.TimerAnimation; import vmm.core.View; /** * A MouseTask that is designed to handle the most common cases of interaction with a three-dimensional * Exhibit. Clicking and dragging with the left mouse button on a 3D Exhibit will do trackball-style rotation * of the Exhibit. Using the middle mouse button, or the left mouse button with the Shift or ALT/Option key, will * zoom the Exhibit as the mouse is dragged up and down. Using the right mouse button, or the left mouse button * with the Control or Meta/Command key, will drag the Exhibit. *
"Dragging" the exhibit has a special meaning when
* the exhibit is displayed in dual, sterographic mode. In that case, dragging the mouse horizontally will slide
* the two views of the exhibit closer together and farther apart. This is to allow the user to adjust the
* distance to make it easier to fuse the two views into one 3D view.
*/
public class BasicMouseTask3D extends MouseTask {
private int operation;
private static final int NONE = 0, DRAG = 1, ZOOM = 2, ROTATE = 3, MOVE_NORMAL_TO_SCREEN = 4;
private int startx, starty;
private int prevx, prevy;
private double[] startWindow;
private double requestedAspectRatio;
private Transform3D transform;
private double virtualSphereRadius;
private Vector3D previousRay, currentRay;
private long lastDragTime;
private long mouseDownTime;
private boolean startedDragging;
private boolean saveFastDrawing;
private boolean isStereographView; // set to true if the View is a View3D that is set up for Stereograph or Cross-Eyed Stereograph viewing.
// In this case, mouse input has to be interpreted differently.
private Rectangle stereographLeftEyeRect, stereographRightEyeRect;
private boolean inLeftEyeRect;
private Transform3D stereographTransform;
private double zoom_startx_real, zoom_starty_real; // window coords for mouse start postion for zoom operation.
private double zoom_startx_pixel, zoom_starty_pixel; // pixel coords for mouse start position for zoom, adjusted to fill window size for sterographic view
private double originalObjectDisplacementNormalToScreen; // For the MOVE_NORMAL_TO_SCREEN operation
/**
* Decides which operation to perform, if any. For stereographic views, a ZOOM or ROTATE operation is begun
* only if the mouse click is actually inside one of the two view rectangles.
*/
public boolean doMouseDown(MouseEvent evt, Display display, View view, int width, int height) {
operation = NONE;
if (view.getExhibit() == null)
return false;
try {
startWindow = view.getWindow();
transform = (Transform3D)view.getTransform();
requestedAspectRatio = Math.abs((transform.getYmaxRequested() - transform.getYminRequested())
/ (transform.getXmaxRequested() - transform.getXminRequested()));
}
catch (Exception e) {
return false;
}
if ( (view instanceof View3D) && ((View3D)view).getViewStyle() == View3D.RED_GREEN_STEREO_VIEW
&& evt.isAltDown() && evt.isShiftDown() )
operation = MOVE_NORMAL_TO_SCREEN;
else if (evt.isAltDown() || evt.isShiftDown())
operation = ZOOM;
else if (evt.isControlDown() || evt.isMetaDown())
operation = DRAG;
else
operation = ROTATE;
startx = prevx = evt.getX();
starty = prevy = evt.getY();
isStereographView = (view instanceof View3D) &&
( ((View3D)view).getViewStyle() == View3D.STEREOGRAPH_VIEW ||
((View3D)view).getViewStyle() == View3D.CROSS_EYE_STEREO_VIEW );
if (isStereographView && ! (view.getTransform() instanceof Transform3D) )
return false; // shouldn't happen, if things are done in the ordinary way.
if (isStereographView && operation != DRAG) {
stereographLeftEyeRect = ((View3D)view).stereographLeftEyeRect();
stereographRightEyeRect = ((View3D)view).stereographRightEyeRect();
if (stereographLeftEyeRect.contains(startx, starty)) {
startx = prevx = startx - stereographLeftEyeRect.x;
starty = prevy = starty - stereographLeftEyeRect.y;
inLeftEyeRect = true;
}
else if (stereographRightEyeRect.contains(startx, starty)) {
startx = prevx = startx - stereographRightEyeRect.x;
starty = prevy = starty - stereographRightEyeRect.y;
inLeftEyeRect = false;
}
else
return false; // for sterographic views, ignore clicks outside the viewing rectangles.
stereographTransform = (Transform3D)view.getTransform().clone();
stereographTransform.setUpDrawInfo(null,0,0,stereographLeftEyeRect.width,stereographLeftEyeRect.height,
view.getPreserveAspect(),view.getApplyGraphics2DTransform());
}
if (operation == ZOOM) {
operation = ZOOM;
if (isStereographView) {
zoom_startx_real = stereographTransform.getXmin() + startx * ((stereographTransform.getXmax() - stereographTransform.getXmin()) / stereographTransform.getWidth());
zoom_starty_real = stereographTransform.getYmin() + (stereographTransform.getHeight() - starty) * ((stereographTransform.getYmax() - stereographTransform.getYmin()) / stereographTransform.getHeight());
Point2D pt = new Point2D.Double(startx,starty);
stereographTransform.viewportToWindow(pt);
transform.windowToViewport(pt);
zoom_startx_pixel = pt.getX();
zoom_starty_pixel = pt.getY();
}
else {
zoom_startx_real = startWindow[0] + startx * ((startWindow[1] - startWindow[0]) / width);
zoom_starty_real = startWindow[2] + (height-starty) * ((startWindow[3] - startWindow[2]) / height);
zoom_startx_pixel = startx;
zoom_starty_pixel = starty;
}
}
else if (operation == ROTATE) {
if (isStereographView) {
virtualSphereRadius = 0.48 * Math.min( Math.abs(stereographTransform.getXmax() - stereographTransform.getXmin()),
Math.abs(stereographTransform.getYmax() - stereographTransform.getYmin()) );
}
else {
virtualSphereRadius = 0.48 * Math.min( Math.abs(transform.getXmax() - transform.getXmin()),
Math.abs(transform.getYmax() - transform.getYmin()) );
}
operation = ROTATE;
}
else if (operation == MOVE_NORMAL_TO_SCREEN)
originalObjectDisplacementNormalToScreen = ((Transform3D)view.getTransform()).getObjectDisplacementNormalToScreen();
saveFastDrawing = view.getFastDrawing();
mouseDownTime = evt.getWhen();
startedDragging = false; // won't actualy start until 1/3 second has passed or user moves mouse
return true;
}
/**
* Continue an operation that was begun in the doMouseDown method. The user can change the
* operation in the middle of a mouse drag by pressing and releasing modifier keys.
*/
public void doMouseDrag(MouseEvent evt, Display display, View view, int width, int height) {
if (operation == NONE)
return;
if (!startedDragging && evt.getWhen() - mouseDownTime < 300
&& Math.abs(evt.getX() - startx) < 3 && Math.abs(evt.getY() - starty) < 3)
return;
if (!startedDragging) {
startedDragging = true;
view.setFastDrawing(true);
}
if (operation == MOVE_NORMAL_TO_SCREEN) {
double extent = Math.max(transform.getXmax() - transform.getXmin(),
transform.getYmax() - transform.getYmin());
double change = (double)(evt.getY() - starty) / (height/2) * extent;
transform.setObjectDisplacementNormalToScreen(originalObjectDisplacementNormalToScreen + change);
return;
}
int newoperation;
if (evt.isAltDown() || evt.isShiftDown())
newoperation = ZOOM;
else if (evt.isControlDown() || evt.isMetaDown())
newoperation = DRAG;
else
newoperation = ROTATE;
if (newoperation != operation) {
operation = newoperation;
startx = prevx = evt.getX();
starty = prevy = evt.getY();
if (isStereographView && operation != DRAG) {
if (inLeftEyeRect) {
startx = prevx = startx - stereographLeftEyeRect.x;
starty = prevy = starty - stereographLeftEyeRect.y;
}
else {
startx = prevx = startx - stereographRightEyeRect.x;
starty = prevy = starty - stereographRightEyeRect.y;
}
stereographTransform = (Transform3D)view.getTransform().clone();
stereographTransform.setUpDrawInfo(null,0,0,stereographLeftEyeRect.width,stereographLeftEyeRect.height,
view.getPreserveAspect(),view.getApplyGraphics2DTransform());
}
startWindow = view.getWindow();
if (newoperation == ZOOM) {
if (isStereographView) {
zoom_startx_real = stereographTransform.getXmin() + startx * ((stereographTransform.getXmax() - stereographTransform.getXmin()) / stereographTransform.getWidth());
zoom_starty_real = stereographTransform.getYmin() + (stereographTransform.getHeight() - starty) * ((stereographTransform.getYmax() - stereographTransform.getYmin()) / stereographTransform.getHeight());
Point2D pt = new Point2D.Double(startx,starty);
stereographTransform.viewportToWindow(pt);
transform.windowToViewport(pt);
zoom_startx_pixel = pt.getX();
zoom_starty_pixel = pt.getY();
}
else {
zoom_startx_real = startWindow[0] + startx * ((startWindow[1] - startWindow[0]) / width);
zoom_starty_real = startWindow[2] + (height-starty) * ((startWindow[3] - startWindow[2]) / height);
zoom_startx_pixel = startx;
zoom_starty_pixel = starty;
}
}
else if (newoperation == ROTATE) {
if (isStereographView) {
virtualSphereRadius = 0.48 * Math.min( Math.abs(stereographTransform.getXmax() - stereographTransform.getXmin()),
Math.abs(stereographTransform.getYmax() - stereographTransform.getYmin()) );
}
else {
virtualSphereRadius = 0.48 * Math.min( Math.abs(transform.getXmax() - transform.getXmin()),
Math.abs(transform.getYmax() - transform.getYmin()) );
}
}
return;
}
int currentX = evt.getX();
int currentY = evt.getY();
if (isStereographView && operation != DRAG) {
if (inLeftEyeRect) {
currentX -= stereographLeftEyeRect.x;
currentY -= stereographLeftEyeRect.y;
}
else {
currentX -= stereographRightEyeRect.x;
currentY -= stereographRightEyeRect.y;
}
}
switch (operation) {
case DRAG:
if (isStereographView) {
int offset = evt.getX() - prevx;
if (offset != 0)
((View3D)view).moveStereographImages(offset);
prevx = evt.getX();
}
else {
double offsetX, offsetY;
double pixelWidth = Math.abs(startWindow[1] - startWindow[0])/width;
double pixelHeight = Math.abs(startWindow[3] - startWindow[2])/height;
if ( isStereographView ) {
double pixelWidthS = Math.abs(stereographTransform.getXmax() - stereographTransform.getXmin()) / stereographLeftEyeRect.width;
double pixelHeightS = Math.abs(stereographTransform.getYmax() - stereographTransform.getYmin()) / stereographLeftEyeRect.height;
offsetX = (startx - currentX) * pixelWidthS;
offsetY = -(starty - currentY) * pixelHeightS;
}
else {
offsetX = (startx - currentX) * pixelWidth;
offsetY = -(starty - currentY) * pixelHeight;
}
newWindow(view,startWindow[0]+offsetX,startWindow[1]+offsetX,
startWindow[2]+offsetY,startWindow[3]+offsetY);
view.forceRedraw();
}
break;
case ZOOM:
if (isStereographView) {
width = stereographTransform.getWidth();
height = stereographTransform.getHeight();
}
double diff = -(starty - currentY) / 200.0;
double mag = Math.exp(diff); // factor of e every 200 pixels
double newwidth = (startWindow[1] - startWindow[0]) * mag;
double newheight = (startWindow[3] - startWindow[2]) * mag;
double newpixelwidth = newwidth / width;
double newpixelheight = newheight / height;
double newxmin = zoom_startx_real - newpixelwidth*zoom_startx_pixel;
double newxmax = newxmin + newwidth;
double newymin = zoom_starty_real - newpixelheight*(height-zoom_starty_pixel);
double newymax = newymin + newheight;
newWindow(view,newxmin,newxmax,newymin,newymax);
view.forceRedraw();
break;
case ROTATE:
if (prevx == currentX && prevy == currentY)
break;
previousRay = mousePointToRay(prevx,prevy);
currentRay = mousePointToRay(currentX,currentY);
transform.applyTransvection(previousRay,currentRay);
prevx = currentX;
prevy = currentY;
view.forceRedraw();
lastDragTime = evt.getWhen();
break;
}
}
/**
* Finish an operation that was begun in the doMouseDown method. If the operation
* is ROTATE and the user was still moving the mouse when the mouse button was released, then
* an animation is installed in the Display that will continue the rotation indefinitely.
*/
public void doMouseUp(MouseEvent evt, final Display display, final View view, int width, int height) {
if (!startedDragging || operation == NONE)
return;
boolean keepGoing = false;
if (operation == ROTATE) {
display.repaint();
final boolean fast = saveFastDrawing;
final Transform3D tr = transform;
final Vector3D ray1 = previousRay;
final Vector3D ray2 = currentRay;
if (evt.getWhen() - lastDragTime < 200 && currentRay != null
&& currentRay.minus(previousRay).norm() >= 0.002) {
keepGoing = true;
display.installAnimation( new TimerAnimation(0,50) {
protected void drawFrame() {
tr.applyTransvection(ray1,ray2);
view.forceRedraw();
}
protected void animationEnding() {
view.setFastDrawing(fast);
}
});
}
}
if (!keepGoing)
view.setFastDrawing(saveFastDrawing);
transform = null;
previousRay = currentRay = null;
stereographLeftEyeRect = null;
stereographRightEyeRect = null;
stereographTransform = null;
operation = NONE;
}
/**
* Returns the cursor that the Display should use during a drag operation.
* The cursor that is returned depends on which operation is being performed
* with the mouse.
* TODO: Use a better cursor for ZOOMing.
*/
public Cursor getCursorForDragging(MouseEvent mouseDownEvent, Display display, View view) {
if (operation == DRAG)
return Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
else if (operation == ZOOM)
return Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR);
else
return null;
}
/**
* Converts the mouse point (x,y) to a point on the unit sphere.
*/
private Vector3D mousePointToRay(int x, int y) {
Transform3D theTransform = isStereographView? stereographTransform : transform;
Point2D pt = new Point2D.Double(x,y);
theTransform.viewportToWindow(pt);
Vector3D xdir = theTransform.getImagePlaneXDirection();
Vector3D ydir = theTransform.getImagePlaneYDirection();
double vx = pt.getX() * xdir.x + pt.getY() * ydir.x; // The mouse point as a vector in the image plane.
double vy = pt.getX() * xdir.y + pt.getY() * ydir.y;
double vz = pt.getX() * xdir.z + pt.getY() * ydir.z;
double normSquared = vx*vx + vy*vy + vz*vz;
Vector3D answer;
if (normSquared > virtualSphereRadius*virtualSphereRadius) { // A point lying in the image plane, on the "equator" of the virtual sphere
double len = Math.sqrt(normSquared);
answer = new Vector3D(vx*virtualSphereRadius/len, vy*virtualSphereRadius/len, vz*virtualSphereRadius/len);
}
else { // Add a z-coordinate to (vx,vy,vz) bring the length up to the virtualSphereRadius
double z = Math.sqrt(virtualSphereRadius*virtualSphereRadius - normSquared);
// in view coordinates; vector we want is -z*ViewDirection
Vector3D zdir = theTransform.getViewDirection();
answer = new Vector3D(vx - z*zdir.x, vy - z*zdir.y, vz -z*zdir.z);
}
answer.normalize();
return answer;
}
/**
* Used to set a new window for the View that has the same requested aspect ratio
* as the current one, and has the specified x and y ranges as the actual ranges
* on the window.
*/
private void newWindow(View view,double xmin, double xmax, double ymin, double ymax) {
double aspect = Math.abs( (ymax - ymin) / (xmax - xmin) );
if (aspect > requestedAspectRatio) { // shrink y range
double shrinkFactor = requestedAspectRatio / aspect;
double newHeight = (ymax - ymin) * shrinkFactor;
double middle = (ymax + ymin) / 2;
ymin = middle - newHeight / 2;
ymax = middle + newHeight / 2;
}
else { // shrink x range
double shrinkFactor = aspect / requestedAspectRatio;
double newWidth = (xmax - xmin) * shrinkFactor;
double middle = (xmax + xmin) / 2;
xmin = middle - newWidth / 2;
xmax = middle + newWidth / 2;
}
view.setWindow(xmin,xmax,ymin,ymax);
}
}