/* * Created on Jun 30, 2003 */ package com.vzome.desktop.controller; import java.awt.event.ActionEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import javax.vecmath.Matrix4d; import javax.vecmath.Point3d; import javax.vecmath.Quat4d; import javax.vecmath.Vector3d; import javax.vecmath.Vector3f; import org.vorthmann.j3d.MouseTool; import org.vorthmann.j3d.MouseToolDefault; import org.vorthmann.j3d.Trackball; import org.vorthmann.ui.DefaultController; import com.vzome.core.viewing.Camera; /** * In this view model, the frustum shape is generally held constant * as other parameters are varied. */ public class CameraController extends DefaultController { /** * The original frustum. */ public static final double ORIG_WIDTH = 18f, ORIG_DISTANCE = 40f; public static final double DEFAULT_STEREO_ANGLE = Math .PI * 5d / 360d; protected static final Vector3f ORIG_LOOK = new Vector3f(0,0,-1); protected static final Vector3f ORIG_UP = new Vector3f(0,1,0); private Camera model; private Camera copied = null; protected final List<CameraController.Viewer> mViewers = new ArrayList<>(); private final Camera initialCamera; public interface Snapper { void snapDirections( Vector3f lookDir, Vector3f upDir ); } public static interface Viewer { int MONOCULAR = 0; int LEFT_EYE = 1; int RIGHT_EYE = 2; void setEye( int eye ); void setViewTransformation( Matrix4d trans, int eye ); void setPerspective( double fov, double aspectRatio, double near, double far ); void setOrthographic( double halfEdge, double near, double far ); } /** * The width of the frustum at the look-at point is held * constant, as well as the other dimensions of the frustum. * @param value */ public void setPerspective( boolean value ) { model .setPerspective( value ); updateViewersTransformation(); updateViewersProjection(); } public void getViewOrientation( Vector3d lookDir, Vector3d upDir ) { model .getViewOrientation( lookDir, upDir ); } public void addViewer( CameraController.Viewer viewer ) { mViewers .add( viewer ); } public void removeViewer( CameraController.Viewer viewer ) { mViewers .remove( viewer ); } public CameraController( Camera init ) { model = init; initialCamera = new Camera( model ); } public void updateViewers() { updateViewersTransformation(); updateViewersProjection(); } // TODO get rid of this public Camera getView() { return new Camera( model ); } public Camera restoreView( Camera view ) { if ( view == null ) return model; boolean wasPerspective = model .isPerspective(); boolean wasStereo = model .isStereo(); float oldMag = model .getMagnification(); model = new Camera( view ); updateViewersTransformation(); updateViewersProjection(); if ( wasPerspective != model .isPerspective() ) properties() .firePropertyChange( "perspective", wasPerspective, model .isPerspective() ); if ( wasStereo != model .isStereo() ) properties() .firePropertyChange( "stereo", wasStereo, model .isStereo() ); if ( oldMag != model .getMagnification() ) properties() .firePropertyChange( "magnification", Float .toString( oldMag ), Float .toString( model .getMagnification() ) ); return model; } private void updateViewersTransformation() { if ( mViewers .size() == 0 ) return; Matrix4d trans = new Matrix4d(); model .getViewTransform( trans, 0d ); trans .invert(); for ( int i = 0; i < mViewers .size(); i++ ) mViewers .get( i ) .setViewTransformation( trans, Viewer .MONOCULAR ); model .getStereoViewTransform( trans, Viewer .LEFT_EYE ); trans .invert(); for ( int i = 0; i < mViewers .size(); i++ ) mViewers .get( i ) .setViewTransformation( trans, Viewer .LEFT_EYE ); model .getStereoViewTransform( trans, Viewer .RIGHT_EYE ); trans .invert(); for ( int i = 0; i < mViewers .size(); i++ ) mViewers .get( i ) .setViewTransformation( trans, Viewer .RIGHT_EYE ); } private void updateViewersProjection() { if ( mViewers .size() == 0 ) return; double near = model .getNearClipDistance(); double far = model .getFarClipDistance(); if ( ! model .isPerspective() ) { double edge = model .getWidth() / 2; for ( int i = 0; i < mViewers .size(); i++ ) mViewers .get( i ) .setOrthographic( edge, near, far ); } else { double field = model .getFieldOfView(); for ( int i = 0; i < mViewers .size(); i++ ) mViewers .get( i ) .setPerspective( field, 1.0d, near, far ); } // TODO - make aspect ratio track the screen window shape } public void getWorldRotation( Quat4d q ) { Vector3d axis = new Vector3d( q.x, q.y, q.z ); Matrix4d viewTrans = new Matrix4d(); model .getViewTransform( viewTrans, 0d ); viewTrans .invert(); // now map the axis back to world coordinates viewTrans .transform( axis ); q.x = axis.x; q.y = axis.y; q.z = axis.z; } public void mapViewToWorld( Vector3f vector ) { Matrix4d viewTrans = new Matrix4d(); model .getViewTransform( viewTrans, 0d ); viewTrans .invert(); viewTrans .transform( vector ); } public void setViewDirection( Vector3f lookDir ) { model .setViewDirection( lookDir ); updateViewersTransformation(); } public void setViewDirection( Vector3f lookDir, Vector3f upDir ) { model .setViewDirection( lookDir, upDir ); updateViewersTransformation(); } public void setLookAtPoint( Point3d lookAt ) { model .setLookAtPoint( lookAt ); updateViewersTransformation(); } public void addViewpointRotation( Quat4d rotation ) { model .addViewpointRotation( rotation ); updateViewersTransformation(); } /** * All view parameters will scale with distance, to keep the frustum * shape fixed. * @param distance */ public void setMagnification( float exp ) { model .setMagnification( exp ); // have to adjust the projection, since the clipping distances // adjust with distance updateViewersTransformation(); updateViewersProjection(); } private final LinkedList<Camera> recentViews = new LinkedList<>(); private Camera baselineView = model; // invariant: baselineView .equals( mParameters) whenever the view // is "at rest" (not rolling or zooming), AND baselineView equals the latest recentView private final static int MAX_RECENT = 20; private int currentRecentView = 0; private boolean saveBaselineView() { if ( model .equals( baselineView ) ) return false; baselineView = new Camera( model ); recentViews .add( baselineView ); if ( recentViews .size() > MAX_RECENT ) recentViews .removeFirst(); currentRecentView = recentViews .size(); return true; } private static final Vector3f Z = new Vector3f( 0f, 0f, -1f ), Y = new Vector3f( 0f, 1f, 0f ); private Snapper mSnapper = null; private boolean mSnapping = false; public void setSnapper( Snapper snapper ) { mSnapper = snapper; if ( mSnapping ) { saveBaselineView(); // might have been zooming snapView(); saveBaselineView(); } } public boolean isSnapping() { return mSnapping; } public void snapView() { Z .set( 0f, 0f, -1f ); mapViewToWorld( Z ); Y .set( 0f, 1f, 0f ); mapViewToWorld( Y ); mSnapper .snapDirections( Z, Y ); setViewDirection( Z, Y ); } @Override public void doAction( String action, ActionEvent e ) throws Exception { if ( action .equals( "toggleSnap" ) ) { mSnapping = !mSnapping; if ( mSnapping ) { saveBaselineView(); // might have been zooming snapView(); saveBaselineView(); } } else if ( action .equals( "toggleStereo" ) ) { boolean wasStereo = model .isStereo(); if ( ! wasStereo ) model .setStereoAngle( CameraController.DEFAULT_STEREO_ANGLE ); else model .setStereoAngle( 0d ); updateViewersTransformation(); updateViewersProjection(); properties() .firePropertyChange( "stereo", wasStereo, !wasStereo ); } else if ( action .equals( "togglePerspective" ) ) { saveBaselineView(); // might have been zooming model .setPerspective( ! model .isPerspective() ); updateViewersTransformation(); updateViewersProjection(); saveBaselineView(); } else if ( action .equals( "goForward" ) ) { if ( currentRecentView >= recentViews .size() ) return; restoreView( recentViews .get( ++currentRecentView ) ); } else if ( action .equals( "goBack" ) ) { if ( currentRecentView == 0 ) return; boolean wasZooming = saveBaselineView(); // might have been zooming if ( ( currentRecentView == recentViews .size() ) && wasZooming ) // we're not browsing recent views --currentRecentView; // skip over the view we just saved restoreView( recentViews .get( --currentRecentView ) ); } else if ( action .equals( "initialView" ) ) { saveBaselineView(); // might have been zooming restoreView( this .initialCamera ); // bookmarked views are not "special"... they are stored in recent, too saveBaselineView(); } } public MouseTool getTrackball() { return new Trackball() { @Override public void mousePressed( MouseEvent e ) { saveBaselineView(); // might have been zooming super .mousePressed( e ); } @Override public void trackballRolled( Quat4d roll ) { Quat4d copy = new Quat4d( roll ); getWorldRotation( copy ); addViewpointRotation( copy ); // TODO give will-snap feedback when drag paused } @Override public void mouseReleased( MouseEvent e ) { if ( mSnapping ) snapView(); saveBaselineView(); } }; } // ticks <-> mag mapping is duplicated in ViewPlatformControlPanel... should live here only private static final float MAG_PER_TICKS = -50f; // MAX_MAG = 4f, MIN_MAG = -2f; private static int magToTicks( float magnification ) { return Math.round( MAG_PER_TICKS * ( magnification - 1f ) ); } private static float ticksToMag( int ticks ) { return ( ticks / MAG_PER_TICKS ) + 1f; } public MouseTool getZoomScroller() { return new MouseToolDefault() { @Override public void mouseWheelMoved( MouseWheelEvent e ) { int amt = e .getWheelRotation(); float oldMag = model .getMagnification(); int ticks = magToTicks( oldMag ); ticks -= amt; float newMag = ticksToMag( ticks ); setMagnification( newMag ); properties() .firePropertyChange( "magnification", Float .toString( oldMag ), Float .toString( newMag ) ); } }; } @Override public String getProperty( String propName ) { if ( "magnification" .equals( propName ) ) return Float .toString( model .getMagnification() ); if ( "perspective" .equals( propName ) ) return Boolean .toString( model .isPerspective() ); if ( "snap" .equals( propName ) ) return Boolean .toString( isSnapping() ); if ( "stereo" .equals( propName ) ) return Boolean .toString( model .isStereo() ); return super .getProperty( propName ); } private final static long ZOOM_PAUSE = 3000; // three seconds private long lastZoom = 0; @Override public void setProperty( String propName, Object value ) { if ( "magnification" .equals( propName ) ) { long now = System .currentTimeMillis(); if ( now - lastZoom > ZOOM_PAUSE ) { // it has been a while... save the last view saveBaselineView(); } setMagnification( Float .parseFloat( (String) value ) ); lastZoom = now; } } public void copyView( Camera newView ) { this .copied = newView; } public void useCopiedView() { this .restoreView( this .copied ); } public boolean hasCopiedView() { return this .copied != null; } }